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(),