Skip to content

Commit

Permalink
Merge pull request frostming#1 from frostming:fix/python-matching
Browse files Browse the repository at this point in the history
Fix the regex to decide whether a name looks like python
  • Loading branch information
frostming authored Feb 22, 2022
2 parents 8f76981 + 8b5b1ee commit 8a98593
Show file tree
Hide file tree
Showing 7 changed files with 44 additions and 27 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"},
Expand Down
2 changes: 1 addition & 1 deletion src/findpython/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
15 changes: 10 additions & 5 deletions src/findpython/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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."""
Expand Down
20 changes: 3 additions & 17 deletions src/findpython/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
import hashlib
import os
import re
import subprocess
import sys
from functools import lru_cache
from pathlib import Path

VERSION_RE = re.compile(
r"(?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<patch>[0-9]+))?\.?"
r"(?P<major>\d+)(?:\.(?P<minor>\d+)(?:\.(?P<patch>[0-9]+))?)?\.?"
r"(?:(?P<prerel>[abc]|rc|dev)(?:(?P<prerelversion>\d+(?:\.\d+)*))?)"
r"?(?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?"
r"(?:-(?P<architecture>32|64))?"
Expand All @@ -33,8 +32,8 @@
else:
KNOWN_EXTS = ("", ".sh", ".bash", ".csh", ".zsh", ".fish", ".py")
PY_MATCH_STR = (
r"((?P<implementation>{0})(?:\d?(?:\.\d[cpm]{{0,3}}))?"
r"(?:-?[\d\.]+)*(?!w))(?:{1})$".format(
r"((?P<implementation>{0})(?:\d(?:\.?\d\d?[cpm]{{0,3}})?)?"
r"(?:-[\d\.]+)*(?!w))(?P<suffix>{1})$".format(
"|".join(PYTHON_IMPLEMENTATIONS),
"|".join(KNOWN_EXTS),
)
Expand Down Expand Up @@ -96,19 +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)).decode("utf-8")


@lru_cache(maxsize=1024)
def get_binary_hash(path: Path) -> str:
"""Return the MD5 hash of the given file."""
Expand Down
4 changes: 2 additions & 2 deletions tests/test_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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


Expand Down
2 changes: 1 addition & 1 deletion tests/test_posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
26 changes: 26 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 8a98593

Please sign in to comment.