-
-
Notifications
You must be signed in to change notification settings - Fork 98
feat(py): propagate type stubs and carry them into venvs #1538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| """A rules_python library's `pyi_srcs` reach a rules_py consumer's runfiles. | ||
|
|
||
| The stub is metadata in rules_python's PyInfo, never in its own runfiles; the | ||
| rules_py venv must carry it beside the module it annotates so a type checker | ||
| pointed at the venv resolves it. | ||
| """ | ||
|
|
||
| import os | ||
|
|
||
| import stubbed | ||
|
|
||
| stub = os.path.splitext(stubbed.__file__)[0] + ".pyi" | ||
| assert os.path.exists(stub), "missing type stub next to " + stubbed.__file__ | ||
| assert stubbed.describe(1) == "stubbed 1" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| def describe(value: object) -> str: | ||
| return "stubbed " + str(value) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| def describe(value: object) -> str: ... |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| """Analysis test: rules_python `pyi_srcs` surface in rules_py's `PyInfo`.""" | ||
|
|
||
| load("@aspect_rules_py//py:defs.bzl", "PyInfo") | ||
| load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") | ||
|
|
||
| def _basenames(files): | ||
| return sorted([file.basename for file in files.to_list()]) | ||
|
|
||
| def _foreign_stubs_test_impl(ctx): | ||
| env = analysistest.begin(ctx) | ||
| info = analysistest.target_under_test(env)[PyInfo] | ||
| asserts.equals(env, ["stubbed.pyi"], _basenames(info.transitive_pyi_files)) | ||
| asserts.equals(env, ["stubbed.py"], _basenames(info.transitive_sources), "the stub is not a runtime source") | ||
| return analysistest.end(env) | ||
|
|
||
| foreign_stubs_test = analysistest.make(_foreign_stubs_test_impl) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| """protobuf's py_proto_library emits `greeting_pb2.pyi` only through | ||
| rules_python's `PyInfo.transitive_pyi_files`; the rules_py venv has to place it | ||
| beside `greeting_pb2.py` for type checkers to see the generated message types. | ||
| """ | ||
|
|
||
| import os | ||
|
|
||
| import greeting_pb2 | ||
|
|
||
| stub = os.path.splitext(greeting_pb2.__file__)[0] + ".pyi" | ||
| assert os.path.exists(stub), "missing generated stub next to " + greeting_pb2.__file__ | ||
|
|
||
| with open(stub) as handle: | ||
| assert "class Greeting" in handle.read(), stub |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| """Analysis test: generated `_pb2.pyi` stubs surface in rules_py's `PyInfo`.""" | ||
|
|
||
| load("@aspect_rules_py//py:defs.bzl", "PyInfo") | ||
| load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") | ||
|
|
||
| def _basenames(files): | ||
| return sorted([file.basename for file in files.to_list()]) | ||
|
|
||
| def _generated_stubs_test_impl(ctx): | ||
| env = analysistest.begin(ctx) | ||
| info = analysistest.target_under_test(env)[PyInfo] | ||
| asserts.true(env, "greeting_pb2.pyi" in _basenames(info.transitive_pyi_files), "py_proto_library's stub propagates") | ||
| asserts.false(env, "greeting_pb2.pyi" in _basenames(info.transitive_sources), "the stub is not a runtime source") | ||
| return analysistest.end(env) | ||
|
|
||
| generated_stubs_test = analysistest.make(_generated_stubs_test_impl) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| GREETING: str |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| """Analysis test: a rules_py stub survives merging into rules_python's PyInfo.""" | ||
|
|
||
| load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") | ||
| load("@rules_python//python:py_info.bzl", "PyInfo") | ||
|
|
||
| def _basenames(files): | ||
| return sorted([file.basename for file in files.to_list()]) | ||
|
|
||
| def _merged_stubs_test_impl(ctx): | ||
| env = analysistest.begin(ctx) | ||
| info = analysistest.target_under_test(env)[PyInfo] | ||
| asserts.equals(env, [], _basenames(info.direct_pyi_files), "the wrapper declares no stubs of its own") | ||
| asserts.equals(env, ["lib.pyi"], _basenames(info.transitive_pyi_files)) | ||
| asserts.equals(env, ["lib.py"], _basenames(info.transitive_sources), "the stub is not a runtime source") | ||
| return analysistest.end(env) | ||
|
|
||
| merged_stubs_test = analysistest.make(_merged_stubs_test_impl) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 the `.pyi` closure from either ruleset's `PyInfo`, or an empty depset for targets carrying neither.""" | ||
| if PyInfo in target: | ||
| return target[PyInfo].transitive_pyi_files | ||
|
Comment on lines
+43
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a downstream custom rule constructs the publicly exported Useful? React with 👍 / 👎. |
||
| if RulesPythonPyInfo in target: | ||
| return target[RulesPythonPyInfo].transitive_pyi_files | ||
| return depset() | ||
There was a problem hiding this comment.
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
srcsplus 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