From d59108c20698dc905a41a7e5ca520fc384d6773b Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 29 May 2026 12:31:45 -0700 Subject: [PATCH 1/5] Add `scripts/check_min_deps.py` to validate lower bounds Ensures that pyright has equivalent results on lowest direct dependencies and latest direct dependencies. --- .github/workflows/min_deps.yml | 64 +++++ AGENTS.md | 1 + pyproject.toml | 1 + scripts/check_min_deps.py | 422 +++++++++++++++++++++++++++++ tests/units/test_check_min_deps.py | 103 +++++++ 5 files changed, 591 insertions(+) create mode 100644 .github/workflows/min_deps.yml create mode 100644 scripts/check_min_deps.py create mode 100644 tests/units/test_check_min_deps.py diff --git a/.github/workflows/min_deps.yml b/.github/workflows/min_deps.yml new file mode 100644 index 00000000000..a1054d27814 --- /dev/null +++ b/.github/workflows/min_deps.yml @@ -0,0 +1,64 @@ +name: check-min-deps + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.id || github.sha }} + cancel-in-progress: true + +on: + push: + branches: ["main"] + paths-ignore: + - "**/*.md" + pull_request: + branches: ["main"] + paths-ignore: + - "**/*.md" + workflow_dispatch: + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + # Single-source the list of checkable packages from the script so the matrix + # below stays in sync as packages are added or removed. + discover: + runs-on: ubuntu-latest + outputs: + packages: ${{ steps.list.outputs.packages }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: ./.github/actions/setup_build_env + with: + python-version: "3.14" + - id: list + run: echo "packages=$(uv run --no-project python scripts/check_min_deps.py --list)" >> "$GITHUB_OUTPUT" + + check: + needs: discover + timeout-minutes: 30 + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + package: ${{ fromJSON(needs.discover.outputs.packages) }} + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + # fetch-tags/fetch-depth are required: each package is built editable via + # uv-dynamic-versioning, which derives the version from git tags/history. + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-tags: true + fetch-depth: 0 + persist-credentials: false + - uses: ./.github/actions/setup_build_env + with: + python-version: ${{ matrix.python-version }} + run-uv-sync: true + - name: Check minimum declared dependency versions + run: uv run --no-sync python scripts/check_min_deps.py --python "${{ matrix.python-version }}" "${{ matrix.package }}" diff --git a/AGENTS.md b/AGENTS.md index e2a4e287b03..b9bcbd1876c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,7 @@ uv run pytest tests/integration # integration t uv run ruff check . # lint uv run ruff format . # format uv run pyright reflex tests # type check +uv run python scripts/check_min_deps.py # validate each package's declared minimum dep versions (pyright in isolated min-version envs) uv run python scripts/make_pyi.py # regenerate .pyi stubs uv run pre-commit run --all-files # all pre-commit hooks ``` diff --git a/pyproject.toml b/pyproject.toml index 38ae4d0ff30..3e2e8342b39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -250,6 +250,7 @@ preview = true "*/.templates/apps/blank/code/*" = ["INP001"] "*/blank.py" = ["I001"] "docs/package/scripts/*.py" = ["INP001"] +"scripts/check_min_deps.py" = ["T201"] [tool.pytest.ini_options] filterwarnings = "ignore:fields may not start with an underscore:RuntimeWarning" diff --git a/scripts/check_min_deps.py b/scripts/check_min_deps.py new file mode 100644 index 00000000000..a6b6bf9518b --- /dev/null +++ b/scripts/check_min_deps.py @@ -0,0 +1,422 @@ +"""Validate that each workspace package's declared minimum dependency versions are workable. + +For every checkable package (the root ``reflex`` package plus the sub-packages under +``packages/*``), this installs the package editable into two isolated virtualenvs (deps +from PyPI, never the local workspace, via ``--no-sources``): one with dependencies resolved +to their *declared minimums* (``--resolution lowest-direct``) and one with the latest +compatible versions. Pyright runs against the package's own source in each, and the check +fails only on errors that are *new* at the minimum versions. + +The delta is what matters, not the absolute error count: a package's source legitimately +references undeclared optional/circular imports under ``TYPE_CHECKING`` (e.g. ``sqlalchemy``, +``pandas``, sibling ``reflex.*`` modules) that are missing in any isolated env. Those errors +appear identically at both resolutions and cancel out. An error that appears *only* at the +minimum versions means the code depends on a newer dependency than its declared lower bound +allows — exactly the bug this catches (e.g. calling a pydantic 2.x API while declaring +``pydantic >=1.10``). + +Run with ``uv run python scripts/check_min_deps.py [package ...]``. With no arguments, +every checkable package is validated. +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +import tempfile +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass +from pathlib import Path + +import tomllib + +REPO_ROOT = Path(__file__).resolve().parent.parent + +# Default isolated interpreter: match the interpreter running this script so the Python +# version is held constant across both resolutions and only dependency versions vary. +DEFAULT_PYTHON = f"{sys.version_info.major}.{sys.version_info.minor}" + +# Packages that are intentionally not validated: +# hatch-reflex-pyi - build-backend plugin, only depends on hatchling +# integrations-docs - has no declared dependencies +# reflex-site-shared - excluded from the root pyright config +SKIP_PACKAGES = frozenset({ + "hatch-reflex-pyi", + "integrations-docs", + "reflex-site-shared", +}) + + +@dataclass(frozen=True) +class Package: + """A workspace package that can be checked against its minimum dependencies.""" + + name: str + """The package directory name (e.g. ``reflex-base``), used as the CLI identifier.""" + + project_dir: Path + """Directory containing the package's ``pyproject.toml`` (the editable install target).""" + + source_dir: Path + """Directory of importable source that pyright should type-check.""" + + extras: tuple[str, ...] + """Names of optional-dependency groups to install alongside the package.""" + + def install_target(self) -> str: + """Build the editable install target, including any extras. + + Returns: + The path passed to ``uv pip install -e``, with ``[extra,...]`` appended. + """ + target = str(self.project_dir) + if self.extras: + target += "[" + ",".join(self.extras) + "]" + return target + + +def _load_pyproject(path: Path) -> dict: + """Parse a ``pyproject.toml`` file. + + Args: + path: Path to the ``pyproject.toml`` file. + + Returns: + The parsed TOML document. + """ + with path.open("rb") as f: + return tomllib.load(f) + + +def _single_source_dir(src: Path) -> Path: + """Return the lone module directory under a ``src/`` layout directory. + + Args: + src: The ``src`` directory of a package. + + Returns: + The single child directory (the importable package). + + Raises: + ValueError: If ``src`` does not contain exactly one child directory. + """ + children = [child for child in src.iterdir() if child.is_dir()] + if len(children) != 1: + msg = f"expected exactly one module directory under {src}, found {children}" + raise ValueError(msg) + return children[0] + + +def discover_packages() -> list[Package]: + """Discover every checkable workspace package. + + Returns: + The checkable packages, sorted by name, with the root ``reflex`` package first. + """ + packages: list[Package] = [] + + root_pyproject = _load_pyproject(REPO_ROOT / "pyproject.toml") + packages.append( + Package( + name="reflex", + project_dir=REPO_ROOT, + source_dir=REPO_ROOT / "reflex", + extras=tuple(root_pyproject["project"].get("optional-dependencies", {})), + ) + ) + + for project_file in sorted((REPO_ROOT / "packages").glob("*/pyproject.toml")): + name = project_file.parent.name + if name in SKIP_PACKAGES: + continue + project = _load_pyproject(project_file)["project"] + if not project.get("dependencies"): + continue + packages.append( + Package( + name=name, + project_dir=project_file.parent, + source_dir=_single_source_dir(project_file.parent / "src"), + extras=tuple(project.get("optional-dependencies", {})), + ) + ) + + return packages + + +@dataclass +class Result: + """The outcome of checking a single package.""" + + package: str + ok: bool + stage: str + """Where the result was decided: ``"resolution"``, ``"min-version"`` or ``"ok"``.""" + detail: str + """Human-readable diagnostics to print on failure.""" + + +def _venv_python(venv: Path) -> Path: + """Return the path to the interpreter inside a virtualenv. + + Args: + venv: The virtualenv directory. + + Returns: + The interpreter path, accounting for the platform's layout. + """ + if sys.platform == "win32": + return venv / "Scripts" / "python.exe" + return venv / "bin" / "python" + + +def _run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess[str]: + """Run a subprocess capturing combined output as text. + + Args: + cmd: The command and arguments to run. + kwargs: Extra keyword arguments forwarded to ``subprocess.run``. + + Returns: + The completed process. + """ + return subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + check=False, + **kwargs, + ) + + +def _pyright_errors(report: dict) -> dict[tuple[str, int, int, str], str]: + """Extract error diagnostics from a pyright JSON report. + + Args: + report: The parsed ``--outputjson`` document. + + Returns: + A mapping from a stable diagnostic key (file, line, character, message) to a + formatted, human-readable display line. + """ + errors: dict[tuple[str, int, int, str], str] = {} + for diagnostic in report.get("generalDiagnostics", []): + if diagnostic.get("severity") != "error": + continue + start = diagnostic["range"]["start"] + key = ( + diagnostic["file"], + start["line"], + start["character"], + diagnostic["message"], + ) + errors[key] = ( + f"{diagnostic['file']}:{start['line'] + 1}:{start['character'] + 1}" + f" - error: {diagnostic['message']}" + ) + return errors + + +def _resolve_and_check( + package: Package, + python_version: str, + venv: Path, + config: Path, + lowest: bool, +) -> tuple[dict[tuple[str, int, int, str], str] | None, str]: + """Install a package into an isolated venv and run pyright against its source. + + Args: + package: The package to install and check. + python_version: The interpreter version for the venv. + venv: Directory in which to create the virtualenv. + config: Path to the pyright options config. + lowest: Whether to pin direct dependencies to their declared minimums. + + Returns: + A ``(errors, detail)`` tuple. ``errors`` is the pyright error map, or ``None`` if + the environment could not be built or pyright produced no parseable output, in + which case ``detail`` carries the captured output. + """ + venv_proc = _run(["uv", "venv", "--python", python_version, str(venv)]) + if venv_proc.returncode != 0: + return None, venv_proc.stdout + venv_python = str(_venv_python(venv)) + + install_cmd = [ + "uv", + "pip", + "install", + "--python", + venv_python, + "--no-sources", + ] + if lowest: + install_cmd += ["--resolution", "lowest-direct"] + install_cmd += ["-e", package.install_target()] + install = _run(install_cmd, cwd=REPO_ROOT) + if install.returncode != 0: + return None, install.stdout + + pyright = _run( + [ + "pyright", + "--outputjson", + "--pythonpath", + venv_python, + "--project", + str(config), + str(package.source_dir), + ], + cwd=REPO_ROOT, + ) + try: + report = json.loads(pyright.stdout) + except json.JSONDecodeError: + return None, pyright.stdout or "(pyright produced no output)" + return _pyright_errors(report), "" + + +def check_package(package: Package, python_version: str) -> Result: + """Check that a package type-checks no worse at its declared minimums than at latest. + + Installs the package twice in isolated environments — once with dependencies at their + latest compatible versions, once pinned to their declared minimums — and compares + pyright errors. Errors present only at the minimum versions indicate the code depends + on a newer dependency than its declared lower bound allows. + + Args: + package: The package to validate. + python_version: The interpreter version for the isolated environments. + + Returns: + The result of the check. + """ + with tempfile.TemporaryDirectory(prefix=f"min-deps-{package.name}-") as tmp: + tmp_path = Path(tmp) + config = tmp_path / "pyrightconfig.json" + config.write_text(json.dumps({"reportIncompatibleMethodOverride": False})) + + baseline, detail = _resolve_and_check( + package, python_version, tmp_path / ".venv-latest", config, lowest=False + ) + if baseline is None: + return Result( + package.name, + False, + "resolution", + f"installing latest deps failed:\n{detail}", + ) + + minimum, detail = _resolve_and_check( + package, python_version, tmp_path / ".venv-lowest", config, lowest=True + ) + if minimum is None: + return Result( + package.name, + False, + "resolution", + f"installing minimum deps failed:\n{detail}", + ) + + new_errors = sorted(minimum.keys() - baseline.keys()) + if new_errors: + return Result( + package.name, + False, + "min-version", + "\n".join(minimum[key] for key in new_errors), + ) + + return Result(package.name, True, "ok", "") + + +def main() -> int: + """Validate the declared minimum dependency versions of workspace packages. + + Returns: + ``0`` if all checked packages pass, ``1`` otherwise. + """ + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "packages", + nargs="*", + help="Package directory names to check (default: all checkable packages).", + ) + parser.add_argument( + "--list", + action="store_true", + help="Print the checkable packages as a JSON array and exit.", + ) + parser.add_argument( + "--python", + default=DEFAULT_PYTHON, + help=f"Interpreter version for the isolated environment (default: {DEFAULT_PYTHON}).", + ) + parser.add_argument( + "-j", + "--jobs", + type=int, + default=1, + help="Number of packages to check in parallel (default: 1).", + ) + args = parser.parse_args() + + all_packages = discover_packages() + + if args.list: + print(json.dumps([p.name for p in all_packages])) + return 0 + + by_name = {p.name: p for p in all_packages} + if args.packages: + unknown = [name for name in args.packages if name not in by_name] + if unknown: + parser.error( + f"unknown package(s): {', '.join(unknown)}. " + f"Choose from: {', '.join(by_name)}" + ) + selected = [by_name[name] for name in args.packages] + else: + selected = all_packages + + print( + f"Checking {len(selected)} package(s) against minimum declared dependencies " + f"(python {args.python})...\n" + ) + + if args.jobs > 1: + with ThreadPoolExecutor(max_workers=args.jobs) as executor: + results = list( + executor.map(lambda p: check_package(p, args.python), selected) + ) + else: + results = [check_package(p, args.python) for p in selected] + + failures = [r for r in results if not r.ok] + for result in sorted(results, key=lambda r: r.package): + status = "PASS" if result.ok else f"FAIL ({result.stage})" + print(f" {status:<18} {result.package}") + + reason = { + "min-version": "type errors that only appear at the declared minimum versions " + "(bump the corresponding lower bound)", + "resolution": "the isolated environment could not be built", + } + if failures: + print("\n" + "=" * 72) + for result in failures: + print(f"\n{result.package} — {reason.get(result.stage, result.stage)}:") + print(result.detail or "(no output captured)") + print(f"\n{len(failures)} of {len(results)} package(s) failed.") + return 1 + + print(f"\nAll {len(results)} package(s) passed.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/units/test_check_min_deps.py b/tests/units/test_check_min_deps.py new file mode 100644 index 00000000000..dde46b7b5e7 --- /dev/null +++ b/tests/units/test_check_min_deps.py @@ -0,0 +1,103 @@ +"""Unit tests for scripts/check_min_deps.py (the minimum-dependency-version checker).""" + +from pathlib import Path + +import pytest + +from scripts import check_min_deps + + +def test_install_target_without_extras(tmp_path: Path): + package = check_min_deps.Package( + name="pkg", project_dir=tmp_path, source_dir=tmp_path, extras=() + ) + assert package.install_target() == str(tmp_path) + + +def test_install_target_with_extras(tmp_path: Path): + package = check_min_deps.Package( + name="pkg", project_dir=tmp_path, source_dir=tmp_path, extras=("db", "extra") + ) + assert package.install_target() == f"{tmp_path}[db,extra]" + + +def test_single_source_dir(tmp_path: Path): + module = tmp_path / "the_module" + module.mkdir() + (tmp_path / "not_a_dir.txt").write_text("") + assert check_min_deps._single_source_dir(tmp_path) == module + + +def test_single_source_dir_requires_exactly_one(tmp_path: Path): + (tmp_path / "a").mkdir() + (tmp_path / "b").mkdir() + with pytest.raises(ValueError, match="exactly one module directory"): + check_min_deps._single_source_dir(tmp_path) + + +def test_discover_packages_includes_root_first_and_skips_excluded(): + packages = check_min_deps.discover_packages() + names = [p.name for p in packages] + + assert names[0] == "reflex", "root reflex package should be checked first" + assert "reflex-base" in names + assert not check_min_deps.SKIP_PACKAGES.intersection(names) + + for package in packages: + assert package.source_dir.is_dir(), f"{package.name} source dir must exist" + assert (package.project_dir / "pyproject.toml").is_file() + + +def test_discover_packages_records_optional_extras(): + by_name = {p.name: p for p in check_min_deps.discover_packages()} + # The root package declares a `db` optional-dependency group. + assert "db" in by_name["reflex"].extras + + +def test_pyright_errors_keys_and_filters_severity(): + report = { + "generalDiagnostics": [ + { + "file": "/abs/foo.py", + "severity": "error", + "message": "boom", + "range": {"start": {"line": 9, "character": 4}}, + }, + { + "file": "/abs/foo.py", + "severity": "warning", + "message": "ignore me", + "range": {"start": {"line": 1, "character": 0}}, + }, + ] + } + errors = check_min_deps._pyright_errors(report) + + assert list(errors) == [("/abs/foo.py", 9, 4, "boom")] + # Line/character are converted to 1-based in the display string. + assert errors["/abs/foo.py", 9, 4, "boom"] == "/abs/foo.py:10:5 - error: boom" + + +def test_pyright_errors_delta_cancels_shared_noise(): + def report(messages: list[tuple[str, int]]) -> dict: + return { + "generalDiagnostics": [ + { + "file": "/abs/foo.py", + "severity": "error", + "message": msg, + "range": {"start": {"line": line, "character": 0}}, + } + for msg, line in messages + ] + } + + # A shared, undeclared-import error appears in both resolutions; only the + # minimum-version-specific error should remain in the delta. + baseline = check_min_deps._pyright_errors(report([("missing optional import", 1)])) + minimum = check_min_deps._pyright_errors( + report([("missing optional import", 1), ("model_dump is unknown", 50)]) + ) + + new = minimum.keys() - baseline.keys() + assert new == {("/abs/foo.py", 50, 0, "model_dump is unknown")} From d43cfeadd97bd99bafabee41f147464a6d0222af Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 29 May 2026 12:33:09 -0700 Subject: [PATCH 2/5] Fix package lower bounds reflex: * httpx bump for `mounts` parameter typing * redis bump for set operation typing to allow bytes keys * reflex-base, reflex-components-radix bump for compiler changes * reflex-hosting-cli min version for reflex_cli_v2 reflex-base: * pydantic bump to align with reflex reflex-components-core: * python_multipart lower bound to get python_multipart.multipart * starlette lower bound to get UploadFiles and friends * typing_extensions lower bound to get Self reflex-components-radix: * reflex-base bump for compiler plugin changes reflex-docgen * pyyaml bump for python3.14 compat --- packages/reflex-base/pyproject.toml | 2 +- packages/reflex-components-core/pyproject.toml | 6 +++--- packages/reflex-components-radix/pyproject.toml | 2 +- packages/reflex-docgen/pyproject.toml | 7 +++---- pyproject.toml | 10 +++++----- uv.lock | 16 +++++++--------- 6 files changed, 20 insertions(+), 23 deletions(-) diff --git a/packages/reflex-base/pyproject.toml b/packages/reflex-base/pyproject.toml index 1c267d80d3c..7fdc1d8599c 100644 --- a/packages/reflex-base/pyproject.toml +++ b/packages/reflex-base/pyproject.toml @@ -9,7 +9,7 @@ maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] requires-python = ">=3.10" dependencies = [ "packaging >=24.2,<27", - "pydantic >=1.10.21,<3.0", + "pydantic >=2.12.0,<3.0", "rich >=13,<15", "typing_extensions >=4.13.0", "platformdirs >=4.3.7,<5.0", diff --git a/packages/reflex-components-core/pyproject.toml b/packages/reflex-components-core/pyproject.toml index 43f997124b8..eab77e789bd 100644 --- a/packages/reflex-components-core/pyproject.toml +++ b/packages/reflex-components-core/pyproject.toml @@ -11,9 +11,9 @@ dependencies = [ "reflex-base >= 0.9.2", "reflex-components-lucide >= 0.9.0", "reflex-components-sonner >= 0.9.0", - "python_multipart", - "starlette", - "typing_extensions", + "python_multipart >= 0.0.21", + "starlette >= 0.47.0", + "typing_extensions >= 4.0.0", ] [tool.hatch.version] diff --git a/packages/reflex-components-radix/pyproject.toml b/packages/reflex-components-radix/pyproject.toml index c4781c05942..327eaca0554 100644 --- a/packages/reflex-components-radix/pyproject.toml +++ b/packages/reflex-components-radix/pyproject.toml @@ -8,7 +8,7 @@ authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] requires-python = ">=3.10" dependencies = [ - "reflex-base >= 0.9.0", + "reflex-base >= 0.9.2", "reflex-components-core >= 0.9.0", "reflex-components-lucide >= 0.9.0", ] diff --git a/packages/reflex-docgen/pyproject.toml b/packages/reflex-docgen/pyproject.toml index 6d7ba17c22f..db805a8e8ac 100644 --- a/packages/reflex-docgen/pyproject.toml +++ b/packages/reflex-docgen/pyproject.toml @@ -8,11 +8,10 @@ authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] requires-python = ">=3.10" dependencies = [ - "griffelib>=2.0.1", - "mistletoe>=1.4.0", - "pyyaml>=6.0", + "griffelib >= 2.0.1", + "mistletoe >= 1.4.0", + "pyyaml >= 6.0.3", "reflex >= 0.9.0", - "typing-extensions", "typing-inspection>=0.4.2", ] diff --git a/pyproject.toml b/pyproject.toml index 3e2e8342b39..310109907e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,18 +22,17 @@ requires-python = ">=3.10,<4.0" dependencies = [ "click >=8.2", "granian[reload] >=2.5.5", - "httpx >=0.23.3,<1.0", + "httpx >=0.26,<1.0", "packaging >=24.2,<27", "psutil >=7.0.0,<8.0; sys_platform == 'win32'", "python-multipart >=0.0.20,<1.0", "python-socketio >=5.12.0,<6.0", - "redis >=5.2.1,<8.0", - "reflex-hosting-cli", + "redis >=6.4,<8.0", "rich >=13,<16", "starlette >=0.47.0", "typing_extensions >=4.13.0", "wrapt >=1.17.0,<3.0", - "reflex-base >= 0.9.0", + "reflex-base >= 0.9.3", "reflex-components-code >= 0.9.0", "reflex-components-core >= 0.9.0", "reflex-components-dataeditor >= 0.9.0", @@ -42,10 +41,11 @@ dependencies = [ "reflex-components-markdown >= 0.9.0", "reflex-components-moment >= 0.9.0", "reflex-components-plotly >= 0.9.0", - "reflex-components-radix >= 0.9.0", + "reflex-components-radix >= 0.9.2", "reflex-components-react-player >= 0.9.0", "reflex-components-recharts >= 0.9.0", "reflex-components-sonner >= 0.9.0", + "reflex-hosting-cli >= 0.1.66", ] classifiers = [ diff --git a/uv.lock b/uv.lock index fea4704d89c..40e7756ac8b 100644 --- a/uv.lock +++ b/uv.lock @@ -3475,13 +3475,13 @@ requires-dist = [ { name = "alembic", marker = "extra == 'db'", specifier = ">=1.15.2,<2.0" }, { name = "click", specifier = ">=8.2" }, { name = "granian", extras = ["reload"], specifier = ">=2.5.5" }, - { name = "httpx", specifier = ">=0.23.3,<1.0" }, + { name = "httpx", specifier = ">=0.26,<1.0" }, { name = "packaging", specifier = ">=24.2,<27" }, { name = "psutil", marker = "sys_platform == 'win32'", specifier = ">=7.0.0,<8.0" }, { name = "pydantic", marker = "extra == 'db'", specifier = ">=2.12.0,<3.0" }, { name = "python-multipart", specifier = ">=0.0.20,<1.0" }, { name = "python-socketio", specifier = ">=5.12.0,<6.0" }, - { name = "redis", specifier = ">=5.2.1,<8.0" }, + { name = "redis", specifier = ">=6.4,<8.0" }, { name = "reflex-base", editable = "packages/reflex-base" }, { name = "reflex-components-code", editable = "packages/reflex-components-code" }, { name = "reflex-components-core", editable = "packages/reflex-components-core" }, @@ -3561,7 +3561,7 @@ dependencies = [ requires-dist = [ { name = "packaging", specifier = ">=24.2,<27" }, { name = "platformdirs", specifier = ">=4.3.7,<5.0" }, - { name = "pydantic", specifier = ">=1.10.21,<3.0" }, + { name = "pydantic", specifier = ">=2.12.0,<3.0" }, { name = "rich", specifier = ">=13,<15" }, { name = "typing-extensions", specifier = ">=4.13.0" }, ] @@ -3598,12 +3598,12 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "python-multipart" }, + { name = "python-multipart", specifier = ">=0.0.21" }, { name = "reflex-base", editable = "packages/reflex-base" }, { name = "reflex-components-lucide", editable = "packages/reflex-components-lucide" }, { name = "reflex-components-sonner", editable = "packages/reflex-components-sonner" }, - { name = "starlette" }, - { name = "typing-extensions" }, + { name = "starlette", specifier = ">=0.47.0" }, + { name = "typing-extensions", specifier = ">=4.0.0" }, ] [[package]] @@ -3754,7 +3754,6 @@ dependencies = [ { name = "mistletoe" }, { name = "pyyaml" }, { name = "reflex" }, - { name = "typing-extensions" }, { name = "typing-inspection" }, ] @@ -3762,9 +3761,8 @@ dependencies = [ requires-dist = [ { name = "griffelib", specifier = ">=2.0.1" }, { name = "mistletoe", specifier = ">=1.4.0" }, - { name = "pyyaml", specifier = ">=6.0" }, + { name = "pyyaml", specifier = ">=6.0.3" }, { name = "reflex", editable = "." }, - { name = "typing-extensions" }, { name = "typing-inspection", specifier = ">=0.4.2" }, ] From 007516a63dc60cccb9a1cf9add0a06bc3c927992 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 29 May 2026 12:39:27 -0700 Subject: [PATCH 3/5] min_deps.yml only tries oldest and latest supported python versions --- .github/workflows/min_deps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/min_deps.yml b/.github/workflows/min_deps.yml index a1054d27814..57cabf5e571 100644 --- a/.github/workflows/min_deps.yml +++ b/.github/workflows/min_deps.yml @@ -47,7 +47,7 @@ jobs: fail-fast: false matrix: package: ${{ fromJSON(needs.discover.outputs.packages) }} - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.10", "3.14"] steps: # fetch-tags/fetch-depth are required: each package is built editable via # uv-dynamic-versioning, which derives the version from git tags/history. From 44636be48efbbbb2df7bcf2de94b5950fed2fcf6 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 29 May 2026 13:44:39 -0700 Subject: [PATCH 4/5] support py3.10 for check_min_deps.py --- scripts/check_min_deps.py | 12 +++++++++++- tests/units/test_check_min_deps.py | 9 +++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/scripts/check_min_deps.py b/scripts/check_min_deps.py index a6b6bf9518b..d23beda478e 100644 --- a/scripts/check_min_deps.py +++ b/scripts/check_min_deps.py @@ -19,6 +19,13 @@ every checkable package is validated. """ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "tomli; python_version < '3.11'", +# ] +# /// + from __future__ import annotations import argparse @@ -30,7 +37,10 @@ from dataclasses import dataclass from pathlib import Path -import tomllib +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib REPO_ROOT = Path(__file__).resolve().parent.parent diff --git a/tests/units/test_check_min_deps.py b/tests/units/test_check_min_deps.py index dde46b7b5e7..795b1d9088a 100644 --- a/tests/units/test_check_min_deps.py +++ b/tests/units/test_check_min_deps.py @@ -1,9 +1,18 @@ """Unit tests for scripts/check_min_deps.py (the minimum-dependency-version checker).""" +import sys from pathlib import Path import pytest +# The script relies on ``tomllib`` (stdlib only on 3.11+); on 3.10 it falls back to the +# ``tomli`` backport. Skip the whole module when neither is available, so the tests still +# run on 3.10 whenever ``tomli`` happens to be installed. +if sys.version_info < (3, 11): + pytest.importorskip( + "tomli", reason="check_min_deps requires tomli on Python < 3.11" + ) + from scripts import check_min_deps From f258236faddab720974696f40109c56a5604c6c8 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 29 May 2026 13:46:17 -0700 Subject: [PATCH 5/5] Avoid KeyError on diagnostic["range"] Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- scripts/check_min_deps.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/check_min_deps.py b/scripts/check_min_deps.py index d23beda478e..09f3288a158 100644 --- a/scripts/check_min_deps.py +++ b/scripts/check_min_deps.py @@ -216,7 +216,9 @@ def _pyright_errors(report: dict) -> dict[tuple[str, int, int, str], str]: for diagnostic in report.get("generalDiagnostics", []): if diagnostic.get("severity") != "error": continue - start = diagnostic["range"]["start"] + start = diagnostic.get("range", {}).get("start", {}) + if "line" not in start or "character" not in start: + continue key = ( diagnostic["file"], start["line"],