Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<platform>]` 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.
Expand Down
44 changes: 17 additions & 27 deletions sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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]:
"""
Expand Down Expand Up @@ -3326,26 +3314,28 @@ 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.<platform>.` 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.<platform>.` clears a
global value for that platform.

Args:
pyproj_setting: Relative key under `tool.flet.<platform>.` 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.
"""

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)
Expand Down
102 changes: 102 additions & 0 deletions sdk/python/packages/flet-cli/tests/test_build_platform_settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"""Platform-over-global precedence of `[tool.flet]` settings in `flet build`."""

import argparse
from types import SimpleNamespace
from unittest.mock import MagicMock

import pytest

from flet_cli.commands.build_base import BaseBuildCommand
Expand Down Expand Up @@ -54,6 +58,104 @@ def test_other_platform_value_ignored():
assert cmd.get_platform_setting("dev_packages") == {"flet": "../flet"}


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",
"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):
"""An empty platform splash value replaces the global one."""
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():
"""An empty platform splash colour resolves to the default colour."""
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():
"""An empty platform icon background resolves 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)


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"),
[
Expand Down
12 changes: 8 additions & 4 deletions website/docs/publish/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<PLATFORM>]`,
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]
Expand Down
Loading