From 9abc68b309adbe0da4117f5b96621d091f26451a Mon Sep 17 00:00:00 2001 From: td3447 <59424730+td3447@users.noreply.github.com> Date: Sat, 21 Mar 2026 13:56:36 -0400 Subject: [PATCH 1/5] fix(flet-cli): correct dependency parsing (#6332) Closes issue: #6332 - Adds a space (' ') in front of dependencies in `pyproject.toml` that have version constraints starting with `<` or `<=`. - Fixes the issue where `flet build` would give a `The system cannot find the file specified.` error. --- .../src/flet_cli/utils/project_dependencies.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py b/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py index 2cae6d72d0..35e356f94f 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py @@ -128,6 +128,15 @@ def get_project_dependencies( if project_dependencies is None: return None - dependencies = set(project_dependencies) + dependencies: set[str] = set() + + for dep in project_dependencies: + for sep in ("<=", "<"): + if sep in dep and "," not in dep: + value, _, suffix = dep.partition(sep) + dependencies.add(f"{value} {sep}{suffix}") + break + else: + dependencies.add(dep.strip()) return sorted(dependencies) From e1a6d1ef4f605ea13c4f666b42ae13b9a9fe21f8 Mon Sep 17 00:00:00 2001 From: td3447 <59424730+td3447@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:46:09 -0400 Subject: [PATCH 2/5] fix(flet-cli): correct dependency parsing - poetry (#6332) - Fixed parsing issue for dependencies in [tool.poetry.dependencies] - Adds single space ' ' to the front of the `<` or `<=` to prevent `The system cannot find the file specified.` error. --- .../flet-cli/src/flet_cli/utils/project_dependencies.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py b/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py index 35e356f94f..404ad9dffe 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py @@ -97,7 +97,10 @@ def format_dependency_version(dependency_name: str, dependency_value: Any): sep = "~=" value = value[1:] return f"{dependency_name}~={value[1:]}" - elif "<" in value or ">" in value: + elif "<=" in value or "<" in value: + sep = " " + value = value.replace(" ", "") + elif ">" in value: sep = "" value = value.replace(" ", "") From d20311afd8039bfb5e93d8be71f85b38f2fb61e3 Mon Sep 17 00:00:00 2001 From: td3447 <59424730+td3447@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:15:53 -0400 Subject: [PATCH 3/5] fix(flet-cli): correct dependency parsing - Fixed an error where a `<` after a `,` was not recognized as a dependency. - Added more checks to ensure a wider variety of typos are covered. - Fixed an issue where using a `*` wouldn't correctly look for any version and raise an error. - Fixed an issue where using a `!` after a `,` would raise an error, since it was missing the required ` ` in front. - Ensured the correct separators were added for all the types of requirements I could think of - Added support for clearing whitespace from uv dependencies, in the same way used with the poetry dependencies. - Tests were conducted on both poetry and uv, using `flet build` --- .../flet_cli/utils/project_dependencies.py | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py b/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py index 404ad9dffe..35df3b3c5c 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py @@ -46,8 +46,7 @@ def format_dependency_version(dependency_name: str, dependency_value: Any): if isinstance(dependency_value, dict): version = dependency_value.get("version") if version: - sep = "==" - value = version + value = version.replace(" ", "") else: git_url = dependency_value.get("git") if git_url: @@ -87,22 +86,27 @@ def format_dependency_version(dependency_name: str, dependency_value: Any): if markers is not None: suffix = f";{markers}" else: - value = dependency_value - sep = "==" + value = dependency_value.replace(" ", "") + sep = "" if value.startswith("^"): sep = ">=" value = value[1:] + elif "*" in value: + sep = "" + value = "" elif value.startswith("~"): - sep = "~=" - value = value[1:] - return f"{dependency_name}~={value[1:]}" - elif "<=" in value or "<" in value: - sep = " " - value = value.replace(" ", "") - elif ">" in value: sep = "" - value = value.replace(" ", "") + if "~=" not in value: + value = value.replace("~", "~=") + elif any(s in value for s in ("!", "<", ">")): + sep = "" + for s in ("!", "<"): + if s in value: + value = value.replace(s, f" {s}") + break + elif value[0].isdigit(): + sep = "==" return f"{dependency_name}{sep}{value}{suffix}" @@ -134,12 +138,13 @@ def get_project_dependencies( dependencies: set[str] = set() for dep in project_dependencies: - for sep in ("<=", "<"): - if sep in dep and "," not in dep: + for sep in ("<=", "<", "!"): + if sep in dep: + dep = dep.replace(" ", "") value, _, suffix = dep.partition(sep) dependencies.add(f"{value} {sep}{suffix}") break else: - dependencies.add(dep.strip()) + dependencies.add(dep.replace(" ", "")) return sorted(dependencies) From a60cec1281abe18b65d946bc99e0d9799ed3398e Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 30 Mar 2026 14:09:21 -0700 Subject: [PATCH 4/5] fix(flet-cli): use packaging library for reliable dependency parsing Replace fragile manual string manipulation with packaging.requirements.Requirement for PEP 508 parsing. Fixes Windows shell redirection issues for both < and > operators, and adds unit tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 1 + .../flet_cli/utils/project_dependencies.py | 197 +++++++------- .../tests/test_project_dependencies.py | 241 ++++++++++++++++++ 3 files changed, 332 insertions(+), 107 deletions(-) create mode 100644 sdk/python/packages/flet-cli/tests/test_project_dependencies.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4c9e22d3c..71752e8fe6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,7 @@ jobs: working-directory: ${{ env.SDK_PYTHON }} run: | uv run --no-dev --group test pytest packages/flet/tests + uv run --no-dev --group test pytest packages/flet-cli/tests - name: Run docs-coverage if: matrix.python-version == '3.12' diff --git a/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py b/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py index 35df3b3c5c..f3d32f2c9e 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py @@ -2,8 +2,86 @@ # Based on: https://pypi.org/project/toml-to-requirements/ +import re from typing import Any, Optional +from packaging.requirements import Requirement + + +def _windows_safe(req_str: str) -> str: + """Insert a space before bare ``<`` or ``>`` so Windows cmd.exe does not + interpret them as shell redirection when the string is passed via ``-r`` + to a ``.BAT`` subprocess.""" + return re.sub(r"(?<=[^ ])([<>])", r" \1", req_str) + + +def _poetry_version_to_pep440(version: str) -> str: + """Convert a Poetry version constraint to PEP 440 syntax. + + - ``^1.2.3`` → ``>=1.2.3`` + - ``~1.2.3`` → ``~=1.2.3`` (``~=`` passes through unchanged) + - ``*`` → ``""`` (no constraint) + - ``1.2.3`` (bare version) → ``==1.2.3`` + - Anything else is returned as-is (already PEP 440). + """ + version = version.replace(" ", "") + if not version or version == "*": + return "" + if version.startswith("^"): + return f">={version[1:]}" + if version.startswith("~") and not version.startswith("~="): + return f"~={version[1:]}" + # Bare version number → pin with == + if version[0].isdigit(): + return f"=={version}" + return version + + +def _poetry_dep_to_pep508(name: str, value: Any) -> str: + """Convert a single Poetry dependency entry to a PEP 508 requirement string.""" + suffix = "" + + if isinstance(value, dict): + version = value.get("version") + if version: + specifier = _poetry_version_to_pep440(version) + markers = value.get("markers") + if markers is not None: + suffix = f"; {markers}" + if specifier: + return f"{name}{specifier}{suffix}" + return f"{name}{suffix}" + + git_url = value.get("git") + if git_url: + url = f"git+{git_url}" if not git_url.startswith("git@") else git_url + rev = value.get("branch") or value.get("rev") or value.get("tag") + if rev: + url = f"{url}@{rev}" + subdirectory = value.get("subdirectory") + if subdirectory: + url = f"{url}#subdirectory={subdirectory}" + markers = value.get("markers") + if markers is not None: + suffix = f"; {markers}" + return f"{name} @ {url}{suffix}" + + path = value.get("path") + if path: + return path + + url = value.get("url") + if url: + return url + + raise ValueError(f"Unsupported dependency specification: {name} = {value}") + + # String value + specifier = _poetry_version_to_pep440(value) + if specifier: + return f"{name}{specifier}" + return name + def get_poetry_dependencies( poetry_dependencies: Optional[dict[str, Any]] = None, @@ -17,134 +95,39 @@ def get_poetry_dependencies( Returns: Sorted requirement strings or `None` when `poetry_dependencies` is `None`. """ - if poetry_dependencies is None: return None - def format_dependency_version(dependency_name: str, dependency_value: Any): - """ - Format a single Poetry dependency entry as a requirement specifier. - - Supports version constraints, git dependencies (including branch/rev/tag - and subdirectory), path/url dependencies, and optional environment markers. - - Args: - dependency_name: Dependency key in Poetry configuration. - dependency_value: String or mapping that describes the dependency. - - Returns: - A requirement string consumable by pip-style tooling. - - Raises: - ValueError: If the dependency mapping uses an unsupported shape. - """ - - sep = "@" - value = "" - suffix = "" - - if isinstance(dependency_value, dict): - version = dependency_value.get("version") - if version: - value = version.replace(" ", "") - else: - git_url = dependency_value.get("git") - if git_url: - value = ( - f"git+{git_url}" if not git_url.startswith("git@") else git_url - ) - rev = ( - dependency_value.get("branch") - or dependency_value.get("rev") - or dependency_value.get("tag") - ) - if rev: - value = f"{value}@{rev}" - subdirectory = dependency_value.get("subdirectory") - if subdirectory: - value = f"{value}#subdirectory={subdirectory}" - else: - path = dependency_value.get("path") - if path: - value = path - dependency_name = "" - sep = "" - else: - url = dependency_value.get("url") - if url: - value = url - dependency_name = "" - sep = "" - else: - raise ValueError( - "Unsupported dependency specification: " - f"{dependency_name} = {dependency_value}" - ) - - # markers - common for all - markers = dependency_value.get("markers") - if markers is not None: - suffix = f";{markers}" - else: - value = dependency_value.replace(" ", "") - sep = "" - - if value.startswith("^"): - sep = ">=" - value = value[1:] - elif "*" in value: - sep = "" - value = "" - elif value.startswith("~"): - sep = "" - if "~=" not in value: - value = value.replace("~", "~=") - elif any(s in value for s in ("!", "<", ">")): - sep = "" - for s in ("!", "<"): - if s in value: - value = value.replace(s, f" {s}") - break - elif value[0].isdigit(): - sep = "==" - - return f"{dependency_name}{sep}{value}{suffix}" - dependencies: set[str] = { - format_dependency_version(dependency, version) - for dependency, version in poetry_dependencies.items() - if dependency != "python" + _windows_safe(_poetry_dep_to_pep508(dep, ver)) + for dep, ver in poetry_dependencies.items() + if dep != "python" } return sorted(dependencies) def get_project_dependencies( - project_dependencies: Optional[dict[str, Any]] = None, + project_dependencies: Optional[list[str]] = None, ) -> Optional[list[str]]: """ - Normalize PEP 621 `project.dependencies` into a sorted unique list. + Normalize PEP 621 ``project.dependencies`` into a sorted unique list. Args: - project_dependencies: Value from `project.dependencies`. + project_dependencies: Value from ``project.dependencies``. Returns: - Sorted dependency strings, or `None` when input is `None`. + Sorted dependency strings, or ``None`` when input is ``None``. """ - if project_dependencies is None: return None dependencies: set[str] = set() - for dep in project_dependencies: - for sep in ("<=", "<", "!"): - if sep in dep: - dep = dep.replace(" ", "") - value, _, suffix = dep.partition(sep) - dependencies.add(f"{value} {sep}{suffix}") - break - else: - dependencies.add(dep.replace(" ", "")) + try: + req = Requirement(dep) + dependencies.add(_windows_safe(str(req))) + except Exception: + dependencies.add(_windows_safe(dep)) return sorted(dependencies) diff --git a/sdk/python/packages/flet-cli/tests/test_project_dependencies.py b/sdk/python/packages/flet-cli/tests/test_project_dependencies.py new file mode 100644 index 0000000000..900b575b29 --- /dev/null +++ b/sdk/python/packages/flet-cli/tests/test_project_dependencies.py @@ -0,0 +1,241 @@ +"""Tests for project_dependencies module.""" + +import pytest + +from flet_cli.utils.project_dependencies import ( + get_poetry_dependencies, + get_project_dependencies, +) + +# --------------------------------------------------------------------------- +# get_poetry_dependencies +# --------------------------------------------------------------------------- + + +class TestGetPoetryDependencies: + def test_none_returns_none(self): + assert get_poetry_dependencies(None) is None + + def test_empty_dict(self): + assert get_poetry_dependencies({}) == [] + + def test_python_excluded(self): + assert get_poetry_dependencies({"python": "^3.10"}) == [] + + def test_exact_version(self): + result = get_poetry_dependencies({"packaging": "26.0"}) + assert result == ["packaging==26.0"] + + def test_caret(self): + result = get_poetry_dependencies({"urllib3": "^2.6.0"}) + assert result == ["urllib3 >=2.6.0"] + + def test_tilde(self): + result = get_poetry_dependencies({"setuptools": "~82.0.0"}) + assert result == ["setuptools~=82.0.0"] + + def test_tilde_equals_passthrough(self): + result = get_poetry_dependencies({"setuptools": "~=82.0.0"}) + assert result == ["setuptools~=82.0.0"] + + def test_wildcard(self): + result = get_poetry_dependencies({"boto3": "*"}) + assert result == ["boto3"] + + def test_less_than(self): + result = get_poetry_dependencies({"chardet": "<6"}) + assert result == ["chardet <6"] + + def test_less_than_equal(self): + result = get_poetry_dependencies({"requests": "<=2.32.4"}) + assert result == ["requests <=2.32.4"] + + def test_greater_than_equal(self): + result = get_poetry_dependencies({"certifi": ">=2026.1.4"}) + assert result == ["certifi >=2026.1.4"] + + def test_range_constraint(self): + result = get_poetry_dependencies({"pydantic": ">=2.9.0,<3.0.0"}) + # _windows_safe ensures spaces before < and > + assert "pydantic" in result[0] + assert " >=" in result[0] + assert " <" in result[0] + + def test_not_equal_combined(self): + result = get_poetry_dependencies({"pandas": ">=2.3,!=2.3.3"}) + assert "pandas" in result[0] + assert " >=" in result[0] + assert "!=" in result[0] + + def test_spaces_in_version_stripped(self): + result = get_poetry_dependencies({"chardet": " < 6 "}) + assert result == ["chardet <6"] + + def test_dict_version(self): + result = get_poetry_dependencies( + {"scipy": {"version": "^1.16", "optional": True}} + ) + assert result == ["scipy >=1.16"] + + def test_dict_version_with_markers(self): + result = get_poetry_dependencies( + {"pywin32": {"version": ">=310", "markers": "sys_platform == 'win32'"}} + ) + assert result == ["pywin32 >=310; sys_platform == 'win32'"] + + def test_dict_git(self): + result = get_poetry_dependencies( + {"numpy": {"git": "https://github.com/numpy/numpy.git", "branch": "main"}} + ) + assert result == ["numpy @ git+https://github.com/numpy/numpy.git@main"] + + def test_dict_git_ssh(self): + result = get_poetry_dependencies( + {"mylib": {"git": "git@github.com:org/repo.git", "tag": "v1.0"}} + ) + assert result == ["mylib @ git@github.com:org/repo.git@v1.0"] + + def test_dict_git_subdirectory(self): + result = get_poetry_dependencies( + { + "mylib": { + "git": "https://github.com/org/mono.git", + "branch": "main", + "subdirectory": "packages/mylib", + } + } + ) + assert "subdirectory=packages/mylib" in result[0] + + def test_dict_path(self): + result = get_poetry_dependencies({"mylib": {"path": "../mylib"}}) + assert result == ["../mylib"] + + def test_dict_url(self): + result = get_poetry_dependencies( + {"mylib": {"url": "https://example.com/mylib.tar.gz"}} + ) + assert result == ["https://example.com/mylib.tar.gz"] + + def test_dict_unsupported_raises(self): + with pytest.raises(ValueError, match="Unsupported"): + get_poetry_dependencies({"bad": {"extras": ["foo"]}}) + + def test_sorted_output(self): + result = get_poetry_dependencies({"zlib": "1.0", "aiohttp": "3.0"}) + assert result == ["aiohttp==3.0", "zlib==1.0"] + + def test_multiple_deps(self): + deps = { + "python": "^3.10", + "boto3": "*", + "chardet": "<6", + "packaging": "26.0", + } + result = get_poetry_dependencies(deps) + assert "boto3" in result + assert any("chardet" in r for r in result) + assert any("packaging" in r for r in result) + # python should be excluded + assert not any("python" in r for r in result) + + +# --------------------------------------------------------------------------- +# get_project_dependencies +# --------------------------------------------------------------------------- + + +class TestGetProjectDependencies: + def test_none_returns_none(self): + assert get_project_dependencies(None) is None + + def test_empty_list(self): + assert get_project_dependencies([]) == [] + + def test_simple_dep(self): + result = get_project_dependencies(["boto3"]) + assert result == ["boto3"] + + def test_exact_version(self): + result = get_project_dependencies(["packaging==26.0"]) + assert result == ["packaging==26.0"] + + def test_gte(self): + result = get_project_dependencies(["flet>=0.82.0"]) + assert result == ["flet >=0.82.0"] + + def test_lt_gets_space(self): + result = get_project_dependencies(["chardet<6"]) + assert result == ["chardet <6"] + + def test_lte_gets_space(self): + result = get_project_dependencies(["requests<=2.32.4"]) + assert result == ["requests <=2.32.4"] + + def test_gt_gets_space(self): + result = get_project_dependencies(["chardet>3"]) + assert result == ["chardet >3"] + + def test_combined_constraints(self): + result = get_project_dependencies(["pydantic>=2.9.0,<3.0.0"]) + assert len(result) == 1 + dep = result[0] + assert "pydantic" in dep + assert " <" in dep or " >" in dep + + def test_not_equal(self): + result = get_project_dependencies(["pandas>=2.3,!=2.3.3"]) + assert len(result) == 1 + assert " >=" in result[0] + assert "!=" in result[0] + + def test_compatible_release(self): + result = get_project_dependencies(["setuptools~=82.0.0"]) + assert result == ["setuptools~=82.0.0"] + + def test_extras(self): + result = get_project_dependencies(["uvicorn[standard]>=0.42.0"]) + dep = result[0] + assert "uvicorn" in dep + assert "[standard]" in dep + assert " >=" in dep + + def test_markers_preserved(self): + result = get_project_dependencies(['pywin32>=310; sys_platform == "win32"']) + assert len(result) == 1 + assert "sys_platform" in result[0] + assert "win32" in result[0] + assert " >=" in result[0] + + def test_url_dep(self): + result = get_project_dependencies( + ["numpy @ git+https://github.com/numpy/numpy.git@main"] + ) + assert len(result) == 1 + assert "git+https://github.com/numpy/numpy.git@main" in result[0] + + def test_extra_spaces_normalized(self): + result = get_project_dependencies(["chardet < 6"]) + assert result == ["chardet <6"] + + def test_sorted_and_deduplicated(self): + result = get_project_dependencies(["zlib>=1.0", "aiohttp>=3.0", "zlib>=1.0"]) + assert result[0].startswith("aiohttp") + assert len(result) == 2 + + def test_downstream_compatible(self): + """Output must be parseable by packaging.requirements.Requirement, + since build_base.py does Requirement(dep).name on our output.""" + from packaging.requirements import Requirement + + deps = [ + "flet>=0.82.0", + "chardet<6", + "pydantic>=2.9.0,<3.0.0", + 'pywin32>=310; sys_platform == "win32"', + "uvicorn[standard]>=0.42.0", + ] + result = get_project_dependencies(deps) + for dep in result: + req = Requirement(dep) + assert req.name # must parse without error From a65517c07819d86d5a5c7aca53f274c8a065ec1f Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 30 Mar 2026 14:10:47 -0700 Subject: [PATCH 5/5] Normalize docstring markup in project_dependencies Replace reST-style double-backtick code markup with single-backtick inline code and tidy wording in project_dependencies.py docstrings. Updates affect _windows_safe, _poetry_version_to_pep440, and get_project_dependencies (e.g., `<`/`>` escaping, version examples, and `None`/`project.dependencies` formatting). No functional or logic changes. --- .../flet_cli/utils/project_dependencies.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py b/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py index f3d32f2c9e..5e0d6f5536 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/utils/project_dependencies.py @@ -9,19 +9,19 @@ def _windows_safe(req_str: str) -> str: - """Insert a space before bare ``<`` or ``>`` so Windows cmd.exe does not - interpret them as shell redirection when the string is passed via ``-r`` - to a ``.BAT`` subprocess.""" + """Insert a space before bare `<` or `>` so Windows cmd.exe does not + interpret them as shell redirection when the string is passed via `-r` + to a `.BAT` subprocess.""" return re.sub(r"(?<=[^ ])([<>])", r" \1", req_str) def _poetry_version_to_pep440(version: str) -> str: """Convert a Poetry version constraint to PEP 440 syntax. - - ``^1.2.3`` → ``>=1.2.3`` - - ``~1.2.3`` → ``~=1.2.3`` (``~=`` passes through unchanged) - - ``*`` → ``""`` (no constraint) - - ``1.2.3`` (bare version) → ``==1.2.3`` + - `^1.2.3` → `>=1.2.3` + - `~1.2.3` → `~=1.2.3` (`~=` passes through unchanged) + - `*` → `""` (no constraint) + - `1.2.3` (bare version) → `==1.2.3` - Anything else is returned as-is (already PEP 440). """ version = version.replace(" ", "") @@ -111,13 +111,13 @@ def get_project_dependencies( project_dependencies: Optional[list[str]] = None, ) -> Optional[list[str]]: """ - Normalize PEP 621 ``project.dependencies`` into a sorted unique list. + Normalize PEP 621 `project.dependencies` into a sorted unique list. Args: - project_dependencies: Value from ``project.dependencies``. + project_dependencies: Value from `project.dependencies`. Returns: - Sorted dependency strings, or ``None`` when input is ``None``. + Sorted dependency strings, or `None` when input is `None`. """ if project_dependencies is None: return None