diff --git a/docs/explanation.rst b/docs/explanation.rst index 3b65f4f..2d1bc22 100644 --- a/docs/explanation.rst +++ b/docs/explanation.rst @@ -90,6 +90,26 @@ with `filelock `_-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 ----------------------- diff --git a/docs/how-to/standalone-usage.rst b/docs/how-to/standalone-usage.rst index 576de3e..adf151e 100644 --- a/docs/how-to/standalone-usage.rst +++ b/docs/how-to/standalone-usage.rst @@ -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 --------------------------- diff --git a/docs/index.rst b/docs/index.rst index 6e1e622..06a4590 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,6 +40,7 @@ repeated lookups are fast. :hidden: reference/api + reference/environment-variables .. toctree:: :caption: Explanation diff --git a/docs/reference/environment-variables.rst b/docs/reference/environment-variables.rst new file mode 100644 index 0000000..b0249d1 --- /dev/null +++ b/docs/reference/environment-variables.rst @@ -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` diff --git a/docs/tutorial/getting-started.rst b/docs/tutorial/getting-started.rst index 6d59713..696efcf 100644 --- a/docs/tutorial/getting-started.rst +++ b/docs/tutorial/getting-started.rst @@ -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) diff --git a/src/python_discovery/_cached_py_info.py b/src/python_discovery/_cached_py_info.py index 5a7d35d..80bc837 100644 --- a/src/python_discovery/_cached_py_info.py +++ b/src/python_discovery/_cached_py_info.py @@ -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) @@ -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() diff --git a/tests/test_cached_py_info.py b/tests/test_cached_py_info.py index 55d65b8..dad4fc4 100644 --- a/tests/test_cached_py_info.py +++ b/tests/test_cached_py_info.py @@ -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")