diff --git a/pipenv/core.py b/pipenv/core.py index 60d9cdce..5c5783f8 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -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()) diff --git a/pipenv/requirements.py b/pipenv/requirements.py new file mode 100644 index 00000000..3120b1dd --- /dev/null +++ b/pipenv/requirements.py @@ -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=.'.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 '' diff --git a/pipenv/utils.py b/pipenv/utils.py index e7619fa4..614b333c 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -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=.'.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