python-discovery 1.4.4__tar.gz → 1.5.1__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 (58) hide show
  1. {python_discovery-1.4.4 → python_discovery-1.5.1}/.github/workflows/check.yaml +4 -5
  2. {python_discovery-1.4.4 → python_discovery-1.5.1}/.github/workflows/prepare-release.yaml +2 -11
  3. {python_discovery-1.4.4 → python_discovery-1.5.1}/.github/workflows/release.yaml +3 -7
  4. {python_discovery-1.4.4 → python_discovery-1.5.1}/.pre-commit-config.yaml +16 -9
  5. {python_discovery-1.4.4 → python_discovery-1.5.1}/PKG-INFO +2 -15
  6. {python_discovery-1.4.4 → python_discovery-1.5.1}/docs/changelog.rst +26 -0
  7. {python_discovery-1.4.4 → python_discovery-1.5.1}/docs/conf.py +1 -1
  8. {python_discovery-1.4.4 → python_discovery-1.5.1}/docs/explanation.rst +67 -34
  9. {python_discovery-1.4.4 → python_discovery-1.5.1}/pyproject.toml +17 -17
  10. {python_discovery-1.4.4 → python_discovery-1.5.1}/src/python_discovery/_cache.py +5 -5
  11. {python_discovery-1.4.4 → python_discovery-1.5.1}/src/python_discovery/_cached_py_info.py +4 -4
  12. {python_discovery-1.4.4 → python_discovery-1.5.1}/src/python_discovery/_discovery.py +149 -22
  13. {python_discovery-1.4.4 → python_discovery-1.5.1}/src/python_discovery/_py_info.py +17 -14
  14. {python_discovery-1.4.4 → python_discovery-1.5.1}/src/python_discovery/_py_spec.py +3 -3
  15. {python_discovery-1.4.4 → python_discovery-1.5.1}/src/python_discovery/_specifier.py +9 -6
  16. {python_discovery-1.4.4 → python_discovery-1.5.1}/src/python_discovery/_windows/_pep514.py +11 -11
  17. {python_discovery-1.4.4 → python_discovery-1.5.1}/tasks/release.py +35 -35
  18. {python_discovery-1.4.4 → python_discovery-1.5.1}/tests/test_discovery.py +134 -62
  19. python_discovery-1.5.1/tests/test_iter_interpreters.py +423 -0
  20. {python_discovery-1.4.4 → python_discovery-1.5.1}/tests/test_py_info_extra.py +4 -1
  21. {python_discovery-1.4.4 → python_discovery-1.5.1}/tests/test_specifier.py +5 -5
  22. {python_discovery-1.4.4 → python_discovery-1.5.1}/tests/windows/conftest.py +1 -1
  23. {python_discovery-1.4.4 → python_discovery-1.5.1}/tox.toml +4 -4
  24. python_discovery-1.4.4/tests/test_iter_interpreters.py +0 -218
  25. {python_discovery-1.4.4 → python_discovery-1.5.1}/.github/FUNDING.yaml +0 -0
  26. {python_discovery-1.4.4 → python_discovery-1.5.1}/.github/SECURITY.md +0 -0
  27. {python_discovery-1.4.4 → python_discovery-1.5.1}/.github/dependabot.yaml +0 -0
  28. {python_discovery-1.4.4 → python_discovery-1.5.1}/.github/release.yaml +0 -0
  29. {python_discovery-1.4.4 → python_discovery-1.5.1}/.gitignore +0 -0
  30. {python_discovery-1.4.4 → python_discovery-1.5.1}/.readthedocs.yaml +0 -0
  31. {python_discovery-1.4.4 → python_discovery-1.5.1}/.readthedocs.yml +0 -0
  32. {python_discovery-1.4.4 → python_discovery-1.5.1}/CODE_OF_CONDUCT.md +0 -0
  33. {python_discovery-1.4.4 → python_discovery-1.5.1}/LICENSE +0 -0
  34. {python_discovery-1.4.4 → python_discovery-1.5.1}/README.md +0 -0
  35. {python_discovery-1.4.4 → python_discovery-1.5.1}/docs/_static/custom.css +0 -0
  36. {python_discovery-1.4.4 → python_discovery-1.5.1}/docs/_static/logo.svg +0 -0
  37. {python_discovery-1.4.4 → python_discovery-1.5.1}/docs/changelog/template.jinja2 +0 -0
  38. {python_discovery-1.4.4 → python_discovery-1.5.1}/docs/how-to/standalone-usage.rst +0 -0
  39. {python_discovery-1.4.4 → python_discovery-1.5.1}/docs/index.rst +0 -0
  40. {python_discovery-1.4.4 → python_discovery-1.5.1}/docs/reference/api.rst +0 -0
  41. {python_discovery-1.4.4 → python_discovery-1.5.1}/docs/reference/environment-variables.rst +0 -0
  42. {python_discovery-1.4.4 → python_discovery-1.5.1}/docs/tutorial/getting-started.rst +0 -0
  43. {python_discovery-1.4.4 → python_discovery-1.5.1}/src/python_discovery/__init__.py +0 -0
  44. {python_discovery-1.4.4 → python_discovery-1.5.1}/src/python_discovery/_compat.py +0 -0
  45. {python_discovery-1.4.4 → python_discovery-1.5.1}/src/python_discovery/_windows/__init__.py +0 -0
  46. {python_discovery-1.4.4 → python_discovery-1.5.1}/src/python_discovery/_windows/_propose.py +0 -0
  47. {python_discovery-1.4.4 → python_discovery-1.5.1}/src/python_discovery/py.typed +0 -0
  48. {python_discovery-1.4.4 → python_discovery-1.5.1}/tests/conftest.py +0 -0
  49. {python_discovery-1.4.4 → python_discovery-1.5.1}/tests/py_info/test_py_info.py +0 -0
  50. {python_discovery-1.4.4 → python_discovery-1.5.1}/tests/py_info/test_py_info_exe_based_of.py +0 -0
  51. {python_discovery-1.4.4 → python_discovery-1.5.1}/tests/test_cache.py +0 -0
  52. {python_discovery-1.4.4 → python_discovery-1.5.1}/tests/test_cached_py_info.py +0 -0
  53. {python_discovery-1.4.4 → python_discovery-1.5.1}/tests/test_discovery_extra.py +0 -0
  54. {python_discovery-1.4.4 → python_discovery-1.5.1}/tests/test_py_spec.py +0 -0
  55. {python_discovery-1.4.4 → python_discovery-1.5.1}/tests/test_py_spec_extra.py +0 -0
  56. {python_discovery-1.4.4 → python_discovery-1.5.1}/tests/windows/test_windows.py +0 -0
  57. {python_discovery-1.4.4 → python_discovery-1.5.1}/tests/windows/test_windows_pep514.py +0 -0
  58. {python_discovery-1.4.4 → python_discovery-1.5.1}/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"
@@ -34,12 +33,12 @@ jobs:
34
33
  - dev
35
34
  - pkg_meta
36
35
  steps:
37
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
36
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
38
37
  with:
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@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
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
@@ -26,24 +23,21 @@ jobs:
26
23
  permissions:
27
24
  contents: write
28
25
  steps:
29
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
26
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
30
27
  with:
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@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
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,23 +2,20 @@ 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
15
12
  steps:
16
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
13
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
17
14
  with:
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@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
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
@@ -47,6 +43,6 @@ jobs:
47
43
  name: ${{ env.dists-artifact-name }}
48
44
  path: dist/
49
45
  - name: Publish to PyPI
50
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
46
+ uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
51
47
  with:
52
48
  attestations: true
@@ -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.26.0"
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.16.0"
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.28.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.1
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,12 @@ 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
- 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
51
  Description-Content-Type: text/markdown
65
52
 
66
53
  # [`python-discovery`](https://python-discovery.readthedocs.io/en/latest/)
@@ -6,6 +6,32 @@
6
6
 
7
7
  .. towncrier release notes start
8
8
 
9
+ ********************
10
+ v1.5.1 (2026-07-31)
11
+ ********************
12
+
13
+ Bug fixes - 1.5.1
14
+ =================
15
+ - Locate uv's interpreter store the way uv does, using XDG on macOS (``~/.local/share/uv/python``) and roaming
16
+ ``%APPDATA%\uv\python`` on Windows, neither of which ``platformdirs`` reported. The uv variables now come from the
17
+ ``env`` mapping rather than ``os.environ`` - by :user:`gaborbernat`. (:issue:`109`)
18
+ - Pick uv-managed interpreters by install directory name instead of globbing the store. A bare ``3.8`` now means
19
+ CPython, the way uv resolves it, rather than whichever implementation the filesystem happened to list first, and
20
+ probing runs newest version first - by :user:`gaborbernat`. (:issue:`110`)
21
+
22
+ Packaging updates and notes for downstreams - 1.5.1
23
+ ===================================================
24
+ - Drop the ``platformdirs`` runtime dependency; locating uv's store was the only thing it did - by
25
+ :user:`gaborbernat`. (:issue:`109`)
26
+
27
+ ********************
28
+ v1.5.0 (2026-07-21)
29
+ ********************
30
+
31
+ Packaging updates and notes for downstreams - 1.5.0
32
+ ===================================================
33
+ - Declare support for Python 3.15 and run the test suite against it, currently in beta. (:issue:`104`)
34
+
9
35
  ********************
10
36
  v1.4.4 (2026-07-08)
11
37
  ********************
@@ -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",
@@ -68,9 +68,33 @@ detects these shims and resolves them to the actual binary.
68
68
  How uv-managed Pythons are discovered
69
69
  ---------------------------------------
70
70
 
71
- `uv <https://docs.astral.sh/uv/>`_ installs Python interpreters under a single root directory (configurable via
72
- ``UV_PYTHON_INSTALL_DIR``, otherwise defaulting under ``XDG_DATA_HOME`` or the platform user-data path). Each
73
- install lives in its own subdirectory, but the actual binary location varies by OS and implementation:
71
+ `uv <https://docs.astral.sh/uv/>`_ installs Python interpreters under a single root directory. ``UV_PYTHON_INSTALL_DIR``
72
+ overrides it; otherwise the store is the ``python`` bucket of uv's state directory, which uv derives as follows:
73
+
74
+ .. list-table::
75
+ :header-rows: 1
76
+ :widths: 20 40 40
77
+
78
+ * - Platform
79
+ - Legacy state directory
80
+ - Current state directory
81
+ * - Windows
82
+ - ``%APPDATA%\uv\data``
83
+ - ``%APPDATA%\uv`` (roaming, not local)
84
+ * - macOS
85
+ - ``~/Library/Application Support/uv``
86
+ - ``$XDG_DATA_HOME/uv`` or ``~/.local/share/uv``
87
+ * - Other
88
+ - same as current
89
+ - ``$XDG_DATA_HOME/uv`` or ``~/.local/share/uv``
90
+
91
+ uv follows the XDG convention on macOS too, and falls back to the legacy directory only when that directory already
92
+ exists. python-discovery searches both, in the same order, so a leftover legacy directory cannot hide the store uv
93
+ installs into. A relative ``XDG_DATA_HOME`` does not count, as the XDG specification requires, and Windows ignores it
94
+ outright. Every one of these variables comes from the ``env`` mapping passed to
95
+ :func:`~python_discovery.get_interpreter`, rather than from the process environment.
96
+
97
+ Each install lives in its own subdirectory, but the actual binary location varies by OS and implementation:
74
98
 
75
99
  .. list-table::
76
100
  :header-rows: 1
@@ -89,42 +113,51 @@ install lives in its own subdirectory, but the actual binary location varies by
89
113
  - ``<root>/<key>/bin/graalpy``
90
114
  - ``<root>/<key>/bin/graalpy.exe``
91
115
 
116
+ GraalPy keeps its ``bin/`` segment on Windows (an upstream choice in uv); PyPy and CPython do not. python-discovery
117
+ globs all of these patterns regardless of the host OS, because globs that do not match anything are essentially
118
+ free, and the cross-platform list is short. Symlinked aliases inside an install (``bin/python``,
119
+ ``bin/python3``, ``bin/python3.14`` all pointing at the same real file) are deduplicated by resolved path before
120
+ the subprocess interrogation, so each install is interrogated once. python-discovery also follows the ``install``
121
+ subdirectory some distributions unpack into, as uv does.
122
+
123
+ Which uv installs get interrogated
124
+ ------------------------------------
125
+
126
+ Interrogating an interpreter costs a subprocess, and a uv install directory announces what it holds in its own name:
127
+ ``<implementation>-<version>[+<variant>]-<os>-<arch>-<libc>``, for example
128
+ ``cpython-3.14.6+freethreaded-macos-aarch64-none``. python-discovery reads that name and skips the installs the spec
129
+ already rules out, then probes what is left in uv's preference order:
130
+
92
131
  .. mermaid::
93
132
 
94
133
  flowchart LR
95
- Call(["iter_interpreters(key)"]) --> Mode{"key is None?"}
96
- Mode -->|"narrow"| N1["*/bin/python"]
97
- Mode -->|"narrow"| N2["*/python.exe"]
98
- Mode -->|"wide"| W1["*/bin/pypy*"]
99
- Mode -->|"wide"| W2["*/bin/graalpy"]
100
- Mode -->|"wide"| W3["*/pypy*.exe"]
101
- Mode -->|"wide"| W4["*/bin/graalpy.exe"]
102
-
103
- N1 --> Dedup[/"realpath dedup"/]
104
- N2 --> Dedup
105
- W1 --> Dedup
106
- W2 --> Dedup
107
- W3 --> Dedup
108
- W4 --> Dedup
109
-
110
- Dedup --> Interrogate(["subprocess interrogation"])
111
-
112
- style Call fill:#4a90d9,stroke:#2a5f8f,color:#fff
113
- style Mode fill:#d9904a,stroke:#8f5f2a,color:#fff
114
- style N1 fill:#3a7fc2,stroke:#1f4d7a,color:#fff
115
- style N2 fill:#3a7fc2,stroke:#1f4d7a,color:#fff
116
- style W1 fill:#9f4ad9,stroke:#5f2a8f,color:#fff
117
- style W2 fill:#9f4ad9,stroke:#5f2a8f,color:#fff
118
- style W3 fill:#9f4ad9,stroke:#5f2a8f,color:#fff
119
- style W4 fill:#9f4ad9,stroke:#5f2a8f,color:#fff
120
- style Dedup fill:#c2873a,stroke:#7a4c1f,color:#fff
134
+ Dirs[/"store subdirectories"/] --> Parse{"name is a uv key?"}
135
+ Parse -->|"no"| Last["probe last"]
136
+ Parse -->|"yes"| Filter["drop what the spec rules out:<br>implementation, version, free-threaded, debug"]
137
+ Filter --> Sort["CPython → PyPy → GraalPy,<br>newest version first,<br>default build before variants"]
138
+ Sort --> Interrogate(["subprocess interrogation"])
139
+ Last --> Interrogate
140
+
141
+ style Dirs fill:#4a90d9,stroke:#2a5f8f,color:#fff
142
+ style Parse fill:#d9904a,stroke:#8f5f2a,color:#fff
143
+ style Filter fill:#9f4ad9,stroke:#5f2a8f,color:#fff
144
+ style Sort fill:#c2873a,stroke:#7a4c1f,color:#fff
121
145
  style Interrogate fill:#4a9f4a,stroke:#2a6f2a,color:#fff
122
146
 
123
- GraalPy keeps its ``bin/`` segment on Windows (an upstream choice in uv); PyPy and CPython do not. python-discovery
124
- globs all of these patterns regardless of the host OS, because globs that do not match anything are essentially
125
- free, and the cross-platform list is short. Symlinked aliases inside an install (``bin/python``,
126
- ``bin/python3``, ``bin/python3.14`` all pointing at the same real file) are deduplicated by resolved path before
127
- the subprocess interrogation, so each install is interrogated once.
147
+ python-discovery reads the variant suffix the same way :class:`~python_discovery.PythonSpec` does: ``3.14t`` wants
148
+ ``+freethreaded``, ``3.14d`` wants ``+debug``. A spec that says nothing about a variant leaves it unconstrained, so
149
+ ``3.14`` still reaches a ``+debug`` install, after the default build rather than instead of it.
150
+
151
+ A bare version spec such as ``3.8`` matches CPython only, which is what uv resolves it to and what the equivalent
152
+ ``python3.8`` probe on ``PATH`` finds in practice. Ask for ``pypy3.8`` or enumerate with
153
+ :func:`~python_discovery.iter_interpreters` to reach the other implementations - PyPy ships a ``bin/python`` symlink,
154
+ so a store-wide glob would otherwise hand back PyPy for ``3.8`` whenever the filesystem listed it first.
155
+
156
+ Two names carry less information than they appear to. GraalPy keys named after the GraalVM release
157
+ (``graalpy-24.1.1``, from older uv versions) do not constrain the version, and uv's minor version links
158
+ (``cpython-3.15`` pointing at ``cpython-3.15.0b3``) move on the next patch release, so the concrete install ranks
159
+ ahead of the link. Anything that is not a uv key - a hand-made ``UV_PYTHON_INSTALL_DIR`` layout, say - stays in
160
+ the running, at the back of the queue.
128
161
 
129
162
  Selecting one interpreter vs. enumerating all of them
130
163
  -------------------------------------------------------
@@ -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",
@@ -44,22 +45,6 @@ dynamic = [
44
45
  ]
45
46
  dependencies = [
46
47
  "filelock>=3.15.4",
47
- "platformdirs<5,>=4.3.6",
48
- ]
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
48
  ]
64
49
  urls.Changelog = "https://github.com/tox-dev/python-discovery/releases"
65
50
  urls.Documentation = "https://python-discovery.readthedocs.io"
@@ -68,6 +53,21 @@ urls.Source = "https://github.com/tox-dev/python-discovery"
68
53
  urls.Tracker = "https://github.com/tox-dev/python-discovery/issues"
69
54
 
70
55
  [dependency-groups]
56
+ test = [
57
+ "covdefaults>=2.3",
58
+ "coverage>=7.5.4",
59
+ "pytest>=8.3.5",
60
+ "pytest-mock>=3.14",
61
+ "setuptools>=75.1",
62
+ ]
63
+ docs = [
64
+ "furo>=2025.12.19",
65
+ "sphinx>=9.1",
66
+ "sphinx-autodoc-typehints>=3.6.3",
67
+ "sphinxcontrib-mermaid>=2",
68
+ "sphinxcontrib-towncrier>=0.4",
69
+ "towncrier>=25.8",
70
+ ]
71
71
  release = [
72
72
  "gitpython>=3.1.46",
73
73
  "packaging>=26",
@@ -145,7 +145,7 @@ write-changes = true
145
145
  count = true
146
146
 
147
147
  [tool.pyproject-fmt]
148
- max_supported_python = "3.14"
148
+ max_supported_python = "3.15"
149
149
 
150
150
  [tool.ty]
151
151
  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,