From ffb2dd1ffa418412d01de91fc12c48bf41239ac0 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. --- .../core/packages/constraints/__init__.py | 6 ++-- src/poetry/core/semver/helpers.py | 32 +++++------------ src/poetry/core/semver/patterns.py | 34 +++++++++++-------- tests/semver/test_parse_constraint.py | 32 +++++++++++++++++ tests/version/test_requirements.py | 1 + 5 files changed, 65 insertions(+), 40 deletions(-) diff --git a/src/poetry/core/packages/constraints/__init__.py b/src/poetry/core/packages/constraints/__init__.py index d61a2cb79..f5c6707c2 100644 --- a/src/poetry/core/packages/constraints/__init__.py +++ b/src/poetry/core/packages/constraints/__init__.py @@ -51,12 +51,12 @@ def parse_single_constraint(constraint: str) -> Constraint: m = BASIC_CONSTRAINT.match(constraint) if m: op = m.group(1) + version_string = m.group(2).strip() + if op is None: op = "==" - version = m.group(2).strip() - - return Constraint(version, op) + return Constraint(version_string, op) raise ValueError(f"Could not parse version constraint: {constraint}") diff --git a/src/poetry/core/semver/helpers.py b/src/poetry/core/semver/helpers.py index 9022add94..cba30eb45 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,9 @@ 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")) + print(version) + if version.release.precision == 2: high = version.stable.next_major() else: high = version.stable.next_minor() @@ -92,14 +85,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 +117,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..c88429837 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})\s*$", 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)(\.\*)?\s*$", + re.VERBOSE | re.IGNORECASE, +) diff --git a/tests/semver/test_parse_constraint.py b/tests/semver/test_parse_constraint.py index 5e80626af..e6d776a20 100644 --- a/tests/semver/test_parse_constraint.py +++ b/tests/semver/test_parse_constraint.py @@ -182,6 +182,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 +212,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( diff --git a/tests/version/test_requirements.py b/tests/version/test_requirements.py index 556ed842d..1073aeb9c 100644 --- a/tests/version/test_requirements.py +++ b/tests/version/test_requirements.py @@ -47,6 +47,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'", {