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/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/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-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/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/cache.md b/sdk/python/packages/flet/docs/types/cache.md new file mode 100644 index 0000000000..b8b35ff9c1 --- /dev/null +++ b/sdk/python/packages/flet/docs/types/cache.md @@ -0,0 +1,9 @@ +::: flet.cache + +## Examples + +### Cached item view + +```python +--8<-- "../../examples/controls/decorators/cache/basic.py" +``` diff --git a/sdk/python/packages/flet/docs/types/context.md b/sdk/python/packages/flet/docs/types/context.md new file mode 100644 index 0000000000..b3b41a06af --- /dev/null +++ b/sdk/python/packages/flet/docs/types/context.md @@ -0,0 +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/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/docs/types/updatebehavior.md b/sdk/python/packages/flet/docs/types/updatebehavior.md deleted file mode 100644 index 77fc9ca354..0000000000 --- a/sdk/python/packages/flet/docs/types/updatebehavior.md +++ /dev/null @@ -1,3 +0,0 @@ -::: flet.UpdateBehavior - 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..c1e131e625 --- /dev/null +++ b/sdk/python/packages/flet/integration_tests/apps/autoupdate.py @@ -0,0 +1,38 @@ +import flet as ft + + +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): + 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() + + assert ft.context.page + 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_context.py b/sdk/python/packages/flet/integration_tests/test_context.py new file mode 100644 index 0000000000..afb7e85270 --- /dev/null +++ b/sdk/python/packages/flet/integration_tests/test_context.py @@ -0,0 +1,45 @@ +import apps.autoupdate as app +import pytest + +import flet as ft +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_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") + 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 + + +@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 diff --git a/sdk/python/packages/flet/mkdocs.yml b/sdk/python/packages/flet/mkdocs.yml index 2f232b9e20..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 @@ -591,7 +594,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 @@ -651,6 +653,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 9189c07cae..301ae6fb1c 100644 --- a/sdk/python/packages/flet/src/flet/__init__.py +++ b/sdk/python/packages/flet/src/flet/__init__.py @@ -48,9 +48,10 @@ 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 +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 ( @@ -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, @@ -494,7 +494,6 @@ VisualDensity, WebRenderer, ) -from flet.controls.update_behavior import UpdateBehavior from flet.pubsub.pubsub_client import PubSubClient from flet.pubsub.pubsub_hub import PubSubHub @@ -575,6 +574,7 @@ "Column", "ConstrainedControl", "Container", + "Context", "ContinuousRectangleBorder", "Control", "ControlBuilder", @@ -897,7 +897,6 @@ "TooltipValue", "TransparentPointer", "UnderlineTabIndicator", - "UpdateBehavior", "Url", "UrlLauncher", "UrlTarget", @@ -918,11 +917,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/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..85d02b6eab 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 @@ -46,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`][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`. + + 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 @@ -260,7 +269,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 +288,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 +313,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/cache.py b/sdk/python/packages/flet/src/flet/controls/cache.py new file mode 100644 index 0000000000..1f73a68a06 --- /dev/null +++ b/sdk/python/packages/flet/src/flet/controls/cache.py @@ -0,0 +1,85 @@ +import functools +import hashlib +import weakref +from typing import Callable, Optional, ParamSpec, TypeVar, overload + +P = ParamSpec("P") +R = TypeVar("R") + + +def _hash_args(*args, **kwargs): + try: + # Convert args/kwargs to a string and hash it + sig = repr((args, kwargs)) + return hashlib.sha256(sig.encode()).hexdigest() + except Exception: + # fallback to id-based hash if unhashable + return str(id(args)) + str(id(kwargs)) + + +def _freeze_controls(control): + if isinstance(control, list): + return [_freeze_controls(c) for c in control] + elif isinstance(control, dict): + return {k: _freeze_controls(v) for k, v in control.items()} + elif hasattr(control, "__dict__"): # assume it's a control + object.__setattr__(control, "_frozen", True) + return control + + +@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): + """ + 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) diff --git a/sdk/python/packages/flet/src/flet/controls/context.py b/sdk/python/packages/flet/src/flet/controls/context.py index 2f797669fd..3eb9736b5d 100644 --- a/sdk/python/packages/flet/src/flet/controls/context.py +++ b/sdk/python/packages/flet/src/flet/controls/context.py @@ -1,15 +1,129 @@ from contextvars import ContextVar -from typing import TYPE_CHECKING, Optional - -from flet.utils.classproperty import classproperty +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. + + Context instance is accessed via [`ft.context`][flet.context]. + """ + + @property + def page(self) -> "Page": + """ + 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. + + 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 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 + + 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 + + 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: + """ + 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 + + _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] diff --git a/sdk/python/packages/flet/src/flet/controls/data_view.py b/sdk/python/packages/flet/src/flet/controls/data_view.py deleted file mode 100644 index 77495f041b..0000000000 --- a/sdk/python/packages/flet/src/flet/controls/data_view.py +++ /dev/null @@ -1,48 +0,0 @@ -import functools -import hashlib -import weakref -from typing import Callable - - -# --- Utility to create a hashable signature from args --- -def _hash_args(*args, **kwargs): - try: - # Convert args/kwargs to a string and hash it - sig = repr((args, kwargs)) - return hashlib.sha256(sig.encode()).hexdigest() - except Exception: - # fallback to id-based hash if unhashable - 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] - elif isinstance(control, dict): - return {k: _freeze_controls(v) for k, v in control.items()} - elif hasattr(control, "__dict__"): # assume it's a control - object.__setattr__(control, "_frozen", True) - 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 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, 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() -) 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 b6cb078ab6..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,8 +721,8 @@ class AppState: ) -def test_data_view_with_cache(): - @ft.data_view +def test_view_with_cache(): + @ft.cache(freeze=True) def user_details(user: User): return ft.Card( ft.Column( @@ -735,7 +735,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]) @@ -778,8 +778,8 @@ def users_list(users): assert len(removed_controls) == 6 -def test_empty_data_view(): - @ft.data_view +def test_empty_view(): + @ft.cache def my_view(): return None @@ -799,7 +799,7 @@ def logout(self, _): state = AppState() - @ft.data_view + @ft.cache def login_view(state: AppState): return ( ft.Column(