python-discovery 1.4.4__tar.gz → 1.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {python_discovery-1.4.4 → python_discovery-1.5.0}/.github/workflows/check.yaml +3 -4
  2. {python_discovery-1.4.4 → python_discovery-1.5.0}/.github/workflows/prepare-release.yaml +1 -10
  3. {python_discovery-1.4.4 → python_discovery-1.5.0}/.github/workflows/release.yaml +1 -5
  4. {python_discovery-1.4.4 → python_discovery-1.5.0}/.pre-commit-config.yaml +16 -9
  5. {python_discovery-1.4.4 → python_discovery-1.5.0}/PKG-INFO +2 -14
  6. {python_discovery-1.4.4 → python_discovery-1.5.0}/docs/changelog.rst +8 -0
  7. {python_discovery-1.4.4 → python_discovery-1.5.0}/docs/conf.py +1 -1
  8. {python_discovery-1.4.4 → python_discovery-1.5.0}/pyproject.toml +17 -16
  9. {python_discovery-1.4.4 → python_discovery-1.5.0}/src/python_discovery/_cache.py +5 -5
  10. {python_discovery-1.4.4 → python_discovery-1.5.0}/src/python_discovery/_cached_py_info.py +4 -4
  11. {python_discovery-1.4.4 → python_discovery-1.5.0}/src/python_discovery/_discovery.py +2 -2
  12. {python_discovery-1.4.4 → python_discovery-1.5.0}/src/python_discovery/_py_info.py +17 -14
  13. {python_discovery-1.4.4 → python_discovery-1.5.0}/src/python_discovery/_py_spec.py +3 -3
  14. {python_discovery-1.4.4 → python_discovery-1.5.0}/src/python_discovery/_specifier.py +2 -2
  15. {python_discovery-1.4.4 → python_discovery-1.5.0}/src/python_discovery/_windows/_pep514.py +11 -11
  16. {python_discovery-1.4.4 → python_discovery-1.5.0}/tasks/release.py +35 -35
  17. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/test_discovery.py +2 -1
  18. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/test_py_info_extra.py +4 -1
  19. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/test_specifier.py +5 -5
  20. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/windows/conftest.py +1 -1
  21. {python_discovery-1.4.4 → python_discovery-1.5.0}/tox.toml +4 -4
  22. {python_discovery-1.4.4 → python_discovery-1.5.0}/.github/FUNDING.yaml +0 -0
  23. {python_discovery-1.4.4 → python_discovery-1.5.0}/.github/SECURITY.md +0 -0
  24. {python_discovery-1.4.4 → python_discovery-1.5.0}/.github/dependabot.yaml +0 -0
  25. {python_discovery-1.4.4 → python_discovery-1.5.0}/.github/release.yaml +0 -0
  26. {python_discovery-1.4.4 → python_discovery-1.5.0}/.gitignore +0 -0
  27. {python_discovery-1.4.4 → python_discovery-1.5.0}/.readthedocs.yaml +0 -0
  28. {python_discovery-1.4.4 → python_discovery-1.5.0}/.readthedocs.yml +0 -0
  29. {python_discovery-1.4.4 → python_discovery-1.5.0}/CODE_OF_CONDUCT.md +0 -0
  30. {python_discovery-1.4.4 → python_discovery-1.5.0}/LICENSE +0 -0
  31. {python_discovery-1.4.4 → python_discovery-1.5.0}/README.md +0 -0
  32. {python_discovery-1.4.4 → python_discovery-1.5.0}/docs/_static/custom.css +0 -0
  33. {python_discovery-1.4.4 → python_discovery-1.5.0}/docs/_static/logo.svg +0 -0
  34. {python_discovery-1.4.4 → python_discovery-1.5.0}/docs/changelog/template.jinja2 +0 -0
  35. {python_discovery-1.4.4 → python_discovery-1.5.0}/docs/explanation.rst +0 -0
  36. {python_discovery-1.4.4 → python_discovery-1.5.0}/docs/how-to/standalone-usage.rst +0 -0
  37. {python_discovery-1.4.4 → python_discovery-1.5.0}/docs/index.rst +0 -0
  38. {python_discovery-1.4.4 → python_discovery-1.5.0}/docs/reference/api.rst +0 -0
  39. {python_discovery-1.4.4 → python_discovery-1.5.0}/docs/reference/environment-variables.rst +0 -0
  40. {python_discovery-1.4.4 → python_discovery-1.5.0}/docs/tutorial/getting-started.rst +0 -0
  41. {python_discovery-1.4.4 → python_discovery-1.5.0}/src/python_discovery/__init__.py +0 -0
  42. {python_discovery-1.4.4 → python_discovery-1.5.0}/src/python_discovery/_compat.py +0 -0
  43. {python_discovery-1.4.4 → python_discovery-1.5.0}/src/python_discovery/_windows/__init__.py +0 -0
  44. {python_discovery-1.4.4 → python_discovery-1.5.0}/src/python_discovery/_windows/_propose.py +0 -0
  45. {python_discovery-1.4.4 → python_discovery-1.5.0}/src/python_discovery/py.typed +0 -0
  46. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/conftest.py +0 -0
  47. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/py_info/test_py_info.py +0 -0
  48. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/py_info/test_py_info_exe_based_of.py +0 -0
  49. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/test_cache.py +0 -0
  50. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/test_cached_py_info.py +0 -0
  51. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/test_discovery_extra.py +0 -0
  52. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/test_iter_interpreters.py +0 -0
  53. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/test_py_spec.py +0 -0
  54. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/test_py_spec_extra.py +0 -0
  55. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/windows/test_windows.py +0 -0
  56. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/windows/test_windows_pep514.py +0 -0
  57. {python_discovery-1.4.4 → python_discovery-1.5.0}/tests/windows/winreg_mock_values.py +0 -0
@@ -7,14 +7,11 @@ on:
7
7
  pull_request:
8
8
  schedule:
9
9
  - cron: "0 8 * * *"
10
-
11
10
  concurrency:
12
11
  group: check-${{ github.ref }}
13
12
  cancel-in-progress: true
14
-
15
13
  permissions:
16
14
  contents: read
17
-
18
15
  jobs:
19
16
  test:
20
17
  runs-on: ubuntu-latest
@@ -22,6 +19,8 @@ jobs:
22
19
  fail-fast: false
23
20
  matrix:
24
21
  env:
22
+ - "3.15"
23
+ - "3.15t"
25
24
  - "3.14"
26
25
  - "3.13"
27
26
  - "3.12"
@@ -39,7 +38,7 @@ jobs:
39
38
  fetch-depth: 0
40
39
  persist-credentials: false
41
40
  - name: Install the latest version of uv
42
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
41
+ uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
43
42
  with:
44
43
  enable-cache: false
45
44
  cache-dependency-glob: "pyproject.toml"
@@ -12,13 +12,10 @@ on:
12
12
  - minor
13
13
  - patch
14
14
  default: auto
15
-
16
15
  permissions:
17
16
  contents: read
18
-
19
17
  env:
20
18
  FORCE_COLOR: 1
21
-
22
19
  jobs:
23
20
  prepare-release:
24
21
  runs-on: ubuntu-24.04
@@ -31,19 +28,16 @@ jobs:
31
28
  fetch-depth: 0
32
29
  token: ${{ secrets.RELEASE_PAT }}
33
30
  persist-credentials: true
34
-
35
31
  - name: Install the latest version of uv
36
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
32
+ uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
37
33
  with:
38
34
  enable-cache: true
39
35
  cache-dependency-glob: "pyproject.toml"
40
36
  github-token: ${{ secrets.GITHUB_TOKEN }}
41
-
42
37
  - name: Configure git
43
38
  run: |
44
39
  git config user.name "${GITHUB_ACTOR}"
45
40
  git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
46
-
47
41
  - name: Calculate next version
48
42
  id: version
49
43
  env:
@@ -84,16 +78,13 @@ jobs:
84
78
 
85
79
  echo "Next version: $next"
86
80
  echo "version=$next" >> $GITHUB_OUTPUT
87
-
88
81
  - name: Install tox
89
82
  run: uv tool install --python-preference only-managed --python 3.14 tox@latest --with tox-uv
90
-
91
83
  - name: Run release process
92
84
  run: tox run -e release -- ${STEPS_VERSION_OUTPUTS_VERSION}
93
85
  env:
94
86
  GH_TOKEN: ${{ secrets.RELEASE_PAT }}
95
87
  STEPS_VERSION_OUTPUTS_VERSION: ${{ steps.version.outputs.version }}
96
-
97
88
  - name: Display completion message
98
89
  run: echo "Release ${STEPS_VERSION_OUTPUTS_VERSION} prepared and pushed successfully!"
99
90
  env:
@@ -2,13 +2,10 @@ name: Release to PyPI
2
2
  on:
3
3
  push:
4
4
  tags: ["*"]
5
-
6
5
  permissions:
7
6
  contents: read
8
-
9
7
  env:
10
8
  dists-artifact-name: python-package-distributions
11
-
12
9
  jobs:
13
10
  build:
14
11
  runs-on: ubuntu-latest
@@ -18,7 +15,7 @@ jobs:
18
15
  fetch-depth: 0
19
16
  persist-credentials: false
20
17
  - name: Install the latest version of uv
21
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
18
+ uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
22
19
  with:
23
20
  enable-cache: false
24
21
  cache-dependency-glob: "pyproject.toml"
@@ -30,7 +27,6 @@ jobs:
30
27
  with:
31
28
  name: ${{ env.dists-artifact-name }}
32
29
  path: dist/*
33
-
34
30
  release:
35
31
  needs:
36
32
  - build
@@ -10,29 +10,36 @@ repos:
10
10
  - id: check-github-workflows
11
11
  args: ["--verbose"]
12
12
  - repo: https://github.com/codespell-project/codespell
13
- rev: v2.4.2
13
+ rev: v2.4.3
14
14
  hooks:
15
15
  - id: codespell
16
16
  additional_dependencies: ["tomli>=2.4"]
17
17
  - repo: https://github.com/tox-dev/pyproject-fmt
18
- rev: "v2.25.1"
18
+ rev: "v2.25.3"
19
19
  hooks:
20
20
  - id: pyproject-fmt
21
21
  - repo: https://github.com/astral-sh/ruff-pre-commit
22
- rev: "v0.15.20"
22
+ rev: "v0.15.22"
23
23
  hooks:
24
24
  - id: ruff-format
25
25
  - id: ruff
26
26
  args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"]
27
- - repo: https://github.com/rbubley/mirrors-prettier
28
- rev: "v3.9.4"
27
+ - repo: https://github.com/google/yamlfmt
28
+ rev: "v0.21.0"
29
29
  hooks:
30
- - id: prettier
30
+ - id: yamlfmt
31
+ - repo: local
32
+ hooks:
33
+ - id: mdformat
34
+ name: mdformat
35
+ entry: mdformat --wrap 120
36
+ language: python
37
+ types: [markdown]
31
38
  additional_dependencies:
32
- - prettier@3.8.1
33
- - "@prettier/plugin-xml@3.4.2"
39
+ - mdformat-gfm>=1
40
+ - mdformat-ruff>=0.1.3
34
41
  - repo: https://github.com/zizmorcore/zizmor-pre-commit
35
- rev: v1.26.1
42
+ rev: v1.27.0
36
43
  hooks:
37
44
  - id: zizmor
38
45
  - repo: meta
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-discovery
3
- Version: 1.4.4
3
+ Version: 1.5.0
4
4
  Summary: Python interpreter discovery
5
5
  Project-URL: Changelog, https://github.com/tox-dev/python-discovery/releases
6
6
  Project-URL: Documentation, https://python-discovery.readthedocs.io
@@ -42,25 +42,13 @@ Classifier: Programming Language :: Python :: 3.11
42
42
  Classifier: Programming Language :: Python :: 3.12
43
43
  Classifier: Programming Language :: Python :: 3.13
44
44
  Classifier: Programming Language :: Python :: 3.14
45
+ Classifier: Programming Language :: Python :: 3.15
45
46
  Classifier: Programming Language :: Python :: Implementation :: CPython
46
47
  Classifier: Topic :: Software Development :: Libraries
47
48
  Classifier: Topic :: Utilities
48
49
  Requires-Python: >=3.8
49
50
  Requires-Dist: filelock>=3.15.4
50
51
  Requires-Dist: platformdirs<5,>=4.3.6
51
- Provides-Extra: docs
52
- Requires-Dist: furo>=2025.12.19; extra == 'docs'
53
- Requires-Dist: sphinx-autodoc-typehints>=3.6.3; extra == 'docs'
54
- Requires-Dist: sphinx>=9.1; extra == 'docs'
55
- Requires-Dist: sphinxcontrib-mermaid>=2; extra == 'docs'
56
- Requires-Dist: sphinxcontrib-towncrier>=0.4; extra == 'docs'
57
- Requires-Dist: towncrier>=25.8; extra == 'docs'
58
- Provides-Extra: testing
59
- Requires-Dist: covdefaults>=2.3; extra == 'testing'
60
- Requires-Dist: coverage>=7.5.4; extra == 'testing'
61
- Requires-Dist: pytest-mock>=3.14; extra == 'testing'
62
- Requires-Dist: pytest>=8.3.5; extra == 'testing'
63
- Requires-Dist: setuptools>=75.1; extra == 'testing'
64
52
  Description-Content-Type: text/markdown
65
53
 
66
54
  # [`python-discovery`](https://python-discovery.readthedocs.io/en/latest/)
@@ -6,6 +6,14 @@
6
6
 
7
7
  .. towncrier release notes start
8
8
 
9
+ ********************
10
+ v1.5.0 (2026-07-21)
11
+ ********************
12
+
13
+ Packaging updates and notes for downstreams - 1.5.0
14
+ ===================================================
15
+ - Declare support for Python 3.15 and run the test suite against it, currently in beta. (:issue:`104`)
16
+
9
17
  ********************
10
18
  v1.4.4 (2026-07-08)
11
19
  ********************
@@ -11,7 +11,7 @@ company = "tox-dev"
11
11
  name = "python-discovery"
12
12
  version = ".".join(__version__.split(".")[:2])
13
13
  release = __version__
14
- copyright = f"2026-{datetime.now(tz=timezone.utc).year}, {company}" # noqa: A001
14
+ copyright = f"2026-{datetime.now(tz=timezone.utc).year}, {company}" # ruff:ignore[builtin-variable-shadowing]
15
15
 
16
16
  extensions = [
17
17
  "sphinx.ext.autodoc",
@@ -35,6 +35,7 @@ classifiers = [
35
35
  "Programming Language :: Python :: 3.12",
36
36
  "Programming Language :: Python :: 3.13",
37
37
  "Programming Language :: Python :: 3.14",
38
+ "Programming Language :: Python :: 3.15",
38
39
  "Programming Language :: Python :: Implementation :: CPython",
39
40
  "Topic :: Software Development :: Libraries",
40
41
  "Topic :: Utilities",
@@ -46,21 +47,6 @@ dependencies = [
46
47
  "filelock>=3.15.4",
47
48
  "platformdirs<5,>=4.3.6",
48
49
  ]
49
- optional-dependencies.docs = [
50
- "furo>=2025.12.19",
51
- "sphinx>=9.1",
52
- "sphinx-autodoc-typehints>=3.6.3",
53
- "sphinxcontrib-mermaid>=2",
54
- "sphinxcontrib-towncrier>=0.4",
55
- "towncrier>=25.8",
56
- ]
57
- optional-dependencies.testing = [
58
- "covdefaults>=2.3",
59
- "coverage>=7.5.4",
60
- "pytest>=8.3.5",
61
- "pytest-mock>=3.14",
62
- "setuptools>=75.1",
63
- ]
64
50
  urls.Changelog = "https://github.com/tox-dev/python-discovery/releases"
65
51
  urls.Documentation = "https://python-discovery.readthedocs.io"
66
52
  urls.Homepage = "https://github.com/tox-dev/python-discovery"
@@ -68,6 +54,21 @@ urls.Source = "https://github.com/tox-dev/python-discovery"
68
54
  urls.Tracker = "https://github.com/tox-dev/python-discovery/issues"
69
55
 
70
56
  [dependency-groups]
57
+ test = [
58
+ "covdefaults>=2.3",
59
+ "coverage>=7.5.4",
60
+ "pytest>=8.3.5",
61
+ "pytest-mock>=3.14",
62
+ "setuptools>=75.1",
63
+ ]
64
+ docs = [
65
+ "furo>=2025.12.19",
66
+ "sphinx>=9.1",
67
+ "sphinx-autodoc-typehints>=3.6.3",
68
+ "sphinxcontrib-mermaid>=2",
69
+ "sphinxcontrib-towncrier>=0.4",
70
+ "towncrier>=25.8",
71
+ ]
71
72
  release = [
72
73
  "gitpython>=3.1.46",
73
74
  "packaging>=26",
@@ -145,7 +146,7 @@ write-changes = true
145
146
  count = true
146
147
 
147
148
  [tool.pyproject-fmt]
148
- max_supported_python = "3.14"
149
+ max_supported_python = "3.15"
149
150
 
150
151
  [tool.ty]
151
152
  src.exclude = [ "tests/windows/winreg_mock_values.py" ]
@@ -104,7 +104,7 @@ class DiskContentStore:
104
104
 
105
105
  @contextmanager
106
106
  def locked(self) -> Generator[None]:
107
- from filelock import FileLock # noqa: PLC0415
107
+ from filelock import FileLock # ruff:ignore[import-outside-top-level]
108
108
 
109
109
  lock_path = self._folder / f"{self._key}.lock"
110
110
  lock_path.parent.mkdir(parents=True, exist_ok=True)
@@ -148,10 +148,10 @@ class DiskCache:
148
148
  class NoOpContentStore(ContentStore):
149
149
  """Content store that does nothing -- implements ContentStore protocol."""
150
150
 
151
- def exists(self) -> bool: # noqa: PLR6301
151
+ def exists(self) -> bool: # ruff:ignore[no-self-use]
152
152
  return False
153
153
 
154
- def read(self) -> dict | None: # noqa: PLR6301
154
+ def read(self) -> dict | None: # ruff:ignore[no-self-use]
155
155
  return None
156
156
 
157
157
  def write(self, content: dict) -> None:
@@ -161,14 +161,14 @@ class NoOpContentStore(ContentStore):
161
161
  pass
162
162
 
163
163
  @contextmanager
164
- def locked(self) -> Generator[None]: # noqa: PLR6301
164
+ def locked(self) -> Generator[None]: # ruff:ignore[no-self-use]
165
165
  yield
166
166
 
167
167
 
168
168
  class NoOpCache(PyInfoCache):
169
169
  """Cache that does nothing -- implements PyInfoCache protocol."""
170
170
 
171
- def py_info(self, path: Path) -> NoOpContentStore: # noqa: ARG002, PLR6301
171
+ def py_info(self, path: Path) -> NoOpContentStore: # ruff:ignore[unused-method-argument, no-self-use]
172
172
  return NoOpContentStore()
173
173
 
174
174
  def py_info_clear(self) -> None:
@@ -8,14 +8,14 @@ import logging
8
8
  import os
9
9
  import pkgutil
10
10
  import secrets
11
- import subprocess # noqa: S404
11
+ import subprocess # ruff:ignore[suspicious-subprocess-import]
12
12
  import sys
13
13
  import tempfile
14
14
  from collections import OrderedDict
15
15
  from contextlib import contextmanager
16
16
  from pathlib import Path
17
17
  from shlex import quote
18
- from subprocess import Popen, TimeoutExpired # noqa: S404
18
+ from subprocess import Popen, TimeoutExpired # ruff:ignore[suspicious-subprocess-import]
19
19
  from typing import TYPE_CHECKING, Final
20
20
 
21
21
  from ._cache import NoOpCache
@@ -32,7 +32,7 @@ _CACHE[Path(sys.executable)] = PythonInfo()
32
32
  _LOGGER: Final[logging.Logger] = logging.getLogger(__name__)
33
33
 
34
34
 
35
- def from_exe( # noqa: PLR0913
35
+ def from_exe( # ruff:ignore[too-many-arguments]
36
36
  cls: type[PythonInfo],
37
37
  cache: PyInfoCache | None,
38
38
  exe: str,
@@ -197,7 +197,7 @@ def _run_subprocess(
197
197
  env["PYTHONUTF8"] = "1"
198
198
  _LOGGER.debug("get interpreter info via cmd: %s", LogCmd(cmd))
199
199
  try:
200
- process = Popen( # noqa: S603
200
+ process = Popen( # ruff:ignore[subprocess-without-shell-equals-true]
201
201
  cmd,
202
202
  universal_newlines=True,
203
203
  stdin=subprocess.PIPE,
@@ -89,7 +89,7 @@ def iter_interpreters(
89
89
  yield from _iter_for_spec(spec_str, first_with, cache, env_map, predicate, seen)
90
90
 
91
91
 
92
- def _iter_for_spec( # noqa: PLR0913, PLR0917
92
+ def _iter_for_spec( # ruff:ignore[too-many-arguments, too-many-positional-arguments]
93
93
  spec_str: str | None,
94
94
  try_first_with: tuple[str, ...],
95
95
  cache: PyInfoCache | None,
@@ -231,7 +231,7 @@ def _propose_current_and_windows(
231
231
  yield current_python, True
232
232
 
233
233
  if IS_WIN: # pragma: win32 cover
234
- from ._windows import propose_interpreters as win_propose # noqa: PLC0415
234
+ from ._windows import propose_interpreters as win_propose # ruff:ignore[import-outside-top-level]
235
235
 
236
236
  for interpreter in win_propose(spec, cache, env):
237
237
  if _is_new_exe(str(interpreter.executable), tested_exes):
@@ -49,7 +49,7 @@ _CONF_VAR_RE: Final[re.Pattern[str]] = re.compile(
49
49
  )
50
50
 
51
51
 
52
- class PythonInfo: # noqa: PLR0904
52
+ class PythonInfo: # ruff:ignore[too-many-public-methods]
53
53
  """Contains information for a Python interpreter."""
54
54
 
55
55
  def __init__(self) -> None:
@@ -161,7 +161,7 @@ class PythonInfo: # noqa: PLR0904
161
161
  """Detect the tcl and tk libraries using tkinter."""
162
162
  tcl_lib, tk_lib = None, None
163
163
  try:
164
- import tkinter as tk # noqa: PLC0415
164
+ import tkinter as tk # ruff:ignore[import-outside-top-level]
165
165
  except ImportError:
166
166
  pass
167
167
  else:
@@ -177,7 +177,7 @@ class PythonInfo: # noqa: PLR0904
177
177
  @staticmethod
178
178
  def _query_tk_library(tcl: tk.Tk) -> str | None: # pragma: no cover
179
179
  """Try to get the TK library path directly from Tcl."""
180
- import tkinter as tk # noqa: PLC0415
180
+ import tkinter as tk # ruff:ignore[import-outside-top-level]
181
181
 
182
182
  try:
183
183
  if (tk_lib := tcl.eval("set tk_library")) and os.path.isdir(tk_lib):
@@ -303,8 +303,11 @@ class PythonInfo: # noqa: PLR0904
303
303
  with warnings.catch_warnings(): # disable warning for PEP-632
304
304
  warnings.simplefilter("ignore")
305
305
  try:
306
- from distutils import dist # noqa: PLC0415 # ty: ignore[unresolved-import]
307
- from distutils.command.install import SCHEME_KEYS # noqa: PLC0415 # ty: ignore[unresolved-import]
306
+ # ruff:ignore[import-outside-top-level]
307
+ from distutils import dist # ty: ignore[unresolved-import]
308
+
309
+ # ruff:ignore[import-outside-top-level]
310
+ from distutils.command.install import SCHEME_KEYS # ty: ignore[unresolved-import]
308
311
  except ImportError: # pragma: no cover # if removed or not installed ignore
309
312
  return {}
310
313
 
@@ -312,7 +315,7 @@ class PythonInfo: # noqa: PLR0904
312
315
  "script_args": "--no-user-cfg",
313
316
  }) # conf files not parsed so they do not hijack paths
314
317
  if hasattr(sys, "_framework"): # pragma: no cover # macOS framework builds only
315
- sys._framework = None # noqa: SLF001 # disable macOS static paths for framework
318
+ sys._framework = None # ruff:ignore[private-member-access] # disable macOS static paths for framework
316
319
 
317
320
  with warnings.catch_warnings(): # disable warning for PEP-632
318
321
  warnings.simplefilter("ignore")
@@ -459,12 +462,12 @@ class PythonInfo: # noqa: PLR0904
459
462
 
460
463
  :param cache: the cache store to clear.
461
464
  """
462
- from ._cached_py_info import clear # noqa: PLC0415
465
+ from ._cached_py_info import clear # ruff:ignore[import-outside-top-level]
463
466
 
464
467
  clear(cache)
465
468
  cls._cache_exe_discovery.clear()
466
469
 
467
- def satisfies(self, spec: PythonSpec, *, impl_must_match: bool) -> bool: # noqa: PLR0911
470
+ def satisfies(self, spec: PythonSpec, *, impl_must_match: bool) -> bool: # ruff:ignore[too-many-return-statements]
468
471
  """
469
472
  Check if a given specification can be satisfied by this python interpreter instance.
470
473
 
@@ -510,7 +513,7 @@ class PythonInfo: # noqa: PLR0904
510
513
  return True
511
514
  version_info = self.version_info
512
515
  for specifier in spec.version_specifier:
513
- assert specifier.version is not None # noqa: S101
516
+ assert specifier.version is not None # ruff:ignore[assert]
514
517
  numeric_version = specifier.version_str
515
518
  for prefix in ("rc", "b", "a"):
516
519
  if prefix in numeric_version:
@@ -520,7 +523,7 @@ class PythonInfo: # noqa: PLR0904
520
523
  release = ".".join(str(c) for c in [version_info.major, version_info.minor, version_info.micro][:precision])
521
524
  if (
522
525
  version_info.releaselevel != "final"
523
- and (precision == 3 or specifier.version.pre_type is not None) # noqa: PLR2004
526
+ and (precision == 3 or specifier.version.pre_type is not None) # ruff:ignore[magic-value-comparison]
524
527
  and (suffix := {"alpha": "a", "beta": "b", "candidate": "rc"}.get(version_info.releaselevel))
525
528
  ):
526
529
  release = f"{release}{suffix}{version_info.serial}"
@@ -573,7 +576,7 @@ class PythonInfo: # noqa: PLR0904
573
576
  return data
574
577
 
575
578
  @classmethod
576
- def from_exe( # noqa: PLR0913
579
+ def from_exe( # ruff:ignore[too-many-arguments]
577
580
  cls,
578
581
  exe: str,
579
582
  cache: PyInfoCache | None = None,
@@ -593,7 +596,7 @@ class PythonInfo: # noqa: PLR0904
593
596
  :param resolve_to_host: resolve through virtualenv layers to the system interpreter.
594
597
  :param env: environment mapping; defaults to :data:`os.environ`.
595
598
  """
596
- from ._cached_py_info import from_exe # noqa: PLC0415
599
+ from ._cached_py_info import from_exe # ruff:ignore[import-outside-top-level]
597
600
 
598
601
  env = os.environ if env is None else env
599
602
  proposed = from_exe(cls, cache, exe, env=env, raise_on_error=raise_on_error, ignore_cache=ignore_cache)
@@ -703,7 +706,7 @@ class PythonInfo: # noqa: PLR0904
703
706
  msg = "failed to detect {} in {}".format("|".join(possible_names), os.pathsep.join(possible_folders))
704
707
  raise RuntimeError(msg)
705
708
 
706
- def _check_exe( # noqa: PLR0913
709
+ def _check_exe( # ruff:ignore[too-many-arguments]
707
710
  self,
708
711
  cache: PyInfoCache | None,
709
712
  folder: str,
@@ -797,7 +800,7 @@ class PythonInfo: # noqa: PLR0904
797
800
  for base in possible_base:
798
801
  lower = base.lower()
799
802
  yield lower
800
- from ._compat import fs_is_case_sensitive # noqa: PLC0415
803
+ from ._compat import fs_is_case_sensitive # ruff:ignore[import-outside-top-level]
801
804
 
802
805
  if fs_is_case_sensitive(): # pragma: no branch
803
806
  if base != lower:
@@ -55,7 +55,7 @@ def _parse_version_parts(version: str) -> tuple[int | None, int | None, int | No
55
55
  raise ValueError(msg)
56
56
  if len(versions) == _MAX_VERSION_PARTS:
57
57
  return versions[0], versions[1], versions[2]
58
- if len(versions) == 2: # noqa: PLR2004
58
+ if len(versions) == 2: # ruff:ignore[magic-value-comparison]
59
59
  return versions[0], versions[1], None
60
60
  version_data = versions[0]
61
61
  major = int(str(version_data)[0])
@@ -127,7 +127,7 @@ class PythonSpec:
127
127
  constraints, or ``None``.
128
128
  """
129
129
 
130
- def __init__( # noqa: PLR0913, PLR0917
130
+ def __init__( # ruff:ignore[too-many-arguments, too-many-positional-arguments]
131
131
  self,
132
132
  str_spec: str,
133
133
  implementation: str | None,
@@ -233,7 +233,7 @@ class PythonSpec:
233
233
  return len(item.version.release)
234
234
  return None
235
235
 
236
- def satisfies(self, spec: PythonSpec) -> bool: # noqa: PLR0911
236
+ def satisfies(self, spec: PythonSpec) -> bool: # ruff:ignore[too-many-return-statements]
237
237
  """
238
238
  Check if this spec is compatible with the given *spec* (e.g. PEP-514 on Windows).
239
239
 
@@ -92,7 +92,7 @@ class SimpleVersion:
92
92
  def __hash__(self) -> int:
93
93
  return hash((self.release, self.pre_type, self.pre_num))
94
94
 
95
- def __lt__(self, other: object) -> bool: # noqa: PLR0911
95
+ def __lt__(self, other: object) -> bool: # ruff:ignore[too-many-return-statements]
96
96
  if not isinstance(other, SimpleVersion):
97
97
  return NotImplemented
98
98
  if self.release != other.release:
@@ -225,7 +225,7 @@ class SimpleSpecifier:
225
225
  return False
226
226
  if candidate < self.version:
227
227
  return False
228
- if len(self.version.release) >= 2: # noqa: PLR2004 # pragma: no branch # SimpleVersion always has 3-part release
228
+ if len(self.version.release) >= 2: # ruff:ignore[magic-value-comparison] # pragma: no branch # SimpleVersion always has 3-part release
229
229
  upper_parts = list(self.version.release[:-1])
230
230
  upper_parts[-1] += 1
231
231
  upper = SimpleVersion.from_string(".".join(str(p) for p in upper_parts))
@@ -47,7 +47,7 @@ _THREADED_TAG_RE: Final[re.Pattern[str]] = re.compile(
47
47
  )
48
48
 
49
49
 
50
- def enum_keys(key: Any) -> Generator[str, None, None]: # noqa: ANN401
50
+ def enum_keys(key: Any) -> Generator[str, None, None]: # ruff:ignore[any-type]
51
51
  at = 0
52
52
  while True:
53
53
  try:
@@ -57,7 +57,7 @@ def enum_keys(key: Any) -> Generator[str, None, None]: # noqa: ANN401
57
57
  at += 1
58
58
 
59
59
 
60
- def get_value(key: Any, value_name: str | None) -> Any: # noqa: ANN401
60
+ def get_value(key: Any, value_name: str | None) -> Any: # ruff:ignore[any-type]
61
61
  try:
62
62
  return winreg.QueryValueEx(key, value_name)[0] # ty: ignore[unresolved-attribute]
63
63
  except OSError:
@@ -93,7 +93,7 @@ def process_set(
93
93
  def process_company(
94
94
  hive_name: str,
95
95
  company: str,
96
- root_key: Any, # noqa: ANN401
96
+ root_key: Any, # ruff:ignore[any-type]
97
97
  default_arch: int,
98
98
  ) -> Generator[_RegistrySpec, None, None]:
99
99
  with winreg.OpenKeyEx(root_key, company) as company_key: # ty: ignore[unresolved-attribute]
@@ -103,7 +103,7 @@ def process_company(
103
103
  yield spec
104
104
 
105
105
 
106
- def process_tag(hive_name: str, company: str, company_key: Any, tag: str, default_arch: int) -> _RegistrySpec | None: # noqa: ANN401
106
+ def process_tag(hive_name: str, company: str, company_key: Any, tag: str, default_arch: int) -> _RegistrySpec | None: # ruff:ignore[any-type]
107
107
  with winreg.OpenKeyEx(company_key, tag) as tag_key: # ty: ignore[unresolved-attribute]
108
108
  version = load_version_data(hive_name, company, tag, tag_key)
109
109
  if version is not None: # if failed to get version bail
@@ -120,7 +120,7 @@ def process_tag(hive_name: str, company: str, company_key: Any, tag: str, defaul
120
120
  return None
121
121
 
122
122
 
123
- def load_exe(hive_name: str, company: str, company_key: Any, tag: str) -> tuple[str, str | None] | None: # noqa: ANN401
123
+ def load_exe(hive_name: str, company: str, company_key: Any, tag: str) -> tuple[str, str | None] | None: # ruff:ignore[any-type]
124
124
  key_path = f"{hive_name}/{company}/{tag}"
125
125
  try:
126
126
  with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key, ip_key: # ty: ignore[unresolved-attribute]
@@ -132,7 +132,7 @@ def load_exe(hive_name: str, company: str, company_key: Any, tag: str) -> tuple[
132
132
  return None
133
133
 
134
134
 
135
- def _resolve_exe(ip_key: Any, key_path: str) -> str | None: # noqa: ANN401
135
+ def _resolve_exe(ip_key: Any, key_path: str) -> str | None: # ruff:ignore[any-type]
136
136
  if (exe := get_value(ip_key, "ExecutablePath")) is not None:
137
137
  return exe
138
138
  if (ip := get_value(ip_key, None)) is None:
@@ -141,7 +141,7 @@ def _resolve_exe(ip_key: Any, key_path: str) -> str | None: # noqa: ANN401
141
141
  return os.path.join(ip, "python.exe")
142
142
 
143
143
 
144
- def load_arch_data(hive_name: str, company: str, tag: str, tag_key: Any, default_arch: int) -> int | None: # noqa: ANN401
144
+ def load_arch_data(hive_name: str, company: str, tag: str, tag_key: Any, default_arch: int) -> int | None: # ruff:ignore[any-type]
145
145
  arch_str = get_value(tag_key, "SysArchitecture")
146
146
  if arch_str is not None:
147
147
  key_path = f"{hive_name}/{company}/{tag}/SysArchitecture"
@@ -152,7 +152,7 @@ def load_arch_data(hive_name: str, company: str, tag: str, tag_key: Any, default
152
152
  return default_arch
153
153
 
154
154
 
155
- def parse_arch(arch_str: Any) -> int: # noqa: ANN401
155
+ def parse_arch(arch_str: Any) -> int: # ruff:ignore[any-type]
156
156
  if isinstance(arch_str, str):
157
157
  if match := _ARCH_RE.match(arch_str):
158
158
  return int(next(iter(match.groups())))
@@ -166,7 +166,7 @@ def load_version_data(
166
166
  hive_name: str,
167
167
  company: str,
168
168
  tag: str,
169
- tag_key: Any, # noqa: ANN401
169
+ tag_key: Any, # ruff:ignore[any-type]
170
170
  ) -> tuple[int | None, int | None, int | None] | None:
171
171
  for candidate, key_path in [
172
172
  (get_value(tag_key, "SysVersion"), f"{hive_name}/{company}/{tag}/SysVersion"),
@@ -180,7 +180,7 @@ def load_version_data(
180
180
  return None
181
181
 
182
182
 
183
- def parse_version(version_str: Any) -> tuple[int | None, int | None, int | None]: # noqa: ANN401
183
+ def parse_version(version_str: Any) -> tuple[int | None, int | None, int | None]: # ruff:ignore[any-type]
184
184
  if isinstance(version_str, str):
185
185
  if match := _VERSION_RE.match(version_str):
186
186
  g1, g2, g3 = match.groups()
@@ -195,7 +195,7 @@ def parse_version(version_str: Any) -> tuple[int | None, int | None, int | None]
195
195
  raise ValueError(error)
196
196
 
197
197
 
198
- def load_threaded(hive_name: str, company: str, tag: str, tag_key: Any) -> bool: # noqa: ANN401
198
+ def load_threaded(hive_name: str, company: str, tag: str, tag_key: Any) -> bool: # ruff:ignore[any-type]
199
199
  display_name = get_value(tag_key, "DisplayName")
200
200
  if display_name is not None:
201
201
  if isinstance(display_name, str):
@@ -41,7 +41,7 @@ def main(version_str: str) -> None:
41
41
 
42
42
 
43
43
  def create_release_branch(repo: Repo, version: Version) -> tuple[Remote, Head]:
44
- print("create release branch from upstream main") # noqa: T201
44
+ print("create release branch from upstream main") # ruff:ignore[print]
45
45
  upstream = get_upstream(repo)
46
46
  upstream.fetch()
47
47
  branch_name = f"release-{version}"
@@ -63,71 +63,71 @@ def get_upstream(repo: Repo) -> Remote:
63
63
  def push_release(repo: Repo, upstream: Remote, release_branch: Head, version: Version) -> tuple[bool, bool, bool]:
64
64
  release_commit = release_changelog(repo, version)
65
65
  tag = tag_release_commit(release_commit, repo, version)
66
- print("push release commit") # noqa: T201
66
+ print("push release commit") # ruff:ignore[print]
67
67
  repo.git.push(upstream.name, f"{release_branch}:main", "-f")
68
- print("push release tag") # noqa: T201
68
+ print("push release tag") # ruff:ignore[print]
69
69
  repo.git.push(upstream.name, tag, "-f")
70
70
  create_github_release(version)
71
71
  return True, True, True
72
72
 
73
73
 
74
74
  def release_changelog(repo: Repo, version: Version) -> Commit:
75
- print("generate release commit") # noqa: T201
76
- check_call(["towncrier", "build", "--yes", "--version", version.public], cwd=str(ROOT_SRC_DIR)) # noqa: S607
77
- print("format changelog with pre-commit") # noqa: T201
75
+ print("generate release commit") # ruff:ignore[print]
76
+ check_call(["towncrier", "build", "--yes", "--version", version.public], cwd=str(ROOT_SRC_DIR)) # ruff:ignore[start-process-with-partial-path]
77
+ print("format changelog with pre-commit") # ruff:ignore[print]
78
78
  changelog_path = ROOT_SRC_DIR / "docs" / "changelog.rst"
79
79
  try:
80
- check_call(["pre-commit", "run", "--files", str(changelog_path)], cwd=str(ROOT_SRC_DIR)) # noqa: S607
80
+ check_call(["pre-commit", "run", "--files", str(changelog_path)], cwd=str(ROOT_SRC_DIR)) # ruff:ignore[start-process-with-partial-path]
81
81
  except CalledProcessError:
82
- print("pre-commit made formatting changes, staging them") # noqa: T201
82
+ print("pre-commit made formatting changes, staging them") # ruff:ignore[print]
83
83
  repo.index.add([str(changelog_path)])
84
84
  return repo.index.commit(f"release {version}")
85
85
 
86
86
 
87
87
  def tag_release_commit(release_commit: Commit, repo: Repo, version: Version) -> TagReference:
88
- print("tag release commit") # noqa: T201
88
+ print("tag release commit") # ruff:ignore[print]
89
89
  version_str = str(version)
90
90
  if version_str in {tag.name for tag in repo.tags}:
91
- print(f"delete existing tag {version_str}") # noqa: T201
91
+ print(f"delete existing tag {version_str}") # ruff:ignore[print]
92
92
  repo.delete_tag(repo.tags[version_str])
93
- print(f"create tag {version_str}") # noqa: T201
93
+ print(f"create tag {version_str}") # ruff:ignore[print]
94
94
  return repo.create_tag(version_str, ref=release_commit.hexsha, force=True)
95
95
 
96
96
 
97
97
  def create_github_release(version: Version) -> None:
98
- print("create github release") # noqa: T201
98
+ print("create github release") # ruff:ignore[print]
99
99
  version_str = str(version)
100
100
  try:
101
101
  result = run(
102
- ["gh", "release", "create", version_str, "--title", f"v{version_str}", "--generate-notes"], # noqa: S607
102
+ ["gh", "release", "create", version_str, "--title", f"v{version_str}", "--generate-notes"], # ruff:ignore[start-process-with-partial-path]
103
103
  cwd=str(ROOT_SRC_DIR),
104
104
  capture_output=True,
105
105
  text=True,
106
106
  check=True,
107
107
  )
108
108
  if result.stdout:
109
- print(result.stdout) # noqa: T201
109
+ print(result.stdout) # ruff:ignore[print]
110
110
  except CalledProcessError as e:
111
- print(f"gh release create failed with exit code {e.returncode}") # noqa: T201
111
+ print(f"gh release create failed with exit code {e.returncode}") # ruff:ignore[print]
112
112
  if e.stdout:
113
- print(f"stdout: {e.stdout}") # noqa: T201
113
+ print(f"stdout: {e.stdout}") # ruff:ignore[print]
114
114
  if e.stderr:
115
- print(f"stderr: {e.stderr}") # noqa: T201
115
+ print(f"stderr: {e.stderr}") # ruff:ignore[print]
116
116
  raise
117
117
 
118
118
 
119
119
  def finalize_release(repo: Repo, upstream: Remote, release_branch: Head) -> None:
120
- print("checkout main to new release and delete release branch") # noqa: T201
120
+ print("checkout main to new release and delete release branch") # ruff:ignore[print]
121
121
  repo.heads.main.checkout()
122
122
  repo.delete_head(release_branch, force=True)
123
- print("delete remote release branch") # noqa: T201
123
+ print("delete remote release branch") # ruff:ignore[print]
124
124
  repo.git.push(upstream.name, f":{release_branch}", "--no-verify")
125
125
  upstream.fetch()
126
126
  repo.git.reset("--hard", f"{upstream.name}/main")
127
- print("All done!") # noqa: T201
127
+ print("All done!") # ruff:ignore[print]
128
128
 
129
129
 
130
- def cleanup_failed_release( # noqa: PLR0913
130
+ def cleanup_failed_release( # ruff:ignore[too-many-arguments]
131
131
  repo: Repo,
132
132
  upstream: Remote,
133
133
  version: Version,
@@ -138,30 +138,30 @@ def cleanup_failed_release( # noqa: PLR0913
138
138
  tag_pushed: bool,
139
139
  main_pushed: bool,
140
140
  ) -> None:
141
- print("Release failed! Cleaning up...") # noqa: T201
141
+ print("Release failed! Cleaning up...") # ruff:ignore[print]
142
142
  if release_created:
143
- print(f"Deleting GitHub release {version}") # noqa: T201
143
+ print(f"Deleting GitHub release {version}") # ruff:ignore[print]
144
144
  try:
145
- check_call(["gh", "release", "delete", str(version), "--yes"], cwd=str(ROOT_SRC_DIR)) # noqa: S607
146
- except Exception as cleanup_error: # noqa: BLE001
147
- print(f"Warning: Failed to delete GitHub release: {cleanup_error}") # noqa: T201
145
+ check_call(["gh", "release", "delete", str(version), "--yes"], cwd=str(ROOT_SRC_DIR)) # ruff:ignore[start-process-with-partial-path]
146
+ except Exception as cleanup_error: # ruff:ignore[blind-except]
147
+ print(f"Warning: Failed to delete GitHub release: {cleanup_error}") # ruff:ignore[print]
148
148
  if tag_pushed:
149
- print(f"Deleting remote tag {version}") # noqa: T201
149
+ print(f"Deleting remote tag {version}") # ruff:ignore[print]
150
150
  try:
151
151
  repo.git.push(upstream.name, f":refs/tags/{version}", "--no-verify")
152
- except Exception as cleanup_error: # noqa: BLE001
153
- print(f"Warning: Failed to delete remote tag: {cleanup_error}") # noqa: T201
152
+ except Exception as cleanup_error: # ruff:ignore[blind-except]
153
+ print(f"Warning: Failed to delete remote tag: {cleanup_error}") # ruff:ignore[print]
154
154
  if main_pushed:
155
- print(f"Reverting main to {original_main_sha[:8]}") # noqa: T201
155
+ print(f"Reverting main to {original_main_sha[:8]}") # ruff:ignore[print]
156
156
  try:
157
157
  repo.git.push(upstream.name, f"{original_main_sha}:main", "-f", "--no-verify")
158
- except Exception as cleanup_error: # noqa: BLE001
159
- print(f"Warning: Failed to revert main: {cleanup_error}") # noqa: T201
160
- print("Deleting remote release branch") # noqa: T201
158
+ except Exception as cleanup_error: # ruff:ignore[blind-except]
159
+ print(f"Warning: Failed to revert main: {cleanup_error}") # ruff:ignore[print]
160
+ print("Deleting remote release branch") # ruff:ignore[print]
161
161
  try:
162
162
  repo.git.push(upstream.name, f":{release_branch}", "--no-verify")
163
- except Exception as cleanup_error: # noqa: BLE001
164
- print(f"Warning: Failed to delete remote branch: {cleanup_error}") # noqa: T201
163
+ except Exception as cleanup_error: # ruff:ignore[blind-except]
164
+ print(f"Warning: Failed to delete remote branch: {cleanup_error}") # ruff:ignore[print]
165
165
 
166
166
 
167
167
  if __name__ == "__main__":
@@ -437,7 +437,8 @@ def test_predicate_with_fallback_specs(session_cache: DiskCache) -> None:
437
437
  proposed.append(str(info.executable))
438
438
  return len(proposed) > 1
439
439
 
440
- result = get_interpreter([f"{major}.{minor}", sys.executable], [], session_cache, predicate=reject_first)
440
+ threaded = "t" if current.free_threaded else ""
441
+ result = get_interpreter([f"{major}.{minor}{threaded}", sys.executable], [], session_cache, predicate=reject_first)
441
442
  # rejecting the first proposal must not abort discovery; a later proposal (same or fallback spec) is accepted -
442
443
  # on hosts where every alias resolves to one interpreter the fallback re-proposes it, so executables may match
443
444
  assert result is not None
@@ -437,7 +437,10 @@ def test_satisfies_version_specifier_prerelease(version_info: VersionInfo, spec_
437
437
 
438
438
  def test_satisfies_path_not_abs_basename_match() -> None:
439
439
  info = copy.deepcopy(CURRENT)
440
- basename = Path(info.original_executable).stem
440
+ # stem drops everything from the first dot, so python3.15t arrives as python3 and stops naming a free-threaded
441
+ # build; only the Windows .exe belongs off the end.
442
+ name = Path(info.original_executable).name
443
+ basename = name[: -len(".exe")] if name.endswith(".exe") else name
441
444
  spec = PythonSpec.from_string_spec(basename)
442
445
  assert info.satisfies(spec, impl_must_match=False) is True
443
446
 
@@ -44,7 +44,7 @@ def test_version_eq_different() -> None:
44
44
 
45
45
 
46
46
  def test_version_eq_not_implemented() -> None:
47
- result = SimpleVersion.from_string("3.11").__eq__("3.11") # noqa: PLC2801
47
+ result = SimpleVersion.from_string("3.11").__eq__("3.11") # ruff:ignore[unnecessary-dunder-call]
48
48
  assert result is NotImplemented
49
49
 
50
50
 
@@ -69,7 +69,7 @@ def test_version_lt(left: str, right: str, expected: bool) -> None:
69
69
 
70
70
 
71
71
  def test_version_lt_not_implemented() -> None:
72
- result = SimpleVersion.from_string("3.11").__lt__("3.12") # noqa: PLC2801
72
+ result = SimpleVersion.from_string("3.11").__lt__("3.12") # ruff:ignore[unnecessary-dunder-call]
73
73
  assert result is NotImplemented
74
74
 
75
75
 
@@ -83,7 +83,7 @@ def test_version_gt() -> None:
83
83
 
84
84
 
85
85
  def test_version_gt_not_implemented() -> None:
86
- result = SimpleVersion.from_string("3.11").__gt__("3.11") # noqa: PLC2801
86
+ result = SimpleVersion.from_string("3.11").__gt__("3.11") # ruff:ignore[unnecessary-dunder-call]
87
87
  assert result is NotImplemented
88
88
 
89
89
 
@@ -198,7 +198,7 @@ def test_specifier_eq() -> None:
198
198
 
199
199
 
200
200
  def test_specifier_eq_not_implemented() -> None:
201
- result = SimpleSpecifier.from_string(">=3.12").__eq__(">=3.12") # noqa: PLC2801
201
+ result = SimpleSpecifier.from_string(">=3.12").__eq__(">=3.12") # ruff:ignore[unnecessary-dunder-call]
202
202
  assert result is NotImplemented
203
203
 
204
204
 
@@ -262,7 +262,7 @@ def test_specifier_set_eq() -> None:
262
262
 
263
263
 
264
264
  def test_specifier_set_eq_not_implemented() -> None:
265
- result = SimpleSpecifierSet.from_string(">=3.12").__eq__(">=3.12") # noqa: PLC2801
265
+ result = SimpleSpecifierSet.from_string(">=3.12").__eq__(">=3.12") # ruff:ignore[unnecessary-dunder-call]
266
266
  assert result is NotImplemented
267
267
 
268
268
 
@@ -42,7 +42,7 @@ def _load_registry_data(
42
42
  loc: dict[str, object] = {}
43
43
  glob: dict[str, object] = {"winreg": winreg}
44
44
  mock_value_str = (Path(__file__).parent / "winreg_mock_values.py").read_text(encoding="utf-8")
45
- exec(mock_value_str, glob, loc) # noqa: S102
45
+ exec(mock_value_str, glob, loc) # ruff:ignore[exec-builtin]
46
46
  return loc["enum_collect"], loc["value_collect"], loc["key_open"], loc["hive_open"] # type: ignore[return-value]
47
47
 
48
48
 
@@ -1,12 +1,12 @@
1
1
  requires = ["tox>=4.38", "tox-uv>=1.29"]
2
- env_list = ["fix", "3.14", "3.13", "3.12", "3.11", "3.10", "3.9", "3.8", "type-3.8", "type-3.14", "docs", "pkg_meta"]
2
+ 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"]
3
3
  skip_missing_interpreters = true
4
4
 
5
5
  [env_run_base]
6
6
  description = "run the tests with pytest"
7
7
  package = "wheel"
8
8
  wheel_build_env = ".pkg"
9
- extras = ["testing"]
9
+ dependency_groups = ["test"]
10
10
  pass_env = ["DIFF_AGAINST", "PYTEST_*"]
11
11
  set_env.COVERAGE_FILE = "{work_dir}{/}.coverage.{env_name}"
12
12
  commands = [
@@ -46,7 +46,7 @@ commands = [["ty", "check", "--output-format", "concise", "--error-on-warning",
46
46
 
47
47
  [env.docs]
48
48
  description = "build documentation"
49
- extras = ["docs"]
49
+ dependency_groups = ["docs"]
50
50
  commands = [
51
51
  [
52
52
  "sphinx-build",
@@ -81,5 +81,5 @@ commands = [["python", "{tox_root}/tasks/release.py", "--version", "{posargs}"]]
81
81
  [env.dev]
82
82
  description = "generate a DEV environment"
83
83
  package = "editable"
84
- extras = ["testing"]
84
+ dependency_groups = ["test"]
85
85
  commands = [["uv", "pip", "tree"], ["python", "-c", "import sys; print(sys.executable)"]]