Merge pull request #2302 from pypa/bugfix/2260

Update requirementslib
This commit is contained in:
Dan Ryan
2018-06-11 12:59:50 -04:00
committed by GitHub
23 changed files with 1260 additions and 395 deletions
+1 -2
View File
@@ -20,7 +20,7 @@ except ImportError:
from pathlib2 import Path
from .cmdparse import Script
from .vendor.requirementslib.requirements import Requirement
from .vendor.requirementslib import Requirement
from .utils import (
atomic_open_for_write,
mkdir_p,
@@ -46,7 +46,6 @@ from .environments import (
PIPENV_PYTHON,
PIPENV_DEFAULT_PYTHON_VERSION,
)
from .vendor.first import first
def _normalized(p):
+5 -8
View File
@@ -248,10 +248,10 @@ def actually_resolve_deps(
dep, url = dep.split(' -i ')
req = Requirement.from_line(dep)
# req.as_line() is theoratically the same as dep, but is guarenteed to
# 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())
constraints.append(req.as_line(sources=None))
# extra_constraints = []
if url:
@@ -509,12 +509,9 @@ def convert_deps_to_pip(deps, project=None, r=True, include_index=False):
dependencies = []
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 = Requirement.from_pipfile(dep_name, indexes, dep)
new_dep = Requirement.from_pipfile(dep_name, dep)
req = new_dep.as_line(
project=project,
include_index=include_index
sources=indexes if include_index else None
).strip()
dependencies.append(req)
if not r:
@@ -1191,7 +1188,7 @@ def get_vcs_deps(
pipfile_name = vcs_uri_map[_vcs_match]["name"]
pipfile_rev = vcs_uri_map[_vcs_match]["ref"]
pipfile_req = Requirement.from_pipfile(pipfile_name, [], packages[pipfile_name])
pipfile_req = Requirement.from_pipfile(pipfile_name, packages[pipfile_name])
names = {pipfile_name}
backend = vcs_registry()._registry.get(pipfile_req.vcs)
# TODO: Why doesn't pip freeze list 'git+git://' formatted urls?
+4 -2
View File
@@ -1,4 +1,6 @@
# -*- coding=utf-8 -*-
__version__ = "0.0.7.dev0"
__version__ = "0.2.0"
from .requirements import Requirement
from .exceptions import RequirementError
from .models import Requirement, Lockfile, Pipfile
+21 -1
View File
@@ -1,6 +1,25 @@
# -*- coding=utf-8 -*-
# -*- coding=utf-8 -*-
import importlib
import six
# Use these imports as compatibility imports
try:
from pathlib import Path
except ImportError:
from pathlib2 import Path
try:
from urllib.parse import urlparse, unquote
except ImportError:
from urlparse import urlparse, unquote
if six.PY2:
class FileNotFoundError(IOError):
pass
else:
class FileNotFoundError(FileNotFoundError):
pass
def do_import(module_path, subimport=None, old_path=None):
@@ -38,3 +57,4 @@ 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")
+14
View File
@@ -0,0 +1,14 @@
# Taken from pip: https://github.com/pypa/pip/blob/95bcf8c5f6394298035a7332c441868f3b0169f4/src/pip/_vendor/Makefile
all: clean vendor
clean:
@# Delete vendored items
find . -maxdepth 1 -mindepth 1 -type d -exec rm -rf {} \;
vendor:
@# Install vendored libraries
pip install -t . -r vendor.txt
@# Cleanup .egg-info directories
rm -rf *.egg-info
rm -rf *.dist-info
+1
View File
@@ -0,0 +1 @@
# -*- coding=utf-8 -*-
+3
View File
@@ -0,0 +1,3 @@
This software is made available under the terms of *either* of the licenses
found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made
under the terms of *both* these licenses.
@@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
@@ -0,0 +1,23 @@
Copyright (c) Kenneth Reitz and individual contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,21 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
from __future__ import absolute_import, division, print_function
__all__ = [
"__title__", "__summary__", "__uri__", "__version__", "__author__",
"__email__", "__license__", "__copyright__",
]
__title__ = "pipfile"
__summary__ = ""
__uri__ = "https://github.com/pypa/pipfile"
__version__ = "0.0.2"
__author__ = "Kenneth Reitz and individual contributors"
__email__ = "me@kennethreitz.org"
__license__ = "BSD or Apache License, Version 2.0"
__copyright__ = "Copyright 2017 %s" % __author__
@@ -0,0 +1,11 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
from __future__ import absolute_import, division, print_function
from .__about__ import (
__author__, __copyright__, __email__, __license__, __summary__, __title__,
__uri__, __version__
)
from .api import load, Pipfile
+230
View File
@@ -0,0 +1,230 @@
import toml
import codecs
import json
import hashlib
import platform
import six
import sys
import os
DEFAULT_SOURCE = {
u'url': u'https://pypi.org/simple',
u'verify_ssl': True,
u'name': u'pypi',
}
def format_full_version(info):
version = '{0.major}.{0.minor}.{0.micro}'.format(info)
kind = info.releaselevel
if kind != 'final':
version += kind[0] + str(info.serial)
return version
def walk_up(bottom):
"""mimic os.walk, but walk 'up' instead of down the directory tree.
From: https://gist.github.com/zdavkeos/1098474
"""
bottom = os.path.realpath(bottom)
# get files in current dir
try:
names = os.listdir(bottom)
except Exception:
return
dirs, nondirs = [], []
for name in names:
if os.path.isdir(os.path.join(bottom, name)):
dirs.append(name)
else:
nondirs.append(name)
yield bottom, dirs, nondirs
new_path = os.path.realpath(os.path.join(bottom, '..'))
# see if we are at the top
if new_path == bottom:
return
for x in walk_up(new_path):
yield x
class PipfileParser(object):
def __init__(self, filename='Pipfile'):
self.filename = filename
self.sources = []
self.groups = {
'default': [],
'develop': []
}
self.group_stack = ['default']
self.requirements = []
def __repr__(self):
return '<PipfileParser path={0!r}'.format(self.filename)
def inject_environment_variables(self, d):
"""
Recursively injects environment variables into TOML values
"""
if not d:
return d
if isinstance(d, six.string_types):
return os.path.expandvars(d)
for k, v in d.items():
if isinstance(v, six.string_types):
d[k] = os.path.expandvars(v)
elif isinstance(v, dict):
d[k] = self.inject_environment_variables(v)
elif isinstance(v, list):
d[k] = [self.inject_environment_variables(e) for e in v]
return d
def parse(self, inject_env=True):
# Open the Pipfile.
with open(self.filename) as f:
content = f.read()
# Load the default configuration.
default_config = {
u'source': [DEFAULT_SOURCE],
u'packages': {},
u'requires': {},
u'dev-packages': {}
}
config = {}
config.update(default_config)
# Deserialize the TOML, and parse for Environment Variables
parsed = toml.loads(content)
if inject_env:
injected_toml = self.inject_environment_variables(parsed)
# Load the Pipfile's configuration.
config.update(injected_toml)
else:
config.update(parsed)
# Structure the data for output.
data = {
'_meta': {
'sources': config['source'],
'requires': config['requires']
},
}
# TODO: Validate given data here.
self.groups['default'] = config['packages']
self.groups['develop'] = config['dev-packages']
# Update the data structure with group information.
data.update(self.groups)
return data
class Pipfile(object):
def __init__(self, filename):
super(Pipfile, self).__init__()
self.filename = filename
self.data = None
@staticmethod
def find(max_depth=3):
"""Returns the path of a Pipfile in parent directories."""
i = 0
for c, d, f in walk_up(os.getcwd()):
i += 1
if i < max_depth:
if 'Pipfile':
p = os.path.join(c, 'Pipfile')
if os.path.isfile(p):
return p
raise RuntimeError('No Pipfile found!')
@classmethod
def load(klass, filename, inject_env=True):
"""Load a Pipfile from a given filename."""
p = PipfileParser(filename=filename)
pipfile = klass(filename=filename)
pipfile.data = p.parse(inject_env=inject_env)
return pipfile
@property
def hash(self):
"""Returns the SHA256 of the pipfile's data."""
content = json.dumps(self.data, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(content.encode("utf8")).hexdigest()
@property
def contents(self):
"""Returns the contents of the pipfile."""
with codecs.open(self.filename, 'r', 'utf-8') as f:
return f.read()
def lock(self):
"""Returns a JSON representation of the Pipfile."""
data = self.data
data['_meta']['hash'] = {"sha256": self.hash}
data['_meta']['pipfile-spec'] = 6
return json.dumps(data, indent=4, separators=(',', ': '))
def assert_requirements(self):
""""Asserts PEP 508 specifiers."""
# Support for 508's implementation_version.
if hasattr(sys, 'implementation'):
implementation_version = format_full_version(sys.implementation.version)
else:
implementation_version = "0"
# Default to cpython for 2.7.
if hasattr(sys, 'implementation'):
implementation_name = sys.implementation.name
else:
implementation_name = 'cpython'
lookup = {
'os_name': os.name,
'sys_platform': sys.platform,
'platform_machine': platform.machine(),
'platform_python_implementation': platform.python_implementation(),
'platform_release': platform.release(),
'platform_system': platform.system(),
'platform_version': platform.version(),
'python_version': platform.python_version()[:3],
'python_full_version': platform.python_version(),
'implementation_name': implementation_name,
'implementation_version': implementation_version
}
# Assert each specified requirement.
for marker, specifier in self.data['_meta']['requires'].items():
if marker in lookup:
try:
assert lookup[marker] == specifier
except AssertionError:
raise AssertionError('Specifier {!r} does not match {!r}.'.format(marker, specifier))
def load(pipfile_path=None, inject_env=True):
"""Loads a pipfile from a given path.
If none is provided, one will try to be found.
"""
if pipfile_path is None:
pipfile_path = Pipfile.find()
return Pipfile.load(filename=pipfile_path, inject_env=inject_env)
@@ -0,0 +1 @@
pipfile
+6
View File
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
class RequirementError(Exception):
pass
+10
View File
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
__all__ = ["Requirement", "Lockfile", "Pipfile", "RequirementError"]
from .requirements import Requirement
from .lockfile import Lockfile
from .pipfile import Pipfile
+30
View File
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import abc
import attr
import six
@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)]
+56
View File
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import attr
import json
from .requirements import Requirement
from .utils import (
optional_instance_of,
)
from .._compat import Path, FileNotFoundError
@attr.s
class Lockfile(object):
dev_requirements = attr.ib(default=list)
requirements = attr.ib(default=list)
path = attr.ib(default=None, validator=optional_instance_of(Path))
pipfile_hash = attr.ib(default=None)
@classmethod
def create(cls, project_path, lockfile_name="Pipfile.lock"):
"""Create a new lockfile instance
:param project_path: Path to the project root
:type project_path: str or :class:`~pathlib.Path`
:returns: List[:class:`~requirementslib.Requirement`] objects
"""
if not isinstance(project_path, Path):
project_path = Path(project_path)
lockfile_path = project_path / lockfile_name
requirements = []
dev_requirements = []
if not lockfile_path.exists():
raise FileNotFoundError("No such lockfile: %s" % lockfile_path)
lockfile = json.loads(lockfile_path.read_text(encoding="utf-8"))
for k in lockfile["develop"].keys():
dev_requirements.append(Requirement.from_pipfile(k, lockfile["develop"][k]))
for k in lockfile["default"].keys():
requirements.append(Requirement.from_pipfile(k, lockfile["default"][k]))
return cls(
path=lockfile_path,
requirements=requirements,
dev_requirements=dev_requirements,
)
def as_requirements(self, include_hashes=False, dev=False):
"""Returns a list of requirements in pip-style format"""
lines = []
section = self.dev_requirements if dev else self.requirements
for req in section:
r = req.as_line()
if not include_hashes:
r = r.split("--hash", 1)[0]
lines.append(r.strip())
return lines
+90
View File
@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
import attr
import six
from packaging.markers import Marker, InvalidMarker
from .baserequirement import BaseRequirement
from .utils import validate_markers, filter_none
from ..exceptions import RequirementError
@attr.s
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))
platform_machine = attr.ib(
default=None, validator=attr.validators.optional(validate_markers)
)
platform_python_implementation = attr.ib(
default=None, validator=attr.validators.optional(validate_markers)
)
platform_release = attr.ib(
default=None, validator=attr.validators.optional(validate_markers)
)
platform_system = attr.ib(
default=None, validator=attr.validators.optional(validate_markers)
)
platform_version = attr.ib(
default=None, validator=attr.validators.optional(validate_markers)
)
python_version = attr.ib(
default=None, validator=attr.validators.optional(validate_markers)
)
python_full_version = attr.ib(
default=None, validator=attr.validators.optional(validate_markers)
)
implementation_name = attr.ib(
default=None, validator=attr.validators.optional(validate_markers)
)
implementation_version = attr.ib(
default=None, validator=attr.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):
try:
marker = Marker(marker_string)
except InvalidMarker:
raise RequirementError("Invalid requirement: Invalid marker %r" % 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)
+191
View File
@@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
import attr
import contoml
import os
import toml
from .._vendor import pipfile
from .requirements import Requirement
from .utils import optional_instance_of, filter_none
from .._compat import Path, FileNotFoundError
from ..exceptions import RequirementError
@attr.s
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)
)
#: human name to refer to this source (can be referenced in packages or dev-packages)
name = attr.ib(default="")
def get_dict(self):
return attr.asdict(self)
@property
def expanded(self):
source_dict = attr.asdict(self).copy()
source_dict['url'] = os.path.expandvars(source_dict.get('url'))
return source_dict
@attr.s
class Section(object):
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
requirements = attr.ib(default=list)
def get_dict(self):
_dict = {}
for req in self.requirements:
_dict.update(req.as_pipfile())
return {self.name: _dict}
@property
def vcs_requirements(self):
return [req for req in self.requirements if req.is_vcs]
@property
def editable_requirements(self):
return [req for req in self.requirements if req.editable]
@attr.s
class RequiresSection(object):
python_version = attr.ib(default=None)
python_full_version = attr.ib(default=None)
def get_dict(self):
requires = attr.asdict(self, filter=filter_none)
if not requires:
return {}
return {'requires': requires}
@attr.s
class PipenvSection(object):
allow_prereleases = attr.ib(default=False)
def get_dict(self):
if self.allow_prereleases:
return {'pipenv': attr.asdict(self)}
return {}
@attr.s
class Pipfile(object):
#: Path to the pipfile
path = attr.ib(default=None, converter=Path, validator=optional_instance_of(Path))
#: Sources listed in the pipfile
sources = attr.ib(default=attr.Factory(list))
#: Sections contained by the pipfile
sections = attr.ib(default=attr.Factory(list))
#: Scripts found in the pipfile
scripts = attr.ib(default=attr.Factory(dict))
#: This section stores information about what python version is required
requires = attr.ib(default=attr.Factory(RequiresSection))
#: This section stores information about pipenv such as prerelease requirements
pipenv = attr.ib(default=attr.Factory(PipenvSection))
#: This is the sha256 hash of the pipfile (without environment interpolation)
pipfile_hash = attr.ib()
@pipfile_hash.default
def get_hash(self):
p = pipfile.load(self.path.as_posix(), inject_env=False)
return p.hash
@property
def requires_python(self):
return self.requires.requires_python
@property
def allow_prereleases(self):
return self.pipenv.allow_prereleases
def get_sources(self):
"""Return a dictionary with a list of dictionaries of pipfile sources"""
_dict = {}
for src in self.sources:
_dict.update(src.get_dict())
return {'source': _dict} if _dict else {}
def get_sections(self):
"""Return a dictionary with both pipfile sections and requirements"""
_dict = {}
for section in self.sections:
_dict.update(section.get_dict())
return _dict
def get_pipenv(self):
pipenv_dict = self.pipenv.get_dict()
if pipenv_dict:
return pipenv_dict
def get_requires(self):
req_dict = self.requires.get_dict()
return req_dict if req_dict else {}
def get_dict(self):
_dict = attr.asdict(self, recurse=False)
for k in ['path', 'pipfile_hash', 'sources', 'sections', 'requires', 'pipenv']:
if k in _dict:
_dict.pop(k)
return _dict
def dump(self, to_dict=False):
"""Dumps the pipfile to a toml string
"""
_dict = self.get_sources()
_dict.update(self.get_sections())
_dict.update(self.get_dict())
_dict.update(self.get_pipenv())
_dict.update(self.get_requires())
if to_dict:
return _dict
return contoml.dumps(_dict)
@classmethod
def load(cls, path):
if not isinstance(path, Path):
path = Path(path)
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', {})
creation_dict = {
'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)
if pipenv:
creation_dict['pipenv'] = PipenvSection(**pipenv)
return cls(**creation_dict)
@staticmethod
def get_section(pf_dict, section):
"""Get section objects from a pipfile dictionary
:param pf_dict: A toml loaded pipfile dictionary
:type pf_dict: dict
:returns: Section objects
"""
sect = pf_dict.get(section)
requirements = []
if section not in Section.ALLOWED_NAMES:
raise ValueError("Not a valid pipfile section name: %s" % section)
for name, pf_entry in sect.items():
requirements.append(Requirement.from_pipfile(name, pf_entry))
return Section(name=section, requirements=requirements)
@@ -1,19 +1,42 @@
# -*- coding=utf-8 -*-
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import abc
import sys
import attr
import hashlib
import os
import requirements
import six
from attr import attrs, attrib, Factory, validators
import attr
from ._compat import Link, path_to_url, _strip_extras, InstallRequirement, Wheel
from distlib.markers import Evaluator
from packaging.markers import Marker, InvalidMarker
from packaging.specifiers import SpecifierSet, InvalidSpecifier
from first import first
from pkg_resources import RequirementParseError
from .baserequirement import BaseRequirement
from .markers import PipenvMarkers
from .utils import (
SCHEME_LIST,
HASH_STRING,
extras_to_string,
get_version,
specs_to_string,
validate_specifiers,
validate_path,
validate_vcs,
build_vcs_link,
add_ssh_scheme_to_git_uri,
strip_ssh_from_git_uri,
split_vcs_method_from_uri,
filter_none,
optional_instance_of,
split_markers_from_line,
)
from .._compat import (
Link,
path_to_url,
_strip_extras,
InstallRequirement,
Path,
urlparse,
unquote,
Wheel,
FileNotFoundError,
)
from ..exceptions import RequirementError
from ..utils import (
VCS_LIST,
is_installable_file,
is_vcs,
@@ -21,232 +44,31 @@ from .utils import (
pep423_name,
get_converted_relative_path,
multi_split,
is_star,
)
from first import first
try:
from pathlib import Path
except ImportError:
from pathlib2 import Path
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
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
@attr.s
class NamedRequirement(BaseRequirement):
name = attrib()
version = attrib(validator=validators.optional(_validate_specifiers))
req = attrib()
name = attr.ib()
version = attr.ib(validator=attr.validators.optional(validate_specifiers))
req = attr.ib()
@req.default
def get_requirement(self):
return first(requirements.parse("{0}{1}".format(self.name, self.version)))
try:
req = first(requirements.parse("{0}{1}".format(self.name, self.version)))
except RequirementParseError:
raise RequirementError(
"Error parsing requirement: %s%s" % (self.name, self.version)
)
return req
@classmethod
def from_line(cls, line):
req = first(requirements.parse(line))
specifiers = None
if req.specifier:
specifiers = _specs_to_string(req.specs)
specifiers = specs_to_string(req.specs)
return cls(name=req.name, version=specifiers, req=req)
@classmethod
@@ -255,7 +77,7 @@ class NamedRequirement(BaseRequirement):
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)
version = get_version(pipfile)
creation_args["version"] = version
creation_args["req"] = first(requirements.parse("{0}{1}".format(name, version)))
return cls(**creation_args)
@@ -266,24 +88,26 @@ class NamedRequirement(BaseRequirement):
@property
def pipfile_part(self):
pipfile_dict = attr.asdict(self, filter=_filter_none).copy()
pipfile_dict = attr.asdict(self, filter=filter_none).copy()
if "version" not in pipfile_dict:
pipfile_dict["version"] = "*"
name = pipfile_dict.pop("name")
return {name: pipfile_dict}
@attrs
@attr.s
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))
setup_path = attr.ib(default=None)
path = attr.ib(default=None, validator=attr.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()
editable = attr.ib(default=None)
uri = attr.ib()
link = attr.ib()
name = attr.ib()
req = attr.ib()
_has_hashed_name = False
_uri_scheme = None
@@ -298,17 +122,50 @@ class FileRequirement(BaseRequirement):
loc = self.path or self.uri
if loc:
self._uri_scheme = "path" if self.path else "uri"
name = None
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:
from distutils.core import run_setup
try:
dist = run_setup(self.setup_path.as_posix(), stop_after="init")
name = dist.get_name()
except (FileNotFoundError, IOError) as e:
dist = None
except (NameError, RuntimeError) as e:
from .._compat import InstallRequirement, make_abstract_dist
try:
if not isinstance(Path, self.path):
_path = Path(self.path)
else:
_path = self.path
if self.editable:
_ireq = InstallRequirement.from_editable(_path.as_uri())
else:
_ireq = InstallRequirement.from_line(_path.as_posix())
dist = make_abstract_dist(_ireq).get_dist()
name = dist.project_name
except (TypeError, ValueError, AttributeError) as e:
dist = None
hashed_loc = hashlib.sha256(loc.encode("utf-8")).hexdigest()
hash_fragment = hashed_loc[-7:]
self._has_hashed_name = True
return hash_fragment
hashed_name = hashed_loc[-7:]
if not name or name == "UNKNOWN":
self._has_hashed_name = True
name = hashed_name
if self.link and not self._has_hashed_name:
self.link = Link("{0}#egg={1}".format(self.link.url, name))
return name
@link.default
def get_link(self):
target = "{0}#egg={1}".format(self.uri, self.name)
target = "{0}".format(self.uri)
if hasattr(self, "name"):
target = "{0}#egg={1}".format(target, self.name)
link = Link(target)
if link.is_wheel and self._has_hashed_name:
self.name = os.path.basename(Wheel(link.path).name)
return link
@req.default
@@ -328,21 +185,25 @@ class FileRequirement(BaseRequirement):
@property
def is_remote_artifact(self):
return any(
self.link.scheme.startswith(scheme)
for scheme in ("http", "https", "ftp", "ftps", "uri")
) and (
self.link.is_artifact or self.link.is_wheel
) and not self.req.editable
return (
any(
self.link.scheme.startswith(scheme)
for scheme in ("http", "https", "ftp", "ftps", "uri")
)
and (self.link.is_artifact or self.link.is_wheel)
and not self.req.editable
)
@classmethod
def from_line(cls, line):
line = line.strip('"').strip("'")
link = None
path = None
editable = line.startswith("-e ")
line = line.split(" ", 1)[1] if editable else line
setup_path = None
if not any([is_installable_file(line), is_valid_url(line)]):
raise ValueError(
raise RequirementError(
"Supplied requirement is not installable: {0!r}".format(line)
)
@@ -351,15 +212,18 @@ class FileRequirement(BaseRequirement):
else:
if is_valid_url(line):
parsed = urlparse(line)
link = Link('{0}'.format(line))
link = Link("{0}".format(line))
if parsed.scheme == "file":
path = Path(parsed.path).absolute().as_posix()
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)
link = Link(_path.absolute().as_uri())
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:
@@ -369,6 +233,7 @@ class FileRequirement(BaseRequirement):
"uri": link.url_without_fragment,
"link": link,
"editable": editable,
"setup_path": setup_path,
}
if link.egg_fragment:
arg_dict["name"] = link.egg_fragment
@@ -382,11 +247,11 @@ class FileRequirement(BaseRequirement):
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
link = Link(unquote(uri)) if uri else None
arg_dict = {
"name": name,
"path": pipfile.get("path"),
"uri": link.url_without_fragment,
"uri": unquote(link.url_without_fragment if link else uri),
"editable": pipfile.get("editable"),
"link": link,
}
@@ -403,8 +268,10 @@ class FileRequirement(BaseRequirement):
@property
def pipfile_part(self):
pipfile_dict = {k: v for k, v in attr.asdict(self, filter=_filter_none).items()}
pipfile_dict = {k: v for k, v in attr.asdict(self, filter=filter_none).items()}
name = pipfile_dict.pop("name")
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:
@@ -419,31 +286,39 @@ class FileRequirement(BaseRequirement):
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)
pipfile_dict.pop(k)
return {name: pipfile_dict}
@attrs
@attr.s
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)
editable = attr.ib(default=None)
uri = attr.ib(default=None)
path = attr.ib(default=None, validator=attr.validators.optional(validate_path))
vcs = attr.ib(validator=attr.validators.optional(validate_vcs), default=None)
# : vcs reference name (branch / commit / tag)
ref = attrib(default=None)
subdirectory = attrib(default=None)
name = attrib()
link = attrib()
req = attrib()
ref = attr.ib(default=None)
subdirectory = attr.ib(default=None)
name = attr.ib()
link = attr.ib()
req = attr.ib()
_INCLUDE_FIELDS = (
"editable", "uri", "path", "vcs", "ref", "subdirectory", "name", "link", "req"
"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),
add_ssh_scheme_to_git_uri(self.uri),
name=self.name,
ref=self.ref,
subdirectory=self.subdirectory,
@@ -451,7 +326,11 @@ class VCSRequirement(FileRequirement):
@name.default
def get_name(self):
return self.link.egg_fragment or self.req.name if self.req else ""
return (
self.link.egg_fragment or self.req.name
if self.req
else super(VCSRequirement, self).get_name()
)
@property
def vcs_uri(self):
@@ -476,8 +355,8 @@ class VCSRequirement(FileRequirement):
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)
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 "
@@ -502,11 +381,9 @@ class VCSRequirement(FileRequirement):
for key in pipfile_keys:
if key in VCS_LIST:
creation_args["vcs"] = key
composed_uri = _clean_git_uri(
composed_uri = add_ssh_scheme_to_git_uri(
"{0}+{1}".format(key, pipfile.get(key))
).lstrip(
"{0}+".format(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)
@@ -521,8 +398,8 @@ class VCSRequirement(FileRequirement):
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)
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))
@@ -530,7 +407,7 @@ class VCSRequirement(FileRequirement):
name = link.egg_fragment
uri = link.url_without_fragment
if "git+git@" in line:
uri = _strip_ssh_from_git_uri(uri)
uri = strip_ssh_from_git_uri(uri)
subdirectory = link.subdirectory_fragment
ref = None
if "@" in link.show_url:
@@ -562,32 +439,32 @@ class VCSRequirement(FileRequirement):
if src_keys:
chosen_key = first(src_keys)
vcs_type = pipfile.pop("vcs")
_, pipfile_url = _split_vcs_method(pipfile.get(chosen_key))
_, pipfile_url = split_vcs_method_from_uri(pipfile.get(chosen_key))
pipfile[vcs_type] = pipfile_url
for removed in src_keys:
_ = pipfile.pop(removed)
pipfile.pop(removed)
return pipfile
@property
def pipfile_part(self):
pipfile_dict = attr.asdict(self, filter=_filter_none).copy()
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
@attr.s
class Requirement(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))
name = attr.ib()
vcs = attr.ib(default=None, validator=attr.validators.optional(validate_vcs))
req = attr.ib(default=None, validator=optional_instance_of(BaseRequirement))
markers = attr.ib(default=None)
specifiers = attr.ib(validator=attr.validators.optional(validate_specifiers))
index = attr.ib(default=None)
editable = attr.ib(default=None)
hashes = attr.ib(default=attr.Factory(list), converter=list)
extras = attr.ib(default=attr.Factory(list))
_ireq = None
_INCLUDE_FIELDS = ("name", "markers", "index", "editable", "hashes", "extras")
@@ -623,7 +500,7 @@ class Requirement(object):
@specifiers.default
def get_specifiers(self):
if self.req and self.req.req.specifier:
return _specs_to_string(self.req.req.specs)
return specs_to_string(self.req.req.specs)
return
@property
@@ -640,9 +517,7 @@ class Requirement(object):
@property
def normalized_name(self):
if not self.is_vcs and not self.is_file_or_url:
return pep423_name(self.name)
return self.name
return pep423_name(self.name)
@classmethod
def from_line(cls, line):
@@ -651,14 +526,15 @@ class Requirement(object):
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, markers = split_markers_from_line(line)
line, extras = _strip_extras(line)
stripped_line = line.split(" ", 1)[1] 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)
@@ -672,7 +548,7 @@ class Requirement(object):
r = NamedRequirement.from_line(stripped_line)
if extras:
extras = first(
requirements.parse("fakepkg{0}".format(_extras_to_string(extras)))
requirements.parse("fakepkg{0}".format(extras_to_string(extras)))
).extras
r.req.extras = extras
if markers:
@@ -691,11 +567,11 @@ class Requirement(object):
return cls(**args)
@classmethod
def from_pipfile(cls, name, indexes, pipfile):
def from_pipfile(cls, name, pipfile):
_pipfile = {}
if hasattr(pipfile, "keys"):
_pipfile = dict(pipfile).copy()
_pipfile["version"] = _get_version(pipfile)
_pipfile["version"] = get_version(pipfile)
vcs = first([vcs for vcs in VCS_LIST if vcs in _pipfile])
if vcs:
_pipfile["vcs"] = vcs
@@ -717,7 +593,15 @@ class Requirement(object):
args["hashes"] = _pipfile.get("hashes", [pipfile.get("hash")])
return cls(**args)
def as_line(self, include_index=False, project=None):
def as_line(self, sources=None):
"""Format this requirement as a line in requirements.txt.
If `sources` provided, it should be an sequence of mappings, containing
all possible sources to be used for this requirement.
If `sources` is omitted or falsy, no index information will be included
in the requirement line.
"""
line = "{0}{1}{2}{3}{4}".format(
self.req.line_part,
self.extras_as_pip,
@@ -725,24 +609,27 @@ class Requirement(object):
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 sources 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))
sources = [s for s in sources if s.get("name") == self.index]
index_string = " ".join(prepare_pip_source_args(sources))
line = "{0} {1}".format(line, index_string)
return line
def as_pipfile(self, include_index=False):
def as_pipfile(self):
good_keys = (
"hashes", "extras", "markers", "editable", "version", "index"
"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()
for k, v in attr.asdict(self, recurse=False, filter=filter_none).items()
if k in good_keys
}
name = self.name
@@ -756,7 +643,7 @@ class Requirement(object):
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)
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:
@@ -777,56 +664,3 @@ class Requirement(object):
else:
self._ireq = InstallRequirement.from_line(ireq_line)
return self._ireq
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 ""
+144
View File
@@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import os
import six
from attr import validators
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,
)
HASH_STRING = " --hash={0}"
def filter_none(k, v):
if v:
return True
return False
def optional_instance_of(cls):
return validators.optional(validators.instance_of(cls))
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 = add_ssh_scheme_to_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 ""
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 add_ssh_scheme_to_git_uri(uri):
"""Cleans VCS uris from pip 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_from_line(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_from_uri(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, InvalidSpecifier):
raise ValueError("Invalid Specifiers {0}".format(value))
+26 -21
View File
@@ -1,5 +1,6 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import
import logging
import os
import six
@@ -17,9 +18,22 @@ VCS_LIST = ("git", "svn", "hg", "bzr")
SCHEME_LIST = ("http://", "https://", "ftp://", "ftps://", "file://")
def setup_logger():
logger = logging.getLogger("requirementslib")
loglevel = logging.DEBUG
handler = logging.StreamHandler()
handler.setLevel(loglevel)
logger.addHandler(handler)
logger.setLevel(loglevel)
return logger
log = setup_logger()
def is_vcs(pipfile_entry):
import requirements
from .requirements import _clean_git_uri
from .models.utils import add_ssh_scheme_to_git_uri
"""Determine if dictionary entry from Pipfile is for a vcs dependency."""
if hasattr(pipfile_entry, "keys"):
@@ -27,7 +41,7 @@ def is_vcs(pipfile_entry):
elif isinstance(pipfile_entry, six.string_types):
return bool(
requirements.requirement.VCS_REGEX.match(_clean_git_uri(pipfile_entry))
requirements.requirement.VCS_REGEX.match(add_ssh_scheme_to_git_uri(pipfile_entry))
)
return False
@@ -36,7 +50,7 @@ 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':
if os.name == "nt":
return os.altsep.join([".", relpath])
return os.path.join(".", relpath)
@@ -57,9 +71,8 @@ def is_installable_file(path):
from ._compat import is_installable_dir, is_archive_file
from packaging import specifiers
if (
hasattr(path, "keys")
and any(key for key in path.keys() if key in ["file", "path"])
if hasattr(path, "keys") and any(
key for key in path.keys() if key in ["file", "path"]
):
path = urlparse(path["file"]).path if "file" in path else path["path"]
if not isinstance(path, six.string_types) or path == "*":
@@ -77,7 +90,7 @@ def is_installable_file(path):
return False
parsed = urlparse(path)
if parsed.scheme == 'file':
if parsed.scheme == "file":
path = parsed.path
if not os.path.exists(os.path.abspath(path)):
@@ -115,25 +128,17 @@ def prepare_pip_source_args(sources, pip_args=None):
pip_args = []
if sources:
# Add the source to pip9.
pip_args.extend(['-i', sources[0]['url']])
pip_args.extend(["-i", sources[0]["url"]])
# Trust the host if it's not verified.
if not sources[0].get('verify_ssl', True):
if not sources[0].get("verify_ssl", True):
pip_args.extend(
[
'--trusted-host',
urlparse(sources[0]['url']).netloc.split(':')[0],
]
["--trusted-host", urlparse(sources[0]["url"]).netloc.split(":")[0]]
)
# Add additional sources as extra indexes.
if len(sources) > 1:
for source in sources[1:]:
pip_args.extend(['--extra-index-url', source['url']])
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,
]
)
if not source.get("verify_ssl", True):
pip_args.extend(["--trusted-host", urlparse(source["url"]).hostname])
return pip_args