Skip to content

Commit ee8d7d6

Browse files
authored
refactor: consolidate version parsing (#2874)
This PR removes all of the custom version parsing functions where we try to make sense about the version (e.g. extracting major/minor versions). Whilst doing this I actually think that I made it easier to support #2837.
1 parent 61b5a8d commit ee8d7d6

File tree

11 files changed

+61
-275
lines changed

11 files changed

+61
-275
lines changed

python/private/BUILD.bazel

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ bzl_library(
139139
name = "config_settings_bzl",
140140
srcs = ["config_settings.bzl"],
141141
deps = [
142-
":semver_bzl",
142+
":version_bzl",
143143
"@bazel_skylib//lib:selects",
144144
"@bazel_skylib//rules:common_settings",
145145
],
@@ -249,9 +249,9 @@ bzl_library(
249249
":python_register_toolchains_bzl",
250250
":pythons_hub_bzl",
251251
":repo_utils_bzl",
252-
":semver_bzl",
253252
":toolchains_repo_bzl",
254253
":util_bzl",
254+
":version_bzl",
255255
"@bazel_features//:features",
256256
],
257257
)
@@ -610,11 +610,6 @@ bzl_library(
610610
],
611611
)
612612

613-
bzl_library(
614-
name = "semver_bzl",
615-
srcs = ["semver.bzl"],
616-
)
617-
618613
bzl_library(
619614
name = "sentinel_bzl",
620615
srcs = ["sentinel.bzl"],

python/private/config_settings.bzl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
load("@bazel_skylib//lib:selects.bzl", "selects")
1919
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
2020
load("//python/private:text_util.bzl", "render")
21-
load(":semver.bzl", "semver")
21+
load(":version.bzl", "version")
2222

2323
_PYTHON_VERSION_FLAG = Label("//python/config_settings:python_version")
2424
_PYTHON_VERSION_MAJOR_MINOR_FLAG = Label("//python/config_settings:python_version_major_minor")
@@ -181,8 +181,8 @@ _python_version_flag = rule(
181181
def _python_version_major_minor_flag_impl(ctx):
182182
input = _flag_value(ctx.attr._python_version_flag)
183183
if input:
184-
version = semver(input)
185-
value = "{}.{}".format(version.major, version.minor)
184+
ver = version.parse(input)
185+
value = "{}.{}".format(ver.release[0], ver.release[1])
186186
else:
187187
value = ""
188188

python/private/hermetic_runtime_repo_setup.bzl

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ load("//python:py_runtime_pair.bzl", "py_runtime_pair")
2020
load("//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain")
2121
load(":glob_excludes.bzl", "glob_excludes")
2222
load(":py_exec_tools_toolchain.bzl", "py_exec_tools_toolchain")
23-
load(":semver.bzl", "semver")
23+
load(":version.bzl", "version")
2424

2525
_IS_FREETHREADED = Label("//python/config_settings:is_py_freethreaded")
2626

@@ -53,8 +53,11 @@ def define_hermetic_runtime_toolchain_impl(
5353
use.
5454
"""
5555
_ = name # @unused
56-
version_info = semver(python_version)
57-
version_dict = version_info.to_dict()
56+
version_info = version.parse(python_version)
57+
version_dict = {
58+
"major": version_info.release[0],
59+
"minor": version_info.release[1],
60+
}
5861
native.filegroup(
5962
name = "files",
6063
srcs = native.glob(
@@ -198,9 +201,9 @@ def define_hermetic_runtime_toolchain_impl(
198201
files = [":files"],
199202
interpreter = python_bin,
200203
interpreter_version_info = {
201-
"major": str(version_info.major),
202-
"micro": str(version_info.patch),
203-
"minor": str(version_info.minor),
204+
"major": str(version_info.release[0]),
205+
"micro": str(version_info.release[2]),
206+
"minor": str(version_info.release[1]),
204207
},
205208
coverage_tool = select({
206209
# Convert empty string to None

python/private/pypi/BUILD.bazel

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ bzl_library(
116116
":whl_target_platforms_bzl",
117117
"//python/private:full_version_bzl",
118118
"//python/private:normalize_name_bzl",
119-
"//python/private:semver_bzl",
119+
"//python/private:version_bzl",
120120
"//python/private:version_label_bzl",
121121
"@bazel_features//:features",
122122
"@pythons_hub//:interpreters_bzl",
@@ -256,7 +256,7 @@ bzl_library(
256256
srcs = ["pep508_evaluate.bzl"],
257257
deps = [
258258
"//python/private:enum_bzl",
259-
"//python/private:semver_bzl",
259+
"//python/private:version_bzl",
260260
],
261261
)
262262

python/private/pypi/extension.bzl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ load("//python/private:auth.bzl", "AUTH_ATTRS")
2222
load("//python/private:full_version.bzl", "full_version")
2323
load("//python/private:normalize_name.bzl", "normalize_name")
2424
load("//python/private:repo_utils.bzl", "repo_utils")
25-
load("//python/private:semver.bzl", "semver")
25+
load("//python/private:version.bzl", "version")
2626
load("//python/private:version_label.bzl", "version_label")
2727
load(":attrs.bzl", "use_isolated")
2828
load(":evaluate_markers.bzl", "evaluate_markers_py", EVALUATE_MARKERS_SRCS = "SRCS")
@@ -36,9 +36,9 @@ load(":whl_config_setting.bzl", "whl_config_setting")
3636
load(":whl_library.bzl", "whl_library")
3737
load(":whl_repo_name.bzl", "pypi_repo_name", "whl_repo_name")
3838

39-
def _major_minor_version(version):
40-
version = semver(version)
41-
return "{}.{}".format(version.major, version.minor)
39+
def _major_minor_version(version_str):
40+
ver = version.parse(version_str)
41+
return "{}.{}".format(ver.release[0], ver.release[1])
4242

4343
def _whl_mods_impl(whl_mods_dict):
4444
"""Implementation of the pip.whl_mods tag class.

python/private/python.bzl

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ load(":full_version.bzl", "full_version")
2121
load(":python_register_toolchains.bzl", "python_register_toolchains")
2222
load(":pythons_hub.bzl", "hub_repo")
2323
load(":repo_utils.bzl", "repo_utils")
24-
load(":semver.bzl", "semver")
2524
load(":toolchains_repo.bzl", "multi_toolchain_aliases")
2625
load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER")
26+
load(":version.bzl", "version")
2727

2828
def parse_modules(*, module_ctx, _fail = fail):
2929
"""Parse the modules and return a struct for registrations.
@@ -458,16 +458,20 @@ def _fail_multiple_default_toolchains(first, second):
458458
second = second,
459459
))
460460

461-
def _validate_version(*, version, _fail = fail):
462-
parsed = semver(version)
463-
if parsed.patch == None or parsed.build or parsed.pre_release:
464-
_fail("The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '{}'".format(version))
461+
def _validate_version(version_str, *, _fail = fail):
462+
v = version.parse(version_str, strict = True, _fail = _fail)
463+
if v == None:
464+
# Only reachable in tests
465+
return False
466+
467+
if len(v.release) < 3:
468+
_fail("The 'python_version' attribute needs to specify the full version in at least 'X.Y.Z' format, got: '{}'".format(v.string))
465469
return False
466470

467471
return True
468472

469473
def _process_single_version_overrides(*, tag, _fail = fail, default):
470-
if not _validate_version(version = tag.python_version, _fail = _fail):
474+
if not _validate_version(tag.python_version, _fail = _fail):
471475
return
472476

473477
available_versions = default["tool_versions"]
@@ -517,7 +521,7 @@ def _process_single_version_overrides(*, tag, _fail = fail, default):
517521
kwargs.setdefault(tag.python_version, {})["distutils"] = tag.distutils
518522

519523
def _process_single_version_platform_overrides(*, tag, _fail = fail, default):
520-
if not _validate_version(version = tag.python_version, _fail = _fail):
524+
if not _validate_version(tag.python_version, _fail = _fail):
521525
return
522526

523527
available_versions = default["tool_versions"]
@@ -558,12 +562,12 @@ def _process_global_overrides(*, tag, default, _fail = fail):
558562

559563
if tag.minor_mapping:
560564
for minor_version, full_version in tag.minor_mapping.items():
561-
parsed = semver(minor_version)
562-
if parsed.patch != None or parsed.build or parsed.pre_release:
563-
fail("Expected the key to be of `X.Y` format but got `{}`".format(minor_version))
564-
parsed = semver(full_version)
565-
if parsed.patch == None:
566-
fail("Expected the value to at least be of `X.Y.Z` format but got `{}`".format(minor_version))
565+
parsed = version.parse(minor_version, strict = True, _fail = _fail)
566+
if len(parsed.release) > 2 or parsed.pre or parsed.post or parsed.dev or parsed.local:
567+
fail("Expected the key to be of `X.Y` format but got `{}`".format(parsed.string))
568+
569+
# Ensure that the version is valid
570+
version.parse(full_version, strict = True, _fail = _fail)
567571

568572
default["minor_mapping"] = tag.minor_mapping
569573

@@ -651,8 +655,11 @@ def _get_toolchain_config(*, modules, _fail = fail):
651655

652656
versions = {}
653657
for version_string in available_versions:
654-
v = semver(version_string)
655-
versions.setdefault("{}.{}".format(v.major, v.minor), []).append((int(v.patch), version_string))
658+
v = version.parse(version_string, strict = True)
659+
versions.setdefault(
660+
"{}.{}".format(v.release[0], v.release[1]),
661+
[],
662+
).append((version.key(v), v.string))
656663

657664
minor_mapping = {
658665
major_minor: max(subset)[1]

python/private/semver.bzl

Lines changed: 0 additions & 85 deletions
This file was deleted.

python/private/version.bzl

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ def normalize_pep440(version):
510510
"""
511511
return _parse(version, strict = True)["norm"]
512512

513-
def _parse(version_str, strict = True):
513+
def _parse(version_str, strict = True, _fail = fail):
514514
"""Escape the version component of a filename.
515515
516516
See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode
@@ -519,6 +519,7 @@ def _parse(version_str, strict = True):
519519
Args:
520520
version_str: version string to be normalized according to PEP 440.
521521
strict: fail if the version is invalid, defaults to True.
522+
_fail: Used for tests
522523
523524
Returns:
524525
string containing the normalized version.
@@ -544,7 +545,7 @@ def _parse(version_str, strict = True):
544545
parser_ctx = parser.context()
545546
if parser.input[parser_ctx["start"]:]:
546547
if strict:
547-
fail(
548+
_fail(
548549
"Failed to parse PEP 440 version identifier '%s'." % parser.input,
549550
"Parse error at '%s'" % parser.input[parser_ctx["start"]:],
550551
)
@@ -554,7 +555,7 @@ def _parse(version_str, strict = True):
554555
parser_ctx["is_prefix"] = is_prefix
555556
return parser_ctx
556557

557-
def parse(version_str, strict = False):
558+
def parse(version_str, strict = False, _fail = fail):
558559
"""Parse a PEP4408 compliant version.
559560
560561
This is similar to `normalize_pep440`, but it parses individual components to
@@ -563,6 +564,7 @@ def parse(version_str, strict = False):
563564
Args:
564565
version_str: version string to be normalized according to PEP 440.
565566
strict: fail if the version is invalid.
567+
_fail: used for tests
566568
567569
Returns:
568570
a struct with individual components of a version:
@@ -580,29 +582,29 @@ def parse(version_str, strict = False):
580582
* `string` {type}`str` normalized value of the input.
581583
"""
582584

583-
parts = _parse(version_str, strict = strict)
585+
parts = _parse(version_str, strict = strict, _fail = _fail)
584586
if not parts:
585587
return None
586588

587589
if parts["is_prefix"] and (parts["local"] or parts["post"] or parts["dev"] or parts["pre"]):
588590
if strict:
589-
fail("local version part has been obtained, but only public segments can have prefix matches")
591+
_fail("local version part has been obtained, but only public segments can have prefix matches")
590592

591593
# https://peps.python.org/pep-0440/#public-version-identifiers
592594
return None
593595

594596
return struct(
595-
epoch = _parse_epoch(parts["epoch"]),
597+
epoch = _parse_epoch(parts["epoch"], _fail),
596598
release = _parse_release(parts["release"]),
597599
pre = _parse_pre(parts["pre"]),
598-
post = _parse_post(parts["post"]),
599-
dev = _parse_dev(parts["dev"]),
600-
local = _parse_local(parts["local"]),
600+
post = _parse_post(parts["post"], _fail),
601+
dev = _parse_dev(parts["dev"], _fail),
602+
local = _parse_local(parts["local"], _fail),
601603
string = parts["norm"],
602604
is_prefix = parts["is_prefix"],
603605
)
604606

605-
def _parse_epoch(value):
607+
def _parse_epoch(value, fail):
606608
if not value:
607609
return 0
608610

@@ -614,7 +616,7 @@ def _parse_epoch(value):
614616
def _parse_release(value):
615617
return tuple([int(d) for d in value.split(".")])
616618

617-
def _parse_local(value):
619+
def _parse_local(value, fail):
618620
if not value:
619621
return None
620622

@@ -624,7 +626,7 @@ def _parse_local(value):
624626
# If the part is numerical, handle it as a number
625627
return tuple([int(part) if part.isdigit() else part for part in value[1:].split(".")])
626628

627-
def _parse_dev(value):
629+
def _parse_dev(value, fail):
628630
if not value:
629631
return None
630632

@@ -646,7 +648,7 @@ def _parse_pre(value):
646648

647649
return (prefix, int(value[len(prefix):]))
648650

649-
def _parse_post(value):
651+
def _parse_post(value, fail):
650652
if not value:
651653
return None
652654

0 commit comments

Comments
 (0)