Skip to content

Commit a6fb2a7

Browse files
abnbostonrwalker
authored andcommitted
semver: improve constraint parsing (python-poetry#327)
This change replaces the custom regex used with `packaging.version.VERSION_PATTERN` for consistency with other parts of the code base. Additionally, this fixes previous issues with parsing pre-release dev releases etc.
1 parent 61f7488 commit a6fb2a7

File tree

4 files changed

+73
-39
lines changed

4 files changed

+73
-39
lines changed

src/poetry/core/semver/helpers.py

+8-23
Original file line numberDiff line numberDiff line change
@@ -63,26 +63,18 @@ def parse_single_constraint(constraint: str) -> VersionConstraint:
6363
# Tilde range
6464
m = TILDE_CONSTRAINT.match(constraint)
6565
if m:
66-
version = Version.parse(m.group(1))
66+
version = Version.parse(m.group("version"))
6767
high = version.stable.next_minor()
68-
if len(m.group(1).split(".")) == 1:
68+
if version.release.precision == 1:
6969
high = version.stable.next_major()
7070

7171
return VersionRange(version, high, include_min=True)
7272

7373
# PEP 440 Tilde range (~=)
7474
m = TILDE_PEP440_CONSTRAINT.match(constraint)
7575
if m:
76-
precision = 1
77-
if m.group(3):
78-
precision += 1
79-
80-
if m.group(4):
81-
precision += 1
82-
83-
version = Version.parse(m.group(1))
84-
85-
if precision == 2:
76+
version = Version.parse(m.group("version"))
77+
if version.release.precision == 2:
8678
high = version.stable.next_major()
8779
else:
8880
high = version.stable.next_minor()
@@ -92,14 +84,14 @@ def parse_single_constraint(constraint: str) -> VersionConstraint:
9284
# Caret range
9385
m = CARET_CONSTRAINT.match(constraint)
9486
if m:
95-
version = Version.parse(m.group(1))
87+
version = Version.parse(m.group("version"))
9688

9789
return VersionRange(version, version.next_breaking(), include_min=True)
9890

9991
# X Range
10092
m = X_CONSTRAINT.match(constraint)
10193
if m:
102-
op = m.group(1)
94+
op = m.group("op")
10395
major = int(m.group(2))
10496
minor = m.group(3)
10597

@@ -124,15 +116,8 @@ def parse_single_constraint(constraint: str) -> VersionConstraint:
124116
# Basic comparator
125117
m = BASIC_CONSTRAINT.match(constraint)
126118
if m:
127-
op = m.group(1)
128-
version_string = m.group(2)
129-
130-
# Technically invalid constraints like `>= 3.*` will appear
131-
# here as `3.`.
132-
# Pip currently supports these and to avoid breaking existing
133-
# users workflows we need to support them as well. To do so,
134-
# we just remove the inconsequential part.
135-
version_string = version_string.rstrip(".")
119+
op = m.group("op")
120+
version_string = m.group("version")
136121

137122
if version_string == "dev":
138123
version_string = "0.0-dev"

src/poetry/core/semver/patterns.py

+20-14
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,27 @@
22

33
import re
44

5+
from packaging.version import VERSION_PATTERN
56

6-
MODIFIERS = (
7-
"[._-]?"
8-
r"((?!post)(?:beta|b|c|pre|RC|alpha|a|patch|pl|p|dev)(?:(?:[.-]?\d+)*)?)?"
9-
r"([+-]?([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?"
10-
)
117

12-
_COMPLETE_VERSION = (
13-
rf"v?(?:\d+!)?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?{MODIFIERS}(?:\+[^\s]+)?"
14-
)
8+
COMPLETE_VERSION = re.compile(VERSION_PATTERN, re.VERBOSE | re.IGNORECASE)
159

16-
COMPLETE_VERSION = re.compile("(?i)" + _COMPLETE_VERSION)
10+
CARET_CONSTRAINT = re.compile(
11+
rf"^\^(?P<version>{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE
12+
)
13+
TILDE_CONSTRAINT = re.compile(
14+
rf"^~(?!=)\s*(?P<version>{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE
15+
)
16+
TILDE_PEP440_CONSTRAINT = re.compile(
17+
rf"^~=\s*(?P<version>{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE
18+
)
19+
X_CONSTRAINT = re.compile(
20+
r"^(?P<op>!=|==)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$"
21+
)
1722

18-
CARET_CONSTRAINT = re.compile(rf"(?i)^\^({_COMPLETE_VERSION})$")
19-
TILDE_CONSTRAINT = re.compile(rf"(?i)^~(?!=)\s*({_COMPLETE_VERSION})$")
20-
TILDE_PEP440_CONSTRAINT = re.compile(rf"(?i)^~=\s*({_COMPLETE_VERSION})$")
21-
X_CONSTRAINT = re.compile(r"^(!=|==)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$")
22-
BASIC_CONSTRAINT = re.compile(rf"(?i)^(<>|!=|>=?|<=?|==?)?\s*({_COMPLETE_VERSION}|dev)")
23+
# note that we also allow technically incorrect version patterns with astrix (eg: 3.5.*)
24+
# as this is supported by pip and appears in metadata within python packages
25+
BASIC_CONSTRAINT = re.compile(
26+
rf"^(?P<op><>|!=|>=?|<=?|==?)?\s*(?P<version>{VERSION_PATTERN}|dev)(\.\*)?$",
27+
re.VERBOSE | re.IGNORECASE,
28+
)

tests/semver/test_parse_constraint.py

+44-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@
2828
include_min=True,
2929
),
3030
),
31+
(
32+
"== 3.8.x",
33+
VersionRange(
34+
min=Version.from_parts(3, 8),
35+
max=Version.from_parts(3, 9, 0),
36+
include_min=True,
37+
),
38+
),
3139
(
3240
"~= 3.8",
3341
VersionRange(
@@ -182,6 +190,28 @@
182190
include_min=True,
183191
),
184192
),
193+
(
194+
"^1.0.0a1.dev0",
195+
VersionRange(
196+
min=Version.from_parts(
197+
1, 0, 0, pre=ReleaseTag("a", 1), dev=ReleaseTag("dev", 0)
198+
),
199+
max=Version.from_parts(2, 0, 0),
200+
include_min=True,
201+
),
202+
),
203+
(
204+
"1.0.0a1.dev0",
205+
VersionRange(
206+
min=Version.from_parts(
207+
1, 0, 0, pre=ReleaseTag("a", 1), dev=ReleaseTag("dev", 0)
208+
),
209+
max=Version.from_parts(
210+
1, 0, 0, pre=ReleaseTag("a", 1), dev=ReleaseTag("dev", 0)
211+
),
212+
include_min=True,
213+
),
214+
),
185215
(
186216
"~1.0.0a1",
187217
VersionRange(
@@ -190,6 +220,16 @@
190220
include_min=True,
191221
),
192222
),
223+
(
224+
"~1.0.0a1.dev0",
225+
VersionRange(
226+
min=Version.from_parts(
227+
1, 0, 0, pre=ReleaseTag("a", 1), dev=ReleaseTag("dev", 0)
228+
),
229+
max=Version.from_parts(1, 1, 0),
230+
include_min=True,
231+
),
232+
),
193233
(
194234
"^0",
195235
VersionRange(
@@ -208,7 +248,9 @@
208248
),
209249
],
210250
)
251+
@pytest.mark.parametrize(("with_whitespace_padding",), [(True,), (False,)])
211252
def test_parse_constraint(
212-
constraint: str, version: VersionRange | VersionUnion
253+
constraint: str, version: VersionRange | VersionUnion, with_whitespace_padding: bool
213254
) -> None:
214-
assert parse_constraint(constraint) == version
255+
padding = " " * (4 if with_whitespace_padding else 0)
256+
assert parse_constraint(f"{padding}{constraint}{padding}") == version

tests/version/test_requirements.py

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def assert_requirement(
4343
("name<3.*", {"name": "name", "constraint": "<3.0"}),
4444
("name>3.5.*", {"name": "name", "constraint": ">3.5"}),
4545
("name==1.0.post1", {"name": "name", "constraint": "==1.0.post1"}),
46+
("name==1.2.0b1.dev0", {"name": "name", "constraint": "==1.2.0b1.dev0"}),
4647
(
4748
"name>=1.2.3;python_version=='2.6'",
4849
{

0 commit comments

Comments
 (0)