Skip to content

Commit ff23111

Browse files
committed
simplify the parsing function
1 parent f7eec91 commit ff23111

File tree

1 file changed

+52
-46
lines changed

1 file changed

+52
-46
lines changed

python/private/version.bzl

Lines changed: 52 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,23 @@ def _open_context(self):
5959
self.contexts.append(_ctx(_context(self)["start"]))
6060
return self.contexts[-1]
6161

62-
def _accept(self):
62+
def _accept(self, key = None):
6363
"""Close the current ctx successfully and merge the results."""
6464
finished = self.contexts.pop()
6565
self.contexts[-1]["norm"] += finished["norm"]
66+
if key:
67+
self.contexts[-1][key] = finished["norm"]
68+
6669
self.contexts[-1]["start"] = finished["start"]
6770
return True
6871

6972
def _context(self):
7073
return self.contexts[-1]
7174

72-
def _discard(self):
75+
def _discard(self, key = None):
7376
self.contexts.pop()
77+
if key:
78+
self.contexts[-1][key] = ""
7479
return False
7580

7681
def _new(input):
@@ -313,9 +318,9 @@ def accept_epoch(parser):
313318
if accept_digits(parser) and accept(parser, _is("!"), "!"):
314319
if ctx["norm"] == "0!":
315320
ctx["norm"] = ""
316-
return parser.accept()
321+
return parser.accept("epoch")
317322
else:
318-
return parser.discard()
323+
return parser.discard("epoch")
319324

320325
def accept_release(parser):
321326
"""Accept the release segment, numbers separated by dots.
@@ -329,10 +334,10 @@ def accept_release(parser):
329334
parser.open_context()
330335

331336
if not accept_digits(parser):
332-
return parser.discard()
337+
return parser.discard("release")
333338

334339
accept_dot_number_sequence(parser)
335-
return parser.accept()
340+
return parser.accept("release")
336341

337342
def accept_pre_l(parser):
338343
"""PEP 440: Pre-release spelling.
@@ -374,15 +379,15 @@ def accept_prerelease(parser):
374379
accept(parser, _in(["-", "_", "."]), "")
375380

376381
if not accept_pre_l(parser):
377-
return parser.discard()
382+
return parser.discard("pre")
378383

379384
accept(parser, _in(["-", "_", "."]), "")
380385

381386
if not accept_digits(parser):
382387
# PEP 440: Implicit pre-release number
383388
ctx["norm"] += "0"
384389

385-
return parser.accept()
390+
return parser.accept("pre")
386391

387392
def accept_implicit_postrelease(parser):
388393
"""PEP 440: Implicit post releases.
@@ -444,9 +449,9 @@ def accept_postrelease(parser):
444449
parser.open_context()
445450

446451
if accept_implicit_postrelease(parser) or accept_explicit_postrelease(parser):
447-
return parser.accept()
452+
return parser.accept("post")
448453

449-
return parser.discard()
454+
return parser.discard("post")
450455

451456
def accept_devrelease(parser):
452457
"""PEP 440: Developmental releases.
@@ -470,9 +475,9 @@ def accept_devrelease(parser):
470475
# PEP 440: Implicit development release number
471476
ctx["norm"] += "0"
472477

473-
return parser.accept()
478+
return parser.accept("dev")
474479

475-
return parser.discard()
480+
return parser.discard("dev")
476481

477482
def accept_local(parser):
478483
"""PEP 440: Local version identifiers.
@@ -487,9 +492,9 @@ def accept_local(parser):
487492

488493
if accept(parser, _is("+"), "+") and accept_alnum(parser):
489494
accept_separator_alnum_sequence(parser)
490-
return parser.accept()
495+
return parser.accept("local")
491496

492-
return parser.discard()
497+
return parser.discard("local")
493498

494499
def normalize_pep440(version):
495500
"""Escape the version component of a filename.
@@ -518,63 +523,66 @@ def _parse(version_str, strict = True):
518523
Returns:
519524
string containing the normalized version.
520525
"""
521-
version_str = version_str.strip() # PEP 440: Leading and Trailing Whitespace
526+
version = version_str.strip() # PEP 440: Leading and Trailing Whitespace
522527
is_prefix = False
523528

524529
if not strict:
525-
is_prefix = version_str.endswith(".*")
526-
version_str = version_str.strip(" .*") # PEP 440: Leading and Trailing Whitespace and ".*"
530+
is_prefix = version.endswith(".*")
531+
version = version.strip(" .*") # PEP 440: Leading and Trailing Whitespace and ".*"
527532

528-
parser = _new(version_str)
533+
parser = _new(version)
529534
accept(parser, _is("v"), "") # PEP 440: Preceding v character
530-
fns = [
531-
("epoch", accept_epoch),
532-
("release", accept_release),
533-
("pre", accept_prerelease),
534-
("post", accept_postrelease),
535-
("dev", accept_devrelease),
536-
("local", accept_local),
537-
]
538-
parts = {
539-
"is_prefix": is_prefix,
540-
}
541-
for key, fn in fns:
542-
start = len(parser.context()["norm"])
543-
fn(parser)
544-
parts[key] = parser.context()["norm"][start:]
545-
parts["norm"] = parser.context()["norm"]
546-
547-
# TODO @aignas 2025-05-09: move the `is_prefix` handling to `accept_release`
548-
if is_prefix and (parts["local"] or parts["post"] or parts["dev"] or parts["pre"]):
535+
accept_epoch(parser)
536+
accept_release(parser)
537+
accept_prerelease(parser)
538+
accept_postrelease(parser)
539+
accept_devrelease(parser)
540+
accept_local(parser)
541+
542+
parser_ctx = parser.context()
543+
if is_prefix and (parser_ctx["local"] or parser_ctx["post"] or parser_ctx["dev"] or parser_ctx["pre"]):
549544
if strict:
550545
fail("local version part has been obtained, but only public segments can have prefix matches")
551546

552547
# https://peps.python.org/pep-0440/#public-version-identifiers
553548
return None
554549

555-
if parser.input[parser.context()["start"]:]:
550+
if parser.input[parser_ctx["start"]:]:
556551
if strict:
557552
fail(
558553
"Failed to parse PEP 440 version identifier '%s'." % parser.input,
559-
"Parse error at '%s'" % parser.input[parser.context()["start"]:],
554+
"Parse error at '%s'" % parser.input[parser_ctx["start"]:],
560555
)
561556

562557
return None
563558

564-
return parts
559+
parser_ctx["is_prefix"] = is_prefix
560+
return parser_ctx
565561

566562
def parse(version_str, strict = False):
567-
"""Parse a PEP4408 compliant version
563+
"""Parse a PEP4408 compliant version.
568564
569-
See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode
570-
and https://peps.python.org/pep-0440/
565+
This is similar to `normalize_pep440`, but it parsers individual components to to
566+
comparable types.
571567
572568
Args:
573569
version_str: version string to be normalized according to PEP 440.
574570
strict: fail if the version is invalid.
575571
576572
Returns:
577-
string containing the normalized version.
573+
a struct with individual components of a version:
574+
* `epoch` {type}`int`, defaults to `0`
575+
* `release` {type}`tuple[int]` an n-tuple of ints
576+
* `pre` {type}`tuple[str, int] | None` a tuple of a string and an int,
577+
e.g. ("a", 1)
578+
* `post` {type}`tuple[str, int] | None` a tuple of a string and an int,
579+
e.g. ("~", 1)
580+
* `dev` {type}`tuple[str, int] | None` a tuple of a string and an int,
581+
e.g. ("", 1)
582+
* `local` {type}`tuple[str, int] | None` a tuple of components in the local
583+
version, e.g. ("abc", 123).
584+
* `is_prefix` {type}`bool` whether the version_str ends with `.*`.
585+
* `string` {type}`str` normalized value of the input.
578586
"""
579587

580588
parts = _parse(version_str, strict = strict)
@@ -715,8 +723,6 @@ def _version_compatible(left, right):
715723
else:
716724
right_star = "{}.".format(right_star)
717725

718-
# TODO @aignas 2025-05-09: more tests:
719-
# negative tests
720726
return _version_ge(left, right) and left.string.startswith(right_star)
721727

722728
def _version_ne(left, right):

0 commit comments

Comments
 (0)