Skip to content

Commit c2ebcd3

Browse files
feat(bzlmod): make __init__.py generation configurable module-wide (#3997)
In #3841, a warning pushing users to migrate away from implicit `__init__.py` generation was added. While it's good to flag this bad behavior, silencing it requires users to either explicitly configure this option on every `py_binary` and `py_test` target, or configure the option globally in their `.bazelrc`. To better facilitate a migration, this change introduces a mechanism for modules to configure this option module-wide. This has multiple benefits: 1. Everyone working in the module doesn't need to remember to explicitly set `legacy_create_init` on every target. 2. Everyone that depends on the module receives the correct behavior as configured by the module. 3. It becomes possible to tell BCR-wide which modules have adopted this migration. Work towards #2945 --------- Co-authored-by: Richard Levasseur <richardlev@gmail.com>
1 parent 353b24e commit c2ebcd3

11 files changed

Lines changed: 225 additions & 10 deletions

File tree

examples/bzlmod/MODULE.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ bazel_dep(name = "rules_java", version = "8.16.1")
2222
# were fixed.
2323
bazel_dep(name = "rules_rust", version = "0.67.0")
2424

25+
# Adopt migration away from legacy __init__.py generation.
26+
rules_python_config = use_extension("@rules_python//python/extensions:config.bzl", "config")
27+
rules_python_config.explicit_init_py(default = True)
28+
2529
# We next initialize the python toolchain using the extension.
2630
# You can set different Python versions in this block.
2731
python = use_extension("@rules_python//python/extensions:python.bzl", "python")

examples/bzlmod/other_module/MODULE.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ local_path_override(
1010
path = "../../..",
1111
)
1212

13+
# Adopt migration away from legacy __init__.py generation.
14+
rules_python_config = use_extension("@rules_python//python/extensions:config.bzl", "config")
15+
rules_python_config.explicit_init_py(default = True)
16+
1317
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
1418
python.defaults(
1519
# In a submodule this is ignored

news/3997.added.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(bzlmod) Added the `{obj}`explicit_init_py`` tag class to the
2+
`{obj}`config`` module extension for configuring implicit `__init__.py` file
3+
generation module-wide.
4+
([#3997](https://github.com/bazel-contrib/rules_python/pull/3997),
5+
[#2945](https://github.com/bazel-contrib/rules_python/issues/2945))

python/extensions/config.bzl

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,60 @@ to repositories that are expensive to create or invalidate frequently.
2222
},
2323
)
2424

25+
_explicit_init_py = tag_class(
26+
doc = """
27+
Require explicit `__init__.py` files *in this module*.
28+
29+
Disables the legacy `__init__.py` generation for all `py_*` targets in this
30+
module, requiring all Python targets to explicitly provide `__init__.py` files
31+
when they're needed.
32+
33+
To override this at a per-target level, set `legacy_create_init` on applicable
34+
`py_binary` or `py_test` targets:
35+
36+
```starlark
37+
py_binary(
38+
name = "hello_python",
39+
# ...
40+
# This Binary still relies on legacy behavior, so
41+
# enable the legacy behavior as an exceptional case.
42+
legacy_create_init = 1,
43+
)
44+
```
45+
46+
:::{note}
47+
In the future, this will be enabled by default.
48+
:::
49+
50+
:::{versionadded} VERSION_NEXT_FEATURE
51+
:::
52+
""",
53+
attrs = {
54+
"default": attr.bool(doc = "Whether explicit __init__.py files are required by default.", mandatory = True),
55+
},
56+
)
57+
2558
def _config_impl(module_ctx):
2659
transition_setting_generators = {}
2760
transition_settings = []
61+
explicit_init_py_modules = {}
2862
for mod in module_ctx.modules:
2963
for tag in mod.tags.add_transition_setting:
3064
setting = str(tag.setting)
3165
if setting not in transition_setting_generators:
3266
transition_setting_generators[setting] = []
3367
transition_settings.append(setting)
3468
transition_setting_generators[setting].append(mod.name)
69+
for tag in mod.tags.explicit_init_py:
70+
explicit_init_py_modules[mod.name] = str(tag.default)
71+
if mod.is_root:
72+
explicit_init_py_modules[""] = str(tag.default)
3573

3674
internal_config_repo(
3775
name = "rules_python_internal",
3876
transition_setting_generators = transition_setting_generators,
3977
transition_settings = transition_settings,
78+
explicit_init_py_modules = explicit_init_py_modules,
4079
)
4180

4281
pypi_deps()
@@ -55,5 +94,6 @@ config = module_extension(
5594
implementation = _config_impl,
5695
tag_classes = {
5796
"add_transition_setting": _add_transition_setting,
97+
"explicit_init_py": _explicit_init_py,
5898
},
5999
)

python/private/internal_config_repo.bzl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ config = struct(
3737
BuiltinPyInfo = getattr(getattr(native, "legacy_globals", None), "PyInfo", {builtin_py_info_symbol}),
3838
BuiltinPyRuntimeInfo = getattr(getattr(native, "legacy_globals", None), "PyRuntimeInfo", {builtin_py_runtime_info_symbol}),
3939
BuiltinPyCcLinkParamsProvider = getattr(getattr(native, "legacy_globals", None), "PyCcLinkParamsProvider", {builtin_py_cc_link_params_provider}),
40+
modules_using_explicit_initpy = {modules_using_explicit_initpy},
4041
)
4142
"""
4243

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

104106
rctx.file("rules_python_config.bzl", _CONFIG_TEMPLATE.format(
105107
build_python_zip_default = repo_utils.get_platforms_os_name(rctx) == "windows",
@@ -109,6 +111,7 @@ def _internal_config_repo_impl(rctx):
109111
supports_whl_extraction = str(supports_whl_extraction),
110112
extract_needs_chmod = str(extract_needs_chmod),
111113
builtin_py_cc_link_params_provider = builtin_py_cc_link_params_provider,
114+
modules_using_explicit_initpy = str(explicit_init_py_modules),
112115
bazel_8_or_later = str(bazel_major_version >= 8),
113116
bazel_9_or_later = str(bazel_major_version >= 9),
114117
bazel_10_or_later = str(bazel_major_version > 9),
@@ -140,6 +143,7 @@ internal_config_repo = repository_rule(
140143
configure = True,
141144
environ = [],
142145
attrs = {
146+
"explicit_init_py_modules": attr.string_dict(),
143147
"transition_setting_generators": attr.string_list_dict(),
144148
"transition_settings": attr.string_list(),
145149
},

python/private/py_executable.bzl

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,15 +115,20 @@ The {any}`RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` environment variable
115115
"legacy_create_init": lambda: attrb.Int(
116116
default = -1,
117117
values = [-1, 0, 1],
118-
doc = """\
118+
doc = """
119119
Whether to implicitly create empty `__init__.py` files in the runfiles tree.
120120
These are created in every directory containing Python source code or shared
121121
libraries, and every parent directory of those directories, excluding the repo
122122
root directory. The default, `-1` (auto), means true unless
123-
`--incompatible_default_to_explicit_init_py` is used. If false, the user is
124-
responsible for creating (possibly empty) `__init__.py` files and adding them to
125-
the `srcs` of Python targets as required.
126-
""",
123+
`--incompatible_default_to_explicit_init_py` or the `explicit_init_py`
124+
module configuration option are used. If false, the user is responsible for
125+
creating (possibly empty) `__init__.py` files and adding them to the `srcs` of
126+
Python targets as required.
127+
128+
:::{versionchanged} VERSION_NEXT_FEATURE
129+
Now checks module-level `explicit_init_py` configuration before CLI flags.
130+
:::
131+
""",
127132
),
128133
# TODO(b/203567235): In the Java impl, any file is allowed. While marked
129134
# label, it is more treated as a string, and doesn't have to refer to
@@ -284,11 +289,23 @@ def create_binary_semantics():
284289
)
285290

286291
def _should_create_init_files(ctx):
287-
if ctx.attr.legacy_create_init == -1:
288-
return not read_possibly_native_flag(ctx, "default_to_explicit_init_py")
289-
else:
292+
# Each target has the first say in this setting.
293+
if ctx.attr.legacy_create_init != -1:
290294
return bool(ctx.attr.legacy_create_init)
291295

296+
# Check if it's configured by a module extension.
297+
canonical_name = ctx.label.repo_name
298+
for sep in ("+", "~"):
299+
if canonical_name.startswith(sep):
300+
canonical_name = ""
301+
module_name = canonical_name.rstrip(sep) if sep not in canonical_name else canonical_name.split(sep)[0]
302+
module_configured_explicit_initpy = rp_config.modules_using_explicit_initpy.get(module_name, None)
303+
if module_configured_explicit_initpy != None:
304+
return not module_configured_explicit_initpy
305+
306+
# Fall back to CLI setting.
307+
return not read_possibly_native_flag(ctx, "default_to_explicit_init_py")
308+
292309
def _create_executable(
293310
ctx,
294311
*,
@@ -1586,9 +1603,17 @@ WARNING: Target {} is using implicit __init__.py creation.
15861603
Ensure all __init__.py files are explicitly created and
15871604
added to the srcs or deps of your targets.
15881605
1589-
Disable implicit creation by setting:
1606+
Disable implicit creation for your module in MODULE.bazel:
1607+
1608+
rules_python_config = use_extension("@rules_python//python/extensions:config.bzl", "config")
1609+
rules_python_config.explicit_init_py(default = True)
1610+
1611+
Or for a specific target by setting:
1612+
15901613
legacy_create_init = 0
1591-
on the target, or globally by setting:
1614+
1615+
Or globally with the following Bazel flag:
1616+
15921617
--incompatible_default_to_explicit_init_py
15931618
======================================================================
15941619
""".rstrip().format(ctx.label),

tests/explicit_init_py/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility
2+
3+
test_suite(
4+
name = "explicit_init_py",
5+
tests = ["@other//:explicit_init_py_tests"] if BZLMOD_ENABLED else [],
6+
)

tests/modules/other/BUILD.bazel

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
load("@rules_python//python:py_binary.bzl", "py_binary")
2+
load("@rules_python//tests/support:explicit_init_py_test.bzl", "explicit_init_py_test")
23
load("@rules_python//tests/support:py_reconfig.bzl", "py_reconfig_binary")
34

45
package(
@@ -28,3 +29,28 @@ py_binary(
2829
"//nspkg_gamma",
2930
],
3031
)
32+
33+
explicit_init_py_test(
34+
name = "test_module_dep_no_init",
35+
expect_generated_init = False,
36+
main = "external_main.py",
37+
)
38+
39+
explicit_init_py_test(
40+
name = "test_legacy_create_init_override",
41+
expect_generated_init = True,
42+
legacy_create_init = 1,
43+
main = "external_main.py",
44+
)
45+
46+
# These tests are run by @rules_python//tests/explicit_init_py to ensure a
47+
# module's configuration is respected when it's a dependency.
48+
test_suite(
49+
name = "explicit_init_py_tests",
50+
tests = [
51+
":test_legacy_create_init_override",
52+
":test_module_dep_no_init",
53+
"@init_py_test_extension_repo//:test",
54+
"@init_py_test_repo//:test",
55+
],
56+
)

tests/modules/other/MODULE.bazel

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,18 @@ module(name = "other")
22

33
bazel_dep(name = "rules_python", version = "0")
44
bazel_dep(name = "bazel_skylib", version = "1.7.1")
5+
bazel_dep(name = "rules_testing", version = "0.6.0")
56
bazel_dep(name = "another_module", version = "0")
7+
8+
# Validate behavior of explicit_init_py configuration extension when used by
9+
# a module that's a dependency.
10+
rules_python_config = use_extension("@rules_python//python/extensions:config.bzl", "config")
11+
rules_python_config.explicit_init_py(default = True)
12+
13+
init_py_test_repo = use_repo_rule("//:ext.bzl", "init_py_test_repo")
14+
15+
init_py_test_repo(name = "init_py_test_repo")
16+
17+
other_ext = use_extension("//:ext.bzl", "other_init_py_test_ext")
18+
other_ext.repo(name = "init_py_test_extension_repo")
19+
use_repo(other_ext, "init_py_test_extension_repo")

tests/modules/other/ext.bzl

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Module extension declared by 'other' module for testing __init__.py generation."""
2+
3+
_BUILD_FILE_CONTENT = """\
4+
load("@rules_python//tests/support:explicit_init_py_test.bzl", "explicit_init_py_test")
5+
6+
explicit_init_py_test(
7+
name = "test",
8+
main = "main.py",
9+
expect_generated_init = False,
10+
)
11+
"""
12+
13+
_MAIN_PY_CONTENT = "print('hello, world')"
14+
15+
def _repo_impl(rctx):
16+
rctx.file("main.py", _MAIN_PY_CONTENT)
17+
rctx.file("BUILD.bazel", _BUILD_FILE_CONTENT)
18+
19+
init_py_test_repo = repository_rule(implementation = _repo_impl)
20+
21+
def _other_init_py_test_ext_impl(module_ctx):
22+
for mod in module_ctx.modules:
23+
for tag in mod.tags.repo:
24+
init_py_test_repo(name = tag.name)
25+
26+
_repo_tag = tag_class(
27+
attrs = {
28+
"name": attr.string(mandatory = True),
29+
},
30+
)
31+
32+
other_init_py_test_ext = module_extension(
33+
implementation = _other_init_py_test_ext_impl,
34+
tag_classes = {
35+
"repo": _repo_tag,
36+
},
37+
)

0 commit comments

Comments
 (0)