python-discovery 1.1.1__tar.gz → 1.1.3__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 (52) hide show
  1. {python_discovery-1.1.1 → python_discovery-1.1.3}/.pre-commit-config.yaml +2 -2
  2. {python_discovery-1.1.1 → python_discovery-1.1.3}/PKG-INFO +38 -3
  3. python_discovery-1.1.3/README.md +43 -0
  4. {python_discovery-1.1.1 → python_discovery-1.1.3}/src/python_discovery/_cached_py_info.py +1 -0
  5. {python_discovery-1.1.1 → python_discovery-1.1.3}/src/python_discovery/_py_info.py +17 -5
  6. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/py_info/test_py_info.py +1 -1
  7. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/test_cached_py_info.py +2 -1
  8. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/test_py_info_extra.py +22 -18
  9. python_discovery-1.1.1/README.md +0 -8
  10. {python_discovery-1.1.1 → python_discovery-1.1.3}/.github/FUNDING.yaml +0 -0
  11. {python_discovery-1.1.1 → python_discovery-1.1.3}/.github/SECURITY.md +0 -0
  12. {python_discovery-1.1.1 → python_discovery-1.1.3}/.github/dependabot.yaml +0 -0
  13. {python_discovery-1.1.1 → python_discovery-1.1.3}/.github/release.yaml +0 -0
  14. {python_discovery-1.1.1 → python_discovery-1.1.3}/.github/workflows/check.yaml +0 -0
  15. {python_discovery-1.1.1 → python_discovery-1.1.3}/.github/workflows/release.yaml +0 -0
  16. {python_discovery-1.1.1 → python_discovery-1.1.3}/.gitignore +0 -0
  17. {python_discovery-1.1.1 → python_discovery-1.1.3}/.readthedocs.yaml +0 -0
  18. {python_discovery-1.1.1 → python_discovery-1.1.3}/.readthedocs.yml +0 -0
  19. {python_discovery-1.1.1 → python_discovery-1.1.3}/CODE_OF_CONDUCT.md +0 -0
  20. {python_discovery-1.1.1 → python_discovery-1.1.3}/LICENSE +0 -0
  21. {python_discovery-1.1.1 → python_discovery-1.1.3}/docs/_static/custom.css +0 -0
  22. {python_discovery-1.1.1 → python_discovery-1.1.3}/docs/_static/logo.svg +0 -0
  23. {python_discovery-1.1.1 → python_discovery-1.1.3}/docs/conf.py +0 -0
  24. {python_discovery-1.1.1 → python_discovery-1.1.3}/docs/explanation.rst +0 -0
  25. {python_discovery-1.1.1 → python_discovery-1.1.3}/docs/how-to/standalone-usage.rst +0 -0
  26. {python_discovery-1.1.1 → python_discovery-1.1.3}/docs/index.rst +0 -0
  27. {python_discovery-1.1.1 → python_discovery-1.1.3}/docs/reference/api.rst +0 -0
  28. {python_discovery-1.1.1 → python_discovery-1.1.3}/docs/tutorial/getting-started.rst +0 -0
  29. {python_discovery-1.1.1 → python_discovery-1.1.3}/pyproject.toml +0 -0
  30. {python_discovery-1.1.1 → python_discovery-1.1.3}/src/python_discovery/__init__.py +0 -0
  31. {python_discovery-1.1.1 → python_discovery-1.1.3}/src/python_discovery/_cache.py +0 -0
  32. {python_discovery-1.1.1 → python_discovery-1.1.3}/src/python_discovery/_compat.py +0 -0
  33. {python_discovery-1.1.1 → python_discovery-1.1.3}/src/python_discovery/_discovery.py +0 -0
  34. {python_discovery-1.1.1 → python_discovery-1.1.3}/src/python_discovery/_py_spec.py +0 -0
  35. {python_discovery-1.1.1 → python_discovery-1.1.3}/src/python_discovery/_specifier.py +0 -0
  36. {python_discovery-1.1.1 → python_discovery-1.1.3}/src/python_discovery/_windows/__init__.py +0 -0
  37. {python_discovery-1.1.1 → python_discovery-1.1.3}/src/python_discovery/_windows/_pep514.py +0 -0
  38. {python_discovery-1.1.1 → python_discovery-1.1.3}/src/python_discovery/_windows/_propose.py +0 -0
  39. {python_discovery-1.1.1 → python_discovery-1.1.3}/src/python_discovery/py.typed +0 -0
  40. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/conftest.py +0 -0
  41. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/py_info/test_py_info_exe_based_of.py +0 -0
  42. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/test_cache.py +0 -0
  43. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/test_discovery.py +0 -0
  44. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/test_discovery_extra.py +0 -0
  45. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/test_py_spec.py +0 -0
  46. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/test_py_spec_extra.py +0 -0
  47. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/test_specifier.py +0 -0
  48. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/windows/conftest.py +0 -0
  49. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/windows/test_windows.py +0 -0
  50. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/windows/test_windows_pep514.py +0 -0
  51. {python_discovery-1.1.1 → python_discovery-1.1.3}/tests/windows/winreg_mock_values.py +0 -0
  52. {python_discovery-1.1.1 → python_discovery-1.1.3}/tox.toml +0 -0
@@ -10,7 +10,7 @@ repos:
10
10
  - id: check-github-workflows
11
11
  args: ["--verbose"]
12
12
  - repo: https://github.com/codespell-project/codespell
13
- rev: v2.4.1
13
+ rev: v2.4.2
14
14
  hooks:
15
15
  - id: codespell
16
16
  additional_dependencies: ["tomli>=2.4"]
@@ -19,7 +19,7 @@ repos:
19
19
  hooks:
20
20
  - id: pyproject-fmt
21
21
  - repo: https://github.com/astral-sh/ruff-pre-commit
22
- rev: "v0.15.4"
22
+ rev: "v0.15.5"
23
23
  hooks:
24
24
  - id: ruff-format
25
25
  - id: ruff
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-discovery
3
- Version: 1.1.1
3
+ Version: 1.1.3
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
@@ -64,8 +64,43 @@ Description-Content-Type: text/markdown
64
64
  # [`python-discovery`](https://python-discovery.readthedocs.io/en/latest/)
65
65
 
66
66
  [![PyPI](https://img.shields.io/pypi/v/python-discovery?style=flat-square)](https://pypi.org/project/python-discovery/)
67
- [![Supported Python
68
- versions](https://img.shields.io/pypi/pyversions/python-discovery.svg)](https://pypi.org/project/python-discovery/)
67
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/python-discovery.svg)](https://pypi.org/project/python-discovery/)
69
68
  [![Downloads](https://static.pepy.tech/badge/python-discovery/month)](https://pepy.tech/project/python-discovery)
70
69
  [![check](https://github.com/tox-dev/python-discovery/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/python-discovery/actions/workflows/check.yaml)
71
70
  [![Documentation Status](https://readthedocs.org/projects/python-discovery/badge/?version=latest)](https://python-discovery.readthedocs.io/en/latest/?badge=latest)
71
+
72
+ ## What is python-discovery?
73
+
74
+ `python-discovery` is a library for discovering Python interpreters installed on your machine. You may have multiple
75
+ Python versions from system packages, [pyenv](https://github.com/pyenv/pyenv), [mise](https://mise.jdx.dev/),
76
+ [asdf](https://asdf-vm.com/), [uv](https://docs.astral.sh/uv/), or the Windows registry (PEP 514). This library finds
77
+ the right one for you.
78
+
79
+ Give it a requirement like `python3.12` or `>=3.11,<3.13`, and it searches all known locations, verifies each candidate,
80
+ and returns detailed metadata about the match. Results are cached to disk so repeated lookups are fast.
81
+
82
+ ## Usage
83
+
84
+ ```python
85
+ from pathlib import Path
86
+
87
+ from python_discovery import DiskCache, get_interpreter
88
+
89
+ cache = DiskCache(root=Path("~/.cache/python-discovery").expanduser())
90
+ result = get_interpreter("python3.12", cache=cache)
91
+ if result is not None:
92
+ print(result.executable) # /usr/bin/python3.12
93
+ print(result.implementation) # CPython
94
+ print(result.version_info[:3]) # (3, 12, 1)
95
+ ```
96
+
97
+ The `get_interpreter()` function accepts various specification formats:
98
+
99
+ - Absolute path: `/usr/bin/python3.12`
100
+ - Version: `3.12`
101
+ - Implementation prefix: `cpython3.12`
102
+ - PEP 440 specifier: `>=3.10`, `>=3.11,<3.13`
103
+
104
+ ## Documentation
105
+
106
+ Full documentation is available at [python-discovery.readthedocs.io](https://python-discovery.readthedocs.io/en/latest/)
@@ -0,0 +1,43 @@
1
+ # [`python-discovery`](https://python-discovery.readthedocs.io/en/latest/)
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/python-discovery?style=flat-square)](https://pypi.org/project/python-discovery/)
4
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/python-discovery.svg)](https://pypi.org/project/python-discovery/)
5
+ [![Downloads](https://static.pepy.tech/badge/python-discovery/month)](https://pepy.tech/project/python-discovery)
6
+ [![check](https://github.com/tox-dev/python-discovery/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/python-discovery/actions/workflows/check.yaml)
7
+ [![Documentation Status](https://readthedocs.org/projects/python-discovery/badge/?version=latest)](https://python-discovery.readthedocs.io/en/latest/?badge=latest)
8
+
9
+ ## What is python-discovery?
10
+
11
+ `python-discovery` is a library for discovering Python interpreters installed on your machine. You may have multiple
12
+ Python versions from system packages, [pyenv](https://github.com/pyenv/pyenv), [mise](https://mise.jdx.dev/),
13
+ [asdf](https://asdf-vm.com/), [uv](https://docs.astral.sh/uv/), or the Windows registry (PEP 514). This library finds
14
+ the right one for you.
15
+
16
+ Give it a requirement like `python3.12` or `>=3.11,<3.13`, and it searches all known locations, verifies each candidate,
17
+ and returns detailed metadata about the match. Results are cached to disk so repeated lookups are fast.
18
+
19
+ ## Usage
20
+
21
+ ```python
22
+ from pathlib import Path
23
+
24
+ from python_discovery import DiskCache, get_interpreter
25
+
26
+ cache = DiskCache(root=Path("~/.cache/python-discovery").expanduser())
27
+ result = get_interpreter("python3.12", cache=cache)
28
+ if result is not None:
29
+ print(result.executable) # /usr/bin/python3.12
30
+ print(result.implementation) # CPython
31
+ print(result.version_info[:3]) # (3, 12, 1)
32
+ ```
33
+
34
+ The `get_interpreter()` function accepts various specification formats:
35
+
36
+ - Absolute path: `/usr/bin/python3.12`
37
+ - Version: `3.12`
38
+ - Implementation prefix: `cpython3.12`
39
+ - PEP 440 specifier: `>=3.10`, `>=3.11,<3.13`
40
+
41
+ ## Documentation
42
+
43
+ Full documentation is available at [python-discovery.readthedocs.io](https://python-discovery.readthedocs.io/en/latest/)
@@ -210,6 +210,7 @@ def _run_subprocess(
210
210
  code = process.returncode
211
211
  except TimeoutExpired:
212
212
  process.kill()
213
+ process.communicate()
213
214
  out, err, code = "", "timed out", -1
214
215
  except OSError as os_error:
215
216
  out, err, code = "", os_error.strerror, os_error.errno
@@ -469,12 +469,24 @@ class PythonInfo: # noqa: PLR0904
469
469
  if spec.version_specifier is None: # pragma: no cover
470
470
  return True
471
471
  version_info = self.version_info
472
- release = f"{version_info.major}.{version_info.minor}.{version_info.micro}"
473
- if version_info.releaselevel != "final":
474
- suffix = {"alpha": "a", "beta": "b", "candidate": "rc"}.get(version_info.releaselevel)
475
- if suffix is not None: # pragma: no branch # releaselevel is always alpha/beta/candidate here
472
+ for specifier in spec.version_specifier:
473
+ assert specifier.version is not None # noqa: S101
474
+ numeric_version = specifier.version_str
475
+ for prefix in ("rc", "b", "a"):
476
+ if prefix in numeric_version:
477
+ numeric_version = numeric_version.split(prefix)[0]
478
+ break
479
+ precision = numeric_version.count(".") + 1
480
+ release = ".".join(str(c) for c in [version_info.major, version_info.minor, version_info.micro][:precision])
481
+ if (
482
+ version_info.releaselevel != "final"
483
+ and (precision == 3 or specifier.version.pre_type is not None) # noqa: PLR2004
484
+ and (suffix := {"alpha": "a", "beta": "b", "candidate": "rc"}.get(version_info.releaselevel))
485
+ ):
476
486
  release = f"{release}{suffix}{version_info.serial}"
477
- return spec.version_specifier.contains(release)
487
+ if not specifier.contains(release):
488
+ return False
489
+ return True
478
490
 
479
491
  _current_system = None
480
492
  _current = None
@@ -338,7 +338,7 @@ def test_py_info_machine_property() -> None:
338
338
  assert machine is not None
339
339
  assert isinstance(machine, str)
340
340
  assert len(machine) > 0
341
- known_isas = {"arm64", "i686", "ppc64", "ppc64le", "riscv64", "s390x", "x86", "x86_64"}
341
+ known_isas = {"arm64", "i686", "loongarch64", "ppc64", "ppc64le", "riscv64", "s390x", "x86", "x86_64"}
342
342
  assert machine in known_isas, f"unexpected machine value: {machine}"
343
343
 
344
344
 
@@ -113,13 +113,14 @@ def test_run_subprocess_with_cookies(mocker: MockerFixture) -> None:
113
113
 
114
114
  def test_run_subprocess_timeout(mocker: MockerFixture) -> None:
115
115
  mock_process = MagicMock()
116
- mock_process.communicate.side_effect = TimeoutExpired(cmd="python", timeout=30)
116
+ mock_process.communicate.side_effect = [TimeoutExpired(cmd="python", timeout=30), ("", "")]
117
117
  mocker.patch("python_discovery._cached_py_info.Popen", return_value=mock_process)
118
118
  failure, result = _run_subprocess(PythonInfo, sys.executable, dict(os.environ))
119
119
  assert failure is not None
120
120
  assert "timed out" in str(failure)
121
121
  assert result is None
122
122
  mock_process.kill.assert_called_once()
123
+ assert mock_process.communicate.call_count == 2
123
124
 
124
125
 
125
126
  def test_run_subprocess_nonzero_exit(mocker: MockerFixture) -> None:
@@ -276,25 +276,29 @@ def test_satisfies_version_specifier_fails() -> None:
276
276
  assert CURRENT.satisfies(spec, impl_must_match=False) is False
277
277
 
278
278
 
279
- def test_satisfies_prerelease_version() -> None:
279
+ @pytest.mark.parametrize(
280
+ ("version_info", "spec_str", "expected"),
281
+ [
282
+ pytest.param(VersionInfo(3, 14, 0, "alpha", 1), ">=3.14.0a1", True, id="alpha_match_exact"),
283
+ pytest.param(VersionInfo(3, 14, 0, "beta", 1), ">=3.14.0b1", True, id="beta_match_exact"),
284
+ pytest.param(VersionInfo(3, 14, 0, "candidate", 1), ">=3.14.0rc1", True, id="rc_match_exact"),
285
+ pytest.param(VersionInfo(3, 15, 0, "alpha", 6), ">=3.15", True, id="prerelease_match_major_minor"),
286
+ pytest.param(VersionInfo(3, 15, 0, "alpha", 6), ">=3.15.0", False, id="prerelease_not_match_full_precision"),
287
+ pytest.param(VersionInfo(3, 15, 0, "alpha", 5), "<3.15.0a6", True, id="earlier_prerelease_less_than"),
288
+ pytest.param(VersionInfo(3, 15, 0, "alpha", 6), "<3.15.0a6", False, id="prerelease_not_less_than_itself"),
289
+ pytest.param(VersionInfo(3, 15, 0, "alpha", 6), ">=3.15.0a6", True, id="prerelease_match_itself"),
290
+ pytest.param(VersionInfo(3, 15, 0, "alpha", 6), ">=3.15.0a7", False, id="prerelease_not_match_later"),
291
+ pytest.param(VersionInfo(3, 15, 0, "final", 0), ">=3.15.0a6", True, id="final_greater_than_prerelease"),
292
+ pytest.param(VersionInfo(3, 15, 0, "final", 0), "<3.15.0a6", False, id="final_not_less_than_prerelease"),
293
+ pytest.param(VersionInfo(3, 15, 0, "final", 0), ">=3.15", True, id="final_match_major_minor"),
294
+ pytest.param(VersionInfo(3, 15, 1, "alpha", 1), ">=3.15.0", True, id="later_micro_prerelease_match"),
295
+ ],
296
+ )
297
+ def test_satisfies_version_specifier_prerelease(version_info: VersionInfo, spec_str: str, expected: bool) -> None:
280
298
  info = copy.deepcopy(CURRENT)
281
- info.version_info = VersionInfo(3, 14, 0, "alpha", 1)
282
- spec = PythonSpec.from_string_spec(">=3.14.0a1")
283
- assert info.satisfies(spec, impl_must_match=False) is True
284
-
285
-
286
- def test_satisfies_prerelease_beta() -> None:
287
- info = copy.deepcopy(CURRENT)
288
- info.version_info = VersionInfo(3, 14, 0, "beta", 1)
289
- spec = PythonSpec.from_string_spec(">=3.14.0b1")
290
- assert info.satisfies(spec, impl_must_match=False) is True
291
-
292
-
293
- def test_satisfies_prerelease_candidate() -> None:
294
- info = copy.deepcopy(CURRENT)
295
- info.version_info = VersionInfo(3, 14, 0, "candidate", 1)
296
- spec = PythonSpec.from_string_spec(">=3.14.0rc1")
297
- assert info.satisfies(spec, impl_must_match=False) is True
299
+ info.version_info = version_info
300
+ spec = PythonSpec.from_string_spec(spec_str)
301
+ assert info.satisfies(spec, impl_must_match=False) is expected
298
302
 
299
303
 
300
304
  def test_satisfies_path_not_abs_basename_match() -> None:
@@ -1,8 +0,0 @@
1
- # [`python-discovery`](https://python-discovery.readthedocs.io/en/latest/)
2
-
3
- [![PyPI](https://img.shields.io/pypi/v/python-discovery?style=flat-square)](https://pypi.org/project/python-discovery/)
4
- [![Supported Python
5
- versions](https://img.shields.io/pypi/pyversions/python-discovery.svg)](https://pypi.org/project/python-discovery/)
6
- [![Downloads](https://static.pepy.tech/badge/python-discovery/month)](https://pepy.tech/project/python-discovery)
7
- [![check](https://github.com/tox-dev/python-discovery/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/python-discovery/actions/workflows/check.yaml)
8
- [![Documentation Status](https://readthedocs.org/projects/python-discovery/badge/?version=latest)](https://python-discovery.readthedocs.io/en/latest/?badge=latest)