From 719ec09a4c955f1ed7daebcfc1e29742cf800287 Mon Sep 17 00:00:00 2001 From: Abhay Phalswal Date: Mon, 2 Nov 2026 11:30:50 +0530 Subject: [PATCH 1/3] fix: honor empty platform values for remaining build settings `artifact`, `org`, `bundle_id`, `icon_background` and the `splash` settings resolved `[tool.flet.]` over `[tool.flet]` with `or`, so an explicit empty platform value was skipped and the global value won. They now go through `get_platform_setting()`, which already treats any set value as an override, so an empty platform value clears the global one and the caller's default applies. `get_platform_setting()` gains an optional `platform` argument so `icon_background_value()` can resolve Android's adaptive-icon colour while another platform is being built. Covers the keys from #6949 that #6981 did not. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../src/flet_cli/commands/build_base.py | 44 +++++++--------- .../tests/test_build_platform_settings.py | 51 +++++++++++++++++++ 3 files changed, 69 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e88c5279e..c39d52ba9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ * Fix `Tester.find_by_key()` not finding `Tab` and `PopupMenuItem` controls by their keys ([#6965](https://github.com/flet-dev/flet/issues/6965), [#6976](https://github.com/flet-dev/flet/pull/6976)) by @RezaShamsipour. * Fix `GestureDetector.on_tap_move` not firing, while preserving tap accessibility and updates to `allowed_devices` ([#6961](https://github.com/flet-dev/flet/issues/6961), [#6982](https://github.com/flet-dev/flet/pull/6982)) by @inchang-ing. * Honor an explicit platform `app.hide_window_on_start = false` when the global setting is `true`, in both the Dart bootstrap and Windows runner ([#6949](https://github.com/flet-dev/flet/issues/6949), [#6981](https://github.com/flet-dev/flet/pull/6981)) by @LaoChouPro. +* Honor an empty `[tool.flet.]` value for `artifact`, `org`, `bundle_id`, `icon_background` and `splash` settings in `flet build`, which now clears the global value for that platform instead of being ignored ([#6949](https://github.com/flet-dev/flet/issues/6949)) by @AbhayPhalswal. * Fix a declarative component moved to a structurally different parent (for example a `Column` replaced by a `Row` when a responsive layout switches) being re-rendered as a fresh, never-mounted copy: its state was lost, its mount effects never ran and later state updates were ignored until a page reload, while the old copy stayed in the session index. A moved, already-mounted component now keeps its body ([#6970](https://github.com/flet-dev/flet/issues/6970)) by @FeodorFitsner. * Fix a client method call (e.g. `FletApp.wait_idle()`) failing with "Concurrent modification during iteration" when the control's widget was rebuilt while the call was in progress by @FeodorFitsner. * Fix an app freezing - no more events or updates processed - when a client method result (e.g. an embedded `FletApp`'s `wait_idle()`) arrived after its control had been removed: the session raised on it, which ended the connection's receive loop. The result now reaches its caller, and every transport (Pyodide, socket, FastAPI) logs a failing message and keeps receiving instead of stopping by @FeodorFitsner. diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py index ab598fe331..224f795d89 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py @@ -1097,8 +1097,7 @@ def setup_template_data(self): ) artifact_name = ( self.options.artifact_name - or self.get_pyproject(f"tool.flet.{self.config_platform}.artifact") - or self.get_pyproject("tool.flet.artifact") + or self.get_platform_setting("artifact") or self.default_artifact_name( self.config_platform, product_name, project_name_raw ) @@ -1565,10 +1564,7 @@ def _xml_attr_value(v): # through to it. splash = self._resolve_splash() org_name = ( - self.options.org_name - or self.get_pyproject(f"tool.flet.{self.config_platform}.org") - or self.get_pyproject("tool.flet.org") - or "com.flet" + self.options.org_name or self.get_platform_setting("org") or "com.flet" ) self.template_data = { @@ -1649,8 +1645,7 @@ def _xml_attr_value(v): # The template derives nothing from these: the build names files # after the bundle ID too, such as the app's Linux icons. "bundle_id": self.options.bundle_id - or self.get_pyproject(f"tool.flet.{self.config_platform}.bundle_id") - or self.get_pyproject("tool.flet.bundle_id") + or self.get_platform_setting("bundle_id") or f"{org_name}.{project_name_slug}", "company_name": ( self.options.company_name or self.get_pyproject("tool.flet.company") @@ -1759,10 +1754,11 @@ def splash_setting(self, key: str): """ One splash setting, platform-specific over global. - The same precedence the splash colours use, so every key under - `[tool.flet.splash]` can be overridden per platform without each one - spelling the lookup out again. A key that was renamed is read under - its former name too, so existing configuration keeps working. + Uses the precedence of `get_platform_setting`, so every key under + `[tool.flet.splash]` can be overridden per platform, even with an empty + value, without each one spelling the lookup out again. A key that was + renamed is read under its former name too, so existing configuration + keeps working. Args: key: Setting name below the `splash` table. @@ -1771,14 +1767,10 @@ def splash_setting(self, key: str): The configured value, or `None`. """ - assert self.get_pyproject - for name in (key, SPLASH_KEY_ALIASES.get(key)): if name is None: continue - value = self.get_pyproject( - f"tool.flet.{self.config_platform}.splash.{name}" - ) or self.get_pyproject(f"tool.flet.splash.{name}") + value = self.get_platform_setting(f"splash.{name}") if value is not None: return value return None @@ -2327,11 +2319,7 @@ def icon_background_value(self, platform: str) -> Optional[str]: The colour string, or `None` when nothing is configured. """ - assert self.get_pyproject - - return self.get_pyproject( - f"tool.flet.{platform}.icon_background" - ) or self.get_pyproject("tool.flet.icon_background") + return self.get_platform_setting("icon_background", platform=platform) def _resolve_icon_background(self) -> tuple[int, int, int]: """ @@ -3326,18 +3314,20 @@ def resolve_permissions(self) -> list[str]: self.options.permissions or self.get_platform_setting("permissions") or [] ) - def get_platform_setting(self, pyproj_setting, default_value=None): + def get_platform_setting(self, pyproj_setting, default_value=None, platform=None): """ Resolve a pyproject setting with precedence: platform, global, default. - The platform value wins whenever it is set, even when it is empty, so - `[]` or `{}` under `tool.flet..` clears a global list or table - for that platform. + The platform value wins whenever it is set, even when it is empty or + false, so `[]`, `{}` or `""` under `tool.flet..` clears a + global value for that platform. Args: pyproj_setting: Relative key under `tool.flet..` and `tool.flet.`. default_value: Fallback value when neither key is defined. + platform: Platform whose table overrides the global one. Defaults + to the platform being built. Returns: Resolved setting value. @@ -3345,7 +3335,7 @@ def get_platform_setting(self, pyproj_setting, default_value=None): assert self.get_pyproject for key in ( - f"tool.flet.{self.config_platform}.{pyproj_setting}", + f"tool.flet.{platform or self.config_platform}.{pyproj_setting}", f"tool.flet.{pyproj_setting}", ): value = self.get_pyproject(key) diff --git a/sdk/python/packages/flet-cli/tests/test_build_platform_settings.py b/sdk/python/packages/flet-cli/tests/test_build_platform_settings.py index ba44ed4175..d737967b97 100644 --- a/sdk/python/packages/flet-cli/tests/test_build_platform_settings.py +++ b/sdk/python/packages/flet-cli/tests/test_build_platform_settings.py @@ -1,5 +1,7 @@ """Platform-over-global precedence of `[tool.flet]` settings in `flet build`.""" +from types import SimpleNamespace + import pytest from flet_cli.commands.build_base import BaseBuildCommand @@ -54,6 +56,55 @@ def test_other_platform_value_ignored(): assert cmd.get_platform_setting("dev_packages") == {"flet": "../flet"} +def test_explicit_platform_overrides_the_build_platform(): + cmd = _command( + { + "tool.flet.android.icon_background": "#003366", + "tool.flet.icon_background": "#1a1a2e", + }, + platform="macos", + ) + assert cmd.get_platform_setting("icon_background", platform="android") == "#003366" + assert cmd.get_platform_setting("icon_background") == "#1a1a2e" + + +@pytest.mark.parametrize( + "key", + ["color", "dark_color", "icon_background", "icon_dark_background", "icon_fit"], +) +def test_empty_platform_splash_value_clears_global(key): + cmd = _command( + {f"tool.flet.android.splash.{key}": "", f"tool.flet.splash.{key}": "global"} + ) + assert cmd.splash_setting(key) == "" + + +def test_empty_platform_splash_colour_falls_back_to_the_default(): + cmd = _command( + {"tool.flet.android.splash.color": "", "tool.flet.splash.color": "#000000"} + ) + cmd.options = SimpleNamespace( + no_android_splash=None, + no_ios_splash=None, + no_web_splash=None, + splash_color=None, + splash_dark_color=None, + ) + assert cmd._resolve_splash()["color"] == "#ffffff" + + +def test_empty_platform_icon_background_falls_back_to_white(): + cmd = _command( + { + "tool.flet.macos.icon_background": "", + "tool.flet.icon_background": "#1a1a2e", + }, + platform="macos", + ) + assert cmd.icon_background_value("macos") == "" + assert cmd._resolve_icon_background() == (255, 255, 255) + + @pytest.mark.parametrize( ("cli", "pyproject", "expected"), [ From c210301c0c85d5666f12161997b37a6bcfc1fbb2 Mon Sep 17 00:00:00 2001 From: Abhay Phalswal Date: Mon, 2 Nov 2026 11:36:32 +0530 Subject: [PATCH 2/3] Link #7040 in the changelog entry Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c39d52ba9a..48aedf085a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,7 +77,7 @@ * Fix `Tester.find_by_key()` not finding `Tab` and `PopupMenuItem` controls by their keys ([#6965](https://github.com/flet-dev/flet/issues/6965), [#6976](https://github.com/flet-dev/flet/pull/6976)) by @RezaShamsipour. * Fix `GestureDetector.on_tap_move` not firing, while preserving tap accessibility and updates to `allowed_devices` ([#6961](https://github.com/flet-dev/flet/issues/6961), [#6982](https://github.com/flet-dev/flet/pull/6982)) by @inchang-ing. * Honor an explicit platform `app.hide_window_on_start = false` when the global setting is `true`, in both the Dart bootstrap and Windows runner ([#6949](https://github.com/flet-dev/flet/issues/6949), [#6981](https://github.com/flet-dev/flet/pull/6981)) by @LaoChouPro. -* Honor an empty `[tool.flet.]` value for `artifact`, `org`, `bundle_id`, `icon_background` and `splash` settings in `flet build`, which now clears the global value for that platform instead of being ignored ([#6949](https://github.com/flet-dev/flet/issues/6949)) by @AbhayPhalswal. +* Honor an empty `[tool.flet.]` value for `artifact`, `org`, `bundle_id`, `icon_background` and `splash` settings in `flet build`, which now clears the global value for that platform instead of being ignored ([#6949](https://github.com/flet-dev/flet/issues/6949), [#7040](https://github.com/flet-dev/flet/pull/7040)) by @AbhayPhalswal. * Fix a declarative component moved to a structurally different parent (for example a `Column` replaced by a `Row` when a responsive layout switches) being re-rendered as a fresh, never-mounted copy: its state was lost, its mount effects never ran and later state updates were ignored until a page reload, while the old copy stayed in the session index. A moved, already-mounted component now keeps its body ([#6970](https://github.com/flet-dev/flet/issues/6970)) by @FeodorFitsner. * Fix a client method call (e.g. `FletApp.wait_idle()`) failing with "Concurrent modification during iteration" when the control's widget was rebuilt while the call was in progress by @FeodorFitsner. * Fix an app freezing - no more events or updates processed - when a client method result (e.g. an embedded `FletApp`'s `wait_idle()`) arrived after its control had been removed: the session raised on it, which ended the connection's receive loop. The result now reaches its caller, and every transport (Pyodide, socket, FastAPI) logs a failing message and keeps receiving instead of stopping by @FeodorFitsner. From f623c379bccb6ce1e0f9530072598827e9f2b41e Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 2 Nov 2026 20:50:26 +0100 Subject: [PATCH 3/3] Test platform identity settings through the template data and document empty overrides --- .../tests/test_build_platform_settings.py | 51 +++++++++++++++++++ website/docs/publish/index.md | 12 +++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/sdk/python/packages/flet-cli/tests/test_build_platform_settings.py b/sdk/python/packages/flet-cli/tests/test_build_platform_settings.py index d737967b97..5effd5b1bd 100644 --- a/sdk/python/packages/flet-cli/tests/test_build_platform_settings.py +++ b/sdk/python/packages/flet-cli/tests/test_build_platform_settings.py @@ -1,6 +1,8 @@ """Platform-over-global precedence of `[tool.flet]` settings in `flet build`.""" +import argparse from types import SimpleNamespace +from unittest.mock import MagicMock import pytest @@ -57,6 +59,7 @@ def test_other_platform_value_ignored(): def test_explicit_platform_overrides_the_build_platform(): + """An explicit `platform` reads that platform's table, not the built one's.""" cmd = _command( { "tool.flet.android.icon_background": "#003366", @@ -73,6 +76,7 @@ def test_explicit_platform_overrides_the_build_platform(): ["color", "dark_color", "icon_background", "icon_dark_background", "icon_fit"], ) def test_empty_platform_splash_value_clears_global(key): + """An empty platform splash value replaces the global one.""" cmd = _command( {f"tool.flet.android.splash.{key}": "", f"tool.flet.splash.{key}": "global"} ) @@ -80,6 +84,7 @@ def test_empty_platform_splash_value_clears_global(key): def test_empty_platform_splash_colour_falls_back_to_the_default(): + """An empty platform splash colour resolves to the default colour.""" cmd = _command( {"tool.flet.android.splash.color": "", "tool.flet.splash.color": "#000000"} ) @@ -94,6 +99,7 @@ def test_empty_platform_splash_colour_falls_back_to_the_default(): def test_empty_platform_icon_background_falls_back_to_white(): + """An empty platform icon background resolves to white.""" cmd = _command( { "tool.flet.macos.icon_background": "", @@ -105,6 +111,51 @@ def test_empty_platform_icon_background_falls_back_to_white(): assert cmd._resolve_icon_background() == (255, 255, 255) +def _template_data(pyproject, tmp_path): + """Build the template data of an Android build with the real options.""" + parser = argparse.ArgumentParser(add_help=False) + cmd = BaseBuildCommand(parser) + cmd.options = parser.parse_args([]) + cmd.python_app_path = tmp_path + cmd.flutter_dir = tmp_path + cmd.config_platform = "android" + cmd.target_platform = "apk" + cmd.package_platform = "Android" + cmd.python_release = MagicMock(android_abis=["arm64-v8a"]) + values = {"project.name": "my-app", **pyproject} + cmd.get_pyproject = lambda key=None: values.get(key) if key else {} + cmd.setup_template_data() + return cmd.template_data + + +@pytest.mark.parametrize( + ("platform_value", "org_name", "artifact_name", "bundle_id"), + [ + ("", "com.flet", "my-app", "com.flet.my-app"), + (None, "com.global", "global", "com.global.id"), + ("android", "android", "android", "android"), + ], +) +def test_identity_settings_reach_the_template( + tmp_path, platform_value, org_name, artifact_name, bundle_id +): + """`org`, `artifact` and `bundle_id` resolve platform over global over default.""" + data = _template_data( + { + "tool.flet.org": "com.global", + "tool.flet.artifact": "global", + "tool.flet.bundle_id": "com.global.id", + "tool.flet.android.org": platform_value, + "tool.flet.android.artifact": platform_value, + "tool.flet.android.bundle_id": platform_value, + }, + tmp_path, + ) + assert data["org_name"] == org_name + assert data["artifact_name"] == artifact_name + assert data["bundle_id"] == bundle_id + + @pytest.mark.parametrize( ("cli", "pyproject", "expected"), [ diff --git a/website/docs/publish/index.md b/website/docs/publish/index.md index 43fccd5958..ccf032470d 100644 --- a/website/docs/publish/index.md +++ b/website/docs/publish/index.md @@ -252,10 +252,14 @@ Throughout this documentation, the following placeholders are used: :::note[Platform-specific overrides] When a setting can be set both under `[tool.flet]` and under `[tool.flet.]`, -the platform value wins whenever it is present. For list and table settings (such as -`app.exclude`, `app.include`, `source_packages`, `dev_packages`, `target_arch`, -`cleanup.app_files`, `cleanup.package_files` and `flutter.build_args`), an empty value -clears the global one for that platform: +the platform value wins whenever it is present, including `false` and empty strings. For +example, `hide_window_on_start = false` under `[tool.flet.macos.app]` shows the app at +startup on macOS even when `[tool.flet.app]` hides it, and `org = ""` under +`[tool.flet.ios]` drops the global `org` on iOS, so the default applies. + +For list and table settings (such as `app.exclude`, `app.include`, `source_packages`, +`dev_packages`, `target_arch`, `cleanup.app_files`, `cleanup.package_files` and +`flutter.build_args`), an empty value clears the global one for that platform: ```toml [tool.flet]