diff --git a/docs/api/py.md b/docs/api/py.md index b4e73a9e7..153de6d40 100644 --- a/docs/api/py.md +++ b/docs/api/py.md @@ -288,7 +288,7 @@ from the already extracted whl file.
load("@aspect_rules_py//py:defs.bzl", "PyInfo")
-PyInfo(transitive_sources, imports, virtual_dependencies, virtual_resolutions)
+PyInfo(transitive_sources, transitive_pyi_files, imports, virtual_dependencies, virtual_resolutions)
Python source, import-path, and virtual-dependency information for a target's dependency closure.
@@ -298,6 +298,7 @@ Python source, import-path, and virtual-dependency information for a target's de
| Name | Description |
| :------------- | :------------- |
| transitive_sources | depset[File] — postorder depset of first-party `.py` sources in the transitive closure. |
+| transitive_pyi_files | depset[File] — postorder depset of `.pyi` type stubs in the transitive closure. |
| imports | depset[str] — import roots to place on `sys.path` (rlocation-root-relative). |
| virtual_dependencies | depset[str] — names of required virtual dependencies, independent of their resolution status. |
| virtual_resolutions | depset[struct(virtual, target)] — virtual-dependency-name to concrete-target resolutions. |
diff --git a/py/private/py_info.bzl b/py/private/py_info.bzl
index 57e174397..7734f0a6a 100644
--- a/py/private/py_info.bzl
+++ b/py/private/py_info.bzl
@@ -12,6 +12,7 @@ RulesPyInfo = provider(
doc = "Python source, import-path, and virtual-dependency information for a target's dependency closure.",
fields = {
"transitive_sources": "depset[File] — postorder depset of first-party `.py` sources in the transitive closure.",
+ "transitive_pyi_files": "depset[File] — postorder depset of `.pyi` type stubs in the transitive closure.",
"imports": "depset[str] — import roots to place on `sys.path` (rlocation-root-relative).",
"virtual_dependencies": "depset[str] — names of required virtual dependencies, independent of their resolution status.",
"virtual_resolutions": "depset[struct(virtual, target)] — virtual-dependency-name to concrete-target resolutions.",
diff --git a/py/private/py_info_interop.bzl b/py/private/py_info_interop.bzl
index 85855e0d6..d65c3ec69 100644
--- a/py/private/py_info_interop.bzl
+++ b/py/private/py_info_interop.bzl
@@ -3,8 +3,8 @@
The `deps` attribute on rules_py rules accepts targets built by either
ruleset. rules_py always emits its own `PyInfo`
(`//py/private:py_info.bzl`); native `@rules_python` targets (e.g.
-a `py_proto_library`) carry `@rules_python`'s. Both expose `transitive_sources`
-and `imports`, which is everything rules_py reads from a foreign dep.
+a `py_proto_library`) carry `@rules_python`'s. Both expose the source, type
+stub, and import information rules_py reads from a foreign dep.
This module is the single place that knows about both providers. Rule code
calls these accessors at the API edge instead of loading `@rules_python`'s
@@ -37,3 +37,11 @@ def get_py_info(target):
if RulesPythonPyInfo in target:
return target[RulesPythonPyInfo]
return None
+
+def get_transitive_pyi_files(target):
+ """Return type stubs from rules_py's or `@rules_python`'s `PyInfo`."""
+ if PyInfo in target:
+ return target[PyInfo].transitive_pyi_files
+ if RulesPythonPyInfo in target:
+ return target[RulesPythonPyInfo].transitive_pyi_files
+ return depset()
diff --git a/py/private/py_library.bzl b/py/private/py_library.bzl
index f56ec7f4c..5f03b674f 100644
--- a/py/private/py_library.bzl
+++ b/py/private/py_library.bzl
@@ -10,7 +10,7 @@ load("@rules_cc//cc/common:cc_info.bzl", "CcInfo")
load("//py/private:providers.bzl", "PyWheelsInfo")
load("//py/private:pth.bzl", "make_imports_depset")
load("//py/private:py_info.bzl", "PyInfo")
-load("//py/private:py_info_interop.bzl", "RulesPythonPyInfo", "get_py_info", "has_py_info")
+load("//py/private:py_info_interop.bzl", "RulesPythonPyInfo", "get_py_info", "get_transitive_pyi_files", "has_py_info")
load("//py/private:transitions.bzl", "reset_python_flags_transition")
def _make_instrumented_files_info(ctx):
@@ -34,6 +34,16 @@ def _make_srcs_depset(ctx, extra_depsets = []):
] + extra_depsets,
)
+def _make_pyi_depset(ctx, extra_depsets = []):
+ return depset(
+ order = "postorder",
+ transitive = [
+ get_transitive_pyi_files(target)
+ for target in ctx.attr.deps
+ if has_py_info(target)
+ ] + extra_depsets,
+ )
+
def _make_virtual_depset(ctx):
return depset(
order = "postorder",
@@ -56,6 +66,11 @@ def _make_resolved_virtual_depset(target):
transitive = transitive,
)
+def _make_resolved_virtual_pyi_depset(target):
+ if has_py_info(target):
+ return get_transitive_pyi_files(target)
+ return depset()
+
def _make_virtual_resolutions_depset(ctx):
return depset(
order = "postorder",
@@ -77,6 +92,7 @@ def _resolve_virtuals(ctx):
# Check for duplicate virtual dependency names. Those that map to the same resolution target would have been merged by the depset for us.
seen = {}
v_srcs = []
+ v_pyi_files = []
v_runfiles = []
v_imports = []
@@ -88,6 +104,7 @@ def _resolve_virtuals(ctx):
seen.update([[resolution.virtual, i]])
v_srcs.append(_make_resolved_virtual_depset(resolution.target))
+ v_pyi_files.append(_make_resolved_virtual_pyi_depset(resolution.target))
v_runfiles.append(resolution.target[DefaultInfo].default_runfiles.files)
info = get_py_info(resolution.target)
@@ -100,6 +117,7 @@ def _resolve_virtuals(ctx):
return struct(
srcs = v_srcs,
+ pyi_files = v_pyi_files,
runfiles = v_runfiles,
imports = v_imports,
)
@@ -153,6 +171,7 @@ def _make_merged_runfiles(ctx, extra_depsets = [], extra_runfiles = [], extra_ru
def _py_library_impl(ctx):
transitive_srcs = _make_srcs_depset(ctx)
+ transitive_pyi_files = _make_pyi_depset(ctx)
imports = _make_imports_depset(ctx)
virtuals = _make_virtual_depset(ctx)
resolutions = _make_virtual_resolutions_depset(ctx)
@@ -168,6 +187,7 @@ def _py_library_impl(ctx):
PyInfo(
imports = imports,
transitive_sources = transitive_srcs,
+ transitive_pyi_files = transitive_pyi_files,
virtual_dependencies = virtuals,
virtual_resolutions = resolutions,
),
@@ -185,6 +205,7 @@ def _py_library_impl(ctx):
providers.append(RulesPythonPyInfo(
imports = imports,
transitive_sources = transitive_srcs,
+ transitive_pyi_files = transitive_pyi_files,
))
return providers
@@ -239,6 +260,7 @@ py_library_utils = struct(
implementation = _py_library_impl,
make_imports_depset = _make_imports_depset,
make_merged_runfiles = _make_merged_runfiles,
+ make_pyi_depset = _make_pyi_depset,
make_srcs_depset = _make_srcs_depset,
make_wheels_depset = _make_wheels_depset,
py_library_providers = _providers,
diff --git a/py/private/py_unpacked_wheel.bzl b/py/private/py_unpacked_wheel.bzl
index 895ae8a49..42b024fb3 100644
--- a/py/private/py_unpacked_wheel.bzl
+++ b/py/private/py_unpacked_wheel.bzl
@@ -75,6 +75,7 @@ def _py_unpacked_wheel_impl(ctx):
),
PyInfo(
imports = imports,
+ transitive_pyi_files = depset(),
transitive_sources = depset([unpack_directory]),
virtual_dependencies = depset(),
virtual_resolutions = depset(),
diff --git a/py/private/py_venv/py_venv.bzl b/py/private/py_venv/py_venv.bzl
index 13910411c..e4525a93c 100644
--- a/py/private/py_venv/py_venv.bzl
+++ b/py/private/py_venv/py_venv.bzl
@@ -85,6 +85,10 @@ def _assemble_venv_target(ctx):
ctx,
extra_depsets = virtual_resolution.srcs,
)
+ pyi_depset = _py_library.make_pyi_depset(
+ ctx,
+ extra_depsets = virtual_resolution.pyi_files,
+ )
runtime_files = depset(
direct = assembled.declared_outputs,
transitive = [
@@ -104,6 +108,7 @@ def _assemble_venv_target(ctx):
bin_python = assembled.bin_python,
imports = imports_depset,
runtime_runfiles = runfiles,
+ transitive_pyi_files = pyi_depset,
transitive_sources = srcs_depset,
runtime_files = runtime_files,
)
diff --git a/py/private/py_venv/py_venv_exec.bzl b/py/private/py_venv/py_venv_exec.bzl
index 5ff973ccf..522cad824 100644
--- a/py/private/py_venv/py_venv_exec.bzl
+++ b/py/private/py_venv/py_venv_exec.bzl
@@ -155,6 +155,7 @@ def _py_venv_exec_impl(ctx):
# launcher will run with. `srcs` / `deps` live on the
# sibling venv, not on this rule.
imports = vinfo.imports,
+ transitive_pyi_files = vinfo.transitive_pyi_files,
transitive_sources = vinfo.transitive_sources,
virtual_dependencies = depset(),
virtual_resolutions = depset(),
@@ -169,6 +170,7 @@ def _py_venv_exec_impl(ctx):
if ctx.attr._emit_rules_python_providers[BuildSettingInfo].value:
providers.append(RulesPythonPyInfo(
imports = vinfo.imports,
+ transitive_pyi_files = vinfo.transitive_pyi_files,
transitive_sources = vinfo.transitive_sources,
))
diff --git a/py/private/py_venv/types.bzl b/py/private/py_venv/types.bzl
index 9b5148f5b..e686ed03c 100644
--- a/py/private/py_venv/types.bzl
+++ b/py/private/py_venv/types.bzl
@@ -23,6 +23,7 @@ binary's launcher exec's the venv's `bin_python`.
"bin_python": "File — the venv's bin/python symlink. Callers needing a launcher target point here.",
"imports": "depset[str] — rlocation-root-relative import paths covered by this venv. Mirrors `PyInfo.imports` of the venv's dep closure.",
"runtime_runfiles": "Runfiles — venv, wheels, and data without Python import sources.",
+ "transitive_pyi_files": "depset[File] — type stubs carried separately from runtime sources.",
"transitive_sources": "depset[File] — source artifacts carried by this venv: its own `srcs`, sources from `deps` that emit `PyInfo`, and files contributed by virtual-resolution targets. Surfaced by py_binary as `PyInfo.transitive_sources` so downstream consumers see the same source closure they'd see if srcs/deps lived on the binary directly.",
"runtime_files": "depset[File] — generated venv support files and the runfiles library; excludes dependency and interpreter runfiles.",
},
diff --git a/py/tests/py-info-interop/BUILD.bazel b/py/tests/py-info-interop/BUILD.bazel
new file mode 100644
index 000000000..a6b303617
--- /dev/null
+++ b/py/tests/py-info-interop/BUILD.bazel
@@ -0,0 +1,24 @@
+load("//py:defs.bzl", "py_library")
+load(":pyi_propagation_test.bzl", "pyi_propagation_test", "rules_python_pyi_fixture")
+
+package(default_testonly = True)
+
+rules_python_pyi_fixture(
+ name = "rules_python_library",
+ srcs = ["library.pyi"],
+)
+
+py_library(
+ name = "rules_py_library",
+ deps = [":rules_python_library"],
+)
+
+py_library(
+ name = "rules_py_consumer",
+ deps = [":rules_py_library"],
+)
+
+pyi_propagation_test(
+ name = "pyi_propagation_test",
+ target_under_test = ":rules_py_consumer",
+)
diff --git a/py/tests/py-info-interop/library.pyi b/py/tests/py-info-interop/library.pyi
new file mode 100644
index 000000000..bb97ee22c
--- /dev/null
+++ b/py/tests/py-info-interop/library.pyi
@@ -0,0 +1 @@
+def greeting(name: str) -> str: ...
diff --git a/py/tests/py-info-interop/pyi_propagation_test.bzl b/py/tests/py-info-interop/pyi_propagation_test.bzl
new file mode 100644
index 000000000..225c892ca
--- /dev/null
+++ b/py/tests/py-info-interop/pyi_propagation_test.bzl
@@ -0,0 +1,27 @@
+"""Analysis coverage for rules_python type-stub interoperability."""
+
+load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
+load("@rules_python//python:defs.bzl", RulesPythonPyInfo = "PyInfo")
+load("//py:defs.bzl", "PyInfo")
+
+def _rules_python_pyi_fixture_impl(ctx):
+ return [RulesPythonPyInfo(
+ transitive_sources = depset(),
+ transitive_pyi_files = depset(ctx.files.srcs),
+ )]
+
+rules_python_pyi_fixture = rule(
+ implementation = _rules_python_pyi_fixture_impl,
+ attrs = {
+ "srcs": attr.label_list(allow_files = [".pyi"]),
+ },
+)
+
+def _pyi_propagation_test_impl(ctx):
+ env = analysistest.begin(ctx)
+ target = analysistest.target_under_test(env)
+ asserts.equals(env, ["library.pyi"], [file.basename for file in target[PyInfo].transitive_pyi_files.to_list()])
+ asserts.equals(env, [], target[PyInfo].transitive_sources.to_list())
+ return analysistest.end(env)
+
+pyi_propagation_test = analysistest.make(_pyi_propagation_test_impl)
diff --git a/py/tests/py_venv_conflict/collision_order_test.bzl b/py/tests/py_venv_conflict/collision_order_test.bzl
index a3431bfb3..e047e544a 100644
--- a/py/tests/py_venv_conflict/collision_order_test.bzl
+++ b/py/tests/py_venv_conflict/collision_order_test.bzl
@@ -66,6 +66,7 @@ printf 'VALUE = "namespace"\n' > "$site/mixed_top/from_namespace.py"
),
PyInfo(
imports = depset([site_packages]),
+ transitive_pyi_files = depset(),
transitive_sources = depset([install_tree]),
virtual_dependencies = depset(),
virtual_resolutions = depset(),
@@ -192,6 +193,7 @@ printf 'native' > "$site/collision_order/native_extension.so"
),
PyInfo(
imports = depset([site_packages]),
+ transitive_pyi_files = depset(),
transitive_sources = depset([install_tree]),
virtual_dependencies = depset(),
virtual_resolutions = depset(),
diff --git a/py/tests/py_venv_conflict/site_merge_order_test.bzl b/py/tests/py_venv_conflict/site_merge_order_test.bzl
index 071819fae..13ea2a565 100644
--- a/py/tests/py_venv_conflict/site_merge_order_test.bzl
+++ b/py/tests/py_venv_conflict/site_merge_order_test.bzl
@@ -121,6 +121,7 @@ printf 'VALUE = %s\n' "$4" > "$site/other/from_final.py"
),
PyInfo(
imports = depset(site_packages_paths),
+ transitive_pyi_files = depset(),
transitive_sources = depset(install_trees),
virtual_dependencies = depset(),
virtual_resolutions = depset(),
@@ -230,6 +231,7 @@ printf 'VALUE = %s\n' "$4" > "$site/mixed/sibling.py"
),
PyInfo(
imports = depset(site_packages_paths),
+ transitive_pyi_files = depset(),
transitive_sources = depset(install_trees),
virtual_dependencies = depset(),
virtual_resolutions = depset(),
diff --git a/uv/private/whl_install/rule.bzl b/uv/private/whl_install/rule.bzl
index 1d448c5a0..f3acc6070 100644
--- a/uv/private/whl_install/rule.bzl
+++ b/uv/private/whl_install/rule.bzl
@@ -323,6 +323,7 @@ def _whl_install(ctx):
transitive_sources = depset([
install_dir,
]),
+ transitive_pyi_files = depset(),
imports = depset([site_packages_rfpath]),
virtual_dependencies = depset(),
virtual_resolutions = depset(),