Merge pull request #3259 from pypa/bugfix/resilient-parser

Update pythonfinder for resilient parsing
This commit is contained in:
Dan Ryan
2018-11-20 11:31:52 -05:00
committed by GitHub
4 changed files with 112 additions and 48 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:
+10
View File
@@ -4,6 +4,15 @@ import os
import platform
import sys
def is_type_checking():
try:
from typing import TYPE_CHECKING
except ImportError:
return False
return TYPE_CHECKING
PYENV_INSTALLED = bool(os.environ.get("PYENV_SHELL")) or bool(
os.environ.get("PYENV_ROOT")
)
@@ -24,3 +33,4 @@ else:
IGNORE_UNSUPPORTED = bool(os.environ.get("PYTHONFINDER_IGNORE_UNSUPPORTED", False))
MYPY_RUNNING = os.environ.get("MYPY_RUNNING", is_type_checking())
+5 -44
View File
@@ -10,7 +10,7 @@ from collections import defaultdict
import attr
from packaging.version import Version
from packaging.version import Version, LegacyVersion
from packaging.version import parse as parse_version
from vistir.compat import Path
@@ -355,49 +355,10 @@ class PythonVersion(object):
:rtype: dict.
"""
is_debug = False
if version.endswith("-debug"):
is_debug = True
version, _, _ = version.rpartition("-")
try:
version = parse_version(str(version))
except TypeError:
try:
version_dict = parse_python_version(str(version))
except Exception:
raise ValueError("Unable to parse version: %s" % version)
else:
if not version_dict:
raise ValueError("Not a valid python version: %r" % version)
major = int(version_dict.get("major"))
minor = int(version_dict.get("minor"))
patch = version_dict.get("patch")
if patch:
patch = int(patch)
version = ".".join([v for v in [major, minor, patch] if v is not None])
version = parse_version(version)
else:
if not version or not version.release:
raise ValueError("Not a valid python version: %r" % version)
if len(version.release) >= 3:
major, minor, patch = version.release[:3]
elif len(version.release) == 2:
major, minor = version.release
patch = None
else:
major = version.release[0]
minor = None
patch = None
return {
"major": major,
"minor": minor,
"patch": patch,
"is_prerelease": version.is_prerelease,
"is_postrelease": version.is_postrelease,
"is_devrelease": version.is_devrelease,
"is_debug": is_debug,
"version": version,
}
version_dict = parse_python_version(str(version))
if not version_dict:
raise ValueError("Not a valid python version: %r" % version)
return version_dict
def get_architecture(self):
if self.architecture:
+96 -3
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,8 +26,14 @@ 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>[0-9]+)\.(?P<minor>[0-9]+)\.?(?P<patch>(?<=\.)[0-9]+)")
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+)*))?)"
r"?(?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?")
PYTHON_IMPLEMENTATIONS = (
@@ -52,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:
@@ -66,22 +75,79 @@ 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_str.rpartition("-")
m = version_re.match(version_str)
if not m:
raise InvalidPythonVersion("%s is not a python version" % version_str)
return m.groupdict()
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(version_parts))
return {
"major": major,
"minor": minor,
"patch": patch,
"is_postrelease": is_postrelease,
"is_prerelease": is_prerelease,
"is_devrelease": is_devrelease,
"is_debug": is_debug,
"version": version
}
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)
@@ -91,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)
@@ -98,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.
@@ -119,6 +205,7 @@ def ensure_path(path):
def _filter_none(k, v):
# type: (Any, Any) -> bool
if v:
return True
return False
@@ -126,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))))
))
@@ -133,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))
@@ -143,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:
@@ -157,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:
@@ -167,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: