diff --git a/src/poetry/core/semver/version.py b/src/poetry/core/semver/version.py index 2fe6fe873..9f6822b0a 100644 --- a/src/poetry/core/semver/version.py +++ b/src/poetry/core/semver/version.py @@ -27,26 +27,18 @@ class Version(PEP440Version, VersionRangeConstraint): def precision(self) -> int: return self.release.precision - @property - def stable(self) -> Version: - if self.is_stable(): - return self - - return self.next_patch() - def next_breaking(self) -> Version: - if self.major == 0: - if self.minor is not None and self.minor != 0: - return self.next_minor() + if self.major > 0 or self.precision == 1: + return self.stable.next_major() - if self.precision == 1: - return self.next_major() - elif self.precision == 2: - return self.next_minor() + is_minor_greater_than_0 = self.minor is not None and self.minor > 0 + if is_minor_greater_than_0 or self.precision == 2: + return self.stable.next_minor() + if self.precision > 3: return self.next_patch() - return self.stable.next_major() + return self.stable.next_patch() @property def min(self) -> Version: diff --git a/src/poetry/core/version/pep440/version.py b/src/poetry/core/version/pep440/version.py index eeae009a8..162dff1de 100644 --- a/src/poetry/core/version/pep440/version.py +++ b/src/poetry/core/version/pep440/version.py @@ -143,6 +143,22 @@ def non_semver_parts(self) -> tuple[int, ...]: assert isinstance(self.release.extra, tuple) return self.release.extra + @property + def stable(self: T) -> T: + if self.is_stable(): + return self + + if not self.is_prerelease() and self.is_postrelease(): + return self.next_postrelease() + + if self.release.precision == 1: + return self.next_major() + + if self.release.precision == 2: + return self.next_minor() + + return self.next_patch() + def to_string(self, short: bool = False) -> str: if short: import warnings diff --git a/tests/semver/test_helpers.py b/tests/semver/test_helpers.py index cc136a796..41d0cab30 100644 --- a/tests/semver/test_helpers.py +++ b/tests/semver/test_helpers.py @@ -244,6 +244,30 @@ def test_parse_constraint_tilde(input: str, constraint: VersionRange) -> None: Version.from_parts(0, 0, 3), Version.from_parts(0, 0, 4), True ), ), + ( + "^0.0.3-alpha.21", + VersionRange( + Version.from_parts(0, 0, 3, pre=ReleaseTag("alpha", 21)), + Version.from_parts(0, 0, 4), + True, + ), + ), + ( + "^0.1.3-alpha.21", + VersionRange( + Version.from_parts(0, 1, 3, pre=ReleaseTag("alpha", 21)), + Version.from_parts(0, 2, 0), + True, + ), + ), + ( + "^0.0.0-alpha.21", + VersionRange( + Version.from_parts(0, 0, 0, pre=ReleaseTag("alpha", 21)), + Version.from_parts(0, 0, 1), + True, + ), + ), ], ) def test_parse_constraint_caret(input: str, constraint: VersionRange) -> None: @@ -392,6 +416,15 @@ def test_parse_constraints_with_trailing_comma( ("^1", ">=1,<2"), ("^1.0", ">=1.0,<2.0"), ("^1.0.0", ">=1.0.0,<2.0.0"), + ("^1.0.0-alpha.1", ">=1.0.0-alpha.1,<2.0.0"), + ("^0", ">=0,<1"), + ("^0.1", ">=0.1,<0.2"), + ("^0.0.2", ">=0.0.2,<0.0.3"), + ("^0.1.2", ">=0.1.2,<0.2.0"), + ("^0-alpha.1", ">=0-alpha.1,<1"), + ("^0.1-alpha.1", ">=0.1-alpha.1,<0.2"), + ("^0.0.2-alpha.1", ">=0.0.2-alpha.1,<0.0.3"), + ("^0.1.2-alpha.1", ">=0.1.2-alpha.1,<0.2.0"), ("~1", ">=1,<2"), ("~1.0", ">=1.0,<1.1"), ("~1.0.0", ">=1.0.0,<1.1.0"), diff --git a/tests/semver/test_version.py b/tests/semver/test_version.py index 31a8bffdb..8fb9f6682 100644 --- a/tests/semver/test_version.py +++ b/tests/semver/test_version.py @@ -49,6 +49,106 @@ def test_parse_invalid(value: str | None) -> None: Version.parse(value) # type: ignore[arg-type] +@pytest.mark.parametrize( + "version, expected", + [ + ("1", "2"), + ("1.2", "2.0"), + ("1.2.3", "2.0.0"), + ("2!1.2.3", "2!2.0.0"), + ("1.2.3+local", "2.0.0"), + ("1.2.3.4", "2.0.0.0"), + ("1.dev0", "2"), + ("1.2dev0", "2.0"), + ("1.2.3dev0", "2.0.0"), + ("1.2.3.4dev0", "2.0.0.0"), + ("1.post1", "2"), + ("1.2.post1", "2.0"), + ("1.2.3.post1", "2.0.0"), + ("1.post1.dev0", "2"), + ("1.2.post1.dev0", "2.0"), + ("1.2.3.post1.dev0", "2.0.0"), + ("2.a1", "3"), + ("2.2a1", "3.0"), + ("2.2.3a1", "3.0.0"), + ("2.2.3.4a1", "3.0.0.0"), + ("2.a1.post2", "3"), + ("2.2a1.post2", "3.0"), + ("2.2.3a1.post2", "3.0.0"), + ("2.2.3.4a1.post2", "3.0.0.0"), + ("2.a1.post2.dev0", "3"), + ("2.2a1.post2.dev0", "3.0"), + ("2.2.3a1.post2.dev0", "3.0.0"), + ("2.2.3.4a1.post2.dev0", "3.0.0.0"), + ], +) +def test_next_breaking_for_major_over_0_results_into_next_major_and_preserves_precision( + version: str, expected: str +) -> None: + subject = Version.parse(version) + + assert subject.next_breaking().text == expected + + +@pytest.mark.parametrize( + "version, expected", + [ + ("0", "1"), + ("0.0", "0.1"), + ("0.2", "0.3"), + ("0.2.3", "0.3.0"), + ("2!0.2.3", "2!0.3.0"), + ("0.2.3+local", "0.3.0"), + ("0.2.3.4", "0.3.0.0"), + ("0.0.3.4", "0.0.4.0"), + ("0.dev0", "1"), + ("0.0dev0", "0.1"), + ("0.2dev0", "0.3"), + ("0.2.3dev0", "0.3.0"), + ("0.0.3dev0", "0.0.4"), + ("0.post1", "1"), + ("0.0.post1", "0.1"), + ("0.2.post1", "0.3"), + ("0.2.3.post1", "0.3.0"), + ("0.0.3.post1", "0.0.4"), + ("0.post1.dev0", "1"), + ("0.0.post1.dev0", "0.1"), + ("0.2.post1.dev0", "0.3"), + ("0.2.3.post1.dev0", "0.3.0"), + ("0.0.3.post1.dev0", "0.0.4"), + ("0.a1", "1"), + ("0.0a1", "0.1"), + ("0.2a1", "0.3"), + ("0.2.3a1", "0.3.0"), + ("0.2.3.4a1", "0.3.0.0"), + ("0.0.3.4a1", "0.0.4.0"), + ("0.a1.post2", "1"), + ("0.0a1.post2", "0.1"), + ("0.2a1.post2", "0.3"), + ("0.2.3a1.post2", "0.3.0"), + ("0.2.3.4a1.post2", "0.3.0.0"), + ("0.0.3.4a1.post2", "0.0.4.0"), + ("0.a1.post2.dev0", "1"), + ("0.0a1.post2.dev0", "0.1"), + ("0.2a1.post2.dev0", "0.3"), + ("0.2.3a1.post2.dev0", "0.3.0"), + ("0.2.3.4a1.post2.dev0", "0.3.0.0"), + ("0.0.3.4a1.post2.dev0", "0.0.4.0"), + ("0-alpha.1", "1"), + ("0.0-alpha.1", "0.1"), + ("0.2-alpha.1", "0.3"), + ("0.0.1-alpha.2", "0.0.2"), + ("0.1.2-alpha.1", "0.2.0"), + ], +) +def test_next_breaking_for_major_0_version_is_treated_with_more_care_and_preserves_precision( + version: str, expected: str +) -> None: + subject = Version.parse(version) + + assert subject.next_breaking().text == expected + + @pytest.mark.parametrize( "versions", [ diff --git a/tests/version/pep440/test_version.py b/tests/version/pep440/test_version.py index d9ccb99d7..f8e43dbb2 100644 --- a/tests/version/pep440/test_version.py +++ b/tests/version/pep440/test_version.py @@ -226,6 +226,127 @@ def test_next_prerelease(version: str, expected: str) -> None: assert v.next_prerelease().text == expected +@pytest.mark.parametrize( + "version, expected", + [ + ("1", "1"), + ("1.2", "1.2"), + ("1.2.3", "1.2.3"), + ("2!1.2.3", "2!1.2.3"), + ("1.2.3+local", "1.2.3+local"), + ("1.2.3.4", "1.2.3.4"), + ("1.dev0", "1"), + ("1.2dev0", "1.2"), + ("1.2.3dev0", "1.2.3"), + ("1.2.3.4dev0", "1.2.4.0"), + ("1.post1", "1.post1"), + ("1.2.post1", "1.2.post1"), + ("1.2.3.post1", "1.2.3.post1"), + ("1.post1.dev0", "1.post1"), + ("1.2.post1.dev0", "1.2.post1"), + ("1.2.3.post1.dev0", "1.2.3.post1"), + ("1.a1", "1"), + ("1.2a1", "1.2"), + ("1.2.3a1", "1.2.3"), + ("1.2.3.4a1", "1.2.4.0"), + ("1.a1.post2", "1"), + ("1.2a1.post2", "1.2"), + ("1.2.3a1.post2", "1.2.3"), + ("1.2.3.4a1.post2", "1.2.4.0"), + ("1.a1.post2.dev0", "1"), + ("1.2a1.post2.dev0", "1.2"), + ("1.2.3a1.post2.dev0", "1.2.3"), + ("1.2.3.4a1.post2.dev0", "1.2.4.0"), + ], +) +def test_stable(version: str, expected: str) -> None: + subject = PEP440Version.parse(version) + + assert subject.stable.text == expected + + +@pytest.mark.parametrize( + "version, expected", + [ + ("1", True), + ("1.2", True), + ("1.2.3", True), + ("2!1.2.3", True), + ("1.2.3+local", True), + ("1.2.3.4", True), + ("1.dev0", False), + ("1.2dev0", False), + ("1.2.3dev0", False), + ("1.2.3.4dev0", False), + ("1.post1", True), + ("1.2.post1", True), + ("1.2.3.post1", True), + ("1.post1.dev0", False), + ("1.2.post1.dev0", False), + ("1.2.3.post1.dev0", False), + ("1.a1", False), + ("1.2a1", False), + ("1.2.3a1", False), + ("1.2.3.4a1", False), + ("1.a1.post2", False), + ("1.2a1.post2", False), + ("1.2.3a1.post2", False), + ("1.2.3.4a1.post2", False), + ("1.a1.post2.dev0", False), + ("1.2a1.post2.dev0", False), + ("1.2.3a1.post2.dev0", False), + ("1.2.3.4a1.post2.dev0", False), + ], +) +def test_is_stable(version: str, expected: bool) -> None: + subject = PEP440Version.parse(version) + + assert subject.is_stable() == expected + assert subject.is_unstable() == (not expected) + + +@pytest.mark.parametrize( + "version, expected", + [ + ("0", True), + ("0.2", True), + ("0.2.3", True), + ("2!0.2.3", True), + ("0.2.3+local", True), + ("0.2.3.4", True), + ("0.dev0", False), + ("0.2dev0", False), + ("0.2.3dev0", False), + ("0.2.3.4dev0", False), + ("0.post1", True), + ("0.2.post1", True), + ("0.2.3.post1", True), + ("0.post1.dev0", False), + ("0.2.post1.dev0", False), + ("0.2.3.post1.dev0", False), + ("0.a1", False), + ("0.2a1", False), + ("0.2.3a1", False), + ("0.2.3.4a1", False), + ("0.a1.post2", False), + ("0.2a1.post2", False), + ("0.2.3a1.post2", False), + ("0.2.3.4a1.post2", False), + ("0.a1.post2.dev0", False), + ("0.2a1.post2.dev0", False), + ("0.2.3a1.post2.dev0", False), + ("0.2.3.4a1.post2.dev0", False), + ], +) +def test_all_major_0_versions_are_treated_as_normal_versions( + version: str, expected: bool +) -> None: + subject = PEP440Version.parse(version) + + assert subject.is_stable() == expected + assert subject.is_unstable() == (not expected) + + @pytest.mark.parametrize( "version, expected", [