Implement requirements refactor

Signed-off-by: Dan Ryan <dan@danryan.co>
This commit is contained in:
Dan Ryan
2018-04-11 20:29:17 -04:00
parent 594757fe37
commit ef1b4bccd7
3 changed files with 774 additions and 169 deletions
+3 -2
View File
@@ -25,6 +25,7 @@ import six
from .cmdparse import ScriptEmptyError
from .project import Project, SourceNotFound
from .requirements import PipenvRequirement
from .utils import (
convert_deps_from_pip,
convert_deps_to_pip,
@@ -1414,7 +1415,7 @@ def pip_install(
f.write(package_name)
# Install dependencies when a package is a VCS dependency.
try:
req = get_requirement(
req = PipenvRequirement.from_line(
package_name.split('--hash')[0].split('--trusted-host')[0]
).vcs
except (ParseException, ValueError) as e:
@@ -2566,7 +2567,7 @@ def do_clean(
)
installed_package_names = []
for installed in installed_packages:
r = get_requirement(installed)
r = PipenvRequirement.from_line(installed).requirement
# Ignore editable installations.
if not r.editable:
installed_package_names.append(r.name.lower())
+759
View File
@@ -0,0 +1,759 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import
import abc
import sys
from pipenv import PIPENV_VENDOR, PIPENV_PATCHED
sys.path.insert(0, PIPENV_VENDOR)
sys.path.insert(0, PIPENV_PATCHED)
import hashlib
import os
import requirements
import six
from attr import attrs, attrib, Factory, validators
from pip9.index import Link
from pip9.download import path_to_url
from pip9.req.req_install import _strip_extras
from pip9._vendor.distlib.markers import Evaluator
from pip9._vendor.packaging.markers import Marker, InvalidMarker
from pip9._vendor.packaging.specifiers import SpecifierSet, InvalidSpecifier
from pipenv.utils import SCHEME_LIST, VCS_LIST, is_installable_file, is_vcs, multi_split, get_converted_relative_path, is_star, is_valid_url
from first import first
try:
from pathlib import Path
except ImportError:
from pathlib2 import Path
HASH_STRING = ' --hash={0}'
def _strip_ssh_from_git_uri(uri):
"""Return git+ssh:// formatted URI to git+git@ format"""
if isinstance(uri, six.string_types):
uri = uri.replace('git+ssh://', 'git+')
return uri
def _clean_git_uri(uri):
"""Cleans VCS uris from pip9 format"""
if isinstance(uri, six.string_types):
# Add scheme for parsing purposes, this is also what pip does
if uri.startswith('git+') and '://' not in uri:
uri = uri.replace('git+', 'git+ssh://')
return uri
def _split_markers(line):
"""Split markers from a dependency"""
if not any(line.startswith(uri_prefix) for uri_prefix in SCHEME_LIST):
marker_sep = ';'
else:
marker_sep = '; '
markers = None
if marker_sep in line:
line, markers = line.split(marker_sep, 1)
markers = markers.strip() if markers else None
return line, markers
def _split_vcs_method(uri):
"""Split a vcs+uri formatted uri into (vcs, uri)"""
vcs_start = '{0}+'
vcs = first(
[vcs for vcs in VCS_LIST if uri.startswith(vcs_start.format(vcs))]
)
if vcs:
vcs, uri = uri.split('+', 1)
return vcs, uri
def _validate_vcs(instance, attr_, value):
if value not in VCS_LIST:
raise ValueError('Invalid vcs {0!r}'.format(value))
def _validate_path(instance, attr_, value):
if not os.path.exists(value):
raise ValueError('Invalid path {0!r}', format(value))
def _validate_markers(instance, attr_, value):
try:
Marker('{0}{1}'.format(attr_.name, value))
except InvalidMarker:
raise ValueError('Invalid Marker {0}{1}'.format(attr_, value))
def _validate_specifiers(instance, attr_, value):
if value == '':
return True
try:
SpecifierSet(value)
except InvalidMarker:
raise ValueError('Invalid Specifiers {0}'.format(value))
def _filter_none(k, v):
if v:
return True
return False
def _optional_instance_of(cls):
return validators.optional(validators.instance_of(cls))
@attrs
class Source(object):
# : URL to PyPI instance
url = attrib(default='')
# : If False, skip SSL checks
verify_ssl = attrib(
default=True,
validator=validators.optional(validators.instance_of(bool)),
)
# : human name to refer to this source (can be referenced in packages or dev-packages)
name = attrib(default='')
@six.add_metaclass(abc.ABCMeta)
class BaseRequirement():
@classmethod
def from_line(cls, line):
"""Returns a requirement from a requirements.txt or pip-compatible line"""
raise NotImplementedError
@abc.abstractmethod
def line_part(self):
"""Returns the current requirement as a pip-compatible line"""
@classmethod
def from_pipfile(cls, name, pipfile):
"""Returns a requirement from a pipfile entry"""
raise NotImplementedError
@abc.abstractmethod
def pipfile_part(self):
"""Returns the current requirement as a pipfile entry"""
@classmethod
def attr_fields(cls):
return [field.name for field in attr.fields(cls)]
@attrs
class PipenvMarkers(BaseRequirement):
"""System-level requirements - see PEP508 for more detail"""
os_name = attrib(
default=None, validator=validators.optional(_validate_markers)
)
sys_platform = attrib(
default=None, validator=validators.optional(_validate_markers)
)
platform_machine = attrib(
default=None, validator=validators.optional(_validate_markers)
)
platform_python_implementation = attrib(
default=None, validator=validators.optional(_validate_markers)
)
platform_release = attrib(
default=None, validator=validators.optional(_validate_markers)
)
platform_system = attrib(
default=None, validator=validators.optional(_validate_markers)
)
platform_version = attrib(
default=None, validator=validators.optional(_validate_markers)
)
python_version = attrib(
default=None, validator=validators.optional(_validate_markers)
)
python_full_version = attrib(
default=None, validator=validators.optional(_validate_markers)
)
implementation_name = attrib(
default=None, validator=validators.optional(_validate_markers)
)
implementation_version = attrib(
default=None, validator=validators.optional(_validate_markers)
)
@property
def line_part(self):
return ' and '.join(
['{0} {1}'.format(k, v) for k, v in attr.asdict(self, filter=_filter_none).items()]
)
@property
def pipfile_part(self):
return {'markers': self.as_line}
@classmethod
def make_marker(cls, marker_string):
marker = Marker(marker_string)
marker_dict = {}
for m in marker._markers:
if isinstance(m, six.string_types):
continue
var, op, val = m
if var.value in cls.attr_fields():
marker_dict[var.value] = '{0} "{1}"'.format(op, val)
return marker_dict
@classmethod
def from_line(cls, line):
if ';' in line:
line = line.rsplit(';', 1)[1].strip()
marker_dict = cls.make_marker(line)
return cls(**marker_dict)
@classmethod
def from_pipfile(cls, name, pipfile):
found_keys = [k for k in pipfile.keys() if k in cls.attr_fields()]
marker_strings = ['{0} {1}'.format(k, pipfile[k]) for k in found_keys]
if pipfile.get('markers'):
marker_strings.append(pipfile.get('markers'))
markers = {}
for marker in marker_strings:
marker_dict = cls.make_marker(marker)
if marker_dict:
markers.update(marker_dict)
return cls(**markers)
@attrs
class NamedRequirement(BaseRequirement):
name = attrib()
version = attrib(validator=validators.optional(_validate_specifiers))
req = attrib()
@req.default
def get_requirement(self):
return first(requirements.parse('{0}{1}'.format(self.name, self.version)))
@classmethod
def from_line(cls, line):
req = first(requirements.parse(line))
specifiers = None
if req.specifier:
specifiers = _specs_to_string(req.specs)
return cls(name=req.name, version=specifiers, req=req)
@classmethod
def from_pipfile(cls, name, pipfile):
creation_args = {}
if hasattr(pipfile, 'keys'):
creation_args = {k: v for k, v in pipfile.items() if k in cls.attr_fields()}
creation_args['name'] = name
version = _get_version(pipfile)
creation_args['version'] = version
creation_args['req'] = first(requirements.parse('{0}{1}'.format(name, version)))
return cls(**creation_args)
@property
def line_part(self):
return '{self.name}'.format(self=self)
@property
def pipfile_part(self):
pipfile_dict = attr.asdict(self, filter=_filter_none)
if 'version' not in pipfile_dict:
pipfile_dict['version'] = '*'
name = pipfile_dict.pop('name')
return {name: pipfile_dict}
@attrs
class FileRequirement(BaseRequirement):
"""File requirements for tar.gz installable files or wheels or setup.py
containing directories."""
path = attrib(default=None, validator=validators.optional(_validate_path))
# : path to hit - without any of the VCS prefixes (like git+ / http+ / etc)
uri = attrib()
name = attrib()
link = attrib()
editable = attrib(default=None)
req = attrib()
_has_hashed_name = False
_uri_scheme = None
@uri.default
def get_uri(self):
if self.path and not self.uri:
self._uri_scheme = 'path'
self.uri = path_to_url(os.path.abspath(self.path))
@name.default
def get_name(self):
loc = self.path or self.uri
if loc:
self._uri_scheme = 'path' if self.path else 'uri'
hashed_loc = hashlib.sha256(loc.encode('utf-8')).hexdigest()
hash_fragment = hashed_loc[-7:]
self._has_hashed_name = True
return hash_fragment
@link.default
def get_link(self):
target = '{0}#egg={1}'.format(self.uri, self.name)
return Link(target)
@req.default
def get_requirement(self):
base = '{0}'.format(self.link)
req = first(requirements.parse(base))
if self.editable:
req.editable = True
if self.link and self.link.scheme.startswith('file'):
if self.path:
req.path = self.path
req.local_file = True
self._uri_scheme = 'file'
req.uri = None
req.link = self.link
return req
@property
def is_remote_artifact(self):
return any(self.link.scheme.startswith(scheme) for scheme in ('http', 'https', 'ftp', 'ftps', 'uri')) and not self.req.local_file and (self.link.is_artifact or self.link.is_wheel) and not self.req.editable
@classmethod
def from_line(cls, line):
link = None
path = None
editable = line.startswith('-e ')
line = line.split(' ', 1)[1] if editable else line
if not any([is_installable_file(line), is_valid_url(line)]):
raise ValueError(
'Supplied requirement is not installable: {0!r}'.format(line)
)
if is_valid_url(line):
link = Link(line)
else:
_path = Path(line)
link = Link(_path.absolute().as_uri())
if _path.is_absolute() or _path.as_posix() == '.':
path = _path.as_posix()
else:
path = get_converted_relative_path(line)
arg_dict = {
'path': path,
'uri': link.url_without_fragment,
'link': link,
'editable': editable
}
if link.egg_fragment:
arg_dict['name'] = link.egg_fragment
created = cls(**arg_dict)
return created
@classmethod
def from_pipfile(cls, name, pipfile):
uri_key = first((k for k in ['uri', 'file'] if k in pipfile))
uri = pipfile.get(uri_key, pipfile.get('path'))
if not uri_key:
abs_path = os.path.abspath(uri)
uri = path_to_url(abs_path) if os.path.exists(abs_path) else None
link = Link(uri) if uri else None
arg_dict = {
'name': name,
'path': pipfile.get('path'),
'uri': link.url_without_fragment,
'editable': pipfile.get('editable'),
'link': link
}
return cls(**arg_dict)
@req.default
def get_requirement(self):
base = '{0}'.format(self.link)
req = first(requirements.parse(base))
if self.editable:
req.editable = True
if self.link and self.link.scheme.startswith('file') and self.path:
req.path = self.path
req.local_file = True
req.uri = None
req.link = self.link
return req
@property
def line_part(self):
seed = self.path or self.link.url or self.uri
editable = '-e ' if self.editable else ''
return '{0}{1}'.format(editable, seed)
@property
def pipfile_part(self):
pipfile_dict = {k: v for k, v in attr.asdict(self, filter=_filter_none).items()}
name = pipfile_dict.pop('name')
req = self.req
if self._has_hashed_name and self.is_remote_artifact:
dict_key = 'file'
# Look for uri first because file is a uri format and this is designed
# to make sure we add file keys to the pipfile as a replacement of uri
target_keys = [k for k in pipfile_dict.keys() if k in ['uri', 'path']]
pipfile_dict[dict_key] = pipfile_dict.pop(first(target_keys))
if len(target_keys) > 1:
_ = pipfile_dict.pop(target_keys[1])
else:
collisions = [key for key in ['path', 'uri', 'file'] if key in pipfile_dict]
if len(collisions) > 1:
for k in collisions[1:]:
_ = pipfile_dict.pop(k)
return {name: pipfile_dict}
@attrs
class VCSRequirement(FileRequirement):
editable = attrib(default=None)
uri = attrib(default=None)
path = attrib(default=None, validator=validators.optional(_validate_path))
vcs = attrib(validator=validators.optional(_validate_vcs), default=None)
# : vcs reference name (branch / commit / tag)
ref = attrib(default=None)
#: path to hit - without any of the VCS prefixes (like git+ / http+ / etc)
uri = attrib(default=None)
subdirectory = attrib(default=None)
name = attrib()
link = attrib()
req = attrib()
_INCLUDE_FIELDS = ('editable', 'uri', 'path', 'vcs', 'ref', 'subdirectory', 'name', 'link', 'req')
@link.default
def get_link(self):
return build_vcs_link(self.vcs, _clean_git_uri(self.uri), name=self.name, ref=self.ref, subdirectory=self.subdirectory)
@name.default
def get_name(self):
return self.link.egg_fragment or self.req.name if self.req else ''
@property
def vcs_uri(self):
uri = self.uri
if not any(uri.startswith('{0}+'.format(vcs)) for vcs in VCS_LIST):
uri = '{0}+{1}'.format(self.vcs, uri)
return uri
@req.default
def get_requirement(self):
prefix = '-e ' if self.editable else ''
line = '{0}{1}'.format(prefix, self.link.url)
req = first(requirements.parse(line))
if self.path and self.link and self.link.scheme.startswith('file'):
req.local_file = True
req.path = self.path
if self.editable:
req.editable = True
req.link = self.link
if self.uri != self.link.url and 'git+ssh://' in self.link.url and 'git+git@' in self.uri:
req.line = _strip_ssh_from_git_uri(req.line)
req.uri = _strip_ssh_from_git_uri(req.uri)
if not req.name:
raise ValueError(
'pipenv requires an #egg fragment for version controlled '
'dependencies. Please install remote dependency '
'in the form {0}#egg=<package-name>.'.format(req.uri)
)
if self.vcs and not req.vcs:
req.vcs = self.vcs
if self.ref and not req.revision:
req.revision = self.ref
return req
@classmethod
def from_pipfile(cls, name, pipfile):
creation_args = {}
pipfile_keys = [
k
for k in (
'ref', 'vcs', 'subdirectory', 'path', 'editable', 'file', 'uri'
) +
VCS_LIST
if k in pipfile
]
for key in pipfile_keys:
if key in VCS_LIST:
creation_args['vcs'] = key
composed_uri = _clean_git_uri('{0}+{1}'.format(key, pipfile.get(key))).lstrip('{0}+'.format(key))
is_url = is_valid_url(pipfile.get(key)) or is_valid_url(composed_uri)
target_key = 'uri' if is_url else 'path'
creation_args[target_key] = pipfile.get(key)
else:
creation_args[key] = pipfile.get(key)
creation_args['name'] = name
return cls(**creation_args)
@classmethod
def from_line(cls, line, editable=None):
if line.startswith('-e '):
editable = True
line = line.split(' ', 1)[1]
vcs_line = _clean_git_uri(line)
vcs_method, vcs_location = _split_vcs_method(vcs_line)
if not is_valid_url(vcs_location) and os.path.exists(vcs_location):
path = get_converted_relative_path(vcs_location)
vcs_location = path_to_url(os.path.abspath(vcs_location))
link = Link(vcs_line)
name = link.egg_fragment
uri = link.url_without_fragment
if 'git+git@' in line:
uri = _strip_ssh_from_git_uri(uri)
subdirectory = link.subdirectory_fragment
ref = None
if '@' in link.show_url:
uri, ref = uri.rsplit('@', 1)
return cls(
name=name,
ref=ref,
vcs=vcs_method,
subdirectory=subdirectory,
link=link,
path=path,
editable=editable,
uri=uri,
)
@property
def line_part(self):
"""requirements.txt compatible line part sans-extras"""
if self.req:
return self.req.line
base = '{0}'.format(self.link)
if self.editable:
base = '-e {0}'.format(base)
return base
@staticmethod
def _choose_vcs_source(pipfile):
src_keys = [k for k in pipfile.keys() if k in ['path', 'uri', 'file']]
if src_keys:
chosen_key = first(src_keys)
vcs_type = pipfile.pop('vcs')
_, pipfile_url = _split_vcs_method(pipfile.get(chosen_key))
pipfile[vcs_type] = pipfile_url
for removed in src_keys:
_ = pipfile.pop(removed)
return pipfile
@property
def pipfile_part(self):
pipfile_dict = attr.asdict(self, filter=_filter_none).copy()
if 'vcs' in pipfile_dict:
pipfile_dict = self._choose_vcs_source(pipfile_dict)
name = pipfile_dict.pop('name')
return {name: pipfile_dict}
@attrs
class PipenvRequirement(object):
name = attrib()
vcs = attrib(default=None, validator=validators.optional(_validate_vcs))
req = attrib(
default=None, validator=_optional_instance_of(BaseRequirement)
)
markers = attrib(default=None)
specifiers = attrib(
validator=validators.optional(_validate_specifiers)
)
index = attrib(default=None)
editable = attrib(default=None)
hashes = attrib(default=Factory(list), converter=list)
extras = attrib(default=Factory(list))
_INCLUDE_FIELDS = ('name', 'markers', 'index', 'editable', 'hashes', 'extras')
@name.default
def get_name(self):
return self.req.name
@property
def requirement(self):
return self.req.req
@property
def hashes_as_pip(self):
if self.hashes:
return ''.join([HASH_STRING.format(h) for h in self.hashes])
return ''
@property
def markers_as_pip(self):
if self.markers:
return '; {0}'.format(self.markers)
return ''
@property
def extras_as_pip(self):
if self.extras:
return '[{0}]'.format(','.join(self.extras))
return ''
@specifiers.default
def get_specifiers(self):
if self.req and self.req.req.specifier:
return _specs_to_string(self.req.req.specs)
return
@classmethod
def from_line(cls, line):
hashes = None
if '--hash=' in line:
hashes = line.split(' --hash=')
line, hashes = hashes[0], hashes[1:]
editable = line.startswith('-e ')
stripped_line = line.split(' ', 1)[1] if editable else line
line, markers = _split_markers(line)
line, extras = _strip_extras(line)
vcs = None
# Installable local files and installable non-vcs urls are handled
# as files, generally speaking
if is_installable_file(stripped_line) or (is_valid_url(stripped_line) and not is_vcs(stripped_line)):
r = FileRequirement.from_line(line)
elif is_vcs(stripped_line):
r = VCSRequirement.from_line(line)
else:
name = multi_split(stripped_line, '!=<>~')[0]
if not extras:
name, extras = _strip_extras(name)
r = NamedRequirement.from_line(stripped_line)
if extras:
extras = first(
requirements.parse('fakepkg{0}'.format(_extras_to_string(extras)))
).extras
r.req.extras = extras
if markers:
r.req.markers = markers
args = {
'name': r.name,
'vcs': vcs,
'req': r,
'markers': markers,
'editable': editable,
}
if extras:
args['extras'] = extras
if hashes:
args['hashes'] = hashes
return cls(**args)
@classmethod
def from_pipfile(cls, name, indexes, pipfile):
_pipfile = {}
if hasattr(pipfile, 'keys'):
_pipfile = dict(pipfile).copy()
_pipfile['version'] = _get_version(pipfile)
vcs = first([vcs for vcs in VCS_LIST if vcs in _pipfile])
if vcs:
_pipfile['vcs'] = vcs
r = VCSRequirement.from_pipfile(name, pipfile)
elif any(key in _pipfile for key in ['path', 'file', 'uri']):
r = FileRequirement.from_pipfile(name, pipfile)
else:
r = NamedRequirement.from_pipfile(name, pipfile)
args = {
'name': r.name,
'vcs': vcs,
'req': r,
'markers': PipenvMarkers.from_pipfile(name, _pipfile).line_part,
'extras': _pipfile.get('extras'),
'editable': _pipfile.get('editable', False),
'index': _pipfile.get('index')
}
if any(key in _pipfile for key in ['hash', 'hashes']):
args['hashes'] = _pipfile.get('hashes', [pipfile.get('hash')])
return cls(**args)
def as_line(self, include_index=False, project=None):
line = '{0}{1}{2}{3}{4}'.format(
self.req.line_part,
self.extras_as_pip,
self.specifiers if self.specifiers else '',
self.markers_as_pip,
self.hashes_as_pip,
)
if include_index and not (self.requirement.local_file or self.vcs):
from .utils import prepare_pip_source_args
if self.index:
pip_src_args = [project.get_source(self.index)]
else:
pip_src_args = project.sources
index_string = ' '.join(prepare_pip_source_args(pip_src_args))
line = '{0} {1}'.format(line, index_string)
return line
def as_pipfile(self, include_index=False):
good_keys = ('hashes', 'extras', 'markers', 'editable', 'version', 'index') + VCS_LIST
req_dict = {k: v for k, v in attr.asdict(self, recurse=False, filter=_filter_none).items() if k in good_keys}
name = self.name
base_dict = {k: v for k, v in self.req.pipfile_part[name].items() if k not in ['req', 'link']}
base_dict.update(req_dict)
conflicting_keys = ('file', 'path', 'uri')
if 'file' in base_dict and any(k in base_dict for k in conflicting_keys[1:]):
conflicts = [k for k in (conflicting_keys[1:],) if k in base_dict]
for k in conflicts:
_ = base_dict.pop(k)
if 'hashes' in base_dict and len(base_dict['hashes']) == 1:
base_dict['hash'] = base_dict.pop('hashes')[0]
if len(base_dict.keys()) == 1 and 'version' in base_dict:
base_dict = base_dict.get('version')
return {name: base_dict}
def _extras_to_string(extras):
"""Turn a list of extras into a string"""
if isinstance(extras, six.string_types):
if extras.startswith('['):
return extras
else:
extras = [extras]
return '[{0}]'.format(','.join(extras))
def _specs_to_string(specs):
"""Turn a list of specifier tuples into a string"""
if specs:
if isinstance(specs, six.string_types):
return specs
return ','.join([''.join(spec) for spec in specs])
return ''
def build_vcs_link(
vcs, uri, name=None, ref=None, subdirectory=None, extras=None
):
if extras is None:
extras = []
vcs_start = '{0}+'.format(vcs)
if not uri.startswith(vcs_start):
uri = '{0}{1}'.format(vcs_start, uri)
uri = _clean_git_uri(uri)
if ref:
uri = '{0}@{1}'.format(uri, ref)
if name:
uri = '{0}#egg={1}'.format(uri, name)
if extras:
extras = _extras_to_string(extras)
uri = '{0}{1}'.format(uri, extras)
if subdirectory:
uri = '{0}&subdirectory={1}'.format(uri, subdirectory)
return Link(uri)
def _get_version(pipfile_entry):
if str(pipfile_entry) == '{}' or is_star(pipfile_entry):
return ''
elif hasattr(pipfile_entry, 'keys') and 'version' in pipfile_entry:
if is_star(pipfile_entry.get('version')):
return ''
return pipfile_entry.get('version', '')
if isinstance(pipfile_entry, six.string_types):
return pipfile_entry
return ''
+12 -167
View File
@@ -55,6 +55,7 @@ try:
from collections.abc import Mapping
except ImportError:
from collections import Mapping
from .requirements import PipenvRequirement
if six.PY2:
@@ -583,84 +584,8 @@ def multi_split(s, split):
def convert_deps_from_pip(dep):
""""Converts a pip-formatted dependency to a Pipfile-formatted one."""
try:
from collections.abc import Mapping
except ImportError:
from collections import Mapping
dependency = {}
req = get_requirement(dep)
extras = {'extras': req.extras}
# File installs.
if (req.uri or req.path or is_installable_file(req.name)) and not req.vcs:
# Assign a package name to the file, last 7 of it's sha256 hex digest.
if not req.uri and not req.path:
req.path = os.path.abspath(req.name)
hashable_path = req.uri if req.uri else req.path
if not req.name:
req.name = hashlib.sha256(hashable_path.encode('utf-8')).hexdigest()
req.name = req.name[len(req.name) - 7:]
# {path: uri} TOML (spec 4 I guess...)
if req.uri:
dependency[req.name] = {'file': hashable_path}
else:
dependency[req.name] = {'path': hashable_path}
if req.extras:
dependency[req.name].update(extras)
# Add --editable if applicable
if req.editable:
dependency[req.name].update({'editable': True})
# VCS Installs.
elif req.vcs:
if req.name is None:
raise ValueError(
'pipenv requires an #egg fragment for version controlled '
'dependencies. Please install remote dependency '
'in the form {0}#egg=<package-name>.'.format(req.uri)
)
# Crop off the git+, etc part.
if req.uri.startswith('{0}+'.format(req.vcs)):
req.uri = req.uri[len(req.vcs) + 1:]
dependency.setdefault(req.name, {}).update({req.vcs: req.uri})
# Add --editable, if it's there.
if req.editable:
dependency[req.name].update({'editable': True})
# Add subdirectory, if it's there
if req.subdirectory:
dependency[req.name].update({'subdirectory': req.subdirectory})
# Add the specifier, if it was provided.
if req.revision:
dependency[req.name].update({'ref': req.revision})
# Extras: e.g. #egg=requests[security]
if req.extras:
dependency[req.name].update({'extras': req.extras})
elif req.extras or req.specs or hasattr(req, 'markers'):
specs = None
# Comparison operators: e.g. Django>1.10
if req.specs:
r = multi_split(dep, '!=<>~')
specs = dep[len(r[0]):]
dependency[req.name] = specs
# Extras: e.g. requests[socks]
if req.extras:
dependency[req.name] = extras
if specs:
dependency[req.name].update({'version': specs})
if hasattr(req, 'markers'):
if isinstance(dependency[req.name], six.string_types):
dependency[req.name] = {'version': specs}
dependency[req.name].update({'markers': req.markers})
# Bare dependencies: e.g. requests
else:
dependency[dep] = '*'
# Cleanup when there's multiple values, e.g. -e.
if len(dependency) > 1:
for key in dependency.copy():
if not hasattr(dependency[key], 'keys'):
del dependency[key]
return dependency
req = PipenvRequirement.from_line(dep)
return req.as_pipfile()
def is_star(val):
@@ -677,96 +602,16 @@ def convert_deps_to_pip(deps, project=None, r=True, include_index=False):
""""Converts a Pipfile-formatted dependency to a pip-formatted one."""
from ._compat import NamedTemporaryFile
dependencies = []
for dep in deps.keys():
# Default (e.g. '>1.10').
extra = deps[dep] if isinstance(deps[dep], six.string_types) else ''
editable = False
extras = ''
version = ''
index = ''
# Get rid of '*'.
if is_star(deps[dep]) or str(extra) == '{}':
extra = ''
hash = ''
# Support for single hash (spec 1).
if 'hash' in deps[dep]:
hash = ' --hash={0}'.format(deps[dep]['hash'])
# Support for multiple hashes (spec 2).
if 'hashes' in deps[dep]:
hash = '{0} '.format(
''.join(
[' --hash={0} '.format(h) for h in deps[dep]['hashes']]
)
)
# Support for extras (e.g. requests[socks])
if 'extras' in deps[dep]:
extras = '[{0}]'.format(','.join(deps[dep]['extras']))
if 'version' in deps[dep]:
if not is_star(deps[dep]['version']):
version = deps[dep]['version']
# For lockfile format.
if 'markers' in deps[dep]:
specs = '; {0}'.format(deps[dep]['markers'])
else:
# For pipfile format.
specs = []
for specifier in specifiers:
if specifier in deps[dep]:
if not is_star(deps[dep][specifier]):
specs.append(
'{0} {1}'.format(specifier, deps[dep][specifier])
)
if specs:
specs = '; {0}'.format(' and '.join(specs))
else:
specs = ''
if include_index and not is_file(deps[dep]) and not is_vcs(deps[dep]):
pip_src_args = []
if 'index' in deps[dep]:
pip_src_args = [project.get_source(deps[dep]['index'])]
for idx in project.sources:
if idx['url'] != pip_src_args[0]['url']:
pip_src_args.append(idx)
else:
pip_src_args = project.sources
pip_args = prepare_pip_source_args(pip_src_args)
index = ' '.join(pip_args)
# Support for version control
maybe_vcs = [vcs for vcs in VCS_LIST if vcs in deps[dep]]
vcs = maybe_vcs[0] if maybe_vcs else None
if not any(key in deps[dep] for key in ['path', 'vcs', 'file']):
extra += extras
if isinstance(deps[dep], Mapping):
editable = bool(deps[dep].get('editable', False))
# Support for files.
if 'file' in deps[dep]:
dep_file = deps[dep]['file']
if is_valid_url(dep_file) and dep_file.startswith('http'):
dep_file += '#egg={0}'.format(dep)
extra = '{0}{1}'.format(dep_file, extras).strip()
# Flag the file as editable if it is a local relative path
dep = '-e ' if editable else ''
# Support for paths.
elif 'path' in deps[dep]:
extra = '{1}{0}'.format(extras, deps[dep]['path']).strip()
# Flag the file as editable if it is a local relative path
dep = '-e ' if editable else ''
if vcs:
extra = '{0}+{1}'.format(vcs, deps[dep][vcs])
# Support for @refs.
if 'ref' in deps[dep]:
extra += '@{0}'.format(deps[dep]['ref'])
extra += '#egg={0}{1}'.format(dep, extras)
# Support for subdirectory
if 'subdirectory' in deps[dep]:
extra += '&subdirectory={0}'.format(deps[dep]['subdirectory'])
# Support for editable.
dep = '-e ' if editable else ''
s = '{0}{1}{2}{3}{4} {5}'.format(
dep, extra, version, specs, hash, index
for dep_name, dep in deps.items():
indexes = project.sources if hasattr(project, 'sources') else None
if hasattr(dep, 'keys') and dep.get('index'):
indexes = project.get_source(dep['index'])
new_dep = PipenvRequirement.from_pipfile(dep_name, indexes, dep)
req = new_dep.as_line(
project=project,
include_index=include_index
).strip()
dependencies.append(s)
dependencies.append(req)
if not r:
return dependencies