Skip to content

Commit 98b331e

Browse files
authored
✨ feat(interpreter): increase query timeout to 15s with override (#53)
1 parent 7d30958 commit 98b331e

File tree

7 files changed

+119
-1
lines changed

7 files changed

+119
-1
lines changed

docs/explanation.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,26 @@ with `filelock <https://py-filelock.readthedocs.io/>`_-based locking for safe co
9090
can also pass ``cache=None`` to disable caching, or implement your own backend (see
9191
:doc:`/how-to/standalone-usage`).
9292

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+
93113
Spec format reference
94114
-----------------------
95115

docs/how-to/standalone-usage.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,26 @@ shell. You can override these to control exactly where the library looks.
4242
env = {**os.environ, "PATH": "/usr/local/bin:/usr/bin"}
4343
result = get_interpreter("python3.12", env=env)
4444
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+
4565
Read interpreter metadata
4666
---------------------------
4767

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ repeated lookups are fast.
4040
:hidden:
4141

4242
reference/api
43+
reference/environment-variables
4344

4445
.. toctree::
4546
:caption: Explanation
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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`

docs/tutorial/getting-started.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,30 @@ Every call will run a subprocess to query the interpreter, so this is slower for
178178
from python_discovery import get_interpreter
179179
180180
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)

src/python_discovery/_cached_py_info.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ def _run_subprocess(
189189
) -> tuple[Exception | None, PythonInfo | None]:
190190
start_cookie = gen_cookie()
191191
end_cookie = gen_cookie()
192+
timeout = float(env.get("PY_DISCOVERY_TIMEOUT", "15"))
192193
with _resolve_py_info_script() as py_info_script:
193194
cmd = [exe, str(py_info_script), start_cookie, end_cookie]
194195
env = dict(env)
@@ -206,7 +207,7 @@ def _run_subprocess(
206207
encoding="utf-8",
207208
errors="backslashreplace",
208209
)
209-
out, err = process.communicate(timeout=5)
210+
out, err = process.communicate(timeout=timeout)
210211
code = process.returncode
211212
except TimeoutExpired:
212213
process.kill()

tests/test_cached_py_info.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,17 @@ def test_run_subprocess_timeout(mocker: MockerFixture) -> None:
123123
assert mock_process.communicate.call_count == 2
124124

125125

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+
126137
def test_run_subprocess_nonzero_exit(mocker: MockerFixture) -> None:
127138
mock_process = MagicMock()
128139
mock_process.communicate.return_value = ("some output", "some error")

0 commit comments

Comments
 (0)