flet 0.70.0.dev5776__py3-none-any.whl → 0.70.0.dev6145__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.

Potentially problematic release.


This version of flet might be problematic. Click here for more details.

Files changed (46) hide show
  1. flet/__init__.py +32 -4
  2. flet/components/__init__.py +0 -0
  3. flet/components/component.py +346 -0
  4. flet/components/component_decorator.py +24 -0
  5. flet/components/component_owned.py +22 -0
  6. flet/components/hooks/__init__.py +0 -0
  7. flet/components/hooks/hook.py +12 -0
  8. flet/components/hooks/use_callback.py +28 -0
  9. flet/components/hooks/use_context.py +91 -0
  10. flet/components/hooks/use_effect.py +104 -0
  11. flet/components/hooks/use_memo.py +52 -0
  12. flet/components/hooks/use_state.py +58 -0
  13. flet/components/memo.py +34 -0
  14. flet/components/observable.py +269 -0
  15. flet/components/public_utils.py +10 -0
  16. flet/components/utils.py +85 -0
  17. flet/controls/base_control.py +34 -10
  18. flet/controls/base_page.py +44 -40
  19. flet/controls/context.py +22 -1
  20. flet/controls/control_event.py +19 -2
  21. flet/controls/core/column.py +5 -0
  22. flet/controls/core/drag_target.py +17 -8
  23. flet/controls/core/row.py +5 -0
  24. flet/controls/core/view.py +6 -6
  25. flet/controls/cupertino/cupertino_icons.py +1 -1
  26. flet/controls/id_counter.py +24 -0
  27. flet/controls/material/divider.py +6 -0
  28. flet/controls/material/icons.py +1 -1
  29. flet/controls/material/textfield.py +10 -1
  30. flet/controls/material/vertical_divider.py +6 -0
  31. flet/controls/object_patch.py +434 -197
  32. flet/controls/page.py +203 -84
  33. flet/controls/services/haptic_feedback.py +0 -3
  34. flet/controls/services/shake_detector.py +0 -3
  35. flet/messaging/flet_socket_server.py +13 -6
  36. flet/messaging/session.py +103 -10
  37. flet/{controls/session_storage.py → messaging/session_store.py} +2 -2
  38. flet/version.py +1 -1
  39. {flet-0.70.0.dev5776.dist-info → flet-0.70.0.dev6145.dist-info}/METADATA +4 -5
  40. {flet-0.70.0.dev5776.dist-info → flet-0.70.0.dev6145.dist-info}/RECORD +43 -30
  41. flet/controls/cache.py +0 -87
  42. flet/controls/control_id.py +0 -22
  43. flet/controls/core/state_view.py +0 -60
  44. {flet-0.70.0.dev5776.dist-info → flet-0.70.0.dev6145.dist-info}/WHEEL +0 -0
  45. {flet-0.70.0.dev5776.dist-info → flet-0.70.0.dev6145.dist-info}/entry_points.txt +0 -0
  46. {flet-0.70.0.dev5776.dist-info → flet-0.70.0.dev6145.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,104 @@
1
+ import asyncio
2
+ from collections.abc import Awaitable, Callable, Sequence
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from flet.components.hooks.hook import Hook
7
+ from flet.components.utils import current_component
8
+
9
+
10
+ @dataclass
11
+ class EffectHook(Hook):
12
+ setup: Callable[[], Any | Awaitable[Any]]
13
+ cleanup: Callable[[], Any | Awaitable[Any]] | None = None
14
+ deps: list[Any] | None = None
15
+ prev_deps: list[Any] | None = None
16
+
17
+ # runtime
18
+ _setup_task: asyncio.Task | None = None # last scheduled setup task
19
+ _cleanup_task: asyncio.Task | None = None # last scheduled cleanup task
20
+
21
+ def cancel(self):
22
+ if self._setup_task and not self._setup_task.done():
23
+ self._setup_task.cancel()
24
+ if self._cleanup_task and not self._cleanup_task.done():
25
+ self._cleanup_task.cancel()
26
+
27
+
28
+ def use_effect(
29
+ setup: Callable[[], Any | Awaitable[Any]],
30
+ dependencies: Sequence[Any] | None = None,
31
+ cleanup: Callable[[], Any | Awaitable[Any]] | None = None,
32
+ ):
33
+ """
34
+ Perform side effects in function components.
35
+
36
+ Args:
37
+ setup: A function that performs the side effect. It may optionally return
38
+ a cleanup function.
39
+ dependencies: If present, the effect is only re-run when one of the dependencies
40
+ has changed. If absent, the effect is only run on initial render.
41
+ cleanup: An optional function that cleans up after the effect. It is run
42
+ before the effect is re-run, and when the component unmounts.
43
+ """
44
+ component = current_component()
45
+ deps = list(dependencies) if dependencies is not None else None
46
+
47
+ # get or create effect hook
48
+ hook = component.use_hook(
49
+ lambda: EffectHook(
50
+ component,
51
+ setup=setup,
52
+ deps=deps,
53
+ cleanup=cleanup,
54
+ )
55
+ )
56
+
57
+ # update effect hook
58
+ hook.setup = setup
59
+ hook.prev_deps = hook.deps
60
+ hook.deps = deps
61
+ hook.cleanup = cleanup
62
+
63
+
64
+ def on_mounted(fn: Callable[[], Any | Awaitable[Any]]) -> None:
65
+ """
66
+ Run exactly once after the component mounts.
67
+
68
+ Args:
69
+ fn: A function to run after the component mounts.
70
+ """
71
+ use_effect(fn, dependencies=[])
72
+
73
+
74
+ def on_unmounted(fn: Callable[[], Any | Awaitable[Any]]) -> None:
75
+ """
76
+ Run exactly once when the component unmounts.
77
+
78
+ Args:
79
+ fn: A function to run when the component unmounts.
80
+ """
81
+ # No-op setup; only need cleanup to fire on unmount
82
+ use_effect(lambda: None, dependencies=[], cleanup=fn)
83
+
84
+
85
+ def on_updated(
86
+ fn: Callable[[], Any | Awaitable[Any]], dependencies: Sequence[Any] | None = None
87
+ ) -> None:
88
+ """
89
+ Run after each post-mount render (or when dependencies change).
90
+ With dependencies=None this fires every update; with dependencies=[...] only
91
+ on changes.
92
+
93
+ Args:
94
+ fn: A function to run after each post-mount render (or when dependencies
95
+ change).
96
+ dependencies: If present, fn is only run when one of the dependencies has
97
+ changed. If absent, fn is run after every render.
98
+ """
99
+ use_effect(fn, dependencies=dependencies)
100
+
101
+
102
+ on_mounted = on_mounted # alias
103
+ on_unmounted = on_unmounted # alias
104
+ on_updated = on_updated # alias
@@ -0,0 +1,52 @@
1
+ from collections.abc import Sequence
2
+ from dataclasses import dataclass
3
+ from typing import Any, Callable, Generic, TypeVar
4
+
5
+ from flet.components.hooks.hook import Hook
6
+ from flet.components.utils import current_component, shallow_compare_args
7
+
8
+ MemoValueT = TypeVar("MemoValueT")
9
+
10
+
11
+ @dataclass
12
+ class MemoHook(Hook, Generic[MemoValueT]):
13
+ value: MemoValueT | None = None
14
+ prev_deps: list[Any] | None = None
15
+
16
+
17
+ def use_memo(
18
+ calculate_value: Callable[[], MemoValueT], dependencies: Sequence[Any] | None = None
19
+ ) -> MemoValueT:
20
+ """
21
+ Memoize a computed value between renders.
22
+
23
+ Args:
24
+ calculate_value: A function that computes the value to be memoized.
25
+ dependencies: If present, the value is only recomputed when one of
26
+ the dependencies has changed. If absent, the value is only computed
27
+ on initial render.
28
+
29
+ Returns:
30
+ A memoized value whose identity is stable between renders.
31
+ """
32
+ component = current_component()
33
+
34
+ def _create() -> MemoHook[MemoValueT]:
35
+ h = MemoHook[MemoValueT](component)
36
+ # If deps is None we recompute every render, so no need to preset prev_deps.
37
+ if dependencies is None:
38
+ h.value = calculate_value()
39
+ return h
40
+
41
+ hook = component.use_hook(_create) # type: MemoHook[MemoValueT]
42
+
43
+ if dependencies is None:
44
+ # Always recompute
45
+ hook.value = calculate_value()
46
+ return hook.value # type: ignore[return-value]
47
+
48
+ if hook.prev_deps is None or not shallow_compare_args(hook.prev_deps, dependencies):
49
+ hook.value = calculate_value()
50
+ hook.prev_deps = list(dependencies)
51
+
52
+ return hook.value # type: ignore[return-value]
@@ -0,0 +1,58 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass
3
+ from typing import Any, TypeVar
4
+
5
+ from flet.components.hooks.hook import Hook
6
+ from flet.components.observable import Observable, ObservableSubscription
7
+ from flet.components.utils import current_component
8
+
9
+
10
+ @dataclass
11
+ class StateHook(Hook):
12
+ value: Any
13
+ subscription: ObservableSubscription | None = None
14
+ version: int = 0
15
+
16
+
17
+ StateT = TypeVar("StateT")
18
+
19
+
20
+ def use_state(
21
+ initial: StateT | Callable[[], StateT],
22
+ ) -> tuple[StateT, Callable[[StateT], None]]:
23
+ """
24
+ Add state to function components.
25
+
26
+ Args:
27
+ initial: Initial state value or a function that returns the initial state value.
28
+
29
+ Returns:
30
+ A tuple of the current state value and a function to update it.
31
+ """
32
+ component = current_component()
33
+ hook = component.use_hook(
34
+ lambda: StateHook(
35
+ component,
36
+ initial() if callable(initial) else initial,
37
+ )
38
+ )
39
+
40
+ def update_subscription(hook: StateHook):
41
+ if hook.subscription:
42
+ component._detach_observable_subscription(hook.subscription)
43
+ hook.subscription = None
44
+ if isinstance(hook.value, Observable):
45
+ hook.subscription = component._attach_observable_subscription(hook.value)
46
+
47
+ update_subscription(hook)
48
+
49
+ def set_state(new_value: Any):
50
+ # shallow equality; swap to "is" or custom comparator if needed
51
+ if new_value != hook.value:
52
+ hook.value = new_value
53
+ update_subscription(hook)
54
+ hook.version += 1
55
+ if hook.component:
56
+ hook.component._schedule_update()
57
+
58
+ return hook.value, set_state
@@ -0,0 +1,34 @@
1
+ from flet.components.utils import current_renderer
2
+
3
+
4
+ def memo(fn):
5
+ """
6
+ Lets you skip re-rendering a component when its props are unchanged.
7
+
8
+ Example:
9
+
10
+ ```python
11
+ import flet as ft
12
+
13
+
14
+ @ft.component
15
+ def MyComponent(x, y):
16
+ return ft.Text(f"x={x}, y={y}")
17
+
18
+
19
+ MemoizedMyComponent = ft.memo(MyComponent)
20
+
21
+ flet.run(
22
+ lambda page: page.render(
23
+ lambda: MemoizedMyComponent(x=1, y=2),
24
+ ),
25
+ )
26
+ ```
27
+ """
28
+
29
+ def memo_wrapper(*args, **kwargs):
30
+ r = current_renderer()
31
+ r.set_memo()
32
+ return fn(*args, **kwargs)
33
+
34
+ return memo_wrapper
@@ -0,0 +1,269 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import weakref
5
+ from dataclasses import InitVar, dataclass
6
+ from typing import TYPE_CHECKING, Any, Callable, Optional
7
+
8
+ from flet.components.utils import value_equal
9
+
10
+ if TYPE_CHECKING:
11
+ from flet.components.component import Component
12
+
13
+ from flet.components.component_owned import ComponentOwned
14
+
15
+ Listener = Callable[[Any, Optional[str]], None] # (sender, field|None)
16
+
17
+
18
+ def observable(cls):
19
+ """
20
+ Makes a class observable by mixing in [`Observable`][flet.Observable].
21
+ Can be applied to any class including [`dataclass`][dataclasses.dataclass].
22
+ For dataclasses, decorator can be placed either above or below
23
+ the `@dataclass` decorator.
24
+
25
+ Example:
26
+
27
+ ```python
28
+ from dataclasses import dataclass
29
+ import flet as ft
30
+
31
+
32
+ @ft.observable
33
+ @dataclass
34
+ class MyDataClass:
35
+ x: int
36
+ y: int
37
+ ```
38
+ """
39
+ if Observable in cls.__mro__:
40
+ return cls
41
+ # Build a new class whose MRO is (Observable, cls)
42
+ ns = dict(cls.__dict__)
43
+ # Defensive: avoid carrying these special slots over
44
+ ns.pop("__dict__", None)
45
+ ns.pop("__weakref__", None)
46
+
47
+ Mixed = type(cls.__name__, (Observable, cls), ns)
48
+ Mixed.__module__ = cls.__module__
49
+ Mixed.__qualname__ = cls.__qualname__
50
+ return Mixed
51
+
52
+
53
+ @dataclass
54
+ class ObservableSubscription(ComponentOwned):
55
+ observable: InitVar[Observable]
56
+
57
+ def __post_init__(self, owner: Component, observable: Observable) -> None:
58
+ super().__post_init__(owner)
59
+ self.__disposer = observable.subscribe(self.__on_change)
60
+
61
+ def dispose(self):
62
+ if callable(self.__disposer):
63
+ self.__disposer()
64
+ self.__disposer = None
65
+
66
+ def __on_change(self, _sender, _field):
67
+ if self.component:
68
+ self.component._schedule_update()
69
+
70
+
71
+ class Observable:
72
+ """
73
+ Mixin: notifies when fields change; auto-wraps lists/dicts to be observable.
74
+
75
+ Example:
76
+
77
+ ```python
78
+ import flet as ft
79
+ from dataclasses import dataclass
80
+
81
+
82
+ @ft.observable
83
+ @dataclass
84
+ class MyDataClass:
85
+ x: int
86
+ y: int
87
+
88
+
89
+ obj = MyDataClass(1, 2)
90
+
91
+
92
+ def listener(sender, field):
93
+ print(f"Changed: {field} in {sender}")
94
+
95
+
96
+ obj.subscribe(listener)
97
+ obj.x = 3
98
+ obj.y = 4
99
+ ```
100
+ """
101
+
102
+ __version__ = 0 # optional version counter
103
+
104
+ # listeners store (lazy)
105
+ @property
106
+ def __listeners(self):
107
+ storage_name = "_Observable__listeners_storage"
108
+ try:
109
+ return object.__getattribute__(self, storage_name)
110
+ except AttributeError:
111
+ ws = weakref.WeakSet()
112
+ object.__setattr__(self, storage_name, ws)
113
+ return ws
114
+
115
+ # subscribe / notify
116
+ def subscribe(self, fn: Listener) -> Callable[[], None]:
117
+ self.__listeners.add(fn)
118
+
119
+ def dispose():
120
+ with contextlib.suppress(KeyError):
121
+ self.__listeners.remove(fn)
122
+
123
+ return dispose
124
+
125
+ def _notify(self, field: str | None):
126
+ self.__version__ += 1
127
+ for fn in list(self.__listeners):
128
+ fn(self, field)
129
+
130
+ # collection wrapping
131
+ def _wrap_if_collection(self, name: str, value: Any) -> Any:
132
+ if isinstance(value, list) and not isinstance(value, ObservableList):
133
+ return ObservableList(self, name, value)
134
+ if isinstance(value, dict) and not isinstance(value, ObservableDict):
135
+ return ObservableDict(self, name, value)
136
+ return value
137
+
138
+ # attribute interception
139
+ def __setattr__(self, name: str, value: Any):
140
+ if name.startswith("_"): # private/internal, don't notify
141
+ object.__setattr__(self, name, value)
142
+ return
143
+ value = self._wrap_if_collection(name, value)
144
+ had = hasattr(self, name)
145
+ old = object.__getattribute__(self, name) if had else None
146
+ if not value_equal(old, value):
147
+ object.__setattr__(self, name, value)
148
+ self._notify(name)
149
+
150
+ def __delattr__(self, name: str):
151
+ existed = hasattr(self, name)
152
+ object.__delattr__(self, name)
153
+ if existed:
154
+ self._notify(name)
155
+
156
+ def __repr__(self):
157
+ return f"{super().__repr__()} (version={self.__version__})"
158
+
159
+
160
+ # Observable collections -----------------------------------------
161
+
162
+
163
+ class ObservableList(list):
164
+ __slots__ = ("_owner_ref", "_field")
165
+
166
+ def __init__(self, owner: Observable, field: str, iterable=()):
167
+ super().__init__(iterable)
168
+ self._owner_ref = weakref.ref(owner)
169
+ self._field = field
170
+
171
+ def _touch(self):
172
+ owner = self._owner_ref()
173
+ if owner:
174
+ owner._notify(self._field)
175
+
176
+ def _wrap(self, v):
177
+ owner = self._owner_ref()
178
+ return owner._wrap_if_collection(self._field, v) if owner else v
179
+
180
+ def append(self, x):
181
+ super().append(self._wrap(x))
182
+ self._touch()
183
+
184
+ def extend(self, it):
185
+ super().extend(self._wrap(v) for v in it)
186
+ self._touch()
187
+
188
+ def insert(self, i, x):
189
+ super().insert(i, self._wrap(x))
190
+ self._touch()
191
+
192
+ def remove(self, x):
193
+ super().remove(x)
194
+ self._touch()
195
+
196
+ def clear(self):
197
+ super().clear()
198
+ self._touch()
199
+
200
+ def sort(self, *a, **k):
201
+ super().sort(*a, **k)
202
+ self._touch()
203
+
204
+ def reverse(self):
205
+ super().reverse()
206
+ self._touch()
207
+
208
+ def pop(self, i=-1):
209
+ v = super().pop(i)
210
+ self._touch()
211
+ return v
212
+
213
+ def __setitem__(self, i, v):
214
+ super().__setitem__(i, self._wrap(v))
215
+ self._touch()
216
+
217
+ def __delitem__(self, i):
218
+ super().__delitem__(i)
219
+ self._touch()
220
+
221
+
222
+ class ObservableDict(dict):
223
+ __slots__ = ("_owner_ref", "_field")
224
+
225
+ def __init__(self, owner: Observable, field: str, mapping=()):
226
+ super().__init__(mapping)
227
+ self._owner_ref = weakref.ref(owner)
228
+ self._field = field
229
+
230
+ def _touch(self):
231
+ owner = self._owner_ref()
232
+ if owner:
233
+ owner._notify(self._field)
234
+
235
+ def _wrap(self, v):
236
+ owner = self._owner_ref()
237
+ return owner._wrap_if_collection(self._field, v) if owner else v
238
+
239
+ def __setitem__(self, k, v):
240
+ super().__setitem__(k, self._wrap(v))
241
+ self._touch()
242
+
243
+ def __delitem__(self, k):
244
+ super().__delitem__(k)
245
+ self._touch()
246
+
247
+ def clear(self):
248
+ super().clear()
249
+ self._touch()
250
+
251
+ def update(self, *a, **k):
252
+ super().update(*a, **{kk: self._wrap(vv) for kk, vv in k.items()})
253
+ self._touch()
254
+
255
+ def pop(self, k, *d):
256
+ v = super().pop(k, *d)
257
+ self._touch()
258
+ return v
259
+
260
+ def popitem(self):
261
+ kv = super().popitem()
262
+ self._touch()
263
+ return kv
264
+
265
+ def setdefault(self, k, d=None):
266
+ if k not in self:
267
+ super().__setitem__(k, self._wrap(d))
268
+ self._touch()
269
+ return dict.__getitem__(self, k)
@@ -0,0 +1,10 @@
1
+ from typing import Any
2
+
3
+ from flet.components.component import Component
4
+
5
+
6
+ def unwrap_component(c: Any):
7
+ p = c
8
+ while isinstance(p, Component):
9
+ p = p._b
10
+ return p
@@ -0,0 +1,85 @@
1
+ import contextvars
2
+ import math
3
+ from collections.abc import Sequence
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from flet.components.component import Component, Renderer
8
+
9
+
10
+ _CURRENT_RENDERER: "contextvars.ContextVar[Renderer | None]" = contextvars.ContextVar(
11
+ "CURRENT_RENDERER", default=None
12
+ )
13
+
14
+
15
+ def current_renderer() -> "Renderer":
16
+ r = _CURRENT_RENDERER.get()
17
+ if r is None:
18
+ raise RuntimeError(
19
+ "No current renderer is set. Call via Renderer.render(...) "
20
+ "or Renderer.with_context(...)."
21
+ )
22
+ return r
23
+
24
+
25
+ def current_component() -> "Component":
26
+ r = current_renderer()
27
+ if not r._render_stack:
28
+ raise RuntimeError("Hooks must be called inside a component render.")
29
+ return r._render_stack[-1]
30
+
31
+
32
+ def value_equal(a, b) -> bool:
33
+ # Fast path
34
+ if a is b:
35
+ return True
36
+ # Handle normal equality
37
+ try:
38
+ if a == b:
39
+ return True
40
+ except Exception:
41
+ pass
42
+ # Treat NaN == NaN as equal (like JS Object.is)
43
+ return (
44
+ isinstance(a, float)
45
+ and isinstance(b, float)
46
+ and math.isnan(a)
47
+ and math.isnan(b)
48
+ )
49
+
50
+
51
+ def shallow_compare_args(
52
+ prev_args: Sequence[Any],
53
+ args: Sequence[Any],
54
+ ) -> bool:
55
+ if prev_args is args:
56
+ return True
57
+ if len(prev_args) != len(args):
58
+ return False
59
+ return all(not (a is not b and a != b) for a, b in zip(prev_args, args))
60
+
61
+
62
+ def shallow_compare_kwargs(
63
+ prev_kwargs: dict[str, Any],
64
+ kwargs: dict[str, Any],
65
+ ) -> bool:
66
+ if prev_kwargs is kwargs:
67
+ return True
68
+ if prev_kwargs.keys() != kwargs.keys():
69
+ return False
70
+ for k in prev_kwargs:
71
+ a, b = prev_kwargs[k], kwargs[k]
72
+ if a is not b and a != b:
73
+ return False
74
+ return True
75
+
76
+
77
+ def shallow_compare_args_and_kwargs(
78
+ prev_args: tuple[Any, ...],
79
+ prev_kwargs: dict[str, Any],
80
+ args: tuple[Any, ...],
81
+ kwargs: dict[str, Any],
82
+ ) -> bool:
83
+ return shallow_compare_args(prev_args, args) and shallow_compare_kwargs(
84
+ prev_kwargs, kwargs
85
+ )