pythonnative 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pythonnative/__init__.py CHANGED
@@ -4,51 +4,99 @@ Public API::
4
4
 
5
5
  import pythonnative as pn
6
6
 
7
+ @pn.component
8
+ def counter(initial=0):
9
+ count, set_count = pn.use_state(initial)
10
+ return pn.Column(
11
+ pn.Text(f"Count: {count}", font_size=24),
12
+ pn.Button("+", on_click=lambda: set_count(count + 1)),
13
+ spacing=12,
14
+ )
15
+
7
16
  class MainPage(pn.Page):
8
17
  def __init__(self, native_instance):
9
18
  super().__init__(native_instance)
10
- self.state = {"count": 0}
11
19
 
12
20
  def render(self):
13
21
  return pn.Column(
14
- pn.Text(f"Count: {self.state['count']}", font_size=24),
15
- pn.Button("Increment", on_click=lambda: self.set_state(count=self.state["count"] + 1)),
16
- spacing=12,
22
+ counter(initial=0),
23
+ counter(initial=10),
24
+ spacing=16,
25
+ padding=16,
17
26
  )
18
27
  """
19
28
 
20
- __version__ = "0.5.0"
29
+ __version__ = "0.6.0"
21
30
 
22
31
  from .components import (
23
32
  ActivityIndicator,
24
33
  Button,
25
34
  Column,
35
+ FlatList,
26
36
  Image,
37
+ Modal,
38
+ Pressable,
27
39
  ProgressBar,
28
40
  Row,
41
+ SafeAreaView,
29
42
  ScrollView,
43
+ Slider,
30
44
  Spacer,
31
45
  Switch,
32
46
  Text,
33
47
  TextInput,
48
+ View,
34
49
  WebView,
35
50
  )
36
51
  from .element import Element
52
+ from .hooks import (
53
+ Provider,
54
+ component,
55
+ create_context,
56
+ use_callback,
57
+ use_context,
58
+ use_effect,
59
+ use_memo,
60
+ use_ref,
61
+ use_state,
62
+ )
37
63
  from .page import Page
64
+ from .style import StyleSheet, ThemeContext
38
65
 
39
66
  __all__ = [
67
+ # Components
40
68
  "ActivityIndicator",
41
69
  "Button",
42
70
  "Column",
43
- "Element",
71
+ "FlatList",
44
72
  "Image",
45
- "Page",
73
+ "Modal",
74
+ "Pressable",
46
75
  "ProgressBar",
47
76
  "Row",
77
+ "SafeAreaView",
48
78
  "ScrollView",
79
+ "Slider",
49
80
  "Spacer",
50
81
  "Switch",
51
82
  "Text",
52
83
  "TextInput",
84
+ "View",
53
85
  "WebView",
86
+ # Core
87
+ "Element",
88
+ "Page",
89
+ # Hooks
90
+ "component",
91
+ "create_context",
92
+ "use_callback",
93
+ "use_context",
94
+ "use_effect",
95
+ "use_memo",
96
+ "use_ref",
97
+ "use_state",
98
+ "Provider",
99
+ # Styling
100
+ "StyleSheet",
101
+ "ThemeContext",
54
102
  ]
pythonnative/cli/pn.py CHANGED
@@ -2,6 +2,7 @@ import argparse
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
@@ -44,8 +45,7 @@ def init_project(args: argparse.Namespace) -> None:
44
45
  main_page_py = os.path.join(app_dir, "main_page.py")
45
46
  if not os.path.exists(main_page_py) or args.force:
46
47
  with open(main_page_py, "w", encoding="utf-8") as f:
47
- f.write(
48
- """import pythonnative as pn
48
+ f.write("""import pythonnative as pn
49
49
 
50
50
 
51
51
  class MainPage(pn.Page):
@@ -67,24 +67,24 @@ class MainPage(pn.Page):
67
67
  alignment="fill",
68
68
  )
69
69
  )
70
- """
71
- )
70
+ """)
72
71
 
73
72
  # Create config
74
73
  config = {
75
74
  "name": project_name,
76
75
  "appId": "com.example." + project_name.replace(" ", "").lower(),
77
76
  "entryPoint": "app/main_page.py",
77
+ "pythonVersion": "3.11",
78
78
  "ios": {},
79
79
  "android": {},
80
80
  }
81
81
  with open(config_path, "w", encoding="utf-8") as f:
82
82
  json.dump(config, f, indent=2)
83
83
 
84
- # Requirements
84
+ # Requirements (third-party packages only; pythonnative itself is bundled by the CLI)
85
85
  if not os.path.exists(requirements_path) or args.force:
86
86
  with open(requirements_path, "w", encoding="utf-8") as f:
87
- f.write("pythonnative\n")
87
+ f.write("")
88
88
 
89
89
  # .gitignore
90
90
  default_gitignore = "# PythonNative\n" "__pycache__/\n" "*.pyc\n" ".venv/\n" "build/\n" ".DS_Store\n"
@@ -158,7 +158,11 @@ def _copy_bundled_template_dir(template_dir: str, destination: str) -> None:
158
158
 
159
159
 
160
160
  def _github_json(url: str) -> Any:
161
- req = urllib.request.Request(url, headers={"User-Agent": "pythonnative-cli"})
161
+ headers: dict[str, str] = {"User-Agent": "pythonnative-cli"}
162
+ token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
163
+ if token:
164
+ headers["Authorization"] = f"Bearer {token}"
165
+ req = urllib.request.Request(url, headers=headers)
162
166
  with urllib.request.urlopen(req) as r:
163
167
  return json.loads(r.read().decode("utf-8"))
164
168
 
@@ -213,6 +217,43 @@ def create_ios_project(project_name: str, destination: str) -> None:
213
217
  _copy_bundled_template_dir("ios_template", destination)
214
218
 
215
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
+
216
257
  def run_project(args: argparse.Namespace) -> None:
217
258
  """
218
259
  Run the specified project.
@@ -220,9 +261,15 @@ def run_project(args: argparse.Namespace) -> None:
220
261
  # Determine the platform
221
262
  platform: str = args.platform
222
263
  prepare_only: bool = getattr(args, "prepare_only", False)
264
+ hot_reload: bool = getattr(args, "hot_reload", False)
265
+
266
+ # Read project configuration and save project root before any chdir
267
+ project_dir: str = os.getcwd()
268
+ config = _read_project_config()
269
+ python_version: str = config.get("pythonVersion", "3.11")
223
270
 
224
271
  # Define the build directory
225
- build_dir: str = os.path.join(os.getcwd(), "build", platform)
272
+ build_dir: str = os.path.join(project_dir, "build", platform)
226
273
 
227
274
  # Create the build directory if it doesn't exist
228
275
  os.makedirs(build_dir, exist_ok=True)
@@ -266,10 +313,30 @@ def run_project(args: argparse.Namespace) -> None:
266
313
  # Non-fatal; fallback to the packaged PyPI dependency if present
267
314
  pass
268
315
 
269
- # Install any necessary Python packages into the project environment
316
+ # Validate and read the user's requirements.txt
317
+ requirements_path = os.path.join(project_dir, "requirements.txt")
318
+ pip_reqs = _read_requirements(requirements_path)
319
+
320
+ if platform == "android":
321
+ # Patch the Android build.gradle with the configured Python version
322
+ app_build_gradle = os.path.join(build_dir, "android_template", "app", "build.gradle")
323
+ if os.path.exists(app_build_gradle):
324
+ with open(app_build_gradle, encoding="utf-8") as f:
325
+ content = f.read()
326
+ content = content.replace('version "3.11"', f'version "{python_version}"')
327
+ with open(app_build_gradle, "w", encoding="utf-8") as f:
328
+ f.write(content)
329
+ # Copy requirements.txt into the Android project for Chaquopy
330
+ android_reqs_path = os.path.join(build_dir, "android_template", "app", "requirements.txt")
331
+ if os.path.exists(requirements_path):
332
+ shutil.copy2(requirements_path, android_reqs_path)
333
+ else:
334
+ with open(android_reqs_path, "w", encoding="utf-8") as f:
335
+ f.write("")
336
+
337
+ # Install any necessary Python packages into the host environment
270
338
  # Skip installation during prepare-only to avoid network access and speed up scaffolding
271
339
  if not prepare_only:
272
- requirements_path = os.path.join(os.getcwd(), "requirements.txt")
273
340
  if os.path.exists(requirements_path):
274
341
  subprocess.run([sys.executable, "-m", "pip", "install", "-r", requirements_path], check=False)
275
342
 
@@ -521,6 +588,29 @@ def run_project(args: argparse.Namespace) -> None:
521
588
  except Exception:
522
589
  # Non-fatal; if metadata isn't present, rubicon import may fail and fallback UI will appear
523
590
  pass
591
+ # Install user's pip requirements (pure-Python packages) into the app bundle
592
+ if pip_reqs:
593
+ try:
594
+ reqs_tmp = os.path.join(build_dir, "ios_requirements.txt")
595
+ with open(reqs_tmp, "w", encoding="utf-8") as f:
596
+ f.write("\n".join(pip_reqs) + "\n")
597
+ tmp_reqs_dir = os.path.join(build_dir, "ios_user_packages")
598
+ if os.path.isdir(tmp_reqs_dir):
599
+ shutil.rmtree(tmp_reqs_dir)
600
+ os.makedirs(tmp_reqs_dir, exist_ok=True)
601
+ subprocess.run(
602
+ [sys.executable, "-m", "pip", "install", "-t", tmp_reqs_dir, "-r", reqs_tmp],
603
+ check=False,
604
+ )
605
+ for entry in os.listdir(tmp_reqs_dir):
606
+ src_entry = os.path.join(tmp_reqs_dir, entry)
607
+ dst_entry = os.path.join(platform_site_dir, entry)
608
+ if os.path.isdir(src_entry):
609
+ shutil.copytree(src_entry, dst_entry, dirs_exist_ok=True)
610
+ else:
611
+ shutil.copy2(src_entry, dst_entry)
612
+ except Exception:
613
+ pass
524
614
  # Note: Python.xcframework provides a static library for Simulator; it must be linked at build time.
525
615
  # We copy the XCFramework into the project directory above so Xcode can link it.
526
616
  except Exception:
@@ -567,6 +657,39 @@ def run_project(args: argparse.Namespace) -> None:
567
657
  except Exception:
568
658
  print("Failed to auto-run on Simulator; open the project in Xcode to run.")
569
659
 
660
+ # Hot-reload file watcher
661
+ if hot_reload and not prepare_only:
662
+ _run_hot_reload(platform, project_dir, build_dir)
663
+
664
+
665
+ def _run_hot_reload(platform: str, project_dir: str, build_dir: str) -> None:
666
+ """Watch ``app/`` for changes and push updated files to the device."""
667
+ from .hot_reload import FileWatcher
668
+
669
+ app_dir = os.path.join(project_dir, "app")
670
+
671
+ def on_change(changed_files: List[str]) -> None:
672
+ for fpath in changed_files:
673
+ rel = os.path.relpath(fpath, project_dir)
674
+ print(f"[hot-reload] Changed: {rel}")
675
+ if platform == "android":
676
+ dest = f"/data/data/com.pythonnative.android_template/files/{rel}"
677
+ subprocess.run(["adb", "push", fpath, dest], check=False, capture_output=True)
678
+ elif platform == "ios":
679
+ pass # simctl file push would go here
680
+
681
+ print("[hot-reload] Watching app/ for changes. Press Ctrl+C to stop.")
682
+ watcher = FileWatcher(app_dir, on_change, interval=1.0)
683
+ watcher.start()
684
+ try:
685
+ import time
686
+
687
+ while True:
688
+ time.sleep(1)
689
+ except KeyboardInterrupt:
690
+ watcher.stop()
691
+ print("\n[hot-reload] Stopped.")
692
+
570
693
 
571
694
  def clean_project(args: argparse.Namespace) -> None:
572
695
  """
@@ -601,6 +724,11 @@ def main() -> None:
601
724
  action="store_true",
602
725
  help="Extract templates and stage app without building",
603
726
  )
727
+ parser_run.add_argument(
728
+ "--hot-reload",
729
+ action="store_true",
730
+ help="Watch app/ for changes and push updates to the running app",
731
+ )
604
732
  parser_run.set_defaults(func=run_project)
605
733
 
606
734
  # Create a new command 'clean' that calls clean_project