Skip to content

Commit 5238141

Browse files
mattemrickeylev
andauthored
feat: add interpreter_version_info to py_runtime (#1671)
Adds an `interpreter_version_info` attribute to the `py_runtime` and associated provider that maps to the `sys.version_info` values. This allows the version of the interpreter to be known statically, which can be useful for rule sets that depend on the interpreter, and need to build environments / pathing that contain version info (virtualenvs for example). --------- Co-authored-by: Richard Levasseur <[email protected]>
1 parent a79b66a commit 5238141

File tree

6 files changed

+200
-2
lines changed

6 files changed

+200
-2
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ A brief description of the categories of changes:
5353
method to make imports more ergonomic. Users should only need to import the
5454
`Runfiles` object to locate runfiles.
5555

56+
* (toolchains) `PyRuntimeInfo` now includes a `interpreter_version_info` field
57+
that contains the static version information for the given interpreter.
58+
This can be set via `py_runtime` when registering an interpreter toolchain,
59+
and will done automatically for the builtin interpreter versions registered via
60+
`python_register_toolchains`.
61+
Note that this only available on the Starlark implementation of the provider.
62+
5663
## [0.28.0] - 2024-01-07
5764

5865
[0.28.0]: https://github.com/bazelbuild/rules_python/releases/tag/0.28.0

python/private/common/providers.bzl

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ def _PyRuntimeInfo_init(
4545
coverage_files = None,
4646
python_version,
4747
stub_shebang = None,
48-
bootstrap_template = None):
48+
bootstrap_template = None,
49+
interpreter_version_info = None):
4950
if (interpreter_path and interpreter) or (not interpreter_path and not interpreter):
5051
fail("exactly one of interpreter or interpreter_path must be specified")
5152

@@ -82,13 +83,32 @@ def _PyRuntimeInfo_init(
8283
if not stub_shebang:
8384
stub_shebang = DEFAULT_STUB_SHEBANG
8485

86+
if interpreter_version_info:
87+
if not ("major" in interpreter_version_info and "minor" in interpreter_version_info):
88+
fail("interpreter_version_info must have at least two keys, 'major' and 'minor'")
89+
90+
_interpreter_version_info = dict(**interpreter_version_info)
91+
interpreter_version_info = struct(
92+
major = int(_interpreter_version_info.pop("major")),
93+
minor = int(_interpreter_version_info.pop("minor")),
94+
micro = int(_interpreter_version_info.pop("micro")) if "micro" in _interpreter_version_info else None,
95+
releaselevel = str(_interpreter_version_info.pop("releaselevel")) if "releaselevel" in _interpreter_version_info else None,
96+
serial = int(_interpreter_version_info.pop("serial")) if "serial" in _interpreter_version_info else None,
97+
)
98+
99+
if len(_interpreter_version_info.keys()) > 0:
100+
fail("unexpected keys {} in interpreter_version_info".format(
101+
str(_interpreter_version_info.keys()),
102+
))
103+
85104
return {
86105
"bootstrap_template": bootstrap_template,
87106
"coverage_files": coverage_files,
88107
"coverage_tool": coverage_tool,
89108
"files": files,
90109
"interpreter": interpreter,
91110
"interpreter_path": interpreter_path,
111+
"interpreter_version_info": interpreter_version_info,
92112
"python_version": python_version,
93113
"stub_shebang": stub_shebang,
94114
}
@@ -136,6 +156,18 @@ the same conventions as the standard CPython interpreter.
136156
"filesystem path to the interpreter on the target platform. " +
137157
"Otherwise, this is `None`."
138158
),
159+
"interpreter_version_info": (
160+
"Version information about the interpreter this runtime provides. " +
161+
"It should match the format given by `sys.version_info`, however " +
162+
"for simplicity, the micro, releaselevel, and serial values are " +
163+
"optional." +
164+
"A struct with the following fields:\n" +
165+
" * major: int, the major version number\n" +
166+
" * minor: int, the minor version number\n" +
167+
" * micro: optional int, the micro version number\n" +
168+
" * releaselevel: optional str, the release level\n" +
169+
" * serial: optional int, the serial number of the release"
170+
),
139171
"python_version": (
140172
"Indicates whether this runtime uses Python major version 2 or 3. " +
141173
"Valid values are (only) `\"PY2\"` and " +

python/private/common/py_runtime_rule.bzl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ def _py_runtime_impl(ctx):
7979

8080
python_version = ctx.attr.python_version
8181

82+
interpreter_version_info = ctx.attr.interpreter_version_info
83+
8284
# TODO: Uncomment this after --incompatible_python_disable_py2 defaults to true
8385
# if ctx.fragments.py.disable_py2 and python_version == "PY2":
8486
# fail("Using Python 2 is not supported and disabled; see " +
@@ -93,10 +95,16 @@ def _py_runtime_impl(ctx):
9395
python_version = python_version,
9496
stub_shebang = ctx.attr.stub_shebang,
9597
bootstrap_template = ctx.file.bootstrap_template,
98+
interpreter_version_info = interpreter_version_info,
9699
)
97100
builtin_py_runtime_info_kwargs = dict(py_runtime_info_kwargs)
101+
102+
# Pop this property as it does not exist on BuiltinPyRuntimeInfo
103+
builtin_py_runtime_info_kwargs.pop("interpreter_version_info")
104+
98105
if not IS_BAZEL_7_OR_HIGHER:
99106
builtin_py_runtime_info_kwargs.pop("bootstrap_template")
107+
100108
return [
101109
PyRuntimeInfo(**py_runtime_info_kwargs),
102110
# Return the builtin provider for better compatibility.
@@ -232,6 +240,19 @@ not be set.
232240
For a platform runtime, this is the absolute path of a Python interpreter on
233241
the target platform. For an in-build runtime this attribute must not be set.
234242
"""),
243+
"interpreter_version_info": attr.string_dict(
244+
doc = """
245+
Version information about the interpreter this runtime provides. The
246+
supported keys match the names for `sys.version_info`. While the input
247+
values are strings, most are converted to ints. The supported keys are:
248+
* major: int, the major version number
249+
* minor: int, the minor version number
250+
* micro: optional int, the micro version number
251+
* releaselevel: optional str, the release level
252+
* serial: optional int, the serial number of the release"
253+
""",
254+
mandatory = False,
255+
),
235256
"python_version": attr.string(
236257
default = "PY3",
237258
values = ["PY2", "PY3"],

python/repositories.bzl

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ def _python_repository_impl(rctx):
102102

103103
platform = rctx.attr.platform
104104
python_version = rctx.attr.python_version
105-
python_short_version = python_version.rpartition(".")[0]
105+
python_version_info = python_version.split(".")
106+
python_short_version = "{0}.{1}".format(*python_version_info)
106107
release_filename = rctx.attr.release_filename
107108
urls = rctx.attr.urls or [rctx.attr.url]
108109
auth = get_auth(rctx, urls)
@@ -335,6 +336,11 @@ py_runtime(
335336
files = [":files"],
336337
{coverage_attr}
337338
interpreter = "{python_path}",
339+
interpreter_version_info = {{
340+
"major": "{interpreter_version_info_major}",
341+
"minor": "{interpreter_version_info_minor}",
342+
"micro": "{interpreter_version_info_micro}",
343+
}},
338344
python_version = "PY3",
339345
)
340346
@@ -356,6 +362,9 @@ py_cc_toolchain(
356362
python_version = python_short_version,
357363
python_version_nodot = python_short_version.replace(".", ""),
358364
coverage_attr = coverage_attr_text,
365+
interpreter_version_info_major = python_version_info[0],
366+
interpreter_version_info_minor = python_version_info[1],
367+
interpreter_version_info_micro = python_version_info[2],
359368
)
360369
rctx.delete("python")
361370
rctx.symlink(python_bin, "python")

tests/py_runtime/py_runtime_tests.bzl

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,121 @@ def _test_system_interpreter_must_be_absolute_impl(env, target):
413413

414414
_tests.append(_test_system_interpreter_must_be_absolute)
415415

416+
def _interpreter_version_info_test(name, interpreter_version_info, impl, expect_failure = True):
417+
if config.enable_pystar:
418+
py_runtime_kwargs = {
419+
"interpreter_version_info": interpreter_version_info,
420+
}
421+
attr_values = {}
422+
else:
423+
py_runtime_kwargs = {}
424+
attr_values = _SKIP_TEST
425+
426+
rt_util.helper_target(
427+
py_runtime,
428+
name = name + "_subject",
429+
python_version = "PY3",
430+
interpreter_path = "/py",
431+
**py_runtime_kwargs
432+
)
433+
analysis_test(
434+
name = name,
435+
target = name + "_subject",
436+
impl = impl,
437+
expect_failure = expect_failure,
438+
attr_values = attr_values,
439+
)
440+
441+
def _test_interpreter_version_info_must_define_major_and_minor_only_major(name):
442+
_interpreter_version_info_test(
443+
name,
444+
{
445+
"major": "3",
446+
},
447+
lambda env, target: (
448+
env.expect.that_target(target).failures().contains_predicate(
449+
matching.str_matches("must have at least two keys, 'major' and 'minor'"),
450+
)
451+
),
452+
)
453+
454+
_tests.append(_test_interpreter_version_info_must_define_major_and_minor_only_major)
455+
456+
def _test_interpreter_version_info_must_define_major_and_minor_only_minor(name):
457+
_interpreter_version_info_test(
458+
name,
459+
{
460+
"minor": "3",
461+
},
462+
lambda env, target: (
463+
env.expect.that_target(target).failures().contains_predicate(
464+
matching.str_matches("must have at least two keys, 'major' and 'minor'"),
465+
)
466+
),
467+
)
468+
469+
_tests.append(_test_interpreter_version_info_must_define_major_and_minor_only_minor)
470+
471+
def _test_interpreter_version_info_no_extraneous_keys(name):
472+
_interpreter_version_info_test(
473+
name,
474+
{
475+
"major": "3",
476+
"minor": "3",
477+
"something": "foo",
478+
},
479+
lambda env, target: (
480+
env.expect.that_target(target).failures().contains_predicate(
481+
matching.str_matches("unexpected keys [\"something\"]"),
482+
)
483+
),
484+
)
485+
486+
_tests.append(_test_interpreter_version_info_no_extraneous_keys)
487+
488+
def _test_interpreter_version_info_sets_values_to_none_if_not_given(name):
489+
_interpreter_version_info_test(
490+
name,
491+
{
492+
"major": "3",
493+
"micro": "10",
494+
"minor": "3",
495+
},
496+
lambda env, target: (
497+
env.expect.that_target(target).provider(
498+
PyRuntimeInfo,
499+
factory = py_runtime_info_subject,
500+
).interpreter_version_info().serial().equals(None)
501+
),
502+
expect_failure = False,
503+
)
504+
505+
_tests.append(_test_interpreter_version_info_sets_values_to_none_if_not_given)
506+
507+
def _test_interpreter_version_info_parses_values_to_struct(name):
508+
_interpreter_version_info_test(
509+
name,
510+
{
511+
"major": "3",
512+
"micro": "10",
513+
"minor": "6",
514+
"releaselevel": "alpha",
515+
"serial": "1",
516+
},
517+
impl = _test_interpreter_version_info_parses_values_to_struct_impl,
518+
expect_failure = False,
519+
)
520+
521+
def _test_interpreter_version_info_parses_values_to_struct_impl(env, target):
522+
version_info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject).interpreter_version_info()
523+
version_info.major().equals(3)
524+
version_info.minor().equals(6)
525+
version_info.micro().equals(10)
526+
version_info.releaselevel().equals("alpha")
527+
version_info.serial().equals(1)
528+
529+
_tests.append(_test_interpreter_version_info_parses_values_to_struct)
530+
416531
def py_runtime_test_suite(name):
417532
test_suite(
418533
name = name,

tests/py_runtime_info_subject.bzl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def py_runtime_info_subject(info, *, meta):
3838
files = lambda *a, **k: _py_runtime_info_subject_files(self, *a, **k),
3939
interpreter = lambda *a, **k: _py_runtime_info_subject_interpreter(self, *a, **k),
4040
interpreter_path = lambda *a, **k: _py_runtime_info_subject_interpreter_path(self, *a, **k),
41+
interpreter_version_info = lambda *a, **k: _py_runtime_info_subject_interpreter_version_info(self, *a, **k),
4142
python_version = lambda *a, **k: _py_runtime_info_subject_python_version(self, *a, **k),
4243
stub_shebang = lambda *a, **k: _py_runtime_info_subject_stub_shebang(self, *a, **k),
4344
# go/keep-sorted end
@@ -100,3 +101,16 @@ def _py_runtime_info_subject_stub_shebang(self):
100101
self.actual.stub_shebang,
101102
meta = self.meta.derive("stub_shebang()"),
102103
)
104+
105+
def _py_runtime_info_subject_interpreter_version_info(self):
106+
return subjects.struct(
107+
self.actual.interpreter_version_info,
108+
attrs = dict(
109+
major = subjects.int,
110+
minor = subjects.int,
111+
micro = subjects.int,
112+
releaselevel = subjects.str,
113+
serial = subjects.int,
114+
),
115+
meta = self.meta.derive("interpreter_version_info()"),
116+
)

0 commit comments

Comments
 (0)