Merge pull request #2572 from pypa/pyenv-autodetect

New, better pyenv module
This commit is contained in:
Tzu-ping Chung
2018-07-18 16:14:31 +08:00
committed by GitHub
2 changed files with 131 additions and 31 deletions
+12 -31
View File
@@ -458,27 +458,15 @@ def ensure_python(three=None, python=None):
abort()
else:
if (not PIPENV_DONT_USE_PYENV) and (SESSION_IS_INTERACTIVE or PIPENV_YES):
version_map = {
# TODO: Keep this up to date!
# These versions appear incompatible with virtualenv:
# '2.5': '2.5.6',
"2.6": "2.6.9",
"2.7": "2.7.15",
# '3.1': '3.1.5',
# '3.2': '3.2.6',
"3.3": "3.3.7",
"3.4": "3.4.8",
"3.5": "3.5.5",
"3.6": "3.6.6",
"3.7": "3.7.0",
}
from .pyenv import Runner, PyenvError
pyenv = Runner("pyenv")
try:
if len(python.split(".")) == 2:
# Find the latest version of Python available.
version = version_map[python]
else:
version = python
except KeyError:
version = pyenv.find_version_to_install(python)
except ValueError:
abort()
except PyenvError as e:
click.echo(u"Something went wrong…")
click.echo(crayons.blue(e.err), err=True)
abort()
s = "{0} {1} {2}".format(
"Would you like us to install",
@@ -500,24 +488,17 @@ def ensure_python(three=None, python=None):
)
)
with spinner():
# Install Python.
c = delegator.run(
"pyenv install {0} -s".format(version),
timeout=PIPENV_INSTALL_TIMEOUT,
block=False,
)
# Wait until the process has finished…
c.block()
try:
assert c.return_code == 0
except AssertionError:
c = pyenv.install(version)
except PyenvError as e:
click.echo(u"Something went wrong…")
click.echo(crayons.blue(c.err), err=True)
click.echo(crayons.blue(e.err), err=True)
# Print the results, in a beautiful blue…
click.echo(crayons.blue(c.out), err=True)
# Add new paths to PATH.
activate_pyenv()
# Find the newly installed Python, hopefully.
version = str(version)
path_to_python = find_a_system_python(version)
try:
assert python_version(path_to_python) == version
+119
View File
@@ -0,0 +1,119 @@
import operator
import re
from .vendor import attr, delegator
from .environments import PIPENV_INSTALL_TIMEOUT
@attr.s
class Version(object):
major = attr.ib()
minor = attr.ib()
patch = attr.ib()
def __str__(self):
parts = [self.major, self.minor]
if self.patch is not None:
parts.append(self.patch)
return '.'.join(str(p) for p in parts)
@classmethod
def parse(cls, name):
"""Parse an X.Y.Z or X.Y string into a version tuple.
"""
match = re.match(r'^(\d+)\.(\d+)(?:\.(\d+))?$', name)
if not match:
raise ValueError('invalid version name {0!r}'.format(name))
major = int(match.group(1))
minor = int(match.group(2))
patch = match.group(3)
if patch is not None:
patch = int(patch)
return cls(major, minor, patch)
@property
def cmpkey(self):
"""Make the version a comparable tuple.
Some old Python versions does not have a patch part, e.g. 2.7.0 is
named "2.7" in pyenv. Fix that, otherwise `None` will fail to compare
with int.
"""
return (self.major, self.minor, self.patch or 0)
def matches_minor(self, other):
"""Check whether this version matches the other in (major, minor).
"""
return (self.major, self.minor) == (other.major, other.minor)
class PyenvError(RuntimeError):
def __init__(self, desc, c):
super(PyenvError, self).__init__(desc)
self.out = c.out
self.err = c.err
class Runner(object):
def __init__(self, pyenv):
self._cmd = pyenv
def _pyenv(self, *args, **kwargs):
timeout = kwargs.pop('timeout', delegator.TIMEOUT)
if kwargs:
k = list(kwargs.keys())[0]
raise TypeError('unexpected keyword argument {0!r}'.format(k))
args = (self._cmd,) + tuple(args)
c = delegator.run(args, block=False, timeout=timeout)
c.block()
if c.return_code != 0:
raise PyenvError('faild to run {0}'.format(args), c)
return c
def iter_installable_versions(self):
"""Iterate through CPython versions available for Pipenv to install.
"""
for name in self._pyenv('install', '--list').out.splitlines():
try:
version = Version.parse(name.strip())
except ValueError:
continue
yield version
def find_version_to_install(self, name):
"""Find a version in pyenv from the version supplied.
A ValueError is raised if a matching version cannot be found.
"""
version = Version.parse(name)
if version.patch is not None:
return name
try:
best_match = max((
inst_version
for inst_version in self.iter_installable_versions()
if inst_version.matches_minor(version)
), key=operator.attrgetter('cmpkey'))
except ValueError:
raise ValueError(
'no installable version found for {0!r}'.format(name),
)
return best_match
def install(self, version):
"""Install the given version with pyenv.
The version must be a ``Version`` instance representing a version
found in pyenv.
A ValueError is raised if the given version does not have a match in
pyenv. A PyenvError is raised if the pyenv command fails.
"""
c = self._pyenv(
'install', '-s', str(version),
timeout=PIPENV_INSTALL_TIMEOUT,
)
return c