python-discovery 1.5.0__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.
- {python_discovery-1.5.0 → python_discovery-1.5.1}/.github/workflows/check.yaml +2 -2
- {python_discovery-1.5.0 → python_discovery-1.5.1}/.github/workflows/prepare-release.yaml +2 -2
- {python_discovery-1.5.0 → python_discovery-1.5.1}/.github/workflows/release.yaml +3 -3
- {python_discovery-1.5.0 → python_discovery-1.5.1}/.pre-commit-config.yaml +3 -3
- {python_discovery-1.5.0 → python_discovery-1.5.1}/PKG-INFO +1 -2
- {python_discovery-1.5.0 → python_discovery-1.5.1}/docs/changelog.rst +18 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/docs/explanation.rst +67 -34
- {python_discovery-1.5.0 → python_discovery-1.5.1}/pyproject.toml +0 -1
- {python_discovery-1.5.0 → python_discovery-1.5.1}/src/python_discovery/_discovery.py +147 -20
- {python_discovery-1.5.0 → python_discovery-1.5.1}/src/python_discovery/_specifier.py +7 -4
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/test_discovery.py +132 -61
- python_discovery-1.5.1/tests/test_iter_interpreters.py +423 -0
- python_discovery-1.5.0/tests/test_iter_interpreters.py +0 -218
- {python_discovery-1.5.0 → python_discovery-1.5.1}/.github/FUNDING.yaml +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/.github/SECURITY.md +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/.github/dependabot.yaml +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/.github/release.yaml +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/.gitignore +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/.readthedocs.yaml +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/.readthedocs.yml +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/CODE_OF_CONDUCT.md +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/LICENSE +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/README.md +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/docs/_static/custom.css +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/docs/_static/logo.svg +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/docs/changelog/template.jinja2 +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/docs/conf.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/docs/how-to/standalone-usage.rst +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/docs/index.rst +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/docs/reference/api.rst +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/docs/reference/environment-variables.rst +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/docs/tutorial/getting-started.rst +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/src/python_discovery/__init__.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/src/python_discovery/_cache.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/src/python_discovery/_cached_py_info.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/src/python_discovery/_compat.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/src/python_discovery/_py_info.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/src/python_discovery/_py_spec.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/src/python_discovery/_windows/__init__.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/src/python_discovery/_windows/_pep514.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/src/python_discovery/_windows/_propose.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/src/python_discovery/py.typed +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tasks/release.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/conftest.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/py_info/test_py_info.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/py_info/test_py_info_exe_based_of.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/test_cache.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/test_cached_py_info.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/test_discovery_extra.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/test_py_info_extra.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/test_py_spec.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/test_py_spec_extra.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/test_specifier.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/windows/conftest.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/windows/test_windows.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/windows/test_windows_pep514.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tests/windows/winreg_mock_values.py +0 -0
- {python_discovery-1.5.0 → python_discovery-1.5.1}/tox.toml +0 -0
|
@@ -33,12 +33,12 @@ jobs:
|
|
|
33
33
|
- dev
|
|
34
34
|
- pkg_meta
|
|
35
35
|
steps:
|
|
36
|
-
- uses: actions/checkout@
|
|
36
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
37
37
|
with:
|
|
38
38
|
fetch-depth: 0
|
|
39
39
|
persist-credentials: false
|
|
40
40
|
- name: Install the latest version of uv
|
|
41
|
-
uses: astral-sh/setup-uv@
|
|
41
|
+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
42
42
|
with:
|
|
43
43
|
enable-cache: false
|
|
44
44
|
cache-dependency-glob: "pyproject.toml"
|
|
@@ -23,13 +23,13 @@ jobs:
|
|
|
23
23
|
permissions:
|
|
24
24
|
contents: write
|
|
25
25
|
steps:
|
|
26
|
-
- uses: actions/checkout@
|
|
26
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
27
27
|
with:
|
|
28
28
|
fetch-depth: 0
|
|
29
29
|
token: ${{ secrets.RELEASE_PAT }}
|
|
30
30
|
persist-credentials: true
|
|
31
31
|
- name: Install the latest version of uv
|
|
32
|
-
uses: astral-sh/setup-uv@
|
|
32
|
+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
33
33
|
with:
|
|
34
34
|
enable-cache: true
|
|
35
35
|
cache-dependency-glob: "pyproject.toml"
|
|
@@ -10,12 +10,12 @@ jobs:
|
|
|
10
10
|
build:
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/checkout@
|
|
13
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
14
14
|
with:
|
|
15
15
|
fetch-depth: 0
|
|
16
16
|
persist-credentials: false
|
|
17
17
|
- name: Install the latest version of uv
|
|
18
|
-
uses: astral-sh/setup-uv@
|
|
18
|
+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
19
19
|
with:
|
|
20
20
|
enable-cache: false
|
|
21
21
|
cache-dependency-glob: "pyproject.toml"
|
|
@@ -43,6 +43,6 @@ jobs:
|
|
|
43
43
|
name: ${{ env.dists-artifact-name }}
|
|
44
44
|
path: dist/
|
|
45
45
|
- name: Publish to PyPI
|
|
46
|
-
uses: pypa/gh-action-pypi-publish@
|
|
46
|
+
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
|
|
47
47
|
with:
|
|
48
48
|
attestations: true
|
|
@@ -15,11 +15,11 @@ repos:
|
|
|
15
15
|
- id: codespell
|
|
16
16
|
additional_dependencies: ["tomli>=2.4"]
|
|
17
17
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
|
18
|
-
rev: "v2.
|
|
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.
|
|
22
|
+
rev: "v0.16.0"
|
|
23
23
|
hooks:
|
|
24
24
|
- id: ruff-format
|
|
25
25
|
- id: ruff
|
|
@@ -39,7 +39,7 @@ repos:
|
|
|
39
39
|
- mdformat-gfm>=1
|
|
40
40
|
- mdformat-ruff>=0.1.3
|
|
41
41
|
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
|
42
|
-
rev: v1.
|
|
42
|
+
rev: v1.28.0
|
|
43
43
|
hooks:
|
|
44
44
|
- id: zizmor
|
|
45
45
|
- repo: meta
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-discovery
|
|
3
|
-
Version: 1.5.
|
|
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
|
|
@@ -48,7 +48,6 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
48
48
|
Classifier: Topic :: Utilities
|
|
49
49
|
Requires-Python: >=3.8
|
|
50
50
|
Requires-Dist: filelock>=3.15.4
|
|
51
|
-
Requires-Dist: platformdirs<5,>=4.3.6
|
|
52
51
|
Description-Content-Type: text/markdown
|
|
53
52
|
|
|
54
53
|
# [`python-discovery`](https://python-discovery.readthedocs.io/en/latest/)
|
|
@@ -6,6 +6,24 @@
|
|
|
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
|
+
|
|
9
27
|
********************
|
|
10
28
|
v1.5.0 (2026-07-21)
|
|
11
29
|
********************
|
|
@@ -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
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-------------------------------------------------------
|
|
@@ -4,14 +4,14 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import sys
|
|
6
6
|
from contextlib import suppress
|
|
7
|
+
from dataclasses import dataclass
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import TYPE_CHECKING, Final
|
|
9
10
|
|
|
10
|
-
from platformdirs import user_data_path
|
|
11
|
-
|
|
12
11
|
from ._compat import fs_path_id
|
|
13
12
|
from ._py_info import PythonInfo
|
|
14
13
|
from ._py_spec import PythonSpec
|
|
14
|
+
from ._specifier import DC_KW, SimpleVersion
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence
|
|
@@ -199,7 +199,7 @@ def propose_interpreters(
|
|
|
199
199
|
if spec.path is not None and spec.is_abs: # pragma: no cover # relative spec.path is never abs
|
|
200
200
|
return
|
|
201
201
|
yield from _propose_from_path(spec, cache, env, tested_exes, all_implementations=all_implementations)
|
|
202
|
-
yield from _propose_from_uv(cache, env, all_implementations=all_implementations)
|
|
202
|
+
yield from _propose_from_uv(spec, cache, env, all_implementations=all_implementations)
|
|
203
203
|
|
|
204
204
|
|
|
205
205
|
def _propose_explicit(
|
|
@@ -262,30 +262,157 @@ def _propose_from_path(
|
|
|
262
262
|
|
|
263
263
|
|
|
264
264
|
def _propose_from_uv(
|
|
265
|
+
spec: PythonSpec,
|
|
265
266
|
cache: PyInfoCache | None,
|
|
266
267
|
env: Mapping[str, str],
|
|
267
268
|
*,
|
|
268
269
|
all_implementations: bool = False,
|
|
269
270
|
) -> Generator[tuple[PythonInfo | None, bool], None, None]:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
271
|
+
seen_uv_paths: set[str] = set()
|
|
272
|
+
for root in _uv_python_roots(env):
|
|
273
|
+
for install in _uv_installs(root, spec, all_implementations=all_implementations):
|
|
274
|
+
for exe_path in _uv_executables(install):
|
|
275
|
+
if (resolved := str(exe_path.resolve())) in seen_uv_paths:
|
|
276
|
+
continue
|
|
277
|
+
seen_uv_paths.add(resolved)
|
|
278
|
+
if interpreter := PathPythonInfo.from_exe(str(exe_path), cache, raise_on_error=False, env=env):
|
|
279
|
+
yield interpreter, True
|
|
280
|
+
break # one install directory holds one interpreter
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _uv_python_roots(env: Mapping[str, str]) -> Generator[Path, None, None]:
|
|
284
|
+
"""Yield uv's store roots, legacy first; searching both keeps a stale legacy directory from hiding the live one."""
|
|
285
|
+
if install_dir := env.get("UV_PYTHON_INSTALL_DIR"):
|
|
286
|
+
yield Path(install_dir).expanduser()
|
|
287
|
+
return
|
|
288
|
+
for state_dir in _uv_state_dirs(env):
|
|
289
|
+
yield state_dir / "python"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _uv_state_dirs(env: Mapping[str, str]) -> Generator[Path, None, None]:
|
|
293
|
+
"""Yield uv's legacy state directory then its current one, the order uv prefers them in."""
|
|
294
|
+
windows = sys.platform == "win32"
|
|
295
|
+
home = Path(env.get("USERPROFILE" if windows else "HOME") or "~").expanduser()
|
|
296
|
+
if windows:
|
|
297
|
+
app_data = Path(env["APPDATA"]) if "APPDATA" in env else home / "AppData" / "Roaming"
|
|
298
|
+
yield app_data / "uv" / "data"
|
|
299
|
+
yield app_data / "uv"
|
|
300
|
+
return
|
|
301
|
+
if sys.platform == "darwin":
|
|
302
|
+
yield home / "Library" / "Application Support" / "uv"
|
|
303
|
+
# uv ignores a relative XDG_DATA_HOME, as the XDG specification requires
|
|
304
|
+
if (xdg_data_home := env.get("XDG_DATA_HOME")) and (xdg := Path(xdg_data_home)).is_absolute():
|
|
305
|
+
yield xdg / "uv"
|
|
274
306
|
else:
|
|
275
|
-
|
|
307
|
+
yield home / ".local" / "share" / "uv"
|
|
276
308
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
309
|
+
|
|
310
|
+
def _uv_installs(root: Path, spec: PythonSpec, *, all_implementations: bool) -> list[_UvInstall]:
|
|
311
|
+
try:
|
|
312
|
+
matching = [
|
|
313
|
+
install
|
|
314
|
+
for entry in sorted(root.iterdir())
|
|
315
|
+
if not entry.name.startswith(".")
|
|
316
|
+
and entry.is_dir()
|
|
317
|
+
and _uv_install_matches(install := _parse_uv_install(entry), spec, all_implementations=all_implementations)
|
|
318
|
+
]
|
|
319
|
+
except OSError:
|
|
320
|
+
return []
|
|
321
|
+
matching.sort(key=_uv_sort_key) # stable, so equally ranked installs stay in directory name order
|
|
322
|
+
return matching
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _parse_uv_install(path: Path) -> _UvInstall:
|
|
326
|
+
implementation, _, remainder = path.name.partition("-")
|
|
327
|
+
version_str, _, variant = remainder.partition("-")[0].partition("+")
|
|
328
|
+
try:
|
|
329
|
+
version = SimpleVersion.from_string(version_str)
|
|
330
|
+
except ValueError: # not a uv key, but the directory may still hold an interpreter worth probing
|
|
331
|
+
return _UvInstall(path=path, key=None)
|
|
332
|
+
variants = variant.split("+")
|
|
333
|
+
key = _UvKey(
|
|
334
|
+
implementation=implementation.lower(),
|
|
335
|
+
version=version,
|
|
336
|
+
free_threaded="freethreaded" in variants,
|
|
337
|
+
debug="debug" in variants,
|
|
338
|
+
)
|
|
339
|
+
return _UvInstall(path=path, key=key)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@dataclass(**DC_KW)
|
|
343
|
+
class _UvKey:
|
|
344
|
+
"""The ``<implementation>-<version>[+<variant>]-<os>-<arch>-<libc>`` name uv gives an install directory."""
|
|
345
|
+
|
|
346
|
+
implementation: str
|
|
347
|
+
version: SimpleVersion
|
|
348
|
+
free_threaded: bool
|
|
349
|
+
debug: bool
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@dataclass(**DC_KW)
|
|
353
|
+
class _UvInstall:
|
|
354
|
+
path: Path
|
|
355
|
+
key: _UvKey | None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _uv_install_matches(install: _UvInstall, spec: PythonSpec, *, all_implementations: bool) -> bool:
|
|
359
|
+
if (key := install.key) is None:
|
|
360
|
+
return True
|
|
361
|
+
if spec.implementation is not None:
|
|
362
|
+
if spec.implementation.lower() != key.implementation:
|
|
363
|
+
return False
|
|
364
|
+
elif not all_implementations and key.implementation != "cpython":
|
|
365
|
+
# uv resolves a bare `3.8` to `cpython-3.8`; on PATH the equivalent probe finds CPython too
|
|
366
|
+
return False
|
|
367
|
+
if spec.free_threaded is not None and spec.free_threaded != key.free_threaded:
|
|
368
|
+
return False
|
|
369
|
+
if spec.debug is not None and spec.debug != key.debug:
|
|
370
|
+
return False
|
|
371
|
+
return _uv_version_matches(key, spec)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _uv_version_matches(key: _UvKey, spec: PythonSpec) -> bool:
|
|
375
|
+
# GraalPy keys carry the GraalVM version in older uv releases, so only CPython and PyPy state a Python version
|
|
376
|
+
if key.implementation not in {"cpython", "pypy"}:
|
|
377
|
+
return True
|
|
378
|
+
if spec.version_specifier is not None and not spec.version_specifier.contains(str(key.version)):
|
|
379
|
+
return False
|
|
380
|
+
return all(req is None or our == req for our, req in zip(key.version.release, (spec.major, spec.minor, spec.micro)))
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _uv_sort_key(install: _UvInstall) -> tuple[int, ...]:
|
|
384
|
+
"""Rank like uv does: CPython first, newest version first, default build ahead of free-threaded and debug."""
|
|
385
|
+
impl_order: Final[tuple[str, ...]] = ("cpython", "pypy", "graalpy", "pyodide")
|
|
386
|
+
if (key := install.key) is None:
|
|
387
|
+
return (len(impl_order) + 1,)
|
|
388
|
+
version = key.version
|
|
389
|
+
return (
|
|
390
|
+
impl_order.index(key.implementation) if key.implementation in impl_order else len(impl_order),
|
|
391
|
+
# uv's minor version links point at a concrete install that moves on upgrade, so report the target instead
|
|
392
|
+
int(install.path.is_symlink()),
|
|
393
|
+
-version.major,
|
|
394
|
+
-version.minor,
|
|
395
|
+
-version.micro,
|
|
396
|
+
-{"a": 0, "b": 1, "rc": 2, None: 3}[version.pre_type],
|
|
397
|
+
-(version.pre_num or 0),
|
|
398
|
+
int(key.free_threaded),
|
|
399
|
+
int(key.debug),
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _uv_executables(install: _UvInstall) -> Generator[Path, None, None]:
|
|
404
|
+
# some distributions unpack into an `install` subdirectory, which uv resolves the same way
|
|
405
|
+
base = nested if (nested := install.path / "install").is_dir() else install.path
|
|
406
|
+
for pattern in (
|
|
407
|
+
"bin/python",
|
|
408
|
+
"python.exe",
|
|
409
|
+
"bin/pypy*",
|
|
410
|
+
"pypy*.exe",
|
|
411
|
+
"bin/graalpy",
|
|
412
|
+
"bin/graalpy.exe",
|
|
413
|
+
"python*t.exe", # free-threaded Windows builds that ship no python.exe (astral-sh/uv#8298)
|
|
414
|
+
):
|
|
415
|
+
yield from sorted(base.glob(pattern))
|
|
289
416
|
|
|
290
417
|
|
|
291
418
|
def get_paths(env: Mapping[str, str]) -> Generator[Path, None, None]:
|
|
@@ -9,7 +9,9 @@ import sys
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from typing import TYPE_CHECKING, Final
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
DC_KW: Final[dict[str, bool]] = (
|
|
13
|
+
{"frozen": True, "kw_only": True, "slots": True} if sys.version_info >= (3, 10) else {"frozen": True}
|
|
14
|
+
)
|
|
13
15
|
|
|
14
16
|
if TYPE_CHECKING:
|
|
15
17
|
from collections.abc import Iterator
|
|
@@ -38,7 +40,7 @@ _SPECIFIER_RE: Final[re.Pattern[str]] = re.compile(
|
|
|
38
40
|
_PRE_ORDER: Final[dict[str, int]] = {"a": 1, "b": 2, "rc": 3}
|
|
39
41
|
|
|
40
42
|
|
|
41
|
-
@dataclass(**
|
|
43
|
+
@dataclass(**DC_KW)
|
|
42
44
|
class SimpleVersion:
|
|
43
45
|
"""
|
|
44
46
|
Simple PEP 440-like version parser using only standard library.
|
|
@@ -125,7 +127,7 @@ class SimpleVersion:
|
|
|
125
127
|
return f"SimpleVersion('{self.version_str}')"
|
|
126
128
|
|
|
127
129
|
|
|
128
|
-
@dataclass(**
|
|
130
|
+
@dataclass(**DC_KW)
|
|
129
131
|
class SimpleSpecifier:
|
|
130
132
|
"""
|
|
131
133
|
Simple PEP 440-like version specifier using only standard library.
|
|
@@ -247,7 +249,7 @@ class SimpleSpecifier:
|
|
|
247
249
|
return f"SimpleSpecifier('{self.spec_str}')"
|
|
248
250
|
|
|
249
251
|
|
|
250
|
-
@dataclass(**
|
|
252
|
+
@dataclass(**DC_KW)
|
|
251
253
|
class SimpleSpecifierSet:
|
|
252
254
|
"""
|
|
253
255
|
Simple PEP 440-like specifier set using only standard library.
|
|
@@ -305,6 +307,7 @@ class SimpleSpecifierSet:
|
|
|
305
307
|
|
|
306
308
|
|
|
307
309
|
__all__ = [
|
|
310
|
+
"DC_KW",
|
|
308
311
|
"SimpleSpecifier",
|
|
309
312
|
"SimpleSpecifierSet",
|
|
310
313
|
"SimpleVersion",
|