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.
Files changed (28) hide show
  1. pythonnative/__init__.py +53 -15
  2. pythonnative/cli/pn.py +150 -30
  3. pythonnative/components.py +217 -107
  4. pythonnative/element.py +14 -8
  5. pythonnative/hooks.py +334 -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 +638 -34
  13. pythonnative/page.py +138 -171
  14. pythonnative/reconciler.py +153 -20
  15. pythonnative/style.py +135 -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 -9
  18. pythonnative/templates/android_template/build.gradle +1 -1
  19. pythonnative/templates/ios_template/ios_template/ViewController.swift +7 -20
  20. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/METADATA +18 -38
  21. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/RECORD +25 -20
  22. pythonnative/collection_view.py +0 -0
  23. pythonnative/material_bottom_navigation_view.py +0 -0
  24. pythonnative/material_toolbar.py +0 -0
  25. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/WHEEL +0 -0
  26. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/entry_points.txt +0 -0
  27. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/licenses/LICENSE +0 -0
  28. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/top_level.txt +0 -0
@@ -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.type != new_el.type:
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
- max_len = max(len(old_children), len(new_children))
91
-
92
- for i in range(max_len):
93
- if i >= len(new_children):
94
- self.backend.remove_child(parent.native_view, old_children[i].native_view, parent.element.type)
95
- self._destroy_tree(old_children[i])
96
- elif i >= len(old_children):
97
- node = self._create_tree(new_children[i])
98
- self.backend.add_child(parent.native_view, node.native_view, parent.element.type)
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
- if old_children[i].element.type != new_children[i].type:
102
- self.backend.remove_child(parent.native_view, old_children[i].native_view, parent.element.type)
103
- self._destroy_tree(old_children[i])
104
- node = self._create_tree(new_children[i])
105
- self.backend.insert_child(parent.native_view, node.native_view, parent.element.type, i)
106
- new_child_nodes.append(node)
107
- else:
108
- updated = self._reconcile_node(old_children[i], new_children[i])
109
- new_child_nodes.append(updated)
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.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
  }
@@ -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 moduleName = pagePath.substringBeforeLast('.')
29
- val className = pagePath.substringAfterLast('.')
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 '14.0.2' apply false
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 moduleName = String(pagePath.split(separator: ".").dropLast().joined(separator: "."))
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 page = try pageClass.throwing.dynamicallyCall(withArguments: [addr])
99
- // If args provided, pass into Page via set_args(dict)
100
- if let jsonStr = requestedPageArgsJSON {
101
- let json = Python.import("json")
102
- do {
103
- let args = try json.loads.throwing.dynamicallyCall(withArguments: [jsonStr])
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.5.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.9
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. Describe your UI as a tree of elements, manage state with `set_state()`, and let PythonNative handle creating and updating native views.
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
- - **Reactive state:** Call `self.set_state(key=value)` and the framework re-renders only what changed no manual view mutation.
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 for multi-page apps.
102
- - **Bundled templates:** Android Gradle and iOS Xcode templates are included scaffolding requires no network access.
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
- class MainPage(pn.Page):
119
- def __init__(self, native_instance):
120
- super().__init__(native_instance)
121
- self.state = {"count": 0}
122
-
123
- def render(self):
124
- return pn.Column(
125
- pn.Text(f"Count: {self.state['count']}", font_size=24),
126
- pn.Button(
127
- "Tap me",
128
- on_click=lambda: self.set_state(count=self.state["count"] + 1),
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=E0OXTarwvCIL7DcnuAC8GhLe_EY-7MrQJzwaBH8yVYY,1040
2
- pythonnative/collection_view.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- pythonnative/components.py,sha256=TASfQZS1u_tEF0QvK3E1Uj0Dzb26GA3kvGqOLOpbuxY,6756
4
- pythonnative/element.py,sha256=7gfdjtCAcz4YBrWmUkve3zeyM_495yiPKAJZEXq2QZM,1453
5
- pythonnative/material_bottom_navigation_view.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- pythonnative/material_toolbar.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- pythonnative/native_views.py,sha256=lQK7EMQhG2foBSjonH8iIQbCAkqwPj-QV6xg5e8l9HQ,36357
8
- pythonnative/page.py,sha256=agVamXmMAbOlYB1MLp7GGosiH75p6C2CYvNPVTWg930,15150
9
- pythonnative/reconciler.py,sha256=P7KM71BoQGpsU2wsvB-o2QxHAfOy9zXZwCx2lBBLyz8,4976
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=m6mwfS2SAu9W53yhouzFHfnALXtrCQB_04uFXMev5dc,27239
13
- pythonnative/templates/android_template/build.gradle,sha256=metF5S4LveW05kDE2e-nzVG5rtwe2HESYs-lGNl390A,352
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=JPHjBIEJTzrO4mMXvOdWhlSiIKuwHJmROiQmjr_tPGs,1863
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=fMQUugt9WqUIA9CbE4BafrVdPAbBTR_K_LH6bLed7XQ,4047
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=CvIpVOLRY3WSmm-_25N441b_8kaYKgmfkkQChNcBU7M,9241
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.5.0.dist-info/licenses/LICENSE,sha256=A69iG7TIAe6KkGQf6xoVHkc5JSZtOr5eRSvC5iuivnI,1067
65
- pythonnative-0.5.0.dist-info/METADATA,sha256=TVhRDPT4qFBuijk8eEIgDTXugnxw6G-B_1_JBGpMCpM,7256
66
- pythonnative-0.5.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
67
- pythonnative-0.5.0.dist-info/entry_points.txt,sha256=iUtDawWSAJAEyWTycpZxDuYz73ol31butpzDIEAgPO0,48
68
- pythonnative-0.5.0.dist-info/top_level.txt,sha256=kT4SEATY2ywzrZ2Pgea6_zxyym44Q_PbOsUoOYjJLFE,13
69
- pythonnative-0.5.0.dist-info/RECORD,,
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,,
File without changes
File without changes
File without changes