Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/explanation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,26 @@ with `filelock <https://py-filelock.readthedocs.io/>`_-based locking for safe co
can also pass ``cache=None`` to disable caching, or implement your own backend (see
:doc:`/how-to/standalone-usage`).

Subprocess timeout behavior
----------------------------

When python-discovery verifies an interpreter candidate, it runs a subprocess to query its metadata.
On slow systems (especially Windows), Python startup can take significant time. The default timeout
is **15 seconds** to balance responsiveness with accommodation for real-world conditions.

If your system consistently hits timeouts, you can customize the timeout via the
``PY_DISCOVERY_TIMEOUT`` environment variable (in seconds):

.. code-block:: console

# Increase timeout to 30 seconds
export PY_DISCOVERY_TIMEOUT=30
python -c "from python_discovery import get_interpreter; get_interpreter('python3.12')"

The timeout applies to each individual interpreter being queried. If you set a value that is too low,
legitimate interpreters may be skipped; if too high, the discovery process may take longer to fail
when encountering problematic interpreters.

Spec format reference
-----------------------

Expand Down
20 changes: 20 additions & 0 deletions docs/how-to/standalone-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ shell. You can override these to control exactly where the library looks.
env = {**os.environ, "PATH": "/usr/local/bin:/usr/bin"}
result = get_interpreter("python3.12", env=env)

Customize interpreter query timeout
-------------------------------------

On slower systems (especially Windows), Python startup can take more than the default 15 seconds.
If your discovery process times out when looking for interpreters, you can extend the timeout via
the ``PY_DISCOVERY_TIMEOUT`` environment variable.

.. code-block:: python

import os

from python_discovery import get_interpreter

# Increase timeout to 30 seconds for slow environments
env = {**os.environ, "PY_DISCOVERY_TIMEOUT": "30"}
result = get_interpreter("python3.12", env=env)

The timeout value should be a number in seconds. Each interpreter candidate is given this much time
to respond. If a timeout occurs, the candidate is skipped and the search continues with the next one.

Read interpreter metadata
---------------------------

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ repeated lookups are fast.
:hidden:

reference/api
reference/environment-variables

.. toctree::
:caption: Explanation
Expand Down
38 changes: 38 additions & 0 deletions docs/reference/environment-variables.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Environment Variables
======================

``PY_DISCOVERY_TIMEOUT``
------------------------

Controls the timeout for querying individual Python interpreters.

**Type:** Float (seconds)

**Default:** ``15``

**Description:**

When python-discovery verifies an interpreter candidate, it runs a subprocess to collect metadata
(version, architecture, platform, etc.). On slower systems—particularly Windows with antivirus
software or other tools—Python startup can exceed the default timeout.

Setting this variable extends the allowed time for each interpreter query.

**Examples:**

.. code-block:: bash

# Allow interpreters 30 seconds to respond
export PY_DISCOVERY_TIMEOUT=30

# Or pass in Python
import os
os.environ["PY_DISCOVERY_TIMEOUT"] = "30"

**Notes:**

- The timeout applies per candidate, not to the entire discovery process
- If a candidate times out, it is skipped and discovery continues with the next one
- Setting the value too low may skip legitimate interpreters
- Setting it too high increases discovery time when encountering problematic interpreters
- The value is read from the environment dict passed to :func:`~python_discovery.get_interpreter`
27 changes: 27 additions & 0 deletions docs/tutorial/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,30 @@ Every call will run a subprocess to query the interpreter, so this is slower for
from python_discovery import get_interpreter

result = get_interpreter("python3.12")

Handling slow interpreter queries
----------------------------------

On some systems (especially Windows with antivirus or other tools), Python startup is slow. If discovery
times out, increase the timeout using the ``PY_DISCOVERY_TIMEOUT`` environment variable.

.. code-block:: python

import os

from python_discovery import get_interpreter

# Allow up to 30 seconds per interpreter
os.environ["PY_DISCOVERY_TIMEOUT"] = "30"
result = get_interpreter("python3.12", cache=cache)

Or, pass it directly in a custom environment dict:

.. code-block:: python

import os

from python_discovery import get_interpreter

env = {**os.environ, "PY_DISCOVERY_TIMEOUT": "30"}
result = get_interpreter("python3.12", env=env, cache=cache)
3 changes: 2 additions & 1 deletion src/python_discovery/_cached_py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ def _run_subprocess(
) -> tuple[Exception | None, PythonInfo | None]:
start_cookie = gen_cookie()
end_cookie = gen_cookie()
timeout = float(env.get("PY_DISCOVERY_TIMEOUT", "15"))
with _resolve_py_info_script() as py_info_script:
cmd = [exe, str(py_info_script), start_cookie, end_cookie]
env = dict(env)
Expand All @@ -206,7 +207,7 @@ def _run_subprocess(
encoding="utf-8",
errors="backslashreplace",
)
out, err = process.communicate(timeout=5)
out, err = process.communicate(timeout=timeout)
code = process.returncode
except TimeoutExpired:
process.kill()
Expand Down
11 changes: 11 additions & 0 deletions tests/test_cached_py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ def test_run_subprocess_timeout(mocker: MockerFixture) -> None:
assert mock_process.communicate.call_count == 2


def test_run_subprocess_custom_timeout(mocker: MockerFixture) -> None:
mock_process = MagicMock()
mock_process.communicate.return_value = (json.dumps(PythonInfo().to_dict()), "")
mock_process.returncode = 0
mocker.patch("python_discovery._cached_py_info.Popen", return_value=mock_process)
env = dict(os.environ)
env["PY_DISCOVERY_TIMEOUT"] = "30"
_run_subprocess(PythonInfo, sys.executable, env)
mock_process.communicate.assert_called_once_with(timeout=30.0)


def test_run_subprocess_nonzero_exit(mocker: MockerFixture) -> None:
mock_process = MagicMock()
mock_process.communicate.return_value = ("some output", "some error")
Expand Down