Skip to content

Commit

Permalink
semver: improve constraint parsing (#327)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
abn authored May 2, 2022
1 parent 16cb159 commit c5c5ca4
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 39 deletions.
31 changes: 8 additions & 23 deletions src/poetry/core/semver/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,26 +63,18 @@ def parse_single_constraint(constraint: str) -> VersionConstraint:
# Tilde range
m = TILDE_CONSTRAINT.match(constraint)
if m:
version = Version.parse(m.group(1))
version = Version.parse(m.group("version"))
high = version.stable.next_minor()
if len(m.group(1).split(".")) == 1:
if version.release.precision == 1:
high = version.stable.next_major()

return VersionRange(version, high, include_min=True)

# PEP 440 Tilde range (~=)
m = TILDE_PEP440_CONSTRAINT.match(constraint)
if m:
precision = 1
if m.group(3):
precision += 1

if m.group(4):
precision += 1

version = Version.parse(m.group(1))

if precision == 2:
version = Version.parse(m.group("version"))
if version.release.precision == 2:
high = version.stable.next_major()
else:
high = version.stable.next_minor()
Expand All @@ -92,14 +84,14 @@ def parse_single_constraint(constraint: str) -> VersionConstraint:
# Caret range
m = CARET_CONSTRAINT.match(constraint)
if m:
version = Version.parse(m.group(1))
version = Version.parse(m.group("version"))

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

# X Range
m = X_CONSTRAINT.match(constraint)
if m:
op = m.group(1)
op = m.group("op")
major = int(m.group(2))
minor = m.group(3)

Expand All @@ -124,15 +116,8 @@ def parse_single_constraint(constraint: str) -> VersionConstraint:
# Basic comparator
m = BASIC_CONSTRAINT.match(constraint)
if m:
op = m.group(1)
version_string = m.group(2)

# Technically invalid constraints like `>= 3.*` will appear
# here as `3.`.
# Pip currently supports these and to avoid breaking existing
# users workflows we need to support them as well. To do so,
# we just remove the inconsequential part.
version_string = version_string.rstrip(".")
op = m.group("op")
version_string = m.group("version")

if version_string == "dev":
version_string = "0.0-dev"
Expand Down
34 changes: 20 additions & 14 deletions src/poetry/core/semver/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,27 @@

import re

from packaging.version import VERSION_PATTERN

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

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

COMPLETE_VERSION = re.compile("(?i)" + _COMPLETE_VERSION)
CARET_CONSTRAINT = re.compile(
rf"^\^(?P<version>{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE
)
TILDE_CONSTRAINT = re.compile(
rf"^~(?!=)\s*(?P<version>{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE
)
TILDE_PEP440_CONSTRAINT = re.compile(
rf"^~=\s*(?P<version>{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE
)
X_CONSTRAINT = re.compile(
r"^(?P<op>!=|==)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$"
)

CARET_CONSTRAINT = re.compile(rf"(?i)^\^({_COMPLETE_VERSION})$")
TILDE_CONSTRAINT = re.compile(rf"(?i)^~(?!=)\s*({_COMPLETE_VERSION})$")
TILDE_PEP440_CONSTRAINT = re.compile(rf"(?i)^~=\s*({_COMPLETE_VERSION})$")
X_CONSTRAINT = re.compile(r"^(!=|==)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$")
BASIC_CONSTRAINT = re.compile(rf"(?i)^(<>|!=|>=?|<=?|==?)?\s*({_COMPLETE_VERSION}|dev)")
# note that we also allow technically incorrect version patterns with astrix (eg: 3.5.*)
# as this is supported by pip and appears in metadata within python packages
BASIC_CONSTRAINT = re.compile(
rf"^(?P<op><>|!=|>=?|<=?|==?)?\s*(?P<version>{VERSION_PATTERN}|dev)(\.\*)?$",
re.VERBOSE | re.IGNORECASE,
)
46 changes: 44 additions & 2 deletions tests/semver/test_parse_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
include_min=True,
),
),
(
"== 3.8.x",
VersionRange(
min=Version.from_parts(3, 8),
max=Version.from_parts(3, 9, 0),
include_min=True,
),
),
(
"~= 3.8",
VersionRange(
Expand Down Expand Up @@ -182,6 +190,28 @@
include_min=True,
),
),
(
"^1.0.0a1.dev0",
VersionRange(
min=Version.from_parts(
1, 0, 0, pre=ReleaseTag("a", 1), dev=ReleaseTag("dev", 0)
),
max=Version.from_parts(2, 0, 0),
include_min=True,
),
),
(
"1.0.0a1.dev0",
VersionRange(
min=Version.from_parts(
1, 0, 0, pre=ReleaseTag("a", 1), dev=ReleaseTag("dev", 0)
),
max=Version.from_parts(
1, 0, 0, pre=ReleaseTag("a", 1), dev=ReleaseTag("dev", 0)
),
include_min=True,
),
),
(
"~1.0.0a1",
VersionRange(
Expand All @@ -190,6 +220,16 @@
include_min=True,
),
),
(
"~1.0.0a1.dev0",
VersionRange(
min=Version.from_parts(
1, 0, 0, pre=ReleaseTag("a", 1), dev=ReleaseTag("dev", 0)
),
max=Version.from_parts(1, 1, 0),
include_min=True,
),
),
(
"^0",
VersionRange(
Expand All @@ -208,7 +248,9 @@
),
],
)
@pytest.mark.parametrize(("with_whitespace_padding",), [(True,), (False,)])
def test_parse_constraint(
constraint: str, version: VersionRange | VersionUnion
constraint: str, version: VersionRange | VersionUnion, with_whitespace_padding: bool
) -> None:
assert parse_constraint(constraint) == version
padding = " " * (4 if with_whitespace_padding else 0)
assert parse_constraint(f"{padding}{constraint}{padding}") == version
1 change: 1 addition & 0 deletions tests/version/test_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def assert_requirement(
("name<3.*", {"name": "name", "constraint": "<3.0"}),
("name>3.5.*", {"name": "name", "constraint": ">3.5"}),
("name==1.0.post1", {"name": "name", "constraint": "==1.0.post1"}),
("name==1.2.0b1.dev0", {"name": "name", "constraint": "==1.2.0b1.dev0"}),
(
"name>=1.2.3;python_version=='2.6'",
{
Expand Down

0 comments on commit c5c5ca4

Please sign in to comment.