mirror of
https://github.com/kennethreitz/pipenv.git
synced 2026-06-05 22:50:18 +00:00
16ad0a1005
Installation logic refactored into the new module. Also implemented logic to read newest version from "pyenv install --list" automatically, instead of maintaining a custom mapping.
120 lines
3.6 KiB
Python
120 lines
3.6 KiB
Python
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
|