python-discovery 1.1.3__tar.gz → 1.2.0__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.0}/.pre-commit-config.yaml +2 -2
  2. {python_discovery-1.1.3 → python_discovery-1.2.0}/PKG-INFO +1 -1
  3. {python_discovery-1.1.3 → python_discovery-1.2.0}/docs/explanation.rst +20 -0
  4. {python_discovery-1.1.3 → python_discovery-1.2.0}/docs/how-to/standalone-usage.rst +20 -0
  5. {python_discovery-1.1.3 → python_discovery-1.2.0}/docs/index.rst +1 -0
  6. python_discovery-1.2.0/docs/reference/environment-variables.rst +38 -0
  7. {python_discovery-1.1.3 → python_discovery-1.2.0}/docs/tutorial/getting-started.rst +27 -0
  8. {python_discovery-1.1.3 → python_discovery-1.2.0}/src/python_discovery/_cached_py_info.py +2 -1
  9. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/test_cached_py_info.py +11 -0
  10. {python_discovery-1.1.3 → python_discovery-1.2.0}/.github/FUNDING.yaml +0 -0
  11. {python_discovery-1.1.3 → python_discovery-1.2.0}/.github/SECURITY.md +0 -0
  12. {python_discovery-1.1.3 → python_discovery-1.2.0}/.github/dependabot.yaml +0 -0
  13. {python_discovery-1.1.3 → python_discovery-1.2.0}/.github/release.yaml +0 -0
  14. {python_discovery-1.1.3 → python_discovery-1.2.0}/.github/workflows/check.yaml +0 -0
  15. {python_discovery-1.1.3 → python_discovery-1.2.0}/.github/workflows/release.yaml +0 -0
  16. {python_discovery-1.1.3 → python_discovery-1.2.0}/.gitignore +0 -0
  17. {python_discovery-1.1.3 → python_discovery-1.2.0}/.readthedocs.yaml +0 -0
  18. {python_discovery-1.1.3 → python_discovery-1.2.0}/.readthedocs.yml +0 -0
  19. {python_discovery-1.1.3 → python_discovery-1.2.0}/CODE_OF_CONDUCT.md +0 -0
  20. {python_discovery-1.1.3 → python_discovery-1.2.0}/LICENSE +0 -0
  21. {python_discovery-1.1.3 → python_discovery-1.2.0}/README.md +0 -0
  22. {python_discovery-1.1.3 → python_discovery-1.2.0}/docs/_static/custom.css +0 -0
  23. {python_discovery-1.1.3 → python_discovery-1.2.0}/docs/_static/logo.svg +0 -0
  24. {python_discovery-1.1.3 → python_discovery-1.2.0}/docs/conf.py +0 -0
  25. {python_discovery-1.1.3 → python_discovery-1.2.0}/docs/reference/api.rst +0 -0
  26. {python_discovery-1.1.3 → python_discovery-1.2.0}/pyproject.toml +0 -0
  27. {python_discovery-1.1.3 → python_discovery-1.2.0}/src/python_discovery/__init__.py +0 -0
  28. {python_discovery-1.1.3 → python_discovery-1.2.0}/src/python_discovery/_cache.py +0 -0
  29. {python_discovery-1.1.3 → python_discovery-1.2.0}/src/python_discovery/_compat.py +0 -0
  30. {python_discovery-1.1.3 → python_discovery-1.2.0}/src/python_discovery/_discovery.py +0 -0
  31. {python_discovery-1.1.3 → python_discovery-1.2.0}/src/python_discovery/_py_info.py +0 -0
  32. {python_discovery-1.1.3 → python_discovery-1.2.0}/src/python_discovery/_py_spec.py +0 -0
  33. {python_discovery-1.1.3 → python_discovery-1.2.0}/src/python_discovery/_specifier.py +0 -0
  34. {python_discovery-1.1.3 → python_discovery-1.2.0}/src/python_discovery/_windows/__init__.py +0 -0
  35. {python_discovery-1.1.3 → python_discovery-1.2.0}/src/python_discovery/_windows/_pep514.py +0 -0
  36. {python_discovery-1.1.3 → python_discovery-1.2.0}/src/python_discovery/_windows/_propose.py +0 -0
  37. {python_discovery-1.1.3 → python_discovery-1.2.0}/src/python_discovery/py.typed +0 -0
  38. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/conftest.py +0 -0
  39. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/py_info/test_py_info.py +0 -0
  40. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/py_info/test_py_info_exe_based_of.py +0 -0
  41. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/test_cache.py +0 -0
  42. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/test_discovery.py +0 -0
  43. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/test_discovery_extra.py +0 -0
  44. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/test_py_info_extra.py +0 -0
  45. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/test_py_spec.py +0 -0
  46. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/test_py_spec_extra.py +0 -0
  47. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/test_specifier.py +0 -0
  48. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/windows/conftest.py +0 -0
  49. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/windows/test_windows.py +0 -0
  50. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/windows/test_windows_pep514.py +0 -0
  51. {python_discovery-1.1.3 → python_discovery-1.2.0}/tests/windows/winreg_mock_values.py +0 -0
  52. {python_discovery-1.1.3 → python_discovery-1.2.0}/tox.toml +0 -0
@@ -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.18.1"
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.6"
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.3
3
+ Version: 1.2.0
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)
@@ -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()
@@ -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")