From 8afbb113cee5059630879da7ad6339bf5e1c789e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Fri, 9 Oct 2026 15:45:07 -0700 Subject: [PATCH 1/4] feat(python): default to Python 3.15 --- .github/workflows/check.yaml | 11 ++-- .github/workflows/prepare-release.yaml | 2 +- .github/workflows/release.yaml | 2 +- .pre-commit-config.yaml | 3 +- .readthedocs.yaml | 2 +- .readthedocs.yml | 2 +- README.md | 2 +- docs/conf.py | 6 +- docs/explanation.rst | 2 +- pyproject.toml | 8 +-- src/python_discovery/_discovery.py | 11 ++-- src/python_discovery/_py_info.py | 5 +- src/python_discovery/_py_spec.py | 4 +- src/python_discovery/_specifier.py | 12 +--- tests/py_info/test_py_info.py | 2 +- tests/test_py_info_extra.py | 2 +- tests/windows/conftest.py | 3 +- tox.toml | 82 ++++++++++++++------------ 18 files changed, 78 insertions(+), 83 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 53b5156..5e799ad 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -25,11 +25,8 @@ jobs: - "3.13" - "3.12" - "3.11" - - "3.10" - - "3.9" - - "3.8" - - type-3.8 - - type-3.14 + - type-3.11 + - type-3.15 - dev - pkg_meta steps: @@ -44,9 +41,9 @@ jobs: cache-dependency-glob: "pyproject.toml" github-token: ${{ secrets.GITHUB_TOKEN }} - name: Install tox - run: uv tool install --python-preference only-managed --python 3.14 tox --with tox-uv + run: uv tool install --python-preference only-managed --python 3.15 tox --with tox-uv - name: Install Python - if: startsWith(matrix.env, '3.') && matrix.env != '3.14' + if: startsWith(matrix.env, '3.') && matrix.env != '3.15' run: uv python install --python-preference only-managed ${{ matrix.env }} - name: Setup test suite run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.env }} diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index 16fcbaf..6bf11bd 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -79,7 +79,7 @@ jobs: echo "Next version: $next" echo "version=$next" >> $GITHUB_OUTPUT - name: Install tox - run: uv tool install --python-preference only-managed --python 3.14 tox@latest --with tox-uv + run: uv tool install --python-preference only-managed --python 3.15 tox@latest --with tox-uv - name: Run release process run: tox run -e release -- ${STEPS_VERSION_OUTPUTS_VERSION} env: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0e5598e..66e5a3a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,7 +21,7 @@ jobs: cache-dependency-glob: "pyproject.toml" github-token: ${{ secrets.GITHUB_TOKEN }} - name: Build package - run: uv build --python 3.14 --python-preference only-managed --sdist --wheel . --out-dir dist + run: uv build --python 3.15 --python-preference only-managed --sdist --wheel . --out-dir dist - name: Store the distribution packages uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ab9eab3..643157a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,5 @@ +default_language_version: + python: python3.15 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 @@ -13,7 +15,6 @@ repos: rev: v2.4.3 hooks: - id: codespell - additional_dependencies: ["tomli>=2.4.1"] - repo: https://github.com/tox-dev/pyproject-fmt rev: "v2.30.1" hooks: diff --git a/.readthedocs.yaml b/.readthedocs.yaml index a6066a8..29b0b05 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,5 +4,5 @@ build: tools: {} commands: - curl -LsSf https://astral.sh/uv/install.sh | sh - - ~/.local/bin/uv tool install tox --with tox-uv -p 3.14 --managed-python + - ~/.local/bin/uv tool install tox --with tox-uv -p 3.15 --managed-python - ~/.local/bin/tox run -e docs -- "$READTHEDOCS_OUTPUT/html" diff --git a/.readthedocs.yml b/.readthedocs.yml index e140f35..307bce5 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -2,7 +2,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.12" + python: "3.15" commands: - pip install tox - tox r -e docs -- "${READTHEDOCS_OUTPUT}"/html diff --git a/README.md b/README.md index cdc5964..d74454b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ the right one for you. Give it a requirement like `python3.12` or `>=3.11,<3.13`, and it searches all known locations, verifies each candidate, and returns detailed metadata about the match. Results are cached to disk so repeated lookups are fast. -The library runs on Python 3.8+ and discovers interpreters as old as Python 3.6; anything older is skipped with a +The library runs on Python 3.11+ and discovers interpreters as old as Python 3.6; anything older is skipped with a warning naming the version it found. ## Usage diff --git a/docs/conf.py b/docs/conf.py index 01e7d12..ab001cd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from python_discovery import __version__ @@ -12,7 +12,7 @@ name = "python-discovery" version = ".".join(__version__.split(".")[:2]) release = __version__ -copyright = f"2026-{datetime.now(tz=timezone.utc).year}, {company}" # ruff:ignore[builtin-variable-shadowing] +copyright = f"2026-{datetime.now(tz=UTC).year}, {company}" # ruff:ignore[builtin-variable-shadowing] extensions = [ "sphinx_llm.txt", @@ -50,7 +50,7 @@ html_theme = "furo" html_title = project -html_last_updated_fmt = datetime.now(tz=timezone.utc).isoformat() +html_last_updated_fmt = datetime.now(tz=UTC).isoformat() pygments_dark_style = "monokai" html_show_sourcelink = False html_static_path = ["_static"] diff --git a/docs/explanation.rst b/docs/explanation.rst index 72dfe2f..429668c 100644 --- a/docs/explanation.rst +++ b/docs/explanation.rst @@ -280,7 +280,7 @@ when encountering problematic interpreters. Supported Python versions ------------------------- -Two floors apply: python-discovery itself runs on Python 3.8+ (``requires-python``), while discovery +Two floors apply: python-discovery itself runs on Python 3.11+ (``requires-python``), while discovery reaches down to Python 3.6 - old enough to cover RHEL 8's system Python. python-discovery skips candidates below 3.6 with one warning naming the executable, the version diff --git a/pyproject.toml b/pyproject.toml index f24d954..7e1f11a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,8 +2,7 @@ build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.5", - "hatchling>=1.27; python_version<'3.10'", - "hatchling>=1.28; python_version>='3.10'", + "hatchling>=1.28", ] [project] @@ -19,7 +18,7 @@ license.file = "LICENSE" maintainers = [ { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, ] -requires-python = ">=3.8" +requires-python = ">=3.11" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -28,9 +27,6 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", diff --git a/src/python_discovery/_discovery.py b/src/python_discovery/_discovery.py index 5bde25e..c97e85f 100644 --- a/src/python_discovery/_discovery.py +++ b/src/python_discovery/_discovery.py @@ -11,7 +11,7 @@ from ._compat import fs_path_id from ._py_info import PythonInfo from ._py_spec import PythonSpec -from ._specifier import DC_KW, SimpleVersion +from ._specifier import SimpleVersion if TYPE_CHECKING: from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence @@ -339,7 +339,7 @@ def _parse_uv_install(path: Path) -> _UvInstall: return _UvInstall(path=path, key=key) -@dataclass(**DC_KW) +@dataclass(frozen=True, kw_only=True, slots=True) class _UvKey: """The ``-[+]---`` name uv gives an install directory.""" @@ -349,7 +349,7 @@ class _UvKey: debug: bool -@dataclass(**DC_KW) +@dataclass(frozen=True, kw_only=True, slots=True) class _UvInstall: path: Path key: _UvKey | None @@ -377,7 +377,10 @@ def _uv_version_matches(key: _UvKey, spec: PythonSpec) -> bool: return True if spec.version_specifier is not None and not spec.version_specifier.contains(str(key.version)): return False - return all(req is None or our == req for our, req in zip(key.version.release, (spec.major, spec.minor, spec.micro))) + return all( + req is None or our == req + for our, req in zip(key.version.release, (spec.major, spec.minor, spec.micro), strict=False) + ) def _uv_sort_key(install: _UvInstall) -> tuple[int, ...]: diff --git a/src/python_discovery/_py_info.py b/src/python_discovery/_py_info.py index cfc2b94..22bd46e 100644 --- a/src/python_discovery/_py_info.py +++ b/src/python_discovery/_py_info.py @@ -16,12 +16,11 @@ if TYPE_CHECKING: from collections.abc import Generator, Mapping - from typing import Union from ._cache import PyInfoCache from ._py_spec import PythonSpec - InfoValue = Union[str, int, bool, None, "tuple[int | str, ...]", "list[str]", "dict[str, str | int | None]"] + InfoValue = str | int | bool | None | tuple[int | str, ...] | list[str] | dict[str, str | int | None] class VersionInfo(NamedTuple): @@ -272,7 +271,7 @@ def satisfies(self, spec: PythonSpec, *, impl_must_match: bool) -> bool: # ruff return False return all( req is None or our is None or our == req - for our, req in zip(self.version_info[0:3], (spec.major, spec.minor, spec.micro)) + for our, req in zip(self.version_info[0:3], (spec.major, spec.minor, spec.micro), strict=False) ) def _satisfies_path(self, spec: PythonSpec) -> bool: diff --git a/src/python_discovery/_py_spec.py b/src/python_discovery/_py_spec.py index d389ad6..8f9ff22 100644 --- a/src/python_discovery/_py_spec.py +++ b/src/python_discovery/_py_spec.py @@ -257,7 +257,9 @@ def satisfies(self, spec: PythonSpec) -> bool: # ruff:ignore[too-many-return-st return False return all( req is None or our is None or our == req - for our, req in zip((self.major, self.minor, self.micro), (spec.major, spec.minor, spec.micro)) + for our, req in zip( + (self.major, self.minor, self.micro), (spec.major, spec.minor, spec.micro), strict=False + ) ) def __repr__(self) -> str: diff --git a/src/python_discovery/_specifier.py b/src/python_discovery/_specifier.py index 2607631..199e93e 100644 --- a/src/python_discovery/_specifier.py +++ b/src/python_discovery/_specifier.py @@ -5,14 +5,9 @@ import contextlib import operator import re -import sys from dataclasses import dataclass from typing import TYPE_CHECKING, Final -DC_KW: Final[dict[str, bool]] = ( - {"frozen": True, "kw_only": True, "slots": True} if sys.version_info >= (3, 10) else {"frozen": True} -) - if TYPE_CHECKING: from collections.abc import Iterator @@ -40,7 +35,7 @@ _PRE_ORDER: Final[dict[str, int]] = {"a": 1, "b": 2, "rc": 3} -@dataclass(**DC_KW) +@dataclass(frozen=True, kw_only=True, slots=True) class SimpleVersion: """ Simple PEP 440-like version parser using only standard library. @@ -127,7 +122,7 @@ def __repr__(self) -> str: return f"SimpleVersion('{self.version_str}')" -@dataclass(**DC_KW) +@dataclass(frozen=True, kw_only=True, slots=True) class SimpleSpecifier: """ Simple PEP 440-like version specifier using only standard library. @@ -249,7 +244,7 @@ def __repr__(self) -> str: return f"SimpleSpecifier('{self.spec_str}')" -@dataclass(**DC_KW) +@dataclass(frozen=True, kw_only=True, slots=True) class SimpleSpecifierSet: """ Simple PEP 440-like specifier set using only standard library. @@ -307,7 +302,6 @@ def __repr__(self) -> str: __all__ = [ - "DC_KW", "SimpleSpecifier", "SimpleSpecifierSet", "SimpleVersion", diff --git a/tests/py_info/test_py_info.py b/tests/py_info/test_py_info.py index ec06108..f9d1ed6 100644 --- a/tests/py_info/test_py_info.py +++ b/tests/py_info/test_py_info.py @@ -507,7 +507,7 @@ def test_select_most_likely_prefers_machine_match( target = copy.deepcopy(CURRENT) target.sysconfig_platform = target_platform discovered = [copy.deepcopy(CURRENT) for _ in discovered_platforms] - for d, plat in zip(discovered, discovered_platforms): + for d, plat in zip(discovered, discovered_platforms, strict=False): d.sysconfig_platform = plat result = PythonInfo._select_most_likely(discovered, target) assert result.sysconfig_platform == discovered_platforms[expected_idx] diff --git a/tests/test_py_info_extra.py b/tests/test_py_info_extra.py index 5d6f08f..4995b86 100644 --- a/tests/test_py_info_extra.py +++ b/tests/test_py_info_extra.py @@ -225,7 +225,7 @@ def test_satisfies_path_not_abs_basename_match() -> None: # stem drops everything from the first dot, so python3.15t arrives as python3 and stops naming a free-threaded # build; only the Windows .exe belongs off the end. name = Path(info.original_executable).name - basename = name[: -len(".exe")] if name.endswith(".exe") else name + basename = name.removesuffix(".exe") spec = PythonSpec.from_string_spec(basename) assert info.satisfies(spec, impl_must_match=False) is True diff --git a/tests/windows/conftest.py b/tests/windows/conftest.py index 3df8148..69913e7 100644 --- a/tests/windows/conftest.py +++ b/tests/windows/conftest.py @@ -12,8 +12,7 @@ if TYPE_CHECKING: from collections.abc import Generator - - from typing_extensions import Self + from typing import Self def _create_winreg_mock() -> ModuleType: diff --git a/tox.toml b/tox.toml index 18cabae..166173a 100644 --- a/tox.toml +++ b/tox.toml @@ -1,52 +1,49 @@ -requires = ["tox>=4.38", "tox-uv>=1.29"] -env_list = ["fix", "3.15", "3.15t", "3.14", "3.13", "3.12", "3.11", "3.10", "3.9", "3.8", "type-3.8", "type-3.14", "docs", "pkg_meta"] +requires = [ "tox>=4.64.7", "tox-uv>=1.29" ] +env_list = [ "3.15", "type-3.15", "3.14", "3.13", "3.12", "3.11", "type-3.11", "3.15t", "docs", "fix", "pkg_meta" ] skip_missing_interpreters = true [env_run_base] description = "run the tests with pytest" +default_base_python = [ "3.15" ] package = "wheel" wheel_build_env = ".pkg" -dependency_groups = ["test"] -pass_env = ["DIFF_AGAINST", "PYTEST_*"] +dependency_groups = [ "test" ] +pass_env = [ "DIFF_AGAINST", "PYTEST_*" ] set_env.COVERAGE_FILE = "{work_dir}{/}.coverage.{env_name}" commands = [ - ["coverage", "erase"], + [ "coverage", "erase" ], [ "coverage", "run", "-m", "pytest", - { replace = "posargs", extend = true, default = [ + { replace = "posargs", default = [ "--junitxml", "{work_dir}{/}junit.{env_name}.xml", "tests", - ] }, + ], extend = true }, ], - ["coverage", "combine"], - ["coverage", "report"], - ["coverage", "html", "-d", "{env_tmp_dir}{/}htmlcov"], + [ "coverage", "combine" ], + [ "coverage", "report" ], + [ "coverage", "html", "-d", "{env_tmp_dir}{/}htmlcov" ], ] -[env.fix] -description = "run static analysis and style check using flake8" -skip_install = true -deps = ["pre-commit-uv>=4.2.1"] -pass_env = ["HOMEPATH", "PROGRAMDATA"] -commands = [["pre-commit", "run", "--all-files", "--show-diff-on-failure"]] +[env_pkg_base] +default_base_python = [ "3.15" ] -[env."type-3.8"] -description = "run type check on code base (3.8)" -deps = ["gitpython>=3.2", "packaging>=26.2", "ty==0.0.84"] -commands = [["ty", "check", "--output-format", "concise", "--error-on-warning", "--python-version", "3.8", "."]] +[env."type-3.15"] +description = "run type check on code base (3.15)" +deps = [ "gitpython>=3.2", "packaging>=26.2", "ty==0.0.84" ] +commands = [ [ "ty", "check", "--output-format", "concise", "--error-on-warning", "--python-version", "3.15", "." ] ] -[env."type-3.14"] -description = "run type check on code base (3.14)" -deps = ["gitpython>=3.2", "packaging>=26.2", "ty==0.0.84"] -commands = [["ty", "check", "--output-format", "concise", "--error-on-warning", "--python-version", "3.14", "."]] +[env."type-3.11"] +description = "run type check on code base (3.11)" +deps = [ "gitpython>=3.2", "packaging>=26.2", "ty==0.0.84" ] +commands = [ [ "ty", "check", "--output-format", "concise", "--error-on-warning", "--python-version", "3.11", "." ] ] [env.docs] description = "build documentation" -dependency_groups = ["docs"] +dependency_groups = [ "docs" ] commands = [ [ "sphinx-build", @@ -57,29 +54,36 @@ commands = [ "-b", "html", "-W", - { replace = "posargs", extend = true, default = ["{work_dir}{/}docs_out"] }, + { replace = "posargs", default = [ "{work_dir}{/}docs_out" ], extend = true }, ], ] +[env.fix] +description = "run static analysis and style check using flake8" +skip_install = true +deps = [ "pre-commit-uv>=4.2.1" ] +pass_env = [ "HOMEPATH", "PROGRAMDATA" ] +commands = [ [ "pre-commit", "run", "--all-files", "--show-diff-on-failure" ] ] + [env.pkg_meta] description = "check that the long description is valid" skip_install = true -deps = ["check-wheel-contents>=0.6.3", "twine>=6.2", "uv>=0.12.22"] +deps = [ "check-wheel-contents>=0.6.3", "twine>=6.2", "uv>=0.12.22" ] commands = [ - ["uv", "build", "--sdist", "--wheel", "--out-dir", "{env_tmp_dir}", "."], - ["twine", "check", "{env_tmp_dir}{/}*"], - ["check-wheel-contents", "--no-config", "{env_tmp_dir}"], + [ "uv", "build", "--sdist", "--wheel", "--out-dir", "{env_tmp_dir}", "." ], + [ "twine", "check", "{env_tmp_dir}{/}*" ], + [ "check-wheel-contents", "--no-config", "{env_tmp_dir}" ], ] -[env.release] -description = "do a release, required posargs of the version number" -skip_install = true -dependency_groups = ["release"] -pass_env = ["GH_TOKEN"] -commands = [["python", "{tox_root}/tasks/release.py", "--version", "{posargs}"]] - [env.dev] description = "generate a DEV environment" package = "editable" -dependency_groups = ["test"] -commands = [["uv", "pip", "tree"], ["python", "-c", "import sys; print(sys.executable)"]] +dependency_groups = [ "test" ] +commands = [ [ "uv", "pip", "tree" ], [ "python", "-c", "import sys; print(sys.executable)" ] ] + +[env.release] +description = "do a release, required posargs of the version number" +skip_install = true +dependency_groups = [ "release" ] +pass_env = [ "GH_TOKEN" ] +commands = [ [ "python", "{tox_root}/tasks/release.py", "--version", "{posargs}" ] ] From 76cc59876876e25e8c846aa9839de8bb8ecddc27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Fri, 9 Oct 2026 15:56:27 -0700 Subject: [PATCH 2/4] fix(docs): install Python 3.15 with uv --- .readthedocs.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 307bce5..ca8bcc1 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,8 +1,8 @@ version: 2 build: os: ubuntu-22.04 - tools: - python: "3.15" + tools: {} commands: - - pip install tox - - tox r -e docs -- "${READTHEDOCS_OUTPUT}"/html + - curl -LsSf https://astral.sh/uv/install.sh | sh + - ~/.local/bin/uv tool install tox --with tox-uv -p 3.15 --managed-python + - ~/.local/bin/tox run -e docs -- "${READTHEDOCS_OUTPUT}"/html From 55a0e727a364d923160456277025d5b43954540d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Fri, 9 Oct 2026 16:14:59 -0700 Subject: [PATCH 3/4] feat(ci): default uv to Python 3.15 --- .github/workflows/check.yaml | 2 ++ .github/workflows/prepare-release.yaml | 1 + .github/workflows/release.yaml | 1 + 3 files changed, 4 insertions(+) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 5e799ad..b1bc350 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -37,6 +37,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: + python-version: "3.15" enable-cache: false cache-dependency-glob: "pyproject.toml" github-token: ${{ secrets.GITHUB_TOKEN }} @@ -62,6 +63,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: + python-version: "3.15" enable-cache: false cache-dependency-glob: "pyproject.toml" github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index 6bf11bd..adfa2e3 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -31,6 +31,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: + python-version: "3.15" enable-cache: true cache-dependency-glob: "pyproject.toml" github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 66e5a3a..8cf4248 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -17,6 +17,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: + python-version: "3.15" enable-cache: false cache-dependency-glob: "pyproject.toml" github-token: ${{ secrets.GITHUB_TOKEN }} From 301ae0b9ebc0b3f249b7121d0d4744917be28a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Fri, 9 Oct 2026 17:36:29 -0700 Subject: [PATCH 4/4] fix(python): match virtualenv support versions --- .github/workflows/check.yaml | 4 +++- README.md | 2 +- docs/conf.py | 6 +++--- docs/explanation.rst | 2 +- pyproject.toml | 9 +++++++-- src/python_discovery/_discovery.py | 11 ++++------- src/python_discovery/_py_info.py | 5 +++-- src/python_discovery/_py_spec.py | 4 +--- src/python_discovery/_specifier.py | 12 +++++++++--- tests/py_info/test_py_info.py | 2 +- tests/windows/conftest.py | 3 ++- tox.toml | 8 ++++---- 12 files changed, 39 insertions(+), 29 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index b1bc350..ad7df4f 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -25,7 +25,9 @@ jobs: - "3.13" - "3.12" - "3.11" - - type-3.11 + - "3.10" + - "3.9" + - type-3.9 - type-3.15 - dev - pkg_meta diff --git a/README.md b/README.md index d74454b..d9e8f5a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ the right one for you. Give it a requirement like `python3.12` or `>=3.11,<3.13`, and it searches all known locations, verifies each candidate, and returns detailed metadata about the match. Results are cached to disk so repeated lookups are fast. -The library runs on Python 3.11+ and discovers interpreters as old as Python 3.6; anything older is skipped with a +The library runs on Python 3.9+ and discovers interpreters as old as Python 3.6; anything older is skipped with a warning naming the version it found. ## Usage diff --git a/docs/conf.py b/docs/conf.py index ab001cd..01e7d12 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from datetime import UTC, datetime +from datetime import datetime, timezone from pathlib import Path from python_discovery import __version__ @@ -12,7 +12,7 @@ name = "python-discovery" version = ".".join(__version__.split(".")[:2]) release = __version__ -copyright = f"2026-{datetime.now(tz=UTC).year}, {company}" # ruff:ignore[builtin-variable-shadowing] +copyright = f"2026-{datetime.now(tz=timezone.utc).year}, {company}" # ruff:ignore[builtin-variable-shadowing] extensions = [ "sphinx_llm.txt", @@ -50,7 +50,7 @@ html_theme = "furo" html_title = project -html_last_updated_fmt = datetime.now(tz=UTC).isoformat() +html_last_updated_fmt = datetime.now(tz=timezone.utc).isoformat() pygments_dark_style = "monokai" html_show_sourcelink = False html_static_path = ["_static"] diff --git a/docs/explanation.rst b/docs/explanation.rst index 429668c..03c20fd 100644 --- a/docs/explanation.rst +++ b/docs/explanation.rst @@ -280,7 +280,7 @@ when encountering problematic interpreters. Supported Python versions ------------------------- -Two floors apply: python-discovery itself runs on Python 3.11+ (``requires-python``), while discovery +Two floors apply: python-discovery itself runs on Python 3.9+ (``requires-python``), while discovery reaches down to Python 3.6 - old enough to cover RHEL 8's system Python. python-discovery skips candidates below 3.6 with one warning naming the executable, the version diff --git a/pyproject.toml b/pyproject.toml index 7e1f11a..8b504da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,8 @@ build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.5", - "hatchling>=1.28", + "hatchling>=1.27; python_version<'3.10'", + "hatchling>=1.28; python_version>='3.10'", ] [project] @@ -18,7 +19,7 @@ license.file = "LICENSE" maintainers = [ { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, ] -requires-python = ">=3.11" +requires-python = ">=3.9" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -27,6 +28,8 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -55,6 +58,7 @@ test = [ "pytest>=8.3.5", "pytest-mock>=3.14.1", "setuptools>=75.3.4", + "typing-extensions>=4.12", "vermin>=1.8", ] docs = [ @@ -77,6 +81,7 @@ release = [ version.source = "vcs" [tool.ruff] +target-version = "py39" line-length = 120 format.preview = true format.docstring-code-line-length = 100 diff --git a/src/python_discovery/_discovery.py b/src/python_discovery/_discovery.py index c97e85f..5bde25e 100644 --- a/src/python_discovery/_discovery.py +++ b/src/python_discovery/_discovery.py @@ -11,7 +11,7 @@ from ._compat import fs_path_id from ._py_info import PythonInfo from ._py_spec import PythonSpec -from ._specifier import SimpleVersion +from ._specifier import DC_KW, SimpleVersion if TYPE_CHECKING: from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence @@ -339,7 +339,7 @@ def _parse_uv_install(path: Path) -> _UvInstall: return _UvInstall(path=path, key=key) -@dataclass(frozen=True, kw_only=True, slots=True) +@dataclass(**DC_KW) class _UvKey: """The ``-[+]---`` name uv gives an install directory.""" @@ -349,7 +349,7 @@ class _UvKey: debug: bool -@dataclass(frozen=True, kw_only=True, slots=True) +@dataclass(**DC_KW) class _UvInstall: path: Path key: _UvKey | None @@ -377,10 +377,7 @@ def _uv_version_matches(key: _UvKey, spec: PythonSpec) -> bool: return True if spec.version_specifier is not None and not spec.version_specifier.contains(str(key.version)): return False - return all( - req is None or our == req - for our, req in zip(key.version.release, (spec.major, spec.minor, spec.micro), strict=False) - ) + return all(req is None or our == req for our, req in zip(key.version.release, (spec.major, spec.minor, spec.micro))) def _uv_sort_key(install: _UvInstall) -> tuple[int, ...]: diff --git a/src/python_discovery/_py_info.py b/src/python_discovery/_py_info.py index 22bd46e..cfc2b94 100644 --- a/src/python_discovery/_py_info.py +++ b/src/python_discovery/_py_info.py @@ -16,11 +16,12 @@ if TYPE_CHECKING: from collections.abc import Generator, Mapping + from typing import Union from ._cache import PyInfoCache from ._py_spec import PythonSpec - InfoValue = str | int | bool | None | tuple[int | str, ...] | list[str] | dict[str, str | int | None] + InfoValue = Union[str, int, bool, None, "tuple[int | str, ...]", "list[str]", "dict[str, str | int | None]"] class VersionInfo(NamedTuple): @@ -271,7 +272,7 @@ def satisfies(self, spec: PythonSpec, *, impl_must_match: bool) -> bool: # ruff return False return all( req is None or our is None or our == req - for our, req in zip(self.version_info[0:3], (spec.major, spec.minor, spec.micro), strict=False) + for our, req in zip(self.version_info[0:3], (spec.major, spec.minor, spec.micro)) ) def _satisfies_path(self, spec: PythonSpec) -> bool: diff --git a/src/python_discovery/_py_spec.py b/src/python_discovery/_py_spec.py index 8f9ff22..d389ad6 100644 --- a/src/python_discovery/_py_spec.py +++ b/src/python_discovery/_py_spec.py @@ -257,9 +257,7 @@ def satisfies(self, spec: PythonSpec) -> bool: # ruff:ignore[too-many-return-st return False return all( req is None or our is None or our == req - for our, req in zip( - (self.major, self.minor, self.micro), (spec.major, spec.minor, spec.micro), strict=False - ) + for our, req in zip((self.major, self.minor, self.micro), (spec.major, spec.minor, spec.micro)) ) def __repr__(self) -> str: diff --git a/src/python_discovery/_specifier.py b/src/python_discovery/_specifier.py index 199e93e..2607631 100644 --- a/src/python_discovery/_specifier.py +++ b/src/python_discovery/_specifier.py @@ -5,9 +5,14 @@ import contextlib import operator import re +import sys from dataclasses import dataclass from typing import TYPE_CHECKING, Final +DC_KW: Final[dict[str, bool]] = ( + {"frozen": True, "kw_only": True, "slots": True} if sys.version_info >= (3, 10) else {"frozen": True} +) + if TYPE_CHECKING: from collections.abc import Iterator @@ -35,7 +40,7 @@ _PRE_ORDER: Final[dict[str, int]] = {"a": 1, "b": 2, "rc": 3} -@dataclass(frozen=True, kw_only=True, slots=True) +@dataclass(**DC_KW) class SimpleVersion: """ Simple PEP 440-like version parser using only standard library. @@ -122,7 +127,7 @@ def __repr__(self) -> str: return f"SimpleVersion('{self.version_str}')" -@dataclass(frozen=True, kw_only=True, slots=True) +@dataclass(**DC_KW) class SimpleSpecifier: """ Simple PEP 440-like version specifier using only standard library. @@ -244,7 +249,7 @@ def __repr__(self) -> str: return f"SimpleSpecifier('{self.spec_str}')" -@dataclass(frozen=True, kw_only=True, slots=True) +@dataclass(**DC_KW) class SimpleSpecifierSet: """ Simple PEP 440-like specifier set using only standard library. @@ -302,6 +307,7 @@ def __repr__(self) -> str: __all__ = [ + "DC_KW", "SimpleSpecifier", "SimpleSpecifierSet", "SimpleVersion", diff --git a/tests/py_info/test_py_info.py b/tests/py_info/test_py_info.py index f9d1ed6..ec06108 100644 --- a/tests/py_info/test_py_info.py +++ b/tests/py_info/test_py_info.py @@ -507,7 +507,7 @@ def test_select_most_likely_prefers_machine_match( target = copy.deepcopy(CURRENT) target.sysconfig_platform = target_platform discovered = [copy.deepcopy(CURRENT) for _ in discovered_platforms] - for d, plat in zip(discovered, discovered_platforms, strict=False): + for d, plat in zip(discovered, discovered_platforms): d.sysconfig_platform = plat result = PythonInfo._select_most_likely(discovered, target) assert result.sysconfig_platform == discovered_platforms[expected_idx] diff --git a/tests/windows/conftest.py b/tests/windows/conftest.py index 69913e7..3df8148 100644 --- a/tests/windows/conftest.py +++ b/tests/windows/conftest.py @@ -12,7 +12,8 @@ if TYPE_CHECKING: from collections.abc import Generator - from typing import Self + + from typing_extensions import Self def _create_winreg_mock() -> ModuleType: diff --git a/tox.toml b/tox.toml index 166173a..e104865 100644 --- a/tox.toml +++ b/tox.toml @@ -1,5 +1,5 @@ requires = [ "tox>=4.64.7", "tox-uv>=1.29" ] -env_list = [ "3.15", "type-3.15", "3.14", "3.13", "3.12", "3.11", "type-3.11", "3.15t", "docs", "fix", "pkg_meta" ] +env_list = [ "3.15", "type-3.15", "3.14", "3.13", "3.12", "3.11", "3.10", "3.9", "type-3.9", "3.15t", "docs", "fix", "pkg_meta" ] skip_missing_interpreters = true [env_run_base] @@ -36,10 +36,10 @@ description = "run type check on code base (3.15)" deps = [ "gitpython>=3.2", "packaging>=26.2", "ty==0.0.84" ] commands = [ [ "ty", "check", "--output-format", "concise", "--error-on-warning", "--python-version", "3.15", "." ] ] -[env."type-3.11"] -description = "run type check on code base (3.11)" +[env."type-3.9"] +description = "run type check on code base (3.9)" deps = [ "gitpython>=3.2", "packaging>=26.2", "ty==0.0.84" ] -commands = [ [ "ty", "check", "--output-format", "concise", "--error-on-warning", "--python-version", "3.11", "." ] ] +commands = [ [ "ty", "check", "--output-format", "concise", "--error-on-warning", "--python-version", "3.9", "." ] ] [env.docs] description = "build documentation"