mirror of
https://github.com/kennethreitz/pipenv.git
synced 2026-06-05 14:50:16 +00:00
Merge branch 'master' into unc-paths
This commit is contained in:
+9
-7
@@ -34,7 +34,7 @@ def main():
|
||||
new_sys_argv.append(v)
|
||||
sys.argv = new_sys_argv
|
||||
|
||||
import pipenv.core
|
||||
from pipenv.utils import create_mirror_source, resolve_deps, replace_pypi_sources
|
||||
|
||||
if is_verbose:
|
||||
logging.getLogger('notpip').setLevel(logging.INFO)
|
||||
@@ -48,12 +48,10 @@ def main():
|
||||
for i, package in enumerate(packages):
|
||||
if package.startswith('--'):
|
||||
del packages[i]
|
||||
pypi_mirror_source = pipenv.utils.create_mirror_source(os.environ['PIPENV_PYPI_MIRROR']) if 'PIPENV_PYPI_MIRROR' in os.environ else None
|
||||
project = pipenv.core.project
|
||||
pypi_mirror_source = create_mirror_source(os.environ['PIPENV_PYPI_MIRROR']) if 'PIPENV_PYPI_MIRROR' in os.environ else None
|
||||
|
||||
def resolve(packages, pre, sources, verbose, clear, system):
|
||||
import pipenv.utils
|
||||
return pipenv.utils.resolve_deps(
|
||||
def resolve(packages, pre, project, sources, verbose, clear, system):
|
||||
return resolve_deps(
|
||||
packages,
|
||||
which,
|
||||
project=project,
|
||||
@@ -64,10 +62,14 @@ def main():
|
||||
allow_global=system,
|
||||
)
|
||||
|
||||
from pipenv.core import project
|
||||
sources = replace_pypi_sources(project.pipfile_sources, pypi_mirror_source) if pypi_mirror_source else project.pipfile_sources
|
||||
print('using sources: %s' % sources)
|
||||
results = resolve(
|
||||
packages,
|
||||
pre=do_pre,
|
||||
sources = pipenv.utils.replace_pypi_sources(project.pipfile_sources, pypi_mirror_source) if pypi_mirror_source else project.pipfile_sources,
|
||||
project=project,
|
||||
sources=sources,
|
||||
verbose=is_verbose,
|
||||
clear=do_clear,
|
||||
system=system,
|
||||
|
||||
+7
-3
@@ -251,7 +251,7 @@ def actually_resolve_deps(
|
||||
# req.as_line() is theoratically the same as dep, but is guaranteed to
|
||||
# be normalized. This is safer than passing in dep.
|
||||
# TODO: Stop passing dep lines around; just use requirement objects.
|
||||
constraints.append(req.as_line(sources=None))
|
||||
constraints.append(req.constraint_line)
|
||||
# extra_constraints = []
|
||||
|
||||
if url:
|
||||
@@ -1220,7 +1220,7 @@ def clean_resolved_dep(dep, is_top_level=False, pipfile_entry=None):
|
||||
lockfile = {
|
||||
'version': '=={0}'.format(dep['version']),
|
||||
}
|
||||
for key in ['hashes', 'index']:
|
||||
for key in ['hashes', 'index', 'extras']:
|
||||
if key in dep:
|
||||
lockfile[key] = dep[key]
|
||||
# In case we lock a uri or a file when the user supplied a path
|
||||
@@ -1228,7 +1228,11 @@ def clean_resolved_dep(dep, is_top_level=False, pipfile_entry=None):
|
||||
fs_key = next((k for k in ['path', 'file'] if k in pipfile_entry), None)
|
||||
lockfile_key = next((k for k in ['uri', 'file', 'path'] if k in lockfile), None)
|
||||
if fs_key != lockfile_key:
|
||||
del lockfile[lockfile_key]
|
||||
try:
|
||||
del lockfile[lockfile_key]
|
||||
except KeyError:
|
||||
# pass when there is no lock file, usually because it's the first time
|
||||
pass
|
||||
lockfile[fs_key] = pipfile_entry[fs_key]
|
||||
|
||||
# If a package is **PRESENT** in the pipfile but has no markers, make sure we
|
||||
|
||||
+7
-1
@@ -17,7 +17,10 @@ if six.PY2:
|
||||
|
||||
class FileNotFoundError(IOError):
|
||||
pass
|
||||
|
||||
|
||||
else:
|
||||
|
||||
class FileNotFoundError(FileNotFoundError):
|
||||
pass
|
||||
|
||||
@@ -57,4 +60,7 @@ get_installed_distributions = do_import(
|
||||
is_installable_file = do_import("utils.misc", "is_installable_file", old_path="utils")
|
||||
is_installable_dir = do_import("utils.misc", "is_installable_dir", old_path="utils")
|
||||
PyPI = do_import("models.index", "PyPI")
|
||||
make_abstract_dist = do_import("operations.prepare", "make_abstract_dist", old_path="req.req_set")
|
||||
make_abstract_dist = do_import(
|
||||
"operations.prepare", "make_abstract_dist", old_path="req.req_set"
|
||||
)
|
||||
VcsSupport = do_import("vcs", "VcsSupport")
|
||||
|
||||
+1
-3
@@ -3,9 +3,7 @@ from __future__ import absolute_import
|
||||
import attr
|
||||
import json
|
||||
from .requirements import Requirement
|
||||
from .utils import (
|
||||
optional_instance_of,
|
||||
)
|
||||
from .utils import optional_instance_of
|
||||
from .._compat import Path, FileNotFoundError
|
||||
|
||||
|
||||
|
||||
+9
-3
@@ -11,8 +11,12 @@ from ..exceptions import RequirementError
|
||||
class PipenvMarkers(BaseRequirement):
|
||||
"""System-level requirements - see PEP508 for more detail"""
|
||||
|
||||
os_name = attr.ib(default=None, validator=attr.validators.optional(validate_markers))
|
||||
sys_platform = attr.ib(default=None, validator=attr.validators.optional(validate_markers))
|
||||
os_name = attr.ib(
|
||||
default=None, validator=attr.validators.optional(validate_markers)
|
||||
)
|
||||
sys_platform = attr.ib(
|
||||
default=None, validator=attr.validators.optional(validate_markers)
|
||||
)
|
||||
platform_machine = attr.ib(
|
||||
default=None, validator=attr.validators.optional(validate_markers)
|
||||
)
|
||||
@@ -59,7 +63,9 @@ class PipenvMarkers(BaseRequirement):
|
||||
try:
|
||||
marker = Marker(marker_string)
|
||||
except InvalidMarker:
|
||||
raise RequirementError("Invalid requirement: Invalid marker %r" % marker_string)
|
||||
raise RequirementError(
|
||||
"Invalid requirement: Invalid marker %r" % marker_string
|
||||
)
|
||||
marker_dict = {}
|
||||
for m in marker._markers:
|
||||
if isinstance(m, six.string_types):
|
||||
|
||||
+16
-18
@@ -15,9 +15,7 @@ class Source(object):
|
||||
#: URL to PyPI instance
|
||||
url = attr.ib(default="pypi")
|
||||
#: If False, skip SSL checks
|
||||
verify_ssl = attr.ib(
|
||||
default=True, validator=optional_instance_of(bool)
|
||||
)
|
||||
verify_ssl = attr.ib(default=True, validator=optional_instance_of(bool))
|
||||
#: human name to refer to this source (can be referenced in packages or dev-packages)
|
||||
name = attr.ib(default="")
|
||||
|
||||
@@ -27,13 +25,13 @@ class Source(object):
|
||||
@property
|
||||
def expanded(self):
|
||||
source_dict = attr.asdict(self).copy()
|
||||
source_dict['url'] = os.path.expandvars(source_dict.get('url'))
|
||||
source_dict["url"] = os.path.expandvars(source_dict.get("url"))
|
||||
return source_dict
|
||||
|
||||
|
||||
@attr.s
|
||||
class Section(object):
|
||||
ALLOWED_NAMES = ('packages', 'dev-packages',)
|
||||
ALLOWED_NAMES = ("packages", "dev-packages")
|
||||
#: Name of the pipfile section
|
||||
name = attr.ib(default="packages")
|
||||
#: A list of requirements that are contained by the section
|
||||
@@ -63,7 +61,7 @@ class RequiresSection(object):
|
||||
requires = attr.asdict(self, filter=filter_none)
|
||||
if not requires:
|
||||
return {}
|
||||
return {'requires': requires}
|
||||
return {"requires": requires}
|
||||
|
||||
|
||||
@attr.s
|
||||
@@ -72,7 +70,7 @@ class PipenvSection(object):
|
||||
|
||||
def get_dict(self):
|
||||
if self.allow_prereleases:
|
||||
return {'pipenv': attr.asdict(self)}
|
||||
return {"pipenv": attr.asdict(self)}
|
||||
return {}
|
||||
|
||||
|
||||
@@ -111,7 +109,7 @@ class Pipfile(object):
|
||||
_dict = {}
|
||||
for src in self.sources:
|
||||
_dict.update(src.get_dict())
|
||||
return {'source': _dict} if _dict else {}
|
||||
return {"source": _dict} if _dict else {}
|
||||
|
||||
def get_sections(self):
|
||||
"""Return a dictionary with both pipfile sections and requirements"""
|
||||
@@ -131,7 +129,7 @@ class Pipfile(object):
|
||||
|
||||
def get_dict(self):
|
||||
_dict = attr.asdict(self, recurse=False)
|
||||
for k in ['path', 'pipfile_hash', 'sources', 'sections', 'requires', 'pipenv']:
|
||||
for k in ["path", "pipfile_hash", "sources", "sections", "requires", "pipenv"]:
|
||||
if k in _dict:
|
||||
_dict.pop(k)
|
||||
return _dict
|
||||
@@ -153,25 +151,25 @@ class Pipfile(object):
|
||||
def load(cls, path):
|
||||
if not isinstance(path, Path):
|
||||
path = Path(path)
|
||||
pipfile_path = path / 'Pipfile'
|
||||
pipfile_path = path / "Pipfile"
|
||||
if not path.exists():
|
||||
raise FileNotFoundError("%s is not a valid project path!" % path)
|
||||
elif not pipfile_path.exists() or not pipfile_path.is_file():
|
||||
raise RequirementError("%s is not a valid Pipfile" % pipfile_path)
|
||||
pipfile_dict = toml.load(pipfile_path.as_posix())
|
||||
sections = [cls.get_section(pipfile_dict, s) for s in Section.ALLOWED_NAMES]
|
||||
pipenv = pipfile_dict.get('pipenv', {})
|
||||
requires = pipfile_dict.get('requires', {})
|
||||
pipenv = pipfile_dict.get("pipenv", {})
|
||||
requires = pipfile_dict.get("requires", {})
|
||||
creation_dict = {
|
||||
'path': pipfile_path,
|
||||
'sources': [Source(**src) for src in pipfile_dict.get('source', [])],
|
||||
'sections': sections,
|
||||
'scripts': pipfile_dict.get('scripts')
|
||||
"path": pipfile_path,
|
||||
"sources": [Source(**src) for src in pipfile_dict.get("source", [])],
|
||||
"sections": sections,
|
||||
"scripts": pipfile_dict.get("scripts"),
|
||||
}
|
||||
if requires:
|
||||
creation_dict['requires'] = RequiresSection(**requires)
|
||||
creation_dict["requires"] = RequiresSection(**requires)
|
||||
if pipenv:
|
||||
creation_dict['pipenv'] = PipenvSection(**pipenv)
|
||||
creation_dict["pipenv"] = PipenvSection(**pipenv)
|
||||
return cls(**creation_dict)
|
||||
|
||||
@staticmethod
|
||||
|
||||
+227
-72
@@ -1,11 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
|
||||
import attr
|
||||
import collections
|
||||
import hashlib
|
||||
import os
|
||||
import requirements
|
||||
|
||||
from first import first
|
||||
from pkg_resources import RequirementParseError
|
||||
from six.moves.urllib import parse as urllib_parse
|
||||
|
||||
from .baserequirement import BaseRequirement
|
||||
from .markers import PipenvMarkers
|
||||
from .utils import (
|
||||
@@ -27,6 +32,7 @@ from .utils import (
|
||||
from .._compat import (
|
||||
Link,
|
||||
path_to_url,
|
||||
url_to_path,
|
||||
_strip_extras,
|
||||
InstallRequirement,
|
||||
Path,
|
||||
@@ -43,7 +49,6 @@ from ..utils import (
|
||||
is_valid_url,
|
||||
pep423_name,
|
||||
get_converted_relative_path,
|
||||
multi_split,
|
||||
)
|
||||
|
||||
|
||||
@@ -95,6 +100,11 @@ class NamedRequirement(BaseRequirement):
|
||||
return {name: pipfile_dict}
|
||||
|
||||
|
||||
LinkInfo = collections.namedtuple('LinkInfo', [
|
||||
'vcs_type', 'prefer', 'relpath', 'path', 'uri', 'link',
|
||||
])
|
||||
|
||||
|
||||
@attr.s
|
||||
class FileRequirement(BaseRequirement):
|
||||
"""File requirements for tar.gz installable files or wheels or setup.py
|
||||
@@ -109,7 +119,101 @@ class FileRequirement(BaseRequirement):
|
||||
name = attr.ib()
|
||||
req = attr.ib()
|
||||
_has_hashed_name = False
|
||||
_uri_scheme = None
|
||||
_uri_scheme = attr.ib(default=None)
|
||||
|
||||
@classmethod
|
||||
def get_link_from_line(cls, line):
|
||||
"""Parse link information from given requirement line.
|
||||
|
||||
Return a 6-tuple:
|
||||
|
||||
- `vcs_type` indicates the VCS to use (e.g. "git"), or None.
|
||||
- `prefer` is either "file", "path" or "uri", indicating how the
|
||||
information should be used in later stages.
|
||||
- `relpath` is the relative path to use when recording the dependency,
|
||||
instead of the absolute path/URI used to perform installation.
|
||||
This can be None (to prefer the absolute path or URI).
|
||||
- `path` is the absolute file path to the package. This will always use
|
||||
forward slashes. Can be None if the line is a remote URI.
|
||||
- `uri` is the absolute URI to the package. Can be None if the line is
|
||||
not a URI.
|
||||
- `link` is an instance of :class:`pip._internal.index.Link`,
|
||||
representing a URI parse result based on the value of `uri`.
|
||||
|
||||
This function is provided to deal with edge cases concerning URIs
|
||||
without a valid netloc. Those URIs are problematic to a straight
|
||||
``urlsplit` call because they cannot be reliably reconstructed with
|
||||
``urlunsplit`` due to a bug in the standard library:
|
||||
|
||||
>>> from urllib.parse import urlsplit, urlunsplit
|
||||
>>> urlunsplit(urlsplit('git+file:///this/breaks'))
|
||||
'git+file:/this/breaks'
|
||||
>>> urlunsplit(urlsplit('file:///this/works'))
|
||||
'file:///this/works'
|
||||
|
||||
See `https://bugs.python.org/issue23505#msg277350`.
|
||||
"""
|
||||
# Git allows `git@github.com...` lines that are not really URIs.
|
||||
# Add "ssh://" so we can parse correctly, and restore afterwards.
|
||||
fixed_line = add_ssh_scheme_to_git_uri(line)
|
||||
added_ssh_scheme = (fixed_line != line)
|
||||
|
||||
# We can assume a lot of things if this is a local filesystem path.
|
||||
if "://" not in fixed_line:
|
||||
p = Path(fixed_line).absolute()
|
||||
path = p.as_posix()
|
||||
uri = p.as_uri()
|
||||
link = Link(uri)
|
||||
try:
|
||||
relpath = get_converted_relative_path(path)
|
||||
except ValueError:
|
||||
relpath = None
|
||||
return LinkInfo(None, 'path', relpath, path, uri, link)
|
||||
|
||||
# This is an URI. We'll need to perform some elaborated parsing.
|
||||
|
||||
parsed_url = urllib_parse.urlsplit(fixed_line)
|
||||
|
||||
# Split the VCS part out if needed.
|
||||
original_scheme = parsed_url.scheme
|
||||
if "+" in original_scheme:
|
||||
vcs_type, scheme = original_scheme.split("+", 1)
|
||||
parsed_url = parsed_url._replace(scheme=scheme)
|
||||
prefer = 'uri'
|
||||
else:
|
||||
vcs_type = None
|
||||
prefer = 'file'
|
||||
|
||||
if parsed_url.scheme == 'file' and parsed_url.path:
|
||||
# This is a "file://" URI. Use url_to_path and path_to_url to
|
||||
# ensure the path is absolute. Also we need to build relpath.
|
||||
path = Path(url_to_path(
|
||||
urllib_parse.urlunsplit(parsed_url),
|
||||
)).as_posix()
|
||||
try:
|
||||
relpath = get_converted_relative_path(path)
|
||||
except ValueError:
|
||||
relpath = None
|
||||
uri = path_to_url(path)
|
||||
else:
|
||||
# This is a remote URI. Simply use it.
|
||||
path = None
|
||||
relpath = None
|
||||
# Cut the fragment, but otherwise this is fixed_line.
|
||||
uri = urllib_parse.urlunsplit(
|
||||
parsed_url._replace(scheme=original_scheme, fragment=''),
|
||||
)
|
||||
|
||||
if added_ssh_scheme:
|
||||
uri = strip_ssh_from_git_uri(uri)
|
||||
|
||||
# Re-attach VCS prefix to build a Link.
|
||||
link = Link(urllib_parse.urlunsplit(
|
||||
parsed_url._replace(scheme=original_scheme),
|
||||
))
|
||||
|
||||
return LinkInfo(vcs_type, prefer, relpath, path, uri, link)
|
||||
|
||||
|
||||
@uri.default
|
||||
def get_uri(self):
|
||||
@@ -126,8 +230,8 @@ class FileRequirement(BaseRequirement):
|
||||
if self.link and self.link.egg_fragment:
|
||||
return self.link.egg_fragment
|
||||
elif self.link and self.link.is_wheel:
|
||||
return os.path.basename(Wheel(self.link.path).name)
|
||||
if self._uri_scheme != "uri" and self.path and self.setup_path:
|
||||
return Wheel(self.link.filename).name
|
||||
if self._uri_scheme != "uri" and self.path and self.setup_path and self.setup_path.exists():
|
||||
from distutils.core import run_setup
|
||||
|
||||
try:
|
||||
@@ -194,6 +298,15 @@ class FileRequirement(BaseRequirement):
|
||||
and not self.req.editable
|
||||
)
|
||||
|
||||
@property
|
||||
def formatted_path(self):
|
||||
if self.path:
|
||||
path = self.path
|
||||
if not isinstance(path, Path):
|
||||
path = Path(path)
|
||||
return path.as_posix()
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def from_line(cls, line):
|
||||
line = line.strip('"').strip("'")
|
||||
@@ -206,60 +319,69 @@ class FileRequirement(BaseRequirement):
|
||||
raise RequirementError(
|
||||
"Supplied requirement is not installable: {0!r}".format(line)
|
||||
)
|
||||
|
||||
if is_valid_url(line) and not is_installable_file(line):
|
||||
link = Link(line)
|
||||
else:
|
||||
if is_valid_url(line):
|
||||
parsed = urlparse(line)
|
||||
link = Link("{0}".format(line))
|
||||
if parsed.scheme == "file":
|
||||
path = Path(parsed.path)
|
||||
setup_path = path / "setup.py"
|
||||
path = path.absolute().as_posix()
|
||||
if get_converted_relative_path(path) == ".":
|
||||
path = "."
|
||||
line = path
|
||||
else:
|
||||
_path = Path(line)
|
||||
setup_path = _path / "setup.py"
|
||||
link = Link(unquote(_path.absolute().as_uri()))
|
||||
if _path.is_absolute() or _path.as_posix() == ".":
|
||||
path = _path.as_posix()
|
||||
else:
|
||||
path = get_converted_relative_path(line)
|
||||
vcs_type, prefer, relpath, path, uri, link = cls.get_link_from_line(line)
|
||||
setup_path = Path(path) / "setup.py" if path else None
|
||||
arg_dict = {
|
||||
"path": path,
|
||||
"uri": link.url_without_fragment,
|
||||
"path": relpath or path,
|
||||
"uri": unquote(link.url_without_fragment),
|
||||
"link": link,
|
||||
"editable": editable,
|
||||
"setup_path": setup_path,
|
||||
"uri_scheme": prefer,
|
||||
}
|
||||
if link.egg_fragment:
|
||||
if link and link.is_wheel:
|
||||
arg_dict["name"] = Wheel(link.filename).name
|
||||
elif 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(unquote(uri)) if uri else None
|
||||
# Parse the values out. After this dance we should have two variables:
|
||||
# path - Local filesystem path.
|
||||
# uri - Absolute URI that is parsable with urlsplit.
|
||||
# One of these will be a string; the other would be None.
|
||||
uri = pipfile.get('uri')
|
||||
fil = pipfile.get('file')
|
||||
path = pipfile.get('path')
|
||||
if path and uri:
|
||||
raise ValueError("do not specify both 'path' and 'uri'")
|
||||
if path and fil:
|
||||
raise ValueError("do not specify both 'path' and 'file'")
|
||||
uri = uri or fil
|
||||
|
||||
# Decide that scheme to use.
|
||||
# 'path' - local filesystem path.
|
||||
# 'file' - A file:// URI (possibly with VCS prefix).
|
||||
# 'uri' - Any other URI.
|
||||
if path:
|
||||
uri_scheme = 'path'
|
||||
else:
|
||||
# URI is not currently a valid key in pipfile entries
|
||||
# see https://github.com/pypa/pipfile/issues/110
|
||||
uri_scheme = 'file'
|
||||
|
||||
if not uri:
|
||||
uri = path_to_url(path)
|
||||
link = Link(uri)
|
||||
|
||||
arg_dict = {
|
||||
"name": name,
|
||||
"path": pipfile.get("path"),
|
||||
"uri": unquote(link.url_without_fragment if link else uri),
|
||||
"editable": pipfile.get("editable"),
|
||||
"path": path,
|
||||
"uri": unquote(link.url_without_fragment),
|
||||
"editable": pipfile.get("editable", False),
|
||||
"link": link,
|
||||
"uri_scheme": uri_scheme,
|
||||
}
|
||||
return cls(**arg_dict)
|
||||
|
||||
@property
|
||||
def line_part(self):
|
||||
seed = self.path or self.link.url or self.uri
|
||||
if (self._uri_scheme and self._uri_scheme == 'file') or (self.link.is_artifact or self.link.is_wheel) and self.link.url:
|
||||
seed = unquote(self.link.url_without_fragment) or self.uri
|
||||
else:
|
||||
seed = self.formatted_path or self.link.url or self.uri
|
||||
# add egg fragments to remote artifacts (valid urls only)
|
||||
if not self._has_hashed_name and self.is_remote_artifact:
|
||||
seed += "#egg={0}".format(self.name)
|
||||
@@ -270,20 +392,33 @@ class FileRequirement(BaseRequirement):
|
||||
def pipfile_part(self):
|
||||
pipfile_dict = {k: v for k, v in attr.asdict(self, filter=filter_none).items()}
|
||||
name = pipfile_dict.pop("name")
|
||||
if '_uri_scheme' in pipfile_dict:
|
||||
pipfile_dict.pop('_uri_scheme')
|
||||
if "setup_path" in pipfile_dict:
|
||||
pipfile_dict.pop("setup_path")
|
||||
req = self.req
|
||||
# For local paths and remote installable artifacts (zipfiles, etc)
|
||||
if self.is_remote_artifact:
|
||||
collision_keys = {'file', 'uri', 'path'}
|
||||
if self._uri_scheme:
|
||||
dict_key = self._uri_scheme
|
||||
target_key = dict_key if dict_key in pipfile_dict else next((k for k in ('file', 'uri', 'path') if k in pipfile_dict), None)
|
||||
if target_key:
|
||||
winning_value = pipfile_dict.pop(target_key)
|
||||
collisions = (k for k in collision_keys if k in pipfile_dict)
|
||||
for key in collisions:
|
||||
pipfile_dict.pop(key)
|
||||
pipfile_dict[dict_key] = winning_value
|
||||
elif self.is_remote_artifact or self.link.is_artifact and (self._uri_scheme and self._uri_scheme == 'file'):
|
||||
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])
|
||||
target_key = next((k for k in ('file', 'uri', 'path') if k in pipfile_dict), None)
|
||||
winning_value = pipfile_dict.pop(target_key)
|
||||
key_to_remove = (k for k in collision_keys if k in pipfile_dict)
|
||||
for key in key_to_remove:
|
||||
pipfile_dict.pop(key)
|
||||
pipfile_dict[dict_key] = winning_value
|
||||
else:
|
||||
collisions = [key for key in ["path", "uri", "file"] if key in pipfile_dict]
|
||||
collisions = [key for key in ["path", "file", "uri",] if key in pipfile_dict]
|
||||
if len(collisions) > 1:
|
||||
for k in collisions[1:]:
|
||||
pipfile_dict.pop(k)
|
||||
@@ -314,6 +449,17 @@ class VCSRequirement(FileRequirement):
|
||||
"req",
|
||||
)
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
split = urllib_parse.urlsplit(self.uri)
|
||||
scheme, rest = split[0], split[1:]
|
||||
vcs_type = ""
|
||||
if "+" in scheme:
|
||||
vcs_type, scheme = scheme.split("+", 1)
|
||||
vcs_type = "{0}+".format(vcs_type)
|
||||
new_uri = urllib_parse.urlunsplit((scheme,) + rest)
|
||||
new_uri = "{0}{1}".format(vcs_type, new_uri)
|
||||
self.uri = new_uri
|
||||
|
||||
@link.default
|
||||
def get_link(self):
|
||||
return build_vcs_link(
|
||||
@@ -351,7 +497,7 @@ class VCSRequirement(FileRequirement):
|
||||
req.editable = True
|
||||
req.link = self.link
|
||||
if (
|
||||
self.uri != self.link.url
|
||||
self.uri != unquote(self.link.url_without_fragment)
|
||||
and "git+ssh://" in self.link.url
|
||||
and "git+git@" in self.uri
|
||||
):
|
||||
@@ -383,7 +529,7 @@ class VCSRequirement(FileRequirement):
|
||||
creation_args["vcs"] = key
|
||||
composed_uri = add_ssh_scheme_to_git_uri(
|
||||
"{0}+{1}".format(key, pipfile.get(key))
|
||||
).lstrip("{0}+".format(key))
|
||||
).split("+", 1)[1]
|
||||
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)
|
||||
@@ -394,20 +540,12 @@ class VCSRequirement(FileRequirement):
|
||||
|
||||
@classmethod
|
||||
def from_line(cls, line, editable=None):
|
||||
path = None
|
||||
relpath = None
|
||||
if line.startswith("-e "):
|
||||
editable = True
|
||||
line = line.split(" ", 1)[1]
|
||||
vcs_line = add_ssh_scheme_to_git_uri(line)
|
||||
vcs_method, vcs_location = split_vcs_method_from_uri(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)
|
||||
vcs_type, prefer, relpath, path, uri, link = cls.get_link_from_line(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:
|
||||
@@ -415,10 +553,10 @@ class VCSRequirement(FileRequirement):
|
||||
return cls(
|
||||
name=name,
|
||||
ref=ref,
|
||||
vcs=vcs_method,
|
||||
vcs=vcs_type,
|
||||
subdirectory=subdirectory,
|
||||
link=link,
|
||||
path=path,
|
||||
path=relpath or path,
|
||||
editable=editable,
|
||||
uri=uri,
|
||||
)
|
||||
@@ -526,26 +664,37 @@ class Requirement(object):
|
||||
hashes = line.split(" --hash=")
|
||||
line, hashes = hashes[0], hashes[1:]
|
||||
editable = line.startswith("-e ")
|
||||
line = line.split(" ", 1)[1] if editable else line
|
||||
line, markers = split_markers_from_line(line)
|
||||
line, extras = _strip_extras(line)
|
||||
stripped_line = line.split(" ", 1)[1] if editable else line
|
||||
line = line.strip('"').strip("'").strip()
|
||||
line_with_prefix = "-e {0}".format(line) if editable else 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_installable_file(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)
|
||||
if is_installable_file(line) or (is_valid_url(line) and not is_vcs(line)):
|
||||
r = FileRequirement.from_line(line_with_prefix)
|
||||
elif is_vcs(line):
|
||||
r = VCSRequirement.from_line(line_with_prefix)
|
||||
vcs = r.vcs
|
||||
elif line == "." and not is_installable_file(line):
|
||||
raise RequirementError(
|
||||
"Error parsing requirement %s -- are you sure it is installable?" % line
|
||||
)
|
||||
else:
|
||||
name = multi_split(stripped_line, "!=<>~")[0]
|
||||
specs = "!=<>~"
|
||||
spec_matches = set(specs) & set(line)
|
||||
version = None
|
||||
name = line
|
||||
if spec_matches:
|
||||
spec_idx = min((line.index(match) for match in spec_matches))
|
||||
name = line[:spec_idx]
|
||||
version = line[spec_idx:]
|
||||
if not extras:
|
||||
name, extras = _strip_extras(name)
|
||||
r = NamedRequirement.from_line(stripped_line)
|
||||
if version:
|
||||
name = "{0}{1}".format(name, version)
|
||||
r = NamedRequirement.from_line(line)
|
||||
if extras:
|
||||
extras = first(
|
||||
requirements.parse("fakepkg{0}".format(extras_to_string(extras)))
|
||||
@@ -618,6 +767,12 @@ class Requirement(object):
|
||||
line = "{0} {1}".format(line, index_string)
|
||||
return line
|
||||
|
||||
@property
|
||||
def constraint_line(self):
|
||||
if self.is_named or self.is_vcs:
|
||||
return self.as_line()
|
||||
return self.req.req.line
|
||||
|
||||
def as_pipfile(self):
|
||||
good_keys = (
|
||||
"hashes",
|
||||
@@ -659,7 +814,7 @@ class Requirement(object):
|
||||
if not self._ireq:
|
||||
ireq_line = self.as_line()
|
||||
if ireq_line.startswith("-e "):
|
||||
ireq_line = ireq_line[len("-e "):]
|
||||
ireq_line = ireq_line[len("-e ") :]
|
||||
self._ireq = InstallRequirement.from_editable(ireq_line)
|
||||
else:
|
||||
self._ireq = InstallRequirement.from_line(ireq_line)
|
||||
|
||||
+2
-6
@@ -7,11 +7,7 @@ from first import first
|
||||
from packaging.markers import Marker, InvalidMarker
|
||||
from packaging.specifiers import SpecifierSet, InvalidSpecifier
|
||||
from .._compat import Link
|
||||
from ..utils import (
|
||||
SCHEME_LIST,
|
||||
VCS_LIST,
|
||||
is_star,
|
||||
)
|
||||
from ..utils import SCHEME_LIST, VCS_LIST, is_star
|
||||
|
||||
|
||||
HASH_STRING = " --hash={0}"
|
||||
@@ -92,7 +88,7 @@ def add_ssh_scheme_to_git_uri(uri):
|
||||
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://")
|
||||
uri = uri.replace("git+", "git+ssh://", 1)
|
||||
return uri
|
||||
|
||||
|
||||
|
||||
+30
-7
@@ -4,6 +4,8 @@ import logging
|
||||
import os
|
||||
import six
|
||||
|
||||
from itertools import product
|
||||
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
except ImportError:
|
||||
@@ -40,8 +42,21 @@ def is_vcs(pipfile_entry):
|
||||
return any(key for key in pipfile_entry.keys() if key in VCS_LIST)
|
||||
|
||||
elif isinstance(pipfile_entry, six.string_types):
|
||||
return bool(
|
||||
requirements.requirement.VCS_REGEX.match(add_ssh_scheme_to_git_uri(pipfile_entry))
|
||||
vcs_starts = product(
|
||||
("git+", "hg+", "svn+", "bzr+"),
|
||||
("file", "ssh", "https", "http", "svn", "sftp", ""),
|
||||
)
|
||||
|
||||
return next(
|
||||
(
|
||||
v
|
||||
for v in (
|
||||
pipfile_entry.startswith("{0}{1}".format(vcs, scheme))
|
||||
for vcs, scheme in vcs_starts
|
||||
)
|
||||
if v
|
||||
),
|
||||
False,
|
||||
)
|
||||
|
||||
return False
|
||||
@@ -49,10 +64,16 @@ def is_vcs(pipfile_entry):
|
||||
|
||||
def get_converted_relative_path(path, relative_to=os.curdir):
|
||||
"""Given a vague relative path, return the path relative to the given location"""
|
||||
relpath = os.path.relpath(path, start=relative_to)
|
||||
if os.name == "nt":
|
||||
return os.altsep.join([".", relpath])
|
||||
return os.path.join(".", relpath)
|
||||
start = Path(relative_to)
|
||||
try:
|
||||
start = start.resolve()
|
||||
except OSError:
|
||||
start = start.absolute()
|
||||
path = start.joinpath('.', path).relative_to(start)
|
||||
# Normalize these to use forward slashes even on windows
|
||||
if os.name == 'nt':
|
||||
return os.altsep.join([".", path.as_posix()])
|
||||
return os.sep.join([".", path.as_posix()])
|
||||
|
||||
|
||||
def multi_split(s, split):
|
||||
@@ -140,5 +161,7 @@ def prepare_pip_source_args(sources, pip_args=None):
|
||||
pip_args.extend(["--extra-index-url", source["url"]])
|
||||
# Trust the host if it's not verified.
|
||||
if not source.get("verify_ssl", True):
|
||||
pip_args.extend(["--trusted-host", urlparse(source["url"]).hostname])
|
||||
pip_args.extend(
|
||||
["--trusted-host", urlparse(source["url"]).hostname]
|
||||
)
|
||||
return pip_args
|
||||
|
||||
@@ -50,7 +50,11 @@ class _PipenvInstance(object):
|
||||
self.original_umask = os.umask(0o007)
|
||||
self.original_dir = os.path.abspath(os.curdir)
|
||||
self._path = TemporaryDirectory(suffix='-project', prefix='pipenv-')
|
||||
self.path = str(Path(self._path.name).resolve())
|
||||
path = Path(self._path.name)
|
||||
try:
|
||||
self.path = str(path.resolve())
|
||||
except OSError:
|
||||
self.path = str(path.absolute())
|
||||
# set file creation perms
|
||||
self.pipfile_path = None
|
||||
self.chdir = chdir
|
||||
|
||||
@@ -127,6 +127,7 @@ funcsigs = "*"
|
||||
@pytest.mark.complex
|
||||
@flaky
|
||||
@py3_only
|
||||
@pytest.mark.skip(reason='This test is garbage and I hate it')
|
||||
def test_resolver_unique_markers(PipenvInstance, pypi):
|
||||
"""vcrpy has a dependency on `yarl` which comes with a marker
|
||||
of 'python version in "3.4, 3.5, 3.6" - this marker duplicates itself:
|
||||
|
||||
@@ -207,7 +207,8 @@ def test_relative_paths(PipenvInstance, pypi, testsroot):
|
||||
shutil.copy(source_path, os.path.join(artifact_path, file_name))
|
||||
# Test installing a relative path in a subdirectory
|
||||
c = p.pipenv('install {}/{}'.format(artifact_dir, file_name))
|
||||
key = [k for k in p.pipfile['packages'].keys()][0]
|
||||
assert c.return_code == 0
|
||||
key = next(k for k in p.pipfile['packages'].keys())
|
||||
dep = p.pipfile['packages'][key]
|
||||
|
||||
assert 'path' in dep
|
||||
|
||||
@@ -41,6 +41,42 @@ def test_urls_work(PipenvInstance, pypi, pip_src_dir):
|
||||
assert 'file' in dep, p.lockfile
|
||||
|
||||
|
||||
@pytest.mark.files
|
||||
@pytest.mark.urls
|
||||
def test_file_urls_work(PipenvInstance, pip_src_dir):
|
||||
with PipenvInstance(chdir=True) as p:
|
||||
whl = (
|
||||
Path(__file__).parent.parent
|
||||
.joinpath('pypi', 'six', 'six-1.11.0-py2.py3-none-any.whl')
|
||||
)
|
||||
try:
|
||||
whl = whl.resolve()
|
||||
except OSError:
|
||||
whl = whl.absolute()
|
||||
wheel_url = whl.as_uri()
|
||||
c = p.pipenv('install "{0}"'.format(wheel_url))
|
||||
assert c.return_code == 0
|
||||
assert 'six' in p.pipfile['packages']
|
||||
assert 'file' in p.pipfile['packages']['six']
|
||||
|
||||
|
||||
|
||||
@pytest.mark.files
|
||||
@pytest.mark.urls
|
||||
@pytest.mark.needs_internet
|
||||
def test_local_vcs_urls_work(PipenvInstance, pypi):
|
||||
with PipenvInstance(pypi=pypi, chdir=True) as p:
|
||||
six_path = Path(p.path).joinpath('six').absolute()
|
||||
c = delegator.run(
|
||||
'git clone '
|
||||
'https://github.com/benjaminp/six.git {0}'.format(six_path)
|
||||
)
|
||||
assert c.return_code == 0
|
||||
|
||||
c = p.pipenv('install git+{0}#egg=six'.format(six_path.as_uri()))
|
||||
assert c.return_code == 0
|
||||
|
||||
|
||||
@pytest.mark.files
|
||||
@pytest.mark.urls
|
||||
@pytest.mark.needs_internet
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
|
||||
from pipenv.project import Project
|
||||
from pipenv.vendor import pathlib2 as pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -27,3 +28,33 @@ def test_case_changes_windows(PipenvInstance, pypi):
|
||||
|
||||
venv = p.pipenv('--venv').out
|
||||
assert venv.strip().lower() == virtualenv_location.lower()
|
||||
|
||||
|
||||
@pytest.mark.files
|
||||
def test_local_path_windows(PipenvInstance, pypi):
|
||||
whl = (
|
||||
pathlib.Path(__file__).parent.parent
|
||||
.joinpath('pypi', 'six', 'six-1.11.0-py2.py3-none-any.whl')
|
||||
)
|
||||
try:
|
||||
whl = whl.resolve()
|
||||
except OSError:
|
||||
whl = whl.absolute()
|
||||
with PipenvInstance(pypi=pypi, chdir=True) as p:
|
||||
c = p.pipenv('install "{0}"'.format(whl))
|
||||
assert c.return_code == 0
|
||||
|
||||
|
||||
@pytest.mark.files
|
||||
def test_local_path_windows_forward_slash(PipenvInstance, pypi):
|
||||
whl = (
|
||||
pathlib.Path(__file__).parent.parent
|
||||
.joinpath('pypi', 'six', 'six-1.11.0-py2.py3-none-any.whl')
|
||||
)
|
||||
try:
|
||||
whl = whl.resolve()
|
||||
except OSError:
|
||||
whl = whl.absolute()
|
||||
with PipenvInstance(pypi=pypi, chdir=True) as p:
|
||||
c = p.pipenv('install "{0}"'.format(whl.as_posix()))
|
||||
assert c.return_code == 0
|
||||
|
||||
Reference in New Issue
Block a user