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.
- pythonnative/__init__.py +94 -66
- pythonnative/cli/pn.py +153 -24
- pythonnative/components.py +563 -0
- pythonnative/element.py +53 -0
- pythonnative/hooks.py +287 -0
- pythonnative/hot_reload.py +143 -0
- pythonnative/native_modules/__init__.py +19 -0
- pythonnative/native_modules/camera.py +105 -0
- pythonnative/native_modules/file_system.py +131 -0
- pythonnative/native_modules/location.py +61 -0
- pythonnative/native_modules/notifications.py +151 -0
- pythonnative/native_views.py +1334 -0
- pythonnative/page.py +320 -247
- pythonnative/reconciler.py +262 -0
- pythonnative/style.py +115 -0
- pythonnative/templates/android_template/app/build.gradle +2 -7
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -1
- pythonnative/templates/android_template/build.gradle +1 -1
- pythonnative/utils.py +21 -29
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/METADATA +20 -19
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/RECORD +25 -40
- pythonnative/activity_indicator_view.py +0 -71
- pythonnative/button.py +0 -113
- pythonnative/collection_view.py +0 -0
- pythonnative/date_picker.py +0 -76
- pythonnative/image_view.py +0 -78
- pythonnative/label.py +0 -133
- pythonnative/list_view.py +0 -76
- pythonnative/material_activity_indicator_view.py +0 -71
- pythonnative/material_bottom_navigation_view.py +0 -0
- pythonnative/material_button.py +0 -69
- pythonnative/material_date_picker.py +0 -87
- pythonnative/material_progress_view.py +0 -70
- pythonnative/material_search_bar.py +0 -69
- pythonnative/material_switch.py +0 -69
- pythonnative/material_time_picker.py +0 -76
- pythonnative/material_toolbar.py +0 -0
- pythonnative/picker_view.py +0 -69
- pythonnative/progress_view.py +0 -70
- pythonnative/scroll_view.py +0 -101
- pythonnative/search_bar.py +0 -69
- pythonnative/stack_view.py +0 -199
- pythonnative/switch.py +0 -68
- pythonnative/text_field.py +0 -132
- pythonnative/text_view.py +0 -135
- pythonnative/time_picker.py +0 -77
- pythonnative/view.py +0 -173
- pythonnative/web_view.py +0 -60
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
2
|
-
from typing import Any, Dict
|
|
1
|
+
"""PythonNative — declarative native UI for Android and iOS.
|
|
3
2
|
|
|
4
|
-
|
|
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
|
-
|
|
67
|
+
# Components
|
|
68
|
+
"ActivityIndicator",
|
|
8
69
|
"Button",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
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
|
-
"
|
|
27
|
-
"
|
|
79
|
+
"Slider",
|
|
80
|
+
"Spacer",
|
|
28
81
|
"Switch",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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("
|
|
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
|
-
|
|
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(
|
|
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
|
-
#
|
|
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
|