Skip to content

Commit 611eda8

Browse files
authored
refactor: fold per-target python version into base rules (bazel-contrib#2541)
Today, specifying the Python version for a target requires using the version-aware rules in `transition.bzl` (or the generated equivalents bound to a specific Python version). With the rules rewritten in Bazel, that functionality can be moved into the base rules themselves. Moving the logic into the base rules simplifies the implementation and avoids having to re-implement subtle behaviors in the wrappers to correctly emulate the wrapped target. For backwards compatibility, the symbols in `transition.bzl` are left as aliases to the underlying rules.
1 parent 475a99e commit 611eda8

File tree

4 files changed

+90
-270
lines changed

4 files changed

+90
-270
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ Unreleased changes template.
6262
not using {bzl:obj}`pip.parse.experimental_index_url_overrides`.
6363
* ({bzl:obj}`pip.parse`) Only query SimpleAPI for packages that have
6464
sha values in the `requirements.txt` file.
65+
* (rules) The version-aware rules have been folded into the base rules and
66+
the version-aware rules are now simply aliases for the base rules. The
67+
`python_version` attribute is still used to specify the Python version.
68+
69+
{#v0-0-0-deprecations}
70+
#### Deprecations
71+
* `//python/config_settings:transitions.bzl` and its `py_binary` and `py_test`
72+
wrappers are deprecated. Use the regular rules instead.
6573

6674
{#v0-0-0-fixed}
6775
### Fixed

python/config_settings/transition.bzl

Lines changed: 34 additions & 257 deletions
Original file line numberDiff line numberDiff line change
@@ -14,266 +14,43 @@
1414

1515
"""The transition module contains the rule definitions to wrap py_binary and py_test and transition
1616
them to the desired target platform.
17+
18+
:::{versionchanged} VERSION_NEXT_PATCH
19+
The `py_binary` and `py_test` symbols are aliases to the regular rules. Usages
20+
of them should be changed to load the regular rules directly.
21+
:::
1722
"""
1823

19-
load("@bazel_skylib//lib:dicts.bzl", "dicts")
2024
load("//python:py_binary.bzl", _py_binary = "py_binary")
21-
load("//python:py_info.bzl", "PyInfo")
22-
load("//python:py_runtime_info.bzl", "PyRuntimeInfo")
2325
load("//python:py_test.bzl", _py_test = "py_test")
24-
load("//python/config_settings/private:py_args.bzl", "py_args")
25-
load("//python/private:reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo")
26-
27-
def _transition_python_version_impl(_, attr):
28-
return {"//python/config_settings:python_version": str(attr.python_version)}
29-
30-
_transition_python_version = transition(
31-
implementation = _transition_python_version_impl,
32-
inputs = [],
33-
outputs = ["//python/config_settings:python_version"],
34-
)
35-
36-
def _transition_py_impl(ctx):
37-
target = ctx.attr.target
38-
windows_constraint = ctx.attr._windows_constraint[platform_common.ConstraintValueInfo]
39-
target_is_windows = ctx.target_platform_has_constraint(windows_constraint)
40-
executable = ctx.actions.declare_file(ctx.attr.name + (".exe" if target_is_windows else ""))
41-
ctx.actions.symlink(
42-
is_executable = True,
43-
output = executable,
44-
target_file = target[DefaultInfo].files_to_run.executable,
45-
)
46-
default_outputs = []
47-
if target_is_windows:
48-
# NOTE: Bazel 6 + host=linux + target=windows results in the .exe extension missing
49-
inner_bootstrap_path = _strip_suffix(target[DefaultInfo].files_to_run.executable.short_path, ".exe")
50-
inner_bootstrap = None
51-
inner_zip_file_path = inner_bootstrap_path + ".zip"
52-
inner_zip_file = None
53-
for file in target[DefaultInfo].files.to_list():
54-
if file.short_path == inner_bootstrap_path:
55-
inner_bootstrap = file
56-
elif file.short_path == inner_zip_file_path:
57-
inner_zip_file = file
58-
59-
# TODO: Use `fragments.py.build_python_zip` once Bazel 6 support is dropped.
60-
# Which file the Windows .exe looks for depends on the --build_python_zip file.
61-
# Bazel 7+ has APIs to know the effective value of that flag, but not Bazel 6.
62-
# To work around this, we treat the existence of a .zip in the default outputs
63-
# to mean --build_python_zip=true.
64-
if inner_zip_file:
65-
suffix = ".zip"
66-
underlying_launched_file = inner_zip_file
67-
else:
68-
suffix = ""
69-
underlying_launched_file = inner_bootstrap
70-
71-
if underlying_launched_file:
72-
launched_file_symlink = ctx.actions.declare_file(ctx.attr.name + suffix)
73-
ctx.actions.symlink(
74-
is_executable = True,
75-
output = launched_file_symlink,
76-
target_file = underlying_launched_file,
77-
)
78-
default_outputs.append(launched_file_symlink)
79-
80-
env = {}
81-
for k, v in ctx.attr.env.items():
82-
env[k] = ctx.expand_location(v)
83-
84-
providers = [
85-
DefaultInfo(
86-
executable = executable,
87-
files = depset(default_outputs, transitive = [target[DefaultInfo].files]),
88-
runfiles = ctx.runfiles(default_outputs).merge(target[DefaultInfo].default_runfiles),
89-
),
90-
# Ensure that the binary we're wrapping is included in code coverage.
91-
coverage_common.instrumented_files_info(
92-
ctx,
93-
dependency_attributes = ["target"],
94-
),
95-
target[OutputGroupInfo],
96-
# TODO(f0rmiga): testing.TestEnvironment is deprecated in favour of RunEnvironmentInfo but
97-
# RunEnvironmentInfo is not exposed in Bazel < 5.3.
98-
# https://github.com/bazelbuild/rules_python/issues/901
99-
# https://github.com/bazelbuild/bazel/commit/dbdfa07e92f99497be9c14265611ad2920161483
100-
testing.TestEnvironment(env),
101-
]
102-
if PyInfo in target:
103-
providers.append(target[PyInfo])
104-
if BuiltinPyInfo != None and BuiltinPyInfo in target and PyInfo != BuiltinPyInfo:
105-
providers.append(target[BuiltinPyInfo])
106-
107-
if PyRuntimeInfo in target:
108-
providers.append(target[PyRuntimeInfo])
109-
if BuiltinPyRuntimeInfo != None and BuiltinPyRuntimeInfo in target and PyRuntimeInfo != BuiltinPyRuntimeInfo:
110-
providers.append(target[BuiltinPyRuntimeInfo])
111-
return providers
112-
113-
_COMMON_ATTRS = {
114-
"deps": attr.label_list(
115-
mandatory = False,
116-
),
117-
"env": attr.string_dict(
118-
mandatory = False,
119-
),
120-
"python_version": attr.string(
121-
mandatory = True,
122-
),
123-
"srcs": attr.label_list(
124-
allow_files = True,
125-
mandatory = False,
126-
),
127-
"target": attr.label(
128-
executable = True,
129-
cfg = "target",
130-
mandatory = True,
131-
providers = [PyInfo],
132-
),
133-
# "tools" is a hack here. It should be "data" but "data" is not included by default in the
134-
# location expansion in the same way it is in the native Python rules. The difference on how
135-
# the Bazel deals with those special attributes differ on the LocationExpander, e.g.:
136-
# https://github.com/bazelbuild/bazel/blob/ce611646/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java#L415-L429
137-
#
138-
# Since the default LocationExpander used by ctx.expand_location is not the same as the native
139-
# rules (it doesn't set "allowDataAttributeEntriesInLabel"), we use "tools" temporarily while a
140-
# proper fix in Bazel happens.
141-
#
142-
# A fix for this was proposed in https://github.com/bazelbuild/bazel/pull/16381.
143-
"tools": attr.label_list(
144-
allow_files = True,
145-
mandatory = False,
146-
),
147-
# Required to Opt-in to the transitions feature.
148-
"_allowlist_function_transition": attr.label(
149-
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
150-
),
151-
"_windows_constraint": attr.label(
152-
default = "@platforms//os:windows",
153-
),
154-
}
155-
156-
_PY_TEST_ATTRS = {
157-
# Magic attribute to help C++ coverage work. There's no
158-
# docs about this; see TestActionBuilder.java
159-
"_collect_cc_coverage": attr.label(
160-
default = "@bazel_tools//tools/test:collect_cc_coverage",
161-
executable = True,
162-
cfg = "exec",
163-
),
164-
# Magic attribute to make coverage work. There's no
165-
# docs about this; see TestActionBuilder.java
166-
"_lcov_merger": attr.label(
167-
default = configuration_field(fragment = "coverage", name = "output_generator"),
168-
executable = True,
169-
cfg = "exec",
170-
),
171-
}
17226

173-
_transition_py_binary = rule(
174-
_transition_py_impl,
175-
attrs = _COMMON_ATTRS | _PY_TEST_ATTRS,
176-
cfg = _transition_python_version,
177-
executable = True,
178-
fragments = ["py"],
179-
)
180-
181-
_transition_py_test = rule(
182-
_transition_py_impl,
183-
attrs = _COMMON_ATTRS | _PY_TEST_ATTRS,
184-
cfg = _transition_python_version,
185-
test = True,
186-
fragments = ["py"],
187-
)
188-
189-
def _py_rule(rule_impl, transition_rule, name, python_version, **kwargs):
190-
pyargs = py_args(name, kwargs)
191-
args = pyargs["args"]
192-
data = pyargs["data"]
193-
env = pyargs["env"]
194-
srcs = pyargs["srcs"]
195-
deps = pyargs["deps"]
196-
main = pyargs["main"]
197-
198-
# Attributes common to all build rules.
199-
# https://bazel.build/reference/be/common-definitions#common-attributes
200-
compatible_with = kwargs.pop("compatible_with", None)
201-
deprecation = kwargs.pop("deprecation", None)
202-
exec_compatible_with = kwargs.pop("exec_compatible_with", None)
203-
exec_properties = kwargs.pop("exec_properties", None)
204-
features = kwargs.pop("features", None)
205-
restricted_to = kwargs.pop("restricted_to", None)
206-
tags = kwargs.pop("tags", None)
207-
target_compatible_with = kwargs.pop("target_compatible_with", None)
208-
testonly = kwargs.pop("testonly", None)
209-
toolchains = kwargs.pop("toolchains", None)
210-
visibility = kwargs.pop("visibility", None)
211-
212-
common_attrs = {
213-
"compatible_with": compatible_with,
214-
"deprecation": deprecation,
215-
"exec_compatible_with": exec_compatible_with,
216-
"exec_properties": exec_properties,
217-
"features": features,
218-
"restricted_to": restricted_to,
219-
"target_compatible_with": target_compatible_with,
220-
"testonly": testonly,
221-
"toolchains": toolchains,
222-
}
223-
224-
# Test-specific extra attributes.
225-
if "env_inherit" in kwargs:
226-
common_attrs["env_inherit"] = kwargs.pop("env_inherit")
227-
if "size" in kwargs:
228-
common_attrs["size"] = kwargs.pop("size")
229-
if "timeout" in kwargs:
230-
common_attrs["timeout"] = kwargs.pop("timeout")
231-
if "flaky" in kwargs:
232-
common_attrs["flaky"] = kwargs.pop("flaky")
233-
if "shard_count" in kwargs:
234-
common_attrs["shard_count"] = kwargs.pop("shard_count")
235-
if "local" in kwargs:
236-
common_attrs["local"] = kwargs.pop("local")
237-
238-
# Binary-specific extra attributes.
239-
if "output_licenses" in kwargs:
240-
common_attrs["output_licenses"] = kwargs.pop("output_licenses")
241-
242-
rule_impl(
243-
name = "_" + name,
244-
args = args,
245-
data = data,
246-
deps = deps,
247-
env = env,
248-
srcs = srcs,
249-
main = main,
250-
tags = ["manual"] + (tags if tags else []),
251-
visibility = ["//visibility:private"],
252-
**dicts.add(common_attrs, kwargs)
253-
)
254-
255-
return transition_rule(
256-
name = name,
257-
args = args,
258-
deps = deps,
259-
env = env,
260-
python_version = python_version,
261-
srcs = srcs,
262-
tags = tags,
263-
target = ":_" + name,
264-
tools = data,
265-
visibility = visibility,
266-
**common_attrs
267-
)
268-
269-
def py_binary(name, python_version, **kwargs):
270-
return _py_rule(_py_binary, _transition_py_binary, name, python_version, **kwargs)
271-
272-
def py_test(name, python_version, **kwargs):
273-
return _py_rule(_py_test, _transition_py_test, name, python_version, **kwargs)
27+
_DEPRECATION_MESSAGE = """
28+
The {name} symbol in @rules_python//python/config_settings:transition.bzl
29+
is deprecated. It is an alias to the regular rule; use it directly instead:
30+
load("@rules_python//python:{name}.bzl", "{name}")
31+
"""
27432

275-
def _strip_suffix(s, suffix):
276-
if s.endswith(suffix):
277-
return s[:-len(suffix)]
278-
else:
279-
return s
33+
def py_binary(**kwargs):
34+
"""[DEPRECATED] Deprecated alias for py_binary.
35+
36+
Args:
37+
**kwargs: keyword args forwarded onto {obj}`py_binary`.
38+
"""
39+
40+
deprecation = _DEPRECATION_MESSAGE.format(name = "py_binary")
41+
if kwargs.get("deprecation"):
42+
deprecation = kwargs.get("deprecation") + "\n\n" + deprecation
43+
kwargs["deprecation"] = deprecation
44+
_py_binary(**kwargs)
45+
46+
def py_test(**kwargs):
47+
"""[DEPRECATED] Deprecated alias for py_test.
48+
49+
Args:
50+
**kwargs: keyword args forwarded onto {obj}`py_binary`.
51+
"""
52+
deprecation = _DEPRECATION_MESSAGE.format(name = "py_test")
53+
if kwargs.get("deprecation"):
54+
deprecation = kwargs.get("deprecation") + "\n\n" + deprecation
55+
kwargs["deprecation"] = deprecation
56+
_py_test(**kwargs)

0 commit comments

Comments
 (0)