Skip to content

Commit 67872b7

Browse files
committed
address review comments and finish some todos
* fail if both imports and site_packages_root set * doc the topological ordering more * skip, not fail, for child-path symlinks * Also treat .pyi files as namespace anchors
1 parent f2fad71 commit 67872b7

File tree

4 files changed

+86
-41
lines changed

4 files changed

+86
-41
lines changed

python/private/attributes.bzl

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -283,11 +283,7 @@ that depend on this rule. The strings are repo-runfiles-root relative,
283283
Absolute paths (paths that start with `/`) and paths that references a path
284284
above the execution root are not allowed and will result in an error.
285285
286-
:::{attention}
287-
Setting both this and the {attr}`site_packages_root` attribute may result in
288-
undefined behavior. Both will result in the code being importable, but from
289-
different sys.path (and thus `__file__`) entries.
290-
:::
286+
This attribute is mutually exclusive with the {attr}`site_packages_root` attribute.
291287
""",
292288
),
293289
}
@@ -314,6 +310,17 @@ These are typically `py_library` rules.
314310
315311
Targets that only provide data files used at runtime belong in the `data`
316312
attribute.
313+
314+
:::{note}
315+
The order of this list can matter because it affects the order that information
316+
from dependencies is merged in, which can be relevant depending on the ordering
317+
mode of depsets that are merged.
318+
319+
* {obj}`PyInfo.site_packages_symlinks` uses topological ordering.
320+
321+
See {obj}`PyInfo` for more information about the ordering of its depsets and
322+
how its fields are merged.
323+
:::
317324
""",
318325
),
319326
"precompile": attr.string(

python/private/common.bzl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@ PackageSpecificationInfo = getattr(py_internal, "PackageSpecificationInfo", None
3030
# Extensions without the dot
3131
_PYTHON_SOURCE_EXTENSIONS = ["py"]
3232

33-
# Extensions that make a file considered importable
33+
# Extensions that mean a file is relevant to Python
3434
PYTHON_FILE_EXTENSIONS = [
35-
"py",
36-
"so", # Python C modules, usually Linux
35+
"dll", # Python C modules, Windows specific
3736
"dylib", # Python C modules, Mac specific
37+
"py",
3838
"pyc",
39-
"dll", # Python C modules, Windows specific
39+
"pyi",
40+
"so", # Python C modules, usually Linux
4041
]
4142

4243
def create_binary_semantics_struct(

python/private/py_executable.bzl

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,23 @@ def _create_site_packages_symlinks(ctx, site_packages):
624624
if PyInfo in dep
625625
],
626626
).to_list()
627+
link_map = _build_link_map(entries)
628+
629+
sp_files = []
630+
for sp_dir_path, link_to in link_map.items():
631+
sp_link = ctx.actions.declare_symlink(paths.join(site_packages, sp_dir_path))
632+
sp_link_rf_path = runfiles_root_path(ctx, sp_link.short_path)
633+
rel_path = relative_path(
634+
# dirname is necessary because a relative symlink is relative to
635+
# the directory the symlink resides within.
636+
from_ = paths.dirname(sp_link_rf_path),
637+
to = link_to,
638+
)
639+
ctx.actions.symlink(output = sp_link, target_path = rel_path)
640+
sp_files.append(sp_link)
641+
return sp_files
642+
643+
def _build_link_map(entries):
627644
link_map = {}
628645
for link_to_runfiles_path, site_packages_path in entries:
629646
if site_packages_path in link_map:
@@ -635,33 +652,27 @@ def _create_site_packages_symlinks(ctx, site_packages):
635652

636653
# An empty link_to value means to not create the site package symlink.
637654
# Because of the topological ordering, this allows binaries to remove
638-
# entries by having an earlier dependency produce empty link_to values
655+
# entries by having an earlier dependency produce empty link_to values.
639656
for sp_dir_path, link_to in link_map.items():
640657
if not link_to:
641658
link_map.pop(sp_dir_path)
642659

643-
# This is N^2; we can certainly do better by sorting and exploiting the
644-
# order.
645-
# A trailing slash is appended / to prevent /X matching /XY
646-
sp_dirs = [x + "/" for x in link_map.keys()]
647-
for search_for in sp_dirs:
648-
for prefix in sp_dirs:
649-
if search_for != prefix and search_for.startswith(prefix):
650-
fail("sub-link: {} under {}", search_for, prefix)
660+
# Remove entries that would be a child path of a created symlink.
661+
# Earlier entries have precedence to match how exact matches are handled.
662+
keep_link_map = {}
663+
for _ in range(len(link_map)):
664+
if not link_map:
665+
break
666+
dirname, value = link_map.popitem()
667+
keep_link_map[dirname] = value
651668

652-
sp_files = []
653-
for sp_dir_path, link_to in link_map.items():
654-
sp_link = ctx.actions.declare_symlink(paths.join(site_packages, sp_dir_path))
655-
sp_link_rf_path = runfiles_root_path(ctx, sp_link.short_path)
656-
rel_path = relative_path(
657-
# dirname is necessary because a relative symlink is relative to
658-
# the directory the symlink resides within.
659-
from_ = paths.dirname(sp_link_rf_path),
660-
to = link_to,
661-
)
662-
ctx.actions.symlink(output = sp_link, target_path = rel_path)
663-
sp_files.append(sp_link)
664-
return sp_files
669+
prefix = dirname + "/" # Add slash to prevent /X matching /XY
670+
for maybe_suffix in link_map.keys():
671+
maybe_suffix += "/" # Add slash to prevent /X matching /XY
672+
if maybe_suffix.startswith(prefix) or prefix.startswith(maybe_suffix):
673+
link_map.pop(maybe_suffix)
674+
675+
return keep_link_map
665676

666677
def _map_each_identity(v):
667678
return v

python/private/py_library.bzl

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,25 +62,40 @@ LIBRARY_ATTRS = union_attrs(
6262
doc = """
6363
Package relative prefix to remove from `srcs` for site-packages layouts.
6464
65+
This attribute is mutually exclusive with the {attr}`imports` attribute.
66+
6567
When set, `srcs` are interpreted to have a file layout as if they were installed
6668
in site-packages. This attribute specifies the directory within `srcs` to treat
6769
as the site-packages root so the correct site-packages relative paths for
6870
the files can be computed.
6971
70-
For example, given `srcs=["site-packages/foo/bar.py"]`, specifying
71-
`site_packages_root="site-packages/" means `foo/bar.py` is the file path
72-
under the binary's venv site-packages directory that should be made availble.
73-
7472
:::{note}
7573
This string is relative to the target's *Bazel package*. e.g. Relative to the
7674
directory with the BUILD file that defines the target (the same as how e.g.
7775
`srcs`).
7876
:::
7977
80-
:::{attention}
81-
Setting both this an the {attr}`imports` attribute may result in undefined
82-
behavior. Both will result in the code being importable, but from different
83-
sys.path (and thus `__file__`) entries.
78+
For example, given `srcs=["site-packages/foo/bar.py"]`, specifying
79+
`site_packages_root="site-packages/" means `foo/bar.py` is the file path
80+
under the binary's venv site-packages directory that should be made available.
81+
82+
`__init__.py` files are treated specially to provide basic support for [implicit
83+
namespace packages](
84+
https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages).
85+
However, the *content* of the files cannot be taken into account, merely their
86+
presence or absense. Stated another way: [pkgutil-style namespace packages](
87+
https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages)
88+
won't be understood as namespace packages; they'll be seen as regular packages. This will
89+
likely lead to conflicts with other targets that contribute to the namespace.
90+
91+
:::{tip}
92+
This attributes populates {obj}`PyInfo.site_packages_symlinks`, which is
93+
a topologically ordered depset. This means dependencies closer and earlier
94+
to a consumer have precedence. See {obj}`PyInfo.site_packages_symlinks` for
95+
more information.
96+
:::
97+
98+
:::{versionadded} VERSION_NEXT_FEATURE
8499
:::
85100
""",
86101
),
@@ -128,7 +143,18 @@ def py_library_impl(ctx, *, semantics):
128143
runfiles.add(collect_runfiles(ctx))
129144
runfiles = runfiles.build(ctx)
130145

131-
site_packages_symlinks = _get_site_packages_symlinks(ctx)
146+
imports = []
147+
site_packages_symlinks = []
148+
if ctx.attr.imports and ctx.attr.site_packages_root:
149+
fail(("Only one of the `imports` or `site_packages_root` attributes " +
150+
"can be set: site_packages_root={}, imports={}").format(
151+
ctx.attr.site_packages_root,
152+
ctx.attr.imports,
153+
))
154+
elif ctx.attr.site_packages_root:
155+
site_packages_symlinks = _get_site_packages_symlinks(ctx)
156+
elif ctx.attr.imports:
157+
imports = collect_imports(ctx, semantics)
132158

133159
cc_info = semantics.get_cc_info_for_library(ctx)
134160
py_info, deps_transitive_sources, builtins_py_info = create_py_info(
@@ -138,7 +164,7 @@ def py_library_impl(ctx, *, semantics):
138164
required_pyc_files = required_pyc_files,
139165
implicit_pyc_files = implicit_pyc_files,
140166
implicit_pyc_source_files = implicit_pyc_source_files,
141-
imports = collect_imports(ctx, semantics),
167+
imports = imports,
142168
site_packages_symlinks = site_packages_symlinks,
143169
)
144170

0 commit comments

Comments
 (0)