From fdbacf3971c0ddcdf2f43b719164383cd7a8260f Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Tue, 22 Feb 2022 11:29:34 +0800 Subject: [PATCH 1/3] Fix the regex to decide whether a name looks like python --- src/findpython/utils.py | 10 ++++++---- tests/test_finder.py | 4 ++-- tests/test_posix.py | 2 +- tests/test_utils.py | 26 ++++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 tests/test_utils.py diff --git a/src/findpython/utils.py b/src/findpython/utils.py index ecdaefc..4b36976 100644 --- a/src/findpython/utils.py +++ b/src/findpython/utils.py @@ -9,7 +9,7 @@ from pathlib import Path VERSION_RE = re.compile( - r"(?P\d+)(?:\.(?P\d+))?(?:\.(?P[0-9]+))?\.?" + r"(?P\d+)(?:\.(?P\d+)(?:\.(?P[0-9]+))?)?\.?" r"(?:(?P[abc]|rc|dev)(?:(?P\d+(?:\.\d+)*))?)" r"?(?P(\.post(?P\d+))?(\.dev(?P\d+))?)?" r"(?:-(?P32|64))?" @@ -33,8 +33,8 @@ else: KNOWN_EXTS = ("", ".sh", ".bash", ".csh", ".zsh", ".fish", ".py") PY_MATCH_STR = ( - r"((?P{0})(?:\d?(?:\.\d[cpm]{{0,3}}))?" - r"(?:-?[\d\.]+)*(?!w))(?:{1})$".format( + r"((?P{0})(?:\d(?:\.?\d\d?[cpm]{{0,3}})?)?" + r"(?:-[\d\.]+)*(?!w))(?P{1})$".format( "|".join(PYTHON_IMPLEMENTATIONS), "|".join(KNOWN_EXTS), ) @@ -106,7 +106,9 @@ def subprocess_output(*args: str) -> str: :return: The output of the command. :rtype: str """ - return subprocess.check_output(list(args)).decode("utf-8") + return subprocess.check_output( + list(args), input=None, stderr=subprocess.DEVNULL + ).decode("utf-8") @lru_cache(maxsize=1024) diff --git a/tests/test_finder.py b/tests/test_finder.py index af2d5cd..2c2701e 100644 --- a/tests/test_finder.py +++ b/tests/test_finder.py @@ -82,7 +82,7 @@ def test_find_python_deduplicate_same_file(mocked_python, tmp_path, switch): finder = Finder(no_same_file=switch) all_pythons = finder.find_all() - assert len(all_pythons) == 3 if switch else 4 + assert len(all_pythons) == (3 if switch else 4) assert (new_python in all_pythons) is not switch @@ -97,7 +97,7 @@ def test_find_python_deduplicate_same_interpreter(mocked_python, tmp_path, switc finder = Finder(no_same_interpreter=switch) all_pythons = finder.find_all() - assert len(all_pythons) == 3 if switch else 4 + assert len(all_pythons) == (3 if switch else 4) assert (python in all_pythons) is not switch diff --git a/tests/test_posix.py b/tests/test_posix.py index 5f7b56c..3ee44fe 100644 --- a/tests/test_posix.py +++ b/tests/test_posix.py @@ -18,7 +18,7 @@ def test_find_python_resolve_symlinks(mocked_python, tmp_path, switch): python = mocked_python.add_python(link, "3.7.0") finder = Finder(resolve_symlinks=switch) all_pythons = finder.find_all() - assert len(all_pythons) == 3 if switch else 4 + assert len(all_pythons) == (3 if switch else 4) assert (python in all_pythons) is not switch diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..08db0fb --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,26 @@ +from findpython.utils import WINDOWS, looks_like_python +import pytest + + +matrix = [ + ("python", True), + ("python3", True), + ("python38", True), + ("python3.8", True), + ("python3.10", True), + ("python310", True), + ("python3.6m", True), + ("python3.6.8m", False), + ("anaconda-3.3.0", True), + ("unknown-2.0.0", False), + ("python3.8.unknown", False), + ("python38.bat", WINDOWS), + ("python38.exe", WINDOWS), + ("python38.sh", not WINDOWS), + ("python38.csh", not WINDOWS), +] + + +@pytest.mark.parametrize("name, expected", matrix) +def test_looks_like_python(name, expected): + assert looks_like_python(name) == expected From c31963bff0766d7112dc0de51502ef4ac0b01d9e Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Tue, 22 Feb 2022 11:44:07 +0800 Subject: [PATCH 2/3] bump version to 0.1.2 --- pyproject.toml | 2 +- src/findpython/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f462b72..b41696a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "findpython" -version = "0.1.1" +version = "0.1.2" description = "A utility to find python versions on your system" authors = [ {name = "Frost Ming", email = "mianghong@gmail.com"}, diff --git a/src/findpython/__init__.py b/src/findpython/__init__.py index a2dada2..eaa8c5b 100644 --- a/src/findpython/__init__.py +++ b/src/findpython/__init__.py @@ -8,7 +8,7 @@ from findpython.finder import Finder from findpython.python import PythonVersion -__version__ = "0.1.1" +__version__ = "0.1.2" def find(*args, **kwargs) -> PythonVersion | None: From 8b5b1eeb44014c49e8b1828623163e4aeccfa527 Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Tue, 22 Feb 2022 11:57:11 +0800 Subject: [PATCH 3/3] add timeout for getting version --- src/findpython/python.py | 15 ++++++++++----- src/findpython/utils.py | 16 ---------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/findpython/python.py b/src/findpython/python.py index 55d7c9d..2ff9e74 100644 --- a/src/findpython/python.py +++ b/src/findpython/python.py @@ -3,14 +3,16 @@ import dataclasses as dc import logging import subprocess +from functools import lru_cache from pathlib import Path from packaging.version import Version from packaging.version import parse as parse_version -from findpython.utils import get_binary_hash, subprocess_output +from findpython.utils import get_binary_hash logger = logging.getLogger("findpython") +GET_VERSION_TIMEOUT = 5 @dc.dataclass @@ -26,7 +28,7 @@ def is_valid(self) -> bool: """Return True if the python is not broken.""" try: v = self._get_version() - except (OSError, subprocess.CalledProcessError): + except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired): return False if not isinstance(v, Version): return False @@ -156,7 +158,7 @@ def __str__(self) -> str: def _get_version(self) -> Version: """Get the version of the python.""" script = "import platform; print(platform.python_version())" - version = self._run_script(script).strip() + version = self._run_script(script, timeout=GET_VERSION_TIMEOUT).strip() return parse_version(version) def _get_architecture(self) -> str: @@ -167,11 +169,14 @@ def _get_interpreter(self) -> str: script = "import sys; print(sys.executable)" return self._run_script(script).strip() - def _run_script(self, script: str) -> str: + @lru_cache(maxsize=1024) + def _run_script(self, script: str, timeout: float | None = None) -> str: """Run a script and return the output.""" command = [self.executable.as_posix(), "-c", script] logger.debug("Running script: %s", command) - return subprocess_output(*command) + return subprocess.check_output( + command, input=None, stderr=subprocess.DEVNULL, timeout=timeout + ).decode("utf-8") def __lt__(self, other: PythonVersion) -> bool: """Sort by the version, then by length of the executable path.""" diff --git a/src/findpython/utils.py b/src/findpython/utils.py index 4b36976..b1aab83 100644 --- a/src/findpython/utils.py +++ b/src/findpython/utils.py @@ -3,7 +3,6 @@ import hashlib import os import re -import subprocess import sys from functools import lru_cache from pathlib import Path @@ -96,21 +95,6 @@ def path_is_python(path: Path) -> bool: return path_is_known_executable(path) and looks_like_python(path.name) -@lru_cache(maxsize=1024) -def subprocess_output(*args: str) -> str: - """ - Run a command and return the output. - - :param cmd: The command to run. - :type cmd: list[str] - :return: The output of the command. - :rtype: str - """ - return subprocess.check_output( - list(args), input=None, stderr=subprocess.DEVNULL - ).decode("utf-8") - - @lru_cache(maxsize=1024) def get_binary_hash(path: Path) -> str: """Return the MD5 hash of the given file."""