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
@@ -4,57 +4,42 @@ Each function returns an :class:`Element` describing a native UI widget.
4
4
  These are pure data — no native views are created until the reconciler
5
5
  mounts the element tree.
6
6
 
7
- Naming follows React Native conventions:
8
-
9
- - ``Text`` (was *Label*)
10
- - ``Button``
11
- - ``Column`` / ``Row`` (was *StackView* vertical/horizontal)
12
- - ``ScrollView``
13
- - ``TextInput`` (was *TextField*)
14
- - ``Image`` (was *ImageView*)
15
- - ``Switch``
16
- - ``ProgressBar`` (was *ProgressView*)
17
- - ``ActivityIndicator`` (was *ActivityIndicatorView*)
18
- - ``WebView``
19
- - ``Spacer`` (new)
20
- """
7
+ All visual and layout properties are passed via the ``style`` parameter,
8
+ which accepts a dict or a list of dicts (later entries override earlier).
21
9
 
22
- from typing import Any, Callable, Dict, Optional, Union
10
+ Layout properties supported by all components::
23
11
 
24
- from .element import Element
12
+ width, height, flex, margin, min_width, max_width, min_height,
13
+ max_height, align_self
25
14
 
15
+ Container-specific layout properties (Column / Row)::
26
16
 
27
- def _filter_none(**kwargs: Any) -> Dict[str, Any]:
28
- """Return *kwargs* with ``None``-valued entries removed."""
29
- return {k: v for k, v in kwargs.items() if v is not None}
17
+ spacing, padding, align_items, justify_content
18
+ """
30
19
 
20
+ from typing import Any, Callable, Dict, List, Optional
21
+
22
+ from .element import Element
23
+ from .style import StyleValue, resolve_style
31
24
 
32
- # ---------------------------------------------------------------------------
25
+ # ======================================================================
33
26
  # Leaf components
34
- # ---------------------------------------------------------------------------
27
+ # ======================================================================
35
28
 
36
29
 
37
30
  def Text(
38
31
  text: str = "",
39
32
  *,
40
- font_size: Optional[float] = None,
41
- color: Optional[str] = None,
42
- bold: bool = False,
43
- text_align: Optional[str] = None,
44
- background_color: Optional[str] = None,
45
- max_lines: Optional[int] = None,
46
- key: Optional[str] = None,
47
- ) -> Element:
48
- """Display text."""
49
- props = _filter_none(
50
- text=text,
51
- font_size=font_size,
52
- color=color,
53
- bold=bold or None,
54
- text_align=text_align,
55
- background_color=background_color,
56
- max_lines=max_lines,
57
- )
33
+ style: StyleValue = None,
34
+ key: Optional[str] = None,
35
+ ) -> Element:
36
+ """Display text.
37
+
38
+ Style properties: ``font_size``, ``color``, ``bold``, ``text_align``,
39
+ ``background_color``, ``max_lines``, plus common layout props.
40
+ """
41
+ props: Dict[str, Any] = {"text": text}
42
+ props.update(resolve_style(style))
58
43
  return Element("Text", props, [], key=key)
59
44
 
60
45
 
@@ -62,24 +47,21 @@ def Button(
62
47
  title: str = "",
63
48
  *,
64
49
  on_click: Optional[Callable[[], None]] = None,
65
- color: Optional[str] = None,
66
- background_color: Optional[str] = None,
67
- font_size: Optional[float] = None,
68
50
  enabled: bool = True,
51
+ style: StyleValue = None,
69
52
  key: Optional[str] = None,
70
53
  ) -> Element:
71
- """Create a tappable button."""
54
+ """Create a tappable button.
55
+
56
+ Style properties: ``color``, ``background_color``, ``font_size``,
57
+ plus common layout props.
58
+ """
72
59
  props: Dict[str, Any] = {"title": title}
73
60
  if on_click is not None:
74
61
  props["on_click"] = on_click
75
- if color is not None:
76
- props["color"] = color
77
- if background_color is not None:
78
- props["background_color"] = background_color
79
- if font_size is not None:
80
- props["font_size"] = font_size
81
62
  if not enabled:
82
63
  props["enabled"] = False
64
+ props.update(resolve_style(style))
83
65
  return Element("Button", props, [], key=key)
84
66
 
85
67
 
@@ -89,12 +71,14 @@ def TextInput(
89
71
  placeholder: str = "",
90
72
  on_change: Optional[Callable[[str], None]] = None,
91
73
  secure: bool = False,
92
- font_size: Optional[float] = None,
93
- color: Optional[str] = None,
94
- background_color: Optional[str] = None,
74
+ style: StyleValue = None,
95
75
  key: Optional[str] = None,
96
76
  ) -> Element:
97
- """Create a single-line text entry field."""
77
+ """Create a single-line text entry field.
78
+
79
+ Style properties: ``font_size``, ``color``, ``background_color``,
80
+ plus common layout props.
81
+ """
98
82
  props: Dict[str, Any] = {"value": value}
99
83
  if placeholder:
100
84
  props["placeholder"] = placeholder
@@ -102,32 +86,27 @@ def TextInput(
102
86
  props["on_change"] = on_change
103
87
  if secure:
104
88
  props["secure"] = True
105
- if font_size is not None:
106
- props["font_size"] = font_size
107
- if color is not None:
108
- props["color"] = color
109
- if background_color is not None:
110
- props["background_color"] = background_color
89
+ props.update(resolve_style(style))
111
90
  return Element("TextInput", props, [], key=key)
112
91
 
113
92
 
114
93
  def Image(
115
94
  source: str = "",
116
95
  *,
117
- width: Optional[float] = None,
118
- height: Optional[float] = None,
119
96
  scale_type: Optional[str] = None,
120
- background_color: Optional[str] = None,
97
+ style: StyleValue = None,
121
98
  key: Optional[str] = None,
122
99
  ) -> Element:
123
- """Display an image from a resource path or URL."""
124
- props = _filter_none(
125
- source=source or None,
126
- width=width,
127
- height=height,
128
- scale_type=scale_type,
129
- background_color=background_color,
130
- )
100
+ """Display an image from a resource path or URL.
101
+
102
+ Style properties: ``background_color``, plus common layout props.
103
+ """
104
+ props: Dict[str, Any] = {}
105
+ if source:
106
+ props["source"] = source
107
+ if scale_type is not None:
108
+ props["scale_type"] = scale_type
109
+ props.update(resolve_style(style))
131
110
  return Element("Image", props, [], key=key)
132
111
 
133
112
 
@@ -135,107 +114,238 @@ def Switch(
135
114
  *,
136
115
  value: bool = False,
137
116
  on_change: Optional[Callable[[bool], None]] = None,
117
+ style: StyleValue = None,
138
118
  key: Optional[str] = None,
139
119
  ) -> Element:
140
120
  """Create a toggle switch."""
141
121
  props: Dict[str, Any] = {"value": value}
142
122
  if on_change is not None:
143
123
  props["on_change"] = on_change
124
+ props.update(resolve_style(style))
144
125
  return Element("Switch", props, [], key=key)
145
126
 
146
127
 
147
128
  def ProgressBar(
148
129
  *,
149
130
  value: float = 0.0,
150
- background_color: Optional[str] = None,
131
+ style: StyleValue = None,
151
132
  key: Optional[str] = None,
152
133
  ) -> Element:
153
134
  """Show determinate progress (0.0 – 1.0)."""
154
- props = _filter_none(value=value, background_color=background_color)
135
+ props: Dict[str, Any] = {"value": value}
136
+ props.update(resolve_style(style))
155
137
  return Element("ProgressBar", props, [], key=key)
156
138
 
157
139
 
158
140
  def ActivityIndicator(
159
141
  *,
160
142
  animating: bool = True,
143
+ style: StyleValue = None,
161
144
  key: Optional[str] = None,
162
145
  ) -> Element:
163
146
  """Show an indeterminate loading spinner."""
164
- return Element("ActivityIndicator", {"animating": animating}, [], key=key)
147
+ props: Dict[str, Any] = {"animating": animating}
148
+ props.update(resolve_style(style))
149
+ return Element("ActivityIndicator", props, [], key=key)
165
150
 
166
151
 
167
152
  def WebView(
168
153
  *,
169
154
  url: str = "",
155
+ style: StyleValue = None,
170
156
  key: Optional[str] = None,
171
157
  ) -> Element:
172
158
  """Embed web content."""
173
159
  props: Dict[str, Any] = {}
174
160
  if url:
175
161
  props["url"] = url
162
+ props.update(resolve_style(style))
176
163
  return Element("WebView", props, [], key=key)
177
164
 
178
165
 
179
166
  def Spacer(
180
167
  *,
181
168
  size: Optional[float] = None,
169
+ flex: Optional[float] = None,
182
170
  key: Optional[str] = None,
183
171
  ) -> Element:
184
- """Insert empty space with an optional fixed size."""
185
- props = _filter_none(size=size)
172
+ """Insert empty space with an optional fixed size or flex weight."""
173
+ props: Dict[str, Any] = {}
174
+ if size is not None:
175
+ props["size"] = size
176
+ if flex is not None:
177
+ props["flex"] = flex
186
178
  return Element("Spacer", props, [], key=key)
187
179
 
188
180
 
189
- # ---------------------------------------------------------------------------
190
- # Container components
191
- # ---------------------------------------------------------------------------
181
+ def Slider(
182
+ *,
183
+ value: float = 0.0,
184
+ min_value: float = 0.0,
185
+ max_value: float = 1.0,
186
+ on_change: Optional[Callable[[float], None]] = None,
187
+ style: StyleValue = None,
188
+ key: Optional[str] = None,
189
+ ) -> Element:
190
+ """Continuous value slider."""
191
+ props: Dict[str, Any] = {
192
+ "value": value,
193
+ "min_value": min_value,
194
+ "max_value": max_value,
195
+ }
196
+ if on_change is not None:
197
+ props["on_change"] = on_change
198
+ props.update(resolve_style(style))
199
+ return Element("Slider", props, [], key=key)
192
200
 
193
- PaddingValue = Union[int, float, Dict[str, Union[int, float]]]
201
+
202
+ # ======================================================================
203
+ # Container components
204
+ # ======================================================================
194
205
 
195
206
 
196
207
  def Column(
197
208
  *children: Element,
198
- spacing: float = 0,
199
- padding: Optional[PaddingValue] = None,
200
- alignment: Optional[str] = None,
201
- background_color: Optional[str] = None,
202
- key: Optional[str] = None,
203
- ) -> Element:
204
- """Arrange children vertically."""
205
- props = _filter_none(
206
- spacing=spacing or None,
207
- padding=padding,
208
- alignment=alignment,
209
- background_color=background_color,
210
- )
209
+ style: StyleValue = None,
210
+ key: Optional[str] = None,
211
+ ) -> Element:
212
+ """Arrange children vertically.
213
+
214
+ Style properties: ``spacing``, ``padding``, ``align_items``,
215
+ ``justify_content``, ``background_color``, plus common layout props.
216
+
217
+ ``align_items`` controls cross-axis (horizontal) alignment:
218
+ ``"stretch"`` (default), ``"flex_start"``/``"leading"``,
219
+ ``"center"``, ``"flex_end"``/``"trailing"``.
220
+
221
+ ``justify_content`` controls main-axis (vertical) distribution:
222
+ ``"flex_start"`` (default), ``"center"``, ``"flex_end"``,
223
+ ``"space_between"``, ``"space_around"``, ``"space_evenly"``.
224
+ """
225
+ props: Dict[str, Any] = {}
226
+ props.update(resolve_style(style))
211
227
  return Element("Column", props, list(children), key=key)
212
228
 
213
229
 
214
230
  def Row(
215
231
  *children: Element,
216
- spacing: float = 0,
217
- padding: Optional[PaddingValue] = None,
218
- alignment: Optional[str] = None,
219
- background_color: Optional[str] = None,
220
- key: Optional[str] = None,
221
- ) -> Element:
222
- """Arrange children horizontally."""
223
- props = _filter_none(
224
- spacing=spacing or None,
225
- padding=padding,
226
- alignment=alignment,
227
- background_color=background_color,
228
- )
232
+ style: StyleValue = None,
233
+ key: Optional[str] = None,
234
+ ) -> Element:
235
+ """Arrange children horizontally.
236
+
237
+ Style properties: ``spacing``, ``padding``, ``align_items``,
238
+ ``justify_content``, ``background_color``, plus common layout props.
239
+
240
+ ``align_items`` controls cross-axis (vertical) alignment:
241
+ ``"stretch"`` (default), ``"flex_start"``/``"top"``,
242
+ ``"center"``, ``"flex_end"``/``"bottom"``.
243
+
244
+ ``justify_content`` controls main-axis (horizontal) distribution:
245
+ ``"flex_start"`` (default), ``"center"``, ``"flex_end"``,
246
+ ``"space_between"``, ``"space_around"``, ``"space_evenly"``.
247
+ """
248
+ props: Dict[str, Any] = {}
249
+ props.update(resolve_style(style))
229
250
  return Element("Row", props, list(children), key=key)
230
251
 
231
252
 
232
253
  def ScrollView(
233
254
  child: Optional[Element] = None,
234
255
  *,
235
- background_color: Optional[str] = None,
256
+ style: StyleValue = None,
236
257
  key: Optional[str] = None,
237
258
  ) -> Element:
238
259
  """Wrap a single child in a scrollable container."""
239
260
  children = [child] if child is not None else []
240
- props = _filter_none(background_color=background_color)
261
+ props: Dict[str, Any] = {}
262
+ props.update(resolve_style(style))
241
263
  return Element("ScrollView", props, children, key=key)
264
+
265
+
266
+ def View(
267
+ *children: Element,
268
+ style: StyleValue = None,
269
+ key: Optional[str] = None,
270
+ ) -> Element:
271
+ """Generic container view (``UIView`` / ``android.view.View``)."""
272
+ props: Dict[str, Any] = {}
273
+ props.update(resolve_style(style))
274
+ return Element("View", props, list(children), key=key)
275
+
276
+
277
+ def SafeAreaView(
278
+ *children: Element,
279
+ style: StyleValue = None,
280
+ key: Optional[str] = None,
281
+ ) -> Element:
282
+ """Container that respects safe area insets (notch, status bar)."""
283
+ props: Dict[str, Any] = {}
284
+ props.update(resolve_style(style))
285
+ return Element("SafeAreaView", props, list(children), key=key)
286
+
287
+
288
+ def Modal(
289
+ *children: Element,
290
+ visible: bool = False,
291
+ on_dismiss: Optional[Callable[[], None]] = None,
292
+ title: Optional[str] = None,
293
+ style: StyleValue = None,
294
+ key: Optional[str] = None,
295
+ ) -> Element:
296
+ """Overlay modal dialog.
297
+
298
+ The modal is shown when ``visible=True`` and hidden when ``False``.
299
+ """
300
+ props: Dict[str, Any] = {"visible": visible}
301
+ if on_dismiss is not None:
302
+ props["on_dismiss"] = on_dismiss
303
+ if title is not None:
304
+ props["title"] = title
305
+ props.update(resolve_style(style))
306
+ return Element("Modal", props, list(children), key=key)
307
+
308
+
309
+ def Pressable(
310
+ child: Optional[Element] = None,
311
+ *,
312
+ on_press: Optional[Callable[[], None]] = None,
313
+ on_long_press: Optional[Callable[[], None]] = None,
314
+ key: Optional[str] = None,
315
+ ) -> Element:
316
+ """Wrapper that adds press handling to any child element."""
317
+ props: Dict[str, Any] = {}
318
+ if on_press is not None:
319
+ props["on_press"] = on_press
320
+ if on_long_press is not None:
321
+ props["on_long_press"] = on_long_press
322
+ children = [child] if child is not None else []
323
+ return Element("Pressable", props, children, key=key)
324
+
325
+
326
+ def FlatList(
327
+ *,
328
+ data: Optional[List[Any]] = None,
329
+ render_item: Optional[Callable[[Any, int], Element]] = None,
330
+ key_extractor: Optional[Callable[[Any, int], str]] = None,
331
+ separator_height: float = 0,
332
+ style: StyleValue = None,
333
+ key: Optional[str] = None,
334
+ ) -> Element:
335
+ """Scrollable list that renders items from *data* using *render_item*.
336
+
337
+ Each item is rendered by calling ``render_item(item, index)``. If
338
+ ``key_extractor`` is provided, it is called as ``key_extractor(item, index)``
339
+ to produce a stable key for each child element.
340
+ """
341
+ items: List[Element] = []
342
+ for i, item in enumerate(data or []):
343
+ el = render_item(item, i) if render_item else Text(str(item))
344
+ if key_extractor is not None:
345
+ el = Element(el.type, el.props, el.children, key=key_extractor(item, i))
346
+ items.append(el)
347
+
348
+ inner = Column(*items, style={"spacing": separator_height} if separator_height else None)
349
+ sv_props: Dict[str, Any] = {}
350
+ sv_props.update(resolve_style(style))
351
+ return Element("ScrollView", sv_props, [inner], key=key)
pythonnative/element.py CHANGED
@@ -1,23 +1,28 @@
1
1
  """Lightweight element descriptors for the virtual view tree.
2
2
 
3
3
  An Element is an immutable description of a UI node — analogous to a React
4
- element. It captures a type name, a props dictionary, and an ordered list
5
- of children without creating any native platform objects. The reconciler
6
- consumes these trees to determine what native views must be created,
7
- updated, or removed.
4
+ element. It captures a type (name string **or** component function), a props
5
+ dictionary, and an ordered list of children without creating any native
6
+ platform objects. The reconciler consumes these trees to determine what
7
+ native views must be created, updated, or removed.
8
8
  """
9
9
 
10
- from typing import Any, Dict, List, Optional
10
+ from typing import Any, Dict, List, Optional, Union
11
11
 
12
12
 
13
13
  class Element:
14
- """Immutable description of a single UI node."""
14
+ """Immutable description of a single UI node.
15
+
16
+ ``type_name`` may be a *string* (e.g. ``"Text"``) for built-in native
17
+ elements or a *callable* for function components decorated with
18
+ :func:`~pythonnative.hooks.component`.
19
+ """
15
20
 
16
21
  __slots__ = ("type", "props", "children", "key")
17
22
 
18
23
  def __init__(
19
24
  self,
20
- type_name: str,
25
+ type_name: Union[str, Any],
21
26
  props: Dict[str, Any],
22
27
  children: List["Element"],
23
28
  key: Optional[str] = None,
@@ -28,7 +33,8 @@ class Element:
28
33
  self.key = key
29
34
 
30
35
  def __repr__(self) -> str:
31
- return f"Element({self.type!r}, props={set(self.props)}, children={len(self.children)})"
36
+ t = self.type if isinstance(self.type, str) else getattr(self.type, "__name__", repr(self.type))
37
+ return f"Element({t!r}, props={set(self.props)}, children={len(self.children)})"
32
38
 
33
39
  def __eq__(self, other: object) -> bool:
34
40
  if not isinstance(other, Element):