Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions examples/bzlmod/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ bazel_dep(name = "rules_java", version = "8.16.1")
# were fixed.
bazel_dep(name = "rules_rust", version = "0.67.0")

# Adopt migration away from legacy __init__.py generation.
rules_python_config = use_extension("@rules_python//python/extensions:config.bzl", "config")
rules_python_config.use_explicit_init_py(enabled = True)

# We next initialize the python toolchain using the extension.
# You can set different Python versions in this block.
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
Expand Down
4 changes: 4 additions & 0 deletions examples/bzlmod/other_module/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ local_path_override(
path = "../../..",
)

# Adopt migration away from legacy __init__.py generation.
rules_python_config = use_extension("@rules_python//python/extensions:config.bzl", "config")
rules_python_config.use_explicit_init_py(enabled = True)

python = use_extension("@rules_python//python/extensions:python.bzl", "python")
python.defaults(
# In a submodule this is ignored
Expand Down
14 changes: 14 additions & 0 deletions examples/bzlmod/tests/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
load("@pythons_hub//:versions.bzl", "MINOR_MAPPING")
load("@rules_python//python:py_binary.bzl", "py_binary")
load("@rules_python//python:py_test.bzl", "py_test")
load("@rules_python//tests/support:explicit_init_py_test.bzl", "explicit_init_py_test")
load("@rules_shell//shell:sh_test.bzl", "sh_test")

py_binary(
Expand Down Expand Up @@ -192,3 +193,16 @@ sh_test(
"VERSION_PY_BINARY": "$(rootpaths :version_3_10)",
},
)

explicit_init_py_test(
name = "explicit_init_py_no_generation_test",
expect_generated_init = False,
main = "version.py",
)

explicit_init_py_test(
name = "legacy_create_init_override_test",
expect_generated_init = True,
legacy_create_init = 1,
main = "version.py",
)
39 changes: 39 additions & 0 deletions python/extensions/config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,59 @@ to repositories that are expensive to create or invalidate frequently.
},
)

_use_explicit_init_py = tag_class(
doc = """
Require explicit `__init__.py` files in this module.

Disables the legacy `__init__.py` generation for all `py_*` targets in this
module, requiring all Python targets to explicitly provide `__init__.py` files
when they're needed.

To override this at a per-target level, set `legacy_create_init` on applicable
`py_binary` or `py_test` targets:

```starlark
py_binary(
name = "hello_python",
# ...
# This Binary still relies on legacy behavior, so
# enable the legacy behavior as an exceptional case.
legacy_create_init = 1,
)
```

:::{note}
In the future, this will be enabled by default.
:::
""",
attrs = {
"enabled": attr.bool(doc = "Whether this feature is enabled.", mandatory = True),
},
)

def _config_impl(module_ctx):
transition_setting_generators = {}
transition_settings = []
explicit_init_py_modules = {}
is_root = True
for mod in module_ctx.modules:
for tag in mod.tags.add_transition_setting:
setting = str(tag.setting)
if setting not in transition_setting_generators:
transition_setting_generators[setting] = []
transition_settings.append(setting)
transition_setting_generators[setting].append(mod.name)
for tag in mod.tags.use_explicit_init_py:
explicit_init_py_modules[mod.name] = str(tag.enabled)
if is_root:
explicit_init_py_modules[""] = str(tag.enabled)
is_root = False

internal_config_repo(
name = "rules_python_internal",
transition_setting_generators = transition_setting_generators,
transition_settings = transition_settings,
explicit_init_py_modules = explicit_init_py_modules,
)

pypi_deps()
Expand All @@ -55,5 +93,6 @@ config = module_extension(
implementation = _config_impl,
tag_classes = {
"add_transition_setting": _add_transition_setting,
"use_explicit_init_py": _use_explicit_init_py,
},
)
4 changes: 4 additions & 0 deletions python/private/internal_config_repo.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ config = struct(
BuiltinPyInfo = getattr(getattr(native, "legacy_globals", None), "PyInfo", {builtin_py_info_symbol}),
BuiltinPyRuntimeInfo = getattr(getattr(native, "legacy_globals", None), "PyRuntimeInfo", {builtin_py_runtime_info_symbol}),
BuiltinPyCcLinkParamsProvider = getattr(getattr(native, "legacy_globals", None), "PyCcLinkParamsProvider", {builtin_py_cc_link_params_provider}),
modules_using_explicit_initpy = {modules_using_explicit_initpy},
)
"""

Expand Down Expand Up @@ -100,6 +101,7 @@ def _internal_config_repo_impl(rctx):
builtin_py_info_symbol = "PyInfo"
builtin_py_runtime_info_symbol = "PyRuntimeInfo"
builtin_py_cc_link_params_provider = "PyCcLinkParamsProvider"
explicit_init_py_modules = {k: str(v) == "True" for k, v in rctx.attr.explicit_init_py_modules.items()}

rctx.file("rules_python_config.bzl", _CONFIG_TEMPLATE.format(
build_python_zip_default = repo_utils.get_platforms_os_name(rctx) == "windows",
Expand All @@ -109,6 +111,7 @@ def _internal_config_repo_impl(rctx):
supports_whl_extraction = str(supports_whl_extraction),
extract_needs_chmod = str(extract_needs_chmod),
builtin_py_cc_link_params_provider = builtin_py_cc_link_params_provider,
modules_using_explicit_initpy = str(explicit_init_py_modules),
bazel_8_or_later = str(bazel_major_version >= 8),
bazel_9_or_later = str(bazel_major_version >= 9),
bazel_10_or_later = str(bazel_major_version > 9),
Expand Down Expand Up @@ -140,6 +143,7 @@ internal_config_repo = repository_rule(
configure = True,
environ = [],
attrs = {
"explicit_init_py_modules": attr.string_dict(),
"transition_setting_generators": attr.string_list_dict(),
"transition_settings": attr.string_list(),
},
Expand Down
37 changes: 27 additions & 10 deletions python/private/py_executable.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,10 @@ Whether to implicitly create empty `__init__.py` files in the runfiles tree.
These are created in every directory containing Python source code or shared
libraries, and every parent directory of those directories, excluding the repo
root directory. The default, `-1` (auto), means true unless
`--incompatible_default_to_explicit_init_py` is used. If false, the user is
responsible for creating (possibly empty) `__init__.py` files and adding them to
the `srcs` of Python targets as required.
`--incompatible_default_to_explicit_init_py` or the `use_explicit_init_py`
module configuration option are used. If false, the user is responsible for
creating (possibly empty) `__init__.py` files and adding them to the `srcs` of
Python targets as required.
""",
),
# TODO(b/203567235): In the Java impl, any file is allowed. While marked
Expand Down Expand Up @@ -284,11 +285,23 @@ def create_binary_semantics():
)

def _should_create_init_files(ctx):
if ctx.attr.legacy_create_init == -1:
return not read_possibly_native_flag(ctx, "default_to_explicit_init_py")
else:
# Each target has the first say in this setting.
if ctx.attr.legacy_create_init != -1:
return bool(ctx.attr.legacy_create_init)

# Check if it's configured by a module extension.
canonical_name = ctx.label.repo_name
for sep in ("+", "~"):
if canonical_name.startswith(sep):
canonical_name = ""
module_name = canonical_name.rstrip(sep) if sep not in canonical_name else canonical_name.split(sep)[0]
module_configured_explicit_initpy = rp_config.modules_using_explicit_initpy.get(module_name, None)
if module_configured_explicit_initpy != None:
return not module_configured_explicit_initpy

# Fall back to CLI setting.
return not read_possibly_native_flag(ctx, "default_to_explicit_init_py")

def _create_executable(
ctx,
*,
Expand Down Expand Up @@ -1584,11 +1597,15 @@ WARNING: Target {} is using implicit __init__.py creation.
See https://github.com/bazel-contrib/rules_python/issues/2945

Ensure all __init__.py files are explicitly created and
added to the srcs or deps of your targets.
added to the srcs or deps of your targets, then enable this
setting in your MODULE.bazel:

rules_python_config = use_extension("@rules_python//python/extensions:config.bzl", "config")
rules_python_config.use_explicit_init_py(enabled = True)

If this warning is coming from an external module, you can configure this
globally with the following Bazel flag:

Disable implicit creation by setting:
legacy_create_init = 0
on the target, or globally by setting:
--incompatible_default_to_explicit_init_py
======================================================================
""".rstrip().format(ctx.label),
Expand Down
6 changes: 6 additions & 0 deletions tests/explicit_init_py/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility

test_suite(
name = "explicit_init_py",
tests = ["@other//:explicit_init_py_tests"] if BZLMOD_ENABLED else [],
)
26 changes: 26 additions & 0 deletions tests/modules/other/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
load("@rules_python//python:py_binary.bzl", "py_binary")
load("@rules_python//tests/support:explicit_init_py_test.bzl", "explicit_init_py_test")
load("@rules_python//tests/support:py_reconfig.bzl", "py_reconfig_binary")

package(
Expand Down Expand Up @@ -28,3 +29,28 @@ py_binary(
"//nspkg_gamma",
],
)

explicit_init_py_test(
name = "test_module_dep_no_init",
expect_generated_init = False,
main = "external_main.py",
)

explicit_init_py_test(
name = "test_legacy_create_init_override",
expect_generated_init = True,
legacy_create_init = 1,
main = "external_main.py",
)

# These tests are run by @rules_python//tests/explicit_init_py to ensure a
# module's configuration is respected when it's a dependency.
test_suite(
name = "explicit_init_py_tests",
tests = [
":test_legacy_create_init_override",
":test_module_dep_no_init",
"@init_py_test_extension_repo//:test",
"@init_py_test_repo//:test",
],
)
13 changes: 13 additions & 0 deletions tests/modules/other/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,16 @@ module(name = "other")
bazel_dep(name = "rules_python", version = "0")
bazel_dep(name = "bazel_skylib", version = "1.7.1")
bazel_dep(name = "another_module", version = "0")

# Validate behavior of use_explicit_init_py configuration extension when used by
# a module that's a dependency.
rules_python_config = use_extension("@rules_python//python/extensions:config.bzl", "config")
rules_python_config.use_explicit_init_py(enabled = True)

init_py_test_repo = use_repo_rule("//:ext.bzl", "init_py_test_repo")

init_py_test_repo(name = "init_py_test_repo")

other_ext = use_extension("//:ext.bzl", "other_init_py_test_ext")
other_ext.repo(name = "init_py_test_extension_repo")
use_repo(other_ext, "init_py_test_extension_repo")
37 changes: 37 additions & 0 deletions tests/modules/other/ext.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Module extension declared by 'other' module for testing __init__.py generation."""

_BUILD_FILE_CONTENT = """\
load("@rules_python//tests/support:explicit_init_py_test.bzl", "explicit_init_py_test")

explicit_init_py_test(
name = "test",
main = "main.py",
expect_generated_init = False,
)
"""

_MAIN_PY_CONTENT = "print('hello, world')"

def _repo_impl(ctx):
ctx.file("main.py", _MAIN_PY_CONTENT)
ctx.file("BUILD.bazel", _BUILD_FILE_CONTENT)

init_py_test_repo = repository_rule(implementation = _repo_impl)

def _other_init_py_test_ext_impl(module_ctx):
for mod in module_ctx.modules:
for tag in mod.tags.repo:
init_py_test_repo(name = tag.name)

_repo_tag = tag_class(
attrs = {
"name": attr.string(mandatory = True),
},
)

other_init_py_test_ext = module_extension(
implementation = _other_init_py_test_ext_impl,
tag_classes = {
"repo": _repo_tag,
},
)
56 changes: 56 additions & 0 deletions tests/support/explicit_init_py_test.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Parameterized analysis test for __init__.py generation behavior."""

load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
load("//python:py_binary.bzl", "py_binary")
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility

def _explicit_init_py_test_impl(ctx):
env = analysistest.begin(ctx)
target = analysistest.target_under_test(env)
empty_filenames = target[DefaultInfo].default_runfiles.empty_filenames.to_list()
init_pys = [f for f in empty_filenames if f.endswith("__init__.py")]

if ctx.attr.expect_generated_init:
asserts.true(env, len(init_pys) > 0, "Expected __init__.py to be generated")
else:
asserts.true(env, len(init_pys) == 0, "Expected __init__.py to NOT be generated")

return analysistest.end(env)

_explicit_init_py_test = analysistest.make(
_explicit_init_py_test_impl,
attrs = {
"expect_generated_init": attr.bool(mandatory = True),
},
)

def explicit_init_py_test(*, name, main, expect_generated_init, legacy_create_init = -1, **kwargs):
"""Test that verifies whether __init__.py is generated for a py_binary.

Args:
name: Test name.
main: Source file for the py_binary subject.
expect_generated_init: Whether __init__.py generation is expected.
legacy_create_init: Value for the legacy_create_init attribute (-1, 0, or 1).
**kwargs: Additional args forwarded to the test rule (e.g. tags).
"""

if not BZLMOD_ENABLED:
native.test_suite(name = name, tests = [])
return

subject_name = name + "_subject"
py_binary(
name = subject_name,
srcs = [main],
main = main,
legacy_create_init = legacy_create_init,
tags = kwargs.pop("tags", ["manual"]),
**kwargs
)
_explicit_init_py_test(
name = name,
target_under_test = subject_name,
expect_generated_init = expect_generated_init,
**kwargs
)