login page

This commit is contained in:
Alicja Cięciwa
2020-10-27 12:57:58 +01:00
commit cb8886666c
8545 changed files with 1082463 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
Copyright (c) 2018 Dan Ryan
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,19 @@
from __future__ import absolute_import, print_function
# Add NullHandler to "pythonfinder" logger, because Python2's default root
# logger has no handler and warnings like this would be reported:
#
# > No handlers could be found for logger "pythonfinder.models.pyenv"
import logging
from .exceptions import InvalidPythonVersion
from .models import SystemPath, WindowsFinder
from .pythonfinder import Finder
__version__ = "1.2.4"
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
__all__ = ["Finder", "WindowsFinder", "SystemPath", "InvalidPythonVersion"]

View File

@@ -0,0 +1,17 @@
#!env python
# -*- coding=utf-8 -*-
from __future__ import absolute_import
import os
import sys
from pythonfinder.cli import cli
PYTHONFINDER_MAIN = os.path.dirname(os.path.abspath(__file__))
PYTHONFINDER_PACKAGE = os.path.dirname(PYTHONFINDER_MAIN)
if __name__ == "__main__":
sys.exit(cli())

View File

@@ -0,0 +1,14 @@
# Taken from pip: https://github.com/pypa/pip/blob/95bcf8c5f6394298035a7332c441868f3b0169f4/src/pip/_vendor/Makefile
all: clean vendor
clean:
@# Delete vendored items
find . -maxdepth 1 -mindepth 1 -type d -exec rm -rf {} \;
vendor:
@# Install vendored libraries
pip install -t . -r vendor.txt
@# Cleanup .egg-info directories
rm -rf *.egg-info
rm -rf *.dist-info

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Steve Dower
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,11 @@
#-------------------------------------------------------------------------
# Copyright (c) Steve Dower
# All rights reserved.
#
# Distributed under the terms of the MIT License
#-------------------------------------------------------------------------
__author__ = 'Steve Dower <steve.dower@python.org>'
__version__ = '0.1.0'
from pythonfinder._vendor.pep514tools.environment import findall, find, findone

View File

@@ -0,0 +1,7 @@
#-------------------------------------------------------------------------
# Copyright (c) Steve Dower
# All rights reserved.
#
# Distributed under the terms of the MIT License
#-------------------------------------------------------------------------

View File

@@ -0,0 +1,198 @@
#-------------------------------------------------------------------------
# Copyright (c) Steve Dower
# All rights reserved.
#
# Distributed under the terms of the MIT License
#-------------------------------------------------------------------------
__all__ = ['open_source', 'REGISTRY_SOURCE_LM', 'REGISTRY_SOURCE_LM_WOW6432', 'REGISTRY_SOURCE_CU']
from itertools import count
import re
try:
import winreg
except ImportError:
import _winreg as winreg
REGISTRY_SOURCE_LM = 1
REGISTRY_SOURCE_LM_WOW6432 = 2
REGISTRY_SOURCE_CU = 3
_REG_KEY_INFO = {
REGISTRY_SOURCE_LM: (winreg.HKEY_LOCAL_MACHINE, r'Software\Python', winreg.KEY_WOW64_64KEY),
REGISTRY_SOURCE_LM_WOW6432: (winreg.HKEY_LOCAL_MACHINE, r'Software\Python', winreg.KEY_WOW64_32KEY),
REGISTRY_SOURCE_CU: (winreg.HKEY_CURRENT_USER, r'Software\Python', 0),
}
def get_value_from_tuple(value, vtype):
if vtype == winreg.REG_SZ:
if '\0' in value:
return value[:value.index('\0')]
return value
return None
def join(x, y):
return x + '\\' + y
_VALID_ATTR = re.compile('^[a-z_]+$')
_VALID_KEY = re.compile('^[A-Za-z]+$')
_KEY_TO_ATTR = re.compile('([A-Z]+[a-z]+)')
class PythonWrappedDict(object):
@staticmethod
def _attr_to_key(attr):
if not attr:
return ''
if not _VALID_ATTR.match(attr):
return attr
return ''.join(c.capitalize() for c in attr.split('_'))
@staticmethod
def _key_to_attr(key):
if not key:
return ''
if not _VALID_KEY.match(key):
return key
return '_'.join(k for k in _KEY_TO_ATTR.split(key) if k).lower()
def __init__(self, d):
self._d = d
def __getattr__(self, attr):
if attr.startswith('_'):
return object.__getattribute__(self, attr)
if attr == 'value':
attr = ''
key = self._attr_to_key(attr)
try:
return self._d[key]
except KeyError:
pass
except Exception:
raise AttributeError(attr)
raise AttributeError(attr)
def __setattr__(self, attr, value):
if attr.startswith('_'):
return object.__setattr__(self, attr, value)
if attr == 'value':
attr = ''
self._d[self._attr_to_key(attr)] = value
def __dir__(self):
k2a = self._key_to_attr
return list(map(k2a, self._d))
def _setdefault(self, key, value):
self._d.setdefault(key, value)
def _items(self):
return self._d.items()
def __repr__(self):
k2a = self._key_to_attr
return 'info(' + ', '.join('{}={!r}'.format(k2a(k), v) for k, v in self._d.items()) + ')'
class RegistryAccessor(object):
def __init__(self, root, subkey, flags):
self._root = root
self.subkey = subkey
_, _, self.name = subkey.rpartition('\\')
self._flags = flags
def __iter__(self):
subkey_names = []
try:
with winreg.OpenKeyEx(self._root, self.subkey, 0, winreg.KEY_READ | self._flags) as key:
for i in count():
subkey_names.append(winreg.EnumKey(key, i))
except OSError:
pass
return iter(self[k] for k in subkey_names)
def __getitem__(self, key):
return RegistryAccessor(self._root, join(self.subkey, key), self._flags)
def get_value(self, value_name):
try:
with winreg.OpenKeyEx(self._root, self.subkey, 0, winreg.KEY_READ | self._flags) as key:
return get_value_from_tuple(*winreg.QueryValueEx(key, value_name))
except OSError:
return None
def get_all_values(self):
schema = {}
for subkey in self:
schema[subkey.name] = subkey.get_all_values()
key = winreg.OpenKeyEx(self._root, self.subkey, 0, winreg.KEY_READ | self._flags)
try:
with key:
for i in count():
vname, value, vtype = winreg.EnumValue(key, i)
value = get_value_from_tuple(value, vtype)
if value:
schema[vname or ''] = value
except OSError:
pass
return PythonWrappedDict(schema)
def set_value(self, value_name, value):
with winreg.CreateKeyEx(self._root, self.subkey, 0, winreg.KEY_WRITE | self._flags) as key:
if value is None:
winreg.DeleteValue(key, value_name)
elif isinstance(value, str):
winreg.SetValueEx(key, value_name, 0, winreg.REG_SZ, value)
else:
raise TypeError('cannot write {} to registry'.format(type(value)))
def _set_all_values(self, rootkey, name, info, errors):
with winreg.CreateKeyEx(rootkey, name, 0, winreg.KEY_WRITE | self._flags) as key:
for k, v in info:
if isinstance(v, PythonWrappedDict):
self._set_all_values(key, k, v._items(), errors)
elif isinstance(v, dict):
self._set_all_values(key, k, v.items(), errors)
elif v is None:
winreg.DeleteValue(key, k)
elif isinstance(v, str):
winreg.SetValueEx(key, k, 0, winreg.REG_SZ, v)
else:
errors.append('cannot write {} to registry'.format(type(v)))
def set_all_values(self, info):
errors = []
if isinstance(info, PythonWrappedDict):
items = info._items()
elif isinstance(info, dict):
items = info.items()
else:
raise TypeError('info must be a dictionary')
self._set_all_values(self._root, self.subkey, items, errors)
if len(errors) == 1:
raise ValueError(errors[0])
elif errors:
raise ValueError(errors)
def delete(self):
for k in self:
k.delete()
try:
key = winreg.OpenKeyEx(self._root, None, 0, winreg.KEY_READ | self._flags)
except OSError:
return
with key:
winreg.DeleteKeyEx(key, self.subkey)
def open_source(registry_source):
info = _REG_KEY_INFO.get(registry_source)
if not info:
raise ValueError("unsupported registry source")
root, subkey, flags = info
return RegistryAccessor(root, subkey, flags)

View File

@@ -0,0 +1,124 @@
#-------------------------------------------------------------------------
# Copyright (c) Steve Dower
# All rights reserved.
#
# Distributed under the terms of the MIT License
#-------------------------------------------------------------------------
__all__ = ['Environment', 'findall', 'find', 'findone']
from itertools import count
from pythonfinder._vendor.pep514tools._registry import open_source, REGISTRY_SOURCE_LM, REGISTRY_SOURCE_LM_WOW6432, REGISTRY_SOURCE_CU
import re
import sys
# These tags are treated specially when the Company is 'PythonCore'
_PYTHONCORE_COMPATIBILITY_TAGS = {
'2.0', '2.1', '2.2', '2.3', '2.4', '2.5', '2.6', '2.7',
'3.0', '3.1', '3.2', '3.3', '3.4', '3.5', '3.6', '3.7',
'3.8', '3.9'
}
_IS_64BIT_OS = None
def _is_64bit_os():
global _IS_64BIT_OS
if _IS_64BIT_OS is None:
if sys.maxsize > 2**32:
import platform
_IS_64BIT_OS = (platform.machine() == 'AMD64')
else:
_IS_64BIT_OS = False
return _IS_64BIT_OS
class Environment(object):
def __init__(self, source, company, tag, guessed_arch=None):
self._source = source
self.company = company
self.tag = tag
self._guessed_arch = guessed_arch
self._orig_info = company, tag
self.info = {}
def load(self):
if not self._source:
raise ValueError('Environment not initialized with a source')
self.info = info = self._source[self.company][self.tag].get_all_values()
if self.company == 'PythonCore':
info._setdefault('DisplayName', 'Python ' + self.tag)
info._setdefault('SupportUrl', 'http://www.python.org/')
info._setdefault('Version', self.tag[:3])
info._setdefault('SysVersion', self.tag[:3])
if self._guessed_arch:
info._setdefault('SysArchitecture', self._guessed_arch)
def save(self, copy=False):
if not self._source:
raise ValueError('Environment not initialized with a source')
if (self.company, self.tag) != self._orig_info:
if not copy:
self._source[self._orig_info[0]][self._orig_info[1]].delete()
self._orig_info = self.company, self.tag
src = self._source[self.company][self.tag]
src.set_all_values(self.info)
self.info = src.get_all_values()
def delete(self):
if (self.company, self.tag) != self._orig_info:
raise ValueError("cannot delete Environment when company/tag have been modified")
if not self._source:
raise ValueError('Environment not initialized with a source')
self._source.delete()
def __repr__(self):
return '<environment {}\\{}>'.format(self.company, self.tag)
def _get_sources(include_per_machine=True, include_per_user=True):
if _is_64bit_os():
if include_per_user:
yield open_source(REGISTRY_SOURCE_CU), None
if include_per_machine:
yield open_source(REGISTRY_SOURCE_LM), '64bit'
yield open_source(REGISTRY_SOURCE_LM_WOW6432), '32bit'
else:
if include_per_user:
yield open_source(REGISTRY_SOURCE_CU), '32bit'
if include_per_machine:
yield open_source(REGISTRY_SOURCE_LM), '32bit'
def findall(include_per_machine=True, include_per_user=True):
for src, arch in _get_sources(include_per_machine=include_per_machine, include_per_user=include_per_user):
for company in src:
for tag in company:
try:
env = Environment(src, company.name, tag.name, arch)
env.load()
except OSError:
pass
else:
yield env
def find(company_or_tag, tag=None, include_per_machine=True, include_per_user=True, maxcount=None):
if not tag:
env = Environment(None, 'PythonCore', company_or_tag)
else:
env = Environment(None, company_or_tag, tag)
results = []
for src, arch in _get_sources(include_per_machine=include_per_machine, include_per_user=include_per_user):
try:
env._source = src
env._guessed_arch = arch
env.load()
except OSError:
pass
else:
results.append(env)
return results
def findone(company_or_tag, tag=None, include_per_machine=True, include_per_user=True):
found = find(company_or_tag, tag, include_per_machine, include_per_user, maxcount=1)
if found:
return found[0]

View File

@@ -0,0 +1 @@
git+https://github.com/zooba/pep514tools.git@master#egg=pep514tools

View File

@@ -0,0 +1,98 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
import click
from . import __version__
from .pythonfinder import Finder
@click.command()
@click.option("--find", default=False, nargs=1, help="Find a specific python version.")
@click.option("--which", default=False, nargs=1, help="Run the which command.")
@click.option("--findall", is_flag=True, default=False, help="Find all python versions.")
@click.option(
"--version", is_flag=True, default=False, help="Display PythonFinder version."
)
@click.option(
"--ignore-unsupported/--no-unsupported",
is_flag=True,
default=True,
envvar="PYTHONFINDER_IGNORE_UNSUPPORTED",
help="Ignore unsupported python versions.",
)
@click.version_option(prog_name="pyfinder", version=__version__)
@click.pass_context
def cli(
ctx, find=False, which=False, findall=False, version=False, ignore_unsupported=True
):
if version:
click.echo(
"{0} version {1}".format(
click.style("PythonFinder", fg="white", bold=True),
click.style(str(__version__), fg="yellow")
)
)
ctx.exit()
finder = Finder(ignore_unsupported=ignore_unsupported)
if findall:
versions = [v for v in finder.find_all_python_versions()]
if versions:
click.secho("Found python at the following locations:", fg="green")
for v in versions:
py = v.py_version
comes_from = getattr(py, "comes_from", None)
if comes_from is not None:
comes_from_path = getattr(comes_from, "path", v.path)
else:
comes_from_path = v.path
click.secho(
"{py.name!s}: {py.version!s} ({py.architecture!s}) @ {comes_from!s}".format(
py=py, comes_from=comes_from_path
),
fg="yellow",
)
ctx.exit()
else:
click.secho(
"ERROR: No valid python versions found! Check your path and try again.",
fg="red",
)
if find:
click.secho("Searching for python: {0!s}".format(find.strip()), fg="yellow")
found = finder.find_python_version(find.strip())
if found:
py = found.py_version
comes_from = getattr(py, "comes_from", None)
if comes_from is not None:
comes_from_path = getattr(comes_from, "path", found.path)
else:
comes_from_path = found.path
arch = getattr(py, "architecture", None)
click.secho("Found python at the following locations:", fg="green")
click.secho(
"{py.name!s}: {py.version!s} ({py.architecture!s}) @ {comes_from!s}".format(
py=py, comes_from=comes_from_path
),
fg="yellow",
)
ctx.exit()
else:
click.secho("Failed to find matching executable...", fg="yellow")
ctx.exit(1)
elif which:
found = finder.system_path.which(which.strip())
if found:
click.secho("Found Executable: {0}".format(found), fg="white")
ctx.exit()
else:
click.secho("Failed to find matching executable...", fg="yellow")
ctx.exit(1)
else:
click.echo("Please provide a command", color="red")
ctx.exit(1)
ctx.exit()
if __name__ == "__main__":
cli()

View File

@@ -0,0 +1,42 @@
# -*- coding=utf-8 -*-
import sys
import six
if sys.version_info[:2] <= (3, 4):
from pipenv.vendor.pathlib2 import Path # type: ignore # noqa
else:
from pathlib import Path
if six.PY3:
from functools import lru_cache
from builtins import TimeoutError
else:
from backports.functools_lru_cache import lru_cache # type: ignore # noqa
class TimeoutError(OSError):
pass
def getpreferredencoding():
import locale
# Borrowed from Invoke
# (see https://github.com/pyinvoke/invoke/blob/93af29d/invoke/runners.py#L881)
_encoding = locale.getpreferredencoding(False)
if six.PY2 and not sys.platform == "win32":
_default_encoding = locale.getdefaultlocale()[1]
if _default_encoding is not None:
_encoding = _default_encoding
return _encoding
DEFAULT_ENCODING = getpreferredencoding()
def fs_str(string):
"""Encodes a string into the proper filesystem encoding"""
if isinstance(string, str):
return string
assert not isinstance(string, bytes)
return string.encode(DEFAULT_ENCODING)

View File

@@ -0,0 +1,54 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function
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")
)
ASDF_INSTALLED = bool(os.environ.get("ASDF_DIR"))
PYENV_ROOT = os.path.expanduser(
os.path.expandvars(os.environ.get("PYENV_ROOT", "~/.pyenv"))
)
ASDF_DATA_DIR = os.path.expanduser(
os.path.expandvars(os.environ.get("ASDF_DATA_DIR", "~/.asdf"))
)
IS_64BIT_OS = None
SYSTEM_ARCH = platform.architecture()[0]
if sys.maxsize > 2 ** 32:
IS_64BIT_OS = platform.machine() == "AMD64"
else:
IS_64BIT_OS = False
IGNORE_UNSUPPORTED = bool(os.environ.get("PYTHONFINDER_IGNORE_UNSUPPORTED", False))
MYPY_RUNNING = os.environ.get("MYPY_RUNNING", is_type_checking())
SUBPROCESS_TIMEOUT = os.environ.get("PYTHONFINDER_SUBPROCESS_TIMEOUT", 5)
"""The default subprocess timeout for determining python versions
Set to **5** by default.
"""
def get_shim_paths():
shim_paths = []
if ASDF_INSTALLED:
shim_paths.append(os.path.join(ASDF_DATA_DIR, "shims"))
if PYENV_INSTALLED:
shim_paths.append(os.path.join(PYENV_ROOT, "shims"))
return [os.path.normpath(os.path.normcase(p)) for p in shim_paths]
SHIM_PATHS = get_shim_paths()

View File

@@ -0,0 +1,8 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function
class InvalidPythonVersion(Exception):
"""Raised when parsing an invalid python version"""
pass

View File

@@ -0,0 +1,14 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function
import abc
import operator
from itertools import chain
import six
from ..utils import KNOWN_EXTS, unnest
from .path import SystemPath
from .python import PythonVersion
from .windows import WindowsFinder

View File

@@ -0,0 +1,417 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, unicode_literals
import abc
import operator
from collections import defaultdict
from pipenv.vendor import attr
import six
from ..compat import fs_str
from ..environment import MYPY_RUNNING
from ..exceptions import InvalidPythonVersion
from ..utils import (
KNOWN_EXTS,
Sequence,
expand_paths,
looks_like_python,
path_is_known_executable,
)
if MYPY_RUNNING:
from .path import PathEntry
from .python import PythonVersion
from typing import (
Optional,
Union,
Any,
Dict,
Iterator,
List,
DefaultDict,
Generator,
Tuple,
TypeVar,
Type,
)
from ..compat import Path # noqa
BaseFinderType = TypeVar("BaseFinderType")
@attr.s(slots=True)
class BasePath(object):
path = attr.ib(default=None) # type: Path
_children = attr.ib(
default=attr.Factory(dict), cmp=False
) # type: Dict[str, PathEntry]
only_python = attr.ib(default=False) # type: bool
name = attr.ib(type=str)
_py_version = attr.ib(default=None, cmp=False) # type: Optional[PythonVersion]
_pythons = attr.ib(
default=attr.Factory(defaultdict), cmp=False
) # type: DefaultDict[str, PathEntry]
_is_dir = attr.ib(default=None, cmp=False) # type: Optional[bool]
_is_executable = attr.ib(default=None, cmp=False) # type: Optional[bool]
_is_python = attr.ib(default=None, cmp=False) # type: Optional[bool]
def __str__(self):
# type: () -> str
return fs_str("{0}".format(self.path.as_posix()))
def __lt__(self, other):
# type: ("BasePath") -> bool
return self.path.as_posix() < other.path.as_posix()
def __lte__(self, other):
# type: ("BasePath") -> bool
return self.path.as_posix() <= other.path.as_posix()
def __gt__(self, other):
# type: ("BasePath") -> bool
return self.path.as_posix() > other.path.as_posix()
def __gte__(self, other):
# type: ("BasePath") -> bool
return self.path.as_posix() >= other.path.as_posix()
def which(self, name):
# type: (str) -> Optional[PathEntry]
"""Search in this path for an executable.
:param executable: The name of an executable to search for.
:type executable: str
:returns: :class:`~pythonfinder.models.PathEntry` instance.
"""
valid_names = [name] + [
"{0}.{1}".format(name, ext).lower() if ext else "{0}".format(name).lower()
for ext in KNOWN_EXTS
]
children = self.children
found = None
if self.path is not None:
found = next(
(
children[(self.path / child).as_posix()]
for child in valid_names
if (self.path / child).as_posix() in children
),
None,
)
return found
def __del__(self):
for key in ["_is_dir", "_is_python", "_is_executable", "_py_version"]:
if getattr(self, key, None):
try:
delattr(self, key)
except Exception:
print("failed deleting key: {0}".format(key))
self._children = {}
for key in list(self._pythons.keys()):
del self._pythons[key]
self._pythons = None
self._py_version = None
self.path = None
@property
def children(self):
# type: () -> Dict[str, PathEntry]
if not self.is_dir:
return {}
return self._children
@property
def as_python(self):
# type: () -> PythonVersion
py_version = None
if self.py_version:
return self.py_version
if not self.is_dir and self.is_python:
try:
from .python import PythonVersion
py_version = PythonVersion.from_path( # type: ignore
path=self, name=self.name
)
except (ValueError, InvalidPythonVersion):
pass
if py_version is None:
pass
self.py_version = py_version
return py_version # type: ignore
@name.default
def get_name(self):
# type: () -> Optional[str]
if self.path:
return self.path.name
return None
@property
def is_dir(self):
# type: () -> bool
if self._is_dir is None:
if not self.path:
ret_val = False
try:
ret_val = self.path.is_dir()
except OSError:
ret_val = False
self._is_dir = ret_val
return self._is_dir
@is_dir.setter
def is_dir(self, val):
# type: (bool) -> None
self._is_dir = val
@is_dir.deleter
def is_dir(self):
# type: () -> None
self._is_dir = None
@property
def is_executable(self):
# type: () -> bool
if self._is_executable is None:
if not self.path:
self._is_executable = False
else:
self._is_executable = path_is_known_executable(self.path)
return self._is_executable
@is_executable.setter
def is_executable(self, val):
# type: (bool) -> None
self._is_executable = val
@is_executable.deleter
def is_executable(self):
# type: () -> None
self._is_executable = None
@property
def is_python(self):
# type: () -> bool
if self._is_python is None:
if not self.path:
self._is_python = False
else:
self._is_python = self.is_executable and (
looks_like_python(self.path.name)
)
return self._is_python
@is_python.setter
def is_python(self, val):
# type: (bool) -> None
self._is_python = val
@is_python.deleter
def is_python(self):
# type: () -> None
self._is_python = None
def get_py_version(self):
# type: () -> Optional[PythonVersion]
from ..environment import IGNORE_UNSUPPORTED
if self.is_dir:
return None
if self.is_python:
py_version = None
from .python import PythonVersion
try:
py_version = PythonVersion.from_path( # type: ignore
path=self, name=self.name
)
except (InvalidPythonVersion, ValueError):
py_version = None
except Exception:
if not IGNORE_UNSUPPORTED:
raise
return py_version
return None
@property
def py_version(self):
# type: () -> Optional[PythonVersion]
if not self._py_version:
py_version = self.get_py_version()
self._py_version = py_version
else:
py_version = self._py_version
return py_version
@py_version.setter
def py_version(self, val):
# type: (Optional[PythonVersion]) -> None
self._py_version = val
@py_version.deleter
def py_version(self):
# type: () -> None
self._py_version = None
def _iter_pythons(self):
# type: () -> Iterator
if self.is_dir:
for entry in self.children.values():
if entry is None:
continue
elif entry.is_dir:
for python in entry._iter_pythons():
yield python
elif entry.is_python and entry.as_python is not None:
yield entry
elif self.is_python and self.as_python is not None:
yield self # type: ignore
@property
def pythons(self):
# type: () -> DefaultDict[Union[str, Path], PathEntry]
if not self._pythons:
from .path import PathEntry
self._pythons = defaultdict(PathEntry)
for python in self._iter_pythons():
python_path = python.path.as_posix() # type: ignore
self._pythons[python_path] = python
return self._pythons
def __iter__(self):
# type: () -> Iterator
for entry in self.children.values():
yield entry
def __next__(self):
# type: () -> Generator
return next(iter(self))
def next(self):
# type: () -> Generator
return self.__next__()
def find_all_python_versions(
self,
major=None, # type: Optional[Union[str, int]]
minor=None, # type: Optional[int]
patch=None, # type: Optional[int]
pre=None, # type: Optional[bool]
dev=None, # type: Optional[bool]
arch=None, # type: Optional[str]
name=None, # type: Optional[str]
):
# type: (...) -> List[PathEntry]
"""Search for a specific python version on the path. Return all copies
:param major: Major python version to search for.
:type major: int
:param int minor: Minor python version to search for, defaults to None
:param int patch: Patch python version to search for, defaults to None
:param bool pre: Search for prereleases (default None) - prioritize releases if None
:param bool dev: Search for devreleases (default None) - prioritize releases if None
:param str arch: Architecture to include, e.g. '64bit', defaults to None
:param str name: The name of a python version, e.g. ``anaconda3-5.3.0``
:return: A list of :class:`~pythonfinder.models.PathEntry` instances matching the version requested.
:rtype: List[:class:`~pythonfinder.models.PathEntry`]
"""
call_method = "find_all_python_versions" if self.is_dir else "find_python_version"
sub_finder = operator.methodcaller(
call_method, major, minor, patch, pre, dev, arch, name
)
if not self.is_dir:
return sub_finder(self)
unnested = [sub_finder(path) for path in expand_paths(self)]
version_sort = operator.attrgetter("as_python.version_sort")
unnested = [p for p in unnested if p is not None and p.as_python is not None]
paths = sorted(unnested, key=version_sort, reverse=True)
return list(paths)
def find_python_version(
self,
major=None, # type: Optional[Union[str, int]]
minor=None, # type: Optional[int]
patch=None, # type: Optional[int]
pre=None, # type: Optional[bool]
dev=None, # type: Optional[bool]
arch=None, # type: Optional[str]
name=None, # type: Optional[str]
):
# type: (...) -> Optional[PathEntry]
"""Search or self for the specified Python version and return the first match.
:param major: Major version number.
:type major: int
:param int minor: Minor python version to search for, defaults to None
:param int patch: Patch python version to search for, defaults to None
:param bool pre: Search for prereleases (default None) - prioritize releases if None
:param bool dev: Search for devreleases (default None) - prioritize releases if None
:param str arch: Architecture to include, e.g. '64bit', defaults to None
:param str name: The name of a python version, e.g. ``anaconda3-5.3.0``
:returns: A :class:`~pythonfinder.models.PathEntry` instance matching the version requested.
"""
version_matcher = operator.methodcaller(
"matches", major, minor, patch, pre, dev, arch, python_name=name
)
if not self.is_dir:
if self.is_python and self.as_python and version_matcher(self.py_version):
return self # type: ignore
matching_pythons = [
[entry, entry.as_python.version_sort]
for entry in self._iter_pythons()
if (
entry is not None
and entry.as_python is not None
and version_matcher(entry.py_version)
)
]
results = sorted(matching_pythons, key=operator.itemgetter(1, 0), reverse=True)
return next(iter(r[0] for r in results if r is not None), None)
@six.add_metaclass(abc.ABCMeta)
class BaseFinder(object):
def __init__(self):
#: Maps executable paths to PathEntries
from .path import PathEntry
self._pythons = defaultdict(PathEntry) # type: DefaultDict[str, PathEntry]
self._versions = defaultdict(PathEntry) # type: Dict[Tuple, PathEntry]
def get_versions(self):
# type: () -> DefaultDict[Tuple, PathEntry]
"""Return the available versions from the finder"""
raise NotImplementedError
@classmethod
def create(cls, *args, **kwargs):
# type: (Any, Any) -> BaseFinderType
raise NotImplementedError
@property
def version_paths(self):
# type: () -> Any
return self._versions.values()
@property
def expanded_paths(self):
# type: () -> Any
return (p.paths.values() for p in self.version_paths)
@property
def pythons(self):
# type: () -> DefaultDict[str, PathEntry]
return self._pythons
@pythons.setter
def pythons(self, value):
# type: (DefaultDict[str, PathEntry]) -> None
self._pythons = value

View File

@@ -0,0 +1,834 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function
import operator
import os
import sys
from collections import defaultdict
from itertools import chain
from pipenv.vendor import attr
import six
from cached_property import cached_property
from ..compat import Path, fs_str
from ..environment import (
ASDF_DATA_DIR,
ASDF_INSTALLED,
MYPY_RUNNING,
PYENV_INSTALLED,
PYENV_ROOT,
SHIM_PATHS,
get_shim_paths,
)
from ..exceptions import InvalidPythonVersion
from ..utils import (
Iterable,
Sequence,
dedup,
ensure_path,
filter_pythons,
is_in_path,
normalize_path,
optional_instance_of,
parse_asdf_version_order,
parse_pyenv_version_order,
path_is_known_executable,
split_version_and_name,
unnest,
)
from .mixins import BaseFinder, BasePath
if MYPY_RUNNING:
from typing import (
Optional,
Dict,
DefaultDict,
Iterator,
List,
Union,
Tuple,
Generator,
Callable,
Type,
Any,
TypeVar,
)
from .python import PythonFinder, PythonVersion
from .windows import WindowsFinder
FinderType = TypeVar("FinderType", BaseFinder, PythonFinder, WindowsFinder)
ChildType = Union[PythonFinder, "PathEntry"]
PathType = Union[PythonFinder, "PathEntry"]
@attr.s
class SystemPath(object):
global_search = attr.ib(default=True)
paths = attr.ib(
default=attr.Factory(defaultdict)
) # type: DefaultDict[str, Union[PythonFinder, PathEntry]]
_executables = attr.ib(default=attr.Factory(list)) # type: List[PathEntry]
_python_executables = attr.ib(
default=attr.Factory(dict)
) # type: Dict[str, PathEntry]
path_order = attr.ib(default=attr.Factory(list)) # type: List[str]
python_version_dict = attr.ib() # type: DefaultDict[Tuple, List[PythonVersion]]
only_python = attr.ib(default=False, type=bool)
pyenv_finder = attr.ib(default=None) # type: Optional[PythonFinder]
asdf_finder = attr.ib(default=None) # type: Optional[PythonFinder]
windows_finder = attr.ib(default=None) # type: Optional[WindowsFinder]
system = attr.ib(default=False, type=bool)
_version_dict = attr.ib(
default=attr.Factory(defaultdict)
) # type: DefaultDict[Tuple, List[PathEntry]]
ignore_unsupported = attr.ib(default=False, type=bool)
__finders = attr.ib(
default=attr.Factory(dict)
) # type: Dict[str, Union[WindowsFinder, PythonFinder]]
def _register_finder(self, finder_name, finder):
# type: (str, Union[WindowsFinder, PythonFinder]) -> "SystemPath"
if finder_name not in self.__finders:
self.__finders[finder_name] = finder
return self
def clear_caches(self):
for key in ["executables", "python_executables", "version_dict", "path_entries"]:
if key in self.__dict__:
del self.__dict__[key]
for finder in list(self.__finders.keys()):
del self.__finders[finder]
self.__finders = {}
return attr.evolve(
self,
executables=[],
python_executables={},
python_version_dict=defaultdict(list),
version_dict=defaultdict(list),
pyenv_finder=None,
windows_finder=None,
asdf_finder=None,
path_order=[],
paths=defaultdict(PathEntry),
)
def __del__(self):
for key in ["executables", "python_executables", "version_dict", "path_entries"]:
try:
del self.__dict__[key]
except KeyError:
pass
for finder in list(self.__finders.keys()):
del self.__finders[finder]
self.__finders = {}
self._python_executables = {}
self._executables = []
self.python_version_dict = defaultdict(list)
self._version_dict = defaultdict(list)
self.path_order = []
self.pyenv_finder = None
self.asdf_finder = None
self.paths = defaultdict(PathEntry)
self.__finders = {}
@property
def finders(self):
# type: () -> List[str]
return [k for k in self.__finders.keys()]
@staticmethod
def check_for_pyenv():
return PYENV_INSTALLED or os.path.exists(normalize_path(PYENV_ROOT))
@staticmethod
def check_for_asdf():
return ASDF_INSTALLED or os.path.exists(normalize_path(ASDF_DATA_DIR))
@python_version_dict.default
def create_python_version_dict(self):
# type: () -> DefaultDict[Tuple, List[PythonVersion]]
return defaultdict(list)
@cached_property
def executables(self):
# type: () -> List[PathEntry]
self.executables = [
p
for p in chain(*(child.children.values() for child in self.paths.values()))
if p.is_executable
]
return self.executables
@cached_property
def python_executables(self):
# type: () -> Dict[str, PathEntry]
python_executables = {}
for child in self.paths.values():
if child.pythons:
python_executables.update(dict(child.pythons))
for finder_name, finder in self.__finders.items():
if finder.pythons:
python_executables.update(dict(finder.pythons))
self._python_executables = python_executables
return self._python_executables
@cached_property
def version_dict(self):
# type: () -> DefaultDict[Tuple, List[PathEntry]]
self._version_dict = defaultdict(
list
) # type: DefaultDict[Tuple, List[PathEntry]]
for finder_name, finder in self.__finders.items():
for version, entry in finder.versions.items():
if finder_name == "windows":
if entry not in self._version_dict[version]:
self._version_dict[version].append(entry)
continue
if entry not in self._version_dict[version] and entry.is_python:
self._version_dict[version].append(entry)
for p, entry in self.python_executables.items():
version = entry.as_python # type: PythonVersion
if not version:
continue
if not isinstance(version, tuple):
version = version.version_tuple
if version and entry not in self._version_dict[version]:
self._version_dict[version].append(entry)
return self._version_dict
def _run_setup(self):
# type: () -> "SystemPath"
if not self.__class__ == SystemPath:
return self
new_instance = self
path_order = new_instance.path_order[:]
path_entries = self.paths.copy()
if self.global_search and "PATH" in os.environ:
path_order = path_order + os.environ["PATH"].split(os.pathsep)
path_order = list(dedup(path_order))
path_instances = [
ensure_path(p.strip('"'))
for p in path_order
if not any(
is_in_path(normalize_path(str(p)), normalize_path(shim))
for shim in SHIM_PATHS
)
]
path_entries.update(
{
p.as_posix(): PathEntry.create(
path=p.absolute(), is_root=True, only_python=self.only_python
)
for p in path_instances
}
)
new_instance = attr.evolve(
new_instance,
path_order=[p.as_posix() for p in path_instances],
paths=path_entries,
)
if os.name == "nt" and "windows" not in self.finders:
new_instance = new_instance._setup_windows()
#: slice in pyenv
if self.check_for_pyenv() and "pyenv" not in self.finders:
new_instance = new_instance._setup_pyenv()
#: slice in asdf
if self.check_for_asdf() and "asdf" not in self.finders:
new_instance = new_instance._setup_asdf()
venv = os.environ.get("VIRTUAL_ENV")
if os.name == "nt":
bin_dir = "Scripts"
else:
bin_dir = "bin"
if venv and (new_instance.system or new_instance.global_search):
p = ensure_path(venv)
path_order = [(p / bin_dir).as_posix()] + new_instance.path_order
new_instance = attr.evolve(new_instance, path_order=path_order)
paths = new_instance.paths.copy()
paths[p] = new_instance.get_path(p.joinpath(bin_dir))
new_instance = attr.evolve(new_instance, paths=paths)
if new_instance.system:
syspath = Path(sys.executable)
syspath_bin = syspath.parent
if syspath_bin.name != bin_dir and syspath_bin.joinpath(bin_dir).exists():
syspath_bin = syspath_bin / bin_dir
path_order = [syspath_bin.as_posix()] + new_instance.path_order
paths = new_instance.paths.copy()
paths[syspath_bin] = PathEntry.create(
path=syspath_bin, is_root=True, only_python=False
)
new_instance = attr.evolve(new_instance, path_order=path_order, paths=paths)
return new_instance
def _get_last_instance(self, path):
# type: (str) -> int
reversed_paths = reversed(self.path_order)
paths = [normalize_path(p) for p in reversed_paths]
normalized_target = normalize_path(path)
last_instance = next(iter(p for p in paths if normalized_target in p), None)
if last_instance is None:
raise ValueError("No instance found on path for target: {0!s}".format(path))
path_index = self.path_order.index(last_instance)
return path_index
def _slice_in_paths(self, start_idx, paths):
# type: (int, List[Path]) -> "SystemPath"
before_path = [] # type: List[str]
after_path = [] # type: List[str]
if start_idx == 0:
after_path = self.path_order[:]
elif start_idx == -1:
before_path = self.path_order[:]
else:
before_path = self.path_order[: start_idx + 1]
after_path = self.path_order[start_idx + 2 :]
path_order = before_path + [p.as_posix() for p in paths] + after_path
if path_order == self.path_order:
return self
return attr.evolve(self, path_order=path_order)
def _remove_path(self, path):
# type: (str) -> "SystemPath"
path_copy = [p for p in reversed(self.path_order[:])]
new_order = []
target = normalize_path(path)
path_map = {normalize_path(pth): pth for pth in self.paths.keys()}
new_paths = self.paths.copy()
if target in path_map:
del new_paths[path_map[target]]
for current_path in path_copy:
normalized = normalize_path(current_path)
if normalized != target:
new_order.append(normalized)
new_order = [p for p in reversed(new_order)]
return attr.evolve(self, path_order=new_order, paths=new_paths)
def _setup_asdf(self):
# type: () -> "SystemPath"
if "asdf" in self.finders and self.asdf_finder is not None:
return self
from .python import PythonFinder
os_path = os.environ["PATH"].split(os.pathsep)
asdf_finder = PythonFinder.create(
root=ASDF_DATA_DIR,
ignore_unsupported=True,
sort_function=parse_asdf_version_order,
version_glob_path="installs/python/*",
)
asdf_index = None
try:
asdf_index = self._get_last_instance(ASDF_DATA_DIR)
except ValueError:
asdf_index = 0 if is_in_path(next(iter(os_path), ""), ASDF_DATA_DIR) else -1
if asdf_index is None:
# we are in a virtualenv without global pyenv on the path, so we should
# not write pyenv to the path here
return self
# * These are the root paths for the finder
_ = [p for p in asdf_finder.roots]
new_instance = self._slice_in_paths(asdf_index, [asdf_finder.root])
paths = self.paths.copy()
paths[asdf_finder.root] = asdf_finder
paths.update(asdf_finder.roots)
return (
attr.evolve(new_instance, paths=paths, asdf_finder=asdf_finder)
._remove_path(normalize_path(os.path.join(ASDF_DATA_DIR, "shims")))
._register_finder("asdf", asdf_finder)
)
def reload_finder(self, finder_name):
# type: (str) -> "SystemPath"
if finder_name is None:
raise TypeError("Must pass a string as the name of the target finder")
finder_attr = "{0}_finder".format(finder_name)
setup_attr = "_setup_{0}".format(finder_name)
try:
current_finder = getattr(self, finder_attr) # type: Any
except AttributeError:
raise ValueError("Must pass a valid finder to reload.")
try:
setup_fn = getattr(self, setup_attr)
except AttributeError:
raise ValueError("Finder has no valid setup function: %s" % finder_name)
if current_finder is None:
# TODO: This is called 'reload', should we load a new finder for the first
# time here? lets just skip that for now to avoid unallowed finders
pass
if (finder_name == "pyenv" and not PYENV_INSTALLED) or (
finder_name == "asdf" and not ASDF_INSTALLED
):
# Don't allow loading of finders that aren't explicitly 'installed' as it were
return self
setattr(self, finder_attr, None)
if finder_name in self.__finders:
del self.__finders[finder_name]
return setup_fn()
def _setup_pyenv(self):
# type: () -> "SystemPath"
if "pyenv" in self.finders and self.pyenv_finder is not None:
return self
from .python import PythonFinder
os_path = os.environ["PATH"].split(os.pathsep)
pyenv_finder = PythonFinder.create(
root=PYENV_ROOT,
sort_function=parse_pyenv_version_order,
version_glob_path="versions/*",
ignore_unsupported=self.ignore_unsupported,
)
pyenv_index = None
try:
pyenv_index = self._get_last_instance(PYENV_ROOT)
except ValueError:
pyenv_index = 0 if is_in_path(next(iter(os_path), ""), PYENV_ROOT) else -1
if pyenv_index is None:
# we are in a virtualenv without global pyenv on the path, so we should
# not write pyenv to the path here
return self
# * These are the root paths for the finder
_ = [p for p in pyenv_finder.roots]
new_instance = self._slice_in_paths(pyenv_index, [pyenv_finder.root])
paths = new_instance.paths.copy()
paths[pyenv_finder.root] = pyenv_finder
paths.update(pyenv_finder.roots)
return (
attr.evolve(new_instance, paths=paths, pyenv_finder=pyenv_finder)
._remove_path(os.path.join(PYENV_ROOT, "shims"))
._register_finder("pyenv", pyenv_finder)
)
def _setup_windows(self):
# type: () -> "SystemPath"
if "windows" in self.finders and self.windows_finder is not None:
return self
from .windows import WindowsFinder
windows_finder = WindowsFinder.create()
root_paths = (p for p in windows_finder.paths if p.is_root)
path_addition = [p.path.as_posix() for p in root_paths]
new_path_order = self.path_order[:] + path_addition
new_paths = self.paths.copy()
new_paths.update({p.path: p for p in root_paths})
return attr.evolve(
self,
windows_finder=windows_finder,
path_order=new_path_order,
paths=new_paths,
)._register_finder("windows", windows_finder)
def get_path(self, path):
# type: (Union[str, Path]) -> PathType
if path is None:
raise TypeError("A path must be provided in order to generate a path entry.")
path = ensure_path(path)
_path = self.paths.get(path)
if not _path:
_path = self.paths.get(path.as_posix())
if not _path and path.as_posix() in self.path_order:
_path = PathEntry.create(
path=path.absolute(), is_root=True, only_python=self.only_python
)
self.paths[path.as_posix()] = _path
if not _path:
raise ValueError("Path not found or generated: {0!r}".format(path))
return _path
def _get_paths(self):
# type: () -> Generator[Union[PathType, WindowsFinder], None, None]
for path in self.path_order:
try:
entry = self.get_path(path)
except ValueError:
continue
else:
yield entry
@cached_property
def path_entries(self):
# type: () -> List[Union[PathType, WindowsFinder]]
paths = list(self._get_paths())
return paths
def find_all(self, executable):
# type: (str) -> List[Union[PathEntry, FinderType]]
"""
Search the path for an executable. Return all copies.
:param executable: Name of the executable
:type executable: str
:returns: List[PathEntry]
"""
sub_which = operator.methodcaller("which", executable)
filtered = (sub_which(self.get_path(k)) for k in self.path_order)
return list(filtered)
def which(self, executable):
# type: (str) -> Union[PathEntry, None]
"""
Search for an executable on the path.
:param executable: Name of the executable to be located.
:type executable: str
:returns: :class:`~pythonfinder.models.PathEntry` object.
"""
sub_which = operator.methodcaller("which", executable)
filtered = (sub_which(self.get_path(k)) for k in self.path_order)
return next(iter(f for f in filtered if f is not None), None)
def _filter_paths(self, finder):
# type: (Callable) -> Iterator
for path in self._get_paths():
if path is None:
continue
python_versions = finder(path)
if python_versions is not None:
for python in python_versions:
if python is not None:
yield python
def _get_all_pythons(self, finder):
# type: (Callable) -> Iterator
for python in self._filter_paths(finder):
if python is not None and python.is_python:
yield python
def get_pythons(self, finder):
# type: (Callable) -> Iterator
sort_key = operator.attrgetter("as_python.version_sort")
pythons = [entry for entry in self._get_all_pythons(finder)]
for python in sorted(pythons, key=sort_key, reverse=True):
if python is not None:
yield python
def find_all_python_versions(
self,
major=None, # type: Optional[Union[str, int]]
minor=None, # type: Optional[int]
patch=None, # type: Optional[int]
pre=None, # type: Optional[bool]
dev=None, # type: Optional[bool]
arch=None, # type: Optional[str]
name=None, # type: Optional[str]
):
# type (...) -> List[PathEntry]
"""Search for a specific python version on the path. Return all copies
:param major: Major python version to search for.
:type major: int
:param int minor: Minor python version to search for, defaults to None
:param int patch: Patch python version to search for, defaults to None
:param bool pre: Search for prereleases (default None) - prioritize releases if None
:param bool dev: Search for devreleases (default None) - prioritize releases if None
:param str arch: Architecture to include, e.g. '64bit', defaults to None
:param str name: The name of a python version, e.g. ``anaconda3-5.3.0``
:return: A list of :class:`~pythonfinder.models.PathEntry` instances matching the version requested.
:rtype: List[:class:`~pythonfinder.models.PathEntry`]
"""
sub_finder = operator.methodcaller(
"find_all_python_versions", major, minor, patch, pre, dev, arch, name
)
alternate_sub_finder = None
if major and not (minor or patch or pre or dev or arch or name):
alternate_sub_finder = operator.methodcaller(
"find_all_python_versions", None, None, None, None, None, None, major
)
if os.name == "nt" and self.windows_finder:
windows_finder_version = sub_finder(self.windows_finder)
if windows_finder_version:
return windows_finder_version
values = list(self.get_pythons(sub_finder))
if not values and alternate_sub_finder is not None:
values = list(self.get_pythons(alternate_sub_finder))
return values
def find_python_version(
self,
major=None, # type: Optional[Union[str, int]]
minor=None, # type: Optional[Union[str, int]]
patch=None, # type: Optional[Union[str, int]]
pre=None, # type: Optional[bool]
dev=None, # type: Optional[bool]
arch=None, # type: Optional[str]
name=None, # type: Optional[str]
sort_by_path=False, # type: bool
):
# type: (...) -> PathEntry
"""Search for a specific python version on the path.
:param major: Major python version to search for.
:type major: int
:param int minor: Minor python version to search for, defaults to None
:param int patch: Patch python version to search for, defaults to None
:param bool pre: Search for prereleases (default None) - prioritize releases if None
:param bool dev: Search for devreleases (default None) - prioritize releases if None
:param str arch: Architecture to include, e.g. '64bit', defaults to None
:param str name: The name of a python version, e.g. ``anaconda3-5.3.0``
:param bool sort_by_path: Whether to sort by path -- default sort is by version(default: False)
:return: A :class:`~pythonfinder.models.PathEntry` instance matching the version requested.
:rtype: :class:`~pythonfinder.models.PathEntry`
"""
major, minor, patch, name = split_version_and_name(major, minor, patch, name)
sub_finder = operator.methodcaller(
"find_python_version", major, minor, patch, pre, dev, arch, name
)
alternate_sub_finder = None
if name and not (minor or patch or pre or dev or arch or major):
alternate_sub_finder = operator.methodcaller(
"find_all_python_versions", None, None, None, None, None, None, name
)
if major and minor and patch:
_tuple_pre = pre if pre is not None else False
_tuple_dev = dev if dev is not None else False
version_tuple = (major, minor, patch, _tuple_pre, _tuple_dev)
version_tuple_pre = (major, minor, patch, True, False)
if os.name == "nt" and self.windows_finder:
windows_finder_version = sub_finder(self.windows_finder)
if windows_finder_version:
return windows_finder_version
if sort_by_path:
paths = [self.get_path(k) for k in self.path_order]
for path in paths:
found_version = sub_finder(path)
if found_version:
return found_version
if alternate_sub_finder:
for path in paths:
found_version = alternate_sub_finder(path)
if found_version:
return found_version
ver = next(iter(self.get_pythons(sub_finder)), None)
if not ver and alternate_sub_finder is not None:
ver = next(iter(self.get_pythons(alternate_sub_finder)), None)
if ver:
if ver.as_python.version_tuple[:5] in self.python_version_dict:
self.python_version_dict[ver.as_python.version_tuple[:5]].append(ver)
else:
self.python_version_dict[ver.as_python.version_tuple[:5]] = [ver]
return ver
@classmethod
def create(
cls,
path=None, # type: str
system=False, # type: bool
only_python=False, # type: bool
global_search=True, # type: bool
ignore_unsupported=True, # type: bool
):
# type: (...) -> SystemPath
"""Create a new :class:`pythonfinder.models.SystemPath` instance.
:param path: Search path to prepend when searching, defaults to None
:param path: str, optional
:param bool system: Whether to use the running python by default instead of searching, defaults to False
:param bool only_python: Whether to search only for python executables, defaults to False
:param bool ignore_unsupported: Whether to ignore unsupported python versions, if False, an error is raised, defaults to True
:return: A new :class:`pythonfinder.models.SystemPath` instance.
:rtype: :class:`pythonfinder.models.SystemPath`
"""
path_entries = defaultdict(
PathEntry
) # type: DefaultDict[str, Union[PythonFinder, PathEntry]]
paths = [] # type: List[str]
if ignore_unsupported:
os.environ["PYTHONFINDER_IGNORE_UNSUPPORTED"] = fs_str("1")
if global_search:
if "PATH" in os.environ:
paths = os.environ["PATH"].split(os.pathsep)
path_order = [] # type: List[str]
if path:
path_order = [path]
path_instance = ensure_path(path)
path_entries.update(
{
path_instance.as_posix(): PathEntry.create(
path=path_instance.absolute(),
is_root=True,
only_python=only_python,
)
}
)
paths = [path] + paths
paths = [p for p in paths if not any(is_in_path(p, shim) for shim in SHIM_PATHS)]
_path_objects = [ensure_path(p.strip('"')) for p in paths]
paths = [p.as_posix() for p in _path_objects]
path_entries.update(
{
p.as_posix(): PathEntry.create(
path=p.absolute(), is_root=True, only_python=only_python
)
for p in _path_objects
}
)
instance = cls(
paths=path_entries,
path_order=path_order,
only_python=only_python,
system=system,
global_search=global_search,
ignore_unsupported=ignore_unsupported,
)
instance = instance._run_setup()
return instance
@attr.s(slots=True)
class PathEntry(BasePath):
is_root = attr.ib(default=True, type=bool, cmp=False)
def __lt__(self, other):
# type: (BasePath) -> bool
return self.path.as_posix() < other.path.as_posix()
def __lte__(self, other):
# type: (BasePath) -> bool
return self.path.as_posix() <= other.path.as_posix()
def __gt__(self, other):
# type: (BasePath) -> bool
return self.path.as_posix() > other.path.as_posix()
def __gte__(self, other):
# type: (BasePath) -> bool
return self.path.as_posix() >= other.path.as_posix()
def __del__(self):
if getattr(self, "_children"):
del self._children
BasePath.__del__(self)
def _filter_children(self):
# type: () -> Iterator[Path]
if self.only_python:
children = filter_pythons(self.path)
else:
children = self.path.iterdir()
return children
def _gen_children(self):
# type: () -> Iterator
shim_paths = get_shim_paths()
pass_name = self.name != self.path.name
pass_args = {"is_root": False, "only_python": self.only_python}
if pass_name:
if self.name is not None and isinstance(self.name, six.string_types):
pass_args["name"] = self.name # type: ignore
elif self.path is not None and isinstance(self.path.name, six.string_types):
pass_args["name"] = self.path.name # type: ignore
if not self.is_dir:
yield (self.path.as_posix(), self)
elif self.is_root:
for child in self._filter_children():
if any(is_in_path(str(child), shim) for shim in shim_paths):
continue
if self.only_python:
try:
entry = PathEntry.create(path=child, **pass_args) # type: ignore
except (InvalidPythonVersion, ValueError):
continue
else:
entry = PathEntry.create(path=child, **pass_args) # type: ignore
yield (child.as_posix(), entry)
return
@property
def children(self):
# type: () -> Dict[str, PathEntry]
children = getattr(self, "_children", {}) # type: Dict[str, PathEntry]
if not children:
for child_key, child_val in self._gen_children():
children[child_key] = child_val
self.children = children
return self._children
@children.setter
def children(self, val):
# type: (Dict[str, PathEntry]) -> None
self._children = val
@children.deleter
def children(self):
# type: () -> None
del self._children
@classmethod
def create(cls, path, is_root=False, only_python=False, pythons=None, name=None):
# type: (Union[str, Path], bool, bool, Dict[str, PythonVersion], Optional[str]) -> PathEntry
"""Helper method for creating new :class:`pythonfinder.models.PathEntry` instances.
:param str path: Path to the specified location.
:param bool is_root: Whether this is a root from the environment PATH variable, defaults to False
:param bool only_python: Whether to search only for python executables, defaults to False
:param dict pythons: A dictionary of existing python objects (usually from a finder), defaults to None
:param str name: Name of the python version, e.g. ``anaconda3-5.3.0``
:return: A new instance of the class.
:rtype: :class:`pythonfinder.models.PathEntry`
"""
target = ensure_path(path)
guessed_name = False
if not name:
guessed_name = True
name = target.name
creation_args = {
"path": target,
"is_root": is_root,
"only_python": only_python,
"name": name,
}
if pythons:
creation_args["pythons"] = pythons
_new = cls(**creation_args)
if pythons and only_python:
children = {}
child_creation_args = {"is_root": False, "only_python": only_python}
if not guessed_name:
child_creation_args["name"] = _new.name # type: ignore
for pth, python in pythons.items():
if any(shim in normalize_path(str(pth)) for shim in SHIM_PATHS):
continue
pth = ensure_path(pth)
children[pth.as_posix()] = PathEntry( # type: ignore
py_version=python, path=pth, **child_creation_args
)
_new._children = children
return _new
@attr.s
class VersionPath(SystemPath):
base = attr.ib(default=None, validator=optional_instance_of(Path)) # type: Path
name = attr.ib(default=None) # type: str
@classmethod
def create(cls, path, only_python=True, pythons=None, name=None):
"""Accepts a path to a base python version directory.
Generates the version listings for it"""
from .path import PathEntry
path = ensure_path(path)
path_entries = defaultdict(PathEntry)
bin_ = "{base}/bin"
if path.as_posix().endswith(Path(bin_).name):
path = path.parent
bin_dir = ensure_path(bin_.format(base=path.as_posix()))
if not name:
name = path.name
current_entry = PathEntry.create(
bin_dir, is_root=True, only_python=True, pythons=pythons, name=name
)
path_entries[bin_dir.as_posix()] = current_entry
return cls(name=name, base=bin_dir, paths=path_entries)

View File

@@ -0,0 +1,704 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function
import logging
import operator
import platform
import sys
from collections import defaultdict
from pipenv.vendor import attr
import six
from packaging.version import Version
from ..compat import Path, lru_cache
from ..environment import ASDF_DATA_DIR, MYPY_RUNNING, PYENV_ROOT, SYSTEM_ARCH
from ..exceptions import InvalidPythonVersion
from ..utils import (
RE_MATCHER,
_filter_none,
ensure_path,
expand_paths,
get_python_version,
guess_company,
is_in_path,
looks_like_python,
optional_instance_of,
parse_asdf_version_order,
parse_pyenv_version_order,
parse_python_version,
path_is_pythoncore,
unnest,
)
from .mixins import BaseFinder, BasePath
if MYPY_RUNNING:
from typing import (
DefaultDict,
Optional,
Callable,
Generator,
Any,
Union,
Tuple,
List,
Dict,
Type,
TypeVar,
Iterator,
overload,
)
from .path import PathEntry
from .._vendor.pep514tools.environment import Environment
else:
def overload(f):
return f
logger = logging.getLogger(__name__)
@attr.s(slots=True)
class PythonFinder(BaseFinder, BasePath):
root = attr.ib(default=None, validator=optional_instance_of(Path), type=Path)
# should come before versions, because its value is used in versions's default initializer.
#: Whether to ignore any paths which raise exceptions and are not actually python
ignore_unsupported = attr.ib(default=True, type=bool)
#: Glob path for python versions off of the root directory
version_glob_path = attr.ib(default="versions/*", type=str)
#: The function to use to sort version order when returning an ordered verion set
sort_function = attr.ib(default=None) # type: Callable
#: The root locations used for discovery
roots = attr.ib(default=attr.Factory(defaultdict), type=defaultdict)
#: List of paths discovered during search
paths = attr.ib(type=list)
#: shim directory
shim_dir = attr.ib(default="shims", type=str)
#: Versions discovered in the specified paths
_versions = attr.ib(default=attr.Factory(defaultdict), type=defaultdict)
_pythons = attr.ib(default=attr.Factory(defaultdict), type=defaultdict)
def __del__(self):
# type: () -> None
self._versions = defaultdict()
self._pythons = defaultdict()
self.roots = defaultdict()
self.paths = []
@property
def expanded_paths(self):
# type: () -> Generator
return (
path for path in unnest(p for p in self.versions.values()) if path is not None
)
@property
def is_pyenv(self):
# type: () -> bool
return is_in_path(str(self.root), PYENV_ROOT)
@property
def is_asdf(self):
# type: () -> bool
return is_in_path(str(self.root), ASDF_DATA_DIR)
def get_version_order(self):
# type: () -> List[Path]
version_paths = [
p
for p in self.root.glob(self.version_glob_path)
if not (p.parent.name == "envs" or p.name == "envs")
]
versions = {v.name: v for v in version_paths}
version_order = [] # type: List[Path]
if self.is_pyenv:
version_order = [
versions[v] for v in parse_pyenv_version_order() if v in versions
]
elif self.is_asdf:
version_order = [
versions[v] for v in parse_asdf_version_order() if v in versions
]
for version in version_order:
if version in version_paths:
version_paths.remove(version)
if version_order:
version_order += version_paths
else:
version_order = version_paths
return version_order
def get_bin_dir(self, base):
# type: (Union[Path, str]) -> Path
if isinstance(base, six.string_types):
base = Path(base)
return base / "bin"
@classmethod
def version_from_bin_dir(cls, entry):
# type: (PathEntry) -> Optional[PathEntry]
py_version = None
py_version = next(iter(entry.find_all_python_versions()), None)
return py_version
def _iter_version_bases(self):
# type: () -> Iterator[Tuple[Path, PathEntry]]
from .path import PathEntry
for p in self.get_version_order():
bin_dir = self.get_bin_dir(p)
if bin_dir.exists() and bin_dir.is_dir():
entry = PathEntry.create(
path=bin_dir.absolute(), only_python=False, name=p.name, is_root=True
)
self.roots[p] = entry
yield (p, entry)
def _iter_versions(self):
# type: () -> Iterator[Tuple[Path, PathEntry, Tuple]]
for base_path, entry in self._iter_version_bases():
version = None
version_entry = None
try:
version = PythonVersion.parse(entry.name)
except (ValueError, InvalidPythonVersion):
version_entry = next(iter(entry.find_all_python_versions()), None)
if version is None:
if not self.ignore_unsupported:
raise
continue
if version_entry is not None:
version = version_entry.py_version.as_dict()
except Exception:
if not self.ignore_unsupported:
raise
logger.warning(
"Unsupported Python version %r, ignoring...",
base_path.name,
exc_info=True,
)
continue
if version is not None:
version_tuple = (
version.get("major"),
version.get("minor"),
version.get("patch"),
version.get("is_prerelease"),
version.get("is_devrelease"),
version.get("is_debug"),
)
yield (base_path, entry, version_tuple)
@property
def versions(self):
# type: () -> DefaultDict[Tuple, PathEntry]
if not self._versions:
for base_path, entry, version_tuple in self._iter_versions():
self._versions[version_tuple] = entry
return self._versions
def _iter_pythons(self):
# type: () -> Iterator
for path, entry, version_tuple in self._iter_versions():
if path.as_posix() in self._pythons:
yield self._pythons[path.as_posix()]
elif version_tuple not in self.versions:
for python in entry.find_all_python_versions():
yield python
else:
yield self.versions[version_tuple]
@paths.default
def get_paths(self):
# type: () -> List[PathEntry]
_paths = [base for _, base in self._iter_version_bases()]
return _paths
@property
def pythons(self):
# type: () -> DefaultDict[str, PathEntry]
if not self._pythons:
from .path import PathEntry
self._pythons = defaultdict(PathEntry) # type: DefaultDict[str, PathEntry]
for python in self._iter_pythons():
python_path = python.path.as_posix() # type: ignore
self._pythons[python_path] = python
return self._pythons
@pythons.setter
def pythons(self, value):
# type: (DefaultDict[str, PathEntry]) -> None
self._pythons = value
def get_pythons(self):
# type: () -> DefaultDict[str, PathEntry]
return self.pythons
@overload
@classmethod
def create(cls, root, sort_function, version_glob_path=None, ignore_unsupported=True):
# type: (str, Callable, Optional[str], bool) -> PythonFinder
root = ensure_path(root)
if not version_glob_path:
version_glob_path = "versions/*"
return cls(
root=root,
path=root,
ignore_unsupported=ignore_unsupported, # type: ignore
sort_function=sort_function,
version_glob_path=version_glob_path,
)
def find_all_python_versions(
self,
major=None, # type: Optional[Union[str, int]]
minor=None, # type: Optional[int]
patch=None, # type: Optional[int]
pre=None, # type: Optional[bool]
dev=None, # type: Optional[bool]
arch=None, # type: Optional[str]
name=None, # type: Optional[str]
):
# type: (...) -> List[PathEntry]
"""Search for a specific python version on the path. Return all copies
:param major: Major python version to search for.
:type major: int
:param int minor: Minor python version to search for, defaults to None
:param int patch: Patch python version to search for, defaults to None
:param bool pre: Search for prereleases (default None) - prioritize releases if None
:param bool dev: Search for devreleases (default None) - prioritize releases if None
:param str arch: Architecture to include, e.g. '64bit', defaults to None
:param str name: The name of a python version, e.g. ``anaconda3-5.3.0``
:return: A list of :class:`~pythonfinder.models.PathEntry` instances matching the version requested.
:rtype: List[:class:`~pythonfinder.models.PathEntry`]
"""
call_method = "find_all_python_versions" if self.is_dir else "find_python_version"
sub_finder = operator.methodcaller(
call_method, major, minor, patch, pre, dev, arch, name
)
if not any([major, minor, patch, name]):
pythons = [
next(iter(py for py in base.find_all_python_versions()), None)
for _, base in self._iter_version_bases()
]
else:
pythons = [sub_finder(path) for path in self.paths]
pythons = expand_paths(pythons, True)
version_sort = operator.attrgetter("as_python.version_sort")
paths = [
p for p in sorted(pythons, key=version_sort, reverse=True) if p is not None
]
return paths
def find_python_version(
self,
major=None, # type: Optional[Union[str, int]]
minor=None, # type: Optional[int]
patch=None, # type: Optional[int]
pre=None, # type: Optional[bool]
dev=None, # type: Optional[bool]
arch=None, # type: Optional[str]
name=None, # type: Optional[str]
):
# type: (...) -> Optional[PathEntry]
"""Search or self for the specified Python version and return the first match.
:param major: Major version number.
:type major: int
:param int minor: Minor python version to search for, defaults to None
:param int patch: Patch python version to search for, defaults to None
:param bool pre: Search for prereleases (default None) - prioritize releases if None
:param bool dev: Search for devreleases (default None) - prioritize releases if None
:param str arch: Architecture to include, e.g. '64bit', defaults to None
:param str name: The name of a python version, e.g. ``anaconda3-5.3.0``
:returns: A :class:`~pythonfinder.models.PathEntry` instance matching the version requested.
"""
sub_finder = operator.methodcaller(
"find_python_version", major, minor, patch, pre, dev, arch, name
)
version_sort = operator.attrgetter("as_python.version_sort")
unnested = [sub_finder(self.roots[path]) for path in self.roots]
unnested = [
p
for p in unnested
if p is not None and p.is_python and p.as_python is not None
]
paths = sorted(list(unnested), key=version_sort, reverse=True)
return next(iter(p for p in paths if p is not None), None)
def which(self, name):
# type: (str) -> Optional[PathEntry]
"""Search in this path for an executable.
:param executable: The name of an executable to search for.
:type executable: str
:returns: :class:`~pythonfinder.models.PathEntry` instance.
"""
matches = (p.which(name) for p in self.paths)
non_empty_match = next(iter(m for m in matches if m is not None), None)
return non_empty_match
@attr.s(slots=True)
class PythonVersion(object):
major = attr.ib(default=0, type=int)
minor = attr.ib(default=None) # type: Optional[int]
patch = attr.ib(default=None) # type: Optional[int]
is_prerelease = attr.ib(default=False, type=bool)
is_postrelease = attr.ib(default=False, type=bool)
is_devrelease = attr.ib(default=False, type=bool)
is_debug = attr.ib(default=False, type=bool)
version = attr.ib(default=None) # type: Version
architecture = attr.ib(default=None) # type: Optional[str]
comes_from = attr.ib(default=None) # type: Optional[PathEntry]
executable = attr.ib(default=None) # type: Optional[str]
company = attr.ib(default=None) # type: Optional[str]
name = attr.ib(default=None, type=str)
def __getattribute__(self, key):
result = super(PythonVersion, self).__getattribute__(key)
if key in ["minor", "patch"] and result is None:
executable = None # type: Optional[str]
if self.executable:
executable = self.executable
elif self.comes_from:
executable = self.comes_from.path.as_posix()
if executable is not None:
if not isinstance(executable, six.string_types):
executable = executable.as_posix()
instance_dict = self.parse_executable(executable)
for k in instance_dict.keys():
try:
super(PythonVersion, self).__getattribute__(k)
except AttributeError:
continue
else:
setattr(self, k, instance_dict[k])
result = instance_dict.get(key)
return result
@property
def version_sort(self):
# type: () -> Tuple[int, int, Optional[int], int, int]
"""
A tuple for sorting against other instances of the same class.
Returns a tuple of the python version but includes points for core python,
non-dev, and non-prerelease versions. So released versions will have 2 points
for this value. E.g. ``(1, 3, 6, 6, 2)`` is a release, ``(1, 3, 6, 6, 1)`` is a
prerelease, ``(1, 3, 6, 6, 0)`` is a dev release, and ``(1, 3, 6, 6, 3)`` is a
postrelease. ``(0, 3, 7, 3, 2)`` represents a non-core python release, e.g. by
a repackager of python like Continuum.
"""
company_sort = 1 if (self.company and self.company == "PythonCore") else 0
release_sort = 2
if self.is_postrelease:
release_sort = 3
elif self.is_prerelease:
release_sort = 1
elif self.is_devrelease:
release_sort = 0
elif self.is_debug:
release_sort = 1
return (
company_sort,
self.major,
self.minor,
self.patch if self.patch else 0,
release_sort,
)
@property
def version_tuple(self):
# type: () -> Tuple[int, Optional[int], Optional[int], bool, bool, bool]
"""
Provides a version tuple for using as a dictionary key.
:return: A tuple describing the python version meetadata contained.
:rtype: tuple
"""
return (
self.major,
self.minor,
self.patch,
self.is_prerelease,
self.is_devrelease,
self.is_debug,
)
def matches(
self,
major=None, # type: Optional[int]
minor=None, # type: Optional[int]
patch=None, # type: Optional[int]
pre=False, # type: bool
dev=False, # type: bool
arch=None, # type: Optional[str]
debug=False, # type: bool
python_name=None, # type: Optional[str]
):
# type: (...) -> bool
result = False
if arch:
own_arch = self.get_architecture()
if arch.isdigit():
arch = "{0}bit".format(arch)
if (
(major is None or self.major and self.major == major)
and (minor is None or self.minor and self.minor == minor)
and (patch is None or self.patch and self.patch == patch)
and (pre is None or self.is_prerelease == pre)
and (dev is None or self.is_devrelease == dev)
and (arch is None or own_arch == arch)
and (debug is None or self.is_debug == debug)
and (
python_name is None
or (python_name and self.name)
and (self.name == python_name or self.name.startswith(python_name))
)
):
result = True
return result
def as_major(self):
# type: () -> PythonVersion
self_dict = attr.asdict(self, recurse=False, filter=_filter_none).copy()
self_dict.update({"minor": None, "patch": None})
return self.create(**self_dict)
def as_minor(self):
# type: () -> PythonVersion
self_dict = attr.asdict(self, recurse=False, filter=_filter_none).copy()
self_dict.update({"patch": None})
return self.create(**self_dict)
def as_dict(self):
# type: () -> Dict[str, Union[int, bool, Version, None]]
return {
"major": self.major,
"minor": self.minor,
"patch": self.patch,
"is_prerelease": self.is_prerelease,
"is_postrelease": self.is_postrelease,
"is_devrelease": self.is_devrelease,
"is_debug": self.is_debug,
"version": self.version,
"company": self.company,
}
def update_metadata(self, metadata):
# type: (Dict[str, Union[str, int, Version]]) -> None
"""
Update the metadata on the current :class:`pythonfinder.models.python.PythonVersion`
Given a parsed version dictionary from :func:`pythonfinder.utils.parse_python_version`,
update the instance variables of the current version instance to reflect the newly
supplied values.
"""
for key in metadata:
try:
_ = getattr(self, key)
except AttributeError:
continue
else:
setattr(self, key, metadata[key])
@classmethod
@lru_cache(maxsize=1024)
def parse(cls, version):
# type: (str) -> Dict[str, Union[str, int, Version]]
"""
Parse a valid version string into a dictionary
Raises:
ValueError -- Unable to parse version string
ValueError -- Not a valid python version
TypeError -- NoneType or unparseable type passed in
:param str version: A valid version string
:return: A dictionary with metadata about the specified python version.
:rtype: dict
"""
if version is None:
raise TypeError("Must pass a value to parse!")
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):
# type: () -> str
if self.architecture:
return self.architecture
arch = None
if self.comes_from is not None:
arch, _ = platform.architecture(self.comes_from.path.as_posix())
elif self.executable is not None:
arch, _ = platform.architecture(self.executable)
if arch is None:
arch, _ = platform.architecture(sys.executable)
self.architecture = arch
return self.architecture
@classmethod
def from_path(cls, path, name=None, ignore_unsupported=True, company=None):
# type: (Union[str, PathEntry], Optional[str], bool, Optional[str]) -> PythonVersion
"""
Parses a python version from a system path.
Raises:
ValueError -- Not a valid python path
:param path: A string or :class:`~pythonfinder.models.path.PathEntry`
:type path: str or :class:`~pythonfinder.models.path.PathEntry` instance
:param str name: Name of the python distribution in question
:param bool ignore_unsupported: Whether to ignore or error on unsupported paths.
:param Optional[str] company: The company or vendor packaging the distribution.
:return: An instance of a PythonVersion.
:rtype: :class:`~pythonfinder.models.python.PythonVersion`
"""
from .path import PathEntry
if not isinstance(path, PathEntry):
path = PathEntry.create(path, is_root=False, only_python=True, name=name)
from ..environment import IGNORE_UNSUPPORTED
ignore_unsupported = ignore_unsupported or IGNORE_UNSUPPORTED
path_name = getattr(path, "name", path.path.name) # str
if not path.is_python:
if not (ignore_unsupported or IGNORE_UNSUPPORTED):
raise ValueError("Not a valid python path: %s" % path.path)
try:
instance_dict = cls.parse(path_name)
except Exception:
instance_dict = cls.parse_executable(path.path.absolute().as_posix())
else:
if instance_dict.get("minor") is None and looks_like_python(path.path.name):
instance_dict = cls.parse_executable(path.path.absolute().as_posix())
if (
not isinstance(instance_dict.get("version"), Version)
and not ignore_unsupported
):
raise ValueError("Not a valid python path: %s" % path)
if instance_dict.get("patch") is None:
instance_dict = cls.parse_executable(path.path.absolute().as_posix())
if name is None:
name = path_name
if company is None:
company = guess_company(path.path.as_posix())
instance_dict.update(
{"comes_from": path, "name": name, "executable": path.path.as_posix()}
)
return cls(**instance_dict) # type: ignore
@classmethod
@lru_cache(maxsize=1024)
def parse_executable(cls, path):
# type: (str) -> Dict[str, Optional[Union[str, int, Version]]]
result_dict = {} # type: Dict[str, Optional[Union[str, int, Version]]]
result_version = None # type: Optional[str]
if path is None:
raise TypeError("Must pass a valid path to parse.")
if not isinstance(path, six.string_types):
path = path.as_posix()
# if not looks_like_python(path):
# raise ValueError("Path %r does not look like a valid python path" % path)
try:
result_version = get_python_version(path)
except Exception:
raise ValueError("Not a valid python path: %r" % path)
if result_version is None:
raise ValueError("Not a valid python path: %s" % path)
result_dict = cls.parse(result_version.strip())
return result_dict
@classmethod
def from_windows_launcher(cls, launcher_entry, name=None, company=None):
# type: (Environment, Optional[str], Optional[str]) -> PythonVersion
"""Create a new PythonVersion instance from a Windows Launcher Entry
:param launcher_entry: A python launcher environment object.
:param Optional[str] name: The name of the distribution.
:param Optional[str] company: The name of the distributing company.
:return: An instance of a PythonVersion.
:rtype: :class:`~pythonfinder.models.python.PythonVersion`
"""
from .path import PathEntry
creation_dict = cls.parse(launcher_entry.info.version)
base_path = ensure_path(launcher_entry.info.install_path.__getattr__(""))
default_path = base_path / "python.exe"
if not default_path.exists():
default_path = base_path / "Scripts" / "python.exe"
exe_path = ensure_path(
getattr(launcher_entry.info.install_path, "executable_path", default_path)
)
company = getattr(launcher_entry, "company", guess_company(exe_path.as_posix()))
creation_dict.update(
{
"architecture": getattr(
launcher_entry.info, "sys_architecture", SYSTEM_ARCH
),
"executable": exe_path,
"name": name,
"company": company,
}
)
py_version = cls.create(**creation_dict)
comes_from = PathEntry.create(exe_path, only_python=True, name=name)
py_version.comes_from = comes_from
py_version.name = comes_from.name
return py_version
@classmethod
def create(cls, **kwargs):
# type: (...) -> PythonVersion
if "architecture" in kwargs:
if kwargs["architecture"].isdigit():
kwargs["architecture"] = "{0}bit".format(kwargs["architecture"])
return cls(**kwargs)
@attr.s
class VersionMap(object):
versions = attr.ib(
factory=defaultdict
) # type: DefaultDict[Tuple[int, Optional[int], Optional[int], bool, bool, bool], List[PathEntry]]
def add_entry(self, entry):
# type: (...) -> None
version = entry.as_python # type: PythonVersion
if version:
_ = self.versions[version.version_tuple]
paths = {p.path for p in self.versions.get(version.version_tuple, [])}
if entry.path not in paths:
self.versions[version.version_tuple].append(entry)
def merge(self, target):
# type: (VersionMap) -> None
for version, entries in target.versions.items():
if version not in self.versions:
self.versions[version] = entries
else:
current_entries = {
p.path
for p in self.versions[version] # type: ignore
if version in self.versions
}
new_entries = {p.path for p in entries}
new_entries -= current_entries
self.versions[version].extend(
[e for e in entries if e.path in new_entries]
)

View File

@@ -0,0 +1,146 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function
import operator
from collections import defaultdict
from pipenv.vendor import attr
from ..environment import MYPY_RUNNING
from ..exceptions import InvalidPythonVersion
from ..utils import ensure_path
from .mixins import BaseFinder
from .path import PathEntry
from .python import PythonVersion, VersionMap
if MYPY_RUNNING:
from typing import DefaultDict, Tuple, List, Optional, Union, TypeVar, Type, Any
FinderType = TypeVar("FinderType")
@attr.s
class WindowsFinder(BaseFinder):
paths = attr.ib(default=attr.Factory(list), type=list)
version_list = attr.ib(default=attr.Factory(list), type=list)
_versions = attr.ib() # type: DefaultDict[Tuple, PathEntry]
_pythons = attr.ib() # type: DefaultDict[str, PathEntry]
def find_all_python_versions(
self,
major=None, # type: Optional[Union[str, int]]
minor=None, # type: Optional[int]
patch=None, # type: Optional[int]
pre=None, # type: Optional[bool]
dev=None, # type: Optional[bool]
arch=None, # type: Optional[str]
name=None, # type: Optional[str]
):
# type (...) -> List[PathEntry]
version_matcher = operator.methodcaller(
"matches", major, minor, patch, pre, dev, arch, python_name=name
)
pythons = [py for py in self.version_list if version_matcher(py)]
version_sort = operator.attrgetter("version_sort")
return [
c.comes_from for c in sorted(pythons, key=version_sort, reverse=True)
if c.comes_from
]
def find_python_version(
self,
major=None, # type: Optional[Union[str, int]]
minor=None, # type: Optional[int]
patch=None, # type: Optional[int]
pre=None, # type: Optional[bool]
dev=None, # type: Optional[bool]
arch=None, # type: Optional[str]
name=None, # type: Optional[str]
):
# type: (...) -> Optional[PathEntry]
return next(
iter(
v
for v in self.find_all_python_versions(
major=major,
minor=minor,
patch=patch,
pre=pre,
dev=dev,
arch=arch,
name=name,
)
),
None,
)
@_versions.default
def get_versions(self):
# type: () -> DefaultDict[Tuple, PathEntry]
versions = defaultdict(PathEntry) # type: DefaultDict[Tuple, PathEntry]
from pythonfinder._vendor.pep514tools import environment as pep514env
env_versions = pep514env.findall()
path = None
for version_object in env_versions:
install_path = getattr(version_object.info, "install_path", None)
name = getattr(version_object, "tag", None)
company = getattr(version_object, "company", None)
if install_path is None:
continue
try:
path = ensure_path(install_path.__getattr__(""))
except AttributeError:
continue
try:
py_version = PythonVersion.from_windows_launcher(
version_object, name=name, company=company
)
except InvalidPythonVersion:
continue
if py_version is None:
continue
self.version_list.append(py_version)
python_path = (
py_version.comes_from.path
if py_version.comes_from
else py_version.executable
)
python_kwargs = {python_path: py_version} if python_path is not None else {}
base_dir = PathEntry.create(
path, is_root=True, only_python=True, pythons=python_kwargs
)
versions[py_version.version_tuple[:5]] = base_dir
self.paths.append(base_dir)
return versions
@property
def versions(self):
# type: () -> DefaultDict[Tuple, PathEntry]
if not self._versions:
self._versions = self.get_versions()
return self._versions
@_pythons.default
def get_pythons(self):
# type: () -> DefaultDict[str, PathEntry]
pythons = defaultdict() # type: DefaultDict[str, PathEntry]
for version in self.version_list:
_path = ensure_path(version.comes_from.path)
pythons[_path.as_posix()] = version.comes_from
return pythons
@property
def pythons(self):
# type: () -> DefaultDict[str, PathEntry]
return self._pythons
@pythons.setter
def pythons(self, value):
# type: (DefaultDict[str, PathEntry]) -> None
self._pythons = value
@classmethod
def create(cls, *args, **kwargs):
# type: (Type[FinderType], Any, Any) -> FinderType
return cls()

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Steve Dower
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,337 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function
import importlib
import operator
import os
import six
from click import secho
from . import environment
from .compat import lru_cache
from .exceptions import InvalidPythonVersion
from .utils import Iterable, filter_pythons, version_re
if environment.MYPY_RUNNING:
from typing import Optional, Dict, Any, Union, List, Iterator, Text
from .models.path import Path, PathEntry
from .models.windows import WindowsFinder
from .models.path import SystemPath
STRING_TYPE = Union[str, Text, bytes]
class Finder(object):
"""
A cross-platform Finder for locating python and other executables.
Searches for python and other specified binaries starting in *path*, if supplied,
but searching the bin path of ``sys.executable`` if *system* is ``True``, and then
searching in the ``os.environ['PATH']`` if *global_search* is ``True``. When *global_search*
is ``False``, this search operation is restricted to the allowed locations of
*path* and *system*.
"""
def __init__(
self,
path=None,
system=False,
global_search=True,
ignore_unsupported=True,
sort_by_path=False,
):
# type: (Optional[str], bool, bool, bool, bool) -> None
"""Create a new :class:`~pythonfinder.pythonfinder.Finder` instance.
:param path: A bin-directory search location, defaults to None
:param path: str, optional
:param system: Whether to include the bin-dir of ``sys.executable``, defaults to False
:param system: bool, optional
:param global_search: Whether to search the global path from os.environ, defaults to True
:param global_search: bool, optional
:param ignore_unsupported: Whether to ignore unsupported python versions, if False, an
error is raised, defaults to True
:param ignore_unsupported: bool, optional
:param bool sort_by_path: Whether to always sort by path
:returns: a :class:`~pythonfinder.pythonfinder.Finder` object.
"""
self.path_prepend = path # type: Optional[str]
self.global_search = global_search # type: bool
self.system = system # type: bool
self.sort_by_path = sort_by_path # type: bool
self.ignore_unsupported = ignore_unsupported # type: bool
self._system_path = None # type: Optional[SystemPath]
self._windows_finder = None # type: Optional[WindowsFinder]
def __hash__(self):
# type: () -> int
return hash(
(self.path_prepend, self.system, self.global_search, self.ignore_unsupported)
)
def __eq__(self, other):
# type: (Any) -> bool
return self.__hash__() == other.__hash__()
def create_system_path(self):
# type: () -> SystemPath
pyfinder_path = importlib.import_module("pythonfinder.models.path")
return pyfinder_path.SystemPath.create(
path=self.path_prepend,
system=self.system,
global_search=self.global_search,
ignore_unsupported=self.ignore_unsupported,
)
def reload_system_path(self):
# type: () -> None
"""
Rebuilds the base system path and all of the contained finders within it.
This will re-apply any changes to the environment or any version changes on the system.
"""
if self._system_path is not None:
self._system_path = self._system_path.clear_caches()
self._system_path = None
pyfinder_path = importlib.import_module("pythonfinder.models.path")
six.moves.reload_module(pyfinder_path)
self._system_path = self.create_system_path()
def rehash(self):
# type: () -> "Finder"
if not self._system_path:
self._system_path = self.create_system_path()
self.find_all_python_versions.cache_clear()
self.find_python_version.cache_clear()
if self._windows_finder is not None:
self._windows_finder = None
filter_pythons.cache_clear()
self.reload_system_path()
return self
@property
def system_path(self):
# type: () -> SystemPath
if self._system_path is None:
self._system_path = self.create_system_path()
return self._system_path
@property
def windows_finder(self):
# type: () -> Optional[WindowsFinder]
if os.name == "nt" and not self._windows_finder:
from .models import WindowsFinder
self._windows_finder = WindowsFinder()
return self._windows_finder
def which(self, exe):
# type: (str) -> Optional[PathEntry]
return self.system_path.which(exe)
@classmethod
def parse_major(
cls,
major, # type: Optional[str]
minor=None, # type: Optional[int]
patch=None, # type: Optional[int]
pre=None, # type: Optional[bool]
dev=None, # type: Optional[bool]
arch=None, # type: Optional[str]
):
# type: (...) -> Dict[str, Union[int, str, bool, None]]
from .models import PythonVersion
major_is_str = major and isinstance(major, six.string_types)
is_num = (
major
and major_is_str
and all(part.isdigit() for part in major.split(".")[:2])
)
major_has_arch = (
arch is None
and major
and major_is_str
and "-" in major
and major[0].isdigit()
)
name = None
if major and major_has_arch:
orig_string = "{0!s}".format(major)
major, _, arch = major.rpartition("-")
if arch:
arch = arch.lower().lstrip("x").replace("bit", "")
if not (arch.isdigit() and (int(arch) & int(arch) - 1) == 0):
major = orig_string
arch = None
else:
arch = "{0}bit".format(arch)
try:
version_dict = PythonVersion.parse(major)
except (ValueError, InvalidPythonVersion):
if name is None:
name = "{0!s}".format(major)
major = None
version_dict = {}
elif major and major[0].isalpha():
return {"major": None, "name": major, "arch": arch}
elif major and is_num:
match = version_re.match(major)
version_dict = match.groupdict() if match else {} # type: ignore
version_dict.update(
{
"is_prerelease": bool(version_dict.get("prerel", False)),
"is_devrelease": bool(version_dict.get("dev", False)),
}
)
else:
version_dict = {
"major": major,
"minor": minor,
"patch": patch,
"pre": pre,
"dev": dev,
"arch": arch,
}
if not version_dict.get("arch") and arch:
version_dict["arch"] = arch
version_dict["minor"] = (
int(version_dict["minor"]) if version_dict.get("minor") is not None else minor
)
version_dict["patch"] = (
int(version_dict["patch"]) if version_dict.get("patch") is not None else patch
)
version_dict["major"] = (
int(version_dict["major"]) if version_dict.get("major") is not None else major
)
if not (version_dict["major"] or version_dict.get("name")):
version_dict["major"] = major
if name:
version_dict["name"] = name
return version_dict
@lru_cache(maxsize=1024)
def find_python_version(
self,
major=None, # type: Optional[Union[str, int]]
minor=None, # type: Optional[int]
patch=None, # type: Optional[int]
pre=None, # type: Optional[bool]
dev=None, # type: Optional[bool]
arch=None, # type: Optional[str]
name=None, # type: Optional[str]
sort_by_path=False, # type: bool
):
# type: (...) -> Optional[PathEntry]
"""
Find the python version which corresponds most closely to the version requested.
:param Union[str, int] major: The major version to look for, or the full version, or the name of the target version.
:param Optional[int] minor: The minor version. If provided, disables string-based lookups from the major version field.
:param Optional[int] patch: The patch version.
:param Optional[bool] pre: If provided, specifies whether to search pre-releases.
:param Optional[bool] dev: If provided, whether to search dev-releases.
:param Optional[str] arch: If provided, which architecture to search.
:param Optional[str] name: *Name* of the target python, e.g. ``anaconda3-5.3.0``
:param bool sort_by_path: Whether to sort by path -- default sort is by version(default: False)
:return: A new *PathEntry* pointer at a matching python version, if one can be located.
:rtype: :class:`pythonfinder.models.path.PathEntry`
"""
minor = int(minor) if minor is not None else minor
patch = int(patch) if patch is not None else patch
version_dict = {
"minor": minor,
"patch": patch,
"name": name,
"arch": arch,
} # type: Dict[str, Union[str, int, Any]]
if (
isinstance(major, six.string_types)
and pre is None
and minor is None
and dev is None
and patch is None
):
version_dict = self.parse_major(major, minor=minor, patch=patch, arch=arch)
major = version_dict["major"]
minor = version_dict.get("minor", minor) # type: ignore
patch = version_dict.get("patch", patch) # type: ignore
arch = version_dict.get("arch", arch) # type: ignore
name = version_dict.get("name", name) # type: ignore
_pre = version_dict.get("is_prerelease", pre)
pre = bool(_pre) if _pre is not None else pre
_dev = version_dict.get("is_devrelease", dev)
dev = bool(_dev) if _dev is not None else dev
if "architecture" in version_dict and isinstance(
version_dict["architecture"], six.string_types
):
arch = version_dict["architecture"] # type: ignore
if os.name == "nt" and self.windows_finder is not None:
found = self.windows_finder.find_python_version(
major=major,
minor=minor,
patch=patch,
pre=pre,
dev=dev,
arch=arch,
name=name,
)
if found:
return found
return self.system_path.find_python_version(
major=major,
minor=minor,
patch=patch,
pre=pre,
dev=dev,
arch=arch,
name=name,
sort_by_path=self.sort_by_path,
)
@lru_cache(maxsize=1024)
def find_all_python_versions(
self,
major=None, # type: Optional[Union[str, int]]
minor=None, # type: Optional[int]
patch=None, # type: Optional[int]
pre=None, # type: Optional[bool]
dev=None, # type: Optional[bool]
arch=None, # type: Optional[str]
name=None, # type: Optional[str]
):
# type: (...) -> List[PathEntry]
version_sort = operator.attrgetter("as_python.version_sort")
python_version_dict = getattr(self.system_path, "python_version_dict", {})
if python_version_dict:
paths = (
path
for version in python_version_dict.values()
for path in version
if path is not None and path.as_python
)
path_list = sorted(paths, key=version_sort, reverse=True)
return path_list
versions = self.system_path.find_all_python_versions(
major=major, minor=minor, patch=patch, pre=pre, dev=dev, arch=arch, name=name
)
if not isinstance(versions, Iterable):
versions = [versions]
# This list has already been mostly sorted on windows, we don't need to reverse it again
path_list = sorted(versions, key=version_sort, reverse=True)
path_map = {} # type: Dict[str, PathEntry]
for path in path_list:
try:
resolved_path = path.path.resolve()
except OSError:
resolved_path = path.path.absolute()
if not path_map.get(resolved_path.as_posix()):
path_map[resolved_path.as_posix()] = path
return path_list

View File

@@ -0,0 +1,451 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function
import io
import itertools
import os
import re
import subprocess
from collections import OrderedDict
from fnmatch import fnmatch
from threading import Timer
from pipenv.vendor import attr
import six
from packaging.version import LegacyVersion, Version
from .compat import Path, lru_cache, TimeoutError # noqa
from .environment import MYPY_RUNNING, PYENV_ROOT, SUBPROCESS_TIMEOUT
from .exceptions import InvalidPythonVersion
six.add_move(
six.MovedAttribute("Iterable", "collections", "collections.abc")
) # type: ignore # noqa
six.add_move(
six.MovedAttribute("Sequence", "collections", "collections.abc")
) # type: ignore # noqa
# fmt: off
from six.moves import Iterable # type: ignore # noqa # isort:skip
from six.moves import Sequence # type: ignore # noqa # isort:skip
# fmt: on
if MYPY_RUNNING:
from typing import Any, Union, List, Callable, Set, Tuple, Dict, Optional, Iterator
from attr.validators import _OptionalValidator # type: ignore
from .models.path import PathEntry
version_re_str = (
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+))?)?"
)
version_re = re.compile(version_re_str)
PYTHON_IMPLEMENTATIONS = (
"python",
"ironpython",
"jython",
"pypy",
"anaconda",
"miniconda",
"stackless",
"activepython",
"pyston",
"micropython",
)
if os.name == "nt":
KNOWN_EXTS = {"exe", "py", "bat", ""}
else:
KNOWN_EXTS = {"sh", "bash", "csh", "zsh", "fish", "py", ""}
KNOWN_EXTS = KNOWN_EXTS | set(
filter(None, os.environ.get("PATHEXT", "").split(os.pathsep))
)
PY_MATCH_STR = r"((?P<implementation>{0})(?:\d?(?:\.\d[cpm]{{0,3}}))?(?:-?[\d\.]+)*[^zw])".format(
"|".join(PYTHON_IMPLEMENTATIONS)
)
EXE_MATCH_STR = r"{0}(?:\.(?P<ext>{1}))?".format(PY_MATCH_STR, "|".join(KNOWN_EXTS))
RE_MATCHER = re.compile(r"({0}|{1})".format(version_re_str, PY_MATCH_STR))
EXE_MATCHER = re.compile(EXE_MATCH_STR)
RULES_BASE = [
"*{0}",
"*{0}?",
"*{0}?.?",
"*{0}?.?m",
"{0}?-?.?",
"{0}?-?.?.?",
"{0}?.?-?.?.?",
]
RULES = [rule.format(impl) for impl in PYTHON_IMPLEMENTATIONS for rule in RULES_BASE]
MATCH_RULES = []
for rule in RULES:
MATCH_RULES.extend(
["{0}.{1}".format(rule, ext) if ext else "{0}".format(rule) for ext in KNOWN_EXTS]
)
@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('.'.join([str(i) for i in sys.version_info[:3]]))",
]
subprocess_kwargs = {
"env": os.environ.copy(),
"universal_newlines": True,
"stdout": subprocess.PIPE,
"stderr": subprocess.PIPE,
"shell": False,
}
c = subprocess.Popen(version_cmd, **subprocess_kwargs)
timer = Timer(SUBPROCESS_TIMEOUT, c.kill)
try:
out, _ = c.communicate()
except (SystemExit, KeyboardInterrupt, TimeoutError):
c.terminate()
out, _ = c.communicate()
raise
except OSError:
raise InvalidPythonVersion("%s is not a valid python path" % path)
if not out:
raise InvalidPythonVersion("%s is not a valid python path" % path)
return out.strip()
@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("-")
match = version_re.match(version_str)
if not match:
raise InvalidPythonVersion("%s is not a python version" % version_str)
version_dict = match.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 = None
if isinstance(version, LegacyVersion) or version is None:
v_dict = version_dict.copy()
pre = ""
if v_dict.get("prerel") and v_dict.get("prerelversion"):
pre = v_dict.pop("prerel")
pre = "{0}{1}".format(pre, v_dict.pop("prerelversion"))
v_dict["pre"] = pre
keys = ["major", "minor", "patch", "pre", "postdev", "post", "dev"]
values = [v_dict.get(val) for val in keys]
version_str = ".".join([str(v) for v in values if v])
version = parse_version(version_str)
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: (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:`~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)
and path.suffix in KNOWN_EXTS
)
@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
match = RE_MATCHER.match(name)
if match:
return any(fnmatch(name, rule) for rule in MATCH_RULES)
return False
@lru_cache(maxsize=1024)
def path_is_python(path):
# type: (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:`~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 guess_company(path):
# type: (str) -> Optional[str]
"""Given a path to python, guess the company who created it
:param str path: The path to guess about
:return: The guessed company
:rtype: Optional[str]
"""
non_core_pythons = [impl for impl in PYTHON_IMPLEMENTATIONS if impl != "python"]
return next(
iter(impl for impl in non_core_pythons if impl in path.lower()), "PythonCore"
)
@lru_cache(maxsize=1024)
def path_is_pythoncore(path):
# type: (str) -> bool
"""Given a path, determine whether it appears to be pythoncore.
Does not verify whether the path is in fact a path to python, but simply
does an exclusionary check on the possible known python implementations
to see if their names are present in the path (fairly dumb check).
:param str path: The path to check
:return: Whether that path is a PythonCore path or not
:rtype: bool
"""
company = guess_company(path)
if company:
return company == "PythonCore"
return False
@lru_cache(maxsize=1024)
def ensure_path(path):
# type: (Union[Path, str]) -> Path
"""
Given a path (either a string or a Path object), expand variables and return a Path object.
:param path: A string or a :class:`~pathlib.Path` object.
:type path: str or :class:`~pathlib.Path`
:return: A fully expanded Path object.
:rtype: :class:`~pathlib.Path`
"""
if isinstance(path, Path):
return path
path = Path(os.path.expandvars(path))
return path.absolute()
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, Path]) -> Iterable
"""Return all valid pythons in a given path"""
if not isinstance(path, Path):
path = Path(str(path))
if not path.is_dir():
return path if path_is_python(path) else None
return filter(path_is_python, path.iterdir())
# 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:
target = item
if getattr(target, "__iter__", None):
for el in target:
if isinstance(el, Iterable) and not isinstance(el, six.string_types):
el, el_copy = itertools.tee(el, 2)
for sub in unnest(el_copy):
yield sub
else:
yield el
else:
yield target
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:
contents = fh.read()
version_order = [v for v in contents.splitlines()]
return version_order
return []
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:
contents = fh.read()
python_section = next(
iter(line for line in contents.splitlines() if line.startswith("python")),
None,
)
if python_section:
# python_key, _, versions
_, _, versions = python_section.partition(" ")
if versions:
return versions.split()
return []
def split_version_and_name(
major=None, # type: Optional[Union[str, int]]
minor=None, # type: Optional[Union[str, int]]
patch=None, # type: Optional[Union[str, int]]
name=None, # type: Optional[str]
):
# type: (...) -> Tuple[Optional[Union[str, int]], Optional[Union[str, int]], Optional[Union[str, int]], Optional[str]] # noqa
if isinstance(major, six.string_types) and not minor and not patch:
# Only proceed if this is in the format "x.y.z" or similar
if major.isdigit() or (major.count(".") > 0 and major[0].isdigit()):
version = major.split(".", 2)
if isinstance(version, (tuple, list)):
if len(version) > 3:
major, minor, patch, _ = version
elif len(version) == 3:
major, minor, patch = version
elif len(version) == 2:
major, minor = version
else:
major = major[0]
else:
major = major
name = None
else:
name = "{0!s}".format(major)
major = None
return (major, minor, patch, name)
# TODO: Reimplement in vistir
def is_in_path(path, parent):
return normalize_path(str(path)).startswith(normalize_path(str(parent)))
def expand_paths(path, only_python=True):
# type: (Union[Sequence, PathEntry], bool) -> Iterator
"""
Recursively expand a list or :class:`~pythonfinder.models.path.PathEntry` instance
:param Union[Sequence, PathEntry] path: The path or list of paths to expand
:param bool only_python: Whether to filter to include only python paths, default True
:returns: An iterator over the expanded set of path entries
:rtype: Iterator[PathEntry]
"""
if path is not None and (
isinstance(path, Sequence)
and not getattr(path.__class__, "__name__", "") == "PathEntry"
):
for p in path:
if p is None:
continue
for expanded in itertools.chain.from_iterable(
expand_paths(p, only_python=only_python)
):
yield expanded
elif path is not None and path.is_dir:
for p in path.children.values():
if p is not None and p.is_python and p.as_python is not None:
for sub_path in itertools.chain.from_iterable(
expand_paths(p, only_python=only_python)
):
yield sub_path
else:
if path is not None and (
not only_python or (path.is_python and path.as_python is not None)
):
yield path
def dedup(iterable):
# type: (Iterable) -> Iterable
"""Deduplicate an iterable object like iter(set(iterable)) but
order-reserved.
"""
return iter(OrderedDict.fromkeys(iterable))