Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
84ccbb8
basic impl to optimize namespace packages
rickeylev Dec 11, 2025
0139ae8
have non-regex_match fallback for pre bazel 8
rickeylev Dec 11, 2025
776e184
investigate bad file-level merging
rickeylev Dec 12, 2025
8a752a0
fix dupe file conflict merging
rickeylev Dec 13, 2025
01196b4
Merge branch 'main' of https://github.com/bazel-contrib/rules_python …
rickeylev Dec 13, 2025
234bf5f
Merge branch 'fix.conflict.merging.files' into refactor.optimize.venv…
rickeylev Dec 13, 2025
8c705b8
revert docs requirements changes
rickeylev Dec 13, 2025
48a5772
Merge branch 'fix.conflict.merging.files' into refactor.optimize.venv…
rickeylev Dec 13, 2025
894233a
remove debugging changes
rickeylev Dec 13, 2025
d00109d
fix typo
rickeylev Dec 14, 2025
da6ba5e
Merge branch 'main' into fix.conflict.merging.files
rickeylev Dec 14, 2025
8c14f89
Merge branch 'fix.conflict.merging.files' into refactor.optimize.venv…
rickeylev Dec 14, 2025
247957c
basic impl and tests for pkgutil namespace packages
rickeylev Dec 11, 2025
9bd980a
Merge branch 'main' of https://github.com/bazel-contrib/rules_python …
rickeylev Dec 14, 2025
85ccfb4
Merge branch 'fix.conflict.merging.files' into refactor.optimize.venv…
rickeylev Dec 14, 2025
0d7908c
Merge branch 'refactor.optimize.venv.namespace.packages' into refacto…
rickeylev Dec 14, 2025
44e2f39
Merge branch 'main' of https://github.com/bazel-contrib/rules_python …
rickeylev Dec 15, 2025
364bb25
Merge branch 'refactor.optimize.venv.namespace.packages' into refacto…
rickeylev Dec 15, 2025
c0e346a
fix re-init of namespace_package_dirs variable
rickeylev Dec 15, 2025
961e962
revert debug changes to docs targets
rickeylev Dec 15, 2025
d022d87
fix doc build
rickeylev Dec 15, 2025
efb33d9
fix test asserts
rickeylev Dec 15, 2025
759eb35
skip whl test that requires bzlmod+unixy
rickeylev Dec 15, 2025
0effdb0
set --incompatible_use_plus_in_repo_names so tests match on bazel 7
rickeylev Dec 15, 2025
c97c65c
Merge branch 'main' of https://github.com/bazel-contrib/rules_python …
rickeylev Dec 15, 2025
ac6722c
move find_namespace_pkgs to utils
rickeylev Dec 17, 2025
4de1ed3
add some agent helper text
rickeylev Dec 17, 2025
9d31c46
note mrctx in agents
rickeylev Dec 17, 2025
3c0c5fe
Merge branch 'main' into refactor.optimize.venv.pkgutil.namespace.pac…
rickeylev Dec 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ build --//python/config_settings:incompatible_default_to_explicit_init_py=True

# Ensure ongoing compatibility with this flag.
common --incompatible_disallow_struct_provider_syntax
# Makes Bazel 7 act more like Bazel 8
common --incompatible_use_plus_in_repo_names

# Windows makes use of runfiles for some rules
build --enable_runfiles
Expand Down
62 changes: 62 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,68 @@ end of the documentation text.
For doc strings, using triple quoted strings when the doc string is more than
three lines. Do not use a trailing backslack (`\`) for the opening triple-quote.

### Starlark Code

Starlark does not support recursion. Use iterative algorithms instead.

Starlark does not support `while` loops. Use `for` loop with an appropriately
sized iterable instead.

#### Starlark testing

For Starlark tests:

* Use `rules_testing`, not `bazel_skylib`.
* See https://rules-testing.readthedocs.io/en/latest/analysis_tests.html for
examples on using rules_testing.
* See `tests/builders/builders_tests.bzl` for an example of using it in
this project.

A test is defined in two parts:
* A setup function, e.g. `def _test_foo(name)`. This defines targets
and calls `analysis_test`.
* An implementation function, e.g. `def _test_foo_impl(env, target)`. This
contains asserts.

Example:

```
# File: foo_tests.bzl

load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
load("@rules_testing//lib:test_suite.bzl", "test_suite")

_tests = []

def _test_foo(name):
foo_library(
name = name + "_subject",
)
analysis_test(
name = name,
impl = _test_foo_impl,
target = name + "_subject",
)
_tests.append(_test_foo)

def _test_foo_impl(env, target):
env.expect.that_whatever(target[SomeInfo].whatever).equals(expected)

def foo_test_suite(name):
test_suite(name=name, tests=_tests)
```


#### Repository rules

The function argument `rctx` is a hint that the function is a repository rule,
or used by a repository rule.

The function argument `mrctx` is a hint that the function can be used by a
repository rule or module extension.

The `repository_ctx` API docs are at: https://bazel.build/rules/lib/builtins/repository_ctx

### bzl_library targets for bzl source files

* A `bzl_library` target should be defined for every `.bzl` file outside
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ END_UNRELEASED_TEMPLATE
`RULES_PYTHON_ENABLE_PIPSTAR=1` by default. Users of `experimental_index_url` that perform
cross-builds should add {obj}`target_platforms` to their `pip.parse` invocations, which will
become mandatory if any cross-builds are required from the next release.
* (py_library) Attribute {obj}`namespace_package_files` added. It is a hint for
optimizing venv creation.

[20251031]: https://github.com/astral-sh/python-build-standalone/releases/tag/20251031
[20251202]: https://github.com/astral-sh/python-build-standalone/releases/tag/20251202
Expand Down
2 changes: 2 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ use_repo(
"buildkite_config",
"implicit_namespace_ns_sub1",
"implicit_namespace_ns_sub2",
"pkgutil_nspkg1",
"pkgutil_nspkg2",
"rules_python_runtime_env_tc_info",
"somepkg_with_build_files",
"whl_with_build_files",
Expand Down
1 change: 1 addition & 0 deletions python/private/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,7 @@ bzl_library(
"//:__subpackages__",
],
deps = [
":py_internal_bzl",
"@bazel_skylib//lib:types",
],
)
Expand Down
24 changes: 24 additions & 0 deletions python/private/internal_dev_deps.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,30 @@ def _internal_dev_deps_impl(mctx):
enable_implicit_namespace_pkgs = False,
)

whl_from_dir_repo(
name = "pkgutil_nspkg1_whl",
root = "//tests/repos/pkgutil_nspkg1:BUILD.bazel",
output = "pkgutil_nspkg1-1.0-any-none-any.whl",
)
whl_library(
name = "pkgutil_nspkg1",
whl_file = "@pkgutil_nspkg1_whl//:pkgutil_nspkg1-1.0-any-none-any.whl",
requirement = "pkgutil_nspkg1",
enable_implicit_namespace_pkgs = False,
)

whl_from_dir_repo(
name = "pkgutil_nspkg2_whl",
root = "//tests/repos/pkgutil_nspkg2:BUILD.bazel",
output = "pkgutil_nspkg2-1.0-any-none-any.whl",
)
whl_library(
name = "pkgutil_nspkg2",
whl_file = "@pkgutil_nspkg2_whl//:pkgutil_nspkg2-1.0-any-none-any.whl",
requirement = "pkgutil_nspkg2",
enable_implicit_namespace_pkgs = False,
)

internal_dev_deps = module_extension(
implementation = _internal_dev_deps_impl,
doc = "This extension creates internal rules_python dev dependencies.",
Expand Down
15 changes: 15 additions & 0 deletions python/private/py_library.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ The topological order has been removed and if 2 different versions of the same P
package are observed, the behaviour has no guarantees except that it is deterministic
and that only one package version will be included.
:::
""",
),
"namespace_package_files": lambda: attrb.LabelList(
allow_empty = True,
allow_files = True,
doc = """
Files whose directories are namespace packages.

When {obj}`--venv_site_packages=yes` is set, this helps inform which directories should be
treated as namespace packages and expect files from other targets to be contributed.
This allows optimizing the generation of symlinks to be cheaper at analysis time.

:::{versionadded} VERSION_NEXT_FEATURE
:::
""",
),
"_add_srcs_to_runfiles_flag": lambda: attrb.Label(
Expand Down Expand Up @@ -251,6 +265,7 @@ def _get_imports_and_venv_symlinks(ctx, semantics):
package,
version_str,
site_packages_root = imports[0],
namespace_package_files = ctx.files.namespace_package_files,
)
else:
imports = collect_imports(ctx, semantics)
Expand Down
4 changes: 2 additions & 2 deletions python/private/pypi/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -431,16 +431,16 @@ bzl_library(
":attrs_bzl",
":deps_bzl",
":generate_whl_library_build_bazel_bzl",
":parse_whl_name_bzl",
":patch_whl_bzl",
":pep508_requirement_bzl",
":pypi_repo_utils_bzl",
":whl_metadata_bzl",
":whl_target_platforms_bzl",
"//python/private:auth_bzl",
"//python/private:bzlmod_enabled_bzl",
"//python/private:envsubst_bzl",
"//python/private:is_standalone_interpreter_bzl",
"//python/private:repo_utils_bzl",
"//python/private:util_bzl",
"@rules_python_internal//:rules_python_config_bzl",
],
)
Expand Down
36 changes: 36 additions & 0 deletions python/private/pypi/pypi_repo_utils.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

load("@bazel_skylib//lib:types.bzl", "types")
load("//python/private:repo_utils.bzl", "repo_utils")
load("//python/private:util.bzl", "is_importable_name")

def _get_python_interpreter_attr(mrctx, *, python_interpreter = None):
"""A helper function for getting the `python_interpreter` attribute or it's default
Expand Down Expand Up @@ -161,9 +162,44 @@ def _execute_checked_stdout(mrctx, *, python, srcs, **kwargs):
**_execute_prep(mrctx, python = python, srcs = srcs, **kwargs)
)

def _find_namespace_package_files(rctx, install_dir):
"""Finds all `__init__.py` files that belong to namespace packages.

A `__init__.py` file belongs to a namespace package if it contains `__path__ =`,
`pkgutil`, and `extend_path(`.

Args:
rctx (repository_ctx): The repository context.
install_dir (path): The path to the install directory.

Returns:
list[str]: A list of relative paths to `__init__.py` files that belong
to namespace packages.
"""

repo_root = str(rctx.path(".")) + "/"
namespace_package_files = []
for top_level_dir in install_dir.readdir():
if not is_importable_name(top_level_dir.basename):
continue
init_py = top_level_dir.get_child("__init__.py")
if not init_py.exists:
continue
content = rctx.read(init_py)

# Look for code resembling the pkgutil namespace setup code:
# __path__ = __import__("pkgutil").extend_path(__path__, __name__)
if ("__path__ =" in content and
"pkgutil" in content and
"extend_path(" in content):
namespace_package_files.append(str(init_py).removeprefix(repo_root))

return namespace_package_files

pypi_repo_utils = struct(
construct_pythonpath = _construct_pypath,
execute_checked = _execute_checked,
execute_checked_stdout = _execute_checked_stdout,
find_namespace_package_files = _find_namespace_package_files,
resolve_python_interpreter = _resolve_python_interpreter,
)
8 changes: 7 additions & 1 deletion python/private/pypi/whl_library.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -384,11 +384,13 @@ def _whl_library_impl(rctx):
supports_whl_extraction = rp_config.supports_whl_extraction,
)

install_dir_path = whl_path.dirname.get_child("site-packages")
metadata = whl_metadata(
install_dir = whl_path.dirname.get_child("site-packages"),
install_dir = install_dir_path,
read_fn = rctx.read,
logger = logger,
)
namespace_package_files = pypi_repo_utils.find_namespace_package_files(rctx, install_dir_path)

# NOTE @aignas 2024-06-22: this has to live on until we stop supporting
# passing `twine` as a `:pkg` library via the `WORKSPACE` builds.
Expand Down Expand Up @@ -432,6 +434,7 @@ def _whl_library_impl(rctx):
data_exclude = rctx.attr.pip_data_exclude,
group_deps = rctx.attr.group_deps,
group_name = rctx.attr.group_name,
namespace_package_files = namespace_package_files,
)
else:
target_platforms = rctx.attr.experimental_target_platforms or []
Expand Down Expand Up @@ -491,6 +494,8 @@ def _whl_library_impl(rctx):
)
entry_points[entry_point_without_py] = entry_point_script_name

namespace_package_files = pypi_repo_utils.find_namespace_package_files(rctx, rctx.path("site-packages"))

build_file_contents = generate_whl_library_build_bazel(
name = whl_path.basename,
sdist_filename = sdist_filename,
Expand All @@ -509,6 +514,7 @@ def _whl_library_impl(rctx):
"pypi_name={}".format(metadata["name"]),
"pypi_version={}".format(metadata["version"]),
],
namespace_package_files = namespace_package_files,
)

# Delete these in case the wheel had them. They generally don't cause
Expand Down
8 changes: 7 additions & 1 deletion python/private/pypi/whl_library_targets.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def whl_library_targets(
entry_points = {},
native = native,
enable_implicit_namespace_pkgs = False,
namespace_package_files = [],
rules = struct(
copy_file = copy_file,
py_binary = py_binary,
Expand Down Expand Up @@ -169,6 +170,8 @@ def whl_library_targets(
enable_implicit_namespace_pkgs: {type}`boolean` generate __init__.py
files for namespace pkgs.
native: {type}`native` The native struct for overriding in tests.
namespace_package_files: {type}`list[str]` A list of labels of files whose
directories are namespace packages.
rules: {type}`struct` A struct with references to rules for creating targets.
"""
dependencies = sorted([normalize_name(d) for d in dependencies])
Expand Down Expand Up @@ -365,14 +368,16 @@ def whl_library_targets(
)

if not enable_implicit_namespace_pkgs:
srcs = srcs + select({
generated_namespace_package_files = select({
Label("//python/config_settings:is_venvs_site_packages"): [],
"//conditions:default": rules.create_inits(
srcs = srcs + data + pyi_srcs,
ignored_dirnames = [], # If you need to ignore certain folders, you can patch rules_python here to do so.
root = "site-packages",
),
})
namespace_package_files += generated_namespace_package_files
srcs = srcs + generated_namespace_package_files

rules.py_library(
name = py_library_label,
Expand All @@ -391,6 +396,7 @@ def whl_library_targets(
tags = tags,
visibility = impl_vis,
experimental_venvs_site_packages = Label("@rules_python//python/config_settings:venvs_site_packages"),
namespace_package_files = namespace_package_files,
)

def _config_settings(dependencies_by_platform, dependencies_with_markers, rules, native = native, **kwargs):
Expand Down
16 changes: 16 additions & 0 deletions python/private/util.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""Functionality shared by multiple pieces of code."""

load("@bazel_skylib//lib:types.bzl", "types")
load("//python/private:py_internal.bzl", "py_internal")

def copy_propagating_kwargs(from_kwargs, into_kwargs = None):
"""Copies args that must be compatible between two targets with a dependency relationship.
Expand Down Expand Up @@ -69,3 +70,18 @@ def add_tag(attrs, tag):
attrs["tags"] = tags + [tag]
else:
attrs["tags"] = [tag]

def is_importable_name(name):
# Requires Bazel 8+
if hasattr(py_internal, "regex_match"):
# ?U means activates unicode matching (Python allows most unicode
# in module names / identifiers).
# \w matches alphanumeric and underscore.
# NOTE: regex_match has an implicit ^ and $
return py_internal.regex_match(name, "(?U)\\w+")
else:
# Otherwise, use a rough hueristic that should catch most cases.
return (
"." not in name and
"-" not in name
)
Loading