python-discovery 1.4.4__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.
@@ -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,
@@ -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
@@ -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,
@@ -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(
@@ -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):
@@ -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
- if uv_python_dir := os.getenv("UV_PYTHON_INSTALL_DIR"):
271
- uv_python_path = Path(uv_python_dir).expanduser()
272
- elif xdg_data_home := os.getenv("XDG_DATA_HOME"):
273
- uv_python_path = Path(xdg_data_home).expanduser() / "uv" / "python"
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
- uv_python_path = user_data_path("uv") / "python"
307
+ yield home / ".local" / "share" / "uv"
276
308
 
277
- patterns: list[str] = ["*/bin/python", "*/python.exe"]
278
- if all_implementations:
279
- patterns.extend(("*/bin/pypy*", "*/bin/graalpy", "*/pypy*.exe", "*/bin/graalpy.exe"))
280
- seen_uv_paths: set[str] = set()
281
- for pattern in patterns:
282
- for exe_path in uv_python_path.glob(pattern):
283
- resolved = str(Path(exe_path).resolve())
284
- if resolved in seen_uv_paths:
285
- continue
286
- seen_uv_paths.add(resolved)
287
- if interpreter := PathPythonInfo.from_exe(str(exe_path), cache, raise_on_error=False, env=env):
288
- yield interpreter, True
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]:
@@ -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
 
@@ -9,7 +9,9 @@ import sys
9
9
  from dataclasses import dataclass
10
10
  from typing import TYPE_CHECKING, Final
11
11
 
12
- _DC_KW = {"frozen": True, "kw_only": True, "slots": True} if sys.version_info >= (3, 10) else {"frozen": True}
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(**_DC_KW)
43
+ @dataclass(**DC_KW)
42
44
  class SimpleVersion:
43
45
  """
44
46
  Simple PEP 440-like version parser using only standard library.
@@ -92,7 +94,7 @@ class SimpleVersion:
92
94
  def __hash__(self) -> int:
93
95
  return hash((self.release, self.pre_type, self.pre_num))
94
96
 
95
- def __lt__(self, other: object) -> bool: # noqa: PLR0911
97
+ def __lt__(self, other: object) -> bool: # ruff:ignore[too-many-return-statements]
96
98
  if not isinstance(other, SimpleVersion):
97
99
  return NotImplemented
98
100
  if self.release != other.release:
@@ -125,7 +127,7 @@ class SimpleVersion:
125
127
  return f"SimpleVersion('{self.version_str}')"
126
128
 
127
129
 
128
- @dataclass(**_DC_KW)
130
+ @dataclass(**DC_KW)
129
131
  class SimpleSpecifier:
130
132
  """
131
133
  Simple PEP 440-like version specifier using only standard library.
@@ -225,7 +227,7 @@ class SimpleSpecifier:
225
227
  return False
226
228
  if candidate < self.version:
227
229
  return False
228
- if len(self.version.release) >= 2: # noqa: PLR2004 # pragma: no branch # SimpleVersion always has 3-part release
230
+ if len(self.version.release) >= 2: # ruff:ignore[magic-value-comparison] # pragma: no branch # SimpleVersion always has 3-part release
229
231
  upper_parts = list(self.version.release[:-1])
230
232
  upper_parts[-1] += 1
231
233
  upper = SimpleVersion.from_string(".".join(str(p) for p in upper_parts))
@@ -247,7 +249,7 @@ class SimpleSpecifier:
247
249
  return f"SimpleSpecifier('{self.spec_str}')"
248
250
 
249
251
 
250
- @dataclass(**_DC_KW)
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",
@@ -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):
@@ -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/)
@@ -0,0 +1,16 @@
1
+ python_discovery/__init__.py,sha256=Hi4j3pMvC8Fo8KoNL-wOHsPSCUHwY4c0qvv5sjnFzME,808
2
+ python_discovery/_cache.py,sha256=svmG2GzUQzd2xTjAbm2eX1CKxxGAglfv4kxaPuxwPIA,5147
3
+ python_discovery/_cached_py_info.py,sha256=l9Y9UJGjKjp4M7iR9HfXYzW3hWdQ3KyNOD5IbSEphLw,8659
4
+ python_discovery/_compat.py,sha256=iqxGuhK9rPaiui80G1H1KrweoMJ16gyLYEmUrSXrp5c,709
5
+ python_discovery/_discovery.py,sha256=ABPD8MRweUO6B7cG3U3ZBfUIYs08rh6dYPA225EURyY,22032
6
+ python_discovery/_py_info.py,sha256=sXsOX4epGDRhrLvEcpDThh3r1yKMf5KZVaF2ZegoSOA,37311
7
+ python_discovery/_py_spec.py,sha256=O1RMC9JQ7m5SRzF6cEELxnIWZpadXYWO4bWDD7TemUs,10965
8
+ python_discovery/_specifier.py,sha256=o7MHb0z_mFSLz9gXqMwJoNkHYjgi2biq3Ibvd0_slvc,10457
9
+ python_discovery/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ python_discovery/_windows/__init__.py,sha256=qthcyGh36b3WkeSbZ4ECRRYLbCOCaQu5X_DRZ98tXfk,315
11
+ python_discovery/_windows/_pep514.py,sha256=1TqLTdofSCPu3VFE7D4u2fXweyaqhKJR7b48VmJ9AQQ,7787
12
+ python_discovery/_windows/_propose.py,sha256=iMJf6l0TU4SvuV8UsCHHINAe9L5dUT949xyeyMVdong,1565
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,,
@@ -1,16 +0,0 @@
1
- python_discovery/__init__.py,sha256=Hi4j3pMvC8Fo8KoNL-wOHsPSCUHwY4c0qvv5sjnFzME,808
2
- python_discovery/_cache.py,sha256=bG4fD-h2pdPmb4v9BldbXmJ5Tlt1boiH1Qrz8kp9Dds,5063
3
- python_discovery/_cached_py_info.py,sha256=1AFEoc_-OaCi1PXmaFz9Iy_bEuqOhJdFSDFHljzD-EE,8540
4
- python_discovery/_compat.py,sha256=iqxGuhK9rPaiui80G1H1KrweoMJ16gyLYEmUrSXrp5c,709
5
- python_discovery/_discovery.py,sha256=cdo6xlktNMEwsQn2R8pn4T1VDE4LD2EJPUyCT7vlHnM,16830
6
- python_discovery/_py_info.py,sha256=fFhS-gZoYIJYscqM56h1RwFknJou8bgf3h8VXrqm5g8,36974
7
- python_discovery/_py_spec.py,sha256=FN7nsWOkxdFpmd5st5hz5e2f7egbLzzbRiNiBndAYZc,10877
8
- python_discovery/_specifier.py,sha256=PTh6suNTC0hMnFSxFYF_vggdciFLnwxoh1YflSbgCXA,10368
9
- python_discovery/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- python_discovery/_windows/__init__.py,sha256=qthcyGh36b3WkeSbZ4ECRRYLbCOCaQu5X_DRZ98tXfk,315
11
- python_discovery/_windows/_pep514.py,sha256=zXxje07QIXfrCB3eYbZHOBgXcvAQQpZ-ZWun3_zrapA,7688
12
- python_discovery/_windows/_propose.py,sha256=iMJf6l0TU4SvuV8UsCHHINAe9L5dUT949xyeyMVdong,1565
13
- python_discovery-1.4.4.dist-info/METADATA,sha256=15xJaaopcuujqLtLazKC4ZadMMCtHPWntqISeeYMFCQ,5592
14
- python_discovery-1.4.4.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
15
- python_discovery-1.4.4.dist-info/licenses/LICENSE,sha256=kOJeH68qSq6o2V7o5_VziwUqGswMnFgGgQH5Mahr1Yg,1023
16
- python_discovery-1.4.4.dist-info/RECORD,,