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
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
14 changes: 6 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 @@ -18,6 +18,7 @@
from flet.controls.control import Control
from flet.testing.remote_tester import RemoteTester
from flet.testing.tester import Tester
from flet.utils.environment import without_host_python_config
from flet.utils.network import get_free_tcp_port
from flet.utils.platform_utils import get_bool_env_var

Expand Down Expand Up @@ -314,19 +315,16 @@ 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,
# `flutter test` builds and runs the app under test, which embeds
# its own interpreter - the host's Python configuration must not
# reach it. PATH and the FLET_*/SERIOUS_PYTHON_* variables that
# `flet test` sets for the native build phase are preserved.
env=without_host_python_config(),
)

if self.__flutter_process.stdout is not None:
Expand Down
38 changes: 38 additions & 0 deletions sdk/python/packages/flet/src/flet/utils/environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import os
from collections.abc import Mapping
from typing import Optional

# Variables through which a host interpreter's configuration reaches any Python
# started underneath it. `PYTHONNOUSERSITE` is absent on purpose: user
# site-packages is opt-out, so it has to be *set* rather than removed.
_HOST_PYTHON_CONFIG_VARS = ("PYTHONPATH", "PYTHONHOME", "PYTHONEXECUTABLE")


def without_host_python_config(
env: Optional[Mapping[str, str]] = None,
) -> dict[str, str]:
"""
Copy `env` (defaults to `os.environ`) with this process's Python
configuration removed, for handing to a child that embeds its own
interpreter.

An embedded interpreter reads `PYTHONPATH`/`PYTHONHOME` at initialization,
so anything the host has on them - notably the debugger and
`sitecustomize` paths an IDE injects, PyCharm being the usual source -
lands on the child's `sys.path` and is imported at its startup, where those
modules do not belong and typically kill it. `PYTHONEXECUTABLE` is dropped
for the same reason: macOS framework builds use it to seed
`sys.executable`, and the host's value points at the host's interpreter.

`PYTHONNOUSERSITE` is set rather than removed, because user site-packages
is opt-out: leaving it unset lets a host `~/.local/lib/pythonX.Y/site-packages`
whose version happens to match the embedded interpreter's leak in the same
way.

Every other variable is preserved.
"""
result = dict(os.environ if env is None else env)
for name in _HOST_PYTHON_CONFIG_VARS:
result.pop(name, None)
result["PYTHONNOUSERSITE"] = "1"
return result
70 changes: 70 additions & 0 deletions sdk/python/packages/flet/tests/test_environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import os
from unittest.mock import patch

from flet.utils.environment import without_host_python_config

# 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():
env = without_host_python_config(HOST_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.
assert without_host_python_config(HOST_ENV)["PYTHONNOUSERSITE"] == "1"


def test_build_env_is_preserved():
env = without_host_python_config(HOST_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():
assert without_host_python_config({"PATH": "/usr/bin"}) == {
"PATH": "/usr/bin",
"PYTHONNOUSERSITE": "1",
}


def test_source_mapping_is_not_mutated():
source = dict(HOST_ENV)
without_host_python_config(source)

assert source == HOST_ENV


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

assert "PYTHONPATH" not in env
assert env["PATH"] == HOST_ENV["PATH"]
# The live environment of the *host* process is left alone.
assert os.environ["PYTHONPATH"] == HOST_ENV["PYTHONPATH"]
assert "PYTHONNOUSERSITE" not in os.environ
Loading