Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update pythonfinder for resilient parsing #3259

Merged
merged 4 commits into from
Nov 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pipenv/vendor/pythonfinder/__init__.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
10 changes: 10 additions & 0 deletions pipenv/vendor/pythonfinder/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
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")
)
Expand All @@ -24,3 +33,4 @@


IGNORE_UNSUPPORTED = bool(os.environ.get("PYTHONFINDER_IGNORE_UNSUPPORTED", False))
MYPY_RUNNING = os.environ.get("MYPY_RUNNING", is_type_checking())
49 changes: 5 additions & 44 deletions pipenv/vendor/pythonfinder/models/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

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

Expand Down Expand Up @@ -355,49 +355,10 @@ def parse(cls, version):
: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:
Expand Down
99 changes: 96 additions & 3 deletions pipenv/vendor/pythonfinder/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

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"))
Expand All @@ -24,8 +26,14 @@
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 = (
Expand All @@ -52,6 +60,7 @@

@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:
Expand All @@ -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)
Expand All @@ -91,18 +157,38 @@ 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)


@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.

Expand All @@ -119,20 +205,23 @@ def ensure_path(path):


def _filter_none(k, v):
# type: (Any, Any) -> bool
if v:
return True
return False


# 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))))
))


@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))
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down