From c3b71f3b89352cfeefff7e535593826433aff382 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Sat, 16 Aug 2025 19:06:40 -0700 Subject: [PATCH 01/12] Refactor auto update control to context API Replaces usage of UpdateBehavior with a new context API for managing auto update state across the codebase. Updates examples, main app logic, and event handling to use ft.context methods. Removes UpdateBehavior from public API and documentation, adds integration tests for auto update behavior, and updates documentation and navigation to reflect the new context API. --- .../examples/apps/controls-gallery/main.py | 7 ++-- sdk/python/examples/apps/counter/counter.py | 2 + sdk/python/examples/controls/canvas/brush.py | 2 +- .../flet-web/src/flet_web/fastapi/flet_app.py | 15 ++++---- .../types/{updatebehavior.md => context.md} | 2 +- .../flet/integration_tests/apps/autoupdate.py | 36 ++++++++++++++++++ .../flet/integration_tests/test_autoupdate.py | 38 +++++++++++++++++++ sdk/python/packages/flet/mkdocs.yml | 2 +- sdk/python/packages/flet/src/flet/__init__.py | 2 - sdk/python/packages/flet/src/flet/app.py | 11 +++--- .../flet/src/flet/controls/base_control.py | 15 ++++---- .../flet/src/flet/controls/context.py | 35 ++++++++++++++--- .../cupertino/cupertino_navigation_bar.py | 4 +- 13 files changed, 133 insertions(+), 38 deletions(-) rename sdk/python/packages/flet/docs/types/{updatebehavior.md => context.md} (66%) create mode 100644 sdk/python/packages/flet/integration_tests/apps/autoupdate.py create mode 100644 sdk/python/packages/flet/integration_tests/test_autoupdate.py diff --git a/sdk/python/examples/apps/controls-gallery/main.py b/sdk/python/examples/apps/controls-gallery/main.py index b7bc85179a..8dc9b870c0 100644 --- a/sdk/python/examples/apps/controls-gallery/main.py +++ b/sdk/python/examples/apps/controls-gallery/main.py @@ -1,15 +1,16 @@ import logging -import flet as ft -import flet.version from components.gallery_view import GalleryView from gallerydata import GalleryData +import flet as ft +import flet.version + gallery = GalleryData() logging.basicConfig(level=logging.DEBUG) -# ft.UpdateBehavior.disable_auto_update() +ft.context.disable_auto_update() def main(page: ft.Page): diff --git a/sdk/python/examples/apps/counter/counter.py b/sdk/python/examples/apps/counter/counter.py index ac8fd1d1fa..d8f2926d60 100644 --- a/sdk/python/examples/apps/counter/counter.py +++ b/sdk/python/examples/apps/counter/counter.py @@ -1,5 +1,7 @@ import flet as ft +ft.context.disable_auto_update() + def main(page: ft.Page): page.title = "Flet counter example" diff --git a/sdk/python/examples/controls/canvas/brush.py b/sdk/python/examples/controls/canvas/brush.py index 00b0216d45..a7480e1594 100644 --- a/sdk/python/examples/controls/canvas/brush.py +++ b/sdk/python/examples/controls/canvas/brush.py @@ -27,7 +27,7 @@ def handle_pan_start(e: ft.DragStartEvent): state.y = e.local_position.y async def handle_pan_update(e: ft.DragUpdateEvent): - ft.UpdateBehavior.disable_auto_update() + ft.context.disable_auto_update() canvas.shapes.append( cv.Line( x1=state.x, diff --git a/sdk/python/packages/flet-web/src/flet_web/fastapi/flet_app.py b/sdk/python/packages/flet-web/src/flet_web/fastapi/flet_app.py index 783c870e66..5f3944a66a 100644 --- a/sdk/python/packages/flet-web/src/flet_web/fastapi/flet_app.py +++ b/sdk/python/packages/flet-web/src/flet_web/fastapi/flet_app.py @@ -10,10 +10,11 @@ import msgpack from fastapi import WebSocket, WebSocketDisconnect + +import flet_web.fastapi as flet_fastapi from flet.controls.base_control import BaseControl -from flet.controls.context import _context_page +from flet.controls.context import _context_page, context from flet.controls.exceptions import FletPageDisconnectedException -from flet.controls.update_behavior import UpdateBehavior from flet.messaging.connection import Connection from flet.messaging.protocol import ( ClientAction, @@ -28,8 +29,6 @@ ) from flet.messaging.session import Session from flet.utils import random_string, sha1 - -import flet_web.fastapi as flet_fastapi from flet_web.fastapi.flet_app_manager import app_manager from flet_web.fastapi.oauth_state import OAuthState from flet_web.uploads import build_upload_url @@ -140,24 +139,24 @@ async def __on_session_created(self): try: assert self.__main is not None _context_page.set(self.__session.page) - UpdateBehavior.reset() + context.reset_auto_update() if asyncio.iscoroutinefunction(self.__main): await self.__main(self.__session.page) elif inspect.isasyncgenfunction(self.__main): async for _ in self.__main(self.__session.page): - if UpdateBehavior.auto_update_enabled(): + if context.auto_update_enabled(): await self.__session.auto_update(self.__session.page) elif inspect.isgeneratorfunction(self.__main): for _ in self.__main(self.__session.page): - if UpdateBehavior.auto_update_enabled(): + if context.auto_update_enabled(): await self.__session.auto_update(self.__session.page) else: self.__main(self.__session.page) - if UpdateBehavior.auto_update_enabled(): + if context.auto_update_enabled(): await self.__session.auto_update(self.__session.page) except FletPageDisconnectedException: logger.debug( diff --git a/sdk/python/packages/flet/docs/types/updatebehavior.md b/sdk/python/packages/flet/docs/types/context.md similarity index 66% rename from sdk/python/packages/flet/docs/types/updatebehavior.md rename to sdk/python/packages/flet/docs/types/context.md index 77fc9ca354..d120eca1ca 100644 --- a/sdk/python/packages/flet/docs/types/updatebehavior.md +++ b/sdk/python/packages/flet/docs/types/context.md @@ -1,3 +1,3 @@ -::: flet.UpdateBehavior +::: flet.context options: separate_signature: false diff --git a/sdk/python/packages/flet/integration_tests/apps/autoupdate.py b/sdk/python/packages/flet/integration_tests/apps/autoupdate.py new file mode 100644 index 0000000000..bb6971799f --- /dev/null +++ b/sdk/python/packages/flet/integration_tests/apps/autoupdate.py @@ -0,0 +1,36 @@ +import flet as ft + + +def main(page: ft.Page): + page.title = "Autoupdate test" + + def auto_update_global_enabled_click(e): + page.controls.append(ft.Text("Global auto update")) + + def disable_autoupdate_no_update_click(e): + ft.context.disable_auto_update() + page.controls.append(ft.Text("Auto update no update")) + + def disable_autoupdate_with_update_click(e): + ft.context.disable_auto_update() + page.controls.append(ft.Text("Auto update with update")) + page.update() + + page.add( + ft.Text(f"Auto update enabled: {ft.context.auto_update_enabled()}"), + ft.Button( + "auto_update_global_enabled", on_click=auto_update_global_enabled_click + ), + ft.Button( + "disable_autoupdate_no_update", + on_click=disable_autoupdate_no_update_click, + ), + ft.Button( + "disable_autoupdate_with_update", + on_click=disable_autoupdate_with_update_click, + ), + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/packages/flet/integration_tests/test_autoupdate.py b/sdk/python/packages/flet/integration_tests/test_autoupdate.py new file mode 100644 index 0000000000..58fbb9eda3 --- /dev/null +++ b/sdk/python/packages/flet/integration_tests/test_autoupdate.py @@ -0,0 +1,38 @@ +import apps.autoupdate as app +import pytest + +import flet.testing as ftt + + +@pytest.mark.parametrize( + "flet_app", + [ + { + "flet_app_main": app.main, + } + ], + indirect=True, +) +class TestApp: + @pytest.mark.asyncio(loop_scope="module") + async def test_app(self, flet_app: ftt.FletTestApp): + tester = flet_app.tester + await tester.pump_and_settle() + auto_update_enabled = await tester.find_by_text("Auto update enabled: True") + assert auto_update_enabled.count == 1 + + # tap 1st button + await tester.tap(await tester.find_by_text("auto_update_global_enabled")) + await tester.pump_and_settle() + assert (await tester.find_by_text("Global auto update")).count == 1 + + # tap 2nd button + await tester.tap(await tester.find_by_text("disable_autoupdate_no_update")) + await tester.pump_and_settle() + assert (await tester.find_by_text("Auto update no update")).count == 0 + + # tap 3rd button + await tester.tap(await tester.find_by_text("disable_autoupdate_with_update")) + await tester.pump_and_settle() + assert (await tester.find_by_text("Auto update with update")).count == 1 + assert (await tester.find_by_text("Auto update no update")).count == 1 diff --git a/sdk/python/packages/flet/mkdocs.yml b/sdk/python/packages/flet/mkdocs.yml index 79c1e7c4b0..c053745e93 100644 --- a/sdk/python/packages/flet/mkdocs.yml +++ b/sdk/python/packages/flet/mkdocs.yml @@ -393,6 +393,7 @@ nav: - Types: # - types/index.md - Aliases: types/aliases.md + - Context: types/context.md - Dataclasses: - Alignment: types/alignment.md - Animation: types/animation.md @@ -588,7 +589,6 @@ nav: - TileAffinity: types/tileaffinity.md - TimePickerEntryMode: types/timepickerentrymode.md - TooltipTriggerMode: types/tooltiptriggermode.md - - UpdateBehavior: types/updatebehavior.md - UrlTarget: types/urltarget.md - VerticalAlignment: types/verticalalignment.md - VisualDensity: types/visualdensity.md diff --git a/sdk/python/packages/flet/src/flet/__init__.py b/sdk/python/packages/flet/src/flet/__init__.py index 7da9377440..dc3af4ed47 100644 --- a/sdk/python/packages/flet/src/flet/__init__.py +++ b/sdk/python/packages/flet/src/flet/__init__.py @@ -487,7 +487,6 @@ VisualDensity, WebRenderer, ) -from flet.controls.update_behavior import UpdateBehavior from flet.pubsub.pubsub_client import PubSubClient from flet.pubsub.pubsub_hub import PubSubHub @@ -886,7 +885,6 @@ "TooltipValue", "TransparentPointer", "UnderlineTabIndicator", - "UpdateBehavior", "UrlLauncher", "UrlTarget", "ValueKey", diff --git a/sdk/python/packages/flet/src/flet/app.py b/sdk/python/packages/flet/src/flet/app.py index 12b4fbf118..97d597af46 100644 --- a/sdk/python/packages/flet/src/flet/app.py +++ b/sdk/python/packages/flet/src/flet/app.py @@ -10,10 +10,9 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, Optional, Union -from flet.controls.context import _context_page +from flet.controls.context import _context_page, context from flet.controls.page import Page from flet.controls.types import AppView, RouteUrlStrategy, WebRenderer -from flet.controls.update_behavior import UpdateBehavior from flet.messaging.session import Session from flet.utils import ( get_bool_env_var, @@ -254,24 +253,24 @@ async def on_session_created(session: Session): try: assert main is not None _context_page.set(session.page) - UpdateBehavior.reset() + context.reset_auto_update() if asyncio.iscoroutinefunction(main): await main(session.page) elif inspect.isasyncgenfunction(main): async for _ in main(session.page): - if UpdateBehavior.auto_update_enabled(): + if context.auto_update_enabled(): await session.auto_update(session.page) elif inspect.isgeneratorfunction(main): for _ in main(session.page): - if UpdateBehavior.auto_update_enabled(): + if context.auto_update_enabled(): await session.auto_update(session.page) else: # run synchronously main(session.page) - if UpdateBehavior.auto_update_enabled(): + if context.auto_update_enabled(): await session.auto_update(session.page) except Exception as e: diff --git a/sdk/python/packages/flet/src/flet/controls/base_control.py b/sdk/python/packages/flet/src/flet/controls/base_control.py index 682ee3c94f..efb8535e2b 100644 --- a/sdk/python/packages/flet/src/flet/controls/base_control.py +++ b/sdk/python/packages/flet/src/flet/controls/base_control.py @@ -5,12 +5,11 @@ from dataclasses import InitVar, dataclass, field from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union -from flet.controls.context import _context_page +from flet.controls.context import _context_page, context from flet.controls.control_event import ControlEvent, get_event_field_type from flet.controls.control_id import ControlId from flet.controls.keys import KeyValue from flet.controls.ref import Ref -from flet.controls.update_behavior import UpdateBehavior from flet.utils.from_dict import from_dict from flet.utils.object_model import get_param_count @@ -260,7 +259,7 @@ async def _trigger_event(self, event_name: str, event_data: Any): if handle_event is None or handle_event: _context_page.set(self.page) - UpdateBehavior.reset() + context.reset_auto_update() assert self.page, ( "Control must be added to a page before triggering events. " @@ -279,22 +278,22 @@ async def _trigger_event(self, event_name: str, event_data: Any): elif inspect.isasyncgenfunction(event_handler): if get_param_count(event_handler) == 0: async for _ in event_handler(): - if UpdateBehavior.auto_update_enabled(): + if context.auto_update_enabled(): await session.auto_update(session.index.get(self._i)) else: async for _ in event_handler(e): - if UpdateBehavior.auto_update_enabled(): + if context.auto_update_enabled(): await session.auto_update(session.index.get(self._i)) return elif inspect.isgeneratorfunction(event_handler): if get_param_count(event_handler) == 0: for _ in event_handler(): - if UpdateBehavior.auto_update_enabled(): + if context.auto_update_enabled(): await session.auto_update(session.index.get(self._i)) else: for _ in event_handler(e): - if UpdateBehavior.auto_update_enabled(): + if context.auto_update_enabled(): await session.auto_update(session.index.get(self._i)) return @@ -304,5 +303,5 @@ async def _trigger_event(self, event_name: str, event_data: Any): else: event_handler(e) - if UpdateBehavior.auto_update_enabled(): + if context.auto_update_enabled(): await session.auto_update(session.index.get(self._i)) diff --git a/sdk/python/packages/flet/src/flet/controls/context.py b/sdk/python/packages/flet/src/flet/controls/context.py index 2f797669fd..02ff6d07ef 100644 --- a/sdk/python/packages/flet/src/flet/controls/context.py +++ b/sdk/python/packages/flet/src/flet/controls/context.py @@ -1,15 +1,38 @@ from contextvars import ContextVar from typing import TYPE_CHECKING, Optional -from flet.utils.classproperty import classproperty - if TYPE_CHECKING: from flet.controls.page import Page + +class Context: + @property + def page(self) -> Optional["Page"]: + return _context_page.get() + + def reset_auto_update(self): + """Copies parent state into current context.""" + current = _update_behavior_context_var.get() + new = UpdateBehavior() + new._auto_update_enabled = current._auto_update_enabled + _update_behavior_context_var.set(new) + + def enable_auto_update(self): + _update_behavior_context_var.get()._auto_update_enabled = True + + def disable_auto_update(self): + _update_behavior_context_var.get()._auto_update_enabled = False + + def auto_update_enabled(self): + return _update_behavior_context_var.get()._auto_update_enabled + + +class UpdateBehavior: + _auto_update_enabled: bool = True + + _context_page = ContextVar("flet_session_page", default=None) +_update_behavior_context_var = ContextVar("update_behavior", default=UpdateBehavior()) # noqa: B039 -class context: - @classproperty - def page(cls) -> Optional["Page"]: - return _context_page.get() +context = Context() diff --git a/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_navigation_bar.py b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_navigation_bar.py index a1c866e0b5..857bb44142 100644 --- a/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_navigation_bar.py +++ b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_navigation_bar.py @@ -24,8 +24,8 @@ class CupertinoNavigationBar(ConstrainedControl): Raises: AssertionError: If [`destinations`][(c).] does not contain at least two visible - [`NavigationBarDestination`][flet.NavigationBarDestination]s. - IndexError: If [`selected_index`][(c).] is out of range. + [`NavigationBarDestination`][flet.NavigationBarDestination]s. + IndexError: If [`selected_index`][(c).] is out of range. """ destinations: list[NavigationBarDestination] From b4606193efc8e0ed54ebab9f044c524ec9faf05d Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Sat, 16 Aug 2025 19:15:45 -0700 Subject: [PATCH 02/12] Add docstrings and improve type hints in Context class Added detailed docstrings to Context class methods for better clarity and documentation. Improved type hints and assertions, including stricter return type for page property and assertion to ensure context is associated with a page. --- .../flet/src/flet/controls/context.py | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/sdk/python/packages/flet/src/flet/controls/context.py b/sdk/python/packages/flet/src/flet/controls/context.py index 02ff6d07ef..2decd2d2d5 100644 --- a/sdk/python/packages/flet/src/flet/controls/context.py +++ b/sdk/python/packages/flet/src/flet/controls/context.py @@ -1,29 +1,59 @@ from contextvars import ContextVar -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING if TYPE_CHECKING: from flet.controls.page import Page class Context: + """ + Manages the context for Flet controls, including page reference + and auto-update behavior. + """ + @property - def page(self) -> Optional["Page"]: - return _context_page.get() + def page(self) -> "Page": + """ + Returns the current page associated with the context. + + Returns: + The current page. + + Raises: + AssertionError: if property is called outside of Flet app. + """ + page = _context_page.get() + assert page, "The context is not associated with any page." + return page def reset_auto_update(self): - """Copies parent state into current context.""" + """ + Copies the parent auto-update state into the current context. + """ current = _update_behavior_context_var.get() new = UpdateBehavior() new._auto_update_enabled = current._auto_update_enabled _update_behavior_context_var.set(new) def enable_auto_update(self): + """ + Enables auto-update behavior for the current context. + """ _update_behavior_context_var.get()._auto_update_enabled = True def disable_auto_update(self): + """ + Disables auto-update behavior for the current context. + """ _update_behavior_context_var.get()._auto_update_enabled = False - def auto_update_enabled(self): + def auto_update_enabled(self) -> bool: + """ + Returns whether auto-update is enabled in the current context. + + Returns: + `True` if auto-update is enabled, `False` otherwise. + """ return _update_behavior_context_var.get()._auto_update_enabled From 0cdb665e4e14adfae13cccb8a0f0db8e4571bcd1 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Sat, 16 Aug 2025 21:13:01 -0700 Subject: [PATCH 03/12] Replace data_view decorator with cache decorator Renamed and enhanced the data_view decorator to cache, adding a freeze option for control freezing. Updated imports, __all__, and all usages in tests to use the new cache decorator. --- sdk/python/packages/flet/src/flet/__init__.py | 4 +- .../flet/controls/{data_view.py => cache.py} | 47 +++++++++++-------- .../flet/tests/test_object_diff_frozen.py | 13 ++--- 3 files changed, 36 insertions(+), 28 deletions(-) rename sdk/python/packages/flet/src/flet/controls/{data_view.py => cache.py} (52%) diff --git a/sdk/python/packages/flet/src/flet/__init__.py b/sdk/python/packages/flet/src/flet/__init__.py index dc3af4ed47..a66174083d 100644 --- a/sdk/python/packages/flet/src/flet/__init__.py +++ b/sdk/python/packages/flet/src/flet/__init__.py @@ -48,6 +48,7 @@ ShapeBorder, StadiumBorder, ) +from flet.controls.cache import cache from flet.controls.colors import Colors from flet.controls.constrained_control import ConstrainedControl from flet.controls.context import context @@ -182,7 +183,6 @@ CupertinoTimerPickerMode, ) from flet.controls.cupertino.cupertino_tinted_button import CupertinoTintedButton -from flet.controls.data_view import data_view from flet.controls.dialog_control import DialogControl from flet.controls.duration import ( DateTimeValue, @@ -904,11 +904,11 @@ "app_async", "border", "border_radius", + "cache", "context", "control", "cupertino_colors", "cupertino_icons", - "data_view", "dropdown", "dropdownm2", "icons", diff --git a/sdk/python/packages/flet/src/flet/controls/data_view.py b/sdk/python/packages/flet/src/flet/controls/cache.py similarity index 52% rename from sdk/python/packages/flet/src/flet/controls/data_view.py rename to sdk/python/packages/flet/src/flet/controls/cache.py index 77495f041b..dda6b577f5 100644 --- a/sdk/python/packages/flet/src/flet/controls/data_view.py +++ b/sdk/python/packages/flet/src/flet/controls/cache.py @@ -26,23 +26,30 @@ def _freeze_controls(control): return control -# --- Main decorator --- -def data_view(fn: Callable): - cache = weakref.WeakValueDictionary() - - @functools.wraps(fn) - def wrapper(*args, **kwargs): - key = _hash_args(*args, **kwargs) - - if key in cache: - return cache[key] - - result = fn(*args, **kwargs) - if result is not None: - _freeze_controls(result) - cache[key] = result - elif key in cache: - del cache[key] - return result - - return wrapper +# --- Main decorator with `freeze` option --- +def cache(_fn=None, *, freeze: bool = False): + def decorator(fn: Callable): + cache_store = weakref.WeakValueDictionary() + + @functools.wraps(fn) + def wrapper(*args, **kwargs): + key = _hash_args(*args, **kwargs) + + if key in cache_store: + return cache_store[key] + + result = fn(*args, **kwargs) + if result is not None: + if freeze: + _freeze_controls(result) + cache_store[key] = result + elif key in cache_store: + del cache_store[key] + return result + + return wrapper + + if _fn is None: + return decorator + else: + return decorator(_fn) diff --git a/sdk/python/packages/flet/tests/test_object_diff_frozen.py b/sdk/python/packages/flet/tests/test_object_diff_frozen.py index a129d5e319..4ec593ba0a 100644 --- a/sdk/python/packages/flet/tests/test_object_diff_frozen.py +++ b/sdk/python/packages/flet/tests/test_object_diff_frozen.py @@ -1,8 +1,9 @@ from dataclasses import dataclass from typing import Optional -import flet as ft import pytest + +import flet as ft from flet.controls.base_control import BaseControl, control from .common import ( @@ -728,7 +729,7 @@ class AppState: def test_data_view_with_cache(): - @ft.data_view + @ft.cache(freeze=True) def user_details(user: User): return ft.Card( ft.Column( @@ -741,7 +742,7 @@ def user_details(user: User): key=user.id, ) - @ft.data_view + @ft.cache(freeze=True) def users_list(users): return ft.Column([user_details(user) for user in users]) @@ -785,7 +786,7 @@ def users_list(users): def test_empty_data_view(): - @ft.data_view + @ft.cache def my_view(): return None @@ -805,7 +806,7 @@ def logout(self, _): state = AppState() - @ft.data_view + @ft.cache def login_view(state: AppState): return ( ft.Column( @@ -822,4 +823,4 @@ def login_view(state: AppState): ) ) - app = ft.View("/", [login_view(state)]) + ft.View("/", [login_view(state)]) From 0d9928259b7a599c674cdc72e39b054f1f67e226 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Sun, 17 Aug 2025 17:10:12 -0700 Subject: [PATCH 04/12] Add type annotations and overloads to cache decorator Introduces ParamSpec and TypeVar for improved type safety in the cache decorator. Adds overloads to the cache function and updates argument and return type annotations for better compatibility with type checkers. --- .../packages/flet/src/flet/controls/cache.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/sdk/python/packages/flet/src/flet/controls/cache.py b/sdk/python/packages/flet/src/flet/controls/cache.py index dda6b577f5..8cd44e5fd6 100644 --- a/sdk/python/packages/flet/src/flet/controls/cache.py +++ b/sdk/python/packages/flet/src/flet/controls/cache.py @@ -1,7 +1,10 @@ import functools import hashlib import weakref -from typing import Callable +from typing import Callable, Optional, ParamSpec, TypeVar, overload + +P = ParamSpec("P") +R = TypeVar("R") # --- Utility to create a hashable signature from args --- @@ -27,12 +30,22 @@ def _freeze_controls(control): # --- Main decorator with `freeze` option --- -def cache(_fn=None, *, freeze: bool = False): - def decorator(fn: Callable): + + +@overload +def cache( + _fn: None = ..., *, freeze: bool = False +) -> Callable[[Callable[P, R]], Callable[P, R]]: ... +@overload +def cache(_fn: Callable[P, R], *, freeze: bool = False) -> Callable[P, R]: ... + + +def cache(_fn: Optional[Callable[P, R]] = None, *, freeze: bool = False): + def decorator(fn: Callable[P, R]) -> Callable[P, R]: cache_store = weakref.WeakValueDictionary() @functools.wraps(fn) - def wrapper(*args, **kwargs): + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: key = _hash_args(*args, **kwargs) if key in cache_store: From 1036adbae87c8e328627b58cc5e48dacaede0c84 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 18 Aug 2025 09:34:05 -0700 Subject: [PATCH 05/12] Add context exception test and update autoupdate app Added assertions for ft.context.page in autoupdate.py to ensure context is associated with a page. Renamed test_autoupdate.py to test_context.py and added a test to verify that accessing ft.context.page outside of a Flet app raises an exception. --- .../packages/flet/integration_tests/apps/autoupdate.py | 2 ++ .../{test_autoupdate.py => test_context.py} | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) rename sdk/python/packages/flet/integration_tests/{test_autoupdate.py => test_context.py} (79%) diff --git a/sdk/python/packages/flet/integration_tests/apps/autoupdate.py b/sdk/python/packages/flet/integration_tests/apps/autoupdate.py index bb6971799f..c1e131e625 100644 --- a/sdk/python/packages/flet/integration_tests/apps/autoupdate.py +++ b/sdk/python/packages/flet/integration_tests/apps/autoupdate.py @@ -5,6 +5,7 @@ def main(page: ft.Page): page.title = "Autoupdate test" def auto_update_global_enabled_click(e): + assert ft.context.page page.controls.append(ft.Text("Global auto update")) def disable_autoupdate_no_update_click(e): @@ -16,6 +17,7 @@ def disable_autoupdate_with_update_click(e): page.controls.append(ft.Text("Auto update with update")) page.update() + assert ft.context.page page.add( ft.Text(f"Auto update enabled: {ft.context.auto_update_enabled()}"), ft.Button( diff --git a/sdk/python/packages/flet/integration_tests/test_autoupdate.py b/sdk/python/packages/flet/integration_tests/test_context.py similarity index 79% rename from sdk/python/packages/flet/integration_tests/test_autoupdate.py rename to sdk/python/packages/flet/integration_tests/test_context.py index 58fbb9eda3..afb7e85270 100644 --- a/sdk/python/packages/flet/integration_tests/test_autoupdate.py +++ b/sdk/python/packages/flet/integration_tests/test_context.py @@ -1,6 +1,7 @@ import apps.autoupdate as app import pytest +import flet as ft import flet.testing as ftt @@ -15,7 +16,7 @@ ) class TestApp: @pytest.mark.asyncio(loop_scope="module") - async def test_app(self, flet_app: ftt.FletTestApp): + async def test_auto_update(self, flet_app: ftt.FletTestApp): tester = flet_app.tester await tester.pump_and_settle() auto_update_enabled = await tester.find_by_text("Auto update enabled: True") @@ -36,3 +37,9 @@ async def test_app(self, flet_app: ftt.FletTestApp): await tester.pump_and_settle() assert (await tester.find_by_text("Auto update with update")).count == 1 assert (await tester.find_by_text("Auto update no update")).count == 1 + + +@pytest.mark.asyncio(loop_scope="module") +async def test_context_throws_exception_outside_flet_app(): + with pytest.raises(Exception, match="The context is not associated with any page."): + p = ft.context.page # noqa: F841 From 2edbe36399295e35352232b5941abafba041305f Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 18 Aug 2025 10:07:25 -0700 Subject: [PATCH 06/12] Refactor Context docs and API exposure Updated documentation for the Context class with usage examples and improved docstrings. Exposed Context in the public API via __init__.py and reorganized mkdocs navigation to move Context under Utility. Minor code reordering for clarity. --- .../packages/flet/docs/types/context.md | 4 +- sdk/python/packages/flet/mkdocs.yml | 3 +- sdk/python/packages/flet/src/flet/__init__.py | 3 +- .../flet/src/flet/controls/context.py | 72 ++++++++++++++++--- 4 files changed, 67 insertions(+), 15 deletions(-) diff --git a/sdk/python/packages/flet/docs/types/context.md b/sdk/python/packages/flet/docs/types/context.md index d120eca1ca..4bbd95a620 100644 --- a/sdk/python/packages/flet/docs/types/context.md +++ b/sdk/python/packages/flet/docs/types/context.md @@ -1,3 +1 @@ -::: flet.context - options: - separate_signature: false +::: flet.Context diff --git a/sdk/python/packages/flet/mkdocs.yml b/sdk/python/packages/flet/mkdocs.yml index c053745e93..f2353e27fe 100644 --- a/sdk/python/packages/flet/mkdocs.yml +++ b/sdk/python/packages/flet/mkdocs.yml @@ -393,7 +393,6 @@ nav: - Types: # - types/index.md - Aliases: types/aliases.md - - Context: types/context.md - Dataclasses: - Alignment: types/alignment.md - Animation: types/animation.md @@ -647,6 +646,8 @@ nav: # - types/pubsub/index.md - PubSubClient: types/pubsub/pubsubclient.md - PubSubHub: types/pubsub/pubsubhub.md + - Utility: + - Context: types/context.md - Environment Variables: environment-variables.md - Publish: - Build and Publish a Flet app: publish/index.md diff --git a/sdk/python/packages/flet/src/flet/__init__.py b/sdk/python/packages/flet/src/flet/__init__.py index a66174083d..08b409ba4f 100644 --- a/sdk/python/packages/flet/src/flet/__init__.py +++ b/sdk/python/packages/flet/src/flet/__init__.py @@ -51,7 +51,7 @@ from flet.controls.cache import cache from flet.controls.colors import Colors from flet.controls.constrained_control import ConstrainedControl -from flet.controls.context import context +from flet.controls.context import Context, context from flet.controls.control import Control from flet.controls.control_builder import ControlBuilder from flet.controls.control_event import ( @@ -567,6 +567,7 @@ "Column", "ConstrainedControl", "Container", + "Context", "ContinuousRectangleBorder", "Control", "ControlBuilder", diff --git a/sdk/python/packages/flet/src/flet/controls/context.py b/sdk/python/packages/flet/src/flet/controls/context.py index 2decd2d2d5..6f624984cb 100644 --- a/sdk/python/packages/flet/src/flet/controls/context.py +++ b/sdk/python/packages/flet/src/flet/controls/context.py @@ -9,12 +9,21 @@ class Context: """ Manages the context for Flet controls, including page reference and auto-update behavior. + + Context instance is accessed via `ft.context`. """ @property def page(self) -> "Page": """ - Returns the current page associated with the context. + Returns the current [`Page`][flet.Page] associated with the context. + + For example: + + ```python + # take page width anywhere in the app + width = ft.context.page.width + ``` Returns: The current page. @@ -26,24 +35,58 @@ def page(self) -> "Page": assert page, "The context is not associated with any page." return page - def reset_auto_update(self): - """ - Copies the parent auto-update state into the current context. - """ - current = _update_behavior_context_var.get() - new = UpdateBehavior() - new._auto_update_enabled = current._auto_update_enabled - _update_behavior_context_var.set(new) - def enable_auto_update(self): """ Enables auto-update behavior for the current context. + + For example: + + ```python + import flet as ft + + # disable auto-update globally for the app + ft.context.disable_auto_update() + + + def main(page: ft.Page): + # enable auto-update just inside main + ft.context.enable_auto_update() + + page.controls.append(ft.Text("Hello, world!")) + # page.update() - we don't need to call it explicitly + + + ft.run(main) + ``` """ _update_behavior_context_var.get()._auto_update_enabled = True def disable_auto_update(self): """ Disables auto-update behavior for the current context. + + For example: + + ```python + import flet as ft + + + def main(page: ft.Page): + def button_click(): + ft.context.disable_auto_update() + b.content = "Button clicked!" + # update just the button + b.update() + + page.controls.append(ft.Text("This won't appear")) + # no page.update() will be called here + + page.controls.append(b := ft.Button("Action!", on_click=button_click)) + # page.update() - auto-update is enabled by default + + + ft.run(main) + ``` """ _update_behavior_context_var.get()._auto_update_enabled = False @@ -56,6 +99,15 @@ def auto_update_enabled(self) -> bool: """ return _update_behavior_context_var.get()._auto_update_enabled + def reset_auto_update(self): + """ + Copies the parent auto-update state into the current context. + """ + current = _update_behavior_context_var.get() + new = UpdateBehavior() + new._auto_update_enabled = current._auto_update_enabled + _update_behavior_context_var.set(new) + class UpdateBehavior: _auto_update_enabled: bool = True From ef13b29da2d94e72982311beb3f019f81d148aa7 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 18 Aug 2025 10:19:05 -0700 Subject: [PATCH 07/12] Copilot fix: Add docs to UpdateBehavior class Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sdk/python/packages/flet/src/flet/controls/context.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sdk/python/packages/flet/src/flet/controls/context.py b/sdk/python/packages/flet/src/flet/controls/context.py index 6f624984cb..13a8b426ee 100644 --- a/sdk/python/packages/flet/src/flet/controls/context.py +++ b/sdk/python/packages/flet/src/flet/controls/context.py @@ -110,6 +110,14 @@ def reset_auto_update(self): class UpdateBehavior: + """ + Internal class used by the Context API to manage auto-update behavior. + + An instance of UpdateBehavior is stored in a context variable and tracks + whether automatic updates are enabled for the current context. The Context + class interacts with UpdateBehavior to enable, disable, and query the + auto-update state. + """ _auto_update_enabled: bool = True From 696367b1f3a12e5eb61ad17e91a52a9401f110b6 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 18 Aug 2025 10:47:24 -0700 Subject: [PATCH 08/12] Delete update_behavior.py --- .../flet/src/flet/controls/update_behavior.py | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 sdk/python/packages/flet/src/flet/controls/update_behavior.py diff --git a/sdk/python/packages/flet/src/flet/controls/update_behavior.py b/sdk/python/packages/flet/src/flet/controls/update_behavior.py deleted file mode 100644 index 673a1dc27d..0000000000 --- a/sdk/python/packages/flet/src/flet/controls/update_behavior.py +++ /dev/null @@ -1,33 +0,0 @@ -import contextvars - - -class UpdateBehavior: - """ - TBD - """ - _auto_update_enabled: bool = True - - @classmethod - def reset(cls): - """Copies parent state into current context.""" - current = _update_behavior_context_var.get() - new = UpdateBehavior() - new._auto_update_enabled = current._auto_update_enabled - _update_behavior_context_var.set(new) - - @classmethod - def enable_auto_update(cls): - _update_behavior_context_var.get()._auto_update_enabled = True - - @classmethod - def disable_auto_update(cls): - _update_behavior_context_var.get()._auto_update_enabled = False - - @classmethod - def auto_update_enabled(cls): - return _update_behavior_context_var.get()._auto_update_enabled - - -_update_behavior_context_var = contextvars.ContextVar( - "update_behavior", default=UpdateBehavior() -) From db0efdef96cec38ef3d232e0373de9a24e5215c5 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 18 Aug 2025 12:12:02 -0700 Subject: [PATCH 09/12] Add context usage examples and update docs Added example scripts for disabling auto update and accessing page via context. Updated documentation to include new examples and improved references to ft.context. Renamed window_width and window_height to web_popup_window_width and web_popup_window_height in Page class for clarity. --- .../types/context/disable_auto_update.py | 18 ++++++++++++++++++ .../controls/types/context/get_page.py | 11 +++++++++++ .../packages/flet/docs/types/aliases.md | 1 + .../packages/flet/docs/types/context.md | 19 +++++++++++++++++++ .../flet/src/flet/controls/context.py | 3 ++- .../packages/flet/src/flet/controls/page.py | 4 ++-- 6 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 sdk/python/examples/controls/types/context/disable_auto_update.py create mode 100644 sdk/python/examples/controls/types/context/get_page.py diff --git a/sdk/python/examples/controls/types/context/disable_auto_update.py b/sdk/python/examples/controls/types/context/disable_auto_update.py new file mode 100644 index 0000000000..171892877e --- /dev/null +++ b/sdk/python/examples/controls/types/context/disable_auto_update.py @@ -0,0 +1,18 @@ +import flet as ft + + +def main(page: ft.Page): + def button_click(): + ft.context.disable_auto_update() + b.content = "Button clicked!" + # update just the button + b.update() + + page.controls.append(ft.Text("This won't appear")) + # no page.update() will be called here + + page.controls.append(b := ft.Button("Action!", on_click=button_click)) + # page.update() - auto-update is enabled by default + + +ft.run(main) diff --git a/sdk/python/examples/controls/types/context/get_page.py b/sdk/python/examples/controls/types/context/get_page.py new file mode 100644 index 0000000000..39d5d8331f --- /dev/null +++ b/sdk/python/examples/controls/types/context/get_page.py @@ -0,0 +1,11 @@ +import flet as ft + + +def main(page: ft.Page): + def button_click(e): + print("Page width:", ft.context.page.width) + + page.add(ft.Button("Get page width", on_click=button_click)) + + +ft.run(main) diff --git a/sdk/python/packages/flet/docs/types/aliases.md b/sdk/python/packages/flet/docs/types/aliases.md index c9e1f18e2c..a3ca6d6196 100644 --- a/sdk/python/packages/flet/docs/types/aliases.md +++ b/sdk/python/packages/flet/docs/types/aliases.md @@ -5,6 +5,7 @@ ::: flet.BorderSideStrokeAlignValue ::: flet.BoxShadowValue ::: flet.ColorValue +::: flet.context ::: flet.ControlEvent ::: flet.ControlEventHandler ::: flet.ControlStateValue diff --git a/sdk/python/packages/flet/docs/types/context.md b/sdk/python/packages/flet/docs/types/context.md index 4bbd95a620..b3b41a06af 100644 --- a/sdk/python/packages/flet/docs/types/context.md +++ b/sdk/python/packages/flet/docs/types/context.md @@ -1 +1,20 @@ +Manages the context for Flet controls, including page reference +and auto-update behavior. + +Context instance is accessed via [`ft.context`][flet.context]. + +## Examples + +### Get page + +```python +--8<-- "../../examples/controls/types/context/get_page.py" +``` + +### Disable auto update + +```python +--8<-- "../../examples/controls/types/context/disable_auto_update.py" +``` + ::: flet.Context diff --git a/sdk/python/packages/flet/src/flet/controls/context.py b/sdk/python/packages/flet/src/flet/controls/context.py index 13a8b426ee..3eb9736b5d 100644 --- a/sdk/python/packages/flet/src/flet/controls/context.py +++ b/sdk/python/packages/flet/src/flet/controls/context.py @@ -10,7 +10,7 @@ class Context: Manages the context for Flet controls, including page reference and auto-update behavior. - Context instance is accessed via `ft.context`. + Context instance is accessed via [`ft.context`][flet.context]. """ @property @@ -118,6 +118,7 @@ class UpdateBehavior: class interacts with UpdateBehavior to enable, disable, and query the auto-update state. """ + _auto_update_enabled: bool = True diff --git a/sdk/python/packages/flet/src/flet/controls/page.py b/sdk/python/packages/flet/src/flet/controls/page.py index 893eaa86e0..96e0ab3217 100644 --- a/sdk/python/packages/flet/src/flet/controls/page.py +++ b/sdk/python/packages/flet/src/flet/controls/page.py @@ -678,8 +678,8 @@ async def launch_url( for a new browser tab (or in external application on mobile device), or a custom name for a named tab. web_popup_window: Display the URL in a browser popup window. - window_width: Popup window width. - window_height: Popup window height. + web_popup_window_width: Popup window width. + web_popup_window_height: Popup window height. """ await self.url_launcher.launch_url( url, From 0897463318a539bfae69991bb9c4d5ce2a22f898 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 18 Aug 2025 12:24:13 -0700 Subject: [PATCH 10/12] Add decorators docs and refactor control decorator Added documentation for 'cache' and 'control' decorators. Updated mkdocs navigation to include new decorator docs. Refactored the 'control' decorator in base_control.py to rename 'cls_or_type_name' to 'dart_widget_name' and improved docstring for clarity. Renamed test functions in test_object_diff_frozen.py for consistency. --- sdk/python/packages/flet/docs/types/cache.md | 1 + .../packages/flet/docs/types/control.md | 1 + sdk/python/packages/flet/mkdocs.yml | 3 ++ .../flet/src/flet/controls/base_control.py | 30 ++++++++++++------- .../flet/tests/test_object_diff_frozen.py | 4 +-- 5 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 sdk/python/packages/flet/docs/types/cache.md create mode 100644 sdk/python/packages/flet/docs/types/control.md diff --git a/sdk/python/packages/flet/docs/types/cache.md b/sdk/python/packages/flet/docs/types/cache.md new file mode 100644 index 0000000000..ece0a4bc77 --- /dev/null +++ b/sdk/python/packages/flet/docs/types/cache.md @@ -0,0 +1 @@ +::: flet.cache diff --git a/sdk/python/packages/flet/docs/types/control.md b/sdk/python/packages/flet/docs/types/control.md new file mode 100644 index 0000000000..509b16f28a --- /dev/null +++ b/sdk/python/packages/flet/docs/types/control.md @@ -0,0 +1 @@ +::: flet.control diff --git a/sdk/python/packages/flet/mkdocs.yml b/sdk/python/packages/flet/mkdocs.yml index 6fbe2b6b5c..46b6f04645 100644 --- a/sdk/python/packages/flet/mkdocs.yml +++ b/sdk/python/packages/flet/mkdocs.yml @@ -513,6 +513,9 @@ nav: - Tooltip: types/tooltip.md - Url: types/url.md - UnderlineTabIndicator: types/underlinetabindicator.md + - Decorators: + - cache: types/cache.md + - control: types/control.md - Enums: - AnimatedSwitcherTransition: types/animatedswitchertransition.md - AnimationCurve: types/animationcurve.md diff --git a/sdk/python/packages/flet/src/flet/controls/base_control.py b/sdk/python/packages/flet/src/flet/controls/base_control.py index efb8535e2b..56ffff9553 100644 --- a/sdk/python/packages/flet/src/flet/controls/base_control.py +++ b/sdk/python/packages/flet/src/flet/controls/base_control.py @@ -45,30 +45,40 @@ def skip_field(): @dataclass_transform() def control( - cls_or_type_name: Optional[Union[type[T], str]] = None, + dart_widget_name: Optional[Union[type[T], str]] = None, *, isolated: Optional[bool] = None, post_init_args: int = 1, **dataclass_kwargs, ) -> Union[type[T], Callable[[type[T]], type[T]]]: - """Decorator to optionally set 'type' and 'isolated' while behaving like @dataclass. - - - Supports `@control` (without parentheses) - - Supports `@control("custom_type")` (with optional arguments) - - Supports `@control("custom_type", post_init_args=1, isolated=True)` to - specify the number of `InitVar` arguments and isolation + """ + Decorator to optionally set widget name and 'isolated' while behaving + like @dataclass. + + Parameters: + dart_widget_name: The name of widget on Dart side. + isolated: If `True`, marks the control as isolated. An isolated control + is excluded from page updates when its parent control is updated. + post_init_args: Number of InitVar arguments to pass to __post_init__. + **dataclass_kwargs: Additional keyword arguments passed to @dataclass. + + Usage: + - Supports `@control` (without parentheses) + - Supports `@control("WidgetName")` (with optional arguments) + - Supports `@control("WidgetName", post_init_args=1, isolated=True)` to + specify the number of `InitVar` arguments and isolation """ # Case 1: If used as `@control` (without parentheses) - if isinstance(cls_or_type_name, type): + if isinstance(dart_widget_name, type): return _apply_control( - cls_or_type_name, None, isolated, post_init_args, **dataclass_kwargs + dart_widget_name, None, isolated, post_init_args, **dataclass_kwargs ) # Case 2: If used as `@control("custom_type", post_init_args=N, isolated=True)` def wrapper(cls: type[T]) -> type[T]: return _apply_control( - cls, cls_or_type_name, isolated, post_init_args, **dataclass_kwargs + cls, dart_widget_name, isolated, post_init_args, **dataclass_kwargs ) return wrapper diff --git a/sdk/python/packages/flet/tests/test_object_diff_frozen.py b/sdk/python/packages/flet/tests/test_object_diff_frozen.py index e55034e068..42e9e972f7 100644 --- a/sdk/python/packages/flet/tests/test_object_diff_frozen.py +++ b/sdk/python/packages/flet/tests/test_object_diff_frozen.py @@ -721,7 +721,7 @@ class AppState: ) -def test_data_view_with_cache(): +def test_view_with_cache(): @ft.cache(freeze=True) def user_details(user: User): return ft.Card( @@ -778,7 +778,7 @@ def users_list(users): assert len(removed_controls) == 6 -def test_empty_data_view(): +def test_empty_view(): @ft.cache def my_view(): return None From 9589f0a7b1b5bb9ca27cea8693396d7462a29306 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 18 Aug 2025 12:52:56 -0700 Subject: [PATCH 11/12] Add cache decorator example and improve docs Introduces a basic example for the cache decorator in Flet, updates documentation to include usage and example, and refines the cache decorator implementation with improved comments and docstring. Also fixes a minor docstring typo in base_control.py. --- .../controls/decorators/cache/basic.py | 50 +++++++++++++++++++ sdk/python/packages/flet/docs/types/cache.md | 8 +++ .../flet/src/flet/controls/base_control.py | 2 +- .../packages/flet/src/flet/controls/cache.py | 27 ++++++++-- 4 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 sdk/python/examples/controls/decorators/cache/basic.py diff --git a/sdk/python/examples/controls/decorators/cache/basic.py b/sdk/python/examples/controls/decorators/cache/basic.py new file mode 100644 index 0000000000..df4b32c6bb --- /dev/null +++ b/sdk/python/examples/controls/decorators/cache/basic.py @@ -0,0 +1,50 @@ +import logging +from dataclasses import dataclass, field + +import flet as ft + +logging.basicConfig(level=logging.DEBUG) + + +@dataclass +class AppState: + number: int = 0 + items: list[int] = field(default_factory=list) + + def __post_init__(self): + for _ in range(10): + self.add_item() + + def add_item(self): + self.items.append(self.number) + self.number += 1 + + +@ft.cache +def item_view(i: int): + return ft.Container( + ft.Text(f"Item {i}"), + padding=10, + bgcolor=ft.Colors.AMBER_100, + key=i, + ) + + +def main(page: ft.Page): + state = AppState() + + page.floating_action_button = ft.FloatingActionButton( + icon=ft.Icons.ADD, on_click=state.add_item + ) + page.add( + ft.ControlBuilder( + state, + lambda state: ft.SafeArea( + ft.Row([item_view(i) for i in state.items], wrap=True) + ), + expand=True, + ) + ) + + +ft.run(main) diff --git a/sdk/python/packages/flet/docs/types/cache.md b/sdk/python/packages/flet/docs/types/cache.md index ece0a4bc77..b8b35ff9c1 100644 --- a/sdk/python/packages/flet/docs/types/cache.md +++ b/sdk/python/packages/flet/docs/types/cache.md @@ -1 +1,9 @@ ::: flet.cache + +## Examples + +### Cached item view + +```python +--8<-- "../../examples/controls/decorators/cache/basic.py" +``` diff --git a/sdk/python/packages/flet/src/flet/controls/base_control.py b/sdk/python/packages/flet/src/flet/controls/base_control.py index 56ffff9553..e336654798 100644 --- a/sdk/python/packages/flet/src/flet/controls/base_control.py +++ b/sdk/python/packages/flet/src/flet/controls/base_control.py @@ -60,7 +60,7 @@ def control( isolated: If `True`, marks the control as isolated. An isolated control is excluded from page updates when its parent control is updated. post_init_args: Number of InitVar arguments to pass to __post_init__. - **dataclass_kwargs: Additional keyword arguments passed to @dataclass. + dataclass_kwargs: Additional keyword arguments passed to @dataclass. Usage: - Supports `@control` (without parentheses) diff --git a/sdk/python/packages/flet/src/flet/controls/cache.py b/sdk/python/packages/flet/src/flet/controls/cache.py index 8cd44e5fd6..1f73a68a06 100644 --- a/sdk/python/packages/flet/src/flet/controls/cache.py +++ b/sdk/python/packages/flet/src/flet/controls/cache.py @@ -7,7 +7,6 @@ R = TypeVar("R") -# --- Utility to create a hashable signature from args --- def _hash_args(*args, **kwargs): try: # Convert args/kwargs to a string and hash it @@ -18,7 +17,6 @@ def _hash_args(*args, **kwargs): return str(id(args)) + str(id(kwargs)) -# --- Freeze controls in the returned structure --- def _freeze_controls(control): if isinstance(control, list): return [_freeze_controls(c) for c in control] @@ -29,9 +27,6 @@ def _freeze_controls(control): return control -# --- Main decorator with `freeze` option --- - - @overload def cache( _fn: None = ..., *, freeze: bool = False @@ -41,28 +36,50 @@ def cache(_fn: Callable[P, R], *, freeze: bool = False) -> Callable[P, R]: ... def cache(_fn: Optional[Callable[P, R]] = None, *, freeze: bool = False): + """ + A decorator to cache the results of a function based on its arguments. + Used with Flet controls to optimize comparisons in declarative apps. + + Args: + _fn: The function to be decorated. + If None, the decorator is used with arguments. + freeze: If `True`, freezes the returned controls + by setting a `_frozen` attribute. + + Returns: + A decorated function that caches its results. + """ + def decorator(fn: Callable[P, R]) -> Callable[P, R]: + # Use a weak reference dictionary to store cached results cache_store = weakref.WeakValueDictionary() @functools.wraps(fn) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + # Generate a unique hash key based on the function arguments key = _hash_args(*args, **kwargs) + # Return cached result if it exists if key in cache_store: return cache_store[key] + # Call the original function and cache the result result = fn(*args, **kwargs) if result is not None: if freeze: + # Freeze the controls if the freeze flag is set _freeze_controls(result) cache_store[key] = result elif key in cache_store: + # Remove the cache entry if the result is None del cache_store[key] return result return wrapper + # If _fn is None, return the decorator itself for use with arguments if _fn is None: return decorator else: + # Apply the decorator directly if _fn is provided return decorator(_fn) From 2d44d56e46e8957f4e69098bb4506e0e2be64e83 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Tue, 19 Aug 2025 10:59:14 -0700 Subject: [PATCH 12/12] Update control decorator docstring for clarity Improved the docstring for the control decorator by clarifying references to @dataclass and its keyword arguments, and enhancing formatting for better readability. --- sdk/python/packages/flet/src/flet/controls/base_control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/python/packages/flet/src/flet/controls/base_control.py b/sdk/python/packages/flet/src/flet/controls/base_control.py index e336654798..85d02b6eab 100644 --- a/sdk/python/packages/flet/src/flet/controls/base_control.py +++ b/sdk/python/packages/flet/src/flet/controls/base_control.py @@ -53,14 +53,14 @@ def control( ) -> Union[type[T], Callable[[type[T]], type[T]]]: """ Decorator to optionally set widget name and 'isolated' while behaving - like @dataclass. + like [`@dataclass`][dataclasses.dataclass]. Parameters: dart_widget_name: The name of widget on Dart side. isolated: If `True`, marks the control as isolated. An isolated control is excluded from page updates when its parent control is updated. post_init_args: Number of InitVar arguments to pass to __post_init__. - dataclass_kwargs: Additional keyword arguments passed to @dataclass. + **dataclass_kwargs: Additional keyword arguments passed to `@dataclass`. Usage: - Supports `@control` (without parentheses)