Skip to content

Commit 1a517c0

Browse files
committed
fix(py): reset Python flags on data edges
Data dependencies are runtime artifacts shared by callers in different Python environments. A terminal incoming transition carries its python_version and dep_group overrides into data, so each environment reanalyzes the same runtime subtree. Remember the exact settings inherited before terminal overrides and reset data edges to that baseline. Put Python-sensitive artifacts in deps so they retain the terminal configuration. Keep the transition surface limited to attr-driven overrides; global-only interpreter feature settings remain inherited without copy-through. An analysis test counts configured probe outputs across real data edges, both override dimensions, and nested terminals.
1 parent 70e8137 commit 1a517c0

11 files changed

Lines changed: 319 additions & 10 deletions

File tree

py/private/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ bzl_library(
7272
":py_info",
7373
":py_semantics",
7474
":py_wheel",
75+
":transitions",
7576
"@bazel_skylib//lib:new_sets",
7677
"@bazel_skylib//lib:types",
7778
],

py/private/interpreter/BUILD.bazel

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@ string_flag(
2828
build_setting_default = "",
2929
)
3030

31+
# Internal transition scratch state. Python terminals capture the version
32+
# settings they inherited before applying their own python_version attr, then
33+
# data edges use them to return runtime files to the caller's configuration.
34+
string_flag(
35+
name = "baseline_python_version",
36+
build_setting_default = "<unset>",
37+
visibility = ["//py:__subpackages__"],
38+
)
39+
40+
string_flag(
41+
name = "baseline_rules_python_version",
42+
build_setting_default = "<unset>",
43+
visibility = ["//py:__subpackages__"],
44+
)
45+
3146
# Interpreter feature exclusions. Pass one or more --exclude_feature flags to
3247
# strip optional components from interpreter filegroups. Supported values:
3348
# headers, docs, tkinter, idle, ensurepip, config, pydoc, lib2to3, turtle

py/private/py_library.bzl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ load("@rules_cc//cc/common:cc_info.bzl", "CcInfo")
99
load("//py/private:providers.bzl", "PyVirtualInfo", "PyWheelsInfo")
1010
load("//py/private:pth.bzl", "make_imports_depset")
1111
load("//py/private:py_info.bzl", "PyInfo")
12+
load("//py/private:transitions.bzl", "reset_python_flags_transition")
1213

1314
def _make_instrumented_files_info(ctx):
1415
return coverage_common.instrumented_files_info(
@@ -190,8 +191,11 @@ _attrs = dict({
190191
The transitive closure of the `data` dependencies will be available in the `.runfiles`
191192
folder for this binary/test. The program may optionally use the Runfiles lookup library to
192193
locate the data files, see https://pypi.org/project/bazel-runfiles/.
194+
Data is analyzed in the inherited caller configuration. Put artifacts
195+
that must match the terminal's Python environment in `deps`.
193196
""",
194197
allow_files = True,
198+
cfg = reset_python_flags_transition,
195199
),
196200
"imports": attr.string_list(
197201
doc = "List of import directories to be added to the PYTHONPATH.",

py/private/py_venv/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ bzl_library(
5555
"//py/private:py_info",
5656
"//py/private:py_library",
5757
"//py/private:py_semantics",
58+
"//py/private:transitions",
5859
"@bazel_lib//lib:expand_make_vars",
5960
"@bazel_lib//lib:paths",
6061
"@hermetic_launcher//launcher:lib_bzl",

py/private/py_venv/py_venv_exec.bzl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ load("@bazel_lib//lib:expand_make_vars.bzl", "expand_locations", "expand_variabl
1010
load("@hermetic_launcher//launcher:lib.bzl", "launcher")
1111
load("//py/private:py_info.bzl", "PyInfo")
1212
load("//py/private:py_semantics.bzl", _py_semantics = "semantics")
13+
load("//py/private:transitions.bzl", "reset_python_flags_transition")
1314
load(":types.bzl", "VirtualenvInfo", "venv_root")
1415

1516
# Identifiers the launcher always sets to the analysing rule's contextual
@@ -212,8 +213,11 @@ The transitive closure of the `data` dependencies will be available in
212213
the `.runfiles` folder for this binary/test. The program may optionally
213214
use the Runfiles lookup library to locate the data files, see
214215
https://pypi.org/project/bazel-runfiles/.
216+
Data is analyzed in the inherited caller configuration. Put artifacts
217+
that must match the terminal's Python environment in `deps`.
215218
""",
216219
allow_files = True,
220+
cfg = reset_python_flags_transition,
217221
),
218222
# Forwarded to the sibling py_venv (which is where srcs actually
219223
# feed sys.path). Carried on the launcher only so Bazel's `args`
@@ -224,6 +228,9 @@ https://pypi.org/project/bazel-runfiles/.
224228
doc = "Python source files. Forwarded to the sibling py_venv.",
225229
allow_files = [".py"],
226230
),
231+
"_allowlist_function_transition": attr.label(
232+
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
233+
),
227234
})
228235

229236
_test_attrs = dict({

py/private/transitions.bzl

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,43 @@
11
"""Common transition implementation used by the various terminals."""
22

33
DEP_GROUP_FLAG = "@aspect_rules_py//uv/private/constraints/dep_group:dep_group"
4+
_DEP_GROUP_BASELINE_FLAG = "@aspect_rules_py//uv/private/constraints/dep_group:baseline"
45

56
# Our own python_version flag, replacing the rules_python one.
67
PYTHON_VERSION_FLAG = "@aspect_rules_py//py/private/interpreter:python_version"
8+
_PYTHON_VERSION_BASELINE_FLAG = "@aspect_rules_py//py/private/interpreter:baseline_python_version"
79

810
# rules_python's flag, kept for backward compatibility during migration.
911
_RPY_VERSION_FLAG = "@rules_python//python/config_settings:python_version"
12+
_RPY_VERSION_BASELINE_FLAG = "@aspect_rules_py//py/private/interpreter:baseline_rules_python_version"
1013

11-
# Interpreter feature flags that must be propagated through transitions.
12-
_FREETHREADED_FLAG = "@aspect_rules_py//py/private/interpreter:freethreaded"
14+
_BASELINE_UNSET = "<unset>"
15+
16+
def _baseline(settings, flag, current):
17+
baseline = settings[flag]
18+
if baseline == _BASELINE_UNSET:
19+
return current
20+
return baseline
21+
22+
def _python_version(settings):
23+
return settings[PYTHON_VERSION_FLAG] or settings[_RPY_VERSION_FLAG]
1324

1425
def _python_transition_impl(settings, attr):
1526
acc = {}
27+
acc[_PYTHON_VERSION_BASELINE_FLAG] = _baseline(
28+
settings,
29+
_PYTHON_VERSION_BASELINE_FLAG,
30+
settings[PYTHON_VERSION_FLAG],
31+
)
32+
acc[_RPY_VERSION_BASELINE_FLAG] = _baseline(
33+
settings,
34+
_RPY_VERSION_BASELINE_FLAG,
35+
settings[_RPY_VERSION_FLAG],
36+
)
1637
if attr.python_version:
1738
version = str(attr.python_version)
1839
else:
19-
version = settings[PYTHON_VERSION_FLAG] or settings[_RPY_VERSION_FLAG]
40+
version = _python_version(settings)
2041

2142
acc[PYTHON_VERSION_FLAG] = version
2243
acc[_RPY_VERSION_FLAG] = version
@@ -28,11 +49,14 @@ def _python_transition_impl(settings, attr):
2849
dep_group = getattr(attr, "dep_group", None)
2950
if dep_group:
3051
acc[DEP_GROUP_FLAG] = str(dep_group)
52+
acc[_DEP_GROUP_BASELINE_FLAG] = _baseline(
53+
settings,
54+
_DEP_GROUP_BASELINE_FLAG,
55+
settings[DEP_GROUP_FLAG],
56+
)
3157
else:
3258
acc[DEP_GROUP_FLAG] = settings[DEP_GROUP_FLAG]
33-
34-
# Propagate interpreter feature flags
35-
acc[_FREETHREADED_FLAG] = settings[_FREETHREADED_FLAG]
59+
acc[_DEP_GROUP_BASELINE_FLAG] = settings[_DEP_GROUP_BASELINE_FLAG]
3660

3761
return acc
3862

@@ -41,13 +65,63 @@ python_transition = transition(
4165
inputs = [
4266
PYTHON_VERSION_FLAG,
4367
_RPY_VERSION_FLAG,
68+
_PYTHON_VERSION_BASELINE_FLAG,
69+
_RPY_VERSION_BASELINE_FLAG,
70+
DEP_GROUP_FLAG,
71+
_DEP_GROUP_BASELINE_FLAG,
72+
],
73+
outputs = [
74+
PYTHON_VERSION_FLAG,
75+
_RPY_VERSION_FLAG,
76+
_PYTHON_VERSION_BASELINE_FLAG,
77+
_RPY_VERSION_BASELINE_FLAG,
78+
DEP_GROUP_FLAG,
79+
_DEP_GROUP_BASELINE_FLAG,
80+
],
81+
)
82+
83+
# Runtime data is outside the Python environment selected by terminal attrs.
84+
# Return every setting those attrs can override to its inherited value, then
85+
# clear the scratch state so data targets share the caller's canonical
86+
# configuration.
87+
def _reset_python_flags_transition_impl(settings, _attr):
88+
return {
89+
PYTHON_VERSION_FLAG: _baseline(
90+
settings,
91+
_PYTHON_VERSION_BASELINE_FLAG,
92+
settings[PYTHON_VERSION_FLAG],
93+
),
94+
_RPY_VERSION_FLAG: _baseline(
95+
settings,
96+
_RPY_VERSION_BASELINE_FLAG,
97+
settings[_RPY_VERSION_FLAG],
98+
),
99+
_PYTHON_VERSION_BASELINE_FLAG: _BASELINE_UNSET,
100+
_RPY_VERSION_BASELINE_FLAG: _BASELINE_UNSET,
101+
DEP_GROUP_FLAG: _baseline(
102+
settings,
103+
_DEP_GROUP_BASELINE_FLAG,
104+
settings[DEP_GROUP_FLAG],
105+
),
106+
_DEP_GROUP_BASELINE_FLAG: _BASELINE_UNSET,
107+
}
108+
109+
reset_python_flags_transition = transition(
110+
implementation = _reset_python_flags_transition_impl,
111+
inputs = [
112+
PYTHON_VERSION_FLAG,
113+
_RPY_VERSION_FLAG,
114+
_PYTHON_VERSION_BASELINE_FLAG,
115+
_RPY_VERSION_BASELINE_FLAG,
44116
DEP_GROUP_FLAG,
45-
_FREETHREADED_FLAG,
117+
_DEP_GROUP_BASELINE_FLAG,
46118
],
47119
outputs = [
48120
PYTHON_VERSION_FLAG,
49121
_RPY_VERSION_FLAG,
122+
_PYTHON_VERSION_BASELINE_FLAG,
123+
_RPY_VERSION_BASELINE_FLAG,
50124
DEP_GROUP_FLAG,
51-
_FREETHREADED_FLAG,
125+
_DEP_GROUP_BASELINE_FLAG,
52126
],
53127
)

py/tests/cc-deps/BUILD.bazel

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,22 @@ cc_binary(
3131
deps = ["@rules_python//python/cc:current_py_cc_headers_abi3"],
3232
)
3333

34+
# Python extensions retain the terminal's interpreter configuration through
35+
# deps so their headers match the runtime that imports them.
3436
py_test(
3537
name = "test_smoke",
3638
srcs = ["test_smoke.py"],
37-
data = [":example_library.so"],
3839
imports = ["."],
3940
python_version = "3.13",
4041
target_compatible_with = _WINDOWS_INCOMPATIBLE,
42+
deps = [":example_library.so"],
4143
)
4244

4345
py_test(
4446
name = "test_limited_api",
4547
srcs = ["test_limited_api.py"],
46-
data = [":limited_api_library.so"],
4748
imports = ["."],
4849
python_version = "3.13",
4950
target_compatible_with = _WINDOWS_INCOMPATIBLE,
51+
deps = [":limited_api_library.so"],
5052
)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
load("//py:defs.bzl", "py_binary", "py_library", "py_venv")
2+
load(":tests.bzl", "probe", "reset_data_edges_test_suite", "root", "terminal")
3+
4+
probe(name = "probe")
5+
6+
# The launcher's data edge is reached through each parent venv's deps edge.
7+
py_binary(
8+
name = "launcher",
9+
srcs = ["main.py"],
10+
data = [":probe"],
11+
main = "main.py",
12+
tags = ["manual"],
13+
)
14+
15+
py_library(
16+
name = "library",
17+
srcs = ["main.py"],
18+
data = [":probe"],
19+
tags = ["manual"],
20+
)
21+
22+
py_venv(
23+
name = "first",
24+
srcs = ["main.py"],
25+
data = [":probe"],
26+
dep_group = "first",
27+
python_version = "3.12",
28+
tags = ["manual"],
29+
deps = [
30+
":launcher",
31+
":library",
32+
],
33+
)
34+
35+
# Nested terminal transitions must keep the baseline inherited above their
36+
# parent, not recapture the parent's selected environment.
37+
terminal(
38+
name = "parent_terminal",
39+
data = [":probe"],
40+
dep_group = "parent",
41+
python_version = "3.12",
42+
tags = ["manual"],
43+
deps = [":nested_terminal"],
44+
)
45+
46+
terminal(
47+
name = "nested_terminal",
48+
data = [":probe"],
49+
dep_group = "nested",
50+
python_version = "3.9",
51+
tags = ["manual"],
52+
)
53+
54+
py_venv(
55+
name = "second",
56+
srcs = ["main.py"],
57+
data = [":probe"],
58+
dep_group = "second",
59+
python_version = "3.12",
60+
tags = ["manual"],
61+
deps = [
62+
":launcher",
63+
":library",
64+
],
65+
)
66+
67+
root(
68+
name = "root",
69+
tags = ["manual"],
70+
deps = [
71+
":first",
72+
":parent_terminal",
73+
":probe",
74+
":second",
75+
],
76+
)
77+
78+
reset_data_edges_test_suite()

py/tests/reset-data-edges/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pass

0 commit comments

Comments
 (0)