| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent ab162c5 commit a529834
7 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -11,7 +11,7 @@ jobs: | |||
| 11 | 11 | runs-on: ubuntu-latest | |
| 12 | 12 | strategy: | |
| 13 | 13 | matrix: | |
| 14 | - python-version: ['3.10', '3.11'] | ||
| 14 | + python-version: ['3.10', '3.11', '3.12'] | ||
| 15 | 15 | ||
| 16 | 16 | steps: | |
| 17 | 17 | - name: Checkout | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,7 +1,11 @@ | |||
| 1 | 1 | from typing import Any | |
| 2 | 2 | ||
| 3 | + import emoji | ||
| 4 | + | ||
| 3 | 5 | import pythonnative as pn | |
| 4 | 6 | ||
| 7 | + MEDALS = [":1st_place_medal:", ":2nd_place_medal:", ":3rd_place_medal:"] | ||
| 8 | + | ||
| 5 | 9 | ||
| 6 | 10 | class MainPage(pn.Page): | |
| 7 | 11 | def __init__(self, native_instance: Any) -> None: | |
@@ -12,10 +16,13 @@ def increment(self) -> None: | |||
| 12 | 16 | self.set_state(count=self.state["count"] + 1) | |
| 13 | 17 | ||
| 14 | 18 | def render(self) -> pn.Element: | |
| 19 | + count = self.state["count"] | ||
| 20 | + medal = emoji.emojize(MEDALS[count] if count < len(MEDALS) else ":star:") | ||
| 15 | 21 | return pn.ScrollView( | |
| 16 | 22 | pn.Column( | |
| 17 | 23 | pn.Text("Hello from PythonNative Demo!", font_size=24, bold=True), | |
| 18 | - pn.Text(f"Tapped {self.state['count']} times", font_size=16), | ||
| 24 | + pn.Text(f"Tapped {count} times", font_size=16), | ||
| 25 | + pn.Text(medal, font_size=32), | ||
| 19 | 26 | pn.Button("Tap me", on_click=self.increment, background_color="#FF1E88E5"), | |
| 20 | 27 | pn.Button( | |
| 21 | 28 | "Go to Second Page", | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,6 +2,7 @@ | |||
| 2 | 2 | "name": "PythonNative Demo", | |
| 3 | 3 | "appId": "com.pythonnative.demo", | |
| 4 | 4 | "entryPoint": "app/main_page.py", | |
| 5 | + "pythonVersion": "3.11", | ||
| 5 | 6 | "ios": {}, | |
| 6 | 7 | "android": {} | |
| 7 | 8 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1 +1 @@ | |||
| 1 | - pythonnative | ||
| 1 | + emoji | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,6 +2,7 @@ | |||
| 2 | 2 | import hashlib | |
| 3 | 3 | import json | |
| 4 | 4 | import os | |
| 5 | + import re | ||
| 5 | 6 | import shutil | |
| 6 | 7 | import subprocess | |
| 7 | 8 | import sys | |
@@ -73,16 +74,17 @@ def render(self): | |||
| 73 | 74 | "name": project_name, | |
| 74 | 75 | "appId": "com.example." + project_name.replace(" ", "").lower(), | |
| 75 | 76 | "entryPoint": "app/main_page.py", | |
| 77 | + "pythonVersion": "3.11", | ||
| 76 | 78 | "ios": {}, | |
| 77 | 79 | "android": {}, | |
| 78 | 80 | } | |
| 79 | 81 | with open(config_path, "w", encoding="utf-8") as f: | |
| 80 | 82 | json.dump(config, f, indent=2) | |
| 81 | 83 | ||
| 82 | - # Requirements | ||
| 84 | + # Requirements (third-party packages only; pythonnative itself is bundled by the CLI) | ||
| 83 | 85 | if not os.path.exists(requirements_path) or args.force: | |
| 84 | 86 | with open(requirements_path, "w", encoding="utf-8") as f: | |
| 85 | - f.write("pythonnative\n") | ||
| 87 | + f.write("") | ||
| 86 | 88 | ||
| 87 | 89 | # .gitignore | |
| 88 | 90 | default_gitignore = "# PythonNative\n" "__pycache__/\n" "*.pyc\n" ".venv/\n" "build/\n" ".DS_Store\n" | |
@@ -215,6 +217,43 @@ def create_ios_project(project_name: str, destination: str) -> None: | |||
| 215 | 217 | _copy_bundled_template_dir("ios_template", destination) | |
| 216 | 218 | ||
| 217 | 219 | ||
| 220 | + def _read_project_config() -> dict: | ||
| 221 | + """Read pythonnative.json from the current working directory.""" | ||
| 222 | + config_path = os.path.join(os.getcwd(), "pythonnative.json") | ||
| 223 | + if os.path.exists(config_path): | ||
| 224 | + with open(config_path, encoding="utf-8") as f: | ||
| 225 | + return json.load(f) | ||
| 226 | + return {} | ||
| 227 | + | ||
| 228 | + | ||
| 229 | + def _read_requirements(requirements_path: str) -> list[str]: | ||
| 230 | + """Read a requirements file and return non-empty, non-comment lines. | ||
| 231 | + | ||
| 232 | + Exits with an error if pythonnative is listed — the CLI bundles it | ||
| 233 | + directly, so it must not be installed separately via pip/Chaquopy. | ||
| 234 | + """ | ||
| 235 | + if not os.path.exists(requirements_path): | ||
| 236 | + return [] | ||
| 237 | + with open(requirements_path, encoding="utf-8") as f: | ||
| 238 | + lines = f.readlines() | ||
| 239 | + result: list[str] = [] | ||
| 240 | + for line in lines: | ||
| 241 | + stripped = line.strip() | ||
| 242 | + if not stripped or stripped.startswith("#") or stripped.startswith("-"): | ||
| 243 | + continue | ||
| 244 | + pkg_name = re.split(r"[\[><=!;]", stripped)[0].strip() | ||
| 245 | + if pkg_name.lower().replace("-", "_") == "pythonnative": | ||
| 246 | + print( | ||
| 247 | + "Error: 'pythonnative' must not be in requirements.txt.\n" | ||
| 248 | + "The pn CLI automatically bundles the installed pythonnative into your app.\n" | ||
| 249 | + "requirements.txt is for third-party packages only (e.g. humanize, requests).\n" | ||
| 250 | + "Remove the pythonnative line from requirements.txt and try again." | ||
| 251 | + ) | ||
| 252 | + sys.exit(1) | ||
| 253 | + result.append(stripped) | ||
| 254 | + return result | ||
| 255 | + | ||
| 256 | + | ||
| 218 | 257 | def run_project(args: argparse.Namespace) -> None: | |
| 219 | 258 | """ | |
| 220 | 259 | Run the specified project. | |
@@ -223,8 +262,13 @@ def run_project(args: argparse.Namespace) -> None: | |||
| 223 | 262 | platform: str = args.platform | |
| 224 | 263 | prepare_only: bool = getattr(args, "prepare_only", False) | |
| 225 | 264 | ||
| 265 | + # Read project configuration and save project root before any chdir | ||
| 266 | + project_dir: str = os.getcwd() | ||
| 267 | + config = _read_project_config() | ||
| 268 | + python_version: str = config.get("pythonVersion", "3.11") | ||
| 269 | + | ||
| 226 | 270 | # Define the build directory | |
| 227 | - build_dir: str = os.path.join(os.getcwd(), "build", platform) | ||
| 271 | + build_dir: str = os.path.join(project_dir, "build", platform) | ||
| 228 | 272 | ||
| 229 | 273 | # Create the build directory if it doesn't exist | |
| 230 | 274 | os.makedirs(build_dir, exist_ok=True) | |
@@ -268,10 +312,30 @@ def run_project(args: argparse.Namespace) -> None: | |||
| 268 | 312 | # Non-fatal; fallback to the packaged PyPI dependency if present | |
| 269 | 313 | pass | |
| 270 | 314 | ||
| 271 | - # Install any necessary Python packages into the project environment | ||
| 315 | + # Validate and read the user's requirements.txt | ||
| 316 | + requirements_path = os.path.join(project_dir, "requirements.txt") | ||
| 317 | + pip_reqs = _read_requirements(requirements_path) | ||
| 318 | + | ||
| 319 | + if platform == "android": | ||
| 320 | + # Patch the Android build.gradle with the configured Python version | ||
| 321 | + app_build_gradle = os.path.join(build_dir, "android_template", "app", "build.gradle") | ||
| 322 | + if os.path.exists(app_build_gradle): | ||
| 323 | + with open(app_build_gradle, encoding="utf-8") as f: | ||
| 324 | + content = f.read() | ||
| 325 | + content = content.replace('version "3.11"', f'version "{python_version}"') | ||
| 326 | + with open(app_build_gradle, "w", encoding="utf-8") as f: | ||
| 327 | + f.write(content) | ||
| 328 | + # Copy requirements.txt into the Android project for Chaquopy | ||
| 329 | + android_reqs_path = os.path.join(build_dir, "android_template", "app", "requirements.txt") | ||
| 330 | + if os.path.exists(requirements_path): | ||
| 331 | + shutil.copy2(requirements_path, android_reqs_path) | ||
| 332 | + else: | ||
| 333 | + with open(android_reqs_path, "w", encoding="utf-8") as f: | ||
| 334 | + f.write("") | ||
| 335 | + | ||
| 336 | + # Install any necessary Python packages into the host environment | ||
| 272 | 337 | # Skip installation during prepare-only to avoid network access and speed up scaffolding | |
| 273 | 338 | if not prepare_only: | |
| 274 | - requirements_path = os.path.join(os.getcwd(), "requirements.txt") | ||
| 275 | 339 | if os.path.exists(requirements_path): | |
| 276 | 340 | subprocess.run([sys.executable, "-m", "pip", "install", "-r", requirements_path], check=False) | |
| 277 | 341 | ||
@@ -523,6 +587,29 @@ def run_project(args: argparse.Namespace) -> None: | |||
| 523 | 587 | except Exception: | |
| 524 | 588 | # Non-fatal; if metadata isn't present, rubicon import may fail and fallback UI will appear | |
| 525 | 589 | pass | |
| 590 | + # Install user's pip requirements (pure-Python packages) into the app bundle | ||
| 591 | + if pip_reqs: | ||
| 592 | + try: | ||
| 593 | + reqs_tmp = os.path.join(build_dir, "ios_requirements.txt") | ||
| 594 | + with open(reqs_tmp, "w", encoding="utf-8") as f: | ||
| 595 | + f.write("\n".join(pip_reqs) + "\n") | ||
| 596 | + tmp_reqs_dir = os.path.join(build_dir, "ios_user_packages") | ||
| 597 | + if os.path.isdir(tmp_reqs_dir): | ||
| 598 | + shutil.rmtree(tmp_reqs_dir) | ||
| 599 | + os.makedirs(tmp_reqs_dir, exist_ok=True) | ||
| 600 | + subprocess.run( | ||
| 601 | + [sys.executable, "-m", "pip", "install", "-t", tmp_reqs_dir, "-r", reqs_tmp], | ||
| 602 | + check=False, | ||
| 603 | + ) | ||
| 604 | + for entry in os.listdir(tmp_reqs_dir): | ||
| 605 | + src_entry = os.path.join(tmp_reqs_dir, entry) | ||
| 606 | + dst_entry = os.path.join(platform_site_dir, entry) | ||
| 607 | + if os.path.isdir(src_entry): | ||
| 608 | + shutil.copytree(src_entry, dst_entry, dirs_exist_ok=True) | ||
| 609 | + else: | ||
| 610 | + shutil.copy2(src_entry, dst_entry) | ||
| 611 | + except Exception: | ||
| 612 | + pass | ||
| 526 | 613 | # Note: Python.xcframework provides a static library for Simulator; it must be linked at build time. | |
| 527 | 614 | # We copy the XCFramework into the project directory above so Xcode can link it. | |
| 528 | 615 | except Exception: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -20,14 +20,9 @@ android { | |||
| 20 | 20 | abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" | |
| 21 | 21 | } | |
| 22 | 22 | python { | |
| 23 | - version "3.8" | ||
| 23 | + version "3.11" | ||
| 24 | 24 | pip { | |
| 25 | - install "matplotlib" | ||
| 26 | - install "pythonnative" | ||
| 27 | - | ||
| 28 | - // "-r"` followed by a requirements filename, relative to the | ||
| 29 | - // project directory: | ||
| 30 | - // install "-r", "requirements.txt" | ||
| 25 | + install "-r", "requirements.txt" | ||
| 31 | 26 | } | |
| 32 | 27 | } | |
| 33 | 28 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3,5 +3,5 @@ plugins { | |||
| 3 | 3 | id 'com.android.application' version '8.2.2' apply false | |
| 4 | 4 | id 'com.android.library' version '8.2.2' apply false | |
| 5 | 5 | id 'org.jetbrains.kotlin.android' version '1.9.22' apply false | |
| 6 | - id 'com.chaquo.python' version '14.0.2' apply false | ||
| 6 | + id 'com.chaquo.python' version '15.0.1' apply false | ||
| 7 | 7 | } | |
| Back | FazBrowse Home | New Git URL |
0 commit comments