Files
pipenv/pipenv/pyenv.py
T
Tzu-ping Chung 16ad0a1005 New, better pyenv module
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.
2018-07-12 17:55:38 +08:00

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