Skip to content

Commit c8346f9

Browse files
authored
feat: add pyi attributes/fields, original source fields (#2538)
This adds attributes and fields of use to static analysis. For type definition files (usually `.pyi` files), the `pyi_srcs` and `pyi_deps` fields are added to the rules. They end up in the PyInfo fields direct_pyi_files and transitive_pyi_files. So that static analysis tools can retain access to a target's Python source files, even if precompiling is enabled, `direct_original_sources` and `transitive_original_sources` fields are added to PyInfo. Work towards #2537, #296
1 parent 2136215 commit c8346f9

File tree

9 files changed

+332
-5
lines changed

9 files changed

+332
-5
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ Unreleased changes template.
9797
* 3.11.11
9898
* 3.12.8
9999
* 3.13.1
100+
* (rules) Attributes for type definition files (`.pyi` files) and type-checking
101+
only dependencies added. See {obj}`py_library.pyi_srcs` and
102+
`py_library.pyi_deps` (and the same named attributes for `py_binary` and
103+
`py_test`).
104+
* (providers) {obj}`PyInfo` has new fields to aid static analysis tools:
105+
{obj}`direct_original_sources`, {obj}`direct_pyi_files`,
106+
{obj}`transitive_original_sources`, {obj}`transitive_pyi_files`.
100107

101108
[20241206]: https://github.com/astral-sh/python-build-standalone/releases/tag/20241206
102109

python/private/attributes.bzl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,35 @@ in the resulting output or not. Valid values are:
375375
* `omit_source`: Don't include the original py source.
376376
""",
377377
),
378+
"pyi_deps": attr.label_list(
379+
doc = """
380+
Dependencies providing type definitions the library needs.
381+
382+
These are dependencies that satisfy imports guarded by `typing.TYPE_CHECKING`.
383+
These are build-time only dependencies and not included as part of a runnable
384+
program (packaging rules may include them, however).
385+
386+
:::{versionadded} VERSION_NEXT_FEATURE
387+
:::
388+
""",
389+
providers = [
390+
[PyInfo],
391+
[CcInfo],
392+
] + _MaybeBuiltinPyInfo,
393+
),
394+
"pyi_srcs": attr.label_list(
395+
doc = """
396+
Type definition files for the library.
397+
398+
These are typically `.pyi` files, but other file types for type-checker specific
399+
formats are allowed. These files are build-time only dependencies and not included
400+
as part of a runnable program (packaging rules may include them, however).
401+
402+
:::{versionadded} VERSION_NEXT_FEATURE
403+
:::
404+
""",
405+
allow_files = True,
406+
),
378407
# Required attribute, but details vary by rule.
379408
# Use create_srcs_attr to create one.
380409
"srcs": None,

python/private/common.bzl

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ def collect_runfiles(ctx, files = depset()):
408408
def create_py_info(
409409
ctx,
410410
*,
411+
original_sources,
411412
required_py_files,
412413
required_pyc_files,
413414
implicit_pyc_files,
@@ -417,6 +418,7 @@ def create_py_info(
417418
418419
Args:
419420
ctx: rule ctx.
421+
original_sources: `depset[File]`; the original input sources from `srcs`
420422
required_py_files: `depset[File]`; the direct, `.py` sources for the
421423
target that **must** be included by downstream targets. This should
422424
only be Python source files. It should not include pyc files.
@@ -435,10 +437,13 @@ def create_py_info(
435437
transitive sources collected from dependencies (the latter is only
436438
necessary for deprecated extra actions support).
437439
"""
438-
439440
py_info = PyInfoBuilder()
441+
py_info.direct_original_sources.add(original_sources)
440442
py_info.direct_pyc_files.add(required_pyc_files)
443+
py_info.direct_pyi_files.add(ctx.files.pyi_srcs)
444+
py_info.transitive_original_sources.add(original_sources)
441445
py_info.transitive_pyc_files.add(required_pyc_files)
446+
py_info.transitive_pyi_files.add(ctx.files.pyi_srcs)
442447
py_info.transitive_implicit_pyc_files.add(implicit_pyc_files)
443448
py_info.transitive_implicit_pyc_source_files.add(implicit_pyc_source_files)
444449
py_info.imports.add(imports)
@@ -457,6 +462,10 @@ def create_py_info(
457462
if f.extension == "py":
458463
py_info.transitive_sources.add(f)
459464
py_info.merge_uses_shared_libraries(cc_helper.is_valid_shared_library_artifact(f))
465+
for target in ctx.attr.pyi_deps:
466+
# PyInfo may not be present e.g. cc_library rules.
467+
if PyInfo in target or (BuiltinPyInfo != None and BuiltinPyInfo in target):
468+
py_info.merge(_get_py_info(target))
460469

461470
deps_transitive_sources = py_info.transitive_sources.build()
462471
py_info.transitive_sources.add(required_py_files)

python/private/py_executable.bzl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,7 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment =
985985
runfiles_details = runfiles_details,
986986
main_py = main_py,
987987
imports = imports,
988+
original_sources = direct_sources,
988989
required_py_files = required_py_files,
989990
required_pyc_files = required_pyc_files,
990991
implicit_pyc_files = implicit_pyc_files,
@@ -1548,6 +1549,7 @@ def _create_providers(
15481549
ctx,
15491550
executable,
15501551
main_py,
1552+
original_sources,
15511553
required_py_files,
15521554
required_pyc_files,
15531555
implicit_pyc_files,
@@ -1566,6 +1568,8 @@ def _create_providers(
15661568
ctx: The rule ctx.
15671569
executable: File; the target's executable file.
15681570
main_py: File; the main .py entry point.
1571+
original_sources: `depset[File]` the direct `.py` sources for the
1572+
target that were the original input sources.
15691573
required_py_files: `depset[File]` the direct, `.py` sources for the
15701574
target that **must** be included by downstream targets. This should
15711575
only be Python source files. It should not include pyc files.
@@ -1649,6 +1653,7 @@ def _create_providers(
16491653

16501654
py_info, deps_transitive_sources, builtin_py_info = create_py_info(
16511655
ctx,
1656+
original_sources = original_sources,
16521657
required_py_files = required_py_files,
16531658
required_pyc_files = required_pyc_files,
16541659
implicit_pyc_files = implicit_pyc_files,

python/private/py_info.bzl

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ def _PyInfo_init(
3838
direct_pyc_files = depset(),
3939
transitive_pyc_files = depset(),
4040
transitive_implicit_pyc_files = depset(),
41-
transitive_implicit_pyc_source_files = depset()):
41+
transitive_implicit_pyc_source_files = depset(),
42+
direct_original_sources = depset(),
43+
transitive_original_sources = depset(),
44+
direct_pyi_files = depset(),
45+
transitive_pyi_files = depset()):
4246
_check_arg_type("transitive_sources", "depset", transitive_sources)
4347

4448
# Verify it's postorder compatible, but retain is original ordering.
@@ -53,14 +57,24 @@ def _PyInfo_init(
5357

5458
_check_arg_type("transitive_implicit_pyc_files", "depset", transitive_pyc_files)
5559
_check_arg_type("transitive_implicit_pyc_source_files", "depset", transitive_pyc_files)
60+
61+
_check_arg_type("direct_original_sources", "depset", direct_original_sources)
62+
_check_arg_type("transitive_original_sources", "depset", transitive_original_sources)
63+
64+
_check_arg_type("direct_pyi_files", "depset", direct_pyi_files)
65+
_check_arg_type("transitive_pyi_files", "depset", transitive_pyi_files)
5666
return {
67+
"direct_original_sources": direct_original_sources,
5768
"direct_pyc_files": direct_pyc_files,
69+
"direct_pyi_files": direct_pyi_files,
5870
"has_py2_only_sources": has_py2_only_sources,
5971
"has_py3_only_sources": has_py2_only_sources,
6072
"imports": imports,
6173
"transitive_implicit_pyc_files": transitive_implicit_pyc_files,
6274
"transitive_implicit_pyc_source_files": transitive_implicit_pyc_source_files,
75+
"transitive_original_sources": transitive_original_sources,
6376
"transitive_pyc_files": transitive_pyc_files,
77+
"transitive_pyi_files": transitive_pyi_files,
6478
"transitive_sources": transitive_sources,
6579
"uses_shared_libraries": uses_shared_libraries,
6680
}
@@ -69,6 +83,18 @@ PyInfo, _unused_raw_py_info_ctor = define_bazel_6_provider(
6983
doc = "Encapsulates information provided by the Python rules.",
7084
init = _PyInfo_init,
7185
fields = {
86+
"direct_original_sources": """
87+
:type: depset[File]
88+
89+
The `.py` source files (if any) that are considered directly provided by
90+
the target. This field is intended so that static analysis tools can recover the
91+
original Python source files, regardless of any build settings (e.g.
92+
precompiling), so they can analyze source code. The values are typically the
93+
`.py` files in the `srcs` attribute (or equivalent).
94+
95+
::::{versionadded} 1.1.0
96+
::::
97+
""",
7298
"direct_pyc_files": """
7399
:type: depset[File]
74100
@@ -78,6 +104,21 @@ by the target and **must be included**.
78104
These files usually come from, e.g., a library setting {attr}`precompile=enabled`
79105
to forcibly enable precompiling for itself. Downstream binaries are expected
80106
to always include these files, as the originating target expects them to exist.
107+
""",
108+
"direct_pyi_files": """
109+
:type: depset[File]
110+
111+
Type definition files (usually `.pyi` files) for the Python modules provided by
112+
this target. Usually they describe the source files listed in
113+
`direct_original_sources`. This field is primarily for static analysis tools.
114+
115+
:::{note}
116+
This may contain implementation-specific file types specific to a particular
117+
type checker.
118+
:::
119+
120+
::::{versionadded} 1.1.0
121+
::::
81122
""",
82123
"has_py2_only_sources": """
83124
:type: bool
@@ -116,6 +157,21 @@ then {obj}`transitive_implicit_pyc_files` should be included instead.
116157
117158
::::{versionadded} 0.37.0
118159
::::
160+
""",
161+
"transitive_original_sources": """
162+
:type: depset[File]
163+
164+
The transitive set of `.py` source files (if any) that are considered the
165+
original sources for this target and its transitive dependencies. This field is
166+
intended so that static analysis tools can recover the original Python source
167+
files, regardless of any build settings (e.g. precompiling), so they can analyze
168+
source code. The values are typically the `.py` files in the `srcs` attribute
169+
(or equivalent).
170+
171+
This is superset of `direct_original_sources`.
172+
173+
::::{versionadded} 1.1.0
174+
::::
119175
""",
120176
"transitive_pyc_files": """
121177
:type: depset[File]
@@ -125,6 +181,22 @@ The transitive set of precompiled files that must be included.
125181
These files usually come from, e.g., a library setting {attr}`precompile=enabled`
126182
to forcibly enable precompiling for itself. Downstream binaries are expected
127183
to always include these files, as the originating target expects them to exist.
184+
""",
185+
"transitive_pyi_files": """
186+
:type: depset[File]
187+
188+
The transitive set of type definition files (usually `.pyi` files) for the
189+
Python modules for this target and its transitive dependencies. this target.
190+
Usually they describe the source files listed in `transitive_original_sources`.
191+
This field is primarily for static analysis tools.
192+
193+
:::{note}
194+
This may contain implementation-specific file types specific to a particular
195+
type checker.
196+
:::
197+
198+
::::{versionadded} 1.1.0
199+
::::
128200
""",
129201
"transitive_sources": """\
130202
:type: depset[File]
@@ -165,7 +237,9 @@ def PyInfoBuilder():
165237
_uses_shared_libraries = [False],
166238
build = lambda *a, **k: _PyInfoBuilder_build(self, *a, **k),
167239
build_builtin_py_info = lambda *a, **k: _PyInfoBuilder_build_builtin_py_info(self, *a, **k),
240+
direct_original_sources = builders.DepsetBuilder(),
168241
direct_pyc_files = builders.DepsetBuilder(),
242+
direct_pyi_files = builders.DepsetBuilder(),
169243
get_has_py2_only_sources = lambda *a, **k: _PyInfoBuilder_get_has_py2_only_sources(self, *a, **k),
170244
get_has_py3_only_sources = lambda *a, **k: _PyInfoBuilder_get_has_py3_only_sources(self, *a, **k),
171245
get_uses_shared_libraries = lambda *a, **k: _PyInfoBuilder_get_uses_shared_libraries(self, *a, **k),
@@ -182,7 +256,9 @@ def PyInfoBuilder():
182256
set_uses_shared_libraries = lambda *a, **k: _PyInfoBuilder_set_uses_shared_libraries(self, *a, **k),
183257
transitive_implicit_pyc_files = builders.DepsetBuilder(),
184258
transitive_implicit_pyc_source_files = builders.DepsetBuilder(),
259+
transitive_original_sources = builders.DepsetBuilder(),
185260
transitive_pyc_files = builders.DepsetBuilder(),
261+
transitive_pyi_files = builders.DepsetBuilder(),
186262
transitive_sources = builders.DepsetBuilder(),
187263
)
188264
return self
@@ -221,13 +297,39 @@ def _PyInfoBuilder_set_uses_shared_libraries(self, value):
221297
return self
222298

223299
def _PyInfoBuilder_merge(self, *infos, direct = []):
300+
"""Merge other PyInfos into this PyInfo.
301+
302+
Args:
303+
self: implicitly added.
304+
*infos: {type}`PyInfo` objects to merge in, but only merge in their
305+
information into this object's transitive fields.
306+
direct: {type}`list[PyInfo]` objects to merge in, but also merge their
307+
direct fields into this object's direct fields.
308+
309+
Returns:
310+
{type}`PyInfoBuilder` the current object
311+
"""
224312
return self.merge_all(list(infos), direct = direct)
225313

226314
def _PyInfoBuilder_merge_all(self, transitive, *, direct = []):
315+
"""Merge other PyInfos into this PyInfo.
316+
317+
Args:
318+
self: implicitly added.
319+
transitive: {type}`list[PyInfo]` objects to merge in, but only merge in
320+
their information into this object's transitive fields.
321+
direct: {type}`list[PyInfo]` objects to merge in, but also merge their
322+
direct fields into this object's direct fields.
323+
324+
Returns:
325+
{type}`PyInfoBuilder` the current object
326+
"""
227327
for info in direct:
228328
# BuiltinPyInfo doesn't have this field
229329
if hasattr(info, "direct_pyc_files"):
330+
self.direct_original_sources.add(info.direct_original_sources)
230331
self.direct_pyc_files.add(info.direct_pyc_files)
332+
self.direct_pyi_files.add(info.direct_pyi_files)
231333

232334
for info in direct + transitive:
233335
self.imports.add(info.imports)
@@ -240,29 +342,58 @@ def _PyInfoBuilder_merge_all(self, transitive, *, direct = []):
240342
if hasattr(info, "transitive_pyc_files"):
241343
self.transitive_implicit_pyc_files.add(info.transitive_implicit_pyc_files)
242344
self.transitive_implicit_pyc_source_files.add(info.transitive_implicit_pyc_source_files)
345+
self.transitive_original_sources.add(info.transitive_original_sources)
243346
self.transitive_pyc_files.add(info.transitive_pyc_files)
347+
self.transitive_pyi_files.add(info.transitive_pyi_files)
244348

245349
return self
246350

247351
def _PyInfoBuilder_merge_target(self, target):
352+
"""Merge a target's Python information in this object.
353+
354+
Args:
355+
self: implicitly added.
356+
target: {type}`Target` targets that provide PyInfo, or other relevant
357+
providers, will be merged into this object. If a target doesn't provide
358+
any relevant providers, it is ignored.
359+
360+
Returns:
361+
{type}`PyInfoBuilder` the current object.
362+
"""
248363
if PyInfo in target:
249364
self.merge(target[PyInfo])
250365
elif BuiltinPyInfo != None and BuiltinPyInfo in target:
251366
self.merge(target[BuiltinPyInfo])
252367
return self
253368

254369
def _PyInfoBuilder_merge_targets(self, targets):
370+
"""Merge multiple targets into this object.
371+
372+
Args:
373+
self: implicitly added.
374+
targets: {type}`list[Target]`
375+
targets that provide PyInfo, or other relevant
376+
providers, will be merged into this object. If a target doesn't provide
377+
any relevant providers, it is ignored.
378+
379+
Returns:
380+
{type}`PyInfoBuilder` the current object.
381+
"""
255382
for t in targets:
256383
self.merge_target(t)
257384
return self
258385

259386
def _PyInfoBuilder_build(self):
260387
if config.enable_pystar:
261388
kwargs = dict(
389+
direct_original_sources = self.direct_original_sources.build(),
262390
direct_pyc_files = self.direct_pyc_files.build(),
263-
transitive_pyc_files = self.transitive_pyc_files.build(),
391+
direct_pyi_files = self.direct_pyi_files.build(),
264392
transitive_implicit_pyc_files = self.transitive_implicit_pyc_files.build(),
265393
transitive_implicit_pyc_source_files = self.transitive_implicit_pyc_source_files.build(),
394+
transitive_original_sources = self.transitive_original_sources.build(),
395+
transitive_pyc_files = self.transitive_pyc_files.build(),
396+
transitive_pyi_files = self.transitive_pyi_files.build(),
266397
)
267398
else:
268399
kwargs = {}

python/private/py_library.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def py_library_impl(ctx, *, semantics):
102102
cc_info = semantics.get_cc_info_for_library(ctx)
103103
py_info, deps_transitive_sources, builtins_py_info = create_py_info(
104104
ctx,
105+
original_sources = direct_sources,
105106
required_py_files = required_py_files,
106107
required_pyc_files = required_pyc_files,
107108
implicit_pyc_files = implicit_pyc_files,

0 commit comments

Comments
 (0)