Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

* Fix the bundled Pyodide URL being origin-absolute in `--no-cdn` builds, so a sub-path deployment (`flet build web --base-url myapp`) requested `/pyodide/pyodide.mjs` and got a 404 while the file sat at `/myapp/pyodide/pyodide.mjs`. It now renders relative to the configured base URL, as `canvasKitBaseUrl` does. Builds without `--base-url` render exactly the same URL as before by @FeodorFitsner.

* Fix integration tests failing to start when the host Python environment carries IDE configuration: `flutter test` exited with code 79 and "No tests were found" while the `flet_app` fixture failed during setup. `FletTestApp` launched the Flutter test process with the host environment inherited wholesale, and the interpreter embedded in the app under test reads `PYTHONPATH`/`PYTHONHOME` at initialization - so the debugger and `sitecustomize` paths PyCharm injects landed on the packaged app's `sys.path` and killed it before it could connect to `RemoteTester`. Since the failure happened inside the app rather than in the test process, it surfaced only as a Flutter exit code, which made it look like the tests themselves were missing. The Flutter subprocess now gets an explicit environment with `PYTHONPATH`, `PYTHONHOME` and `PYTHONEXECUTABLE` removed and `PYTHONNOUSERSITE=1` set - user site-packages is opt-out, so a host `~/.local/lib/pythonX.Y/site-packages` matching the embedded interpreter's version leaks in the same way. `PATH`, and every `FLET_*` and `SERIOUS_PYTHON_*` variable the native build phase needs, are untouched ([#6747](https://github.com/flet-dev/flet/pull/6747)) by @PythBuster.

### Improvements

* `flet build web` and `flet publish` no longer bundle CanvasKit and Pyodide when CDN mode is on (the default), taking a minimal web build from **71 MB to 19 MB**. In CDN mode Flutter loads CanvasKit from `gstatic.com` and Flet points `pyodideUrl` at jsdelivr, so both copies were dead weight the browser never requested — yet `flutter build web` always emits `canvaskit/` (~37 MB), and `ensure_pyodide()` ran unconditionally in both commands, downloading and copying a further ~15 MB. Neither is fetched, so nothing about how a CDN-mode app loads changes; verified with a network log showing the built app pulling `chromium/canvaskit.{js,wasm}` from gstatic and the full Pyodide runtime from jsdelivr, with no request to a local `canvaskit/` or `pyodide/` path. `--no-cdn` (or `[tool.flet.web] cdn = false`) still bundles everything and is unchanged. `flet build` also clears a `pyodide/` left in the reused Flutter project by an earlier `--no-cdn` build, so switching modes doesn't silently keep shipping it by @FeodorFitsner.
Expand Down
35 changes: 27 additions & 8 deletions sdk/python/packages/flet/src/flet/testing/flet_test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,32 @@
__all__ = ["FletTestApp"]


def _flutter_subprocess_env() -> dict[str, str]:
"""
Build the environment for the `flutter test` child process.

Host-Python configuration must not reach the interpreter embedded in the
app under test. It reads `PYTHONPATH`/`PYTHONHOME` at initialization, so
the debugger and `sitecustomize` paths an IDE injects (PyCharm does) land
on the packaged app's `sys.path` and kill it at startup - which surfaces
as Flutter exiting with code 79 and "No tests were found", before the app
ever connects to `RemoteTester`.

`PYTHONNOUSERSITE` is *set* rather than removed: user site-packages is
opt-out, so leaving it unset lets a version-matched host
`~/.local/lib/pythonX.Y/site-packages` leak in the same way.

Everything else is preserved - `flutter test` needs `PATH`, and its native
build phase needs the `FLET_*` and `SERIOUS_PYTHON_*` variables that
`flet test` sets (see `_flutter_path_env` in `flet_cli.commands.test`).
"""
env = os.environ.copy()
for name in ("PYTHONPATH", "PYTHONHOME", "PYTHONEXECUTABLE"):
env.pop(name, None)
env["PYTHONNOUSERSITE"] = "1"
return env


class DisposalMode(Enum):
"""
Indicates the way in which a frame is treated after being displayed.
Expand Down Expand Up @@ -314,19 +340,12 @@ async def main(page: ft.Page):
f"--dart-define=FLET_TEST_ASSETS_DIR={self.__assets_dir}"
]

# Do not leak host-Python configuration into Flutter's embedded Python
# IDEs such as PyCharm add debugger/sitecustomize modules through
# PYTHONPATH, which can break the packaged integration-test app.
flutter_env = os.environ.copy()
flutter_env.pop("PYTHONPATH", None)
flutter_env.pop("PYTHONHOME", None)

self.__flutter_process = await asyncio.create_subprocess_exec(
*flutter_args,
cwd=str(self.__flutter_app_dir),
stdout=stdout,
stderr=stderr,
env=flutter_env,
env=_flutter_subprocess_env(),
)

if self.__flutter_process.stdout is not None:
Expand Down
64 changes: 64 additions & 0 deletions sdk/python/packages/flet/tests/test_flet_test_app_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import os
from unittest.mock import patch

from flet.testing.flet_test_app import _flutter_subprocess_env

# A host environment as an IDE-launched pytest run sees it: PyCharm's debugger
# and sitecustomize helpers on PYTHONPATH, a Homebrew interpreter's PYTHONHOME,
# plus the variables `flet test` sets for the native build phase.
HOST_ENV = {
"PATH": "/usr/local/bin:/usr/bin",
"PYTHONPATH": "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev",
"PYTHONHOME": "/opt/homebrew/opt/python@3.13/Frameworks/Python.framework",
"PYTHONEXECUTABLE": "/opt/homebrew/bin/python3.13",
"FLET_TEST_FLUTTER_EXE": "/opt/flutter/bin/flutter",
"FLET_TEST_DEVICE_MODE": "1",
"SERIOUS_PYTHON_SITE_PACKAGES": "/tmp/app/site-packages",
"SP_NATIVE_SET": "1",
}


def test_host_python_config_is_stripped():
with patch.dict(os.environ, HOST_ENV, clear=True):
env = _flutter_subprocess_env()

assert "PYTHONPATH" not in env
assert "PYTHONHOME" not in env
assert "PYTHONEXECUTABLE" not in env


def test_user_site_packages_is_disabled():
# Opt-out, so it must be set rather than removed - a host user site dir
# matching the embedded interpreter's version leaks in otherwise.
with patch.dict(os.environ, HOST_ENV, clear=True):
assert _flutter_subprocess_env()["PYTHONNOUSERSITE"] == "1"


def test_build_env_is_preserved():
with patch.dict(os.environ, HOST_ENV, clear=True):
env = _flutter_subprocess_env()

for name in (
"PATH",
"FLET_TEST_FLUTTER_EXE",
"FLET_TEST_DEVICE_MODE",
"SERIOUS_PYTHON_SITE_PACKAGES",
"SP_NATIVE_SET",
):
assert env[name] == HOST_ENV[name]


def test_missing_vars_are_not_an_error():
with patch.dict(os.environ, {"PATH": "/usr/bin"}, clear=True):
env = _flutter_subprocess_env()

assert env == {"PATH": "/usr/bin", "PYTHONNOUSERSITE": "1"}


def test_host_environ_is_not_mutated():
with patch.dict(os.environ, HOST_ENV, clear=True):
_flutter_subprocess_env()

assert os.environ["PYTHONPATH"] == HOST_ENV["PYTHONPATH"]
assert os.environ["PYTHONHOME"] == HOST_ENV["PYTHONHOME"]
assert "PYTHONNOUSERSITE" not in os.environ
Loading