python-discovery 1.1.3__tar.gz → 1.2.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 (52) hide show
  1. {python_discovery-1.1.3 → python_discovery-1.2.1}/.github/dependabot.yaml +2 -0
  2. {python_discovery-1.1.3 → python_discovery-1.2.1}/.github/workflows/check.yaml +4 -3
  3. {python_discovery-1.1.3 → python_discovery-1.2.1}/.github/workflows/release.yaml +7 -6
  4. {python_discovery-1.1.3 → python_discovery-1.2.1}/.pre-commit-config.yaml +6 -2
  5. {python_discovery-1.1.3 → python_discovery-1.2.1}/PKG-INFO +1 -1
  6. {python_discovery-1.1.3 → python_discovery-1.2.1}/docs/explanation.rst +20 -0
  7. {python_discovery-1.1.3 → python_discovery-1.2.1}/docs/how-to/standalone-usage.rst +20 -0
  8. {python_discovery-1.1.3 → python_discovery-1.2.1}/docs/index.rst +1 -0
  9. python_discovery-1.2.1/docs/reference/environment-variables.rst +38 -0
  10. {python_discovery-1.1.3 → python_discovery-1.2.1}/docs/tutorial/getting-started.rst +27 -0
  11. {python_discovery-1.1.3 → python_discovery-1.2.1}/src/python_discovery/__init__.py +2 -1
  12. {python_discovery-1.1.3 → python_discovery-1.2.1}/src/python_discovery/_cached_py_info.py +2 -1
  13. {python_discovery-1.1.3 → python_discovery-1.2.1}/src/python_discovery/_py_info.py +15 -1
  14. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/py_info/test_py_info.py +2 -3
  15. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/test_cached_py_info.py +11 -0
  16. {python_discovery-1.1.3 → python_discovery-1.2.1}/.github/FUNDING.yaml +0 -0
  17. {python_discovery-1.1.3 → python_discovery-1.2.1}/.github/SECURITY.md +0 -0
  18. {python_discovery-1.1.3 → python_discovery-1.2.1}/.github/release.yaml +0 -0
  19. {python_discovery-1.1.3 → python_discovery-1.2.1}/.gitignore +0 -0
  20. {python_discovery-1.1.3 → python_discovery-1.2.1}/.readthedocs.yaml +0 -0
  21. {python_discovery-1.1.3 → python_discovery-1.2.1}/.readthedocs.yml +0 -0
  22. {python_discovery-1.1.3 → python_discovery-1.2.1}/CODE_OF_CONDUCT.md +0 -0
  23. {python_discovery-1.1.3 → python_discovery-1.2.1}/LICENSE +0 -0
  24. {python_discovery-1.1.3 → python_discovery-1.2.1}/README.md +0 -0
  25. {python_discovery-1.1.3 → python_discovery-1.2.1}/docs/_static/custom.css +0 -0
  26. {python_discovery-1.1.3 → python_discovery-1.2.1}/docs/_static/logo.svg +0 -0
  27. {python_discovery-1.1.3 → python_discovery-1.2.1}/docs/conf.py +0 -0
  28. {python_discovery-1.1.3 → python_discovery-1.2.1}/docs/reference/api.rst +0 -0
  29. {python_discovery-1.1.3 → python_discovery-1.2.1}/pyproject.toml +17 -17
  30. {python_discovery-1.1.3 → python_discovery-1.2.1}/src/python_discovery/_cache.py +0 -0
  31. {python_discovery-1.1.3 → python_discovery-1.2.1}/src/python_discovery/_compat.py +0 -0
  32. {python_discovery-1.1.3 → python_discovery-1.2.1}/src/python_discovery/_discovery.py +0 -0
  33. {python_discovery-1.1.3 → python_discovery-1.2.1}/src/python_discovery/_py_spec.py +0 -0
  34. {python_discovery-1.1.3 → python_discovery-1.2.1}/src/python_discovery/_specifier.py +0 -0
  35. {python_discovery-1.1.3 → python_discovery-1.2.1}/src/python_discovery/_windows/__init__.py +0 -0
  36. {python_discovery-1.1.3 → python_discovery-1.2.1}/src/python_discovery/_windows/_pep514.py +0 -0
  37. {python_discovery-1.1.3 → python_discovery-1.2.1}/src/python_discovery/_windows/_propose.py +0 -0
  38. {python_discovery-1.1.3 → python_discovery-1.2.1}/src/python_discovery/py.typed +0 -0
  39. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/conftest.py +0 -0
  40. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/py_info/test_py_info_exe_based_of.py +0 -0
  41. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/test_cache.py +0 -0
  42. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/test_discovery.py +0 -0
  43. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/test_discovery_extra.py +0 -0
  44. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/test_py_info_extra.py +0 -0
  45. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/test_py_spec.py +0 -0
  46. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/test_py_spec_extra.py +0 -0
  47. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/test_specifier.py +0 -0
  48. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/windows/conftest.py +0 -0
  49. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/windows/test_windows.py +0 -0
  50. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/windows/test_windows_pep514.py +0 -0
  51. {python_discovery-1.1.3 → python_discovery-1.2.1}/tests/windows/winreg_mock_values.py +0 -0
  52. {python_discovery-1.1.3 → python_discovery-1.2.1}/tox.toml +0 -0
@@ -4,3 +4,5 @@ updates:
4
4
  directory: "/"
5
5
  schedule:
6
6
  interval: "daily"
7
+ cooldown:
8
+ default-days: 7
@@ -34,13 +34,14 @@ jobs:
34
34
  - dev
35
35
  - pkg_meta
36
36
  steps:
37
- - uses: actions/checkout@v6
37
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
38
38
  with:
39
39
  fetch-depth: 0
40
+ persist-credentials: false
40
41
  - name: Install the latest version of uv
41
- uses: astral-sh/setup-uv@v7
42
+ uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
42
43
  with:
43
- enable-cache: true
44
+ enable-cache: false
44
45
  cache-dependency-glob: "pyproject.toml"
45
46
  github-token: ${{ secrets.GITHUB_TOKEN }}
46
47
  - name: Install tox
@@ -13,19 +13,20 @@ jobs:
13
13
  build:
14
14
  runs-on: ubuntu-latest
15
15
  steps:
16
- - uses: actions/checkout@v6
16
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
17
17
  with:
18
18
  fetch-depth: 0
19
+ persist-credentials: false
19
20
  - name: Install the latest version of uv
20
- uses: astral-sh/setup-uv@v7
21
+ uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
21
22
  with:
22
- enable-cache: true
23
+ enable-cache: false
23
24
  cache-dependency-glob: "pyproject.toml"
24
25
  github-token: ${{ secrets.GITHUB_TOKEN }}
25
26
  - name: Build package
26
27
  run: uv build --python 3.14 --python-preference only-managed --sdist --wheel . --out-dir dist
27
28
  - name: Store the distribution packages
28
- uses: actions/upload-artifact@v7
29
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
29
30
  with:
30
31
  name: ${{ env.dists-artifact-name }}
31
32
  path: dist/*
@@ -41,11 +42,11 @@ jobs:
41
42
  id-token: write
42
43
  steps:
43
44
  - name: Download all the dists
44
- uses: actions/download-artifact@v8
45
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
45
46
  with:
46
47
  name: ${{ env.dists-artifact-name }}
47
48
  path: dist/
48
49
  - name: Publish to PyPI
49
- uses: pypa/gh-action-pypi-publish@v1.13.0
50
+ uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
50
51
  with:
51
52
  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.16.2"
18
+ rev: "v2.20.0"
19
19
  hooks:
20
20
  - id: pyproject-fmt
21
21
  - repo: https://github.com/astral-sh/ruff-pre-commit
22
- rev: "v0.15.5"
22
+ rev: "v0.15.7"
23
23
  hooks:
24
24
  - id: ruff-format
25
25
  - id: ruff
@@ -31,6 +31,10 @@ repos:
31
31
  additional_dependencies:
32
32
  - prettier@3.8.1
33
33
  - "@prettier/plugin-xml@3.4.2"
34
+ - repo: https://github.com/zizmorcore/zizmor-pre-commit
35
+ rev: v1.23.1
36
+ hooks:
37
+ - id: zizmor
34
38
  - repo: meta
35
39
  hooks:
36
40
  - id: check-hooks-apply
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-discovery
3
- Version: 1.1.3
3
+ Version: 1.2.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
@@ -90,6 +90,26 @@ with `filelock <https://py-filelock.readthedocs.io/>`_-based locking for safe co
90
90
  can also pass ``cache=None`` to disable caching, or implement your own backend (see
91
91
  :doc:`/how-to/standalone-usage`).
92
92
 
93
+ Subprocess timeout behavior
94
+ ----------------------------
95
+
96
+ When python-discovery verifies an interpreter candidate, it runs a subprocess to query its metadata.
97
+ On slow systems (especially Windows), Python startup can take significant time. The default timeout
98
+ is **15 seconds** to balance responsiveness with accommodation for real-world conditions.
99
+
100
+ If your system consistently hits timeouts, you can customize the timeout via the
101
+ ``PY_DISCOVERY_TIMEOUT`` environment variable (in seconds):
102
+
103
+ .. code-block:: console
104
+
105
+ # Increase timeout to 30 seconds
106
+ export PY_DISCOVERY_TIMEOUT=30
107
+ python -c "from python_discovery import get_interpreter; get_interpreter('python3.12')"
108
+
109
+ The timeout applies to each individual interpreter being queried. If you set a value that is too low,
110
+ legitimate interpreters may be skipped; if too high, the discovery process may take longer to fail
111
+ when encountering problematic interpreters.
112
+
93
113
  Spec format reference
94
114
  -----------------------
95
115
 
@@ -42,6 +42,26 @@ shell. You can override these to control exactly where the library looks.
42
42
  env = {**os.environ, "PATH": "/usr/local/bin:/usr/bin"}
43
43
  result = get_interpreter("python3.12", env=env)
44
44
 
45
+ Customize interpreter query timeout
46
+ -------------------------------------
47
+
48
+ On slower systems (especially Windows), Python startup can take more than the default 15 seconds.
49
+ If your discovery process times out when looking for interpreters, you can extend the timeout via
50
+ the ``PY_DISCOVERY_TIMEOUT`` environment variable.
51
+
52
+ .. code-block:: python
53
+
54
+ import os
55
+
56
+ from python_discovery import get_interpreter
57
+
58
+ # Increase timeout to 30 seconds for slow environments
59
+ env = {**os.environ, "PY_DISCOVERY_TIMEOUT": "30"}
60
+ result = get_interpreter("python3.12", env=env)
61
+
62
+ The timeout value should be a number in seconds. Each interpreter candidate is given this much time
63
+ to respond. If a timeout occurs, the candidate is skipped and the search continues with the next one.
64
+
45
65
  Read interpreter metadata
46
66
  ---------------------------
47
67
 
@@ -40,6 +40,7 @@ repeated lookups are fast.
40
40
  :hidden:
41
41
 
42
42
  reference/api
43
+ reference/environment-variables
43
44
 
44
45
  .. toctree::
45
46
  :caption: Explanation
@@ -0,0 +1,38 @@
1
+ Environment Variables
2
+ ======================
3
+
4
+ ``PY_DISCOVERY_TIMEOUT``
5
+ ------------------------
6
+
7
+ Controls the timeout for querying individual Python interpreters.
8
+
9
+ **Type:** Float (seconds)
10
+
11
+ **Default:** ``15``
12
+
13
+ **Description:**
14
+
15
+ When python-discovery verifies an interpreter candidate, it runs a subprocess to collect metadata
16
+ (version, architecture, platform, etc.). On slower systems—particularly Windows with antivirus
17
+ software or other tools—Python startup can exceed the default timeout.
18
+
19
+ Setting this variable extends the allowed time for each interpreter query.
20
+
21
+ **Examples:**
22
+
23
+ .. code-block:: bash
24
+
25
+ # Allow interpreters 30 seconds to respond
26
+ export PY_DISCOVERY_TIMEOUT=30
27
+
28
+ # Or pass in Python
29
+ import os
30
+ os.environ["PY_DISCOVERY_TIMEOUT"] = "30"
31
+
32
+ **Notes:**
33
+
34
+ - The timeout applies per candidate, not to the entire discovery process
35
+ - If a candidate times out, it is skipped and discovery continues with the next one
36
+ - Setting the value too low may skip legitimate interpreters
37
+ - Setting it too high increases discovery time when encountering problematic interpreters
38
+ - The value is read from the environment dict passed to :func:`~python_discovery.get_interpreter`
@@ -178,3 +178,30 @@ Every call will run a subprocess to query the interpreter, so this is slower for
178
178
  from python_discovery import get_interpreter
179
179
 
180
180
  result = get_interpreter("python3.12")
181
+
182
+ Handling slow interpreter queries
183
+ ----------------------------------
184
+
185
+ On some systems (especially Windows with antivirus or other tools), Python startup is slow. If discovery
186
+ times out, increase the timeout using the ``PY_DISCOVERY_TIMEOUT`` environment variable.
187
+
188
+ .. code-block:: python
189
+
190
+ import os
191
+
192
+ from python_discovery import get_interpreter
193
+
194
+ # Allow up to 30 seconds per interpreter
195
+ os.environ["PY_DISCOVERY_TIMEOUT"] = "30"
196
+ result = get_interpreter("python3.12", cache=cache)
197
+
198
+ Or, pass it directly in a custom environment dict:
199
+
200
+ .. code-block:: python
201
+
202
+ import os
203
+
204
+ from python_discovery import get_interpreter
205
+
206
+ env = {**os.environ, "PY_DISCOVERY_TIMEOUT": "30"}
207
+ result = get_interpreter("python3.12", env=env, cache=cache)
@@ -6,13 +6,14 @@ from importlib.metadata import version
6
6
 
7
7
  from ._cache import ContentStore, DiskCache, PyInfoCache
8
8
  from ._discovery import get_interpreter
9
- from ._py_info import PythonInfo
9
+ from ._py_info import KNOWN_ARCHITECTURES, PythonInfo
10
10
  from ._py_spec import PythonSpec
11
11
  from ._specifier import SimpleSpecifier, SimpleSpecifierSet, SimpleVersion
12
12
 
13
13
  __version__ = version("python-discovery")
14
14
 
15
15
  __all__ = [
16
+ "KNOWN_ARCHITECTURES",
16
17
  "ContentStore",
17
18
  "DiskCache",
18
19
  "PyInfoCache",
@@ -189,6 +189,7 @@ def _run_subprocess(
189
189
  ) -> tuple[Exception | None, PythonInfo | None]:
190
190
  start_cookie = gen_cookie()
191
191
  end_cookie = gen_cookie()
192
+ timeout = float(env.get("PY_DISCOVERY_TIMEOUT", "15"))
192
193
  with _resolve_py_info_script() as py_info_script:
193
194
  cmd = [exe, str(py_info_script), start_cookie, end_cookie]
194
195
  env = dict(env)
@@ -206,7 +207,7 @@ def _run_subprocess(
206
207
  encoding="utf-8",
207
208
  errors="backslashreplace",
208
209
  )
209
- out, err = process.communicate(timeout=5)
210
+ out, err = process.communicate(timeout=timeout)
210
211
  code = process.returncode
211
212
  except TimeoutExpired:
212
213
  process.kill()
@@ -768,9 +768,23 @@ class PythonInfo: # noqa: PLR0904
768
768
  yield upper
769
769
 
770
770
 
771
+ KNOWN_ARCHITECTURES: frozenset[str] = frozenset({
772
+ "arm64",
773
+ "i686",
774
+ "loongarch64",
775
+ "ppc64",
776
+ "ppc64le",
777
+ "riscv64",
778
+ "s390x",
779
+ "x86",
780
+ "x86_64",
781
+ })
782
+ """Known CPU architecture (ISA) values after normalization."""
783
+
784
+
771
785
  def normalize_isa(isa: str) -> str:
772
786
  low = isa.lower()
773
- return {"amd64": "x86_64", "aarch64": "arm64"}.get(low, low)
787
+ return {"amd64": "x86_64", "aarch64": "arm64", "i386": "x86", "i586": "x86"}.get(low, low)
774
788
 
775
789
 
776
790
  def _main() -> None: # pragma: no cover
@@ -14,7 +14,7 @@ from typing import TYPE_CHECKING, NamedTuple
14
14
  import pytest
15
15
  from setuptools.dist import Distribution
16
16
 
17
- from python_discovery import DiskCache, PythonInfo, PythonSpec
17
+ from python_discovery import KNOWN_ARCHITECTURES, DiskCache, PythonInfo, PythonSpec
18
18
  from python_discovery import _cached_py_info as cached_py_info
19
19
  from python_discovery._py_info import VersionInfo
20
20
 
@@ -338,8 +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", "loongarch64", "ppc64", "ppc64le", "riscv64", "s390x", "x86", "x86_64"}
342
- assert machine in known_isas, f"unexpected machine value: {machine}"
341
+ assert machine in KNOWN_ARCHITECTURES, f"unexpected machine value: {machine}"
343
342
 
344
343
 
345
344
  def test_py_info_machine_in_spec() -> None:
@@ -123,6 +123,17 @@ def test_run_subprocess_timeout(mocker: MockerFixture) -> None:
123
123
  assert mock_process.communicate.call_count == 2
124
124
 
125
125
 
126
+ def test_run_subprocess_custom_timeout(mocker: MockerFixture) -> None:
127
+ mock_process = MagicMock()
128
+ mock_process.communicate.return_value = (json.dumps(PythonInfo().to_dict()), "")
129
+ mock_process.returncode = 0
130
+ mocker.patch("python_discovery._cached_py_info.Popen", return_value=mock_process)
131
+ env = dict(os.environ)
132
+ env["PY_DISCOVERY_TIMEOUT"] = "30"
133
+ _run_subprocess(PythonInfo, sys.executable, env)
134
+ mock_process.communicate.assert_called_once_with(timeout=30.0)
135
+
136
+
126
137
  def test_run_subprocess_nonzero_exit(mocker: MockerFixture) -> None:
127
138
  mock_process = MagicMock()
128
139
  mock_process.communicate.return_value = ("some output", "some error")
@@ -129,6 +129,23 @@ count = true
129
129
  [tool.pyproject-fmt]
130
130
  max_supported_python = "3.14"
131
131
 
132
+ [tool.ty]
133
+ src.exclude = [ "tests/windows/winreg_mock_values.py" ]
134
+
135
+ [[tool.ty.overrides]]
136
+ include = [ "src/python_discovery/_py_info.py", "src/python_discovery/_py_spec.py" ]
137
+ rules.unused-ignore-comment = "ignore"
138
+ rules.invalid-argument-type = "ignore"
139
+ rules.invalid-return-type = "ignore"
140
+ rules.no-matching-overload = "ignore"
141
+
142
+ [[tool.ty.overrides]]
143
+ include = [ "tests/**/*.py" ]
144
+ rules.unused-ignore-comment = "ignore"
145
+ rules.invalid-argument-type = "ignore"
146
+ rules.no-matching-overload = "ignore"
147
+ rules.unresolved-attribute = "ignore"
148
+
132
149
  [tool.pytest]
133
150
  ini_options.markers = [ "graalpy" ]
134
151
 
@@ -162,20 +179,3 @@ report.partial_branches = [
162
179
  report.show_missing = true
163
180
  html.show_contexts = true
164
181
  html.skip_covered = false
165
-
166
- [tool.ty]
167
- src.exclude = [ "tests/windows/winreg_mock_values.py" ]
168
-
169
- [[tool.ty.overrides]]
170
- include = [ "src/python_discovery/_py_info.py", "src/python_discovery/_py_spec.py" ]
171
- rules.unused-ignore-comment = "ignore"
172
- rules.invalid-argument-type = "ignore"
173
- rules.invalid-return-type = "ignore"
174
- rules.no-matching-overload = "ignore"
175
-
176
- [[tool.ty.overrides]]
177
- include = [ "tests/**/*.py" ]
178
- rules.unused-ignore-comment = "ignore"
179
- rules.invalid-argument-type = "ignore"
180
- rules.no-matching-overload = "ignore"
181
- rules.unresolved-attribute = "ignore"