From 72ca110edfd60186682811c394485ca7aba3b152 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 28 Aug 2026 07:00:16 +0000 Subject: [PATCH 1/6] Stop shipping sibling packages' .pyi stubs in the reflex distributions Hatchling matches `include` and `artifacts` with gitignore semantics, so an unanchored pattern matches at any depth, and `artifacts` is checked before `include` and bypasses it entirely. Both mattered here. `artifacts = ["*.pyi"]` swept in every generated stub in the workspace -- 163 files under packages/, each already bundled by the package that owns it, plus the 12 pyi_generator golden fixtures under tests/. And `include = ["reflex", ...]` matched the nested packages/reflex-base/src/reflex_base/.templates/web/components/reflex/ directory, dragging a stray .js file along with it. The wheel therefore installed a top-level `packages/` directory next to `reflex/` in site-packages. Anchoring both patterns to the repository root drops the sdist and wheel from 308 files to 132, and the wheel from 770 KB to 280 KB. Add tests that assert both builders select nothing outside reflex/ and the build hook, checked against the config we ship rather than a copy of it. --- news/+reflex-dist-sibling-pyi.bugfix.md | 1 + pyproject.toml | 10 ++-- tests/units/test_build_config.py | 62 +++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 news/+reflex-dist-sibling-pyi.bugfix.md create mode 100644 tests/units/test_build_config.py diff --git a/news/+reflex-dist-sibling-pyi.bugfix.md b/news/+reflex-dist-sibling-pyi.bugfix.md new file mode 100644 index 00000000000..16ac045eaa4 --- /dev/null +++ b/news/+reflex-dist-sibling-pyi.bugfix.md @@ -0,0 +1 @@ +The `reflex` sdist and wheel no longer ship the sibling workspace packages' generated `.pyi` stubs (or the `pyi_generator` golden test fixtures). Hatchling matches `include` and `artifacts` with gitignore semantics, so the unanchored patterns also matched `packages/**` — and `artifacts` bypasses `include` entirely — which dragged 176 files that each sibling package already bundles into every `reflex` distribution and installed a stray top-level `packages/` directory alongside `reflex/`. The wheel is now roughly a third of its previous size. diff --git a/pyproject.toml b/pyproject.toml index 107456caa98..54abf217bdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,9 +137,13 @@ source = "uv-dynamic-versioning" fallback-version = "0.0.0dev0" [tool.hatch.build] -include = ["reflex", "scripts/hatch_build.py"] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Anchored to the repository root: hatchling matches these with gitignore +# semantics, so an unanchored "reflex" also matches nested directories named +# `reflex` under packages/, and `artifacts` patterns bypass `include` +# altogether, which pulled every sibling package's generated stubs into the +# reflex sdist and wheel. +include = ["/reflex", "/scripts/hatch_build.py"] +artifacts = ["/reflex/**/*.pyi"] [tool.hatch.build.hooks.custom] path = "scripts/hatch_build.py" diff --git a/tests/units/test_build_config.py b/tests/units/test_build_config.py new file mode 100644 index 00000000000..4962f8cca28 --- /dev/null +++ b/tests/units/test_build_config.py @@ -0,0 +1,62 @@ +"""Tests for the file selection of the reflex package's hatch build config.""" + +from pathlib import Path + +import pytest +from hatchling.builders.plugin.interface import BuilderInterface +from hatchling.builders.sdist import SdistBuilder +from hatchling.builders.wheel import WheelBuilder + +REPO_ROOT = Path(__file__).parents[2] + +# Everything the reflex distributions are meant to ship: the package itself, +# its generated stubs, and the build hook that regenerates them when the wheel +# is built from the sdist. +SELECTED = [ + "reflex/app.py", + "reflex/__init__.pyi", + "reflex/components/__init__.pyi", + "scripts/hatch_build.py", +] + +# Sibling workspace packages bundle their own stubs and templates, so none of +# their files belong in the reflex distributions. Neither do the pyi_generator +# golden files, which are only test fixtures. +NOT_SELECTED = [ + "packages/reflex-components-core/src/reflex_components_core/el/element.py", + "packages/reflex-components-core/src/reflex_components_core/el/element.pyi", + "packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi", + "packages/reflex-base/src/reflex_base/.templates/web/components/reflex/color_mode.js", + "tests/units/reflex_base/utils/pyi_generator/golden/var_types.pyi", + "docs/app/docs.py", +] + + +@pytest.fixture(params=[SdistBuilder, WheelBuilder], ids=["sdist", "wheel"]) +def builder(request) -> BuilderInterface: + return request.param(str(REPO_ROOT)) + + +@pytest.mark.parametrize("relative_path", SELECTED) +def test_build_selects_reflex_files(builder: BuilderInterface, relative_path: str): + assert builder.config.include_path(relative_path) + + +@pytest.mark.parametrize("relative_path", NOT_SELECTED) +def test_build_skips_files_outside_reflex( + builder: BuilderInterface, relative_path: str +): + assert not builder.config.include_path(relative_path) + + +def test_build_walks_only_the_reflex_tree(builder: BuilderInterface): + selected = { + Path(included.relative_path).as_posix() + for included in builder.recurse_included_files() + } + outside = sorted( + path + for path in selected + if not path.startswith("reflex/") and path != "scripts/hatch_build.py" + ) + assert outside == [] From 06c1d68022271e46f6c18e104b1033edc7516723 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 28 Aug 2026 07:17:39 +0000 Subject: [PATCH 2/6] Keep the build hook out of the wheel, anchor the packages' stub artifacts Two follow-ups in the same vein as anchoring the root patterns. `scripts/hatch_build.py` only needs to reach the sdist, so that a wheel can be built from it. Shipping it in the wheel installed a top-level `scripts` directory into site-packages -- a far more collision-prone name than the `packages` one just removed. Moving it to `targets.sdist.include` leaves the wheel with `reflex/` alone. The 14 packages that generate stubs still declared `artifacts = ["*.pyi"]`. That is harmless today only because each build root is the package's own directory; the pattern is the same footgun that let the root config reach into packages/. Anchor them to `/src/**/*.pyi` and collapse the two identical per-target keys into one. Tests cover both: the walk test now distinguishes the two targets, and every stub-generating package is checked to select its own `src` stubs and nothing beside them. Discovery keys off the `reflex-pyi` build hook so that removing a package's patterns fails its assertions rather than dropping it from the parametrization. --- news/+reflex-dist-sibling-pyi.bugfix.md | 2 +- .../reflex-components-code/pyproject.toml | 6 +- .../reflex-components-core/pyproject.toml | 6 +- .../pyproject.toml | 6 +- .../reflex-components-gridjs/pyproject.toml | 6 +- .../reflex-components-internal/pyproject.toml | 6 +- .../reflex-components-lucide/pyproject.toml | 6 +- .../reflex-components-markdown/pyproject.toml | 6 +- .../reflex-components-moment/pyproject.toml | 6 +- .../reflex-components-plotly/pyproject.toml | 6 +- .../reflex-components-radix/pyproject.toml | 6 +- .../pyproject.toml | 6 +- .../reflex-components-recharts/pyproject.toml | 6 +- .../reflex-components-sonner/pyproject.toml | 6 +- packages/reflex-site-shared/pyproject.toml | 6 +- pyproject.toml | 5 +- tests/units/test_build_config.py | 72 ++++++++++++++++--- 17 files changed, 123 insertions(+), 40 deletions(-) diff --git a/news/+reflex-dist-sibling-pyi.bugfix.md b/news/+reflex-dist-sibling-pyi.bugfix.md index 16ac045eaa4..b074ef9735d 100644 --- a/news/+reflex-dist-sibling-pyi.bugfix.md +++ b/news/+reflex-dist-sibling-pyi.bugfix.md @@ -1 +1 @@ -The `reflex` sdist and wheel no longer ship the sibling workspace packages' generated `.pyi` stubs (or the `pyi_generator` golden test fixtures). Hatchling matches `include` and `artifacts` with gitignore semantics, so the unanchored patterns also matched `packages/**` — and `artifacts` bypasses `include` entirely — which dragged 176 files that each sibling package already bundles into every `reflex` distribution and installed a stray top-level `packages/` directory alongside `reflex/`. The wheel is now roughly a third of its previous size. +The `reflex` sdist and wheel no longer ship the sibling workspace packages' generated `.pyi` stubs (or the `pyi_generator` golden test fixtures). Hatchling matches `include` and `artifacts` with gitignore semantics, so the unanchored patterns also matched `packages/**` — and `artifacts` bypasses `include` entirely — which dragged 176 files that each sibling package already bundles into every `reflex` distribution and installed a stray top-level `packages/` directory alongside `reflex/`. The wheel is now roughly a third of its previous size. The build hook is likewise no longer installed as a top-level `scripts/` directory; it is shipped in the sdist only, where building the wheel needs it. diff --git a/packages/reflex-components-code/pyproject.toml b/packages/reflex-components-code/pyproject.toml index b9f9ac4c2b0..5dba253286c 100644 --- a/packages/reflex-components-code/pyproject.toml +++ b/packages/reflex-components-code/pyproject.toml @@ -22,8 +22,10 @@ pattern-prefix = "reflex-components-code-" fallback-version = "0.0.0dev0" [tool.hatch.build] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Generated stubs are gitignored, so they need to be named as build artifacts +# to be shipped. Anchored to `src` because hatchling matches artifacts with +# gitignore semantics and checks them before (and instead of) `include`. +artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] dependencies = [ diff --git a/packages/reflex-components-core/pyproject.toml b/packages/reflex-components-core/pyproject.toml index aa98c9f391b..aab03b2513c 100644 --- a/packages/reflex-components-core/pyproject.toml +++ b/packages/reflex-components-core/pyproject.toml @@ -24,8 +24,10 @@ pattern-prefix = "reflex-components-core-" fallback-version = "0.0.0dev0" [tool.hatch.build] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Generated stubs are gitignored, so they need to be named as build artifacts +# to be shipped. Anchored to `src` because hatchling matches artifacts with +# gitignore semantics and checks them before (and instead of) `include`. +artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] dependencies = [ diff --git a/packages/reflex-components-dataeditor/pyproject.toml b/packages/reflex-components-dataeditor/pyproject.toml index 945713f08b8..a48cf270c42 100644 --- a/packages/reflex-components-dataeditor/pyproject.toml +++ b/packages/reflex-components-dataeditor/pyproject.toml @@ -20,8 +20,10 @@ pattern-prefix = "reflex-components-dataeditor-" fallback-version = "0.0.0dev0" [tool.hatch.build] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Generated stubs are gitignored, so they need to be named as build artifacts +# to be shipped. Anchored to `src` because hatchling matches artifacts with +# gitignore semantics and checks them before (and instead of) `include`. +artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] dependencies = [ diff --git a/packages/reflex-components-gridjs/pyproject.toml b/packages/reflex-components-gridjs/pyproject.toml index b4faf7bdace..1f48510e251 100644 --- a/packages/reflex-components-gridjs/pyproject.toml +++ b/packages/reflex-components-gridjs/pyproject.toml @@ -17,8 +17,10 @@ pattern-prefix = "reflex-components-gridjs-" fallback-version = "0.0.0dev0" [tool.hatch.build] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Generated stubs are gitignored, so they need to be named as build artifacts +# to be shipped. Anchored to `src` because hatchling matches artifacts with +# gitignore semantics and checks them before (and instead of) `include`. +artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] dependencies = ["ruff", "reflex-base"] diff --git a/packages/reflex-components-internal/pyproject.toml b/packages/reflex-components-internal/pyproject.toml index 0c9df6658fc..418dbdeb061 100644 --- a/packages/reflex-components-internal/pyproject.toml +++ b/packages/reflex-components-internal/pyproject.toml @@ -19,8 +19,10 @@ pattern-prefix = "reflex-components-internal-" fallback-version = "0.0.0dev0" [tool.hatch.build] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Generated stubs are gitignored, so they need to be named as build artifacts +# to be shipped. Anchored to `src` because hatchling matches artifacts with +# gitignore semantics and checks them before (and instead of) `include`. +artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] dependencies = [ diff --git a/packages/reflex-components-lucide/pyproject.toml b/packages/reflex-components-lucide/pyproject.toml index 11e0ba5cfe8..48e8458dd5d 100644 --- a/packages/reflex-components-lucide/pyproject.toml +++ b/packages/reflex-components-lucide/pyproject.toml @@ -17,8 +17,10 @@ pattern-prefix = "reflex-components-lucide-" fallback-version = "0.0.0dev0" [tool.hatch.build] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Generated stubs are gitignored, so they need to be named as build artifacts +# to be shipped. Anchored to `src` because hatchling matches artifacts with +# gitignore semantics and checks them before (and instead of) `include`. +artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] dependencies = ["ruff", "reflex-base"] diff --git a/packages/reflex-components-markdown/pyproject.toml b/packages/reflex-components-markdown/pyproject.toml index 3269e93e648..171e6831d41 100644 --- a/packages/reflex-components-markdown/pyproject.toml +++ b/packages/reflex-components-markdown/pyproject.toml @@ -22,8 +22,10 @@ pattern-prefix = "reflex-components-markdown-" fallback-version = "0.0.0dev0" [tool.hatch.build] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Generated stubs are gitignored, so they need to be named as build artifacts +# to be shipped. Anchored to `src` because hatchling matches artifacts with +# gitignore semantics and checks them before (and instead of) `include`. +artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] dependencies = [ diff --git a/packages/reflex-components-moment/pyproject.toml b/packages/reflex-components-moment/pyproject.toml index 80debf17400..a512c7349f5 100644 --- a/packages/reflex-components-moment/pyproject.toml +++ b/packages/reflex-components-moment/pyproject.toml @@ -17,8 +17,10 @@ pattern-prefix = "reflex-components-moment-" fallback-version = "0.0.0dev0" [tool.hatch.build] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Generated stubs are gitignored, so they need to be named as build artifacts +# to be shipped. Anchored to `src` because hatchling matches artifacts with +# gitignore semantics and checks them before (and instead of) `include`. +artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] dependencies = ["ruff", "reflex-base"] diff --git a/packages/reflex-components-plotly/pyproject.toml b/packages/reflex-components-plotly/pyproject.toml index a265512c125..84775a673b3 100644 --- a/packages/reflex-components-plotly/pyproject.toml +++ b/packages/reflex-components-plotly/pyproject.toml @@ -20,8 +20,10 @@ pattern-prefix = "reflex-components-plotly-" fallback-version = "0.0.0dev0" [tool.hatch.build] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Generated stubs are gitignored, so they need to be named as build artifacts +# to be shipped. Anchored to `src` because hatchling matches artifacts with +# gitignore semantics and checks them before (and instead of) `include`. +artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] dependencies = [ diff --git a/packages/reflex-components-radix/pyproject.toml b/packages/reflex-components-radix/pyproject.toml index 9b786ce4c72..2d848e13a2e 100644 --- a/packages/reflex-components-radix/pyproject.toml +++ b/packages/reflex-components-radix/pyproject.toml @@ -21,8 +21,10 @@ pattern-prefix = "reflex-components-radix-" fallback-version = "0.0.0dev0" [tool.hatch.build] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Generated stubs are gitignored, so they need to be named as build artifacts +# to be shipped. Anchored to `src` because hatchling matches artifacts with +# gitignore semantics and checks them before (and instead of) `include`. +artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] dependencies = [ diff --git a/packages/reflex-components-react-player/pyproject.toml b/packages/reflex-components-react-player/pyproject.toml index ce09493fbe0..19f933e40ae 100644 --- a/packages/reflex-components-react-player/pyproject.toml +++ b/packages/reflex-components-react-player/pyproject.toml @@ -20,8 +20,10 @@ pattern-prefix = "reflex-components-react-player-" fallback-version = "0.0.0dev0" [tool.hatch.build] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Generated stubs are gitignored, so they need to be named as build artifacts +# to be shipped. Anchored to `src` because hatchling matches artifacts with +# gitignore semantics and checks them before (and instead of) `include`. +artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] dependencies = [ diff --git a/packages/reflex-components-recharts/pyproject.toml b/packages/reflex-components-recharts/pyproject.toml index 7f82ea8baf7..8e74bfb91c7 100644 --- a/packages/reflex-components-recharts/pyproject.toml +++ b/packages/reflex-components-recharts/pyproject.toml @@ -17,8 +17,10 @@ pattern-prefix = "reflex-components-recharts-" fallback-version = "0.0.0dev0" [tool.hatch.build] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Generated stubs are gitignored, so they need to be named as build artifacts +# to be shipped. Anchored to `src` because hatchling matches artifacts with +# gitignore semantics and checks them before (and instead of) `include`. +artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] dependencies = ["ruff", "reflex-base"] diff --git a/packages/reflex-components-sonner/pyproject.toml b/packages/reflex-components-sonner/pyproject.toml index e6db6d409c2..98ffc82cf7e 100644 --- a/packages/reflex-components-sonner/pyproject.toml +++ b/packages/reflex-components-sonner/pyproject.toml @@ -20,8 +20,10 @@ pattern-prefix = "reflex-components-sonner-" fallback-version = "0.0.0dev0" [tool.hatch.build] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Generated stubs are gitignored, so they need to be named as build artifacts +# to be shipped. Anchored to `src` because hatchling matches artifacts with +# gitignore semantics and checks them before (and instead of) `include`. +artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] dependencies = ["ruff", "reflex-base", "reflex-components-lucide"] diff --git a/packages/reflex-site-shared/pyproject.toml b/packages/reflex-site-shared/pyproject.toml index 9b9917ecae1..bef0b6155d9 100644 --- a/packages/reflex-site-shared/pyproject.toml +++ b/packages/reflex-site-shared/pyproject.toml @@ -27,8 +27,10 @@ pattern-prefix = "reflex-site-shared-" fallback-version = "0.0.0dev0" [tool.hatch.build] -targets.sdist.artifacts = ["*.pyi"] -targets.wheel.artifacts = ["*.pyi"] +# Generated stubs are gitignored, so they need to be named as build artifacts +# to be shipped. Anchored to `src` because hatchling matches artifacts with +# gitignore semantics and checks them before (and instead of) `include`. +artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] dependencies = [ diff --git a/pyproject.toml b/pyproject.toml index 54abf217bdc..be05bafd500 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -142,8 +142,11 @@ fallback-version = "0.0.0dev0" # `reflex` under packages/, and `artifacts` patterns bypass `include` # altogether, which pulled every sibling package's generated stubs into the # reflex sdist and wheel. -include = ["/reflex", "/scripts/hatch_build.py"] +include = ["/reflex"] artifacts = ["/reflex/**/*.pyi"] +# Only the sdist ships the build hook, so that the wheel can be built from it. +# In the wheel it would just install a stray top-level `scripts` package. +targets.sdist.include = ["/reflex", "/scripts/hatch_build.py"] [tool.hatch.build.hooks.custom] path = "scripts/hatch_build.py" diff --git a/tests/units/test_build_config.py b/tests/units/test_build_config.py index 4962f8cca28..8c98bcff1b7 100644 --- a/tests/units/test_build_config.py +++ b/tests/units/test_build_config.py @@ -1,22 +1,25 @@ -"""Tests for the file selection of the reflex package's hatch build config.""" +"""Tests for the file selection of the workspace's hatch build configs.""" from pathlib import Path import pytest +import tomllib from hatchling.builders.plugin.interface import BuilderInterface from hatchling.builders.sdist import SdistBuilder from hatchling.builders.wheel import WheelBuilder REPO_ROOT = Path(__file__).parents[2] -# Everything the reflex distributions are meant to ship: the package itself, -# its generated stubs, and the build hook that regenerates them when the wheel -# is built from the sdist. +# The build hook that regenerates the stubs. The sdist ships it so that the +# wheel can be built from the sdist; the wheel itself has no use for it. +BUILD_HOOK = "scripts/hatch_build.py" + +# Everything the reflex distributions are meant to ship: the package and its +# generated stubs. SELECTED = [ "reflex/app.py", "reflex/__init__.pyi", "reflex/components/__init__.pyi", - "scripts/hatch_build.py", ] # Sibling workspace packages bundle their own stubs and templates, so none of @@ -31,17 +34,42 @@ "docs/app/docs.py", ] +BUILDERS = pytest.mark.parametrize( + "builder_class", [SdistBuilder, WheelBuilder], ids=["sdist", "wheel"] +) + + +@pytest.fixture +def builder(builder_class: type[BuilderInterface]) -> BuilderInterface: + return builder_class(str(REPO_ROOT)) + -@pytest.fixture(params=[SdistBuilder, WheelBuilder], ids=["sdist", "wheel"]) -def builder(request) -> BuilderInterface: - return request.param(str(REPO_ROOT)) +def stub_packages() -> list[Path]: + """Find the workspace packages that generate stubs at build time. + Discovery keys off the build hook rather than off the artifact patterns, so + that dropping a package's patterns fails its assertions below instead of + quietly dropping it from the parametrization. + Returns: + The directory of every package that runs the stub generator. + """ + packages = [] + for path in sorted((REPO_ROOT / "packages").glob("*/pyproject.toml")): + config = tomllib.loads(path.read_text()) + build = config.get("tool", {}).get("hatch", {}).get("build", {}) + if "reflex-pyi" in build.get("hooks", {}): + packages.append(path.parent) + return packages + + +@BUILDERS @pytest.mark.parametrize("relative_path", SELECTED) def test_build_selects_reflex_files(builder: BuilderInterface, relative_path: str): assert builder.config.include_path(relative_path) +@BUILDERS @pytest.mark.parametrize("relative_path", NOT_SELECTED) def test_build_skips_files_outside_reflex( builder: BuilderInterface, relative_path: str @@ -49,7 +77,11 @@ def test_build_skips_files_outside_reflex( assert not builder.config.include_path(relative_path) -def test_build_walks_only_the_reflex_tree(builder: BuilderInterface): +@BUILDERS +def test_build_walks_only_the_reflex_tree( + builder: BuilderInterface, builder_class: type[BuilderInterface] +): + allowed = {BUILD_HOOK} if builder_class is SdistBuilder else set() selected = { Path(included.relative_path).as_posix() for included in builder.recurse_included_files() @@ -57,6 +89,26 @@ def test_build_walks_only_the_reflex_tree(builder: BuilderInterface): outside = sorted( path for path in selected - if not path.startswith("reflex/") and path != "scripts/hatch_build.py" + if not path.startswith("reflex/") and path not in allowed ) assert outside == [] + + +def test_sdist_ships_the_build_hook(): + assert SdistBuilder(str(REPO_ROOT)).config.include_path(BUILD_HOOK) + + +def test_wheel_omits_the_build_hook(): + assert not WheelBuilder(str(REPO_ROOT)).config.include_path(BUILD_HOOK) + + +@BUILDERS +@pytest.mark.parametrize("package", stub_packages(), ids=lambda path: path.name) +def test_package_ships_only_its_own_stubs( + builder_class: type[BuilderInterface], package: Path +): + config = builder_class(str(package)).config + assert config.include_path("src/module/component.pyi") + # Anything a package keeps beside `src` — fixtures, docs, a vendored + # checkout — is not part of what it distributes. + assert not config.include_path("tests/golden.pyi") From 7d58b9b2d1dd30472588e2f02f0c77705324400d Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 28 Aug 2026 07:35:14 +0000 Subject: [PATCH 3/6] Trim the build comments, rewrite the news fragment for its audience The news fragment was written for a reviewer: it explained hatchling's pattern semantics, counted the files, and narrated the two bugs. All of that is a click away in the PR. Cut it to what a downstream user needs to know. Document that expectation in AGENTS.md so the next fragment starts there, and add the fragment to the submission checklist. The per-package build comment repeated a paragraph of hatchling semantics across 14 files. One line is enough; the reason the two per-target keys collapse into one is that both targets want the same patterns, and a build-level `artifacts` applies to every target. --- AGENTS.md | 21 ++++++++++++++++++- news/+reflex-dist-sibling-pyi.bugfix.md | 2 +- .../reflex-components-code/pyproject.toml | 4 +--- .../reflex-components-core/pyproject.toml | 4 +--- .../pyproject.toml | 4 +--- .../reflex-components-gridjs/pyproject.toml | 4 +--- .../reflex-components-internal/pyproject.toml | 4 +--- .../reflex-components-lucide/pyproject.toml | 4 +--- .../reflex-components-markdown/pyproject.toml | 4 +--- .../reflex-components-moment/pyproject.toml | 4 +--- .../reflex-components-plotly/pyproject.toml | 4 +--- .../reflex-components-radix/pyproject.toml | 4 +--- .../pyproject.toml | 4 +--- .../reflex-components-recharts/pyproject.toml | 4 +--- .../reflex-components-sonner/pyproject.toml | 4 +--- packages/reflex-site-shared/pyproject.toml | 4 +--- pyproject.toml | 11 ++++------ 17 files changed, 39 insertions(+), 51 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fe40510b295..8cf17fdd227 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -91,6 +91,24 @@ Playwright tests use the `page` fixture and navigate to `harness.frontend_url`. When adding/modifying components: `uv run python scripts/make_pyi.py`. Commit `pyi_hashes.json` (not `.pyi` files). If the diff removes many modules, run `uv sync`, delete `.pyi_generator_last_run`, and regenerate. +## Changelog fragments + +User-facing changes need a news fragment in the `news/` directory of each +package they touch (the repo root's `news/` for `reflex`), named +`..md`, or `+..md` before the PR number is known. +Types: `breaking`, `deprecation`, `feature`, `bugfix`, `performance`, `docs`, +`misc`. + +Write for external downstream users, not for reviewers. Every entry links to +its PR, so motivation, narrative, and implementation details belong in the PR +and the commit message — a reader who wants them will follow the link. Keep the +fragment to a sentence or two saying what changed and what it means for a user: + +> Reduce published wheel and sdist size by removing misplaced generated artifacts. + +CI requires a fragment for every package whose source the PR touches; the +`skip-changelog` label waives it for changes that are genuinely not user-facing. + ## Breaking changes and deprecation Reflex has downstream users — don't break them. Provide a fallback path during deprecation. @@ -128,4 +146,5 @@ Before submitting: 3. `uv run pyright reflex tests` passes 4. `pyi_hashes.json` updated if components changed 5. Documentation updated if user-facing behavior changed -6. Deprecation warnings added if breaking changes introduced +6. News fragment added for user-facing changes +7. Deprecation warnings added if breaking changes introduced diff --git a/news/+reflex-dist-sibling-pyi.bugfix.md b/news/+reflex-dist-sibling-pyi.bugfix.md index b074ef9735d..e00ab26a2dd 100644 --- a/news/+reflex-dist-sibling-pyi.bugfix.md +++ b/news/+reflex-dist-sibling-pyi.bugfix.md @@ -1 +1 @@ -The `reflex` sdist and wheel no longer ship the sibling workspace packages' generated `.pyi` stubs (or the `pyi_generator` golden test fixtures). Hatchling matches `include` and `artifacts` with gitignore semantics, so the unanchored patterns also matched `packages/**` — and `artifacts` bypasses `include` entirely — which dragged 176 files that each sibling package already bundles into every `reflex` distribution and installed a stray top-level `packages/` directory alongside `reflex/`. The wheel is now roughly a third of its previous size. The build hook is likewise no longer installed as a top-level `scripts/` directory; it is shipped in the sdist only, where building the wheel needs it. +Reduce published wheel and sdist size by removing misplaced generated artifacts. diff --git a/packages/reflex-components-code/pyproject.toml b/packages/reflex-components-code/pyproject.toml index 5dba253286c..d94a730635a 100644 --- a/packages/reflex-components-code/pyproject.toml +++ b/packages/reflex-components-code/pyproject.toml @@ -22,9 +22,7 @@ pattern-prefix = "reflex-components-code-" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Generated stubs are gitignored, so they need to be named as build artifacts -# to be shipped. Anchored to `src` because hatchling matches artifacts with -# gitignore semantics and checks them before (and instead of) `include`. +# Include uncommitted pyi stubs generated for this package. artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] diff --git a/packages/reflex-components-core/pyproject.toml b/packages/reflex-components-core/pyproject.toml index aab03b2513c..b5de1a12e80 100644 --- a/packages/reflex-components-core/pyproject.toml +++ b/packages/reflex-components-core/pyproject.toml @@ -24,9 +24,7 @@ pattern-prefix = "reflex-components-core-" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Generated stubs are gitignored, so they need to be named as build artifacts -# to be shipped. Anchored to `src` because hatchling matches artifacts with -# gitignore semantics and checks them before (and instead of) `include`. +# Include uncommitted pyi stubs generated for this package. artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] diff --git a/packages/reflex-components-dataeditor/pyproject.toml b/packages/reflex-components-dataeditor/pyproject.toml index a48cf270c42..c14fe2d473b 100644 --- a/packages/reflex-components-dataeditor/pyproject.toml +++ b/packages/reflex-components-dataeditor/pyproject.toml @@ -20,9 +20,7 @@ pattern-prefix = "reflex-components-dataeditor-" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Generated stubs are gitignored, so they need to be named as build artifacts -# to be shipped. Anchored to `src` because hatchling matches artifacts with -# gitignore semantics and checks them before (and instead of) `include`. +# Include uncommitted pyi stubs generated for this package. artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] diff --git a/packages/reflex-components-gridjs/pyproject.toml b/packages/reflex-components-gridjs/pyproject.toml index 1f48510e251..d5cade74326 100644 --- a/packages/reflex-components-gridjs/pyproject.toml +++ b/packages/reflex-components-gridjs/pyproject.toml @@ -17,9 +17,7 @@ pattern-prefix = "reflex-components-gridjs-" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Generated stubs are gitignored, so they need to be named as build artifacts -# to be shipped. Anchored to `src` because hatchling matches artifacts with -# gitignore semantics and checks them before (and instead of) `include`. +# Include uncommitted pyi stubs generated for this package. artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] diff --git a/packages/reflex-components-internal/pyproject.toml b/packages/reflex-components-internal/pyproject.toml index 418dbdeb061..60a60491415 100644 --- a/packages/reflex-components-internal/pyproject.toml +++ b/packages/reflex-components-internal/pyproject.toml @@ -19,9 +19,7 @@ pattern-prefix = "reflex-components-internal-" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Generated stubs are gitignored, so they need to be named as build artifacts -# to be shipped. Anchored to `src` because hatchling matches artifacts with -# gitignore semantics and checks them before (and instead of) `include`. +# Include uncommitted pyi stubs generated for this package. artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] diff --git a/packages/reflex-components-lucide/pyproject.toml b/packages/reflex-components-lucide/pyproject.toml index 48e8458dd5d..27551e0635d 100644 --- a/packages/reflex-components-lucide/pyproject.toml +++ b/packages/reflex-components-lucide/pyproject.toml @@ -17,9 +17,7 @@ pattern-prefix = "reflex-components-lucide-" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Generated stubs are gitignored, so they need to be named as build artifacts -# to be shipped. Anchored to `src` because hatchling matches artifacts with -# gitignore semantics and checks them before (and instead of) `include`. +# Include uncommitted pyi stubs generated for this package. artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] diff --git a/packages/reflex-components-markdown/pyproject.toml b/packages/reflex-components-markdown/pyproject.toml index 171e6831d41..6a97c0bb7fb 100644 --- a/packages/reflex-components-markdown/pyproject.toml +++ b/packages/reflex-components-markdown/pyproject.toml @@ -22,9 +22,7 @@ pattern-prefix = "reflex-components-markdown-" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Generated stubs are gitignored, so they need to be named as build artifacts -# to be shipped. Anchored to `src` because hatchling matches artifacts with -# gitignore semantics and checks them before (and instead of) `include`. +# Include uncommitted pyi stubs generated for this package. artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] diff --git a/packages/reflex-components-moment/pyproject.toml b/packages/reflex-components-moment/pyproject.toml index a512c7349f5..4c39653b356 100644 --- a/packages/reflex-components-moment/pyproject.toml +++ b/packages/reflex-components-moment/pyproject.toml @@ -17,9 +17,7 @@ pattern-prefix = "reflex-components-moment-" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Generated stubs are gitignored, so they need to be named as build artifacts -# to be shipped. Anchored to `src` because hatchling matches artifacts with -# gitignore semantics and checks them before (and instead of) `include`. +# Include uncommitted pyi stubs generated for this package. artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] diff --git a/packages/reflex-components-plotly/pyproject.toml b/packages/reflex-components-plotly/pyproject.toml index 84775a673b3..b8f5dcb3171 100644 --- a/packages/reflex-components-plotly/pyproject.toml +++ b/packages/reflex-components-plotly/pyproject.toml @@ -20,9 +20,7 @@ pattern-prefix = "reflex-components-plotly-" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Generated stubs are gitignored, so they need to be named as build artifacts -# to be shipped. Anchored to `src` because hatchling matches artifacts with -# gitignore semantics and checks them before (and instead of) `include`. +# Include uncommitted pyi stubs generated for this package. artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] diff --git a/packages/reflex-components-radix/pyproject.toml b/packages/reflex-components-radix/pyproject.toml index 2d848e13a2e..55652aac732 100644 --- a/packages/reflex-components-radix/pyproject.toml +++ b/packages/reflex-components-radix/pyproject.toml @@ -21,9 +21,7 @@ pattern-prefix = "reflex-components-radix-" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Generated stubs are gitignored, so they need to be named as build artifacts -# to be shipped. Anchored to `src` because hatchling matches artifacts with -# gitignore semantics and checks them before (and instead of) `include`. +# Include uncommitted pyi stubs generated for this package. artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] diff --git a/packages/reflex-components-react-player/pyproject.toml b/packages/reflex-components-react-player/pyproject.toml index 19f933e40ae..a578b0fe876 100644 --- a/packages/reflex-components-react-player/pyproject.toml +++ b/packages/reflex-components-react-player/pyproject.toml @@ -20,9 +20,7 @@ pattern-prefix = "reflex-components-react-player-" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Generated stubs are gitignored, so they need to be named as build artifacts -# to be shipped. Anchored to `src` because hatchling matches artifacts with -# gitignore semantics and checks them before (and instead of) `include`. +# Include uncommitted pyi stubs generated for this package. artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] diff --git a/packages/reflex-components-recharts/pyproject.toml b/packages/reflex-components-recharts/pyproject.toml index 8e74bfb91c7..a9b27fd3c80 100644 --- a/packages/reflex-components-recharts/pyproject.toml +++ b/packages/reflex-components-recharts/pyproject.toml @@ -17,9 +17,7 @@ pattern-prefix = "reflex-components-recharts-" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Generated stubs are gitignored, so they need to be named as build artifacts -# to be shipped. Anchored to `src` because hatchling matches artifacts with -# gitignore semantics and checks them before (and instead of) `include`. +# Include uncommitted pyi stubs generated for this package. artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] diff --git a/packages/reflex-components-sonner/pyproject.toml b/packages/reflex-components-sonner/pyproject.toml index 98ffc82cf7e..607c9053979 100644 --- a/packages/reflex-components-sonner/pyproject.toml +++ b/packages/reflex-components-sonner/pyproject.toml @@ -20,9 +20,7 @@ pattern-prefix = "reflex-components-sonner-" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Generated stubs are gitignored, so they need to be named as build artifacts -# to be shipped. Anchored to `src` because hatchling matches artifacts with -# gitignore semantics and checks them before (and instead of) `include`. +# Include uncommitted pyi stubs generated for this package. artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] diff --git a/packages/reflex-site-shared/pyproject.toml b/packages/reflex-site-shared/pyproject.toml index bef0b6155d9..f745b86d273 100644 --- a/packages/reflex-site-shared/pyproject.toml +++ b/packages/reflex-site-shared/pyproject.toml @@ -27,9 +27,7 @@ pattern-prefix = "reflex-site-shared-" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Generated stubs are gitignored, so they need to be named as build artifacts -# to be shipped. Anchored to `src` because hatchling matches artifacts with -# gitignore semantics and checks them before (and instead of) `include`. +# Include uncommitted pyi stubs generated for this package. artifacts = ["/src/**/*.pyi"] [tool.hatch.build.hooks.reflex-pyi] diff --git a/pyproject.toml b/pyproject.toml index be05bafd500..a382e04e931 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,15 +137,12 @@ source = "uv-dynamic-versioning" fallback-version = "0.0.0dev0" [tool.hatch.build] -# Anchored to the repository root: hatchling matches these with gitignore -# semantics, so an unanchored "reflex" also matches nested directories named -# `reflex` under packages/, and `artifacts` patterns bypass `include` -# altogether, which pulled every sibling package's generated stubs into the -# reflex sdist and wheel. +# Leading slashes anchor to the repo root; unanchored patterns match nested +# directories under packages/ too. include = ["/reflex"] +# Include uncommitted pyi stubs generated for this package. artifacts = ["/reflex/**/*.pyi"] -# Only the sdist ships the build hook, so that the wheel can be built from it. -# In the wheel it would just install a stray top-level `scripts` package. +# The build hook is only needed to build the wheel from the sdist. targets.sdist.include = ["/reflex", "/scripts/hatch_build.py"] [tool.hatch.build.hooks.custom] From 93b4ef1548c0fc0d48e48cc74a237c9503b33188 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 28 Aug 2026 07:41:42 +0000 Subject: [PATCH 4/6] Note that fragments may carry an example or a migration snippet The brevity rule was easy to read as "never more than two sentences", which would cut the one thing a deprecation entry is most useful for: showing the supported form beside the old one. Say what the rule is actually about, and where the cutoff to docs falls. --- AGENTS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 8cf17fdd227..33d278a6e23 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -106,6 +106,12 @@ fragment to a sentence or two saying what changed and what it means for a user: > Reduce published wheel and sdist size by removing misplaced generated artifacts. +Brevity is about the narrative, not the substance: whatever is genuinely useful +downstream belongs in the fragment. A brief usage example for a new feature, or +the before/after of converting deprecated usage to the supported style, earns +its place. Once it runs past a few sentences and a small code block, it is +documentation — write it under `docs/` and let the fragment link there. + CI requires a fragment for every package whose source the PR touches; the `skip-changelog` label waives it for changes that are genuinely not user-facing. From 35f540dc7360fc03e96f83b533154647b095f275 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 28 Aug 2026 07:43:39 +0000 Subject: [PATCH 5/6] Name the news fragment for its PR --- news/{+reflex-dist-sibling-pyi.bugfix.md => 6966.bugfix.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{+reflex-dist-sibling-pyi.bugfix.md => 6966.bugfix.md} (100%) diff --git a/news/+reflex-dist-sibling-pyi.bugfix.md b/news/6966.bugfix.md similarity index 100% rename from news/+reflex-dist-sibling-pyi.bugfix.md rename to news/6966.bugfix.md From 379f43f6c727fc7cdaa25f65bcabf72eb83a8823 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 28 Aug 2026 07:48:22 +0000 Subject: [PATCH 6/6] Read the build config through hatchling instead of tomllib tomllib is stdlib only on 3.11+, so collection failed on the 3.10 leg of the matrix. The repo's fallback elsewhere is `import tomli as tomllib`, but there is no second parser to reach for here: the builder already exposes the parsed `[tool.hatch.build]` table, and it is the same parse the assertions run against. Verified on 3.10 and 3.14. --- tests/units/test_build_config.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/units/test_build_config.py b/tests/units/test_build_config.py index 8c98bcff1b7..646ee98edf6 100644 --- a/tests/units/test_build_config.py +++ b/tests/units/test_build_config.py @@ -3,7 +3,6 @@ from pathlib import Path import pytest -import tomllib from hatchling.builders.plugin.interface import BuilderInterface from hatchling.builders.sdist import SdistBuilder from hatchling.builders.wheel import WheelBuilder @@ -56,9 +55,8 @@ def stub_packages() -> list[Path]: """ packages = [] for path in sorted((REPO_ROOT / "packages").glob("*/pyproject.toml")): - config = tomllib.loads(path.read_text()) - build = config.get("tool", {}).get("hatch", {}).get("build", {}) - if "reflex-pyi" in build.get("hooks", {}): + build_config = SdistBuilder(str(path.parent)).config.build_config + if "reflex-pyi" in build_config.get("hooks", {}): packages.append(path.parent) return packages