From af7f8152ec6d5dd28aeeedc29de9426657de7d3f Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Tue, 19 Apr 2022 15:13:26 +0200 Subject: [PATCH] semver: improve constraint parsing 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. --- src/poetry/core/semver/helpers.py | 31 +++++------------- src/poetry/core/semver/patterns.py | 34 ++++++++++++-------- tests/semver/test_parse_constraint.py | 46 +++++++++++++++++++++++++-- tests/version/test_requirements.py | 1 + 4 files changed, 73 insertions(+), 39 deletions(-) diff --git a/src/poetry/core/semver/helpers.py b/src/poetry/core/semver/helpers.py index 9022add94..87fc6df35 100644 --- a/src/poetry/core/semver/helpers.py +++ b/src/poetry/core/semver/helpers.py @@ -63,9 +63,9 @@ 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) @@ -73,16 +73,8 @@ def parse_single_constraint(constraint: str) -> VersionConstraint: # 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() @@ -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) @@ -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" diff --git a/src/poetry/core/semver/patterns.py b/src/poetry/core/semver/patterns.py index 927c8c13d..0dd213cf3 100644 --- a/src/poetry/core/semver/patterns.py +++ b/src/poetry/core/semver/patterns.py @@ -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_PATTERN})$", re.VERBOSE | re.IGNORECASE +) +TILDE_CONSTRAINT = re.compile( + rf"^~(?!=)\s*(?P{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE +) +TILDE_PEP440_CONSTRAINT = re.compile( + rf"^~=\s*(?P{VERSION_PATTERN})$", re.VERBOSE | re.IGNORECASE +) +X_CONSTRAINT = re.compile( + r"^(?P!=|==)?\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<>|!=|>=?|<=?|==?)?\s*(?P{VERSION_PATTERN}|dev)(\.\*)?$", + re.VERBOSE | re.IGNORECASE, +) diff --git a/tests/semver/test_parse_constraint.py b/tests/semver/test_parse_constraint.py index 1f1984617..ae6f2d3a1 100644 --- a/tests/semver/test_parse_constraint.py +++ b/tests/semver/test_parse_constraint.py @@ -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( @@ -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( @@ -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( @@ -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 diff --git a/tests/version/test_requirements.py b/tests/version/test_requirements.py index 5bfe5fe24..cdbea457e 100644 --- a/tests/version/test_requirements.py +++ b/tests/version/test_requirements.py @@ -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'", {