Skip to content

Commit 30f67d2

Browse files
authored
🐛 fix(subprocess): add timeout to interpreter probing (#42)
1 parent 1ee6488 commit 30f67d2

3 files changed

Lines changed: 19 additions & 3 deletions

File tree

src/python_discovery/_cached_py_info.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from contextlib import contextmanager
1616
from pathlib import Path
1717
from shlex import quote
18-
from subprocess import Popen # noqa: S404
18+
from subprocess import Popen, TimeoutExpired # noqa: S404
1919
from typing import TYPE_CHECKING, Final
2020

2121
from ._cache import NoOpCache
@@ -206,8 +206,11 @@ def _run_subprocess(
206206
encoding="utf-8",
207207
errors="backslashreplace",
208208
)
209-
out, err = process.communicate()
209+
out, err = process.communicate(timeout=5)
210210
code = process.returncode
211+
except TimeoutExpired:
212+
process.kill()
213+
out, err, code = "", "timed out", -1
211214
except OSError as os_error:
212215
out, err, code = "", os_error.strerror, os_error.errno
213216
if code != 0:

tests/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ def _ensure_py_info_cache_empty(session_cache: DiskCache) -> Generator[None]:
2626
def _skip_if_test_in_system(session_cache: DiskCache) -> None:
2727
current = PythonInfo.current(session_cache)
2828
if current.system_executable is not None: # pragma: no cover
29-
pytest.skip("test not valid if run under system")
29+
msg = "test not valid if run under system"
30+
raise pytest.skip.Exception(msg)

tests/test_cached_py_info.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import sys
77
from pathlib import Path
8+
from subprocess import TimeoutExpired
89
from typing import TYPE_CHECKING
910
from unittest.mock import MagicMock, patch
1011

@@ -110,6 +111,17 @@ def test_run_subprocess_with_cookies(mocker: MockerFixture) -> None:
110111
assert mock_stdout.write.call_count == 2
111112

112113

114+
def test_run_subprocess_timeout(mocker: MockerFixture) -> None:
115+
mock_process = MagicMock()
116+
mock_process.communicate.side_effect = TimeoutExpired(cmd="python", timeout=30)
117+
mocker.patch("python_discovery._cached_py_info.Popen", return_value=mock_process)
118+
failure, result = _run_subprocess(PythonInfo, sys.executable, dict(os.environ))
119+
assert failure is not None
120+
assert "timed out" in str(failure)
121+
assert result is None
122+
mock_process.kill.assert_called_once()
123+
124+
113125
def test_run_subprocess_nonzero_exit(mocker: MockerFixture) -> None:
114126
mock_process = MagicMock()
115127
mock_process.communicate.return_value = ("some output", "some error")

0 commit comments

Comments
 (0)