python-discovery 1.5.0__py3-none-any.whl → 1.5.1__py3-none-any.whl
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/_discovery.py +147 -20
- python_discovery/_specifier.py +7 -4
- {python_discovery-1.5.0.dist-info → python_discovery-1.5.1.dist-info}/METADATA +1 -2
- {python_discovery-1.5.0.dist-info → python_discovery-1.5.1.dist-info}/RECORD +6 -6
- {python_discovery-1.5.0.dist-info → python_discovery-1.5.1.dist-info}/WHEEL +0 -0
- {python_discovery-1.5.0.dist-info → python_discovery-1.5.1.dist-info}/licenses/LICENSE +0 -0
python_discovery/_discovery.py
CHANGED
|
@@ -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]:
|
python_discovery/_specifier.py
CHANGED
|
@@ -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",
|
|
@@ -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/)
|
|
@@ -2,15 +2,15 @@ python_discovery/__init__.py,sha256=Hi4j3pMvC8Fo8KoNL-wOHsPSCUHwY4c0qvv5sjnFzME,
|
|
|
2
2
|
python_discovery/_cache.py,sha256=svmG2GzUQzd2xTjAbm2eX1CKxxGAglfv4kxaPuxwPIA,5147
|
|
3
3
|
python_discovery/_cached_py_info.py,sha256=l9Y9UJGjKjp4M7iR9HfXYzW3hWdQ3KyNOD5IbSEphLw,8659
|
|
4
4
|
python_discovery/_compat.py,sha256=iqxGuhK9rPaiui80G1H1KrweoMJ16gyLYEmUrSXrp5c,709
|
|
5
|
-
python_discovery/_discovery.py,sha256=
|
|
5
|
+
python_discovery/_discovery.py,sha256=ABPD8MRweUO6B7cG3U3ZBfUIYs08rh6dYPA225EURyY,22032
|
|
6
6
|
python_discovery/_py_info.py,sha256=sXsOX4epGDRhrLvEcpDThh3r1yKMf5KZVaF2ZegoSOA,37311
|
|
7
7
|
python_discovery/_py_spec.py,sha256=O1RMC9JQ7m5SRzF6cEELxnIWZpadXYWO4bWDD7TemUs,10965
|
|
8
|
-
python_discovery/_specifier.py,sha256=
|
|
8
|
+
python_discovery/_specifier.py,sha256=o7MHb0z_mFSLz9gXqMwJoNkHYjgi2biq3Ibvd0_slvc,10457
|
|
9
9
|
python_discovery/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
python_discovery/_windows/__init__.py,sha256=qthcyGh36b3WkeSbZ4ECRRYLbCOCaQu5X_DRZ98tXfk,315
|
|
11
11
|
python_discovery/_windows/_pep514.py,sha256=1TqLTdofSCPu3VFE7D4u2fXweyaqhKJR7b48VmJ9AQQ,7787
|
|
12
12
|
python_discovery/_windows/_propose.py,sha256=iMJf6l0TU4SvuV8UsCHHINAe9L5dUT949xyeyMVdong,1565
|
|
13
|
-
python_discovery-1.5.
|
|
14
|
-
python_discovery-1.5.
|
|
15
|
-
python_discovery-1.5.
|
|
16
|
-
python_discovery-1.5.
|
|
13
|
+
python_discovery-1.5.1.dist-info/METADATA,sha256=DWitu_0XNhVQusKTT4aIXioAlImTpg-XH4Yp5ZhHbRc,4980
|
|
14
|
+
python_discovery-1.5.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
15
|
+
python_discovery-1.5.1.dist-info/licenses/LICENSE,sha256=kOJeH68qSq6o2V7o5_VziwUqGswMnFgGgQH5Mahr1Yg,1023
|
|
16
|
+
python_discovery-1.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|