pythonnative 0.5.0__py3-none-any.whl → 0.7.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 +53 -15
- pythonnative/cli/pn.py +150 -30
- pythonnative/components.py +217 -107
- pythonnative/element.py +14 -8
- pythonnative/hooks.py +334 -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 +638 -34
- pythonnative/page.py +138 -171
- pythonnative/reconciler.py +153 -20
- pythonnative/style.py +135 -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 -9
- pythonnative/templates/android_template/build.gradle +1 -1
- pythonnative/templates/ios_template/ios_template/ViewController.swift +7 -20
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/METADATA +18 -38
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/RECORD +25 -20
- pythonnative/collection_view.py +0 -0
- pythonnative/material_bottom_navigation_view.py +0 -0
- pythonnative/material_toolbar.py +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/top_level.txt +0 -0
pythonnative/reconciler.py
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
Maintains a tree of :class:`VNode` objects (each wrapping a native view)
|
|
4
4
|
and diffs incoming :class:`Element` trees to apply the minimal set of
|
|
5
5
|
native mutations.
|
|
6
|
+
|
|
7
|
+
Supports:
|
|
8
|
+
|
|
9
|
+
- **Native elements** (type is a string like ``"Text"``).
|
|
10
|
+
- **Function components** (type is a callable decorated with
|
|
11
|
+
``@component``). Their hook state is preserved across renders.
|
|
12
|
+
- **Provider elements** (type ``"__Provider__"``), which push/pop
|
|
13
|
+
context values during tree traversal.
|
|
14
|
+
- **Key-based child reconciliation** for stable identity across
|
|
15
|
+
re-renders.
|
|
6
16
|
"""
|
|
7
17
|
|
|
8
18
|
from typing import Any, List, Optional
|
|
@@ -13,12 +23,14 @@ from .element import Element
|
|
|
13
23
|
class VNode:
|
|
14
24
|
"""A mounted element paired with its native view and child VNodes."""
|
|
15
25
|
|
|
16
|
-
__slots__ = ("element", "native_view", "children")
|
|
26
|
+
__slots__ = ("element", "native_view", "children", "hook_state", "_rendered")
|
|
17
27
|
|
|
18
28
|
def __init__(self, element: Element, native_view: Any, children: List["VNode"]) -> None:
|
|
19
29
|
self.element = element
|
|
20
30
|
self.native_view = native_view
|
|
21
31
|
self.children = children
|
|
32
|
+
self.hook_state: Any = None
|
|
33
|
+
self._rendered: Optional[Element] = None
|
|
22
34
|
|
|
23
35
|
|
|
24
36
|
class Reconciler:
|
|
@@ -35,6 +47,7 @@ class Reconciler:
|
|
|
35
47
|
def __init__(self, backend: Any) -> None:
|
|
36
48
|
self.backend = backend
|
|
37
49
|
self._tree: Optional[VNode] = None
|
|
50
|
+
self._page_re_render: Optional[Any] = None
|
|
38
51
|
|
|
39
52
|
# ------------------------------------------------------------------
|
|
40
53
|
# Public API
|
|
@@ -62,6 +75,37 @@ class Reconciler:
|
|
|
62
75
|
# ------------------------------------------------------------------
|
|
63
76
|
|
|
64
77
|
def _create_tree(self, element: Element) -> VNode:
|
|
78
|
+
# Provider: push context, create child, pop context
|
|
79
|
+
if element.type == "__Provider__":
|
|
80
|
+
context = element.props["__context__"]
|
|
81
|
+
context._stack.append(element.props["__value__"])
|
|
82
|
+
try:
|
|
83
|
+
child_node = self._create_tree(element.children[0]) if element.children else None
|
|
84
|
+
finally:
|
|
85
|
+
context._stack.pop()
|
|
86
|
+
native_view = child_node.native_view if child_node else None
|
|
87
|
+
children = [child_node] if child_node else []
|
|
88
|
+
return VNode(element, native_view, children)
|
|
89
|
+
|
|
90
|
+
# Function component: call with hook context
|
|
91
|
+
if callable(element.type):
|
|
92
|
+
from .hooks import HookState, _set_hook_state
|
|
93
|
+
|
|
94
|
+
hook_state = HookState()
|
|
95
|
+
hook_state._trigger_render = self._page_re_render
|
|
96
|
+
_set_hook_state(hook_state)
|
|
97
|
+
try:
|
|
98
|
+
rendered = element.type(**element.props)
|
|
99
|
+
finally:
|
|
100
|
+
_set_hook_state(None)
|
|
101
|
+
|
|
102
|
+
child_node = self._create_tree(rendered)
|
|
103
|
+
vnode = VNode(element, child_node.native_view, [child_node])
|
|
104
|
+
vnode.hook_state = hook_state
|
|
105
|
+
vnode._rendered = rendered
|
|
106
|
+
return vnode
|
|
107
|
+
|
|
108
|
+
# Native element
|
|
65
109
|
native_view = self.backend.create_view(element.type, element.props)
|
|
66
110
|
children: List[VNode] = []
|
|
67
111
|
for child_el in element.children:
|
|
@@ -71,11 +115,58 @@ class Reconciler:
|
|
|
71
115
|
return VNode(element, native_view, children)
|
|
72
116
|
|
|
73
117
|
def _reconcile_node(self, old: VNode, new_el: Element) -> VNode:
|
|
74
|
-
if old.element
|
|
118
|
+
if not self._same_type(old.element, new_el):
|
|
75
119
|
new_node = self._create_tree(new_el)
|
|
76
120
|
self._destroy_tree(old)
|
|
77
121
|
return new_node
|
|
78
122
|
|
|
123
|
+
# Provider
|
|
124
|
+
if new_el.type == "__Provider__":
|
|
125
|
+
context = new_el.props["__context__"]
|
|
126
|
+
context._stack.append(new_el.props["__value__"])
|
|
127
|
+
try:
|
|
128
|
+
if old.children and new_el.children:
|
|
129
|
+
child = self._reconcile_node(old.children[0], new_el.children[0])
|
|
130
|
+
old.children = [child]
|
|
131
|
+
old.native_view = child.native_view
|
|
132
|
+
elif new_el.children:
|
|
133
|
+
child = self._create_tree(new_el.children[0])
|
|
134
|
+
old.children = [child]
|
|
135
|
+
old.native_view = child.native_view
|
|
136
|
+
finally:
|
|
137
|
+
context._stack.pop()
|
|
138
|
+
old.element = new_el
|
|
139
|
+
return old
|
|
140
|
+
|
|
141
|
+
# Function component
|
|
142
|
+
if callable(new_el.type):
|
|
143
|
+
from .hooks import _set_hook_state
|
|
144
|
+
|
|
145
|
+
hook_state = old.hook_state
|
|
146
|
+
if hook_state is None:
|
|
147
|
+
from .hooks import HookState
|
|
148
|
+
|
|
149
|
+
hook_state = HookState()
|
|
150
|
+
hook_state.reset_index()
|
|
151
|
+
hook_state._trigger_render = self._page_re_render
|
|
152
|
+
_set_hook_state(hook_state)
|
|
153
|
+
try:
|
|
154
|
+
rendered = new_el.type(**new_el.props)
|
|
155
|
+
finally:
|
|
156
|
+
_set_hook_state(None)
|
|
157
|
+
|
|
158
|
+
if old.children:
|
|
159
|
+
child = self._reconcile_node(old.children[0], rendered)
|
|
160
|
+
else:
|
|
161
|
+
child = self._create_tree(rendered)
|
|
162
|
+
old.children = [child]
|
|
163
|
+
old.native_view = child.native_view
|
|
164
|
+
old.element = new_el
|
|
165
|
+
old.hook_state = hook_state
|
|
166
|
+
old._rendered = rendered
|
|
167
|
+
return old
|
|
168
|
+
|
|
169
|
+
# Native element
|
|
79
170
|
changed = self._diff_props(old.element.props, new_el.props)
|
|
80
171
|
if changed:
|
|
81
172
|
self.backend.update_view(old.native_view, old.element.type, changed)
|
|
@@ -86,44 +177,86 @@ class Reconciler:
|
|
|
86
177
|
|
|
87
178
|
def _reconcile_children(self, parent: VNode, new_children: List[Element]) -> None:
|
|
88
179
|
old_children = parent.children
|
|
180
|
+
parent_type = parent.element.type
|
|
181
|
+
is_native = isinstance(parent_type, str) and parent_type != "__Provider__"
|
|
182
|
+
|
|
183
|
+
old_by_key: dict = {}
|
|
184
|
+
old_unkeyed: list = []
|
|
185
|
+
for child in old_children:
|
|
186
|
+
if child.element.key is not None:
|
|
187
|
+
old_by_key[child.element.key] = child
|
|
188
|
+
else:
|
|
189
|
+
old_unkeyed.append(child)
|
|
190
|
+
|
|
89
191
|
new_child_nodes: List[VNode] = []
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
192
|
+
used_keyed: set = set()
|
|
193
|
+
unkeyed_iter = iter(old_unkeyed)
|
|
194
|
+
|
|
195
|
+
for i, new_el in enumerate(new_children):
|
|
196
|
+
matched: Optional[VNode] = None
|
|
197
|
+
|
|
198
|
+
if new_el.key is not None and new_el.key in old_by_key:
|
|
199
|
+
matched = old_by_key[new_el.key]
|
|
200
|
+
used_keyed.add(new_el.key)
|
|
201
|
+
elif new_el.key is None:
|
|
202
|
+
matched = next(unkeyed_iter, None)
|
|
203
|
+
|
|
204
|
+
if matched is None:
|
|
205
|
+
node = self._create_tree(new_el)
|
|
206
|
+
if is_native:
|
|
207
|
+
self.backend.add_child(parent.native_view, node.native_view, parent_type)
|
|
208
|
+
new_child_nodes.append(node)
|
|
209
|
+
elif not self._same_type(matched.element, new_el):
|
|
210
|
+
if is_native:
|
|
211
|
+
self.backend.remove_child(parent.native_view, matched.native_view, parent_type)
|
|
212
|
+
self._destroy_tree(matched)
|
|
213
|
+
node = self._create_tree(new_el)
|
|
214
|
+
if is_native:
|
|
215
|
+
self.backend.insert_child(parent.native_view, node.native_view, parent_type, i)
|
|
99
216
|
new_child_nodes.append(node)
|
|
100
217
|
else:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
218
|
+
updated = self._reconcile_node(matched, new_el)
|
|
219
|
+
new_child_nodes.append(updated)
|
|
220
|
+
|
|
221
|
+
# Destroy unused old nodes
|
|
222
|
+
for key, node in old_by_key.items():
|
|
223
|
+
if key not in used_keyed:
|
|
224
|
+
if is_native:
|
|
225
|
+
self.backend.remove_child(parent.native_view, node.native_view, parent_type)
|
|
226
|
+
self._destroy_tree(node)
|
|
227
|
+
for node in unkeyed_iter:
|
|
228
|
+
if is_native:
|
|
229
|
+
self.backend.remove_child(parent.native_view, node.native_view, parent_type)
|
|
230
|
+
self._destroy_tree(node)
|
|
110
231
|
|
|
111
232
|
parent.children = new_child_nodes
|
|
112
233
|
|
|
113
234
|
def _destroy_tree(self, node: VNode) -> None:
|
|
235
|
+
if node.hook_state is not None:
|
|
236
|
+
node.hook_state.cleanup_all_effects()
|
|
114
237
|
for child in node.children:
|
|
115
238
|
self._destroy_tree(child)
|
|
116
239
|
node.children = []
|
|
117
240
|
|
|
241
|
+
@staticmethod
|
|
242
|
+
def _same_type(old_el: Element, new_el: Element) -> bool:
|
|
243
|
+
if isinstance(old_el.type, str):
|
|
244
|
+
return old_el.type == new_el.type
|
|
245
|
+
return old_el.type is new_el.type
|
|
246
|
+
|
|
118
247
|
@staticmethod
|
|
119
248
|
def _diff_props(old: dict, new: dict) -> dict:
|
|
120
249
|
"""Return props that changed (callables always count as changed)."""
|
|
121
250
|
changed = {}
|
|
122
251
|
for key, new_val in new.items():
|
|
252
|
+
if key.startswith("__"):
|
|
253
|
+
continue
|
|
123
254
|
old_val = old.get(key)
|
|
124
255
|
if callable(new_val) or old_val != new_val:
|
|
125
256
|
changed[key] = new_val
|
|
126
257
|
for key in old:
|
|
258
|
+
if key.startswith("__"):
|
|
259
|
+
continue
|
|
127
260
|
if key not in new:
|
|
128
261
|
changed[key] = None
|
|
129
262
|
return changed
|
pythonnative/style.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""StyleSheet, style resolution, and theming support.
|
|
2
|
+
|
|
3
|
+
Provides a :class:`StyleSheet` helper for creating and composing
|
|
4
|
+
reusable style dictionaries, a :func:`resolve_style` utility for
|
|
5
|
+
flattening the ``style`` prop, and built-in theme contexts.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
import pythonnative as pn
|
|
10
|
+
|
|
11
|
+
styles = pn.StyleSheet.create(
|
|
12
|
+
title={"font_size": 24, "bold": True, "color": "#333"},
|
|
13
|
+
container={"padding": 16, "spacing": 12},
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
pn.Text("Hello", style=styles["title"])
|
|
17
|
+
pn.Column(..., style=styles["container"])
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Any, Dict, List, Optional, Union
|
|
21
|
+
|
|
22
|
+
from .hooks import Context, create_context
|
|
23
|
+
|
|
24
|
+
_StyleDict = Dict[str, Any]
|
|
25
|
+
StyleValue = Union[None, _StyleDict, List[Optional[_StyleDict]]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_style(style: StyleValue) -> _StyleDict:
|
|
29
|
+
"""Flatten a ``style`` prop into a single dict.
|
|
30
|
+
|
|
31
|
+
Accepts ``None``, a single dict, or a list of dicts (later entries
|
|
32
|
+
override earlier ones, mirroring React Native's array style pattern).
|
|
33
|
+
"""
|
|
34
|
+
if style is None:
|
|
35
|
+
return {}
|
|
36
|
+
if isinstance(style, dict):
|
|
37
|
+
return dict(style)
|
|
38
|
+
result: _StyleDict = {}
|
|
39
|
+
for entry in style:
|
|
40
|
+
if entry:
|
|
41
|
+
result.update(entry)
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ======================================================================
|
|
46
|
+
# StyleSheet
|
|
47
|
+
# ======================================================================
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class StyleSheet:
|
|
51
|
+
"""Utility for creating and composing style dictionaries."""
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def create(**named_styles: _StyleDict) -> Dict[str, _StyleDict]:
|
|
55
|
+
"""Create a set of named styles.
|
|
56
|
+
|
|
57
|
+
Each keyword argument is a style name mapping to a dict of
|
|
58
|
+
property values::
|
|
59
|
+
|
|
60
|
+
styles = StyleSheet.create(
|
|
61
|
+
heading={"font_size": 28, "bold": True},
|
|
62
|
+
body={"font_size": 16},
|
|
63
|
+
)
|
|
64
|
+
"""
|
|
65
|
+
return {name: dict(props) for name, props in named_styles.items()}
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def compose(*styles: _StyleDict) -> _StyleDict:
|
|
69
|
+
"""Merge multiple style dicts, later values overriding earlier ones."""
|
|
70
|
+
merged: _StyleDict = {}
|
|
71
|
+
for style in styles:
|
|
72
|
+
if style:
|
|
73
|
+
merged.update(style)
|
|
74
|
+
return merged
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def flatten(styles: Any) -> _StyleDict:
|
|
78
|
+
"""Flatten a style or list of styles into a single dict.
|
|
79
|
+
|
|
80
|
+
Accepts a single dict, a list of dicts, or ``None``.
|
|
81
|
+
"""
|
|
82
|
+
if styles is None:
|
|
83
|
+
return {}
|
|
84
|
+
if isinstance(styles, dict):
|
|
85
|
+
return dict(styles)
|
|
86
|
+
result: _StyleDict = {}
|
|
87
|
+
for s in styles:
|
|
88
|
+
if s:
|
|
89
|
+
result.update(s)
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ======================================================================
|
|
94
|
+
# Theming
|
|
95
|
+
# ======================================================================
|
|
96
|
+
|
|
97
|
+
DEFAULT_LIGHT_THEME: _StyleDict = {
|
|
98
|
+
"primary_color": "#007AFF",
|
|
99
|
+
"secondary_color": "#5856D6",
|
|
100
|
+
"background_color": "#FFFFFF",
|
|
101
|
+
"surface_color": "#F2F2F7",
|
|
102
|
+
"text_color": "#000000",
|
|
103
|
+
"text_secondary_color": "#8E8E93",
|
|
104
|
+
"error_color": "#FF3B30",
|
|
105
|
+
"success_color": "#34C759",
|
|
106
|
+
"warning_color": "#FF9500",
|
|
107
|
+
"font_size": 16,
|
|
108
|
+
"font_size_small": 13,
|
|
109
|
+
"font_size_large": 20,
|
|
110
|
+
"font_size_title": 28,
|
|
111
|
+
"spacing": 8,
|
|
112
|
+
"spacing_large": 16,
|
|
113
|
+
"border_radius": 8,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
DEFAULT_DARK_THEME: _StyleDict = {
|
|
117
|
+
"primary_color": "#0A84FF",
|
|
118
|
+
"secondary_color": "#5E5CE6",
|
|
119
|
+
"background_color": "#000000",
|
|
120
|
+
"surface_color": "#1C1C1E",
|
|
121
|
+
"text_color": "#FFFFFF",
|
|
122
|
+
"text_secondary_color": "#8E8E93",
|
|
123
|
+
"error_color": "#FF453A",
|
|
124
|
+
"success_color": "#30D158",
|
|
125
|
+
"warning_color": "#FF9F0A",
|
|
126
|
+
"font_size": 16,
|
|
127
|
+
"font_size_small": 13,
|
|
128
|
+
"font_size_large": 20,
|
|
129
|
+
"font_size_title": 28,
|
|
130
|
+
"spacing": 8,
|
|
131
|
+
"spacing_large": 16,
|
|
132
|
+
"border_radius": 8,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
ThemeContext: Context = create_context(DEFAULT_LIGHT_THEME)
|
|
@@ -20,14 +20,9 @@ android {
|
|
|
20
20
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
|
|
21
21
|
}
|
|
22
22
|
python {
|
|
23
|
-
version "3.
|
|
23
|
+
version "3.11"
|
|
24
24
|
pip {
|
|
25
|
-
install "
|
|
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
|
}
|
|
@@ -25,15 +25,8 @@ class PageFragment : Fragment() {
|
|
|
25
25
|
val py = Python.getInstance()
|
|
26
26
|
val pagePath = arguments?.getString("page_path") ?: "app.main_page.MainPage"
|
|
27
27
|
val argsJson = arguments?.getString("args_json")
|
|
28
|
-
val
|
|
29
|
-
|
|
30
|
-
val pyModule = py.getModule(moduleName)
|
|
31
|
-
val pageClass = pyModule.get(className)
|
|
32
|
-
// Pass the hosting Activity as native_instance for context
|
|
33
|
-
page = pageClass?.call(requireActivity())
|
|
34
|
-
if (!argsJson.isNullOrEmpty()) {
|
|
35
|
-
page?.callAttr("set_args", argsJson)
|
|
36
|
-
}
|
|
28
|
+
val pnPage = py.getModule("pythonnative.page")
|
|
29
|
+
page = pnPage.callAttr("create_page", pagePath, requireActivity(), argsJson)
|
|
37
30
|
} catch (e: Exception) {
|
|
38
31
|
Log.e(TAG, "Failed to instantiate page", e)
|
|
39
32
|
}
|
|
@@ -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 '
|
|
6
|
+
id 'com.chaquo.python' version '15.0.1' apply false
|
|
7
7
|
}
|
|
@@ -85,28 +85,15 @@ class ViewController: UIViewController {
|
|
|
85
85
|
// Determine which Python page to load
|
|
86
86
|
let pagePath: String = requestedPagePath ?? "app.main_page.MainPage"
|
|
87
87
|
do {
|
|
88
|
-
let
|
|
89
|
-
let className = String(pagePath.split(separator: ".").last ?? "MainPage")
|
|
90
|
-
let pyModule = try Python.attemptImport(moduleName)
|
|
91
|
-
// Resolve class by name via builtins.getattr to avoid subscripting issues
|
|
92
|
-
let builtins = Python.import("builtins")
|
|
93
|
-
let getattrFn = builtins.getattr
|
|
94
|
-
let pageClass = try getattrFn.throwing.dynamicallyCall(withArguments: [pyModule, className])
|
|
95
|
-
// Pass native pointer so Python Page can wrap via rubicon.objc
|
|
88
|
+
let pnPage = try Python.attemptImport("pythonnative.page")
|
|
96
89
|
let ptr = Unmanaged.passUnretained(self).toOpaque()
|
|
97
90
|
let addr = UInt(bitPattern: ptr)
|
|
98
|
-
let
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
_ = try page.set_args.throwing.dynamicallyCall(withArguments: [args])
|
|
105
|
-
} catch {
|
|
106
|
-
NSLog("[PN] Failed to decode requestedPageArgsJSON: \(error)")
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// Call on_create immediately so Python can insert its root view
|
|
91
|
+
let argsJson: PythonObject = (requestedPageArgsJSON != nil)
|
|
92
|
+
? PythonObject(requestedPageArgsJSON!)
|
|
93
|
+
: Python.None
|
|
94
|
+
let page = try pnPage.create_page.throwing.dynamicallyCall(
|
|
95
|
+
withArguments: [pagePath, addr, argsJson]
|
|
96
|
+
)
|
|
110
97
|
_ = try page.on_create.throwing.dynamicallyCall(withArguments: [])
|
|
111
98
|
return
|
|
112
99
|
} catch {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonnative
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Cross-platform native UI toolkit for Android and iOS
|
|
5
5
|
Author: Owen Carey
|
|
6
6
|
License: MIT License
|
|
@@ -34,12 +34,11 @@ Classifier: Intended Audience :: Developers
|
|
|
34
34
|
Classifier: License :: OSI Approved :: MIT License
|
|
35
35
|
Classifier: Programming Language :: Python :: 3
|
|
36
36
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
37
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
38
37
|
Classifier: Programming Language :: Python :: 3.10
|
|
39
38
|
Classifier: Programming Language :: Python :: 3.11
|
|
40
39
|
Classifier: Programming Language :: Python :: 3.12
|
|
41
40
|
Classifier: Topic :: Software Development :: User Interfaces
|
|
42
|
-
Requires-Python: >=3.
|
|
41
|
+
Requires-Python: >=3.10
|
|
43
42
|
Description-Content-Type: text/markdown
|
|
44
43
|
License-File: LICENSE
|
|
45
44
|
Requires-Dist: requests>=2.31.0
|
|
@@ -89,17 +88,18 @@ Dynamic: license-file
|
|
|
89
88
|
|
|
90
89
|
## Overview
|
|
91
90
|
|
|
92
|
-
PythonNative is a cross-platform toolkit for building native Android and iOS apps in Python. It provides a **declarative, React-like component model** with automatic reconciliation, powered by Chaquopy on Android and rubicon-objc on iOS.
|
|
91
|
+
PythonNative is a cross-platform toolkit for building native Android and iOS apps in Python. It provides a **declarative, React-like component model** with hooks and automatic reconciliation, powered by Chaquopy on Android and rubicon-objc on iOS. Write function components with `use_state`, `use_effect`, and friends, just like React, and let PythonNative handle creating and updating native views.
|
|
93
92
|
|
|
94
93
|
## Features
|
|
95
94
|
|
|
96
95
|
- **Declarative UI:** Describe *what* your UI should look like with element functions (`Text`, `Button`, `Column`, `Row`, etc.). PythonNative creates and updates native views automatically.
|
|
97
|
-
- **
|
|
96
|
+
- **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern.
|
|
97
|
+
- **`style` prop:** Pass all visual and layout properties through a single `style` dict, composable via `StyleSheet`.
|
|
98
98
|
- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
|
|
99
99
|
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
|
|
100
100
|
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
|
|
101
|
-
- **Navigation:** Push and pop screens with argument passing
|
|
102
|
-
- **Bundled templates:** Android Gradle and iOS Xcode templates are included
|
|
101
|
+
- **Navigation:** Push and pop screens with argument passing via the `use_navigation()` hook.
|
|
102
|
+
- **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.
|
|
103
103
|
|
|
104
104
|
## Quick Start
|
|
105
105
|
|
|
@@ -115,39 +115,19 @@ pip install pythonnative
|
|
|
115
115
|
import pythonnative as pn
|
|
116
116
|
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
),
|
|
130
|
-
spacing=12,
|
|
131
|
-
padding=16,
|
|
132
|
-
)
|
|
118
|
+
@pn.component
|
|
119
|
+
def MainPage():
|
|
120
|
+
count, set_count = pn.use_state(0)
|
|
121
|
+
return pn.Column(
|
|
122
|
+
pn.Text(f"Count: {count}", style={"font_size": 24}),
|
|
123
|
+
pn.Button(
|
|
124
|
+
"Tap me",
|
|
125
|
+
on_click=lambda: set_count(count + 1),
|
|
126
|
+
),
|
|
127
|
+
style={"spacing": 12, "padding": 16},
|
|
128
|
+
)
|
|
133
129
|
```
|
|
134
130
|
|
|
135
|
-
### Available Components
|
|
136
|
-
|
|
137
|
-
| Component | Description |
|
|
138
|
-
|---|---|
|
|
139
|
-
| `Text` | Display text |
|
|
140
|
-
| `Button` | Tappable button with `on_click` callback |
|
|
141
|
-
| `Column` / `Row` | Vertical / horizontal layout containers |
|
|
142
|
-
| `ScrollView` | Scrollable wrapper |
|
|
143
|
-
| `TextInput` | Text entry field with `on_change` callback |
|
|
144
|
-
| `Image` | Display images |
|
|
145
|
-
| `Switch` | Toggle with `on_change` callback |
|
|
146
|
-
| `ProgressBar` | Determinate progress (0.0–1.0) |
|
|
147
|
-
| `ActivityIndicator` | Indeterminate loading spinner |
|
|
148
|
-
| `WebView` | Embedded web content |
|
|
149
|
-
| `Spacer` | Empty space |
|
|
150
|
-
|
|
151
131
|
## Documentation
|
|
152
132
|
|
|
153
133
|
Visit [docs.pythonnative.com](https://docs.pythonnative.com/) for the full documentation, including getting started guides, platform-specific instructions for Android and iOS, API reference, and working examples.
|
|
@@ -1,27 +1,32 @@
|
|
|
1
|
-
pythonnative/__init__.py,sha256=
|
|
2
|
-
pythonnative/
|
|
3
|
-
pythonnative/
|
|
4
|
-
pythonnative/
|
|
5
|
-
pythonnative/
|
|
6
|
-
pythonnative/
|
|
7
|
-
pythonnative/
|
|
8
|
-
pythonnative/
|
|
9
|
-
pythonnative/
|
|
1
|
+
pythonnative/__init__.py,sha256=jhPsaI_qyTEQ_YMT5pJumYOQKYamfHbPalWd5VbJ5vY,1575
|
|
2
|
+
pythonnative/components.py,sha256=dvLaXmCFU8O6bnYdudwBVsvad1DkwvmMJoTZRAZi540,10534
|
|
3
|
+
pythonnative/element.py,sha256=RBUsXzzzM7KdK-NqMD-InVPKdAb8XJ0h0VpI2rwsfHs,1795
|
|
4
|
+
pythonnative/hooks.py,sha256=UMoNTFoMKON4uJpUU9WHBv60tR2GUvx4Q_OgfG24Xcg,10361
|
|
5
|
+
pythonnative/hot_reload.py,sha256=GoooBkRm2_zu_bQGOsOvUx-x9GdFcvRwSWEkhltFg2Y,4498
|
|
6
|
+
pythonnative/native_views.py,sha256=VuqseL-QRHsEoGe3j3Ubh-iPo0dAkx-rcieZqWlt5ho,62037
|
|
7
|
+
pythonnative/page.py,sha256=Pjf2o-0ufmn_6q2hyRZ3zrnHWkDTm2PD6rI4niQTfS8,14803
|
|
8
|
+
pythonnative/reconciler.py,sha256=4Ah8NtYawZH9ZHXoMMjMTfdrTkZ0b4OT3e_85WLTrbU,9850
|
|
9
|
+
pythonnative/style.py,sha256=NG58FSJCBTBBWRrDOyUiHZsTxQDA3jg2PSOcCsU2F5g,3882
|
|
10
10
|
pythonnative/utils.py,sha256=IqR_GYknveM_NfAblcaizg9S66hCZfrfiH08HzpOc-4,2537
|
|
11
11
|
pythonnative/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
pythonnative/cli/pn.py,sha256=
|
|
13
|
-
pythonnative/
|
|
12
|
+
pythonnative/cli/pn.py,sha256=Yi-RHjl3wUWHgGdwtA4UyEDy-tnjpmBB-PGupyDTae0,32823
|
|
13
|
+
pythonnative/native_modules/__init__.py,sha256=M_4SW-wCZ8azLuB0JLwUc8PESw00xMCM887-HuN9YNo,647
|
|
14
|
+
pythonnative/native_modules/camera.py,sha256=b3UErkABhBm2nQ-e72lEeFOgXoVDY6245cV6TN9iPBk,3509
|
|
15
|
+
pythonnative/native_modules/file_system.py,sha256=2fvYsIboCtjEyxmVeV72glOjtc9fJN3jTM6uaccru6E,4478
|
|
16
|
+
pythonnative/native_modules/location.py,sha256=bMSNbtG60hAorKXh4RR2vybX88M_BwHkCpTprRbMsZU,1883
|
|
17
|
+
pythonnative/native_modules/notifications.py,sha256=g-zT1GD-ojPsLN5eGXWkyNHTncSxdiEYtjllCXlsBSc,5377
|
|
18
|
+
pythonnative/templates/android_template/build.gradle,sha256=4gE6CRS6RuBu9kp-_e_uYYU9mBgHVZrqQg9caSxgyuc,352
|
|
14
19
|
pythonnative/templates/android_template/gradle.properties,sha256=REPaKLRfQiiVfIV8wYmgwzPWvF1f3bhh_kAMV9p4HME,1358
|
|
15
20
|
pythonnative/templates/android_template/gradlew,sha256=YxNShxF6Hm0SyEWA8fScYdG6AiGOzShmBgXpf5dufWU,5766
|
|
16
21
|
pythonnative/templates/android_template/gradlew.bat,sha256=xGonx5AHdG3lkisXq7YjDWStixujrRWF7lxlQ8KpsSk,2674
|
|
17
22
|
pythonnative/templates/android_template/settings.gradle,sha256=GKZiYUYWsaXxaiKOB65xnOs4jLmf0rhvI_3f8x0ic-o,333
|
|
18
|
-
pythonnative/templates/android_template/app/build.gradle,sha256=
|
|
23
|
+
pythonnative/templates/android_template/app/build.gradle,sha256=CpLxgnjwd4_WempE8ptbQ1C_GudMjZ1tFLB_4C-ZTAo,1669
|
|
19
24
|
pythonnative/templates/android_template/app/proguard-rules.pro,sha256=Vv2WDPIl9spA-YKxOl27DYvD394T_3ZCKCXGBw0KGJA,750
|
|
20
25
|
pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt,sha256=Am8Yla3i1eR_ac5FVgPU_RsuMrCbyT79h1BcajGE-zI,693
|
|
21
26
|
pythonnative/templates/android_template/app/src/main/AndroidManifest.xml,sha256=MdWrXxOrwUjnqtDbV952NI4nVF2dTUX9xwSS8chhd9I,940
|
|
22
27
|
pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt,sha256=sqOQ4k--WVyfnrhfNkzqAEQ211uYNPxhmIUXDrIb0zY,1321
|
|
23
28
|
pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt,sha256=dWOpdJFuGO2CWZZQjYPmSNxljjDyGUuys7-ehHhAqyM,931
|
|
24
|
-
pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt,sha256=
|
|
29
|
+
pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt,sha256=lvJ6llJaL7bU8vs90LRpr77eo60L5G7eUTceYcPNkcw,3729
|
|
25
30
|
pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml,sha256=7UI8c6b0Ck0pCfCQHmBSezqAfNWeG1WTvKrhgIscYyE,5606
|
|
26
31
|
pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml,sha256=AdGmpsEjTrf-Jw0JfrKD1yucla5RGIhvG2VzqtKA8fc,1702
|
|
27
32
|
pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml,sha256=HIgdCNktb3YoJC8QOTIv-0qZRtMRoPdARK59nyYFO6g,461
|
|
@@ -50,7 +55,7 @@ pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties
|
|
|
50
55
|
pythonnative/templates/ios_template/ios_template/AppDelegate.swift,sha256=_6G8GNcw4idXd75qKgQKTDCr45Ez73QB8WTvhBqqcMw,1349
|
|
51
56
|
pythonnative/templates/ios_template/ios_template/Info.plist,sha256=ZQIJGpo8Y2qP0j29xqOsIEGvPpEVICLTAw2NehC5CSo,704
|
|
52
57
|
pythonnative/templates/ios_template/ios_template/SceneDelegate.swift,sha256=lqtre92dc6d6s-f4ieh_M_4xmc_zMGW79j46tDu9cOY,2177
|
|
53
|
-
pythonnative/templates/ios_template/ios_template/ViewController.swift,sha256=
|
|
58
|
+
pythonnative/templates/ios_template/ios_template/ViewController.swift,sha256=eRyxIIVYAYWY9Lt9IH498C2c0FDERUcq-L2TalkEoe0,8315
|
|
54
59
|
pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json,sha256=D9Sbo8NYXHCWeOAEaoIcPGBoXscGNyDTDTo0SL46IIs,63
|
|
55
60
|
pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json,sha256=mvZQhvowtJJS-uGhIlcxaR3nlPd3WvdNcb7-tQfRK3w,123
|
|
56
61
|
pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json,sha256=VUwGr7K_geOvQjFh5VKB6iVXV1mi0tjGMinUmB2JvQs,177
|
|
@@ -61,9 +66,9 @@ pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/x
|
|
|
61
66
|
pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift,sha256=YnwzZx7yXB13xKAXEGNgz17VuhWeqkHTRTtBJ2Vu3_E,1238
|
|
62
67
|
pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift,sha256=l2Pwa50F_rv-qPu2go6e4bQernM6PTQJeNPFl_c4ivY,1387
|
|
63
68
|
pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift,sha256=f5JrG0uVtLMeJQy26Yyz7Om-JUkT220osqcbeIVkj2g,815
|
|
64
|
-
pythonnative-0.
|
|
65
|
-
pythonnative-0.
|
|
66
|
-
pythonnative-0.
|
|
67
|
-
pythonnative-0.
|
|
68
|
-
pythonnative-0.
|
|
69
|
-
pythonnative-0.
|
|
69
|
+
pythonnative-0.7.0.dist-info/licenses/LICENSE,sha256=A69iG7TIAe6KkGQf6xoVHkc5JSZtOr5eRSvC5iuivnI,1067
|
|
70
|
+
pythonnative-0.7.0.dist-info/METADATA,sha256=aa5sabkb9mMAcl_PSvvkIeFfHKlkpOnxJZRkMLTc3gE,6692
|
|
71
|
+
pythonnative-0.7.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
72
|
+
pythonnative-0.7.0.dist-info/entry_points.txt,sha256=iUtDawWSAJAEyWTycpZxDuYz73ol31butpzDIEAgPO0,48
|
|
73
|
+
pythonnative-0.7.0.dist-info/top_level.txt,sha256=kT4SEATY2ywzrZ2Pgea6_zxyym44Q_PbOsUoOYjJLFE,13
|
|
74
|
+
pythonnative-0.7.0.dist-info/RECORD,,
|
pythonnative/collection_view.py
DELETED
|
File without changes
|
|
File without changes
|
pythonnative/material_toolbar.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|