Further updates to pythonfinder

Signed-off-by: Dan Ryan <dan@danryan.co>
This commit is contained in:
Dan Ryan
2018-11-20 01:11:37 -05:00
parent 14d69fe07e
commit f374caccc6
2 changed files with 71 additions and 8 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
from __future__ import print_function, absolute_import
__version__ = '1.1.9'
__version__ = '1.1.10'
# Add NullHandler to "pythonfinder" logger, because Python2's default root
# logger has no handler and warnings like this would be reported:
+70 -7
View File
@@ -13,7 +13,9 @@ import six
import vistir
from .environment import PYENV_ROOT, ASDF_DATA_DIR
from packaging.version import LegacyVersion, Version
from .environment import PYENV_ROOT, ASDF_DATA_DIR, MYPY_RUNNING
from .exceptions import InvalidPythonVersion
six.add_move(six.MovedAttribute("Iterable", "collections", "collections.abc"))
@@ -24,6 +26,10 @@ try:
except ImportError:
from backports.functools_lru_cache import lru_cache
if MYPY_RUNNING:
from typing import Any, Union, List, Callable, Iterable, Set, Tuple, Dict, Optional
from attr.validators import _OptionalValidator
version_re = re.compile(r"(?P<major>\d+)\.(?P<minor>\d+)\.?(?P<patch>(?<=\.)[0-9]+)?\.?"
r"(?:(?P<prerel>[abc]|rc|dev)(?:(?P<prerelversion>\d+(?:\.\d+)*))?)"
@@ -54,6 +60,7 @@ for rule in RULES:
@lru_cache(maxsize=1024)
def get_python_version(path):
# type: (str) -> str
"""Get python version string using subprocess from a given path."""
version_cmd = [path, "-c", "import sys; print(sys.version.split()[0])"]
try:
@@ -68,28 +75,30 @@ def get_python_version(path):
@lru_cache(maxsize=1024)
def parse_python_version(version_str):
# type: (str) -> Dict[str, Union[str, int, Version]]
from packaging.version import parse as parse_version
is_debug = False
if version_str.endswith("-debug"):
is_debug = True
version_str, _, _ = version.rpartition("-")
version_str, _, _ = version_str.rpartition("-")
m = version_re.match(version_str)
if not m:
raise InvalidPythonVersion("%s is not a python version" % version_str)
version_dict = m.groupdict()
major = int(version_dict.get("major"))
minor = int(version_dict.get("minor"))
patch = version_dict.get("patch")
version_dict = m.groupdict() # type: Dict[str, str]
major = int(version_dict.get("major", 0)) if version_dict.get("major") else None
minor = int(version_dict.get("minor", 0)) if version_dict.get("minor") else None
patch = int(version_dict.get("patch", 0)) if version_dict.get("patch") else None
is_postrelease = True if version_dict.get("post") else False
is_prerelease = True if version_dict.get("prerel") else False
is_devrelease = True if version_dict.get("dev") else False
if patch:
patch = int(patch)
version = None # type: Optional[Union[Version, LegacyVersion]]
try:
version = parse_version(version_str)
except TypeError:
version_parts = [str(v) for v in [major, minor, patch] if v is not None]
version = parse_version(".".join(v))
version = parse_version(".".join(version_parts))
return {
"major": major,
"minor": minor,
@@ -103,15 +112,42 @@ def parse_python_version(version_str):
def optional_instance_of(cls):
# type: (Any) -> _OptionalValidator
"""
Return an validator to determine whether an input is an optional instance of a class.
:return: A validator to determine optional instance membership.
:rtype: :class:`~attr.validators._OptionalValidator`
"""
return attr.validators.optional(attr.validators.instance_of(cls))
def path_is_executable(path):
# type: (str) -> bool
"""
Determine whether the supplied path is executable.
:return: Whether the provided path is executable.
:rtype: bool
"""
return os.access(str(path), os.X_OK)
@lru_cache(maxsize=1024)
def path_is_known_executable(path):
# type: (vistir.compat.Path) -> bool
"""
Returns whether a given path is a known executable from known executable extensions
or has the executable bit toggled.
:param path: The path to the target executable.
:type path: :class:`~vistir.compat.Path`
:return: True if the path has chmod +x, or is a readable, known executable extension.
:rtype: bool
"""
return (
path_is_executable(path)
or os.access(str(path), os.R_OK)
@@ -121,6 +157,15 @@ def path_is_known_executable(path):
@lru_cache(maxsize=1024)
def looks_like_python(name):
# type: (str) -> bool
"""
Determine whether the supplied filename looks like a possible name of python.
:param str name: The name of the provided file.
:return: Whether the provided name looks like python.
:rtype: bool
"""
if not any(name.lower().startswith(py_name) for py_name in PYTHON_IMPLEMENTATIONS):
return False
return any(fnmatch(name, rule) for rule in MATCH_RULES)
@@ -128,11 +173,22 @@ def looks_like_python(name):
@lru_cache(maxsize=1024)
def path_is_python(path):
# type: (vistir.compat.Path) -> bool
"""
Determine whether the supplied path is executable and looks like a possible path to python.
:param path: The path to an executable.
:type path: :class:`~vistir.compat.Path`
:return: Whether the provided path is an executable path to python.
:rtype: bool
"""
return path_is_executable(path) and looks_like_python(path.name)
@lru_cache(maxsize=1024)
def ensure_path(path):
# type: (Union[vistir.compat.Path, str]) -> bool
"""
Given a path (either a string or a Path object), expand variables and return a Path object.
@@ -149,6 +205,7 @@ def ensure_path(path):
def _filter_none(k, v):
# type: (Any, Any) -> bool
if v:
return True
return False
@@ -156,6 +213,7 @@ def _filter_none(k, v):
# TODO: Reimplement in vistir
def normalize_path(path):
# type: (str) -> str
return os.path.normpath(os.path.normcase(
os.path.abspath(os.path.expandvars(os.path.expanduser(str(path))))
))
@@ -163,6 +221,7 @@ def normalize_path(path):
@lru_cache(maxsize=1024)
def filter_pythons(path):
# type: (Union[str, vistir.compat.Path]) -> Iterable
"""Return all valid pythons in a given path"""
if not isinstance(path, vistir.compat.Path):
path = vistir.compat.Path(str(path))
@@ -173,6 +232,8 @@ def filter_pythons(path):
# TODO: Port to vistir
def unnest(item):
# type: (Any) -> Iterable[Any]
target = None # type: Optional[Iterable]
if isinstance(item, Iterable) and not isinstance(item, six.string_types):
item, target = itertools.tee(item, 2)
else:
@@ -187,6 +248,7 @@ def unnest(item):
def parse_pyenv_version_order(filename="version"):
# type: (str) -> List[str]
version_order_file = normalize_path(os.path.join(PYENV_ROOT, filename))
if os.path.exists(version_order_file) and os.path.isfile(version_order_file):
with io.open(version_order_file, encoding="utf-8") as fh:
@@ -197,6 +259,7 @@ def parse_pyenv_version_order(filename="version"):
def parse_asdf_version_order(filename=".tool-versions"):
# type: (str) -> List[str]
version_order_file = normalize_path(os.path.join("~", filename))
if os.path.exists(version_order_file) and os.path.isfile(version_order_file):
with io.open(version_order_file, encoding="utf-8") as fh: