Merge branch 'master' into feature/keep-outdated-peep

This commit is contained in:
Dan Ryan
2018-11-25 16:43:29 -05:00
61 changed files with 873 additions and 602 deletions
+3 -2
View File
@@ -10,5 +10,6 @@ steps:
- bash: |
export GIT_SSL_CAINFO=$(python -m certifi)
export LANG=C.UTF-8
python -m pip install check-manifest
check-manifest
python -m pip install --upgrade setuptools twine readme_renderer[md]
python setup.py sdist
twine check dist/*
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
pip install certifi
export GIT_SSL_CAINFO=$(python -m certifi)
export LANG=C.UTF-8
python -m pip install --upgrade invoke requests parver
python -m pip install --upgrade invoke requests parver bs4 vistir towncrier
python -m invoke vendoring.update
- template: ./run-manifest-check.yml
+12
View File
@@ -13,3 +13,15 @@
[submodule "tests/test_artifacts/git/dateutil"]
path = tests/test_artifacts/git/dateutil
url = https://github.com/dateutil/dateutil
[submodule "tests/test_artifacts/git/pyinstaller"]
path = tests/test_artifacts/git/pyinstaller
url = https://github.com/pyinstaller/pyinstaller.git
[submodule "tests/test_artifacts/git/jinja2"]
path = tests/test_artifacts/git/jinja2
url = https://github.com/pallets/jinja.git
[submodule "tests/test_artifacts/git/flask"]
path = tests/test_artifacts/git/flask
url = https://github.com/pallets/flask.git
[submodule "tests/test_artifacts/git/requests-2.18.4"]
path = tests/test_artifacts/git/requests-2.18.4
url = https://github.com/requests/requests
+1
View File
@@ -21,6 +21,7 @@ jedi = "*"
isort = "*"
rope = "*"
passa = {editable = true, git = "https://github.com/sarugaku/passa.git"}
bs4 = "*"
[packages]
Generated
+30 -14
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "6acc712d82698e574727d19b22d05bf46565ecaa414e288fd0d79e385f8fdd10"
"sha256": "f0a57ba6f180e7312f799a460a9d2790c9c26c3556eff2b5d1fe7d9b9174d05b"
},
"pipfile-spec": 6,
"requires": {},
@@ -72,6 +72,14 @@
],
"version": "==2.6.0"
},
"beautifulsoup4": {
"hashes": [
"sha256:194ec62a25438adcb3fdb06378b26559eda1ea8a747367d34c33cef9c7f48d57",
"sha256:90f8e61121d6ae58362ce3bed8cd997efb00c914eae0ff3d363c32f9a9822d10",
"sha256:f0abd31228055d698bb392a826528ea08ebb9959e6bea17c606fd9c9009db938"
],
"version": "==4.6.3"
},
"black": {
"hashes": [
"sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739",
@@ -88,6 +96,13 @@
],
"version": "==3.0.2"
},
"bs4": {
"hashes": [
"sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"
],
"index": "pypi",
"version": "==0.0.1"
},
"cerberus": {
"hashes": [
"sha256:f5c2e048fb15ecb3c088d192164316093fcfa602a74b3386eefb2983aa7e800a"
@@ -595,10 +610,10 @@
},
"requirementslib": {
"hashes": [
"sha256:441a5bfa487d3f3f5fd5d81c27071d9fd36bb385f538b3a87d20556a80b76f76",
"sha256:89e1e02ff0b52ce9c610124eb990ae706e0aee08beef8c718e7b87e470cdceeb"
"sha256:c2c00c7bd3bd4984c97d10cd4d143efbe33b5ed9e55961bea30ca7a9a4927289",
"sha256:dc6b692e8dee03d6e90c29db1e337b0bf8152cce84a57f0fb4765e596afde4e0"
],
"version": "==1.3.1.post1"
"version": "==1.3.3"
},
"resolvelib": {
"hashes": [
@@ -675,10 +690,10 @@
},
"tomlkit": {
"hashes": [
"sha256:82a8fbb8d8c6af72e96ba00b9db3e20ef61be6c79082552c9363f4559702258b",
"sha256:a43e0195edc9b3c198cd4b5f0f3d427a395d47c4a76ceba7cc875ed030756c39"
"sha256:d6506342615d051bc961f70bfcfa3d29b6616cc08a3ddfd4bc24196f16fd4ec2",
"sha256:f077456d35303e7908cc233b340f71e0bec96f63429997f38ca9272b7d64029e"
],
"version": "==0.5.2"
"version": "==0.5.3"
},
"towncrier": {
"editable": true,
@@ -734,14 +749,15 @@
"spinner"
],
"hashes": [
"sha256:851bd783f2b85a372e563db741dc689cb9263ce2e067e387facdca0c36b6a6ea",
"sha256:b38ffc8ef83f85d81b4efa4cd31ea3bcd37bdb2bc9e8da9f20a40859bc44b57e"
"sha256:3a1020fb7be000b268af96641ced9ead844b1f75840c41e20e473647688fc630",
"sha256:6d2005ad670f77bd9c9b5415c4e2a4a20dce5b0cf0e0d11598eb463b2e0ebe44"
],
"version": "==0.2.4"
"version": "==0.2.5"
},
"webencodings": {
"hashes": [
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
],
"version": "==0.5.1"
},
@@ -754,10 +770,10 @@
},
"wheel": {
"hashes": [
"sha256:196c9842d79262bb66fcf59faa4bd0deb27da911dbc7c6cdca931080eb1f0783",
"sha256:c93e2d711f5f9841e17f53b0e6c0ff85593f3b416b6eec7a9452041a59a42688"
"sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6",
"sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44"
],
"version": "==0.32.2"
"version": "==0.32.3"
},
"yaspin": {
"hashes": [
+1
View File
@@ -0,0 +1 @@
``pipenv install`` will now unset the ``PYTHONHOME`` environment variable when not combined with ``--system``.
+1
View File
@@ -0,0 +1 @@
Fixed an issue which prevented variables from the environment, such as ``PIPENV_DEV`` or ``PIPENV_SYSTEM``, from being parsed and implemented correctly.
+4
View File
@@ -0,0 +1,4 @@
Update vendored dependencies to resolve resolution output parsing and python finding:
- `pythonfinder 1.1.9 -> 1.1.10`
- `requirementslib 1.3.1 -> 1.3.3`
- `vistir 0.2.3 -> 0.2.5`
+1
View File
@@ -0,0 +1 @@
Clear pythonfinder cache after Python install
+1
View File
@@ -0,0 +1 @@
Fixed a race condition in hash resolution for dependencies for certain dependencies with missing cache entries or fresh Pipenv installs.
+1
View File
@@ -0,0 +1 @@
Pipenv will now respect top-level pins over VCS dependency locks.
+1 -1
View File
@@ -1,4 +1,4 @@
from .cli import cli
if __name__ == "__main__":
cli(auto_envvar_prefix="PIPENV")
cli()
+8 -277
View File
@@ -13,31 +13,7 @@ import six
import sys
import warnings
import vistir
from tempfile import _bin_openflags, gettempdir, _mkstemp_inner, mkdtemp
try:
from tempfile import _infer_return_type
except ImportError:
def _infer_return_type(*args):
_types = set()
for arg in args:
if isinstance(type(arg), six.string_types):
_types.add(str)
elif isinstance(type(arg), bytes):
_types.add(bytes)
elif arg:
_types.add(type(arg))
return _types.pop()
if sys.version_info[:2] >= (3, 5):
try:
from pathlib import Path
except ImportError:
from .vendor.pathlib2 import Path
else:
from .vendor.pathlib2 import Path
from .vendor.vistir.compat import NamedTemporaryFile, Path, ResourceWarning, TemporaryDirectory
# Backport required for earlier versions of Python.
if sys.version_info < (3, 3):
@@ -45,257 +21,14 @@ if sys.version_info < (3, 3):
else:
from shutil import get_terminal_size
try:
from weakref import finalize
except ImportError:
try:
from .vendor.backports.weakref import finalize
except ImportError:
class finalize(object):
def __init__(self, *args, **kwargs):
from .utils import logging
logging.warn("weakref.finalize unavailable, not cleaning...")
def detach(self):
return False
from vistir.compat import ResourceWarning
warnings.filterwarnings("ignore", category=ResourceWarning)
class TemporaryDirectory(object):
"""
Create and return a temporary directory. This has the same
behavior as mkdtemp but can be used as a context manager. For
example:
with TemporaryDirectory() as tmpdir:
...
Upon exiting the context, the directory and everything contained
in it are removed.
"""
def __init__(self, suffix="", prefix="", dir=None):
if "RAM_DISK" in os.environ:
import uuid
name = uuid.uuid4().hex
dir_name = os.path.join(os.environ["RAM_DISK"].strip(), name)
os.mkdir(dir_name)
self.name = dir_name
else:
self.name = mkdtemp(suffix, prefix, dir)
self._finalizer = finalize(
self,
self._cleanup,
self.name,
warn_message="Implicitly cleaning up {!r}".format(self),
)
@classmethod
def _cleanup(cls, name, warn_message):
vistir.path.rmtree(name)
warnings.warn(warn_message, ResourceWarning)
def __repr__(self):
return "<{} {!r}>".format(self.__class__.__name__, self.name)
def __enter__(self):
return self
def __exit__(self, exc, value, tb):
self.cleanup()
def cleanup(self):
if self._finalizer.detach():
vistir.path.rmtree(self.name)
def _sanitize_params(prefix, suffix, dir):
"""Common parameter processing for most APIs in this module."""
output_type = _infer_return_type(prefix, suffix, dir)
if suffix is None:
suffix = output_type()
if prefix is None:
if output_type is str:
prefix = "tmp"
else:
prefix = os.fsencode("tmp")
if dir is None:
if output_type is str:
dir = gettempdir()
else:
dir = os.fsencode(gettempdir())
return prefix, suffix, dir, output_type
class _TemporaryFileCloser:
"""
A separate object allowing proper closing of a temporary file's
underlying file object, without adding a __del__ method to the
temporary file.
"""
file = None # Set here since __del__ checks it
close_called = False
def __init__(self, file, name, delete=True):
self.file = file
self.name = name
self.delete = delete
# NT provides delete-on-close as a primitive, so we don't need
# the wrapper to do anything special. We still use it so that
# file.name is useful (i.e. not "(fdopen)") with NamedTemporaryFile.
if os.name != "nt":
# Cache the unlinker so we don't get spurious errors at
# shutdown when the module-level "os" is None'd out. Note
# that this must be referenced as self.unlink, because the
# name TemporaryFileWrapper may also get None'd out before
# __del__ is called.
def close(self, unlink=os.unlink):
if not self.close_called and self.file is not None:
self.close_called = True
try:
self.file.close()
finally:
if self.delete:
unlink(self.name)
# Need to ensure the file is deleted on __del__
def __del__(self):
self.close()
else:
def close(self):
if not self.close_called:
self.close_called = True
self.file.close()
class _TemporaryFileWrapper:
"""
Temporary file wrapper
This class provides a wrapper around files opened for
temporary use. In particular, it seeks to automatically
remove the file when it is no longer needed.
"""
def __init__(self, file, name, delete=True):
self.file = file
self.name = name
self.delete = delete
self._closer = _TemporaryFileCloser(file, name, delete)
def __getattr__(self, name):
# Attribute lookups are delegated to the underlying file
# and cached for non-numeric results
# (i.e. methods are cached, closed and friends are not)
file = self.__dict__["file"]
a = getattr(file, name)
if hasattr(a, "__call__"):
func = a
@functools.wraps(func)
def func_wrapper(*args, **kwargs):
return func(*args, **kwargs)
# Avoid closing the file as long as the wrapper is alive,
# see issue #18879.
func_wrapper._closer = self._closer
a = func_wrapper
if not isinstance(a, int):
setattr(self, name, a)
return a
# The underlying __enter__ method returns the wrong object
# (self.file) so override it to return the wrapper
def __enter__(self):
self.file.__enter__()
return self
# Need to trap __exit__ as well to ensure the file gets
# deleted when used in a with statement
def __exit__(self, exc, value, tb):
result = self.file.__exit__(exc, value, tb)
self.close()
return result
def close(self):
"""
Close the temporary file, possibly deleting it.
"""
self._closer.close()
# iter() doesn't use __getattr__ to find the __iter__ method
def __iter__(self):
# Don't return iter(self.file), but yield from it to avoid closing
# file as long as it's being used as iterator (see issue #23700). We
# can't use 'yield from' here because iter(file) returns the file
# object itself, which has a close method, and thus the file would get
# closed when the generator is finalized, due to PEP380 semantics.
for line in self.file:
yield line
def NamedTemporaryFile(
mode="w+b",
buffering=-1,
encoding=None,
newline=None,
suffix=None,
prefix=None,
dir=None,
delete=True,
):
"""
Create and return a temporary file.
Arguments:
'prefix', 'suffix', 'dir' -- as for mkstemp.
'mode' -- the mode argument to io.open (default "w+b").
'buffering' -- the buffer size argument to io.open (default -1).
'encoding' -- the encoding argument to io.open (default None)
'newline' -- the newline argument to io.open (default None)
'delete' -- whether the file is deleted on close (default True).
The file is created as mkstemp() would do it.
Returns an object with a file-like interface; the name of the file
is accessible as its 'name' attribute. The file will be automatically
deleted when it is closed unless the 'delete' argument is set to False.
"""
prefix, suffix, dir, output_type = _sanitize_params(prefix, suffix, dir)
flags = _bin_openflags
# Setting O_TEMPORARY in the flags causes the OS to delete
# the file when it is closed. This is only supported by Windows.
if os.name == "nt" and delete:
flags |= os.O_TEMPORARY
if sys.version_info < (3, 5):
(fd, name) = _mkstemp_inner(dir, prefix, suffix, flags)
else:
(fd, name) = _mkstemp_inner(dir, prefix, suffix, flags, output_type)
try:
file = io.open(
fd, mode, buffering=buffering, newline=newline, encoding=encoding
)
return _TemporaryFileWrapper(file, name, delete)
except BaseException:
os.unlink(name)
os.close(fd)
raise
__all__ = [
"NamedTemporaryFile", "Path", "ResourceWarning", "TemporaryDirectory",
"get_terminal_size", "getpreferredencoding", "DEFAULT_ENCODING", "force_encoding",
"UNICODE_TO_ASCII_TRANSLATION_MAP", "decode_output", "fix_utf8"
]
def getpreferredencoding():
@@ -366,8 +99,8 @@ OUT_ENCODING, ERR_ENCODING = force_encoding()
UNICODE_TO_ASCII_TRANSLATION_MAP = {
8230: u"...",
8211: u"-",
10004: u"x",
10008: u"Ok"
10004: u"OK",
10008: u"x",
}
@@ -384,13 +117,11 @@ def decode_output(output):
output = output.translate(UNICODE_TO_ASCII_TRANSLATION_MAP)
output = output.encode(DEFAULT_ENCODING, "replace")
return vistir.misc.to_text(output, encoding=DEFAULT_ENCODING, errors="replace")
return output
def fix_utf8(text):
if not isinstance(text, six.string_types):
return text
from ._compat import decode_output
try:
text = decode_output(text)
except UnicodeDecodeError:
+36 -21
View File
@@ -27,16 +27,20 @@ from .options import (
# Enable shell completion.
click_completion.init()
subcommand_context = CONTEXT_SETTINGS.copy()
subcommand_context.update({
"ignore_unknown_options": True,
"allow_extra_args": True
})
subcommand_context_no_interspersion = subcommand_context.copy()
subcommand_context_no_interspersion["allow_interspersed_args"] = False
@group(cls=PipenvGroup, invoke_without_command=True, context_settings=CONTEXT_SETTINGS)
@option("--where", is_flag=True, default=False, help="Output project home information.")
@option("--venv", is_flag=True, default=False, help="Output virtualenv information.")
@option(
"--py", is_flag=True, default=False, help="Output Python interpreter information."
)
@option(
"--envs", is_flag=True, default=False, help="Output Environment Variable options."
)
@option("--py", is_flag=True, default=False, help="Output Python interpreter information.")
@option("--envs", is_flag=True, default=False, help="Output Environment Variable options.")
@option("--rm", is_flag=True, default=False, help="Remove the virtualenv.")
@option("--bare", is_flag=True, default=False, help="Minimal output.")
@option(
@@ -211,7 +215,7 @@ def cli(
@cli.command(
short_help="Installs provided packages and adds them to Pipfile, or (if no packages are given), installs all packages from Pipfile.",
context_settings=dict(ignore_unknown_options=True, allow_extra_args=True),
context_settings=subcommand_context,
)
@system_option
@code_option
@@ -253,7 +257,10 @@ def install(
ctx.abort()
@cli.command(short_help="Un-installs a provided package and removes it from Pipfile.")
@cli.command(
short_help="Un-installs a provided package and removes it from Pipfile.",
context_settings=subcommand_context
)
@option("--skip-lock/--lock", is_flag=True, default=False, help="Lock afterwards.")
@option(
"--all-dev",
@@ -296,7 +303,7 @@ def uninstall(
if retcode:
sys.exit(retcode)
@cli.command(short_help="Generates Pipfile.lock.")
@cli.command(short_help="Generates Pipfile.lock.", context_settings=CONTEXT_SETTINGS)
@lock_options
@pass_state
@pass_context
@@ -328,7 +335,7 @@ def lock(
@cli.command(
short_help="Spawns a shell within the virtualenv.",
context_settings=dict(ignore_unknown_options=True, allow_extra_args=True),
context_settings=subcommand_context,
)
@option(
"--fancy",
@@ -387,11 +394,7 @@ def shell(
@cli.command(
add_help_option=False,
short_help="Spawns a command installed into the virtualenv.",
context_settings=dict(
ignore_unknown_options=True,
allow_interspersed_args=False,
allow_extra_args=True,
),
context_settings=subcommand_context_no_interspersion,
)
@common_options
@argument("command")
@@ -408,7 +411,7 @@ def run(state, command, args):
@cli.command(
short_help="Checks for security vulnerabilities and against PEP 508 markers provided in Pipfile.",
context_settings=dict(ignore_unknown_options=True, allow_extra_args=True),
context_settings=subcommand_context
)
@option(
"--unused",
@@ -448,7 +451,7 @@ def check(
)
@cli.command(short_help="Runs lock, then sync.")
@cli.command(short_help="Runs lock, then sync.", context_settings=CONTEXT_SETTINGS)
@option("--bare", is_flag=True, default=False, help="Minimal output.")
@option(
"--outdated", is_flag=True, default=False, help=u"List out-of-date dependencies."
@@ -525,7 +528,10 @@ def update(
)
@cli.command(short_help=u"Displays currently-installed dependency graph information.")
@cli.command(
short_help=u"Displays currently-installed dependency graph information.",
context_settings=CONTEXT_SETTINGS
)
@option("--bare", is_flag=True, default=False, help="Minimal output.")
@option("--json", is_flag=True, default=False, help="Output JSON.")
@option("--json-tree", is_flag=True, default=False, help="Output JSON in nested tree.")
@@ -537,7 +543,10 @@ def graph(bare=False, json=False, json_tree=False, reverse=False):
do_graph(bare=bare, json=json, json_tree=json_tree, reverse=reverse)
@cli.command(short_help="View a given module in your editor.", name="open")
@cli.command(
short_help="View a given module in your editor.", name="open",
context_settings=CONTEXT_SETTINGS
)
@common_options
@argument("module", nargs=1)
@pass_state
@@ -573,7 +582,10 @@ def run_open(state, module, *args, **kwargs):
return 0
@cli.command(short_help="Installs all packages specified in Pipfile.lock.")
@cli.command(
short_help="Installs all packages specified in Pipfile.lock.",
context_settings=CONTEXT_SETTINGS
)
@option("--bare", is_flag=True, default=False, help="Minimal output.")
@sync_options
@pass_state
@@ -606,7 +618,10 @@ def sync(
ctx.abort()
@cli.command(short_help="Uninstalls all packages not specified in Pipfile.lock.")
@cli.command(
short_help="Uninstalls all packages not specified in Pipfile.lock.",
context_settings=CONTEXT_SETTINGS
)
@option("--bare", is_flag=True, default=False, help="Minimal output.")
@option("--dry-run", is_flag=True, default=False, help="Just output unneeded packages.")
@verbose_option
+17 -12
View File
@@ -12,7 +12,10 @@ from .. import environments
from ..utils import is_valid_url
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
CONTEXT_SETTINGS = {
"help_option_names": ["-h", "--help"],
"auto_envvar_prefix": "PIPENV"
}
class PipenvGroup(Group):
@@ -117,7 +120,7 @@ def sequential_option(f):
return value
return option("--sequential", is_flag=True, default=False, expose_value=False,
help="Install dependencies one-at-a-time, instead of concurrently.",
callback=callback, type=click.types.BOOL)(f)
callback=callback, type=click.types.BOOL, show_envvar=True)(f)
def skip_lock_option(f):
@@ -127,7 +130,8 @@ def skip_lock_option(f):
return value
return option("--skip-lock", is_flag=True, default=False, expose_value=False,
help=u"Skip locking mechanisms and use the Pipfile instead during operation.",
envvar="PIPENV_SKIP_LOCK", callback=callback, type=click.types.BOOL)(f)
envvar="PIPENV_SKIP_LOCK", callback=callback, type=click.types.BOOL,
show_envvar=True)(f)
def keep_outdated_option(f):
@@ -137,7 +141,7 @@ def keep_outdated_option(f):
return value
return option("--keep-outdated", is_flag=True, default=False, expose_value=False,
help=u"Keep out-dated dependencies from being updated in Pipfile.lock.",
callback=callback, type=click.types.BOOL)(f)
callback=callback, type=click.types.BOOL, show_envvar=True)(f)
def selective_upgrade_option(f):
@@ -157,7 +161,7 @@ def ignore_pipfile_option(f):
return value
return option("--ignore-pipfile", is_flag=True, default=False, expose_value=False,
help="Ignore Pipfile when installing, using the Pipfile.lock.",
callback=callback, type=click.types.BOOL)(f)
callback=callback, type=click.types.BOOL, show_envvar=True)(f)
def dev_option(f):
@@ -167,7 +171,7 @@ def dev_option(f):
return value
return option("--dev", "-d", is_flag=True, default=False, type=click.types.BOOL,
help="Install both develop and default packages.", callback=callback,
expose_value=False)(f)
expose_value=False, show_envvar=True)(f)
def pre_option(f):
@@ -208,7 +212,7 @@ def python_option(f):
return value
return option("--python", default=False, nargs=1, callback=callback,
help="Specify which version of Python virtualenv should use.",
expose_value=False)(f)
expose_value=False, allow_from_autoenv=False)(f)
def pypi_mirror_option(f):
@@ -238,7 +242,7 @@ def site_packages_option(f):
return value
return option("--site-packages", is_flag=True, default=False, type=click.types.BOOL,
help="Enable site-packages for the virtualenv.", callback=callback,
expose_value=False)(f)
expose_value=False, show_envvar=True)(f)
def clear_option(f):
@@ -248,7 +252,7 @@ def clear_option(f):
return value
return option("--clear", is_flag=True, callback=callback, type=click.types.BOOL,
help="Clears caches (pipenv, pip, and pip-tools).",
expose_value=False)(f)
expose_value=False, show_envvar=True)(f)
def system_option(f):
@@ -258,7 +262,8 @@ def system_option(f):
state.system = value
return value
return option("--system", is_flag=True, default=False, help="System pip management.",
callback=callback, type=click.types.BOOL, expose_value=False)(f)
callback=callback, type=click.types.BOOL, expose_value=False,
show_envvar=True)(f)
def requirementstxt_option(f):
@@ -288,7 +293,7 @@ def code_option(f):
state.installstate.code = value
return value
return option("--code", "-c", nargs=1, default=False, help="Import from codebase.",
callback=callback, expose_value=False)(f)
callback=callback, expose_value=False)(f)
def deploy_option(f):
@@ -298,7 +303,7 @@ def deploy_option(f):
return value
return option("--deploy", is_flag=True, default=False, type=click.types.BOOL,
help=u"Abort if the Pipfile.lock is out-of-date, or Python version is"
" wrong.", callback=callback, expose_value=False)(f)
" wrong.", callback=callback, expose_value=False)(f)
def setup_verbosity(ctx, param, value):
+18 -22
View File
@@ -455,6 +455,11 @@ def ensure_python(three=None, python=None):
sp.ok(environments.PIPENV_SPINNER_OK_TEXT.format("Success!"))
# Print the results, in a beautiful blue…
click.echo(crayons.blue(c.out), err=True)
# Clear the pythonfinder caches
from .vendor.pythonfinder import Finder
finder = Finder(system=False, global_search=True)
finder.find_python_version.cache_clear()
finder.find_all_python_versions.cache_clear()
# Find the newly installed Python, hopefully.
version = str(version)
path_to_python = find_a_system_python(version)
@@ -727,11 +732,20 @@ def batch_install(deps_list, procs, failed_deps_queue,
with vistir.contextmanagers.temp_environ():
if not allow_global:
os.environ["PIP_USER"] = vistir.compat.fs_str("0")
if "PYTHONHOME" in os.environ:
del os.environ["PYTHONHOME"]
if no_deps:
link = getattr(dep.req, "link", None)
is_wheel = False
if link:
is_wheel = link.is_wheel
is_non_editable_vcs = (dep.is_vcs and not dep.editable)
no_deps = not (dep.is_file_or_url and not (is_wheel or dep.editable))
c = pip_install(
dep,
ignore_hashes=any([ignore_hashes, dep.editable, dep.is_vcs]),
allow_global=allow_global,
no_deps=False if is_artifact else no_deps,
no_deps=no_deps,
block=any([dep.editable, dep.is_vcs, blocking]),
index=index,
requirements_dir=requirements_dir,
@@ -739,7 +753,7 @@ def batch_install(deps_list, procs, failed_deps_queue,
trusted_hosts=trusted_hosts,
extra_indexes=extra_indexes
)
if dep.is_vcs:
if dep.is_vcs or dep.editable:
c.block()
if procs.qsize() < nprocs:
c.dep = dep
@@ -1824,26 +1838,6 @@ def do_install(
for req in import_from_code(code):
click.echo(" Found {0}!".format(crayons.green(req)))
project.add_package_to_pipfile(req)
# Install editable local packages before locking - this gives us access to dist-info
if project.pipfile_exists and (
# double negatives are for english readability, leave them alone.
(not project.lockfile_exists and not deploy)
or (not project.virtualenv_exists and not system)
):
section = (
project.editable_packages if not dev else project.editable_dev_packages
)
for package in section.keys():
req = convert_deps_to_pip(
{package: section[package]}, project=project, r=False
)
if req:
req = req[0]
req = req[len("-e ") :] if req.startswith("-e ") else req
if not editable_packages:
editable_packages = [req]
else:
editable_packages.extend([req])
# Allow more than one package to be provided.
package_args = [p for p in packages] + [
"-e {0}".format(pkg) for pkg in editable_packages
@@ -1916,6 +1910,8 @@ def do_install(
with vistir.contextmanagers.temp_environ(), create_spinner("Installing...") as sp:
if not system:
os.environ["PIP_USER"] = vistir.compat.fs_str("0")
if "PYTHONHOME" in os.environ:
del os.environ["PYTHONHOME"]
try:
pkg_requirement = Requirement.from_line(pkg_line)
except ValueError as e:
+6 -6
View File
@@ -752,10 +752,9 @@ class Project(object):
"develop": self._lockfile["develop"].copy()
}
lockfile_dict.update({"_meta": self.get_lockfile_meta()})
_created_lockfile = Req_Lockfile.from_data(
lockfile = Req_Lockfile.from_data(
path=self.lockfile_location, data=lockfile_dict, meta_from_project=False
)
lockfile._lockfile = _created_lockfile
elif self.lockfile_exists:
try:
lockfile = Req_Lockfile.load(self.lockfile_location)
@@ -785,10 +784,11 @@ class Project(object):
def get_lockfile_meta(self):
from .vendor.plette.lockfiles import PIPFILE_SPEC_CURRENT
sources = self.lockfile_content.get("_meta", {}).get("sources", [])
if not sources:
sources = self.pipfile_sources
elif not isinstance(sources, list):
if self.lockfile_exists:
sources = self.lockfile_content.get("_meta", {}).get("sources", [])
else:
sources = [dict(source) for source in self.parsed_pipfile["source"]]
if not isinstance(sources, list):
sources = [sources,]
return {
"hash": {"sha256": self.calculate_pipfile_hash()},
+89 -58
View File
@@ -19,26 +19,13 @@ from click import echo as click_echo
from first import first
from vistir.misc import fs_str
six.add_move(six.MovedAttribute("Mapping", "collections", "collections.abc"))
six.add_move(six.MovedAttribute("Sequence", "collections", "collections.abc"))
from six.moves import Mapping, Sequence
six.add_move(six.MovedAttribute("Mapping", "collections", "collections.abc")) # noqa
six.add_move(six.MovedAttribute("Sequence", "collections", "collections.abc")) # noqa
six.add_move(six.MovedAttribute("Set", "collections", "collections.abc")) # noqa
from six.moves import Mapping, Sequence, Set
from vistir.compat import ResourceWarning
try:
from weakref import finalize
except ImportError:
try:
from .vendor.backports.weakref import finalize
except ImportError:
class finalize(object):
def __init__(self, *args, **kwargs):
logging.warn("weakref.finalize unavailable, not cleaning...")
def detach(self):
return False
logging.basicConfig(level=logging.ERROR)
@@ -320,7 +307,7 @@ class Resolver(object):
if self.sources:
requirementstxt_sources = " ".join(self.pip_args) if self.pip_args else ""
requirementstxt_sources = requirementstxt_sources.replace(" --", "\n--")
constraints_file.write(u"{0}\n".format(requirementstxt_sources))
constraints_file.write(u"{0}\n".format(requirementstxt_sources))
constraints = self.initial_constraints
constraints_file.write(u"\n".join([c for c in constraints]))
constraints_file.close()
@@ -411,40 +398,55 @@ class Resolver(object):
self.resolved_tree.update(results)
return self.resolved_tree
@staticmethod
def _should_include_hash(ireq):
from pipenv.vendor.vistir.compat import Path, to_native_string
from pipenv.vendor.vistir.path import url_to_path
# We can only hash artifacts.
try:
if not ireq.link.is_artifact:
return False
except AttributeError:
return False
# But we don't want normal pypi artifcats since the normal resolver
# handles those
if is_pypi_url(ireq.link.url):
return False
# We also don't want to try to hash directories as this will fail
# as these are editable deps and are not hashable.
if (ireq.link.scheme == "file" and
Path(to_native_string(url_to_path(ireq.link.url))).is_dir()):
return False
return True
def resolve_hashes(self):
def _should_include_hash(ireq):
from pipenv.vendor.vistir.compat import Path, to_native_string
from pipenv.vendor.vistir.path import url_to_path
# We can only hash artifacts.
try:
if not ireq.link.is_artifact:
return False
except AttributeError:
return False
# But we don't want normal pypi artifcats since the normal resolver
# handles those
if is_pypi_url(ireq.link.url):
return False
# We also don't want to try to hash directories as this will fail
# as these are editable deps and are not hashable.
if (ireq.link.scheme == "file" and
Path(to_native_string(url_to_path(ireq.link.url))).is_dir()):
return False
return True
if self.results is not None:
resolved_hashes = self.resolver.resolve_hashes(self.results)
for ireq, ireq_hashes in resolved_hashes.items():
# We _ALWAYS MUST PRIORITIZE_ the inclusion of hashes from local sources
# PLEASE *DO NOT MODIFY THIS* TO CHECK WHETHER AN IREQ ALREADY HAS A HASH
# RESOLVED. The resolver will pull hashes from PyPI and only from PyPI.
# The entire purpose of this approach is to include missing hashes.
# This fixes a race condition in resolution for missing dependency caches
# see pypa/pipenv#3289
if self._should_include_hash(ireq) and (
not ireq_hashes or ireq.link.scheme == "file"
):
if not ireq_hashes:
ireq_hashes = set()
new_hashes = self.resolver.repository._hash_cache.get_hash(ireq.link)
add_to_set(ireq_hashes, new_hashes)
else:
ireq_hashes = set(ireq_hashes)
# The _ONLY CASE_ where we flat out set the value is if it isn't present
# It's a set, so otherwise we *always* need to do a union update
if ireq not in self.hashes:
if _should_include_hash(ireq):
self.hashes[ireq] = [
self.resolver.repository._hash_cache.get_hash(ireq.link)
]
else:
self.hashes[ireq] = ireq_hashes
self.hashes[ireq] = ireq_hashes
else:
self.hashes[ireq] |= ireq_hashes
return self.hashes
@@ -471,12 +473,12 @@ def actually_resolve_deps(
warning_list = []
with warnings.catch_warnings(record=True) as warning_list:
constraints = get_resolver_metadata(
deps, index_lookup, markers_lookup, project, sources,
)
resolver = Resolver(constraints, req_dir, project, sources, clear=clear, pre=pre)
resolved_tree = resolver.resolve()
hashes = resolver.resolve_hashes()
constraints = get_resolver_metadata(
deps, index_lookup, markers_lookup, project, sources,
)
resolver = Resolver(constraints, req_dir, project, sources, clear=clear, pre=pre)
resolved_tree = resolver.resolve()
hashes = resolver.resolve_hashes()
for warning in warning_list:
_show_warning(warning.message, warning.category, warning.filename, warning.lineno,
@@ -538,7 +540,10 @@ def resolve(cmd, sp):
return c
def get_locked_dep(dep, pipfile_section):
def get_locked_dep(dep, pipfile_section, prefer_pipfile=False):
# the prefer pipfile flag is not used yet, but we are introducing
# it now for development purposes
# TODO: Is this implementation clear? How can it be improved?
entry = None
cleaner_kwargs = {
"is_top_level": False,
@@ -552,6 +557,14 @@ def get_locked_dep(dep, pipfile_section):
if entry:
cleaner_kwargs.update({"is_top_level": True, "pipfile_entry": entry})
lockfile_entry = clean_resolved_dep(dep, **cleaner_kwargs)
if entry and isinstance(entry, Mapping):
version = entry.get("version", "") if entry else ""
else:
version = entry if entry else ""
lockfile_version = lockfile_entry.get("version", "")
# Keep pins from the lockfile
if prefer_pipfile and lockfile_version != version and version.startswith("=="):
lockfile_version = version
return lockfile_entry
@@ -562,7 +575,10 @@ def prepare_lockfile(results, pipfile, lockfile):
# markers, normalized names, URL info, etc that we may have dropped during lock
if not is_vcs(dep):
lockfile_entry = get_locked_dep(dep, pipfile)
lockfile.update(lockfile_entry)
name = next(iter(k for k in lockfile_entry.keys()))
current_entry = lockfile.get(name)
if not current_entry or not is_vcs(current_entry):
lockfile.update(lockfile_entry)
return lockfile
@@ -591,7 +607,7 @@ def venv_resolve_deps(
pipfile_section = "dev_packages" if dev else "packages"
lockfile_section = "develop" if dev else "default"
vcs_section = "vcs_{0}".format(pipfile_section)
vcs_deps = getattr(project, vcs_section, [])
vcs_deps = getattr(project, vcs_section, {})
if not deps and not vcs_deps:
return {}
@@ -611,6 +627,7 @@ def venv_resolve_deps(
dev=dev,
)
vcs_deps = [req.as_line() for req in vcs_reqs if req.editable]
lockfile[lockfile_section].update(vcs_lockfile)
cmd = [
which("python", allow_global=allow_global),
Path(resolver.__file__.rstrip("co")).as_posix()
@@ -665,7 +682,9 @@ def venv_resolve_deps(
click_echo(err.strip(), err=True)
raise RuntimeError("There was a problem with locking.")
lockfile[lockfile_section] = prepare_lockfile(results, pipfile, lockfile[lockfile_section])
lockfile[lockfile_section].update(vcs_lockfile)
for k, v in vcs_lockfile.items():
if k in getattr(project, vcs_section, {}) or k not in lockfile[lockfile_section]:
lockfile[lockfile_section][k].update(v)
def resolve_deps(
@@ -860,7 +879,7 @@ def mkdir_p(newdir):
if exn.errno != errno.EEXIST:
raise
def is_required_version(version, specified_version):
"""Check to see if there's a hard requirement for version
number provided in the Pipfile.
@@ -1490,3 +1509,15 @@ def sys_version(version_tuple):
sys.version_info = version_tuple
yield
sys.version_info = old_version
def add_to_set(original_set, element):
"""Given a set and some arbitrary element, add the element(s) to the set"""
if not element:
return original_set
if isinstance(element, Set):
original_set |= element
elif isinstance(element, (list, tuple)):
original_set |= set(element)
else:
original_set.add(element)
+1 -1
View File
@@ -3,7 +3,7 @@ __all__ = [
"Lockfile", "Pipfile",
]
__version__ = '0.2.2'
__version__ = '0.2.3.dev0'
from .lockfiles import Lockfile
from .pipfiles import Pipfile
+1 -1
View File
@@ -22,7 +22,7 @@ def validate(cls, data):
v = VALIDATORS[key]
except KeyError:
v = VALIDATORS[key] = cerberus.Validator(schema, allow_unknown=True)
if v.validate(data, normalize=False):
if v.validate(dict(data), normalize=False):
return
raise ValidationError(data, v)
+1
View File
@@ -513,6 +513,7 @@ class PathEntry(BasePath):
if self.is_dir:
return None
if self.is_python:
py_version = None
try:
py_version = PythonVersion.from_path(path=self, name=self.name)
except (InvalidPythonVersion, ValueError):
+11 -9
View File
@@ -15,6 +15,7 @@ from packaging.version import parse as parse_version
from vistir.compat import Path
from ..environment import SYSTEM_ARCH, PYENV_ROOT, ASDF_DATA_DIR
from ..exceptions import InvalidPythonVersion
from .mixins import BaseFinder, BasePath
from ..utils import (
_filter_none,
@@ -109,7 +110,7 @@ class PythonFinder(BaseFinder, BasePath):
version = None
try:
version = PythonVersion.parse(p.name)
except ValueError:
except (ValueError, InvalidPythonVersion):
entry = next(iter(version_path.find_all_python_versions()), None)
if not entry:
if self.ignore_unsupported:
@@ -246,7 +247,7 @@ class PythonVersion(object):
is_postrelease = attr.ib(default=False)
is_devrelease = attr.ib(default=False)
is_debug = attr.ib(default=False)
version = attr.ib(default=None, validator=optional_instance_of(Version))
version = attr.ib(default=None)
architecture = attr.ib(default=None)
comes_from = attr.ib(default=None)
executable = attr.ib(default=None)
@@ -368,7 +369,7 @@ class PythonVersion(object):
return self.architecture
@classmethod
def from_path(cls, path, name=None):
def from_path(cls, path, name=None, ignore_unsupported=True):
"""Parses a python version from a system path.
Raises:
@@ -377,23 +378,24 @@ class PythonVersion(object):
:param path: A string or :class:`~pythonfinder.models.path.PathEntry`
:type path: str or :class:`~pythonfinder.models.path.PathEntry` instance
:param str name: Name of the python distribution in question
:param bool ignore_unsupported: Whether to ignore or error on unsupported paths.
:return: An instance of a PythonVersion.
:rtype: :class:`~pythonfinder.models.python.PythonVersion`
"""
from .path import PathEntry
from ..environment import IGNORE_UNSUPPORTED
if not isinstance(path, PathEntry):
path = PathEntry.create(path, is_root=False, only_python=True, name=name)
if not path.is_python and not IGNORE_UNSUPPORTED:
raise ValueError("Not a valid python path: %s" % path.path)
return
from ..environment import IGNORE_UNSUPPORTED
ignore_unsupported = ignore_unsupported or IGNORE_UNSUPPORTED
if not path.is_python:
if not (ignore_unsupported or IGNORE_UNSUPPORTED):
raise ValueError("Not a valid python path: %s" % path.path)
py_version = get_python_version(path.path.absolute().as_posix())
instance_dict = cls.parse(py_version.strip())
if not isinstance(instance_dict.get("version"), Version) and not IGNORE_UNSUPPORTED:
if not isinstance(instance_dict.get("version"), Version) and not ignore_unsupported:
raise ValueError("Not a valid python path: %s" % path.path)
return
if not name:
name = path.name
instance_dict.update(
+16 -6
View File
@@ -31,7 +31,7 @@ if MYPY_RUNNING:
from attr.validators import _OptionalValidator
version_re = re.compile(r"(?P<major>\d+)\.(?P<minor>\d+)\.?(?P<patch>(?<=\.)[0-9]+)?\.?"
version_re = re.compile(r"(?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<patch>(?<=\.)[0-9]+))?\.?"
r"(?:(?P<prerel>[abc]|rc|dev)(?:(?P<prerelversion>\d+(?:\.\d+)*))?)"
r"?(?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?")
@@ -81,10 +81,10 @@ def parse_python_version(version_str):
if version_str.endswith("-debug"):
is_debug = True
version_str, _, _ = version_str.rpartition("-")
m = version_re.match(version_str)
if not m:
match = version_re.match(version_str)
if not match:
raise InvalidPythonVersion("%s is not a python version" % version_str)
version_dict = m.groupdict() # type: Dict[str, str]
version_dict = match.groupdict() # type: Dict[str, str]
major = int(version_dict.get("major", 0)) if version_dict.get("major") else None
minor = int(version_dict.get("minor", 0)) if version_dict.get("minor") else None
patch = int(version_dict.get("patch", 0)) if version_dict.get("patch") else None
@@ -97,8 +97,18 @@ def parse_python_version(version_str):
try:
version = parse_version(version_str)
except TypeError:
version_parts = [str(v) for v in [major, minor, patch] if v is not None]
version = parse_version(".".join(version_parts))
version = None
if isinstance(version, LegacyVersion) or version is None:
v_dict = version_dict.copy()
pre = ""
if v_dict.get("prerel") and v_dict.get("prerelversion"):
pre = v_dict.pop("prerel")
pre = "{0}{1}".format(pre, v_dict.pop("prerelversion"))
v_dict["pre"] = pre
keys = ["major", "minor", "patch", "pre", "postdev", "post", "dev"]
values = [v_dict.get(val) for val in keys]
version_str = ".".join([str(v) for v in values if v])
version = parse_version(version_str)
return {
"major": major,
"minor": minor,
+1 -1
View File
@@ -1,5 +1,5 @@
# -*- coding=utf-8 -*-
__version__ = '1.3.1'
__version__ = '1.3.2'
import logging
import warnings
+17
View File
@@ -0,0 +1,17 @@
# -*- coding=utf-8 -*-
from __future__ import print_function, absolute_import
import os
from appdirs import user_cache_dir
def is_type_checking():
try:
from typing import TYPE_CHECKING
except ImportError:
return False
return TYPE_CHECKING
REQUIREMENTSLIB_CACHE_DIR = os.getenv("REQUIREMENTSLIB_CACHE_DIR", user_cache_dir("pipenv"))
MYPY_RUNNING = os.environ.get("MYPY_RUNNING", is_type_checking())
+102 -22
View File
@@ -1,22 +1,81 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals, print_function
from __future__ import absolute_import, print_function, unicode_literals
import attr
import copy
import os
import sys
import attr
import tomlkit
from vistir.compat import Path, FileNotFoundError
from .requirements import Requirement
from .project import ProjectFile
from .utils import optional_instance_of
from ..exceptions import RequirementError
from ..utils import is_vcs, is_editable, merge_items
import plette.models.base
import plette.pipfiles
from vistir.compat import FileNotFoundError, Path
from ..exceptions import RequirementError
from ..utils import is_editable, is_vcs, merge_items
from .project import ProjectFile
from .requirements import Requirement
from .utils import optional_instance_of
from ..environment import MYPY_RUNNING
if MYPY_RUNNING:
from typing import Union, Any, Dict, Iterable, Sequence, Mapping, List, NoReturn
package_type = Dict[str, Dict[str, Union[List[str], str]]]
source_type = Dict[str, Union[str, bool]]
sources_type = Iterable[source_type]
meta_type = Dict[str, Union[int, Dict[str, str], sources_type]]
lockfile_type = Dict[str, Union[package_type, meta_type]]
# Let's start by patching plette to make sure we can validate data without being broken
try:
import cerberus
except ImportError:
cerberus = None
VALIDATORS = plette.models.base.VALIDATORS
def patch_plette():
# type: () -> None
global VALIDATORS
def validate(cls, data):
# type: (Any, Dict[str, Any]) -> None
if not cerberus: # Skip validation if Cerberus is not available.
return
schema = cls.__SCHEMA__
key = id(schema)
try:
v = VALIDATORS[key]
except KeyError:
v = VALIDATORS[key] = cerberus.Validator(schema, allow_unknown=True)
if v.validate(dict(data), normalize=False):
return
raise plette.models.base.ValidationError(data, v)
names = ["plette.models.base", plette.models.base.__name__]
names = [name for name in names if name in sys.modules]
for name in names:
if name in sys.modules:
module = sys.modules[name]
else:
module = plette.models.base
original_fn = getattr(module, "validate")
for key in ["__qualname__", "__name__", "__module__"]:
original_val = getattr(original_fn, key, None)
if original_val is not None:
setattr(validate, key, original_val)
setattr(module, "validate", validate)
sys.modules[name] = module
patch_plette()
is_pipfile = optional_instance_of(plette.pipfiles.Pipfile)
is_path = optional_instance_of(Path)
@@ -24,8 +83,10 @@ is_projectfile = optional_instance_of(ProjectFile)
def reorder_source_keys(data):
for i, entry in enumerate(data["source"]):
table = tomlkit.table()
# type: ignore
sources = data["source"] # type: sources_type
for i, entry in enumerate(sources):
table = tomlkit.table() # type: Mapping
table["name"] = entry["name"]
table["url"] = entry["url"]
table["verify_ssl"] = entry["verify_ssl"]
@@ -36,6 +97,7 @@ def reorder_source_keys(data):
class PipfileLoader(plette.pipfiles.Pipfile):
@classmethod
def validate(cls, data):
# type: (Dict[str, Any]) -> None
for key, klass in plette.pipfiles.PIPFILE_SECTIONS.items():
if key not in data or key == "source":
continue
@@ -46,6 +108,7 @@ class PipfileLoader(plette.pipfiles.Pipfile):
@classmethod
def load(cls, f, encoding=None):
# type: (Any, str) -> PipfileLoader
content = f.read()
if encoding is not None:
content = content.decode(encoding)
@@ -69,6 +132,7 @@ class PipfileLoader(plette.pipfiles.Pipfile):
return instance
def __getattribute__(self, key):
# type: (str) -> Any
if key == "source":
return self._data[key]
return super(PipfileLoader, self).__getattribute__(key)
@@ -78,7 +142,7 @@ class PipfileLoader(plette.pipfiles.Pipfile):
class Pipfile(object):
path = attr.ib(validator=is_path, type=Path)
projectfile = attr.ib(validator=is_projectfile, type=ProjectFile)
_pipfile = attr.ib(type=plette.pipfiles.Pipfile)
_pipfile = attr.ib(type=PipfileLoader)
_pyproject = attr.ib(default=attr.Factory(tomlkit.document), type=tomlkit.toml_document.TOMLDocument)
build_system = attr.ib(default=attr.Factory(dict), type=dict)
requirements = attr.ib(default=attr.Factory(list), type=list)
@@ -86,22 +150,27 @@ class Pipfile(object):
@path.default
def _get_path(self):
# type: () -> Path
return Path(os.curdir).absolute()
@projectfile.default
def _get_projectfile(self):
# type: () -> ProjectFile
return self.load_projectfile(os.curdir, create=False)
@_pipfile.default
def _get_pipfile(self):
# type: () -> Union[plette.pipfiles.Pipfile, PipfileLoader]
return self.projectfile.model
@property
def pipfile(self):
# type: () -> Union[PipfileLoader, plette.pipfiles.Pipfile]
return self._pipfile
def get_deps(self, dev=False, only=True):
deps = {}
# type: (bool, bool) -> Dict[str, Dict[str, Union[List[str], str]]]
deps = {} # type: Dict[str, Dict[str, Union[List[str], str]]]
if dev:
deps.update(self.pipfile._data["dev-packages"])
if only:
@@ -109,15 +178,18 @@ class Pipfile(object):
return merge_items([deps, self.pipfile._data["packages"]])
def get(self, k):
# type: (str) -> Any
return self.__getitem__(k)
def __contains__(self, k):
# type: (str) -> bool
check_pipfile = k in self.extended_keys or self.pipfile.__contains__(k)
if check_pipfile:
return True
return super(Pipfile, self).__contains__(k)
return False
def __getitem__(self, k, *args, **kwargs):
# type: ignore
retval = None
pipfile = self._pipfile
section = None
@@ -139,6 +211,7 @@ class Pipfile(object):
return retval
def __getattr__(self, k, *args, **kwargs):
# type: ignore
retval = None
pipfile = super(Pipfile, self).__getattribute__("_pipfile")
try:
@@ -151,14 +224,17 @@ class Pipfile(object):
@property
def requires_python(self):
# type: () -> bool
return self._pipfile.requires.requires_python
@property
def allow_prereleases(self):
# type: () -> bool
return self._pipfile.get("pipenv", {}).get("allow_prereleases", False)
@classmethod
def read_projectfile(cls, path):
# type: (str) -> ProjectFile
"""Read the specified project file and provide an interface for writing/updating.
:param str path: Path to the target file.
@@ -174,6 +250,7 @@ class Pipfile(object):
@classmethod
def load_projectfile(cls, path, create=False):
# type: (str, bool) -> ProjectFile
"""Given a path, load or create the necessary pipfile.
:param str path: Path to the project root or pipfile
@@ -198,6 +275,7 @@ class Pipfile(object):
@classmethod
def load(cls, path, create=False):
# type: (str, bool) -> Pipfile
"""Given a path, load or create the necessary pipfile.
:param str path: Path to the project root or pipfile
@@ -226,22 +304,22 @@ class Pipfile(object):
return cls(**creation_args)
def write(self):
# type: () -> None
self.projectfile.model = copy.deepcopy(self._pipfile)
self.projectfile.write()
@property
def dev_packages(self, as_requirements=True):
if as_requirements:
return self.dev_requirements
return self._pipfile.get('dev-packages', {})
def dev_packages(self):
# type: () -> List[Requirement]
return self.dev_requirements
@property
def packages(self, as_requirements=True):
if as_requirements:
return self.requirements
return self._pipfile.get('packages', {})
def packages(self):
# type: () -> List[Requirement]
return self.requirements
def _read_pyproject(self):
# type: () -> None
pyproject = self.path.parent.joinpath("pyproject.toml")
if pyproject.exists():
self._pyproject = tomlkit.load(pyproject)
@@ -256,8 +334,10 @@ class Pipfile(object):
@property
def build_requires(self):
# type: () -> List[str]
return self.build_system.get("requires", [])
@property
def build_backend(self):
# type: () -> str
return self.build_system.get("build-backend", None)
+26 -6
View File
@@ -3,6 +3,7 @@
from __future__ import absolute_import
import collections
import copy
import hashlib
import os
@@ -289,7 +290,7 @@ class FileRequirement(object):
if self.path and not self.uri:
self._uri_scheme = "path"
return pip_shims.shims.path_to_url(os.path.abspath(self.path))
elif self.req and getattr(self.req, "url"):
elif getattr(self, "req", None) and getattr(self.req, "url"):
return self.req.url
@name.default
@@ -312,9 +313,19 @@ class FileRequirement(object):
)):
if self.editable:
line = pip_shims.shims.path_to_url(self.setup_py_dir)
if self.extras:
line = "{0}[{1}]".format(line, ",".join(self.extras))
_ireq = pip_shims.shims.install_req_from_editable(line)
else:
_ireq = pip_shims.shims.install_req_from_line(Path(self.setup_py_dir).as_posix())
line = Path(self.setup_py_dir).as_posix()
if self.extras:
line = "{0}[{1}]".format(line, ",".join(self.extras))
_ireq = pip_shims.shims.install_req_from_line(line)
if getattr(self, "req", None):
_ireq.req = copy.deepcopy(self.req)
else:
if self.extras:
_ireq.extras = set(self.extras)
from .setup_info import SetupInfo
subdir = getattr(self, "subdirectory", None)
setupinfo = SetupInfo.from_ireq(_ireq, subdir=subdir)
@@ -453,10 +464,16 @@ class FileRequirement(object):
if not name:
_line = unquote(link.url_without_fragment) if link.url else uri
if editable:
if extras:
_line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras))))
ireq = pip_shims.shims.install_req_from_editable(_line)
else:
_line = path if (uri_scheme and uri_scheme == "path") else _line
if extras:
_line = "{0}[{1}]".format(_line, ",".join(sorted(set(extras))))
ireq = pip_shims.shims.install_req_from_line(_line)
if extras and not ireq.extras:
ireq.extras = set(extras)
setup_info = SetupInfo.from_ireq(ireq)
setupinfo_dict = setup_info.as_dict()
setup_name = setupinfo_dict.get("name", None)
@@ -488,7 +505,7 @@ class FileRequirement(object):
return cls_inst
@classmethod
def from_line(cls, line):
def from_line(cls, line, extras=None):
line = line.strip('"').strip("'")
link = None
path = None
@@ -497,6 +514,8 @@ class FileRequirement(object):
setup_path = None
name = None
req = None
if not extras:
extras = []
if not any([is_installable_file(line), is_valid_url(line), is_file_url(line)]):
try:
req = init_requirement(line)
@@ -515,7 +534,8 @@ class FileRequirement(object):
"editable": editable,
"setup_path": setup_path,
"uri_scheme": prefer,
"line": line
"line": line,
"extras": extras
}
if link and link.is_wheel:
from pip_shims import Wheel
@@ -690,7 +710,7 @@ class VCSRequirement(FileRequirement):
def get_name(self):
return (
self.link.egg_fragment or self.req.name
if self.req
if getattr(self, "req", None)
else super(VCSRequirement, self).get_name()
)
@@ -1069,7 +1089,7 @@ class Requirement(object):
(is_valid_url(possible_url) or is_file_url(line) or is_valid_url(line)) and
not (line_is_vcs or is_vcs(possible_url))
):
r = FileRequirement.from_line(line_with_prefix)
r = FileRequirement.from_line(line_with_prefix, extras=extras)
elif line_is_vcs:
r = VCSRequirement.from_line(line_with_prefix, extras=extras)
vcs = r.vcs
+33 -14
View File
@@ -7,6 +7,7 @@ import attr
import packaging.version
import packaging.specifiers
import packaging.utils
import six
try:
from setuptools.dist import distutils
@@ -16,7 +17,7 @@ except ImportError:
from appdirs import user_cache_dir
from six.moves import configparser
from six.moves.urllib.parse import unquote
from vistir.compat import Path
from vistir.compat import Path, Iterable
from vistir.contextmanagers import cd
from vistir.misc import run
from vistir.path import create_tracked_tempdir, ensure_mkdir_p, mkdir_p
@@ -67,6 +68,20 @@ def _get_src_dir():
return os.path.join(os.getcwd(), "src") # Match pip's behavior.
def ensure_reqs(reqs):
import pkg_resources
if not isinstance(reqs, Iterable):
raise TypeError("Expecting an Iterable, got %r" % reqs)
new_reqs = []
for req in reqs:
if not req:
continue
if isinstance(req, six.string_types):
req = pkg_resources.Requirement.parse("{0}".format(str(req)))
new_reqs.append(req)
return new_reqs
def _prepare_wheel_building_kwargs(ireq):
download_dir = os.path.join(CACHE_DIR, "pkgs")
mkdir_p(download_dir)
@@ -153,10 +168,8 @@ def get_metadata(path, pkg_name=None):
else:
marker = ""
extra = "{0}".format(k)
_deps = [
pkg_resources.Requirement.parse("{0}{1}".format(str(req), marker))
for req in _deps
]
_deps = ["{0}{1}".format(str(req), marker) for req in _deps]
_deps = ensure_reqs(_deps)
if extra:
extras[extra] = _deps
else:
@@ -220,19 +233,25 @@ class SetupInfo(object):
python_requires = parser.get("options", "python_requires")
if python_requires and not self.python_requires:
self.python_requires = python_requires
if parser.has_option("options", "extras_require"):
if "options.extras_require" in parser.sections():
self.extras.update(
{
section: [
dep.strip()
init_requirement(dep.strip())
for dep in parser.get(
"options.extras_require", section
).split("\n")
if dep
]
for section in parser.options("options.extras_require")
if section not in ["options", "metadata"]
}
)
if self.ireq.extras:
self.requires.update({
extra: self.extras[extra]
for extra in self.ireq.extras if extra in self.extras
})
def run_setup(self):
if self.setup_py is not None and self.setup_py.exists():
@@ -304,13 +323,13 @@ class SetupInfo(object):
)
if getattr(self.ireq, "extras", None):
for extra in self.ireq.extras:
self.requires.update(
{
req.key: req for req
in metadata.get("extras", {}).get(extra)
if req is not None
}
)
extras = metadata.get("extras", {}).get(extra, [])
if extras:
extras = ensure_reqs(extras)
self.extras[extra] = set(extras)
self.requires.update(
{req.key: req for req in extras if req is not None}
)
def run_pyproject(self):
if self.pyproject and self.pyproject.exists():
+4 -4
View File
@@ -21,18 +21,18 @@ pipdeptree==0.13.0
pipreqs==0.4.9
docopt==0.6.2
yarg==0.1.9
pythonfinder==1.1.9.post1
pythonfinder==1.1.10
requests==2.20.1
chardet==3.0.4
idna==2.7
urllib3==1.24
certifi==2018.10.15
requirementslib==1.3.1.post1
requirementslib==1.3.3
attrs==18.2.0
distlib==0.2.8
packaging==18.0
pyparsing==2.2.2
plette==0.2.2
git+https://github.com/sarugaku/plette.git@master#egg=plette
tomlkit==0.5.2
shellingham==1.2.7
six==1.11.0
@@ -40,7 +40,7 @@ semver==2.8.1
shutilwhich==1.1.0
toml==0.10.0
cached-property==1.4.3
vistir==0.2.4
vistir==0.2.5
pip-shims==0.3.2
ptyprocess==0.6.0
enum34==1.1.6
+1 -1
View File
@@ -31,7 +31,7 @@ from .path import mkdir_p, rmtree, create_tracked_tempdir, create_tracked_tempfi
from .spin import VistirSpinner, create_spinner
__version__ = '0.2.4'
__version__ = '0.2.5'
__all__ = [
+3
View File
@@ -83,6 +83,9 @@ if six.PY2:
else:
from builtins import ResourceWarning, FileNotFoundError, PermissionError, IsADirectoryError
six.add_move(six.MovedAttribute("Iterable", "collections", "collections.abc"))
from six.moves import Iterable
if not sys.warnoptions:
warnings.simplefilter("default", ResourceWarning)
+12 -10
View File
@@ -10,12 +10,12 @@ import sys
from collections import OrderedDict
from functools import partial
from itertools import islice
from itertools import islice, tee
import six
from .cmdparse import Script
from .compat import Path, fs_str, partialmethod, to_native_string
from .compat import Path, fs_str, partialmethod, to_native_string, Iterable
from .contextmanagers import spinner as spinner
if os.name != "nt":
@@ -78,15 +78,17 @@ def unnest(elem):
[1234, 3456, 4398345, 234234, 2396, 23895750, 9283798, 29384, 289375983275, 293759, 2347, 2098, 7987, 27599]
"""
if _is_iterable(elem):
for item in elem:
if _is_iterable(item):
for sub_item in unnest(item):
yield sub_item
else:
yield item
if isinstance(elem, Iterable) and not isinstance(elem, six.string_types):
elem, target = tee(elem, 2)
else:
raise ValueError("Expecting an iterable, got %r" % elem)
target = elem
for el in target:
if isinstance(el, Iterable) and not isinstance(el, six.string_types):
el, el_copy = tee(el, 2)
for sub in unnest(el_copy):
yield sub
else:
yield el
def _is_iterable(elem):
+29
View File
@@ -30,6 +30,8 @@ __all__ = [
"check_for_unc_path",
"get_converted_relative_path",
"handle_remove_readonly",
"normalize_path",
"is_in_path",
"is_file_url",
"is_readonly_path",
"is_valid_url",
@@ -80,6 +82,33 @@ else:
return os.path.normpath(path)
def normalize_path(path):
"""
Return a case-normalized absolute variable-expanded path.
:param str path: The non-normalized path
:return: A normalized, expanded, case-normalized path
:rtype: str
"""
return os.path.normpath(os.path.normcase(
os.path.abspath(os.path.expandvars(os.path.expanduser(str(path))))
))
def is_in_path(path, parent):
"""
Determine if the provided full path is in the given parent root.
:param str path: The full path to check the location of.
:param str parent: The parent path to check for membership in
:return: Whether the full path is a member of the provided parent.
:rtype: bool
"""
return normalize_path(str(path)).startswith(normalize_path(str(parent)))
def normalize_drive(path):
"""Normalize drive in path so they stay consistent.
+4 -2
View File
@@ -12,7 +12,7 @@ import cursor
import six
from .compat import to_native_string
from .termcolors import COLOR_MAP, COLORS, colored
from .termcolors import COLOR_MAP, COLORS, colored, DISABLE_COLORS
from io import StringIO
try:
@@ -34,7 +34,9 @@ CLEAR_LINE = chr(27) + "[K"
class DummySpinner(object):
def __init__(self, text="", **kwargs):
colorama.init()
super(DummySpinner, self).__init__()
if DISABLE_COLORS:
colorama.init()
from .misc import decode_for_output
self.text = to_native_string(decode_for_output(text)) if text else ""
self.stdout = kwargs.get("stdout", sys.stdout)
+5
View File
@@ -5,6 +5,11 @@ import os
from .compat import to_native_string
DISABLE_COLORS = os.getenv("CI", False) or os.getenv("ANSI_COLORS_DISABLED",
os.getenv("VISTIR_DISABLE_COLORS", False)
)
ATTRIBUTES = dict(
list(zip([
'bold',
+1 -9
View File
@@ -11,12 +11,4 @@ from pathlib import Path
ROOT = Path(".").parent.parent.absolute()
@invoke.task
def clean_mdchangelog(ctx):
changelog = ROOT / "CHANGELOG.md"
content = changelog.read_text()
content = re.sub(r"([^\n]+)\n?\s+\[[\\]+(#\d+)\]\(https://github\.com/pypa/[\w\-]+/issues/\d+\)", r"\1 \2", content, flags=re.MULTILINE)
changelog.write_text(content)
ns = invoke.Collection(vendoring, release, clean_mdchangelog, vendor_passa.vendor_passa)
ns = invoke.Collection(vendoring, release, release.clean_mdchangelog, vendor_passa.vendor_passa)
+145 -44
View File
@@ -1,13 +1,27 @@
# -*- coding=utf-8 -*-
import datetime
import invoke
import pathlib
import os
import re
import sys
from pipenv.__version__ import __version__
import invoke
from parver import Version
from towncrier._builder import (
find_fragments, render_fragments, split_fragments
)
from towncrier._settings import load_config
from pipenv.__version__ import __version__
from pipenv.vendor.vistir.contextmanagers import temp_environ
from .vendoring import _get_git_root, drop_dir
VERSION_FILE = 'pipenv/__version__.py'
ROOT = pathlib.Path(".").parent.parent.absolute()
PACKAGE_NAME = "pipenv"
def log(msg):
@@ -18,6 +32,14 @@ def get_version_file(ctx):
return _get_git_root(ctx).joinpath(VERSION_FILE)
def find_version(ctx):
version_file = get_version_file(ctx).read_text()
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
def get_history_file(ctx):
return _get_git_root(ctx).joinpath('HISTORY.txt')
@@ -30,6 +52,66 @@ def get_build_dir(ctx):
return _get_git_root(ctx) / 'build'
def _render_log():
"""Totally tap into Towncrier internals to get an in-memory result.
"""
config = load_config(ROOT)
definitions = config['types']
fragments, fragment_filenames = find_fragments(
pathlib.Path(config['directory']).absolute(),
config['sections'],
None,
definitions,
)
rendered = render_fragments(
pathlib.Path(config['template']).read_text(encoding='utf-8'),
config['issue_format'],
split_fragments(fragments, definitions),
definitions,
config['underlines'][1:],
False, # Don't add newlines to wrapped text.
)
return rendered
@invoke.task
def release(ctx, dry_run=False):
drop_dist_dirs(ctx)
bump_version(ctx, dry_run=dry_run)
version = find_version(ctx)
tag_content = _render_log()
if dry_run:
ctx.run('towncrier --draft > CHANGELOG.draft.rst')
log('would remove: news/*')
log('would remove: CHANGELOG.draft.rst')
log(f'Would commit with message: "Release v{version}"')
else:
ctx.run('towncrier')
ctx.run("git add CHANGELOG.rst news/")
ctx.run("git rm CHANGELOG.draft.rst")
ctx.run(f'git commit -m "Release v{version}"')
tag_content = tag_content.replace('"', '\\"')
if dry_run:
log(f"Generated tag content: {tag_content}")
markdown = ctx.run("pandoc CHANGELOG.draft.rst -f rst -t markdown", hide=True).stdout.strip()
content = clean_mdchangelog(ctx, markdown)
log(f"would generate markdown: {content}")
else:
generate_markdown(ctx)
clean_mdchangelog(ctx)
ctx.run(f'git tag -a v{version} -m "Version v{version}\n\n{tag_content}"')
build_dists(ctx)
if dry_run:
dist_pattern = f'{PACKAGE_NAME.replace("-", "[-_]")}-*'
artifacts = list(ROOT.joinpath('dist').glob(dist_pattern))
filename_display = '\n'.join(f' {a}' for a in artifacts)
log(f"Would upload dists: {filename_display}")
else:
upload_dists(ctx)
bump_version(ctx, dev=True)
def drop_dist_dirs(ctx):
log('Dropping Dist dir...')
drop_dir(get_dist_dir(ctx))
@@ -40,20 +122,36 @@ def drop_dist_dirs(ctx):
@invoke.task
def build_dists(ctx):
drop_dist_dirs(ctx)
log('Building sdist using %s ....' % sys.executable)
for py_version in ['2.7', '3.6', '3.7']:
for py_version in ['3.6', '2.7']:
env = {'PIPENV_PYTHON': py_version}
ctx.run('pipenv install --dev', env=env)
if py_version == '3.6':
ctx.run('pipenv run python setup.py sdist', env=env)
log('Building wheel using python %s ....' % py_version)
ctx.run('pipenv run python setup.py bdist_wheel', env=env)
with ctx.cd(ROOT.as_posix()), temp_environ():
executable = ctx.run("python -c 'import sys; print(sys.executable)'", hide=True).stdout.strip()
log('Building sdist using %s ....' % executable)
os.environ["PIPENV_PYTHON"] = py_version
ctx.run('pipenv install --dev', env=env)
ctx.run('pipenv run pip install -e . --upgrade --upgrade-strategy=eager', env=env)
log('Building wheel using python %s ....' % py_version)
if py_version == '3.6':
ctx.run('pipenv run python setup.py sdist bdist_wheel', env=env)
else:
ctx.run('pipenv run python setup.py bdist_wheel', env=env)
@invoke.task(build_dists)
def upload_dists(ctx):
log('Uploading distributions to pypi...')
ctx.run('twine upload dist/*')
def upload_dists(ctx, repo="pypi"):
dist_pattern = f'{PACKAGE_NAME.replace("-", "[-_]")}-*'
artifacts = list(ROOT.joinpath('dist').glob(dist_pattern))
filename_display = '\n'.join(f' {a}' for a in artifacts)
print(f'[release] Will upload:\n{filename_display}')
try:
input('[release] Release ready. ENTER to upload, CTRL-C to abort: ')
except KeyboardInterrupt:
print('\nAborted!')
return
arg_display = ' '.join(f'"{n}"' for n in artifacts)
ctx.run(f'twine upload --repository="{repo}" {arg_display}')
@invoke.task
@@ -69,68 +167,71 @@ def generate_changelog(ctx, commit=False, draft=False):
commit = False
log('Writing draft to file...')
ctx.run('towncrier --draft > CHANGELOG.draft.rst')
if commit:
else:
ctx.run('towncrier')
if commit:
log('Committing...')
ctx.run('git add CHANGELOG.rst')
ctx.run('git rm CHANGELOG.draft.rst')
ctx.run('git commit -m "Update changelog."')
@invoke.task
def clean_mdchangelog(ctx, content=None):
changelog = None
if not content:
changelog = _get_git_root(ctx) / "CHANGELOG.md"
content = changelog.read_text()
content = re.sub(r"([^\n]+)\n?\s+\[[\\]+(#\d+)\]\(https://github\.com/pypa/[\w\-]+/issues/\d+\)", r"\1 \2", content, flags=re.MULTILINE)
if changelog:
changelog.write_text(content)
else:
return content
@invoke.task
def tag_version(ctx, push=False):
version = Version.parse(__version__)
log('Tagging revision: v%s' % version)
ctx.run('git tag v%s' % version)
version = find_version(ctx)
version = Version.parse(version)
log('Tagging revision: v%s' % version.normalize())
ctx.run('git tag v%s' % version.normalize())
if push:
log('Pushing tags...')
ctx.run('git push origin master')
ctx.run('git push --tags')
@invoke.task
def bump_version(ctx, dry_run=False, increment=True, release=False, dev=False, pre=False, tag=None, clear=False, commit=False,):
def bump_version(ctx, dry_run=False, dev=False, pre=False, tag=None, commit=False):
current_version = Version.parse(__version__)
today = datetime.date.today()
next_month_number = today.month + 1 if today.month != 12 else 1
next_year_number = today.year if next_month_number != 1 else today.year+1
next_month = (next_year_number, next_month_number, 0)
tomorrow = today + datetime.timedelta(days=1)
next_month = datetime.date.today().replace(month=today.month+1, day=1)
next_year = datetime.date.today().replace(year=today.year+1, month=1, day=1)
if pre and not tag:
print('Using "pre" requires a corresponding tag.')
return
if release and not dev and not pre and increment:
if not (dev or pre or tag):
new_version = current_version.replace(release=today.timetuple()[:3]).clear(pre=True, dev=True)
elif release and (dev or pre):
if increment:
new_version = current_version.replace(release=today.timetuple()[:3])
else:
new_version = current_version
if pre and dev:
raise RuntimeError("Can't use 'pre' and 'dev' together!")
if dev or pre:
new_version = current_version.replace(release=tomorrow.timetuple()[:3]).clear(pre=True, dev=True)
if dev:
new_version = new_version.bump_dev()
elif pre:
new_version = new_version.bump_pre(tag=tag)
else:
if not release:
increment = False
if increment:
new_version = current_version.replace(release=next_month)
else:
new_version = current_version
if dev:
new_version = new_version.bump_dev()
elif pre:
new_version = new_version.bump_pre(tag=tag)
if clear:
new_version = new_version.clear(dev=True, pre=True, post=True)
log('Updating version to %s' % new_version.normalize())
version_file = get_version_file(ctx)
file_contents = version_file.read_text()
log('Found current version: %s' % __version__)
version = find_version(ctx)
log('Found current version: %s' % version)
if dry_run:
log('Would update to: %s' % new_version.normalize())
else:
log('Updating to: %s' % new_version.normalize())
version_file.write_text(file_contents.replace(__version__, str(new_version.normalize())))
version_file = get_version_file(ctx)
file_contents = version_file.read_text()
version_file.write_text(file_contents.replace(version, str(new_version.normalize())))
if commit:
ctx.run('git add {0}'.format(version_file))
ctx.run('git add {0}'.format(version_file.as_posix()))
log('Committing...')
ctx.run('git commit -s -m "Bumped version."')
+24 -1
View File
@@ -2,9 +2,13 @@
""""Vendoring script, python 3.5 needed"""
# Taken from pip
# see https://github.com/pypa/pip/blob/95bcf8c5f6394298035a7332c441868f3b0169f4/tasks/vendoring/__init__.py
from pipenv._compat import NamedTemporaryFile, TemporaryDirectory
from pipenv.vendor.vistir.compat import NamedTemporaryFile, TemporaryDirectory
from pipenv.vendor.vistir.contextmanagers import open_file
from pathlib import Path
from pipenv.utils import mkdir_p
import io
from urllib3.util import parse_url as urllib3_parse
import bs4
# from tempfile import TemporaryDirectory
import tarfile
import zipfile
@@ -640,3 +644,22 @@ def main(ctx, package=None):
# vendor_passa(ctx)
# update_safety(ctx)
log('Revendoring complete')
@invoke.task
def vendor_artifact(ctx, package, version=None):
simple = requests.get("https://pypi.org/simple/{0}/".format(package))
pkg_str = "{0}-{1}".format(package, version)
soup = bs4.BeautifulSoup(simple.content)
links = [
a.attrs["href"] for a in soup.find_all("a") if a.getText().startswith(pkg_str)
]
for link in links:
dest_dir = _get_git_root(ctx) / "tests" / "pypi" / package
if not dest_dir.exists():
dest_dir.mkdir()
_, _, dest_path = urllib3_parse(link).path.rpartition("/")
dest_file = dest_dir / dest_path
with io.open(dest_file.as_posix(), "wb") as target_handle:
with open_file(link) as fp:
shutil.copyfileobj(fp, target_handle)
+25 -1
View File
@@ -12,7 +12,7 @@ from pipenv.vendor import delegator
from pipenv.vendor import requests
from pipenv.vendor import toml
from pipenv.vendor import tomlkit
from pytest_pypi.app import prepare_packages as prepare_pypi_packages
from pytest_pypi.app import prepare_packages as prepare_pypi_packages, prepare_fixtures
from vistir.compat import ResourceWarning, fs_str
from vistir.path import mkdir_p
@@ -71,6 +71,7 @@ TESTS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PYPI_VENDOR_DIR = os.path.join(TESTS_ROOT, 'pypi')
WE_HAVE_HG = check_for_mercurial()
prepare_pypi_packages(PYPI_VENDOR_DIR)
prepare_fixtures(os.path.join(PYPI_VENDOR_DIR, "fixtures"))
def pytest_runtest_setup(item):
@@ -144,6 +145,9 @@ class _Pipfile(object):
self.document[section][package] = value
self.write()
def add(self, package, value, dev=False):
self.install(package, value, dev=dev)
def loads(self):
self.document = tomlkit.loads(self.path.read_text())
@@ -162,6 +166,25 @@ class _Pipfile(object):
def get_fixture_path(cls, path):
return Path(__file__).absolute().parent.parent / "test_artifacts" / path
@classmethod
def get_url(cls, pkg=None, filename=None):
pypi = os.environ.get("PIPENV_PYPI_URL")
if not pkg and not filename:
return pypi if pypi else "https://pypi.org/"
file_path = filename
if pkg and filename:
file_path = os.path.join(pkg, filename)
if filename and not pkg:
pkg = os.path.basename(filename)
if pypi:
if pkg and not filename:
url = "{0}/artifacts/{1}".format(pypi, pkg)
else:
url = "{0}/artifacts/{1}/{2}".format(pypi, pkg, filename)
return url
if pkg and not filename:
return cls.get_fixture_path(file_path).as_uri()
class _PipenvInstance(object):
"""An instance of a Pipenv Project..."""
@@ -189,6 +212,7 @@ class _PipenvInstance(object):
self.chdir = chdir
if self.pypi:
os.environ['PIPENV_PYPI_URL'] = fs_str('{0}'.format(self.pypi.url))
os.environ['PIPENV_TEST_INDEX'] = fs_str('{0}/simple'.format(self.pypi.url))
if pipfile:
+15 -28
View File
@@ -2,7 +2,7 @@ import os
import shutil
from pipenv.project import Project
from pipenv._compat import Path
from pipenv.vendor import delegator
from pipenv.utils import mkdir_p, temp_environ
import pytest
@@ -338,7 +338,7 @@ six = {{path = "./artifacts/{}"}}
@pytest.mark.files
@pytest.mark.install
@pytest.mark.run
def test_multiple_editable_packages_should_not_race(PipenvInstance, pypi, tmpdir, testsroot):
def test_multiple_editable_packages_should_not_race(PipenvInstance, pypi, testsroot):
"""Test for a race condition that can occur when installing multiple 'editable' packages at
once, and which causes some of them to not be importable.
@@ -347,40 +347,27 @@ def test_multiple_editable_packages_should_not_race(PipenvInstance, pypi, tmpdir
So this test locally installs packages from tarballs that have already been committed in
the local `pypi` dir to avoid using VCS packages.
"""
pkgs = {
"requests-2.19.1": "requests/requests-2.19.1.tar.gz",
"Flask-0.12.2": "flask/Flask-0.12.2.tar.gz",
"six-1.11.0": "six/six-1.11.0.tar.gz",
"Jinja2-2.10": "jinja2/Jinja2-2.10.tar.gz",
}
pkgs = ["requests", "flask", "six", "jinja2"]
pipfile_string = """
[dev-packages]
pipfile_string="""
[packages]
"""
# Unzip tarballs to known location, and update Pipfile template.
for pkg_name, file_name in pkgs.items():
source_path = str(Path(testsroot, "pypi", file_name))
unzip_path = str(Path(tmpdir.strpath, pkg_name))
import tarfile
with tarfile.open(source_path, "r:gz") as tgz:
tgz.extractall(path=tmpdir.strpath)
pipfile_string += "'{0}' = {{path = '{1}', editable = true}}\n".format(pkg_name, unzip_path)
with PipenvInstance(pypi=pypi, chdir=True) as p:
for pkg_name in pkgs:
source_path = p._pipfile.get_fixture_path("git/{0}/".format(pkg_name)).as_posix()
c = delegator.run("git clone {0} ./{1}".format(source_path, pkg_name))
assert c.return_code == 0
pipfile_string += '"{0}" = {{path = "./{0}", editable = true}}\n'.format(pkg_name)
with open(p.pipfile_path, 'w') as f:
f.write(pipfile_string.strip())
c = p.pipenv('install')
assert c.return_code == 0
c = p.pipenv('run python -c "import requests"')
assert c.return_code == 0
c = p.pipenv('run python -c "import flask"')
assert c.return_code == 0
c = p.pipenv('run python -c "import six"')
assert c.return_code == 0
c = p.pipenv('run python -c "import jinja2"')
assert c.return_code == 0
c = p.pipenv('run python -c "import requests, flask, six, jinja2"')
assert c.return_code == 0, c.err
+16 -25
View File
@@ -63,9 +63,10 @@ def test_ssh_vcs_install(PipenvInstance, pip_src_dir, pypi):
@pytest.mark.needs_internet
@flaky
def test_urls_work(PipenvInstance, pypi, pip_src_dir):
with PipenvInstance(pypi=pypi) as p:
with PipenvInstance(pypi=pypi, chdir=True) as p:
path = p._pipfile.get_url("django", "3.4.x.zip")
c = p.pipenv(
"install https://github.com/divio/django-cms/archive/release/3.4.x.zip"
"install {0}".format(path)
)
assert c.return_code == 0
@@ -187,27 +188,15 @@ six = "*"
@pytest.mark.needs_internet
def test_install_local_vcs_not_in_lockfile(PipenvInstance, pip_src_dir):
with PipenvInstance(chdir=True) as p:
six_path = os.path.join(p.path, "six")
c = delegator.run(
"git clone https://github.com/benjaminp/six.git {0}".format(six_path)
)
# six_path = os.path.join(p.path, "six")
six_path = p._pipfile.get_fixture_path("git/six/").as_posix()
c = delegator.run("git clone {0} ./six".format(six_path))
assert c.return_code == 0
c = p.pipenv("install -e ./six")
c = p.pipenv("install -e ./six".format(six_path))
assert c.return_code == 0
six_key = list(p.pipfile["packages"].keys())[0]
c = p.pipenv(
"install -e git+https://github.com/requests/requests.git#egg=requests"
)
assert c.return_code == 0
c = p.pipenv("lock")
assert c.return_code == 0
assert "requests" in p.pipfile["packages"]
assert "requests" in p.lockfile["default"]
# This is the hash of ./six
assert six_key in p.pipfile["packages"]
assert six_key in p.lockfile["default"]
# The hash isn't a hash anymore, its actually the name of the package (we now resolve this)
assert "six" in p.pipfile["packages"]
# we don't need the rest of the test anymore, this just works on its own
assert six_key == "six"
@pytest.mark.vcs
@@ -247,6 +236,7 @@ def test_vcs_entry_supersedes_non_vcs(PipenvInstance, pip_src_dir):
the resolution graph of non-editable vcs dependencies.
"""
with PipenvInstance(chdir=True) as p:
pyinstaller_path = p._pipfile.get_fixture_path("git/pyinstaller")
with open(p.pipfile_path, "w") as f:
f.write(
"""
@@ -257,18 +247,19 @@ name = "pypi"
[packages]
PyUpdater = "*"
PyInstaller = {ref = "develop", git = "https://github.com/pyinstaller/pyinstaller.git"}
""".strip()
PyInstaller = {{ref = "develop", git = "{0}"}}
""".format(pyinstaller_path.as_uri()).strip()
)
p.pipenv("install")
c = p.pipenv("install")
assert c.return_code == 0
installed_packages = ["PyUpdater", "PyInstaller"]
assert all([k in p.pipfile["packages"] for k in installed_packages])
assert all([k.lower() in p.lockfile["default"] for k in installed_packages])
assert all([k in p.lockfile["default"]["pyinstaller"] for k in ["ref", "git"]])
assert all([k in p.lockfile["default"]["pyinstaller"] for k in ["ref", "git"]]), str(p.lockfile["default"])
assert p.lockfile["default"]["pyinstaller"].get("ref") is not None
assert (
p.lockfile["default"]["pyinstaller"]["git"]
== "https://github.com/pyinstaller/pyinstaller.git"
== pyinstaller_path.as_uri()
)
+47
View File
@@ -1,5 +1,6 @@
import pytest
import os
import sys
from pipenv.utils import temp_environ
@@ -512,4 +513,50 @@ def test_lock_no_warnings(PipenvInstance, pypi):
os.environ["PYTHONWARNINGS"] = str("once")
c = p.pipenv("install six")
assert c.return_code == 0
c = p.pipenv('run python -c "import warnings; warnings.warn(\\"This is a warning\\", DeprecationWarning); print(\\"hello\\")"')
assert c.return_code == 0
assert "Warning" in c.err
assert "Warning" not in c.out
assert "hello" in c.out
@pytest.mark.lock
@pytest.mark.install
@pytest.mark.skipif(sys.version_info >= (3, 5), reason="scandir doesn't get installed on python 3.5+")
def test_lock_missing_cache_entries_gets_all_hashes(monkeypatch, PipenvInstance, pypi, tmpdir):
"""
Test locking pathlib2 on python2.7 which needs `scandir`, but fails to resolve when
using a fresh dependency cache.
"""
with monkeypatch.context() as m:
monkeypatch.setattr("pipenv.patched.piptools.locations.CACHE_DIR", tmpdir.strpath)
with PipenvInstance(pypi=pypi, chdir=True) as p:
p._pipfile.add("pathlib2", "*")
assert "pathlib2" in p.pipfile["packages"]
c = p.pipenv("install")
assert c.return_code == 0, c.err
assert "pathlib2" in p.lockfile["default"]
assert "scandir" in p.lockfile["default"]
assert isinstance(p.lockfile["default"]["scandir"]["hashes"], list)
assert len(p.lockfile["default"]["scandir"]["hashes"]) > 1
@pytest.mark.lock
@pytest.mark.vcs
def test_vcs_lock_respects_top_level_pins(PipenvInstance, pypi):
"""Test that locking VCS dependencies respects top level packages pinned in Pipfiles"""
with PipenvInstance(pypi=pypi, chdir=True) as p:
requests_uri = p._pipfile.get_fixture_path("git/requests").as_uri()
p._pipfile.add("requests", {
"editable": True, "git": "{0}".format(requests_uri),
"ref": "v2.18.4"
})
p._pipfile.add("urllib3", "==1.21.1")
c = p.pipenv("install")
assert c.return_code == 0
assert "requests" in p.lockfile["default"]
assert "git" in p.lockfile["default"]["requests"]
assert "urllib3" in p.lockfile["default"]
assert p.lockfile["default"]["urllib3"]["version"] == "==1.21.1"
+1 -1
View File
@@ -98,7 +98,7 @@ def test_directory_with_leading_dash(PipenvInstance):
prefix = '-dir-with-leading-dash'
return mkdtemp(suffix, prefix, dir)
with mock.patch('pipenv._compat.mkdtemp', side_effect=mocked_mkdtemp):
with mock.patch('pipenv.vendor.vistir.compat.mkdtemp', side_effect=mocked_mkdtemp):
with temp_environ(), PipenvInstance(chdir=True) as p:
del os.environ['PIPENV_VENV_IN_PROJECT']
p.pipenv('--python python')
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+70 -2
View File
@@ -1,14 +1,17 @@
import os
import json
import sys
import requests
from flask import Flask, redirect, abort, render_template, send_file, jsonify
from zipfile import is_zipfile
from tarfile import is_tarfile
app = Flask(__name__)
session = requests.Session()
packages = {}
ARTIFACTS = {}
class Package(object):
"""Package represents a collection of releases from one or more directories"""
@@ -38,6 +41,46 @@ class Package(object):
self._package_dirs.add(path)
class Artifact(object):
"""Represents an artifact for download"""
def __init__(self, name):
super(Artifact, self).__init__()
self.name = name
self.files = {}
self._artifact_dirs = set()
def __repr__(self):
return "<Artifact name={0!r} files={1!r}".format(self.name, len(self.files))
def add_file(self, path):
path = os.path.abspath(path)
base_path, fn = os.path.split(path)
self.files[fn] = path
self._artifact_dirs.add(base_path)
def prepare_fixtures(path):
path = os.path.abspath(path)
if not (os.path.exists(path) and os.path.isdir(path)):
raise ValueError("{} is not a directory!".format(path))
for root, dirs, files in os.walk(path):
package_name, _, _ = os.path.relpath(root, start=path).partition(os.path.sep)
if package_name not in ARTIFACTS:
ARTIFACTS[package_name] = Artifact(package_name)
for file in files:
file_path = os.path.join(root, file)
rel_path = os.path.relpath(file_path, start=path)
_, _, subpkg = rel_path.partition(os.path.sep)
subpkg, _, _ = subpkg.partition(os.path.sep)
pkg, ext = os.path.splitext(subpkg)
if not (is_tarfile(file_path) or is_zipfile(file_path) or ext == ".git"):
continue
if subpkg not in ARTIFACTS[package_name].files:
ARTIFACTS[package_name].add_file(os.path.join(root, file))
ARTIFACTS[package_name].add_file(os.path.join(root, file))
def prepare_packages(path):
"""Add packages in path to the registry."""
path = os.path.abspath(path)
@@ -47,7 +90,9 @@ def prepare_packages(path):
for file in files:
if not file.startswith('.') and not file.endswith('.json'):
package_name = os.path.basename(root)
if package_name and package_name == "fixtures":
prepare_fixtures(root)
continue
if package_name not in packages:
packages[package_name] = Package(package_name)
@@ -64,6 +109,11 @@ def simple():
return render_template('simple.html', packages=packages.values())
@app.route('/artifacts')
def artifacts():
return render_template('artifacts.html', artifacts=ARTIFACTS.values())
@app.route('/simple/<package>/')
def simple_package(package):
if package in packages:
@@ -72,6 +122,14 @@ def simple_package(package):
abort(404)
@app.route('/artifacts/<artifact>/')
def simple_artifact(artifact):
if artifact in ARTIFACTS:
return render_template('artifact.html', artifact=ARTIFACTS[artifact])
else:
abort(404)
@app.route('/<package>/<release>')
def serve_package(package, release):
if package in packages:
@@ -83,6 +141,15 @@ def serve_package(package, release):
abort(404)
@app.route('/artifacts/<artifact>/<fn>')
def serve_artifact(artifact, fn):
if artifact in ARTIFACTS:
artifact = ARTIFACTS[artifact]
if fn in artifact.files:
return send_file(artifact.files[fn])
abort(404)
@app.route('/pypi/<package>/json')
def json_for_package(package):
try:
@@ -98,5 +165,6 @@ if __name__ == '__main__':
PYPI_VENDOR_DIR = os.environ.get('PYPI_VENDOR_DIR', './pypi')
PYPI_VENDOR_DIR = os.path.abspath(PYPI_VENDOR_DIR)
prepare_packages(PYPI_VENDOR_DIR)
prepare_fixtures(os.path.join(PYPI_VENDOR_DIR, "fixtures"))
app.run()
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Links for {{ artifact.name }}</title>
</head>
<body>
<h1>Links for {{ artifact.name }}</h1>
{% for fn in artifact.files %}
<a href="/{{ artifact.name }}/{{ fn }}">{{ fn }}</a>
<br>
{% endfor %}
</body>
</html>
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Artifact Index</title>
</head>
<body>
{% for artifact in artifacts %}
<a href="/artifacts/{{ artifact.name }}/" rel="internal">{{ artifact.name }}</a>
<br>
{% endfor %}
</body>
</html>
Binary file not shown.