Skip to content

feat(py): propagate type stubs and carry them into venvs - #1538

Open
xangcastle wants to merge 3 commits into
mainfrom
pyi-propagation
Open

xangcastle wants to merge 3 commits into
mainfrom
pyi-propagation

Conversation

@xangcastle

Copy link
Copy Markdown
Member

Builds on #1523 (its commit is kept as the first commit here, authorship intact) and makes transitive_pyi_files mean the same thing on both sides of the ruleset boundary.

Closes #558.

Changes

  • .pyi files listed in srcs are classified by extension into PyInfo.transitive_pyi_files, mirroring rules_python where stubs never count as runtime sources. transitive_sources no longer carries them, including through virtual-dependency resolutions. No new pyi_srcs attribute.
  • Venvs that include sources and launchers put both depsets in runfiles. Stubs that only exist in a foreign provider (protobuf's py_proto_library _pb2.pyi, a rules_python pyi_srcs) now land beside the modules they annotate, so a type checker pointed at the venv resolves them. Neither rules_python nor protobuf put stubs in their own runfiles; the venv is where rules_py has to do it.
  • Under --@aspect_rules_py//py:emit_rules_python_providers the emitted rules_python PyInfo also populates direct_pyi_files.
  • Drops the redundant resolution wrapper and has_py_info guards; the interop accessor already returns an empty depset for targets carrying neither provider.

Changes are visible to end-users: yes

PyInfo.transitive_sources no longer lists .pyi files from srcs; they move to PyInfo.transitive_pyi_files. Runfiles of venvs and launchers gain stubs that come from rules_python deps.

Test plan

  • //py/tests/type-stubs: analysis coverage for first-party, foreign, virtual-resolution, launcher, venv and compatibility-provider shapes, plus a py_test asserting the stubs sit in runfiles at execution time.
  • e2e/rules-python-interop: a rules_python library with pyi_srcs consumed by a rules_py py_library and py_test.
  • e2e/rules-python-protobuf: protobuf's py_proto_library generating greeting_pb2.pyi, propagated through rules_py and present in runfiles.
  • e2e/rules-python-provider-compat: rules_python's PyInfo builder merging a rules_py stub under the compatibility layer.
  • Full //... plus the three e2e workspaces and test.sh pass locally.

mkanat and others added 2 commits September 10, 2026 22:23
…to venvs

Builds on the transitive_pyi_files plumbing from #1523 and makes the field
mean the same thing on both sides of the ruleset boundary.

- `.pyi` files listed in `srcs` are classified by extension into
  `PyInfo.transitive_pyi_files`, mirroring rules_python where stubs never
  count as runtime sources. `transitive_sources` no longer carries them,
  including through virtual-dependency resolutions.
- Venvs that include sources and launchers put both depsets in runfiles, so
  stubs that only exist in a foreign provider (py_proto_library, a
  rules_python `pyi_srcs`) land beside the modules they annotate.
- Under the migration flag the emitted rules_python PyInfo also populates
  `direct_pyi_files`.
- Drops the redundant resolution wrapper and `has_py_info` guards; the
  interop accessor already returns an empty depset for foreign-less targets.

Tests: analysis coverage in //py/tests/type-stubs for first-party, foreign,
virtual-resolution, launcher, venv and compatibility-provider shapes, plus a
runtime check that stubs sit in runfiles. e2e: a rules_python `pyi_srcs`
library consumed by rules_py, protobuf's py_proto_library `_pb2.pyi`
propagating and landing in runfiles, and rules_python's PyInfo builder
merging a rules_py stub under the compatibility layer.
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: f19c9381-96f1-4e07-b197-2da3eb05a6a4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@aspect-workflows

aspect-workflows Bot commented Sep 11, 2026

Copy link
Copy Markdown

✨ Aspect Workflows Tasks

📅 Fri Sep 11 04:47:26 UTC 2026

✅ 44 successful tasks

  • ✅ buildifier · ⏱ 16.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ gazelle · ⏱ 16.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ test-e2e-bazel-8 [test] · ⏱ 6m 20s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (324/324 passed)
  • ✅ test-e2e-bazel-9 [test] · ⏱ 6m 11s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (314/314 passed)
  • ✅ test-e2e-crossbuild-bazel-8 [test] · ⏱ 6m 41s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (39/39 passed)
  • ✅ test-e2e-crossbuild-bazel-9 [test] · ⏱ 6m 48s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (39/39 passed)
  • ✅ test-e2e-interpreter-build-config-bazel-8 [test] · ⏱ 22s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-build-config-bazel-9 [test] · ⏱ 45.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-input-validation-bazel-8 [test] · ⏱ 20.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-input-validation-bazel-9 [test] · ⏱ 30.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-runtime-metadata-bazel-8 [test] · ⏱ 22.4s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-e2e-interpreter-runtime-metadata-bazel-9 [test] · ⏱ 55.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-e2e-interpreter-toolchain-settings-bazel-8 [test] · ⏱ 21.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-toolchain-settings-bazel-9 [test] · ⏱ 1m 2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-rules-python-interop-bazel-8 [test] · ⏱ 50.4s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (18/18 passed)
  • ✅ test-e2e-rules-python-interop-bazel-9 [test] · ⏱ 1m 53s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (18/18 passed)
  • ✅ test-e2e-rules-python-protobuf-bazel-8 [test] · ⏱ 1m 45s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (3/3 passed)
  • ✅ test-e2e-rules-python-protobuf-bazel-9 [test] · ⏱ 1m 27s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (3/3 passed)
  • ✅ test-e2e-rules-python-provider-compat-bazel-8 [test] · ⏱ 31.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (5/5 passed)
  • ✅ test-e2e-rules-python-provider-compat-bazel-9 [test] · ⏱ 1m 24s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (5/5 passed)
  • ✅ test-examples-debugger-bazel-8 [test] · ⏱ 26.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-debugger-bazel-9 [test] · ⏱ 1m 2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-dev_deps-bazel-8 [test] · ⏱ 30.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-dev_deps-bazel-9 [test] · ⏱ 57.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-django-bazel-8 [test] · ⏱ 26.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-django-bazel-9 [test] · ⏱ 53.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-multi_version-bazel-8 [test] · ⏱ 27.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-examples-multi_version-bazel-9 [test] · ⏱ 59.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-examples-protobuf-bazel-8 [test] · ⏱ 1m 42s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-protobuf-bazel-9 [test] · ⏱ 2m 4s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-py_binary-bazel-8 [test] · ⏱ 22.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-py_binary-bazel-9 [test] · ⏱ 51.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-py_pex_binary-bazel-8 [test] · ⏱ 25.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-py_pex_binary-bazel-9 [test] · ⏱ 44.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-py_venv-bazel-8 [test] · ⏱ 24.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (3/3 passed)
  • ✅ test-examples-py_venv-bazel-9 [test] · ⏱ 56.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (3/3 passed)
  • ✅ test-examples-pytest-bazel-8 [test] · ⏱ 45.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (11/11 passed)
  • ✅ test-examples-pytest-bazel-9 [test] · ⏱ 1m 7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (11/11 passed)
  • ✅ test-examples-uv_pip_compile-bazel-8 [test] · ⏱ 24.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-uv_pip_compile-bazel-9 [test] · ⏱ 55s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-virtual_deps-bazel-8 [test] · ⏱ 22s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-virtual_deps-bazel-9 [test] · ⏱ 1m 2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-root-bazel-8 [test] · ⏱ 15m 35s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (381/381 passed)
  • ✅ test-root-bazel-9 [test] · ⏱ 15m 27s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (380/380 passed)

⏱ Last updated Fri Sep 11 05:03:02 UTC 2026 · 📊 GitHub API quota 423/15,000 (3% used, resets in 54m)
🚀 Powered by Aspect CLI (v2026.28.2)  |  Aspect Build · X · LinkedIn · YouTube

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

py_binary startup benchmark

Version Time (ms) vs BCR vs main Build (s)
BCR 1.11.7 150.9/151.0 ±3.0 1.93
main 52.1/52.2 ±1.2 -65.4% 1.65
PR 53.2/53.4 ±1.4 -64.6% +2.3% 1.40

Time = mean/median ±stddev.
Measured with hyperfine --warmup 5 --runs 50 --shell=none on Linux
Gate: PR vs HEAD main median (threshold: 10%, and must exceed the 2×SE noise floor, here 1.0%). BCR is shown only as a historical baseline.
Build time: cold bazel build //:bench with isolated output base, no disk cache; external repos prefetched so network is excluded.

sys.path quality

Version entries sp roots dupes
BCR 1.11.7 6 1 0
main 7 2 0
PR 7 2 0

sys.path quality measured by bench_syspath inside the assembled venv: sys.path entries, distinct site-packages roots, duplicate realpaths. Duplicates indicate symlink redundancy; many roots suggest an inefficient venv layout.

Bazel analysis benchmark

Version Time (ms) vs BCR vs main Targets Actions Configured (ext) Cfg/target Actions/cfg
BCR 2.0.0-alpha.6 1506/1372 ±324 404 20613 7380 (6675) 18.3 2.79
main 1507/1469 ±268 +7.1% 404 18363 7379 (6674) 18.3 2.49
PR 1412/1308 ±276 -4.7% -11.0% 404 18363 7379 (6674) 18.3 2.49

Measured with hyperfine --warmup 1 --runs 10 on Linux
Time = mean/median ±stddev. Cfg = configured targets; (ext) = the count in external repos (the @pypi hub machinery).
Gate: PR vs HEAD main median (threshold: 10%, and must exceed the 2×SE noise floor, here 16.6%). BCR is shown only as a historical baseline.
Command: warm-server bazel build --nobuild //workspace/..., analysis cache discarded each run via a fresh --action_env value; no disk cache.
Workload: 100 py_test targets — 2 test files/package in file generation mode (one venv per test file); a single dep_group.

py_image_layer benchmark

Scenario Version Time (s) Actions vs BCR vs main
analysis BCR 2.0.0-alpha.6 0.70/0.66 ±0.15 747
analysis main 0.67/0.63 ±0.14 668 -3.7%
analysis PR 0.67/0.63 ±0.15 668 -4.0% -0.3%
1p source BCR 2.0.0-alpha.6 0.68/0.66 ±0.08 4
1p source main 0.67/0.66 ±0.04 5 -0.7%
1p source PR 0.68/0.66 ±0.05 5 +0.2% +0.9%
3p wheel BCR 2.0.0-alpha.6 0.81/0.80 ±0.02 5
3p wheel main 0.85/0.83 ±0.04 6 +3.8%
3p wheel PR 0.83/0.83 ±0.02 6 +3.2% -0.6%

Time = mean/median ±stddev.
Measured with hyperfine on Linux, building //workspace:image_layers (10 binaries, ~30-wheel dep pool, grouped first-party/pip/interpreter tier) with isolated output base, no disk cache.
Scenarios: analysis = warm-server bazel build --nobuild, re-analyzed each run via a fresh --action_env value; incrementals run against a built state with warm analysis: source = append to the last package's lib.py, wheel = rewrite click post_install_patches content.
Actions: for Analysis, the total action count behind the image target from aquery deps(...); for incrementals, actions re-executed for the mutation, from a single instrumented run's BEP build metrics (deterministic; per-mnemonic breakdown in the *-actions.json artifacts). Informational only, not gated.
Gate: PR vs HEAD main median per scenario (threshold: 10%, and must exceed the 2×SE noise floor). BCR is shown only as a historical baseline.

@xangcastle
xangcastle requested a review from jbedard September 15, 2026 19:49
@xangcastle
xangcastle marked this pull request as ready for review September 15, 2026 19:49
@xangcastle

Copy link
Copy Markdown
Member Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e3b746b3f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +43 to +44
if PyInfo in target:
return target[PyInfo].transitive_pyi_files

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Default missing stub fields for legacy PyInfo producers

When a downstream custom rule constructs the publicly exported PyInfo using the previously documented four fields, its provider instance has no transitive_pyi_files value, yet this accessor now reads that field unconditionally whenever the target is used in deps or resolutions. Such existing targets therefore fail during analysis instead of contributing an empty stub set; use getattr(info, "transitive_pyi_files", depset()) so adding this field remains backward-compatible.

Useful? React with 👍 / 👎.

runfiles = venv.runtime_runfiles.merge(ctx.runfiles(files = venv_only))
if include_sources:
runfiles = runfiles.merge(ctx.runfiles(transitive_files = venv.transitive_sources))
runfiles = runfiles.merge(ctx.runfiles(transitive_files = depset(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think PYI should be in the runfiles at all...

Comment thread py/private/py_library.bzl
# A resolution target's default outputs stand in for its sources (a wheel
# install tree has no PyInfo of its own). Stubs among them belong to the
# pyi depset collected alongside, not to the runtime sources.
direct = _runtime_sources(target[DefaultInfo].files.to_list())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't call .to_list()

Comment thread py/private/py_info.bzl
fields = {
"transitive_sources": "depset[File] — postorder depset of first-party `.py` sources in the transitive closure.",
"transitive_sources": "depset[File] — postorder depset of first-party runtime sources in the transitive closure; `.pyi` stubs are excluded.",
"transitive_pyi_files": "depset[File] — postorder depset of `.pyi` type stubs in the transitive closure: stubs listed in `srcs` plus those carried by deps of either ruleset.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can drop the "stubs listed in srcs plus those carried by deps of either ruleset." - that is an implementation detail of rules, and I don't think we should mention "other rulesets" in these docs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR]: Support for pyi files?

3 participants