Skip to content

Commit b2c3926

Browse files
authored
fix: parsing local version with digit followed by non-digits (#3032)
When parsing the local identifier segment of `<digit><letter>` the parser would give an error saying the letter was unexpected. What was happening was `accept_digits()` consumed up to the first non-digit, and considered this success, which prevented calling `accept_alnum()` to finish the parsing. To fix, only call `accept_alnum()`, then post-process the value to normalize an all-digit segment. I'm guessing `accept_digits()` stopping at the first non-digit is WAI because it expects to parse e.g. "3.14b", where the caller handles subsequent characters. Along the way, some minor doc improvements to the parser code. Fixes #3030
1 parent 998e22e commit b2c3926

File tree

2 files changed

+46
-9
lines changed

2 files changed

+46
-9
lines changed

python/private/version.bzl

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ def _in(reference):
4444
return lambda token: token in reference
4545

4646
def _ctx(start):
47-
return {"norm": "", "start": start}
47+
"""Creates a context, which is state for parsing (or sub-parsing)."""
48+
return {
49+
# The result value from parsing
50+
"norm": "",
51+
# Where in the parser's input string this context starts.
52+
"start": start,
53+
}
4854

4955
def _open_context(self):
5056
"""Open an new parsing ctx.
@@ -60,7 +66,16 @@ def _open_context(self):
6066
return self.contexts[-1]
6167

6268
def _accept(self, key = None):
63-
"""Close the current ctx successfully and merge the results."""
69+
"""Close the current ctx successfully and merge the results.
70+
71+
Args:
72+
self: {type}`Parser}
73+
key: {type}`str | None` the key to store the result in
74+
the most recent context. If not set, the key is "norm".
75+
76+
Returns:
77+
{type}`bool` always True
78+
"""
6479
finished = self.contexts.pop()
6580
self.contexts[-1]["norm"] += finished["norm"]
6681
if key:
@@ -79,7 +94,14 @@ def _discard(self, key = None):
7994
return False
8095

8196
def _new(input):
82-
"""Create a new normalizer"""
97+
"""Create a new parser
98+
99+
Args:
100+
input: {type}`str` input to parse
101+
102+
Returns:
103+
{type}`Parser` a struct for a parser object.
104+
"""
83105
self = struct(
84106
input = input,
85107
contexts = [_ctx(0)],
@@ -167,7 +189,7 @@ def accept_placeholder(parser):
167189
return parser.accept()
168190

169191
def accept_digits(parser):
170-
"""Accept multiple digits (or placeholders).
192+
"""Accept multiple digits (or placeholders), up to a non-digit/placeholder.
171193
172194
Args:
173195
parser: The normalizer.
@@ -275,13 +297,20 @@ def accept_separator_alnum(parser):
275297
Returns:
276298
whether a separator and an alphanumeric string were accepted.
277299
"""
278-
parser.open_context()
300+
ctx = parser.open_context()
279301

280302
# PEP 440: Local version segments
281-
if (
282-
accept(parser, _in([".", "-", "_"]), ".") and
283-
(accept_digits(parser) or accept_alnum(parser))
284-
):
303+
if not accept(parser, _in([".", "-", "_"]), "."):
304+
return parser.discard()
305+
306+
if accept_alnum(parser):
307+
# First character is separator; skip it.
308+
value = ctx["norm"][1:]
309+
310+
# PEP 440: Integer Normalization
311+
if value.isdigit():
312+
value = str(int(value))
313+
ctx["norm"] = ctx["norm"][0] + value
285314
return parser.accept()
286315

287316
return parser.discard()

tests/version/version_test.bzl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ def _test_normalization(env):
105105

106106
_tests.append(_test_normalization)
107107

108+
def _test_normalize_local(env):
109+
# Verify a local with a [digit][non-digit] sequence parses ok
110+
in_str = "0.1.0+brt.9e"
111+
actual = version.normalize(in_str)
112+
env.expect.that_str(actual).equals(in_str)
113+
114+
_tests.append(_test_normalize_local)
115+
108116
def _test_ordering(env):
109117
want = [
110118
# Taken from https://peps.python.org/pep-0440/#summary-of-permitted-suffixes-and-relative-ordering

0 commit comments

Comments
 (0)