diff --git a/src/poetry/core/utils/helpers.py b/src/poetry/core/utils/helpers.py index 50e5a223c..68bf4f37a 100644 --- a/src/poetry/core/utils/helpers.py +++ b/src/poetry/core/utils/helpers.py @@ -33,7 +33,7 @@ def module_name(name: str) -> str: def normalize_version(version: str) -> str: - return PEP440Version.parse(version).to_string(short=True) + return PEP440Version.parse(version).norm_string @contextmanager diff --git a/src/poetry/core/version/pep440/segments.py b/src/poetry/core/version/pep440/segments.py index 7586026e2..9cb3513fc 100644 --- a/src/poetry/core/version/pep440/segments.py +++ b/src/poetry/core/version/pep440/segments.py @@ -23,6 +23,15 @@ RELEASE_PHASE_REV: "r", RELEASE_PHASE_DEV: "dev", } +RELEASE_PHASES_NORMALIZED = { + RELEASE_PHASE_ALPHA: "a", + RELEASE_PHASE_BETA: "b", + RELEASE_PHASE_RC: "rc", + RELEASE_PHASE_PREVIEW: "pre", + RELEASE_PHASE_POST: "post", + RELEASE_PHASE_REV: "post", + RELEASE_PHASE_DEV: "dev", +} RELEASE_PHASES_SHORT = {v: k for k, v in RELEASE_PHASES.items() if k != "post"} @@ -111,6 +120,11 @@ class ReleaseTag: def __post_init__(self) -> None: object.__setattr__(self, "phase", self.expand(self.phase)) + @property + def norm_string(self) -> str: + normalized_phase = RELEASE_PHASES_NORMALIZED.get(self.phase, self.phase) + return f"{normalized_phase}{self.number}" + @classmethod def shorten(cls, phase: str) -> str: return RELEASE_PHASES.get(phase, phase) diff --git a/src/poetry/core/version/pep440/version.py b/src/poetry/core/version/pep440/version.py index b5a337d7e..80f3efbea 100644 --- a/src/poetry/core/version/pep440/version.py +++ b/src/poetry/core/version/pep440/version.py @@ -142,6 +142,29 @@ def non_semver_parts(self) -> tuple[int, ...]: assert isinstance(self.release.extra, tuple) return self.release.extra + @property + def norm_string(self) -> str: + version_string = self.release.to_string() + + if self.epoch: + # if epoch is non-zero we should include it + version_string = f"{self.epoch}!{version_string}" + + if self.pre: + version_string += self.pre.norm_string + + if self.post: + version_string = f"{version_string}.{self.post.norm_string}" + + if self.dev: + version_string = f"{version_string}.{self.dev.norm_string}" + + if self.local: + assert isinstance(self.local, tuple) + version_string += "+" + ".".join(map(str, self.local)) + + return version_string.lower() + def to_string(self, short: bool = False) -> str: dash = "-" if not short else "" diff --git a/tests/utils/test_helpers.py b/tests/utils/test_helpers.py index e61c2dbbe..c094400be 100644 --- a/tests/utils/test_helpers.py +++ b/tests/utils/test_helpers.py @@ -9,11 +9,80 @@ from poetry.core.utils.helpers import canonicalize_name from poetry.core.utils.helpers import combine_unicode +from poetry.core.utils.helpers import normalize_version from poetry.core.utils.helpers import parse_requires from poetry.core.utils.helpers import readme_content_type from poetry.core.utils.helpers import temporary_directory +@pytest.mark.parametrize( + "version,normalized_version", + [ + ( # already normalized version + "1!2.3.4.5.6a7.post8.dev9+local1.123.abc", + "1!2.3.4.5.6a7.post8.dev9+local1.123.abc", + ), + # PEP 440 Normalization + # Case sensitivity + ("1.1RC1", "1.1rc1"), + # Integer Normalization + ("00", "0"), + ("09000", "9000"), + ("1.0+foo0100", "1.0+foo0100"), + # Pre-release separators + ("1.1.a1", "1.1a1"), + ("1.1-a1", "1.1a1"), + ("1.1_a1", "1.1a1"), + ("1.1a.1", "1.1a1"), + ("1.1a-1", "1.1a1"), + ("1.1a_1", "1.1a1"), + # Pre-release spelling + ("1.1alpha1", "1.1a1"), + ("1.1beta2", "1.1b2"), + ("1.1c3", "1.1rc3"), + # Implicit pre-release number + ("1.2a", "1.2a0"), + # Post release separators + ("1.2.post2", "1.2.post2"), + ("1.2-post2", "1.2.post2"), + ("1.2_post2", "1.2.post2"), + ("1.2post.2", "1.2.post2"), + ("1.2post-2", "1.2.post2"), + ("1.2post_2", "1.2.post2"), + # Post release spelling + ("1.0-r4", "1.0.post4"), + ("1.0-rev4", "1.0.post4"), + # Implicit post release number + ("1.2.post", "1.2.post0"), + # Implicit post releases + ("1.0-1", "1.0.post1"), + # Development release separators + ("1.2.dev2", "1.2.dev2"), + ("1.2-dev2", "1.2.dev2"), + ("1.2_dev2", "1.2.dev2"), + ("1.2dev.2", "1.2.dev2"), + ("1.2dev-2", "1.2.dev2"), + ("1.2dev_2", "1.2.dev2"), + # Implicit development release number + ("1.2.dev", "1.2.dev0"), + # Local version segments + ("1.0+ubuntu-1", "1.0+ubuntu.1"), + ("1.0+ubuntu_1", "1.0+ubuntu.1"), + # Preceding v character + ("v1.0", "1.0"), + # Leading and Trailing Whitespace + (" 1.0 ", "1.0"), + ("\t1.0\t", "1.0"), + ("\n1.0\n", "1.0"), + ("\r\n1.0\r\n", "1.0"), + ("\f1.0\f", "1.0"), + ("\v1.0\v", "1.0"), + ], +) +def test_normalize_version(version: str, normalized_version: str) -> None: + assert normalize_version(version) == normalized_version + + def test_parse_requires() -> None: requires = """\ jsonschema>=2.6.0.0,<3.0.0.0