pythonnative 0.32.0__py3-none-any.whl → 0.34.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 +27 -15
- pythonnative/animated.py +7 -13
- pythonnative/cli/pn.py +1 -2
- pythonnative/component.py +255 -0
- pythonnative/components/__init__.py +106 -0
- pythonnative/components/_base.py +132 -0
- pythonnative/components/controls.py +515 -0
- pythonnative/components/layout.py +459 -0
- pythonnative/components/lists.py +809 -0
- pythonnative/components/media.py +204 -0
- pythonnative/components/overlays.py +107 -0
- pythonnative/components/pressable.py +252 -0
- pythonnative/components/structural.py +167 -0
- pythonnative/components/text.py +291 -0
- pythonnative/diagnostics.py +1 -1
- pythonnative/element.py +108 -29
- pythonnative/gestures.py +1 -1
- pythonnative/hooks.py +366 -671
- pythonnative/hosts/__init__.py +85 -0
- pythonnative/hosts/android.py +269 -0
- pythonnative/hosts/base.py +665 -0
- pythonnative/hosts/desktop.py +107 -0
- pythonnative/hosts/ios.py +404 -0
- pythonnative/hot_reload.py +16 -24
- pythonnative/layout.py +372 -66
- pythonnative/native_modules/__init__.py +23 -0
- pythonnative/native_modules/net_info.py +5 -1
- pythonnative/native_views/__init__.py +1 -1
- pythonnative/native_views/android.py +50 -6
- pythonnative/native_views/base.py +64 -1
- pythonnative/native_views/desktop.py +11 -3
- pythonnative/native_views/ios.py +72 -12
- pythonnative/navigation/__init__.py +102 -0
- pythonnative/navigation/container.py +154 -0
- pythonnative/navigation/handle.py +573 -0
- pythonnative/navigation/hooks.py +94 -0
- pythonnative/navigation/host.py +58 -0
- pythonnative/navigation/linking.py +200 -0
- pythonnative/navigation/navigators.py +637 -0
- pythonnative/navigation/screen.py +149 -0
- pythonnative/navigation/state.py +248 -0
- pythonnative/net.py +4 -0
- pythonnative/platform_metrics.py +1 -1
- pythonnative/preview.py +21 -19
- pythonnative/project/android.py +7 -1
- pythonnative/project/doctor.py +1 -1
- pythonnative/project/ios.py +1 -0
- pythonnative/project/runtime_assets.py +1 -1
- pythonnative/reconciler/__init__.py +29 -0
- pythonnative/reconciler/boundaries.py +365 -0
- pythonnative/reconciler/children.py +88 -0
- pythonnative/reconciler/core.py +1153 -0
- pythonnative/reconciler/layout_pass.py +359 -0
- pythonnative/reconciler/vnode.py +266 -0
- pythonnative/scheduler.py +159 -0
- pythonnative/sdk/_components.py +2 -4
- pythonnative/style.py +95 -13
- pythonnative/suspense.py +8 -12
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +85 -1
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +6 -7
- pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +1 -1
- pythonnative/templates/android_template/app/src/main/res/values/strings.xml +3 -1
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +33 -0
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +1 -1
- pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -10
- pythonnative/testing/__init__.py +53 -0
- pythonnative/testing/backend.py +276 -0
- pythonnative/testing/harness.py +391 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/METADATA +2 -21
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/RECORD +74 -43
- pythonnative/components.py +0 -2856
- pythonnative/navigation.py +0 -1031
- pythonnative/reconciler.py +0 -2298
- pythonnative/screen.py +0 -2068
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/top_level.txt +0 -0
|
@@ -11,7 +11,8 @@ import UIKit
|
|
|
11
11
|
|
|
12
12
|
class ViewController: UIViewController {
|
|
13
13
|
// Optional keys for dynamic screen navigation. Push navigation sets
|
|
14
|
-
// these before presenting the controller; nil means
|
|
14
|
+
// these before presenting the controller; nil means the app's entry
|
|
15
|
+
// module (Info.plist `PNEntryModule`, default "app.main").
|
|
15
16
|
@objc dynamic var requestedScreenPath: String? = nil
|
|
16
17
|
@objc dynamic var requestedScreenArgsJSON: String? = nil
|
|
17
18
|
|
|
@@ -32,21 +33,22 @@ class ViewController: UIViewController {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
// PythonNative's convention is "import the module and grab its
|
|
35
|
-
// top-level `App` attribute"
|
|
36
|
-
//
|
|
37
|
-
// `
|
|
38
|
-
//
|
|
39
|
-
let
|
|
36
|
+
// top-level `App` attribute". The entry module comes from
|
|
37
|
+
// `app.entry_point` in pythonnative.toml (written to Info.plist as
|
|
38
|
+
// `PNEntryModule` by `pn build`); push navigation overrides it via
|
|
39
|
+
// `requestedScreenPath`.
|
|
40
|
+
let entryModule = Bundle.main.object(forInfoDictionaryKey: "PNEntryModule") as? String
|
|
41
|
+
let screenPath: String = requestedScreenPath ?? entryModule ?? "app.main"
|
|
40
42
|
let addr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
|
|
41
43
|
do {
|
|
42
44
|
let screen: PyRef
|
|
43
45
|
if let argsJSON = requestedScreenArgsJSON {
|
|
44
46
|
screen = try PythonRuntime.shared.call(
|
|
45
|
-
module: "pythonnative.
|
|
47
|
+
module: "pythonnative.hosts", function: "create_screen", screenPath, addr, argsJSON
|
|
46
48
|
)
|
|
47
49
|
} else {
|
|
48
50
|
screen = try PythonRuntime.shared.call(
|
|
49
|
-
module: "pythonnative.
|
|
51
|
+
module: "pythonnative.hosts", function: "create_screen", screenPath, addr
|
|
50
52
|
)
|
|
51
53
|
}
|
|
52
54
|
self.screen = screen
|
|
@@ -69,7 +71,7 @@ class ViewController: UIViewController {
|
|
|
69
71
|
guard screen != nil else { return }
|
|
70
72
|
let addr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
|
|
71
73
|
PythonRuntime.shared.notify(
|
|
72
|
-
module: "pythonnative.
|
|
74
|
+
module: "pythonnative.hosts", function: "forward_lifecycle", addr, event
|
|
73
75
|
)
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -119,7 +121,7 @@ class ViewController: UIViewController {
|
|
|
119
121
|
if screen != nil {
|
|
120
122
|
let addr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
|
|
121
123
|
PythonRuntime.shared.notify(
|
|
122
|
-
module: "pythonnative.
|
|
124
|
+
module: "pythonnative.hosts", function: "forward_lifecycle", addr, "on_destroy"
|
|
123
125
|
)
|
|
124
126
|
}
|
|
125
127
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Test utilities: render components without a device.
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
import pythonnative as pn
|
|
5
|
+
from pythonnative.testing import render, render_hook
|
|
6
|
+
|
|
7
|
+
@pn.component
|
|
8
|
+
def Counter():
|
|
9
|
+
count, set_count = pn.use_state(0)
|
|
10
|
+
return pn.Column(
|
|
11
|
+
pn.Text(f"Count: {count}"),
|
|
12
|
+
pn.Button("+", on_press=lambda: set_count(count + 1)),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def test_counter_increments():
|
|
16
|
+
result = render(Counter())
|
|
17
|
+
result.press(result.get_by_text("+"))
|
|
18
|
+
assert result.get_by_text("Count: 1")
|
|
19
|
+
|
|
20
|
+
def test_use_state():
|
|
21
|
+
hook = render_hook(lambda: pn.use_state("a"))
|
|
22
|
+
hook.act(lambda: hook.current[1]("b"))
|
|
23
|
+
assert hook.current[0] == "b"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- [`render`][pythonnative.testing.render] mounts an element into a
|
|
27
|
+
[`FakeBackend`][pythonnative.testing.FakeBackend] and returns a
|
|
28
|
+
[`RenderResult`][pythonnative.testing.RenderResult] with Testing
|
|
29
|
+
Library-style queries (``get_by_text``, ``get_by_test_id``,
|
|
30
|
+
``get_by_label``, ``get_by_type``) and event helpers (``press``,
|
|
31
|
+
``fire``, ``change_text``, ``back``).
|
|
32
|
+
- [`render_hook`][pythonnative.testing.render_hook] runs a hook in a
|
|
33
|
+
throwaway component.
|
|
34
|
+
- [`settle`][pythonnative.testing.settle] pumps the framework loop so
|
|
35
|
+
async work (resources, queries, transitions) completes.
|
|
36
|
+
- [`FakeHost`][pythonnative.testing.FakeHost] stands in for a native
|
|
37
|
+
screen host so root stack navigators can be tested.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from .backend import DEFAULT_INTRINSIC, FakeBackend, FakeView
|
|
41
|
+
from .harness import FakeHost, HookResult, RenderResult, render, render_hook, settle
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"DEFAULT_INTRINSIC",
|
|
45
|
+
"FakeBackend",
|
|
46
|
+
"FakeHost",
|
|
47
|
+
"FakeView",
|
|
48
|
+
"HookResult",
|
|
49
|
+
"RenderResult",
|
|
50
|
+
"render",
|
|
51
|
+
"render_hook",
|
|
52
|
+
"settle",
|
|
53
|
+
]
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""In-memory backend implementing the batched mutation protocol.
|
|
2
|
+
|
|
3
|
+
[`FakeBackend`][pythonnative.testing.FakeBackend] speaks the same
|
|
4
|
+
protocol as the platform registries (``apply_mutations``,
|
|
5
|
+
``resolve_view``, ``measure_intrinsic``, ``command``, plus the animation
|
|
6
|
+
hooks) while keeping a real tree of
|
|
7
|
+
[`FakeView`][pythonnative.testing.FakeView] objects, so tests can assert
|
|
8
|
+
on structure, props, and frames without a device.
|
|
9
|
+
|
|
10
|
+
Unlike the production registries (which isolate per-op failures so a
|
|
11
|
+
bad prop can't desync a device), the fake **raises** on malformed
|
|
12
|
+
transactions (unknown tags, double destroys, inserting into a destroyed
|
|
13
|
+
parent) so reconciler bugs fail tests loudly.
|
|
14
|
+
|
|
15
|
+
Recorded op shapes (in ``FakeBackend.ops``):
|
|
16
|
+
|
|
17
|
+
- ``("create", type_name, view.id)``
|
|
18
|
+
- ``("update", type_name, view.id, tuple(sorted(changed_keys)))``
|
|
19
|
+
- ``("insert_child", parent.id, child.id, index)``
|
|
20
|
+
- ``("destroy", view.id)``
|
|
21
|
+
- ``("set_frame", view.id, x, y, w, h)``
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple
|
|
27
|
+
|
|
28
|
+
from ..mutations import CreateOp, DestroyOp, InsertOp, Mutation, SetFrameOp, UpdateOp
|
|
29
|
+
|
|
30
|
+
__all__ = ["DEFAULT_INTRINSIC", "FakeBackend", "FakeView"]
|
|
31
|
+
|
|
32
|
+
DEFAULT_INTRINSIC: Dict[str, Tuple[float, float]] = {
|
|
33
|
+
"Text": (60.0, 16.0),
|
|
34
|
+
"Button": (80.0, 32.0),
|
|
35
|
+
"Image": (40.0, 40.0),
|
|
36
|
+
"TextInput": (120.0, 32.0),
|
|
37
|
+
"TabBar": (320.0, 49.0),
|
|
38
|
+
}
|
|
39
|
+
"""Intrinsic sizes reported for content-sized leaves (what platform measure hooks would return)."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FakeView:
|
|
43
|
+
"""Simulated native view: type, props, children, and last frame.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
tag: The reconciler-assigned tag (use with ``fire`` / events).
|
|
47
|
+
type_name: Native type, e.g. ``"Text"``.
|
|
48
|
+
props: Native-safe props (event callbacks are stripped; they
|
|
49
|
+
live in the event registry keyed by ``tag``).
|
|
50
|
+
children: Child views in order.
|
|
51
|
+
frame: ``(x, y, width, height)`` from the last layout pass.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
_next_id = 0
|
|
55
|
+
|
|
56
|
+
def __init__(self, tag: int, type_name: str, props: Dict[str, Any]) -> None:
|
|
57
|
+
FakeView._next_id += 1
|
|
58
|
+
self.id = FakeView._next_id
|
|
59
|
+
self.tag = tag
|
|
60
|
+
self.type_name = type_name
|
|
61
|
+
self.props: Dict[str, Any] = dict(props)
|
|
62
|
+
self.children: List[FakeView] = []
|
|
63
|
+
self.parent: Optional[FakeView] = None
|
|
64
|
+
self.frame: Tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0)
|
|
65
|
+
self.destroyed = False
|
|
66
|
+
|
|
67
|
+
def __repr__(self) -> str:
|
|
68
|
+
label = self.text
|
|
69
|
+
suffix = f" {label!r}" if label else ""
|
|
70
|
+
return f"<{self.type_name} tag={self.tag}{suffix}>"
|
|
71
|
+
|
|
72
|
+
# -- content --------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def text(self) -> Optional[str]:
|
|
76
|
+
"""Visible text for text-bearing views (``Text.text``, ``Button.title``, ``TextInput.value``)."""
|
|
77
|
+
for key in ("text", "title", "value", "placeholder"):
|
|
78
|
+
value = self.props.get(key)
|
|
79
|
+
if isinstance(value, str):
|
|
80
|
+
return value
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def hidden(self) -> bool:
|
|
85
|
+
"""Whether this view is removed from layout (``display: "none"``)."""
|
|
86
|
+
return self.props.get("display") == "none"
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def test_id(self) -> Optional[str]:
|
|
90
|
+
"""The ``test_id`` prop, if set."""
|
|
91
|
+
return self.props.get("test_id")
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def label(self) -> Optional[str]:
|
|
95
|
+
"""The ``accessibility_label`` prop, if set."""
|
|
96
|
+
return self.props.get("accessibility_label")
|
|
97
|
+
|
|
98
|
+
# -- traversal ------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def walk(self, *, include_hidden: bool = True) -> Iterator["FakeView"]:
|
|
101
|
+
"""Yield this view and every descendant, depth-first.
|
|
102
|
+
|
|
103
|
+
With ``include_hidden=False`` subtrees under a ``display: "none"``
|
|
104
|
+
view are skipped (what a user can see).
|
|
105
|
+
"""
|
|
106
|
+
if not include_hidden and self.hidden:
|
|
107
|
+
return
|
|
108
|
+
yield self
|
|
109
|
+
for child in self.children:
|
|
110
|
+
yield from child.walk(include_hidden=include_hidden)
|
|
111
|
+
|
|
112
|
+
def find_all(self, predicate_or_type: Any) -> List["FakeView"]:
|
|
113
|
+
"""Every view in this subtree matching a type name or predicate."""
|
|
114
|
+
if isinstance(predicate_or_type, str):
|
|
115
|
+
wanted = predicate_or_type
|
|
116
|
+
|
|
117
|
+
def predicate(v: "FakeView") -> bool:
|
|
118
|
+
return v.type_name == wanted
|
|
119
|
+
|
|
120
|
+
else:
|
|
121
|
+
predicate = predicate_or_type
|
|
122
|
+
return [v for v in self.walk() if predicate(v)]
|
|
123
|
+
|
|
124
|
+
def find_first(self, predicate_or_type: Any) -> Optional["FakeView"]:
|
|
125
|
+
"""Return the first view in this subtree matching a type name or predicate, or ``None``."""
|
|
126
|
+
found = self.find_all(predicate_or_type)
|
|
127
|
+
return found[0] if found else None
|
|
128
|
+
|
|
129
|
+
def dump(self, indent: int = 0) -> str:
|
|
130
|
+
"""Indented, human-readable subtree (for failing-test output)."""
|
|
131
|
+
line = " " * indent + repr(self)
|
|
132
|
+
x, y, w, h = self.frame
|
|
133
|
+
if w or h:
|
|
134
|
+
line += f" @({x:g},{y:g} {w:g}x{h:g})"
|
|
135
|
+
return "\n".join([line, *(c.dump(indent + 2) for c in self.children)])
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class FakeBackend:
|
|
139
|
+
"""Tag-table backend recording one tuple per applied mutation.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
intrinsic: Override the intrinsic sizes used by
|
|
143
|
+
``measure_intrinsic`` (defaults to
|
|
144
|
+
[`DEFAULT_INTRINSIC`][pythonnative.testing.backend.DEFAULT_INTRINSIC]).
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(self, intrinsic: Optional[Dict[str, Tuple[float, float]]] = None) -> None:
|
|
148
|
+
self.intrinsic = dict(DEFAULT_INTRINSIC if intrinsic is None else intrinsic)
|
|
149
|
+
self.views: Dict[int, FakeView] = {}
|
|
150
|
+
self.ops: List[Any] = []
|
|
151
|
+
self.batches: List[List[Any]] = []
|
|
152
|
+
self.measure_calls: List[int] = []
|
|
153
|
+
self.commands: List[Tuple[int, str, Dict[str, Any]]] = []
|
|
154
|
+
self.animated: List[Tuple[int, str, Any]] = []
|
|
155
|
+
self.last_create_props: Dict[str, Any] = {}
|
|
156
|
+
self.last_update_changes: Dict[str, Any] = {}
|
|
157
|
+
|
|
158
|
+
# ------------------------------------------------------------------
|
|
159
|
+
# Commit channel
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def apply_mutations(self, ops: Sequence[Mutation]) -> None:
|
|
163
|
+
"""Apply one committed batch to the view tree, recording each op in ``ops`` and ``batches``.
|
|
164
|
+
|
|
165
|
+
Raises ``AssertionError`` on malformed transactions (unknown tags, double creates or destroys).
|
|
166
|
+
"""
|
|
167
|
+
batch: List[Any] = []
|
|
168
|
+
for op in ops:
|
|
169
|
+
recorded = self._apply_one(op)
|
|
170
|
+
self.ops.append(recorded)
|
|
171
|
+
batch.append(recorded)
|
|
172
|
+
self.batches.append(batch)
|
|
173
|
+
|
|
174
|
+
def _apply_one(self, op: Mutation) -> Tuple[Any, ...]:
|
|
175
|
+
if isinstance(op, CreateOp):
|
|
176
|
+
if op.tag in self.views:
|
|
177
|
+
raise AssertionError(f"create: tag {op.tag} already registered")
|
|
178
|
+
view = FakeView(op.tag, op.type_name, op.props)
|
|
179
|
+
self.views[op.tag] = view
|
|
180
|
+
self.last_create_props = dict(op.props)
|
|
181
|
+
return ("create", op.type_name, view.id)
|
|
182
|
+
|
|
183
|
+
if isinstance(op, UpdateOp):
|
|
184
|
+
view = self._require(op.tag, "update")
|
|
185
|
+
view.props.update(op.changed_props)
|
|
186
|
+
self.last_update_changes = dict(op.changed_props)
|
|
187
|
+
return ("update", view.type_name, view.id, tuple(sorted(op.changed_props)))
|
|
188
|
+
|
|
189
|
+
if isinstance(op, InsertOp):
|
|
190
|
+
parent = self._require(op.parent_tag, "insert_child")
|
|
191
|
+
child = self._require(op.child_tag, "insert_child")
|
|
192
|
+
if child.parent is not None:
|
|
193
|
+
child.parent.children.remove(child)
|
|
194
|
+
child.parent = None
|
|
195
|
+
index = max(0, min(op.index, len(parent.children)))
|
|
196
|
+
parent.children.insert(index, child)
|
|
197
|
+
child.parent = parent
|
|
198
|
+
return ("insert_child", parent.id, child.id, index)
|
|
199
|
+
|
|
200
|
+
if isinstance(op, DestroyOp):
|
|
201
|
+
view = self.views.pop(op.tag, None)
|
|
202
|
+
if view is None:
|
|
203
|
+
raise AssertionError(f"destroy: unknown tag {op.tag}")
|
|
204
|
+
if view.parent is not None:
|
|
205
|
+
view.parent.children.remove(view)
|
|
206
|
+
view.parent = None
|
|
207
|
+
view.destroyed = True
|
|
208
|
+
return ("destroy", view.id)
|
|
209
|
+
|
|
210
|
+
if isinstance(op, SetFrameOp):
|
|
211
|
+
view = self._require(op.tag, "set_frame")
|
|
212
|
+
view.frame = (op.x, op.y, op.width, op.height)
|
|
213
|
+
return ("set_frame", view.id, op.x, op.y, op.width, op.height)
|
|
214
|
+
|
|
215
|
+
raise AssertionError(f"unknown mutation op: {op!r}")
|
|
216
|
+
|
|
217
|
+
def _require(self, tag: int, op_name: str) -> FakeView:
|
|
218
|
+
view = self.views.get(tag)
|
|
219
|
+
if view is None:
|
|
220
|
+
raise AssertionError(f"{op_name}: unknown tag {tag}")
|
|
221
|
+
return view
|
|
222
|
+
|
|
223
|
+
# ------------------------------------------------------------------
|
|
224
|
+
# Imperative escape hatches
|
|
225
|
+
# ------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
def resolve_view(self, tag: int) -> Optional[FakeView]:
|
|
228
|
+
"""Return the live view registered under ``tag``, or ``None``."""
|
|
229
|
+
return self.views.get(tag)
|
|
230
|
+
|
|
231
|
+
def measure_intrinsic(self, tag: int, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
232
|
+
"""Return the configured intrinsic size for the view's type and record the call in ``measure_calls``.
|
|
233
|
+
|
|
234
|
+
Unknown tags and types without an entry measure as ``(0.0, 0.0)``; the constraints are ignored.
|
|
235
|
+
"""
|
|
236
|
+
view = self.views.get(tag)
|
|
237
|
+
if view is None:
|
|
238
|
+
return (0.0, 0.0)
|
|
239
|
+
self.measure_calls.append(view.id)
|
|
240
|
+
return self.intrinsic.get(view.type_name, (0.0, 0.0))
|
|
241
|
+
|
|
242
|
+
def command(self, tag: int, name: str, args: Optional[Dict[str, Any]] = None) -> Any:
|
|
243
|
+
"""Record an imperative view command in ``commands`` and return ``None``."""
|
|
244
|
+
self.commands.append((tag, name, dict(args or {})))
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
def set_animated_property(self, tag: int, prop_name: str, value: Any) -> None:
|
|
248
|
+
"""Record an animated property write in ``animated`` without touching ``props``."""
|
|
249
|
+
self.animated.append((tag, prop_name, value))
|
|
250
|
+
|
|
251
|
+
def start_animation(self, tag: int, anim_id: int, prop_name: str, spec: Dict[str, Any]) -> bool:
|
|
252
|
+
"""Decline native animation (return ``False``) so animations run through the Python driver."""
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
def cancel_animation(self, tag: int, anim_id: int) -> Any:
|
|
256
|
+
"""Do nothing; the fake never starts native animations."""
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
# ------------------------------------------------------------------
|
|
260
|
+
# Diagnostics
|
|
261
|
+
# ------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
def live_view_count(self) -> int:
|
|
264
|
+
"""Return how many views are currently registered (created and not yet destroyed)."""
|
|
265
|
+
return len(self.views)
|
|
266
|
+
|
|
267
|
+
def ops_of(self, kind: str) -> List[Any]:
|
|
268
|
+
"""Every recorded op tuple whose first element is ``kind``."""
|
|
269
|
+
return [op for op in self.ops if op[0] == kind]
|
|
270
|
+
|
|
271
|
+
def detached_views(self, type_name: Optional[str] = None) -> List[FakeView]:
|
|
272
|
+
"""Live views never inserted into a parent (``Portal`` overlays, the root)."""
|
|
273
|
+
out = [v for v in self.views.values() if v.parent is None]
|
|
274
|
+
if type_name is not None:
|
|
275
|
+
out = [v for v in out if v.type_name == type_name]
|
|
276
|
+
return out
|