pythonnative 0.4.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.
Files changed (52) hide show
  1. pythonnative/__init__.py +94 -66
  2. pythonnative/cli/pn.py +153 -24
  3. pythonnative/components.py +563 -0
  4. pythonnative/element.py +53 -0
  5. pythonnative/hooks.py +287 -0
  6. pythonnative/hot_reload.py +143 -0
  7. pythonnative/native_modules/__init__.py +19 -0
  8. pythonnative/native_modules/camera.py +105 -0
  9. pythonnative/native_modules/file_system.py +131 -0
  10. pythonnative/native_modules/location.py +61 -0
  11. pythonnative/native_modules/notifications.py +151 -0
  12. pythonnative/native_views.py +1334 -0
  13. pythonnative/page.py +320 -247
  14. pythonnative/reconciler.py +262 -0
  15. pythonnative/style.py +115 -0
  16. pythonnative/templates/android_template/app/build.gradle +2 -7
  17. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -1
  18. pythonnative/templates/android_template/build.gradle +1 -1
  19. pythonnative/utils.py +21 -29
  20. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/METADATA +20 -19
  21. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/RECORD +25 -40
  22. pythonnative/activity_indicator_view.py +0 -71
  23. pythonnative/button.py +0 -113
  24. pythonnative/collection_view.py +0 -0
  25. pythonnative/date_picker.py +0 -76
  26. pythonnative/image_view.py +0 -78
  27. pythonnative/label.py +0 -133
  28. pythonnative/list_view.py +0 -76
  29. pythonnative/material_activity_indicator_view.py +0 -71
  30. pythonnative/material_bottom_navigation_view.py +0 -0
  31. pythonnative/material_button.py +0 -69
  32. pythonnative/material_date_picker.py +0 -87
  33. pythonnative/material_progress_view.py +0 -70
  34. pythonnative/material_search_bar.py +0 -69
  35. pythonnative/material_switch.py +0 -69
  36. pythonnative/material_time_picker.py +0 -76
  37. pythonnative/material_toolbar.py +0 -0
  38. pythonnative/picker_view.py +0 -69
  39. pythonnative/progress_view.py +0 -70
  40. pythonnative/scroll_view.py +0 -101
  41. pythonnative/search_bar.py +0 -69
  42. pythonnative/stack_view.py +0 -199
  43. pythonnative/switch.py +0 -68
  44. pythonnative/text_field.py +0 -132
  45. pythonnative/text_view.py +0 -135
  46. pythonnative/time_picker.py +0 -77
  47. pythonnative/view.py +0 -173
  48. pythonnative/web_view.py +0 -60
  49. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/WHEEL +0 -0
  50. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/entry_points.txt +0 -0
  51. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/licenses/LICENSE +0 -0
  52. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/top_level.txt +0 -0
pythonnative/__init__.py CHANGED
@@ -1,74 +1,102 @@
1
- from importlib import import_module
2
- from typing import Any, Dict
1
+ """PythonNative declarative native UI for Android and iOS.
3
2
 
4
- __version__ = "0.4.0"
3
+ Public API::
4
+
5
+ import pythonnative as pn
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
+
16
+ class MainPage(pn.Page):
17
+ def __init__(self, native_instance):
18
+ super().__init__(native_instance)
19
+
20
+ def render(self):
21
+ return pn.Column(
22
+ counter(initial=0),
23
+ counter(initial=10),
24
+ spacing=16,
25
+ padding=16,
26
+ )
27
+ """
28
+
29
+ __version__ = "0.6.0"
30
+
31
+ from .components import (
32
+ ActivityIndicator,
33
+ Button,
34
+ Column,
35
+ FlatList,
36
+ Image,
37
+ Modal,
38
+ Pressable,
39
+ ProgressBar,
40
+ Row,
41
+ SafeAreaView,
42
+ ScrollView,
43
+ Slider,
44
+ Spacer,
45
+ Switch,
46
+ Text,
47
+ TextInput,
48
+ View,
49
+ WebView,
50
+ )
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
+ )
63
+ from .page import Page
64
+ from .style import StyleSheet, ThemeContext
5
65
 
6
66
  __all__ = [
7
- "ActivityIndicatorView",
67
+ # Components
68
+ "ActivityIndicator",
8
69
  "Button",
9
- "DatePicker",
10
- "ImageView",
11
- "Label",
12
- "ListView",
13
- "MaterialActivityIndicatorView",
14
- "MaterialButton",
15
- "MaterialDatePicker",
16
- "MaterialProgressView",
17
- "MaterialSearchBar",
18
- "MaterialSwitch",
19
- "MaterialTimePicker",
20
- "MaterialBottomNavigationView",
21
- "MaterialToolbar",
22
- "Page",
23
- "PickerView",
24
- "ProgressView",
70
+ "Column",
71
+ "FlatList",
72
+ "Image",
73
+ "Modal",
74
+ "Pressable",
75
+ "ProgressBar",
76
+ "Row",
77
+ "SafeAreaView",
25
78
  "ScrollView",
26
- "SearchBar",
27
- "StackView",
79
+ "Slider",
80
+ "Spacer",
28
81
  "Switch",
29
- "TextField",
30
- "TextView",
31
- "TimePicker",
82
+ "Text",
83
+ "TextInput",
84
+ "View",
32
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",
33
102
  ]
34
-
35
- _NAME_TO_MODULE: Dict[str, str] = {
36
- "ActivityIndicatorView": ".activity_indicator_view",
37
- "Button": ".button",
38
- "DatePicker": ".date_picker",
39
- "ImageView": ".image_view",
40
- "Label": ".label",
41
- "ListView": ".list_view",
42
- "MaterialActivityIndicatorView": ".material_activity_indicator_view",
43
- "MaterialButton": ".material_button",
44
- "MaterialDatePicker": ".material_date_picker",
45
- "MaterialProgressView": ".material_progress_view",
46
- "MaterialSearchBar": ".material_search_bar",
47
- "MaterialSwitch": ".material_switch",
48
- "MaterialTimePicker": ".material_time_picker",
49
- "MaterialBottomNavigationView": ".material_bottom_navigation_view",
50
- "MaterialToolbar": ".material_toolbar",
51
- "Page": ".page",
52
- "PickerView": ".picker_view",
53
- "ProgressView": ".progress_view",
54
- "ScrollView": ".scroll_view",
55
- "SearchBar": ".search_bar",
56
- "StackView": ".stack_view",
57
- "Switch": ".switch",
58
- "TextField": ".text_field",
59
- "TextView": ".text_view",
60
- "TimePicker": ".time_picker",
61
- "WebView": ".web_view",
62
- }
63
-
64
-
65
- def __getattr__(name: str) -> Any:
66
- module_path = _NAME_TO_MODULE.get(name)
67
- if not module_path:
68
- raise AttributeError(f"module 'pythonnative' has no attribute {name!r}")
69
- module = import_module(module_path, package=__name__)
70
- return getattr(module, name)
71
-
72
-
73
- def __dir__() -> Any:
74
- return sorted(list(globals().keys()) + __all__)
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
@@ -41,49 +42,49 @@ def init_project(args: argparse.Namespace) -> None:
41
42
 
42
43
  os.makedirs(app_dir, exist_ok=True)
43
44
 
44
- # Minimal hello world app scaffold (no bootstrap function; host instantiates Page directly)
45
45
  main_page_py = os.path.join(app_dir, "main_page.py")
46
46
  if not os.path.exists(main_page_py) or args.force:
47
47
  with open(main_page_py, "w", encoding="utf-8") as f:
48
- f.write(
49
- """import pythonnative as pn
48
+ f.write("""import pythonnative as pn
50
49
 
51
50
 
52
51
  class MainPage(pn.Page):
53
52
  def __init__(self, native_instance):
54
53
  super().__init__(native_instance)
55
-
56
- def on_create(self):
57
- super().on_create()
58
- stack = (
59
- pn.StackView()
60
- .set_axis("vertical")
61
- .set_spacing(12)
62
- .set_alignment("fill")
63
- .set_padding(all=16)
64
- )
65
- stack.add_view(pn.Label("Hello from PythonNative!").set_text_size(18))
66
- button = pn.Button("Tap me").set_on_click(lambda: print("Button clicked"))
67
- stack.add_view(button)
68
- self.set_root_view(stack.wrap_in_scroll())
69
- """
54
+ self.state = {"count": 0}
55
+
56
+ def increment(self):
57
+ self.set_state(count=self.state["count"] + 1)
58
+
59
+ def render(self):
60
+ return pn.ScrollView(
61
+ pn.Column(
62
+ pn.Text("Hello from PythonNative!", font_size=24, bold=True),
63
+ pn.Text(f"Tapped {self.state['count']} times"),
64
+ pn.Button("Tap me", on_click=self.increment),
65
+ spacing=12,
66
+ padding=16,
67
+ alignment="fill",
70
68
  )
69
+ )
70
+ """)
71
71
 
72
72
  # Create config
73
73
  config = {
74
74
  "name": project_name,
75
75
  "appId": "com.example." + project_name.replace(" ", "").lower(),
76
76
  "entryPoint": "app/main_page.py",
77
+ "pythonVersion": "3.11",
77
78
  "ios": {},
78
79
  "android": {},
79
80
  }
80
81
  with open(config_path, "w", encoding="utf-8") as f:
81
82
  json.dump(config, f, indent=2)
82
83
 
83
- # Requirements
84
+ # Requirements (third-party packages only; pythonnative itself is bundled by the CLI)
84
85
  if not os.path.exists(requirements_path) or args.force:
85
86
  with open(requirements_path, "w", encoding="utf-8") as f:
86
- f.write("pythonnative\n")
87
+ f.write("")
87
88
 
88
89
  # .gitignore
89
90
  default_gitignore = "# PythonNative\n" "__pycache__/\n" "*.pyc\n" ".venv/\n" "build/\n" ".DS_Store\n"
@@ -157,7 +158,11 @@ def _copy_bundled_template_dir(template_dir: str, destination: str) -> None:
157
158
 
158
159
 
159
160
  def _github_json(url: str) -> Any:
160
- 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)
161
166
  with urllib.request.urlopen(req) as r:
162
167
  return json.loads(r.read().decode("utf-8"))
163
168
 
@@ -212,6 +217,43 @@ def create_ios_project(project_name: str, destination: str) -> None:
212
217
  _copy_bundled_template_dir("ios_template", destination)
213
218
 
214
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
+
215
257
  def run_project(args: argparse.Namespace) -> None:
216
258
  """
217
259
  Run the specified project.
@@ -219,9 +261,15 @@ def run_project(args: argparse.Namespace) -> None:
219
261
  # Determine the platform
220
262
  platform: str = args.platform
221
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")
222
270
 
223
271
  # Define the build directory
224
- build_dir: str = os.path.join(os.getcwd(), "build", platform)
272
+ build_dir: str = os.path.join(project_dir, "build", platform)
225
273
 
226
274
  # Create the build directory if it doesn't exist
227
275
  os.makedirs(build_dir, exist_ok=True)
@@ -265,10 +313,30 @@ def run_project(args: argparse.Namespace) -> None:
265
313
  # Non-fatal; fallback to the packaged PyPI dependency if present
266
314
  pass
267
315
 
268
- # 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
269
338
  # Skip installation during prepare-only to avoid network access and speed up scaffolding
270
339
  if not prepare_only:
271
- requirements_path = os.path.join(os.getcwd(), "requirements.txt")
272
340
  if os.path.exists(requirements_path):
273
341
  subprocess.run([sys.executable, "-m", "pip", "install", "-r", requirements_path], check=False)
274
342
 
@@ -520,6 +588,29 @@ def run_project(args: argparse.Namespace) -> None:
520
588
  except Exception:
521
589
  # Non-fatal; if metadata isn't present, rubicon import may fail and fallback UI will appear
522
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
523
614
  # Note: Python.xcframework provides a static library for Simulator; it must be linked at build time.
524
615
  # We copy the XCFramework into the project directory above so Xcode can link it.
525
616
  except Exception:
@@ -566,6 +657,39 @@ def run_project(args: argparse.Namespace) -> None:
566
657
  except Exception:
567
658
  print("Failed to auto-run on Simulator; open the project in Xcode to run.")
568
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
+
569
693
 
570
694
  def clean_project(args: argparse.Namespace) -> None:
571
695
  """
@@ -600,6 +724,11 @@ def main() -> None:
600
724
  action="store_true",
601
725
  help="Extract templates and stage app without building",
602
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
+ )
603
732
  parser_run.set_defaults(func=run_project)
604
733
 
605
734
  # Create a new command 'clean' that calls clean_project