From 53a6dfb72702f9ad0427bb55d57893c53508ed8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 16:54:10 -0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20fix(spec):=20exclude=20bound?= =?UTF-8?q?ary=20prereleases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/python_discovery/_specifier.py | 8 ++++++++ tests/test_py_info_extra.py | 1 + tests/test_specifier.py | 5 +++++ 3 files changed, 14 insertions(+) diff --git a/src/python_discovery/_specifier.py b/src/python_discovery/_specifier.py index f9f05ea..89ce8cd 100644 --- a/src/python_discovery/_specifier.py +++ b/src/python_discovery/_specifier.py @@ -210,6 +210,14 @@ def _check_standard(self, candidate: SimpleVersion) -> bool: return str(candidate) == str(self.version) if self.operator == "~=": return self._check_compatible_release(candidate) + # PEP 440 excludes prereleases of a final release named by an exclusive upper bound. + if ( + self.operator == "<" + and candidate.pre_type is not None + and self.version.pre_type is None + and candidate.release == self.version.release + ): + return False cmp_ops = { "==": operator.eq, "!=": operator.ne, diff --git a/tests/test_py_info_extra.py b/tests/test_py_info_extra.py index 016636a..f48b7d2 100644 --- a/tests/test_py_info_extra.py +++ b/tests/test_py_info_extra.py @@ -239,6 +239,7 @@ def test_satisfies_version_specifier_unparsable_version() -> None: pytest.param(VersionInfo(3, 15, 0, "alpha", 6), ">=3.15.0", False, id="prerelease_not_match_full_precision"), pytest.param(VersionInfo(3, 15, 0, "alpha", 5), "<3.15.0a6", True, id="earlier_prerelease_less_than"), pytest.param(VersionInfo(3, 15, 0, "alpha", 6), "<3.15.0a6", False, id="prerelease_not_less_than_itself"), + pytest.param(VersionInfo(3, 12, 0, "candidate", 1), "<3.12.0", False, id="prerelease_not_below_final_bound"), pytest.param(VersionInfo(3, 15, 0, "alpha", 6), ">=3.15.0a6", True, id="prerelease_match_itself"), pytest.param(VersionInfo(3, 15, 0, "alpha", 6), ">=3.15.0a7", False, id="prerelease_not_match_later"), pytest.param(VersionInfo(3, 15, 0, "final", 0), ">=3.15.0a6", True, id="final_greater_than_prerelease"), diff --git a/tests/test_specifier.py b/tests/test_specifier.py index 2c3d218..dca3024 100644 --- a/tests/test_specifier.py +++ b/tests/test_specifier.py @@ -139,6 +139,11 @@ def test_specifier_parse_wildcard() -> None: pytest.param(">3.12", "3.12.0", False, id="gt-equal"), pytest.param("<3.12", "3.11.0", True, id="lt-below"), pytest.param("<3.12", "3.12.0", False, id="lt-equal"), + pytest.param("<3.12", "3.12.0rc1", False, id="lt-boundary-prerelease-two-part"), + pytest.param("<3.12.0", "3.12.0rc1", False, id="lt-boundary-prerelease-three-part"), + pytest.param("<3.12.0rc2", "3.12.0rc1", True, id="lt-explicit-prerelease-bound"), + pytest.param("<3.12.0", "3.11.9rc1", True, id="lt-lower-release-prerelease"), + pytest.param("<=3.12.0", "3.12.0rc1", True, id="lte-boundary-prerelease"), pytest.param("==3.12.0", "3.12.0", True, id="eq-match"), pytest.param("==3.12.0", "3.12.1", False, id="eq-no-match"), pytest.param("!=3.12.0", "3.12.0", False, id="ne-match"), From 29b20dc5761e33b3547f468b3beb39dd611e36d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 16:55:31 -0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20docs(news):=20record=20upper?= =?UTF-8?q?-bound=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog/217.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/changelog/217.bugfix.rst diff --git a/docs/changelog/217.bugfix.rst b/docs/changelog/217.bugfix.rst new file mode 100644 index 0000000..2d8251e --- /dev/null +++ b/docs/changelog/217.bugfix.rst @@ -0,0 +1 @@ +Exclude prereleases of the final version named by an exclusive upper bound, such as ``<3.12.0`` rejecting ``3.12.0rc1``.