diff --git a/pipenv/vendor/pythonfinder/__init__.py b/pipenv/vendor/pythonfinder/__init__.py index fd9ec1be..8e12a198 100644 --- a/pipenv/vendor/pythonfinder/__init__.py +++ b/pipenv/vendor/pythonfinder/__init__.py @@ -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: diff --git a/pipenv/vendor/pythonfinder/environment.py b/pipenv/vendor/pythonfinder/environment.py index 0ea09109..e8878403 100644 --- a/pipenv/vendor/pythonfinder/environment.py +++ b/pipenv/vendor/pythonfinder/environment.py @@ -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()) diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index d7de3e05..1e5caa16 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -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: diff --git a/pipenv/vendor/pythonfinder/utils.py b/pipenv/vendor/pythonfinder/utils.py index f8ec1972..d8edb239 100644 --- a/pipenv/vendor/pythonfinder/utils.py +++ b/pipenv/vendor/pythonfinder/utils.py @@ -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[0-9]+)\.(?P[0-9]+)\.?(?P(?<=\.)[0-9]+)") + +version_re = re.compile(r"(?P\d+)\.(?P\d+)\.?(?P(?<=\.)[0-9]+)?\.?" + r"(?:(?P[abc]|rc|dev)(?:(?P\d+(?:\.\d+)*))?)" + r"?(?P(\.post(?P\d+))?(\.dev(?P\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: