Update requirementslib to 1.1.2

Signed-off-by: Dan Ryan <dan@danryan.co>
This commit is contained in:
Dan Ryan
2018-08-25 13:15:47 -04:00
parent 3cb3f594a1
commit b626a11150
24 changed files with 2039 additions and 1139 deletions
+21 -3
View File
@@ -1,3 +1,21 @@
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.
The MIT License (MIT)
Copyright 2018 Dan Ryan.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-177
View File
@@ -1,177 +0,0 @@
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
-23
View File
@@ -1,23 +0,0 @@
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.
+1 -1
View File
@@ -1,5 +1,5 @@
# -*- coding=utf-8 -*-
__version__ = "1.0.11"
__version__ = '1.1.2'
from .exceptions import RequirementError
-56
View File
@@ -1,56 +0,0 @@
# -*- 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):
internal = "pip._internal.{0}".format(module_path)
old_path = old_path or module_path
pip9 = "pip.{0}".format(old_path)
try:
_tmp = importlib.import_module(internal)
except ImportError:
_tmp = importlib.import_module(pip9)
if subimport:
return getattr(_tmp, subimport, _tmp)
return _tmp
InstallRequirement = do_import("req.req_install", "InstallRequirement")
user_cache_dir = do_import("utils.appdirs", "user_cache_dir")
FAVORITE_HASH = do_import("utils.hashes", "FAVORITE_HASH")
is_file_url = do_import("download", "is_file_url")
url_to_path = do_import("download", "url_to_path")
path_to_url = do_import("download", "path_to_url")
is_archive_file = do_import("download", "is_archive_file")
_strip_extras = do_import("req.req_install", "_strip_extras")
Link = do_import("index", "Link")
Wheel = do_import("wheel", "Wheel")
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")
make_abstract_dist = do_import(
"operations.prepare", "make_abstract_dist", old_path="req.req_set"
)
VcsSupport = do_import("vcs", "VcsSupport")
-1
View File
@@ -1 +0,0 @@
# -*- coding=utf-8 -*-
-3
View File
@@ -1,3 +0,0 @@
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.
@@ -1,177 +0,0 @@
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
@@ -1,23 +0,0 @@
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.
@@ -1,21 +0,0 @@
# 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__
@@ -1,11 +0,0 @@
# 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
@@ -1,230 +0,0 @@
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)
+9
View File
@@ -1,5 +1,14 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import errno
import six
if six.PY2:
class FileExistsError(OSError):
def __init__(self, *args, **kwargs):
self.errno = errno.EEXIST
super(FileExistsError, self).__init__(*args, **kwargs)
class RequirementError(Exception):
+2 -1
View File
@@ -2,9 +2,10 @@
from __future__ import absolute_import
__all__ = ["Requirement", "Lockfile", "Pipfile", "RequirementError"]
__all__ = ["Requirement", "Lockfile", "Pipfile", "DependencyResolver"]
from .requirements import Requirement
from .lockfile import Lockfile
from .pipfile import Pipfile
from .resolvers import DependencyResolver
+266
View File
@@ -0,0 +1,266 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
import copy
import errno
import hashlib
import json
import os
import six
import sys
from contextlib import contextmanager
import requests
import vistir
from appdirs import user_cache_dir
from packaging.requirements import Requirement
from pip_shims.shims import (
FAVORITE_HASH, Link, SafeFileCache, VcsSupport, is_file_url, url_to_path
)
from .utils import as_tuple, key_from_req, lookup_table
if six.PY2:
from ..exceptions import FileExistsError
CACHE_DIR = os.environ.get("PIPENV_CACHE_DIR", user_cache_dir("pipenv"))
# Pip-tools cache implementation
class CorruptCacheError(Exception):
def __init__(self, path):
self.path = path
def __str__(self):
lines = [
'The dependency cache seems to have been corrupted.',
'Inspect, or delete, the following file:',
' {}'.format(self.path),
]
return os.linesep.join(lines)
def read_cache_file(cache_file_path):
with open(cache_file_path, 'r') as cache_file:
try:
doc = json.load(cache_file)
except ValueError:
raise CorruptCacheError(cache_file_path)
# Check version and load the contents
assert doc['__format__'] == 1, 'Unknown cache file format'
return doc['dependencies']
class DependencyCache(object):
"""
Creates a new persistent dependency cache for the current Python version.
The cache file is written to the appropriate user cache dir for the
current platform, i.e.
~/.cache/pip-tools/depcache-pyX.Y.json
Where X.Y indicates the Python version.
"""
def __init__(self, cache_dir=None):
if cache_dir is None:
cache_dir = CACHE_DIR
if not vistir.compat.Path(CACHE_DIR).absolute().is_dir():
try:
vistir.path.mkdir_p(os.path.abspath(cache_dir))
except (FileExistsError, OSError):
pass
py_version = '.'.join(str(digit) for digit in sys.version_info[:2])
cache_filename = 'depcache-py{}.json'.format(py_version)
self._cache_file = os.path.join(cache_dir, cache_filename)
self._cache = None
@property
def cache(self):
"""
The dictionary that is the actual in-memory cache. This property
lazily loads the cache from disk.
"""
if self._cache is None:
self.read_cache()
return self._cache
def as_cache_key(self, ireq):
"""
Given a requirement, return its cache key. This behavior is a little weird in order to allow backwards
compatibility with cache files. For a requirement without extras, this will return, for example:
("ipython", "2.1.0")
For a requirement with extras, the extras will be comma-separated and appended to the version, inside brackets,
like so:
("ipython", "2.1.0[nbconvert,notebook]")
"""
name, version, extras = as_tuple(ireq)
if not extras:
extras_string = ""
else:
extras_string = "[{}]".format(",".join(extras))
return name, "{}{}".format(version, extras_string)
def read_cache(self):
"""Reads the cached contents into memory."""
if os.path.exists(self._cache_file):
self._cache = read_cache_file(self._cache_file)
else:
self._cache = {}
def write_cache(self):
"""Writes the cache to disk as JSON."""
doc = {
'__format__': 1,
'dependencies': self._cache,
}
with open(self._cache_file, 'w') as f:
json.dump(doc, f, sort_keys=True)
def clear(self):
self._cache = {}
self.write_cache()
def __contains__(self, ireq):
pkgname, pkgversion_and_extras = self.as_cache_key(ireq)
return pkgversion_and_extras in self.cache.get(pkgname, {})
def __getitem__(self, ireq):
pkgname, pkgversion_and_extras = self.as_cache_key(ireq)
return self.cache[pkgname][pkgversion_and_extras]
def __setitem__(self, ireq, values):
pkgname, pkgversion_and_extras = self.as_cache_key(ireq)
self.cache.setdefault(pkgname, {})
self.cache[pkgname][pkgversion_and_extras] = values
self.write_cache()
def __delitem__(self, ireq):
pkgname, pkgversion_and_extras = self.as_cache_key(ireq)
try:
del self.cache[pkgname][pkgversion_and_extras]
except KeyError:
return
self.write_cache()
def get(self, ireq, default=None):
pkgname, pkgversion_and_extras = self.as_cache_key(ireq)
return self.cache.get(pkgname, {}).get(pkgversion_and_extras, default)
def reverse_dependencies(self, ireqs):
"""
Returns a lookup table of reverse dependencies for all the given ireqs.
Since this is all static, it only works if the dependency cache
contains the complete data, otherwise you end up with a partial view.
This is typically no problem if you use this function after the entire
dependency tree is resolved.
"""
ireqs_as_cache_values = [self.as_cache_key(ireq) for ireq in ireqs]
return self._reverse_dependencies(ireqs_as_cache_values)
def _reverse_dependencies(self, cache_keys):
"""
Returns a lookup table of reverse dependencies for all the given cache keys.
Example input:
[('pep8', '1.5.7'),
('flake8', '2.4.0'),
('mccabe', '0.3'),
('pyflakes', '0.8.1')]
Example output:
{'pep8': ['flake8'],
'flake8': [],
'mccabe': ['flake8'],
'pyflakes': ['flake8']}
"""
# First, collect all the dependencies into a sequence of (parent, child) tuples, like [('flake8', 'pep8'),
# ('flake8', 'mccabe'), ...]
return lookup_table((key_from_req(Requirement(dep_name)), name)
for name, version_and_extras in cache_keys
for dep_name in self.cache[name][version_and_extras])
class HashCache(SafeFileCache):
"""Caches hashes of PyPI artifacts so we do not need to re-download them
Hashes are only cached when the URL appears to contain a hash in it and the cache key includes
the hash value returned from the server). This ought to avoid ssues where the location on the
server changes."""
def __init__(self, *args, **kwargs):
session = kwargs.pop('session', requests.session())
self.session = session
kwargs.setdefault('directory', os.path.join(CACHE_DIR, 'hash-cache'))
super(HashCache, self).__init__(*args, **kwargs)
def get_hash(self, location):
# if there is no location hash (i.e., md5 / sha256 / etc) we on't want to store it
hash_value = None
vcs = VcsSupport()
orig_scheme = location.scheme
new_location = copy.deepcopy(location)
if orig_scheme in vcs.all_schemes:
new_location.url = new_location.url.split("+", 1)[-1]
can_hash = new_location.hash
if can_hash:
# hash url WITH fragment
hash_value = self.get(new_location.url)
if not hash_value:
hash_value = self._get_file_hash(new_location)
hash_value = hash_value.encode('utf8')
if can_hash:
self.set(new_location.url, hash_value)
return hash_value.decode('utf8')
def _get_file_hash(self, location):
h = hashlib.new(FAVORITE_HASH)
with open_local_or_remote_file(location, self.session) as fp:
for chunk in iter(lambda: fp.read(8096), b""):
h.update(chunk)
return ":".join([FAVORITE_HASH, h.hexdigest()])
@contextmanager
def open_local_or_remote_file(link, session):
"""
Open local or remote file for reading.
:type link: pip._internal.index.Link
:type session: requests.Session
:raises ValueError: If link points to a local directory.
:return: a context manager to the opened file-like object
"""
if isinstance(link, Link):
url = link.url_without_fragment
else:
url = link
if is_file_url(link):
# Local URL
local_path = url_to_path(url)
if os.path.isdir(local_path):
raise ValueError("Cannot open directory for read: {}".format(url))
else:
with open(local_path, 'rb') as local_file:
yield local_file
else:
# Remote URL
headers = {"Accept-Encoding": "identity"}
response = session.get(url, headers=headers, stream=True)
try:
yield response.raw
finally:
response.close()
+650
View File
@@ -0,0 +1,650 @@
# -*- coding=utf-8 -*-
import contextlib
import copy
import functools
import os
import attr
import packaging.markers
import packaging.version
import requests
from first import first
from packaging.utils import canonicalize_name
from pip_shims import (
FormatControl, InstallRequirement, PackageFinder, RequirementPreparer,
RequirementSet, RequirementTracker, Resolver, WheelCache, pip_version
)
from vistir.compat import JSONDecodeError, TemporaryDirectory, fs_str
from vistir.contextmanagers import cd, temp_environ
from vistir.misc import partialclass
from ..utils import get_pip_command, prepare_pip_source_args, _ensure_dir
from .cache import CACHE_DIR, DependencyCache
from .utils import (
clean_requires_python, fix_requires_python_marker, format_requirement,
full_groupby, is_pinned_requirement, key_from_ireq,
make_install_requirement, name_from_req, version_from_ireq
)
PKGS_DOWNLOAD_DIR = fs_str(os.path.join(CACHE_DIR, "pkgs"))
WHEEL_DOWNLOAD_DIR = fs_str(os.path.join(CACHE_DIR, "wheels"))
DEPENDENCY_CACHE = DependencyCache()
WHEEL_CACHE = WheelCache(CACHE_DIR, FormatControl(set(), set()))
def _get_filtered_versions(ireq, versions, prereleases):
return set(ireq.specifier.filter(versions, prereleases=prereleases))
def find_all_matches(finder, ireq, pre=False):
"""Find all matching dependencies using the supplied finder and the
given ireq.
:param finder: A package finder for discovering matching candidates.
:type finder: :class:`~pip._internal.index.PackageFinder`
:param ireq: An install requirement.
:type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
:return: A list of matching candidates.
:rtype: list[:class:`~pip._internal.index.InstallationCandidate`]
"""
candidates = clean_requires_python(finder.find_all_candidates(ireq.name))
versions = {candidate.version for candidate in candidates}
allowed_versions = _get_filtered_versions(ireq, versions, pre)
if not pre and not allowed_versions:
allowed_versions = _get_filtered_versions(ireq, versions, True)
candidates = {c for c in candidates if c.version in allowed_versions}
return candidates
@attr.s
class AbstractDependency(object):
name = attr.ib()
specifiers = attr.ib()
markers = attr.ib()
candidates = attr.ib()
requirement = attr.ib()
parent = attr.ib()
finder = attr.ib()
dep_dict = attr.ib(default=attr.Factory(dict))
@property
def version_set(self):
"""Return the set of versions for the candidates in this abstract dependency.
:return: A set of matching versions
:rtype: set(str)
"""
if len(self.candidates) == 1:
return set()
return set(packaging.version.parse(version_from_ireq(c)) for c in self.candidates)
def compatible_versions(self, other):
"""Find compatible version numbers between this abstract
dependency and another one.
:param other: An abstract dependency to compare with.
:type other: :class:`~requirementslib.models.dependency.AbstractDependency`
:return: A set of compatible version strings
:rtype: set(str)
"""
if len(self.candidates) == 1 and first(self.candidates).editable:
return self
elif len(other.candidates) == 1 and first(other.candidates).editable:
return other
return self.version_set & other.version_set
def compatible_abstract_dep(self, other):
"""Merge this abstract dependency with another one.
Return the result of the merge as a new abstract dependency.
:param other: An abstract dependency to merge with
:type other: :class:`~requirementslib.models.dependency.AbstractDependency`
:return: A new, combined abstract dependency
:rtype: :class:`~requirementslib.models.dependency.AbstractDependency`
"""
from .requirements import Requirement
if len(self.candidates) == 1 and first(self.candidates).editable:
return self
elif len(other.candidates) == 1 and first(other.candidates).editable:
return other
new_specifiers = self.specifiers & other.specifiers
markers = set(self.markers,) if self.markers else set()
if other.markers:
markers.add(other.markers)
new_markers = packaging.markers.Marker(" or ".join(str(m) for m in sorted(markers)))
new_ireq = copy.deepcopy(self.requirement.ireq)
new_ireq.req.specifier = new_specifiers
new_ireq.req.marker = new_markers
new_requirement = Requirement.from_line(format_requirement(new_ireq))
compatible_versions = self.compatible_versions(other)
if isinstance(compatible_versions, AbstractDependency):
return compatible_versions
candidates = [
c
for c in self.candidates
if packaging.version.parse(version_from_ireq(c)) in compatible_versions
]
dep_dict = {}
candidate_strings = [format_requirement(c) for c in candidates]
for c in candidate_strings:
if c in self.dep_dict:
dep_dict[c] = self.dep_dict.get(c)
return AbstractDependency(
name=self.name,
specifiers=new_specifiers,
markers=new_markers,
candidates=candidates,
requirement=new_requirement,
parent=self.parent,
dep_dict=dep_dict,
finder=self.finder
)
def get_deps(self, candidate):
"""Get the dependencies of the supplied candidate.
:param candidate: An installrequirement
:type candidate: :class:`~pip._internal.req.req_install.InstallRequirement`
:return: A list of abstract dependencies
:rtype: list[:class:`~requirementslib.models.dependency.AbstractDependency`]
"""
key = format_requirement(candidate)
if key not in self.dep_dict:
from .requirements import Requirement
req = Requirement.from_line(key)
req.merge_markers(self.markers)
self.dep_dict[key] = req.get_abstract_dependencies()
return self.dep_dict[key]
@classmethod
def from_requirement(cls, requirement, parent=None):
"""Creates a new :class:`~requirementslib.models.dependency.AbstractDependency`
from a :class:`~requirementslib.models.requirements.Requirement` object.
This class is used to find all candidates matching a given set of specifiers
and a given requirement.
:param requirement: A requirement for resolution
:type requirement: :class:`~requirementslib.models.requirements.Requirement` object.
"""
name = requirement.normalized_name
specifiers = requirement.ireq.specifier if not requirement.editable else ""
markers = requirement.ireq.markers
extras = requirement.ireq.extras
is_pinned = is_pinned_requirement(requirement.ireq)
is_constraint = bool(parent)
finder = get_finder(sources=None)
candidates = []
if not is_pinned and not requirement.editable:
for r in requirement.find_all_matches(finder=finder):
req = make_install_requirement(
name, r.version, extras=extras, markers=markers, constraint=is_constraint,
)
req.req.link = r.location
req.parent = parent
candidates.append(req)
candidates = sorted(
set(candidates), key=lambda k: packaging.version.parse(version_from_ireq(k)),
)
else:
candidates = [requirement.ireq]
return cls(
name=name,
specifiers=specifiers,
markers=markers,
candidates=candidates,
requirement=requirement,
parent=parent,
finder=finder,
)
@classmethod
def from_string(cls, line, parent=None):
from .requirements import Requirement
req = Requirement.from_line(line)
abstract_dep = cls.from_requirement(req, parent=parent)
return abstract_dep
def get_abstract_dependencies(reqs, sources=None, parent=None):
"""Get all abstract dependencies for a given list of requirements.
Given a set of requirements, convert each requirement to an Abstract Dependency.
:param reqs: A list of Requirements
:type reqs: list[:class:`~requirementslib.models.requirements.Requirement`]
:param sources: Pipfile-formatted sources, defaults to None
:param sources: list[dict], optional
:param parent: The parent of this list of dependencies, defaults to None
:param parent: :class:`~requirementslib.models.requirements.Requirement`, optional
:return: A list of Abstract Dependencies
:rtype: list[:class:`~requirementslib.models.dependency.AbstractDependency`]
"""
deps = []
from .requirements import Requirement
for req in reqs:
if isinstance(req, InstallRequirement):
requirement = Requirement.from_line(
"{0}{1}".format(req.name, req.specifier)
)
if req.link:
requirement.req.link = req.link
requirement.markers = req.markers
requirement.req.markers = req.markers
requirement.extras = req.extras
requirement.req.extras = req.extras
elif isinstance(req, Requirement):
requirement = copy.deepcopy(req)
else:
requirement = Requirement.from_line(req)
dep = AbstractDependency.from_requirement(requirement, parent=parent)
deps.append(dep)
return deps
def get_dependencies(ireq, sources=None, parent=None):
"""Get all dependencies for a given install requirement.
:param ireq: A single InstallRequirement
:type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
:param sources: Pipfile-formatted sources, defaults to None
:type sources: list[dict], optional
:param parent: The parent of this list of dependencies, defaults to None
:type parent: :class:`~pip._internal.req.req_install.InstallRequirement`
:return: A set of dependency lines for generating new InstallRequirements.
:rtype: set(str)
"""
if not isinstance(ireq, InstallRequirement):
name = getattr(
ireq, "project_name",
getattr(ireq, "project", ireq.name),
)
version = getattr(ireq, "version")
ireq = InstallRequirement.from_line("{0}=={1}".format(name, version))
pip_options = get_pip_options(sources=sources)
getters = [
get_dependencies_from_cache,
get_dependencies_from_wheel_cache,
get_dependencies_from_json,
functools.partial(get_dependencies_from_index, pip_options=pip_options)
]
for getter in getters:
deps = getter(ireq)
if deps is not None:
return deps
raise RuntimeError('failed to get dependencies for {}'.format(ireq))
def get_dependencies_from_wheel_cache(ireq):
"""Retrieves dependencies for the given install requirement from the wheel cache.
:param ireq: A single InstallRequirement
:type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
:return: A set of dependency lines for generating new InstallRequirements.
:rtype: set(str) or None
"""
if ireq.editable or not is_pinned_requirement(ireq):
return
matches = WHEEL_CACHE.get(ireq.link, name_from_req(ireq.req))
if matches:
matches = set(matches)
if not DEPENDENCY_CACHE.get(ireq):
DEPENDENCY_CACHE[ireq] = [format_requirement(m) for m in matches]
return matches
return
def _marker_contains_extra(ireq):
# TODO: Implement better parsing logic avoid false-positives.
return "extra" in repr(ireq.markers)
def get_dependencies_from_json(ireq):
"""Retrieves dependencies for the given install requirement from the json api.
:param ireq: A single InstallRequirement
:type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
:return: A set of dependency lines for generating new InstallRequirements.
:rtype: set(str) or None
"""
if ireq.editable or not is_pinned_requirement(ireq):
return
# It is technically possible to parse extras out of the JSON API's
# requirement format, but it is such a chore let's just use the simple API.
if ireq.extras:
return
session = requests.session()
version = str(ireq.req.specifier).lstrip("=")
def gen(ireq):
info = None
info = session.get(
"https://pypi.org/pypi/{0}/{1}/json".format(ireq.req.name, version)
).json()["info"]
requires_dist = info.get("requires_dist", info.get("requires"))
if not requires_dist: # The API can return None for this.
return
for requires in requires_dist:
i = InstallRequirement.from_line(requires)
# See above, we don't handle requirements with extras.
if not _marker_contains_extra(i):
yield format_requirement(i)
if ireq not in DEPENDENCY_CACHE:
try:
reqs = DEPENDENCY_CACHE[ireq] = list(gen(ireq))
except JSONDecodeError:
return
req_iter = iter(reqs)
else:
req_iter = gen(ireq)
return set(req_iter)
def get_dependencies_from_cache(ireq):
"""Retrieves dependencies for the given install requirement from the dependency cache.
:param ireq: A single InstallRequirement
:type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
:return: A set of dependency lines for generating new InstallRequirements.
:rtype: set(str) or None
"""
if ireq.editable or not is_pinned_requirement(ireq):
return
if ireq not in DEPENDENCY_CACHE:
return
cached = set(DEPENDENCY_CACHE[ireq])
# Preserving sanity: Run through the cache and make sure every entry if
# valid. If this fails, something is wrong with the cache. Drop it.
try:
broken = False
for line in cached:
dep_ireq = InstallRequirement.from_line(line)
name = canonicalize_name(dep_ireq.name)
if _marker_contains_extra(dep_ireq):
broken = True # The "extra =" marker breaks everything.
elif name == canonicalize_name(ireq.name):
broken = True # A package cannot depend on itself.
if broken:
break
except Exception:
broken = True
if broken:
del DEPENDENCY_CACHE[ireq]
return
return cached
def is_python(section):
return section.startswith('[') and ':' in section
def get_dependencies_from_index(dep, sources=None, pip_options=None, wheel_cache=None):
"""Retrieves dependencies for the given install requirement from the pip resolver.
:param dep: A single InstallRequirement
:type dep: :class:`~pip._internal.req.req_install.InstallRequirement`
:param sources: Pipfile-formatted sources, defaults to None
:type sources: list[dict], optional
:return: A set of dependency lines for generating new InstallRequirements.
:rtype: set(str) or None
"""
finder = get_finder(sources=sources, pip_options=pip_options)
if not wheel_cache:
wheel_cache = WHEEL_CACHE
dep.is_direct = True
reqset = RequirementSet()
reqset.add_requirement(dep)
requirements = None
setup_requires = {}
with temp_environ(), start_resolver(finder=finder, wheel_cache=wheel_cache) as resolver:
os.environ['PIP_EXISTS_ACTION'] = 'i'
dist = None
if dep.editable and not dep.prepared and not dep.req:
with cd(dep.setup_py_dir):
from setuptools.dist import distutils
try:
dist = distutils.core.run_setup(dep.setup_py)
except (ImportError, TypeError, AttributeError):
dist = None
else:
setup_requires[dist.get_name()] = dist.setup_requires
if not dist:
try:
dist = dep.get_dist()
except (TypeError, ValueError, AttributeError):
pass
else:
setup_requires[dist.get_name()] = dist.setup_requires
resolver.require_hashes = False
try:
results = resolver._resolve_one(reqset, dep)
except Exception:
# FIXME: Needs to bubble the exception somehow to the user.
results = []
finally:
try:
wheel_cache.cleanup()
except AttributeError:
pass
resolver_requires_python = getattr(resolver, "requires_python", None)
requires_python = getattr(reqset, "requires_python", resolver_requires_python)
if requires_python:
add_marker = fix_requires_python_marker(requires_python)
reqset.remove(dep)
if dep.req.marker:
dep.req.marker._markers.extend(['and',].extend(add_marker._markers))
else:
dep.req.marker = add_marker
reqset.add(dep)
requirements = set()
for r in results:
if requires_python:
if r.req.marker:
r.req.marker._markers.extend(['and',].extend(add_marker._markers))
else:
r.req.marker = add_marker
requirements.add(format_requirement(r))
for section in setup_requires:
python_version = section
not_python = not is_python(section)
# This is for cleaning up :extras: formatted markers
# by adding them to the results of the resolver
# since any such extra would have been returned as a result anyway
for value in setup_requires[section]:
# This is a marker.
if is_python(section):
python_version = value[1:-1]
else:
not_python = True
if ':' not in value and not_python:
try:
requirement_str = "{0}{1}".format(value, python_version).replace(":", ";")
requirements.add(format_requirement(make_install_requirement(requirement_str).ireq))
# Anything could go wrong here -- can't be too careful.
except Exception:
pass
if not dep.editable and is_pinned_requirement(dep) and requirements is not None:
DEPENDENCY_CACHE[dep] = list(requirements)
return requirements
def get_pip_options(args=[], sources=None, pip_command=None):
"""Build a pip command from a list of sources
:param args: positional arguments passed through to the pip parser
:param sources: A list of pipfile-formatted sources, defaults to None
:param sources: list[dict], optional
:param pip_command: A pre-built pip command instance
:type pip_command: :class:`~pip._internal.cli.base_command.Command`
:return: An instance of pip_options using the supplied arguments plus sane defaults
:rtype: :class:`~pip._internal.cli.cmdoptions`
"""
if not pip_command:
pip_command = get_pip_command()
if not sources:
sources = [
{"url": "https://pypi.org/simple", "name": "pypi", "verify_ssl": True}
]
_ensure_dir(CACHE_DIR)
pip_args = args
pip_args = prepare_pip_source_args(sources, pip_args)
pip_options, _ = pip_command.parser.parse_args(pip_args)
pip_options.cache_dir = CACHE_DIR
return pip_options
def get_finder(sources=None, pip_command=None, pip_options=None):
"""Get a package finder for looking up candidates to install
:param sources: A list of pipfile-formatted sources, defaults to None
:param sources: list[dict], optional
:param pip_command: A pip command instance, defaults to None
:type pip_command: :class:`~pip._internal.cli.base_command.Command`
:param pip_options: A pip options, defaults to None
:type pip_options: :class:`~pip._internal.cli.cmdoptions`
:return: A package finder
:rtype: :class:`~pip._internal.index.PackageFinder`
"""
if not pip_command:
pip_command = get_pip_command()
if not sources:
sources = [
{"url": "https://pypi.org/simple", "name": "pypi", "verify_ssl": True}
]
if not pip_options:
pip_options = get_pip_options(sources=sources, pip_command=pip_command)
session = pip_command._build_session(pip_options)
finder = PackageFinder(
find_links=[],
index_urls=[s.get("url") for s in sources],
trusted_hosts=[],
allow_all_prereleases=pip_options.pre,
session=session,
)
return finder
@contextlib.contextmanager
def start_resolver(finder=None, wheel_cache=None):
"""Context manager to produce a resolver.
:param finder: A package finder to use for searching the index
:type finder: :class:`~pip._internal.index.PackageFinder`
:return: A 3-tuple of finder, preparer, resolver
:rtype: (:class:`~pip._internal.operations.prepare.RequirementPreparer`, :class:`~pip._internal.resolve.Resolver`)
"""
pip_command = get_pip_command()
pip_options = get_pip_options(pip_command=pip_command)
if not finder:
finder = get_finder(pip_command=pip_command, pip_options=pip_options)
if not wheel_cache:
wheel_cache = WHEEL_CACHE
_ensure_dir(fs_str(os.path.join(wheel_cache.cache_dir, "wheels")))
download_dir = PKGS_DOWNLOAD_DIR
_ensure_dir(download_dir)
_build_dir = TemporaryDirectory(fs_str("build"))
_source_dir = TemporaryDirectory(fs_str("source"))
preparer = partialclass(
RequirementPreparer,
build_dir=_build_dir.name,
src_dir=_source_dir.name,
download_dir=download_dir,
wheel_download_dir=WHEEL_DOWNLOAD_DIR,
progress_bar="off",
build_isolation=False,
)
resolver = partialclass(
Resolver,
finder=finder,
session=finder.session,
upgrade_strategy="to-satisfy-only",
force_reinstall=True,
ignore_dependencies=False,
ignore_requires_python=True,
ignore_installed=True,
isolated=False,
wheel_cache=wheel_cache,
use_user_site=False,
)
if packaging.version.parse(pip_version) >= packaging.version.parse('18'):
with RequirementTracker() as req_tracker:
preparer = preparer(req_tracker=req_tracker)
yield resolver(preparer=preparer)
else:
preparer = preparer()
yield resolver(preparer=preparer)
def get_grouped_dependencies(constraints):
# We need to track what contributed a specifierset
# as well as which specifiers were required by the root node
# in order to resolve any conflicts when we are deciding which thing to backtrack on
# then we take the loose match (which _is_ flexible) and start moving backwards in
# versions by popping them off of a stack and checking for the conflicting package
for _, ireqs in full_groupby(constraints, key=key_from_ireq):
ireqs = list(ireqs)
editable_ireq = first(ireqs, key=lambda ireq: ireq.editable)
if editable_ireq:
yield editable_ireq # ignore all the other specs: the editable one is the one that counts
continue
ireqs = iter(ireqs)
# deepcopy the accumulator so as to not modify the self.our_constraints invariant
combined_ireq = copy.deepcopy(next(ireqs))
for ireq in ireqs:
# NOTE we may be losing some info on dropped reqs here
try:
combined_ireq.req.specifier &= ireq.req.specifier
except TypeError:
if ireq.req.specifier._specs and not combined_ireq.req.specifier._specs:
combined_ireq.req.specifier._specs = ireq.req.specifier._specs
combined_ireq.constraint &= ireq.constraint
if not combined_ireq.markers:
combined_ireq.markers = ireq.markers
else:
_markers = combined_ireq.markers._markers
if not isinstance(_markers[0], (tuple, list)):
combined_ireq.markers._markers = [
_markers,
"and",
ireq.markers._markers,
]
# Return a sorted, de-duped tuple of extras
combined_ireq.extras = tuple(
sorted(set(tuple(combined_ireq.extras) + tuple(ireq.extras)))
)
yield combined_ireq
+61 -21
View File
@@ -1,24 +1,54 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import attr
import json
import os
import plette.lockfiles
import six
from vistir.compat import Path
from vistir.contextmanagers import atomic_open_for_write
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=attr.Factory(list))
requirements = attr.ib(default=attr.Factory(list))
path = attr.ib(default=None, validator=optional_instance_of(Path))
pipfile_hash = attr.ib(default=None)
DEFAULT_NEWLINES = u"\n"
def preferred_newlines(f):
if isinstance(f.newlines, six.text_type):
return f.newlines
return DEFAULT_NEWLINES
class Lockfile(plette.lockfiles.Lockfile):
def __init__(self, *args, **kwargs):
path = kwargs.pop("path", None)
self.requirements = kwargs.pop("requirements", [])
self.dev_requirements = kwargs.pop("dev_requirements", [])
self.path = Path(path) if path else None
self.newlines = u"\n"
super(Lockfile, self).__init__(*args, **kwargs)
@classmethod
def load(cls, path):
if not path:
path = os.curdir
path = Path(path).absolute()
if path.is_dir():
path = path / "Pipfile.lock"
elif path.name == "Pipfile":
path = path.parent / "Pipfile.lock"
if not path.exists():
raise OSError("Path does not exist: %s" % path)
return cls.create(path.parent, lockfile_name=path.name)
@classmethod
def create(cls, project_path, lockfile_name="Pipfile.lock"):
"""Create a new lockfile instance
:param project_path: Path to the project root
:param project_path: Path to project root
:type project_path: str or :class:`~pathlib.Path`
:returns: List[:class:`~requirementslib.Requirement`] objects
"""
@@ -28,20 +58,30 @@ class Lockfile(object):
lockfile_path = project_path / lockfile_name
requirements = []
dev_requirements = []
if not lockfile_path.exists():
raise FileNotFoundError("No such lockfile: %s" % lockfile_path)
with lockfile_path.open(encoding="utf-8") as f:
lockfile = json.loads(f.read())
lockfile = super(Lockfile, cls).load(f)
lockfile.newlines = preferred_newlines(f)
for k in lockfile["develop"].keys():
dev_requirements.append(Requirement.from_pipfile(k, lockfile["develop"][k]))
dev_requirements.append(Requirement.from_pipfile(k, lockfile.develop[k]._data))
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,
)
requirements.append(Requirement.from_pipfile(k, lockfile.default[k]._data))
lockfile.requirements = requirements
lockfile.dev_requirements = dev_requirements
lockfile.path = lockfile_path
return lockfile
@property
def dev_requirements_list(self):
return [r.as_pipfile() for r in self.dev_requirements]
@property
def requirements_list(self):
return [r.as_pipfile() for r in self.requirements]
def write(self):
open_kwargs = {"newline": self.newlines}
with atomic_open_for_write(self.path.as_posix(), **open_kwargs) as f:
super(Lockfile, self).dump(f, encoding="utf-8")
def as_requirements(self, include_hashes=False, dev=False):
"""Returns a list of requirements in pip-style format"""
+8 -7
View File
@@ -1,10 +1,11 @@
# -*- 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 packaging.markers import InvalidMarker, Marker
from ..exceptions import RequirementError
from .baserequirement import BaseRequirement
from .utils import filter_none, validate_markers
@attr.s
@@ -84,10 +85,10 @@ class PipenvMarkers(BaseRequirement):
markers = []
for marker in marker_strings:
markers.append(marker)
marker = ''
combined_marker = None
try:
marker = cls.make_marker(" and ".join(markers))
combined_marker = cls.make_marker(" and ".join(markers))
except RequirementError:
pass
else:
return marker
return combined_marker
+41 -165
View File
@@ -1,100 +1,13 @@
# -*- coding: utf-8 -*-
import attr
import contoml
import os
import toml
from .._vendor import pipfile
from vistir.compat import Path
from .requirements import Requirement
from .utils import optional_instance_of, filter_none
from .._compat import Path, FileNotFoundError
from ..exceptions import RequirementError
import plette.pipfiles
@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
class Pipfile(plette.pipfiles.Pipfile):
@property
def requires_python(self):
@@ -102,50 +15,7 @@ class Pipfile(object):
@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)
return self.get("pipenv", {}).get("allow_prereleases", False)
@classmethod
def load(cls, path):
@@ -156,34 +26,40 @@ class Pipfile(object):
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)
with pipfile_path.open(encoding="utf-8") as fp:
pipfile = super(Pipfile, cls).load(fp)
pipfile.dev_requirements = [
Requirement.from_pipfile(k, v) for k, v in pipfile.dev_packages.items()
]
pipfile.requirements = [
Requirement.from_pipfile(k, v) for k, v in pipfile.packages.items()
]
pipfile.path = pipfile_path
return pipfile
@staticmethod
def get_section(pf_dict, section):
"""Get section objects from a pipfile dictionary
# def resolve(self):
# It would be nice to still use this api someday
# option_sources = [s.expanded for s in self.sources]
# pip_args = []
# if self.pipenv.allow_prereleases:
# pip_args.append('--pre')
# pip_options = get_pip_options(pip_args, sources=option_sources)
# finder = get_finder(sources=option_sources, pip_options=pip_options)
# resolver = DependencyResolver.create(finder=finder, allow_prereleases=self.pipenv.allow_prereleases)
# pkg_dict = {}
# for pkg in self.dev_packages.requirements + self.packages.requirements:
# pkg_dict[pkg.name] = pkg
# resolver.resolve(list(pkg_dict.values()))
# return resolver
: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)
@property
def dev_packages(self, as_requirements=True):
if as_requirements:
return self.dev_requirements
return self.dev_packages
@property
def packages(self, as_requirements=True):
if as_requirements:
return self.requirements
return self.packages
+311 -114
View File
@@ -1,55 +1,46 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import attr
import collections
import hashlib
import os
import requirements
import attr
import atexit
from first import first
from packaging.markers import Marker
from packaging.specifiers import Specifier, SpecifierSet
from packaging.utils import canonicalize_name
from six.moves.urllib import parse as urllib_parse
from six.moves.urllib.parse import unquote
from pip_shims.shims import (
InstallRequirement, Link, Wheel, _strip_extras, parse_version, path_to_url,
url_to_path
)
from vistir.compat import FileNotFoundError, Path, TemporaryDirectory
from vistir.misc import dedup
from vistir.path import get_converted_relative_path, is_valid_url, is_file_url, mkdir_p
from ..exceptions import RequirementError
from ..utils import VCS_LIST, is_vcs, is_installable_file
from .baserequirement import BaseRequirement
from .dependencies import (
AbstractDependency, find_all_matches, get_abstract_dependencies,
get_dependencies, get_finder
)
from .markers import PipenvMarkers
from .utils import (
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,
parse_extras,
)
from .._compat import (
Link,
path_to_url,
url_to_path,
_strip_extras,
InstallRequirement,
Path,
urlparse,
unquote,
Wheel,
FileNotFoundError,
)
from ..exceptions import RequirementError
from ..utils import (
VCS_LIST,
is_installable_file,
is_vcs,
is_valid_url,
pep423_name,
get_converted_relative_path,
HASH_STRING, add_ssh_scheme_to_git_uri, build_vcs_link, filter_none,
format_requirement, get_version, init_requirement,
is_pinned_requirement, make_install_requirement, optional_instance_of,
parse_extras, specs_to_string, split_markers_from_line,
split_vcs_method_from_uri, strip_ssh_from_git_uri, validate_path,
validate_specifiers, validate_vcs, extras_to_string
)
from .vcs import VCSRepository
from packaging.requirements import Requirement as PackagingRequirement
@attr.s
@@ -58,25 +49,31 @@ class NamedRequirement(BaseRequirement):
version = attr.ib(validator=attr.validators.optional(validate_specifiers))
req = attr.ib()
extras = attr.ib(default=attr.Factory(list))
editable = attr.ib(default=False)
@req.default
def get_requirement(self):
from pkg_resources import RequirementParseError
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)
)
req = init_requirement("{0}{1}".format(canonicalize_name(self.name), self.version))
return req
@classmethod
def from_line(cls, line):
req = first(requirements.parse(line))
req = init_requirement(line)
specifiers = None
if req.specifier:
specifiers = specs_to_string(req.specs)
return cls(name=req.name, version=specifiers, req=req)
specifiers = specs_to_string(req.specifier)
req.line = line
name = getattr(req, "name", None)
if not name:
name = getattr(req, "project_name", None)
req.name = name
if not name:
name = getattr(req, "key", line)
req.name = name
extras = None
if req.extras:
extras = list(req.extras)
return cls(name=name, version=specifiers, req=req, extras=extras)
@classmethod
def from_pipfile(cls, name, pipfile):
@@ -85,13 +82,17 @@ class NamedRequirement(BaseRequirement):
creation_args = {k: v for k, v in pipfile.items() if k in cls.attr_fields()}
creation_args["name"] = name
version = get_version(pipfile)
extras = creation_args.get("extras", None)
creation_args["version"] = version
creation_args["req"] = first(requirements.parse("{0}{1}".format(name, version)))
req = init_requirement("{0}{1}".format(name, version))
if extras:
req.extras += tuple(extras,)
creation_args["req"] = req
return cls(**creation_args)
@property
def line_part(self):
return "{self.name}".format(self=self)
return "{0}".format(canonicalize_name(self.name))
@property
def pipfile_part(self):
@@ -243,17 +244,17 @@ class FileRequirement(BaseRequirement):
and self.setup_path
and self.setup_path.exists()
):
from distutils.core import run_setup
from setuptools.dist import distutils
old_curdir = os.path.abspath(os.getcwd())
try:
os.chdir(str(self.setup_path.parent))
dist = run_setup(self.setup_path.as_posix(), stop_after="init")
dist = distutils.core.run_setup(self.setup_path.as_posix())
name = dist.get_name()
except (FileNotFoundError, IOError) as e:
dist = None
except Exception as e:
from .._compat import InstallRequirement, make_abstract_dist
from pip_shims.shims import InstallRequirement, make_abstract_dist
try:
if not isinstance(Path, self.path):
@@ -289,14 +290,18 @@ class FileRequirement(BaseRequirement):
@req.default
def get_requirement(self):
prefix = "-e " if self.editable else ""
line = "{0}{1}".format(prefix, self.link.url)
req = first(requirements.parse(line))
req = init_requirement(canonicalize_name(self.name))
req.editable = False
req.line = self.link.url_without_fragment
if self.path and self.link and self.link.scheme.startswith("file"):
req.local_file = True
req.path = self.path
req.uri = None
req.url = None
self._uri_scheme = "file"
else:
req.local_file = False
req.path = None
req.url = self.link.url_without_fragment
if self.editable:
req.editable = True
req.link = self.link
@@ -330,7 +335,7 @@ class FileRequirement(BaseRequirement):
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)]):
if not any([is_installable_file(line), is_valid_url(line), is_file_url(line)]):
raise RequirementError(
"Supplied requirement is not installable: {0!r}".format(line)
)
@@ -473,17 +478,6 @@ class VCSRequirement(FileRequirement):
name = attr.ib()
link = attr.ib()
req = attr.ib()
_INCLUDE_FIELDS = (
"editable",
"uri",
"path",
"vcs",
"ref",
"subdirectory",
"name",
"link",
"req",
)
def __attrs_post_init__(self):
split = urllib_parse.urlsplit(self.uri)
@@ -522,16 +516,66 @@ class VCSRequirement(FileRequirement):
uri = "{0}+{1}".format(self.vcs, uri)
return uri
def get_commit_hash(self, src_dir=None):
src_dir = os.environ.get('SRC_DIR', None) if not src_dir else src_dir
if not src_dir:
_src_dir = TemporaryDirectory()
atexit.register(_src_dir.cleanup)
src_dir = _src_dir.name
checkout_dir = Path(src_dir).joinpath(self.name).as_posix()
vcsrepo = VCSRepository(
url=self.link.url,
name=self.name,
ref=self.ref if self.ref else None,
checkout_directory=checkout_dir,
vcs_type=self.vcs
)
vcsrepo.obtain()
return vcsrepo.get_commit_hash()
def update_repo(self, src_dir=None, ref=None):
src_dir = os.environ.get('SRC_DIR', None) if not src_dir else src_dir
if not src_dir:
_src_dir = TemporaryDirectory()
atexit.register(_src_dir.cleanup)
src_dir = _src_dir.name
checkout_dir = Path(src_dir).joinpath(self.name).as_posix()
ref = self.ref if not ref else ref
vcsrepo = VCSRepository(
url=self.link.url,
name=self.name,
ref=ref if ref else None,
checkout_directory=checkout_dir,
vcs_type=self.vcs
)
if not os.path.exists(checkout_dir):
vcsrepo.obtain()
else:
vcsrepo.update()
return vcsrepo.get_commit_hash()
@req.default
def get_requirement(self):
prefix = "-e " if self.editable else ""
line = "{0}{1}".format(prefix, self.link.url)
req = first(requirements.parse(line))
name = self.name or self.link.egg_fragment
url = self.uri or self.link.url_without_fragment
if not name:
raise ValueError(
"pipenv requires an #egg fragment for version controlled "
"dependencies. Please install remote dependency "
"in the form {0}#egg=<package-name>.".format(url)
)
req = init_requirement(canonicalize_name(self.name))
req.editable = self.editable
req.url = self.uri
req.line = self.link.url
if self.ref:
req.revision = self.ref
if self.extras:
req.extras = self.extras
req.vcs = self.vcs
if self.path and self.link and self.link.scheme.startswith("file"):
req.local_file = True
req.path = self.path
if self.editable:
req.editable = True
req.link = self.link
if (
self.uri != unquote(self.link.url_without_fragment)
@@ -539,19 +583,7 @@ class VCSRequirement(FileRequirement):
and "git+git@" in self.uri
):
req.line = self.uri
req.uri = self.uri
if not req.name:
raise ValueError(
"pipenv requires an #egg fragment for version controlled "
"dependencies. Please install remote dependency "
"in the form {0}#egg=<package-name>.".format(req.uri)
)
if self.vcs and not req.vcs:
req.vcs = self.vcs
if self.ref and not req.revision:
req.revision = self.ref
if self.extras and not req.extras:
req.extras = self.extras
req.url = self.uri
return req
@classmethod
@@ -564,6 +596,10 @@ class VCSRequirement(FileRequirement):
if k in pipfile
]
for key in pipfile_keys:
if key == "extras":
extras = pipfile.get(key, None)
if extras:
pipfile[key] = sorted(dedup([extra.lower() for extra in extras]))
if key in VCS_LIST:
creation_args["vcs"] = key
composed_uri = add_ssh_scheme_to_git_uri(
@@ -612,8 +648,12 @@ class VCSRequirement(FileRequirement):
def line_part(self):
"""requirements.txt compatible line part sans-extras"""
if self.req:
return self.req.line
base = "{0}".format(self.link)
base = self.req.line
if base and self.extras and not extras_to_string(self.extras) in base:
if self.subdirectory:
base = "{0}".format(self.get_link().url)
else:
base = "{0}{1}".format(base, extras_to_string(sorted(self.extras)))
if self.editable:
base = "-e {0}".format(base)
return base
@@ -650,8 +690,8 @@ class Requirement(object):
editable = attr.ib(default=None)
hashes = attr.ib(default=attr.Factory(list), converter=list)
extras = attr.ib(default=attr.Factory(list))
abstract_dep = attr.ib(default=None)
_ireq = None
_INCLUDE_FIELDS = ("name", "markers", "index", "editable", "hashes", "extras")
@name.default
def get_name(self):
@@ -678,14 +718,20 @@ class Requirement(object):
@property
def extras_as_pip(self):
if self.extras:
return "[{0}]".format(",".join(self.extras))
return "[{0}]".format(",".join(sorted([extra.lower() for extra in self.extras])))
return ""
@property
def commit_hash(self):
if not self.is_vcs:
return None
return self.req.get_commit_hash()
@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.specifier)
return
@property
@@ -702,10 +748,15 @@ class Requirement(object):
@property
def normalized_name(self):
return pep423_name(self.name)
return canonicalize_name(self.name)
def copy(self):
return attr.evolve(self)
@classmethod
def from_line(cls, line):
if isinstance(line, InstallRequirement):
line = format_requirement(line)
hashes = None
if "--hash=" in line:
hashes = line.split(" --hash=")
@@ -714,6 +765,7 @@ class Requirement(object):
line = line.split(" ", 1)[1] if editable else line
line, markers = split_markers_from_line(line)
line, extras = _strip_extras(line)
specifiers = ''
if extras:
extras = parse_extras(extras)
line = line.strip('"').strip("'").strip()
@@ -721,9 +773,10 @@ class Requirement(object):
vcs = None
# Installable local files and installable non-vcs urls are handled
# as files, generally speaking
if is_installable_file(line) or (is_valid_url(line) and not is_vcs(line)):
line_is_vcs = is_vcs(line)
if is_installable_file(line) or ((is_file_url(line) or is_valid_url(line)) and not line_is_vcs):
r = FileRequirement.from_line(line_with_prefix)
elif is_vcs(line):
elif line_is_vcs:
r = VCSRequirement.from_line(line_with_prefix, extras=extras)
vcs = r.vcs
elif line == "." and not is_installable_file(line):
@@ -739,6 +792,7 @@ class Requirement(object):
spec_idx = min((line.index(match) for match in spec_matches))
name = line[:spec_idx]
version = line[spec_idx:]
specifiers = version
if not extras:
name, extras = _strip_extras(name)
if extras:
@@ -746,8 +800,19 @@ class Requirement(object):
if version:
name = "{0}{1}".format(name, version)
r = NamedRequirement.from_line(line)
req_markers = None
if markers:
r.req.markers = markers
req_markers = PackagingRequirement("fakepkg; {0}".format(markers))
r.req.marker = getattr(req_markers, "marker", None)
r.req.local_file = getattr(r.req, "local_file", False)
name = getattr(r.req, "name", None)
if not name:
name = getattr(r.req, "project_name", None)
r.req.name = name
if not name:
name = getattr(r.req, "key", None)
if name:
r.req.name = name
args = {
"name": r.name,
"vcs": vcs,
@@ -756,15 +821,26 @@ class Requirement(object):
"editable": editable,
}
if extras:
extras = sorted(dedup([extra.lower() for extra in extras]))
args["extras"] = extras
r.req.extras = extras
r.extras = extras
elif r.extras:
args["extras"] = r.extras
args["extras"] = sorted(dedup([extra.lower() for extra in r.extras]))
if hashes:
args["hashes"] = hashes
return cls(**args)
@classmethod
def from_ireq(cls, ireq):
return cls.from_line(format_requirement(ireq))
@classmethod
def from_metadata(cls, name, version, extras, markers):
return cls.from_ireq(make_install_requirement(
name, version, extras=extras, markers=markers,
))
@classmethod
def from_pipfile(cls, name, pipfile):
_pipfile = {}
@@ -780,8 +856,14 @@ class Requirement(object):
else:
r = NamedRequirement.from_pipfile(name, pipfile)
markers = PipenvMarkers.from_pipfile(name, _pipfile)
req_markers = None
if markers:
markers = str(markers)
req_markers = PackagingRequirement("fakepkg; {0}".format(markers))
r.req.marker = getattr(req_markers, "marker", None)
r.req.specifier = SpecifierSet(_pipfile["version"])
extras = _pipfile.get("extras")
r.req.extras = sorted(dedup([extra.lower() for extra in extras])) if extras else []
args = {
"name": r.name,
"vcs": vcs,
@@ -793,9 +875,12 @@ class Requirement(object):
}
if any(key in _pipfile for key in ["hash", "hashes"]):
args["hashes"] = _pipfile.get("hashes", [pipfile.get("hash")])
return cls(**args)
cls_inst = cls(**args)
if cls_inst.is_named:
cls_inst.req.req.line = cls_inst.as_line()
return cls_inst
def as_line(self, sources=None):
def as_line(self, sources=None, include_hashes=True, include_extras=True):
"""Format this requirement as a line in requirements.txt.
If `sources` provided, it should be an sequence of mappings, containing
@@ -804,22 +889,52 @@ class Requirement(object):
If `sources` is omitted or falsy, no index information will be included
in the requirement line.
"""
line = "{0}{1}{2}{3}{4}".format(
if self.is_vcs:
include_extras = False
parts = [
self.req.line_part,
self.extras_as_pip if not self.is_vcs else "",
self.extras_as_pip if include_extras else "",
self.specifiers if self.specifiers else "",
self.markers_as_pip,
self.hashes_as_pip,
)
]
if include_hashes:
parts.append(self.hashes_as_pip)
if sources and not (self.requirement.local_file or self.vcs):
from ..utils import prepare_pip_source_args
if self.index:
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)
parts.extend([" ", index_string])
line = "".join(parts)
return line
def get_markers(self):
markers = self.markers
if markers:
fake_pkg = PackagingRequirement('fakepkg; {0}'.format(markers))
markers = fake_pkg.markers
return markers
def get_specifier(self):
return Specifier(self.specifiers)
def get_version(self):
return parse_version(self.get_specifier().version)
def get_requirement(self):
req_line = self.req.req.line
if req_line.startswith('-e '):
_, req_line = req_line.split(" ", 1)
req = init_requirement(self.name)
req.line = req_line
req.specifier = SpecifierSet(self.specifiers if self.specifiers else '')
if self.is_vcs or self.is_file_or_url:
req.url = self.req.link.url_without_fragment
req.marker = self.get_markers()
req.extras = set(self.extras) if self.extras else set()
return req
@property
def constraint_line(self):
return self.as_line()
@@ -839,6 +954,8 @@ class Requirement(object):
if k in good_keys
}
name = self.name
if 'markers' in req_dict and req_dict['markers']:
req_dict['markers'] = req_dict['markers'].replace('"', "'")
base_dict = {
k: v
for k, v in self.req.pipfile_part[name].items()
@@ -850,23 +967,103 @@ class Requirement(object):
conflicts = [k for k in (conflicting_keys[1:],) if k in base_dict]
for k in conflicts:
base_dict.pop(k)
if "hashes" in base_dict and len(base_dict["hashes"]) == 1:
base_dict["hash"] = base_dict.pop("hashes")[0]
if "hashes" in base_dict:
_hashes = base_dict.pop("hashes")
hashes = []
for _hash in _hashes:
try:
hashes.append(_hash.as_line())
except AttributeError:
hashes.append(_hash)
base_dict["hashes"] = sorted(hashes)
if len(base_dict.keys()) == 1 and "version" in base_dict:
base_dict = base_dict.get("version")
return {name: base_dict}
def as_ireq(self):
ireq_line = self.as_line(include_hashes=False)
if self.editable or self.req.editable:
if ireq_line.startswith("-e "):
ireq_line = ireq_line[len("-e "):]
ireq = InstallRequirement.from_editable(ireq_line)
else:
ireq = InstallRequirement.from_line(ireq_line)
if not getattr(ireq, "req", None):
ireq.req = self.req.req
else:
ireq.req.extras = self.req.req.extras
ireq.req.marker = self.req.req.marker
return ireq
@property
def pipfile_entry(self):
return self.as_pipfile().copy().popitem()
@property
def ireq(self):
if not self._ireq:
ireq_line = self.as_line()
if ireq_line.startswith("-e "):
ireq_line = ireq_line[len("-e ") :]
self._ireq = InstallRequirement.from_editable(ireq_line)
else:
self._ireq = InstallRequirement.from_line(ireq_line)
return self._ireq
return self.as_ireq()
def get_dependencies(self, sources=None):
"""Retrieve the dependencies of the current requirement.
Retrieves dependencies of the current requirement. This only works on pinned
requirements.
:param sources: Pipfile-formatted sources, defaults to None
:param sources: list[dict], optional
:return: A set of requirement strings of the dependencies of this requirement.
:rtype: set(str)
"""
if not sources:
sources = [{
'name': 'pypi',
'url': 'https://pypi.org/simple',
'verify_ssl': True,
}]
return get_dependencies(self.as_ireq(), sources=sources)
def get_abstract_dependencies(self, sources=None):
"""Retrieve the abstract dependencies of this requirement.
Returns the abstract dependencies of the current requirement in order to resolve.
:param sources: A list of sources (pipfile format), defaults to None
:param sources: list, optional
:return: A list of abstract (unpinned) dependencies
:rtype: list[ :class:`~requirementslib.models.dependency.AbstractDependency` ]
"""
if not self.abstract_dep:
parent = getattr(self, 'parent', None)
self.abstract_dep = AbstractDependency.from_requirement(self, parent=parent)
if not sources:
sources = [{'url': 'https://pypi.org/simple', 'name': 'pypi', 'verify_ssl': True},]
if is_pinned_requirement(self.ireq):
deps = self.get_dependencies()
else:
ireq = sorted(self.find_all_matches(), key=lambda k: k.version)
deps = get_dependencies(ireq.pop(), sources=sources)
return get_abstract_dependencies(deps, sources=sources, parent=self.abstract_dep)
def find_all_matches(self, sources=None, finder=None):
"""Find all matching candidates for the current requirement.
Consults a finder to find all matching candidates.
:param sources: Pipfile-formatted sources, defaults to None
:param sources: list[dict], optional
:return: A list of Installation Candidates
:rtype: list[ :class:`~pip._internal.index.InstallationCandidate` ]
"""
if not finder:
finder = get_finder(sources=sources)
return find_all_matches(finder, self.as_ireq())
def merge_markers(self, markers):
if not isinstance(markers, Marker):
markers = Marker(markers)
_markers = set(Marker(self.ireq.markers)) if self.ireq.markers else set(markers)
_markers.add(markers)
new_markers = Marker(" or ".join([str(m) for m in sorted(_markers)]))
self.markers = str(new_markers)
self.req.req.marker = new_markers
+239
View File
@@ -0,0 +1,239 @@
# -*- coding=utf-8 -*-
from contextlib import contextmanager
import attr
import six
from pip_shims.shims import VcsSupport, Wheel
from ..utils import log
from .cache import HashCache
from .dependencies import AbstractDependency, find_all_matches, get_finder
from .utils import format_requirement, is_pinned_requirement, version_from_ireq
class ResolutionError(Exception):
pass
@attr.s
class DependencyResolver(object):
pinned_deps = attr.ib(default=attr.Factory(dict))
#: A dictionary of abstract dependencies by name
dep_dict = attr.ib(default=attr.Factory(dict))
#: A dictionary of sets of version numbers that are valid for a candidate currently
candidate_dict = attr.ib(default=attr.Factory(dict))
#: A historical record of pins
pin_history = attr.ib(default=attr.Factory(dict))
#: Whether to allow prerelease dependencies
allow_prereleases = attr.ib(default=False)
#: Stores hashes for each dependency
hashes = attr.ib(default=attr.Factory(dict))
#: A hash cache
hash_cache = attr.ib(default=attr.Factory(HashCache))
#: A finder for searching the index
finder = attr.ib(default=None)
#: Whether to include hashes even from incompatible wheels
include_incompatible_hashes = attr.ib(default=True)
#: A cache for storing available canddiates when using all wheels
_available_candidates_cache = attr.ib(default=attr.Factory(dict))
@classmethod
def create(cls, finder=None, allow_prereleases=False, get_all_hashes=True):
if not finder:
finder_args = []
if allow_prereleases:
finder_args.append('--pre')
finder = get_finder(*finder_args)
creation_kwargs = {
'allow_prereleases': allow_prereleases,
'include_incompatible_hashes': get_all_hashes,
'finder': finder,
'hash_cache': HashCache(),
}
resolver = cls(**creation_kwargs)
return resolver
@property
def dependencies(self):
return list(self.dep_dict.values())
@property
def resolution(self):
return list(self.pinned_deps.values())
def add_abstract_dep(self, dep):
"""Add an abstract dependency by either creating a new entry or
merging with an old one.
:param dep: An abstract dependency to add
:type dep: :class:`~requirementslib.models.dependency.AbstractDependency`
:raises ResolutionError: Raised when the given dependency is not compatible with
an existing abstract dependency.
"""
if dep.name in self.dep_dict:
compatible_versions = self.dep_dict[dep.name].compatible_versions(dep)
if compatible_versions:
self.candidate_dict[dep.name] = compatible_versions
self.dep_dict[dep.name] = self.dep_dict[
dep.name
].compatible_abstract_dep(dep)
else:
raise ResolutionError
else:
self.candidate_dict[dep.name] = dep.version_set
self.dep_dict[dep.name] = dep
def pin_deps(self):
"""Pins the current abstract dependencies and adds them to the history dict.
Adds any new dependencies to the abstract dependencies already present by
merging them together to form new, compatible abstract dependencies.
"""
for name in list(self.dep_dict.keys()):
candidates = self.dep_dict[name].candidates[:]
abs_dep = self.dep_dict[name]
while candidates:
pin = candidates.pop()
# Move on from existing pins if the new pin isn't compatible
if name in self.pinned_deps:
if self.pinned_deps[name].editable:
continue
old_version = version_from_ireq(self.pinned_deps[name])
if not pin.editable:
new_version = version_from_ireq(pin)
if (new_version != old_version and
new_version not in self.candidate_dict[name]):
continue
pin.parent = abs_dep.parent
pin_subdeps = self.dep_dict[name].get_deps(pin)
backup = self.dep_dict.copy(), self.candidate_dict.copy()
try:
for pin_dep in pin_subdeps:
self.add_abstract_dep(pin_dep)
except ResolutionError:
self.dep_dict, self.candidate_dict = backup
continue
else:
self.pinned_deps[name] = pin
break
def resolve(self, root_nodes, max_rounds=20):
"""Resolves dependencies using a backtracking resolver and multiple endpoints.
Note: this resolver caches aggressively.
Runs for *max_rounds* or until any two pinning rounds yield the same outcome.
:param root_nodes: A list of the root requirements.
:type root_nodes: list[:class:`~requirementslib.models.requirements.Requirement`]
:param max_rounds: The max number of resolution rounds, defaults to 20
:param max_rounds: int, optional
:raises RuntimeError: Raised when max rounds is exceeded without a resolution.
"""
if self.dep_dict:
raise RuntimeError("Do not use the same resolver more than once")
if not self.hash_cache:
self.hash_cache = HashCache()
# Coerce input into AbstractDependency instances.
# We accept str, Requirement, and AbstractDependency as input.
for dep in root_nodes:
if isinstance(dep, six.string_types):
dep = AbstractDependency.from_string(dep)
elif not isinstance(dep, AbstractDependency):
dep = AbstractDependency.from_requirement(dep)
self.add_abstract_dep(dep)
for round_ in range(max_rounds):
self.pin_deps()
self.pin_history[round_] = self.pinned_deps.copy()
if round_ > 0:
previous_round = set(self.pin_history[round_ - 1].values())
current_values = set(self.pin_history[round_].values())
difference = current_values - previous_round
else:
difference = set(self.pin_history[round_].values())
log.debug("\n")
log.debug("{:=^30}".format(" Round {0} ".format(round_)))
log.debug("\n")
if difference:
log.debug("New Packages: ")
for d in difference:
log.debug("{:>30}".format(format_requirement(d)))
elif round_ >= 3:
log.debug("Stable Pins: ")
for d in current_values:
log.debug("{:>30}".format(format_requirement(d)))
return
else:
log.debug("No New Packages.")
# TODO: Raise a better error.
raise RuntimeError("cannot resolve after {} rounds".format(max_rounds))
def get_hashes(self):
for dep in self.pinned_deps.values():
if dep.name not in self.hashes:
self.hashes[dep.name] = self.get_hashes_for_one(dep)
return self.hashes.copy()
def get_hashes_for_one(self, ireq):
if not self.finder:
finder_args = []
if self.allow_prereleases:
finder_args.append('--pre')
self.finder = get_finder(*finder_args)
if ireq.editable:
return set()
vcs = VcsSupport()
if ireq.link and ireq.link.scheme in vcs.all_schemes and 'ssh' in ireq.link.scheme:
return set()
if not is_pinned_requirement(ireq):
raise TypeError(
"Expected pinned requirement, got {}".format(ireq))
matching_candidates = set()
with self.allow_all_wheels():
matching_candidates = (
find_all_matches(self.finder, ireq, pre=self.allow_prereleases)
)
return {
self.hash_cache.get_hash(candidate.location)
for candidate in matching_candidates
}
@contextmanager
def allow_all_wheels(self):
"""
Monkey patches pip.Wheel to allow wheels from all platforms and Python versions.
This also saves the candidate cache and set a new one, or else the results from the
previous non-patched calls will interfere.
"""
def _wheel_supported(self, tags=None):
# Ignore current platform. Support everything.
return True
def _wheel_support_index_min(self, tags=None):
# All wheels are equal priority for sorting.
return 0
original_wheel_supported = Wheel.supported
original_support_index_min = Wheel.support_index_min
Wheel.supported = _wheel_supported
Wheel.support_index_min = _wheel_support_index_min
try:
yield
finally:
Wheel.supported = original_wheel_supported
Wheel.support_index_min = original_support_index_min
+337 -13
View File
@@ -1,10 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import os
import sys
from collections import defaultdict
from itertools import chain, groupby
from operator import attrgetter
import six
from attr import validators
from first import first
from .._compat import Link
from packaging.markers import InvalidMarker, Marker, Op, Value, Variable
from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet
from packaging.version import parse as parse_version
from packaging.requirements import Requirement as PackagingRequirement
from pkg_resources import Requirement
from vistir.misc import dedup
from pip_shims.shims import InstallRequirement, Link
from ..utils import SCHEME_LIST, VCS_LIST, is_star
@@ -21,6 +37,15 @@ def optional_instance_of(cls):
return validators.optional(validators.instance_of(cls))
def init_requirement(name):
req = Requirement.parse(name)
req.vcs = None
req.local_file = None
req.revision = None
req.path = None
return req
def extras_to_string(extras):
"""Turn a list of extras into a string"""
if isinstance(extras, six.string_types):
@@ -29,16 +54,13 @@ def extras_to_string(extras):
else:
extras = [extras]
return "[{0}]".format(",".join(extras))
return "[{0}]".format(",".join(sorted(extras)))
def parse_extras(extras_str):
"""Turn a string of extras into a parsed extras list"""
import requirements
extras = first(
requirements.parse("fakepkg{0}".format(extras_to_string(extras_str)))
).extras
return extras
extras = Requirement.parse("fakepkg{0}".format(extras_to_string(extras_str))).extras
return sorted(dedup([extra.lower() for extra in extras]))
def specs_to_string(specs):
@@ -46,7 +68,11 @@ def specs_to_string(specs):
if specs:
if isinstance(specs, six.string_types):
return specs
return ",".join(["".join(spec) for spec in specs])
try:
extras = ",".join(["".join(spec) for spec in specs])
except TypeError:
extras = ",".join(["".join(spec._spec) for spec in specs])
return extras
return ""
@@ -91,7 +117,7 @@ def strip_ssh_from_git_uri(uri):
def add_ssh_scheme_to_git_uri(uri):
"""Cleans VCS uris from pip format"""
"""Cleans VCS uris from pipenv.patched.notpip 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:
@@ -101,7 +127,6 @@ def add_ssh_scheme_to_git_uri(uri):
def split_markers_from_line(line):
"""Split markers from a dependency"""
from packaging.markers import Marker, InvalidMarker
if not any(line.startswith(uri_prefix) for uri_prefix in SCHEME_LIST):
marker_sep = ";"
else:
@@ -133,7 +158,6 @@ def validate_path(instance, attr_, value):
def validate_markers(instance, attr_, value):
from packaging.markers import Marker, InvalidMarker
try:
Marker("{0}{1}".format(attr_.name, value))
except InvalidMarker:
@@ -141,11 +165,311 @@ def validate_markers(instance, attr_, value):
def validate_specifiers(instance, attr_, value):
from packaging.specifiers import SpecifierSet, InvalidSpecifier
from packaging.markers import InvalidMarker
if value == "":
return True
try:
SpecifierSet(value)
except (InvalidMarker, InvalidSpecifier):
raise ValueError("Invalid Specifiers {0}".format(value))
def key_from_ireq(ireq):
"""Get a standardized key for an InstallRequirement."""
if ireq.req is None and ireq.link is not None:
return str(ireq.link)
else:
return key_from_req(ireq.req)
def key_from_req(req):
"""Get an all-lowercase version of the requirement's name."""
if hasattr(req, 'key'):
# from pkg_resources, such as installed dists for pip-sync
key = req.key
else:
# from packaging, such as install requirements from requirements.txt
key = req.name
key = key.replace('_', '-').lower()
return key
def _requirement_to_str_lowercase_name(requirement):
"""
Formats a packaging.requirements.Requirement with a lowercase name.
This is simply a copy of
https://github.com/pypa/packaging/blob/16.8/packaging/requirements.py#L109-L124
modified to lowercase the dependency name.
Previously, we were invoking the original Requirement.__str__ method and
lowercasing the entire result, which would lowercase the name, *and* other,
important stuff that should not be lowercased (such as the marker). See
this issue for more information: https://github.com/pypa/pipenv/issues/2113.
"""
parts = [requirement.name.lower()]
if requirement.extras:
parts.append("[{0}]".format(",".join(sorted(requirement.extras))))
if requirement.specifier:
parts.append(str(requirement.specifier))
if requirement.url:
parts.append("@ {0}".format(requirement.url))
if requirement.marker:
parts.append("; {0}".format(requirement.marker))
return "".join(parts)
def format_requirement(ireq):
"""
Generic formatter for pretty printing InstallRequirements to the terminal
in a less verbose way than using its `__str__` method.
"""
if ireq.editable:
line = '-e {}'.format(ireq.link)
else:
line = _requirement_to_str_lowercase_name(ireq.req)
if str(ireq.req.marker) != str(ireq.markers):
if not ireq.req.marker:
line = '{}; {}'.format(line, ireq.markers)
else:
name, markers = line.split(";", 1)
markers = markers.strip()
line = '{}; ({}) and ({})'.format(name, markers, ireq.markers)
return line
def format_specifier(ireq):
"""
Generic formatter for pretty printing the specifier part of
InstallRequirements to the terminal.
"""
# TODO: Ideally, this is carried over to the pip library itself
specs = ireq.specifier._specs if ireq.req is not None else []
specs = sorted(specs, key=lambda x: x._spec[1])
return ','.join(str(s) for s in specs) or '<any>'
def is_pinned_requirement(ireq):
"""
Returns whether an InstallRequirement is a "pinned" requirement.
An InstallRequirement is considered pinned if:
- Is not editable
- It has exactly one specifier
- That specifier is "=="
- The version does not contain a wildcard
Examples:
django==1.8 # pinned
django>1.8 # NOT pinned
django~=1.8 # NOT pinned
django==1.* # NOT pinned
"""
if ireq.editable:
return False
specifier = getattr(ireq, "specifier", None)
if not specifier:
return False
if len(specifier._specs) != 1:
return False
op, version = first(specifier._specs)._spec
return (op == '==' or op == '===') and not version.endswith('.*')
def as_tuple(ireq):
"""
Pulls out the (name: str, version:str, extras:(str)) tuple from the pinned InstallRequirement.
"""
if not is_pinned_requirement(ireq):
raise TypeError('Expected a pinned InstallRequirement, got {}'.format(ireq))
name = key_from_req(ireq.req)
version = first(ireq.specifier._specs)._spec[1]
extras = tuple(sorted(ireq.extras))
return name, version, extras
def full_groupby(iterable, key=None):
"""Like groupby(), but sorts the input on the group key first."""
return groupby(sorted(iterable, key=key), key=key)
def flat_map(fn, collection):
"""Map a function over a collection and flatten the result by one-level"""
return chain.from_iterable(map(fn, collection))
def lookup_table(values, key=None, keyval=None, unique=False, use_lists=False):
"""
Builds a dict-based lookup table (index) elegantly.
Supports building normal and unique lookup tables. For example:
>>> assert lookup_table(
... ['foo', 'bar', 'baz', 'qux', 'quux'], lambda s: s[0]) == {
... 'b': {'bar', 'baz'},
... 'f': {'foo'},
... 'q': {'quux', 'qux'}
... }
For key functions that uniquely identify values, set unique=True:
>>> assert lookup_table(
... ['foo', 'bar', 'baz', 'qux', 'quux'], lambda s: s[0],
... unique=True) == {
... 'b': 'baz',
... 'f': 'foo',
... 'q': 'quux'
... }
The values of the resulting lookup table will be values, not sets.
For extra power, you can even change the values while building up the LUT.
To do so, use the `keyval` function instead of the `key` arg:
>>> assert lookup_table(
... ['foo', 'bar', 'baz', 'qux', 'quux'],
... keyval=lambda s: (s[0], s[1:])) == {
... 'b': {'ar', 'az'},
... 'f': {'oo'},
... 'q': {'uux', 'ux'}
... }
"""
if keyval is None:
if key is None:
keyval = (lambda v: v)
else:
keyval = (lambda v: (key(v), v))
if unique:
return dict(keyval(v) for v in values)
lut = {}
for value in values:
k, v = keyval(value)
try:
s = lut[k]
except KeyError:
if use_lists:
s = lut[k] = list()
else:
s = lut[k] = set()
if use_lists:
s.append(v)
else:
s.add(v)
return dict(lut)
def name_from_req(req):
"""Get the name of the requirement"""
if hasattr(req, 'project_name'):
# from pkg_resources, such as installed dists for pip-sync
return req.project_name
else:
# from packaging, such as install requirements from requirements.txt
return req.name
def make_install_requirement(name, version, extras, markers, constraint=False):
"""make_install_requirement Generates an :class:`~pip._internal.req.req_install.InstallRequirement`.
Create an InstallRequirement from the supplied metadata.
:param name: The requirement's name.
:type name: str
:param version: The requirement version (must be pinned).
:type version: str.
:param extras: The desired extras.
:type extras: list[str]
:param markers: The desired markers, without a preceding semicolon.
:type markers: str
:param constraint: Whether to flag the requirement as a constraint, defaults to False.
:param constraint: bool, optional
:return: A generated InstallRequirement
:rtype: :class:`~pip._internal.req.req_install.InstallRequirement`
"""
# If no extras are specified, the extras string is blank
extras_string = ""
if extras:
# Sort extras for stability
extras_string = "[{}]".format(",".join(sorted(extras)))
if not markers:
return InstallRequirement.from_line(
str('{}{}=={}'.format(name, extras_string, version)),
constraint=constraint)
else:
return InstallRequirement.from_line(
str('{}{}=={}; {}'.format(name, extras_string, version, str(markers))),
constraint=constraint)
def version_from_ireq(ireq):
"""version_from_ireq Extract the version from a supplied :class:`~pip._internal.req.req_install.InstallRequirement`
:param ireq: An InstallRequirement
:type ireq: :class:`~pip._internal.req.req_install.InstallRequirement`
:return: The version of the InstallRequirement.
:rtype: str
"""
return first(ireq.specifier._specs).version
def clean_requires_python(candidates):
"""Get a cleaned list of all the candidates with valid specifiers in the `requires_python` attributes."""
all_candidates = []
sys_version = '.'.join(map(str, sys.version_info[:3]))
py_version = parse_version(os.environ.get('PIP_PYTHON_VERSION', sys_version))
for c in candidates:
from_location = attrgetter("location.requires_python")
requires_python = getattr(c, "requires_python", from_location(c))
if requires_python:
# Old specifications had people setting this to single digits
# which is effectively the same as '>=digit,<digit+1'
if requires_python.isdigit():
requires_python = '>={0},<{1}'.format(requires_python, int(requires_python) + 1)
try:
specifierset = SpecifierSet(requires_python)
except InvalidSpecifier:
continue
else:
if not specifierset.contains(py_version):
continue
all_candidates.append(c)
return all_candidates
def fix_requires_python_marker(requires_python):
marker_str = ''
if any(requires_python.startswith(op) for op in Specifier._operators.keys()):
spec_dict = defaultdict(set)
# We are checking first if we have leading specifier operator
# if not, we can assume we should be doing a == comparison
specifierset = list(SpecifierSet(requires_python))
# for multiple specifiers, the correct way to represent that in
# a specifierset is `Requirement('fakepkg; python_version<"3.0,>=2.6"')`
marker_key = Variable('python_version')
for spec in specifierset:
operator, val = spec._spec
cleaned_val = Value(val).serialize().replace('"', "")
spec_dict[Op(operator).serialize()].add(cleaned_val)
marker_str = ' and '.join([
"{0}{1}'{2}'".format(marker_key.serialize(), op, ','.join(vals))
for op, vals in spec_dict.items()
])
marker_to_add = PackagingRequirement('fakepkg; {0}'.format(marker_str)).marker
return marker_to_add
+48
View File
@@ -0,0 +1,48 @@
# -*- coding=utf-8 -*-
import attr
from pip_shims import VcsSupport
import os
VCS_SUPPORT = VcsSupport()
@attr.s
class VCSRepository(object):
url = attr.ib()
name = attr.ib()
checkout_directory = attr.ib()
vcs_type = attr.ib()
commit_sha = attr.ib(default=None)
ref = attr.ib(default=None)
repo_instance = attr.ib()
@repo_instance.default
def get_repo_instance(self):
backend = VCS_SUPPORT._registry.get(self.vcs_type)
return backend(url=self.url)
def obtain(self):
if not os.path.exists(self.checkout_directory):
self.repo_instance.obtain(self.checkout_directory)
if self.ref:
self.checkout_ref(self.ref)
self.commit_sha = self.get_commit_hash(self.ref)
else:
self.ref = self.repo_instance.default_arg_rev
if not self.commit_sha:
self.commit_sha = self.get_commit_hash()
def checkout_ref(self, ref):
target_rev = self.repo_instance.make_rev_options(ref)
if not self.repo_instance.is_commit_id_equal(
self.checkout_directory, self.get_commit_hash(ref)
) and not self.repo_instance.is_commit_id_equal(self.checkout_directory, ref):
self.repo_instance.switch(self.checkout_directory, self.url, target_rev)
def update(self, ref):
target_rev = self.repo_instance.make_rev_options(ref)
self.repo_instance.update(self.checkout_directory, target_rev)
def get_commit_hash(self, ref=None):
return self.repo_instance.get_revision(self.checkout_directory)
+45 -92
View File
@@ -1,25 +1,28 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import
import logging
import os
import posixpath
import six
from itertools import product
from six.moves.urllib.parse import urlparse, urlsplit
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
from pip_shims import (
Command, VcsSupport, cmdoptions, is_archive_file, is_installable_dir
)
from vistir.compat import Path
from vistir.path import is_valid_url, ensure_mkdir_p
try:
from pathlib import Path
except ImportError:
from pathlib2 import Path
VCS_ACCESS = VcsSupport()
VCS_LIST = ("git", "svn", "hg", "bzr")
VCS_SCHEMES = []
SCHEME_LIST = ("http://", "https://", "ftp://", "ftps://", "file://")
if not VCS_SCHEMES:
VCS_SCHEMES = VcsSupport().all_schemes
def setup_logger():
logger = logging.getLogger("requirementslib")
@@ -40,74 +43,14 @@ 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):
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,
)
if not is_valid_url(pipfile_entry) and pipfile_entry.startswith("git+"):
from .models.utils import add_ssh_scheme_to_git_uri
pipfile_entry = add_ssh_scheme_to_git_uri(pipfile_entry)
parsed_entry = urlsplit(pipfile_entry)
return parsed_entry.scheme in VCS_SCHEMES
return False
def check_for_unc_path(path):
""" Checks to see if a pathlib `Path` object is a unc path or not"""
if (
os.name == "nt"
and len(path.drive) > 2
and not path.drive[0].isalpha()
and path.drive[1] != ":"
):
return True
else:
return False
def get_converted_relative_path(path, relative_to=os.curdir):
"""Convert `path` to be relative.
Given a vague relative path, return the path relative to the given
location.
This performs additional conversion to ensure the result is of POSIX form,
and starts with `./`, or is precisely `.`.
"""
start_path = Path(relative_to)
try:
start = start_path.resolve()
except OSError:
start = start_path.absolute()
# check if there is a drive letter or mount point
# if it is a mountpoint use the original absolute path
# instead of the unc path
if check_for_unc_path(start):
start = start_path.absolute()
path = start.joinpath(path).relative_to(start)
# check and see if the path that was passed into the function is a UNC path
# and raise value error if it is not.
if check_for_unc_path(path):
raise ValueError("The path argument does not currently accept UNC paths")
relpath_s = posixpath.normpath(path.as_posix())
if not (relpath_s == "." or relpath_s.startswith("./")):
relpath_s = posixpath.join(".", relpath_s)
return relpath_s
def multi_split(s, split):
"""Splits on multiple given separators."""
for r in split:
@@ -121,7 +64,6 @@ def is_star(val):
def is_installable_file(path):
"""Determine if a path can potentially be installed"""
from ._compat import is_installable_dir, is_archive_file
from packaging import specifiers
if hasattr(path, "keys") and any(
@@ -160,22 +102,6 @@ def is_installable_file(path):
return False
def is_valid_url(url):
"""Checks if a given string is an url"""
pieces = urlparse(url)
return all([pieces.scheme, any([pieces.netloc, pieces.path])])
def pep423_name(name):
"""Normalize package name to PEP 423 style standard."""
name = name.lower()
if any(i not in name for i in (VCS_LIST + SCHEME_LIST)):
return name.replace("_", "-")
else:
return name
def prepare_pip_source_args(sources, pip_args=None):
if pip_args is None:
pip_args = []
@@ -197,3 +123,30 @@ def prepare_pip_source_args(sources, pip_args=None):
["--trusted-host", urlparse(source["url"]).hostname]
)
return pip_args
class PipCommand(Command):
name = 'PipCommand'
def get_pip_command():
# Use pip's parser for pip.conf management and defaults.
# General options (find_links, index_url, extra_index_url, trusted_host,
# and pre) are defered to pip.
import optparse
pip_command = PipCommand()
pip_command.parser.add_option(cmdoptions.no_binary())
pip_command.parser.add_option(cmdoptions.only_binary())
index_opts = cmdoptions.make_option_group(
cmdoptions.index_group,
pip_command.parser,
)
pip_command.parser.insert_option_group(0, index_opts)
pip_command.parser.add_option(optparse.Option('--pre', action='store_true', default=False))
return pip_command
@ensure_mkdir_p(mode=0o777)
def _ensure_dir(path):
return path