diff --git a/.azure-pipelines/jobs/run-manifest-check.yml b/.azure-pipelines/jobs/run-manifest-check.yml index 6d0b97eb..6aa63480 100644 --- a/.azure-pipelines/jobs/run-manifest-check.yml +++ b/.azure-pipelines/jobs/run-manifest-check.yml @@ -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/* diff --git a/.azure-pipelines/jobs/run-vendor-scripts.yml b/.azure-pipelines/jobs/run-vendor-scripts.yml index 7fe75731..5af6f866 100644 --- a/.azure-pipelines/jobs/run-vendor-scripts.yml +++ b/.azure-pipelines/jobs/run-vendor-scripts.yml @@ -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 diff --git a/.gitmodules b/.gitmodules index f40ee10c..04cdc997 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/Pipfile b/Pipfile index a1db217c..db952be7 100644 --- a/Pipfile +++ b/Pipfile @@ -21,6 +21,7 @@ jedi = "*" isort = "*" rope = "*" passa = {editable = true, git = "https://github.com/sarugaku/passa.git"} +bs4 = "*" [packages] diff --git a/Pipfile.lock b/Pipfile.lock index f5e02ec8..2cccee2b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -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": [ diff --git a/news/3261.bugfix.rst b/news/3261.bugfix.rst new file mode 100644 index 00000000..e72b23cd --- /dev/null +++ b/news/3261.bugfix.rst @@ -0,0 +1 @@ +``pipenv install`` will now unset the ``PYTHONHOME`` environment variable when not combined with ``--system``. diff --git a/news/3278.bugfix.rst b/news/3278.bugfix.rst new file mode 100644 index 00000000..92b79b94 --- /dev/null +++ b/news/3278.bugfix.rst @@ -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. diff --git a/news/3280.vendor.rst b/news/3280.vendor.rst new file mode 100644 index 00000000..00157b70 --- /dev/null +++ b/news/3280.vendor.rst @@ -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` diff --git a/news/3287.bugfix.rst b/news/3287.bugfix.rst new file mode 100644 index 00000000..b9e14f0d --- /dev/null +++ b/news/3287.bugfix.rst @@ -0,0 +1 @@ +Clear pythonfinder cache after Python install diff --git a/news/3289.bugfix.rst b/news/3289.bugfix.rst new file mode 100644 index 00000000..ff089313 --- /dev/null +++ b/news/3289.bugfix.rst @@ -0,0 +1 @@ +Fixed a race condition in hash resolution for dependencies for certain dependencies with missing cache entries or fresh Pipenv installs. diff --git a/news/3296.bugfix.rst b/news/3296.bugfix.rst new file mode 100644 index 00000000..fc528848 --- /dev/null +++ b/news/3296.bugfix.rst @@ -0,0 +1 @@ +Pipenv will now respect top-level pins over VCS dependency locks. diff --git a/pipenv/__main__.py b/pipenv/__main__.py index 56494106..98dcca0c 100644 --- a/pipenv/__main__.py +++ b/pipenv/__main__.py @@ -1,4 +1,4 @@ from .cli import cli if __name__ == "__main__": - cli(auto_envvar_prefix="PIPENV") + cli() diff --git a/pipenv/_compat.py b/pipenv/_compat.py index 09f65669..fec8756f 100644 --- a/pipenv/_compat.py +++ b/pipenv/_compat.py @@ -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: diff --git a/pipenv/cli/command.py b/pipenv/cli/command.py index a49e90dd..70d1af0f 100644 --- a/pipenv/cli/command.py +++ b/pipenv/cli/command.py @@ -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 diff --git a/pipenv/cli/options.py b/pipenv/cli/options.py index 606454dd..caa67e38 100644 --- a/pipenv/cli/options.py +++ b/pipenv/cli/options.py @@ -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): diff --git a/pipenv/core.py b/pipenv/core.py index e112b6fe..0618131f 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -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: diff --git a/pipenv/project.py b/pipenv/project.py index 26ec696c..ef34eeb8 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -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()}, diff --git a/pipenv/utils.py b/pipenv/utils.py index 6fd4cf43..da486b86 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -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) diff --git a/pipenv/vendor/plette/__init__.py b/pipenv/vendor/plette/__init__.py index 8099f0b1..5daf460c 100644 --- a/pipenv/vendor/plette/__init__.py +++ b/pipenv/vendor/plette/__init__.py @@ -3,7 +3,7 @@ __all__ = [ "Lockfile", "Pipfile", ] -__version__ = '0.2.2' +__version__ = '0.2.3.dev0' from .lockfiles import Lockfile from .pipfiles import Pipfile diff --git a/pipenv/vendor/plette/models/base.py b/pipenv/vendor/plette/models/base.py index d70752ee..fad0d09e 100644 --- a/pipenv/vendor/plette/models/base.py +++ b/pipenv/vendor/plette/models/base.py @@ -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) diff --git a/pipenv/vendor/pythonfinder/models/path.py b/pipenv/vendor/pythonfinder/models/path.py index 42608cde..221d892f 100644 --- a/pipenv/vendor/pythonfinder/models/path.py +++ b/pipenv/vendor/pythonfinder/models/path.py @@ -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): diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index 1e5caa16..4fcbbca6 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -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( diff --git a/pipenv/vendor/pythonfinder/utils.py b/pipenv/vendor/pythonfinder/utils.py index d8edb239..18441919 100644 --- a/pipenv/vendor/pythonfinder/utils.py +++ b/pipenv/vendor/pythonfinder/utils.py @@ -31,7 +31,7 @@ if MYPY_RUNNING: from attr.validators import _OptionalValidator -version_re = re.compile(r"(?P\d+)\.(?P\d+)\.?(?P(?<=\.)[0-9]+)?\.?" +version_re = re.compile(r"(?P\d+)(?:\.(?P\d+))?(?:\.(?P(?<=\.)[0-9]+))?\.?" r"(?:(?P[abc]|rc|dev)(?:(?P\d+(?:\.\d+)*))?)" r"?(?P(\.post(?P\d+))?(\.dev(?P\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, diff --git a/pipenv/vendor/requirementslib/__init__.py b/pipenv/vendor/requirementslib/__init__.py index 32415c61..aca59917 100644 --- a/pipenv/vendor/requirementslib/__init__.py +++ b/pipenv/vendor/requirementslib/__init__.py @@ -1,5 +1,5 @@ # -*- coding=utf-8 -*- -__version__ = '1.3.1' +__version__ = '1.3.2' import logging import warnings diff --git a/pipenv/vendor/requirementslib/environment.py b/pipenv/vendor/requirementslib/environment.py new file mode 100644 index 00000000..2a7d9b0e --- /dev/null +++ b/pipenv/vendor/requirementslib/environment.py @@ -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()) diff --git a/pipenv/vendor/requirementslib/models/pipfile.py b/pipenv/vendor/requirementslib/models/pipfile.py index e3d353d9..84a4a26d 100644 --- a/pipenv/vendor/requirementslib/models/pipfile.py +++ b/pipenv/vendor/requirementslib/models/pipfile.py @@ -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) diff --git a/pipenv/vendor/requirementslib/models/requirements.py b/pipenv/vendor/requirementslib/models/requirements.py index d034a12d..af3d07ed 100644 --- a/pipenv/vendor/requirementslib/models/requirements.py +++ b/pipenv/vendor/requirementslib/models/requirements.py @@ -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 diff --git a/pipenv/vendor/requirementslib/models/setup_info.py b/pipenv/vendor/requirementslib/models/setup_info.py index c32c2790..ec631e67 100644 --- a/pipenv/vendor/requirementslib/models/setup_info.py +++ b/pipenv/vendor/requirementslib/models/setup_info.py @@ -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(): diff --git a/pipenv/vendor/vendor.txt b/pipenv/vendor/vendor.txt index ff7226b2..1a419eef 100644 --- a/pipenv/vendor/vendor.txt +++ b/pipenv/vendor/vendor.txt @@ -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 diff --git a/pipenv/vendor/vistir/__init__.py b/pipenv/vendor/vistir/__init__.py index 809c973c..4c5c9061 100644 --- a/pipenv/vendor/vistir/__init__.py +++ b/pipenv/vendor/vistir/__init__.py @@ -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__ = [ diff --git a/pipenv/vendor/vistir/compat.py b/pipenv/vendor/vistir/compat.py index 83226481..27d1e75c 100644 --- a/pipenv/vendor/vistir/compat.py +++ b/pipenv/vendor/vistir/compat.py @@ -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) diff --git a/pipenv/vendor/vistir/misc.py b/pipenv/vendor/vistir/misc.py index f7ed26b4..11480e2f 100644 --- a/pipenv/vendor/vistir/misc.py +++ b/pipenv/vendor/vistir/misc.py @@ -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): diff --git a/pipenv/vendor/vistir/path.py b/pipenv/vendor/vistir/path.py index febaddbc..6e9a7f65 100644 --- a/pipenv/vendor/vistir/path.py +++ b/pipenv/vendor/vistir/path.py @@ -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. diff --git a/pipenv/vendor/vistir/spin.py b/pipenv/vendor/vistir/spin.py index 5e975b6f..a2455b7d 100644 --- a/pipenv/vendor/vistir/spin.py +++ b/pipenv/vendor/vistir/spin.py @@ -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) diff --git a/pipenv/vendor/vistir/termcolors.py b/pipenv/vendor/vistir/termcolors.py index 8395d97d..d72e8ebe 100644 --- a/pipenv/vendor/vistir/termcolors.py +++ b/pipenv/vendor/vistir/termcolors.py @@ -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', diff --git a/tasks/__init__.py b/tasks/__init__.py index 6896edb6..04581bb3 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -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) diff --git a/tasks/release.py b/tasks/release.py index 4a242ba5..030f1832 100644 --- a/tasks/release.py +++ b/tasks/release.py @@ -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."') diff --git a/tasks/vendoring/__init__.py b/tasks/vendoring/__init__.py index ea0d038c..0ec59b8e 100644 --- a/tasks/vendoring/__init__.py +++ b/tasks/vendoring/__init__.py @@ -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) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index d2ad24ea..822617ab 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -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: diff --git a/tests/integration/test_install_twists.py b/tests/integration/test_install_twists.py index 2ad12691..4a623040 100644 --- a/tests/integration/test_install_twists.py +++ b/tests/integration/test_install_twists.py @@ -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 diff --git a/tests/integration/test_install_uri.py b/tests/integration/test_install_uri.py index 337181fa..35cdb001 100644 --- a/tests/integration/test_install_uri.py +++ b/tests/integration/test_install_uri.py @@ -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() ) diff --git a/tests/integration/test_lock.py b/tests/integration/test_lock.py index 3605e553..6080c293 100644 --- a/tests/integration/test_lock.py +++ b/tests/integration/test_lock.py @@ -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" diff --git a/tests/integration/test_pipenv.py b/tests/integration/test_pipenv.py index 7824980f..cb7b9249 100644 --- a/tests/integration/test_pipenv.py +++ b/tests/integration/test_pipenv.py @@ -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') diff --git a/tests/pypi/fixtures/django/3.4.x.zip b/tests/pypi/fixtures/django/3.4.x.zip new file mode 100644 index 00000000..6c2ae5f6 Binary files /dev/null and b/tests/pypi/fixtures/django/3.4.x.zip differ diff --git a/tests/pypi/pathlib2/pathlib2-2.3.2-py2.py3-none-any.whl b/tests/pypi/pathlib2/pathlib2-2.3.2-py2.py3-none-any.whl new file mode 100644 index 00000000..0cd52d85 Binary files /dev/null and b/tests/pypi/pathlib2/pathlib2-2.3.2-py2.py3-none-any.whl differ diff --git a/tests/pypi/pathlib2/pathlib2-2.3.2.tar.gz b/tests/pypi/pathlib2/pathlib2-2.3.2.tar.gz new file mode 100644 index 00000000..86e784d1 Binary files /dev/null and b/tests/pypi/pathlib2/pathlib2-2.3.2.tar.gz differ diff --git a/tests/pypi/scandir/scandir-1.9.0-cp27-cp27m-win32.whl b/tests/pypi/scandir/scandir-1.9.0-cp27-cp27m-win32.whl new file mode 100644 index 00000000..fd2f1047 Binary files /dev/null and b/tests/pypi/scandir/scandir-1.9.0-cp27-cp27m-win32.whl differ diff --git a/tests/pypi/scandir/scandir-1.9.0-cp27-cp27m-win_amd64.whl b/tests/pypi/scandir/scandir-1.9.0-cp27-cp27m-win_amd64.whl new file mode 100644 index 00000000..7c718c67 Binary files /dev/null and b/tests/pypi/scandir/scandir-1.9.0-cp27-cp27m-win_amd64.whl differ diff --git a/tests/pypi/scandir/scandir-1.9.0-cp36-cp36m-win32.whl b/tests/pypi/scandir/scandir-1.9.0-cp36-cp36m-win32.whl new file mode 100644 index 00000000..39bff799 Binary files /dev/null and b/tests/pypi/scandir/scandir-1.9.0-cp36-cp36m-win32.whl differ diff --git a/tests/pypi/scandir/scandir-1.9.0-cp36-cp36m-win_amd64.whl b/tests/pypi/scandir/scandir-1.9.0-cp36-cp36m-win_amd64.whl new file mode 100644 index 00000000..10a8d7ef Binary files /dev/null and b/tests/pypi/scandir/scandir-1.9.0-cp36-cp36m-win_amd64.whl differ diff --git a/tests/pypi/scandir/scandir-1.9.0.tar.gz b/tests/pypi/scandir/scandir-1.9.0.tar.gz new file mode 100644 index 00000000..f134aace Binary files /dev/null and b/tests/pypi/scandir/scandir-1.9.0.tar.gz differ diff --git a/tests/pypi/urllib3/urllib3-1.21.1-py2.py3-none-any.whl b/tests/pypi/urllib3/urllib3-1.21.1-py2.py3-none-any.whl new file mode 100644 index 00000000..9061cfdf Binary files /dev/null and b/tests/pypi/urllib3/urllib3-1.21.1-py2.py3-none-any.whl differ diff --git a/tests/pypi/urllib3/urllib3-1.21.1.tar.gz b/tests/pypi/urllib3/urllib3-1.21.1.tar.gz new file mode 100644 index 00000000..d2e458ec Binary files /dev/null and b/tests/pypi/urllib3/urllib3-1.21.1.tar.gz differ diff --git a/tests/pytest-pypi/pytest_pypi/app.py b/tests/pytest-pypi/pytest_pypi/app.py index ba1a5437..4e013ab5 100644 --- a/tests/pytest-pypi/pytest_pypi/app.py +++ b/tests/pytest-pypi/pytest_pypi/app.py @@ -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 "/') def simple_package(package): if package in packages: @@ -72,6 +122,14 @@ def simple_package(package): abort(404) +@app.route('/artifacts//') +def simple_artifact(artifact): + if artifact in ARTIFACTS: + return render_template('artifact.html', artifact=ARTIFACTS[artifact]) + else: + abort(404) + + @app.route('//') def serve_package(package, release): if package in packages: @@ -83,6 +141,15 @@ def serve_package(package, release): abort(404) +@app.route('/artifacts//') +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//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() diff --git a/tests/pytest-pypi/pytest_pypi/templates/artifact.html b/tests/pytest-pypi/pytest_pypi/templates/artifact.html new file mode 100644 index 00000000..5f4199c5 --- /dev/null +++ b/tests/pytest-pypi/pytest_pypi/templates/artifact.html @@ -0,0 +1,14 @@ + + + + + Links for {{ artifact.name }} + + +

Links for {{ artifact.name }}

+ {% for fn in artifact.files %} + {{ fn }} +
+ {% endfor %} + + diff --git a/tests/pytest-pypi/pytest_pypi/templates/artifacts.html b/tests/pytest-pypi/pytest_pypi/templates/artifacts.html new file mode 100644 index 00000000..6bee78da --- /dev/null +++ b/tests/pytest-pypi/pytest_pypi/templates/artifacts.html @@ -0,0 +1,13 @@ + + + + + Artifact Index + + + {% for artifact in artifacts %} + {{ artifact.name }} +
+ {% endfor %} + + diff --git a/tests/test_artifacts/django/3.4.x.zip b/tests/test_artifacts/django/3.4.x.zip new file mode 100644 index 00000000..6c2ae5f6 Binary files /dev/null and b/tests/test_artifacts/django/3.4.x.zip differ diff --git a/tests/test_artifacts/git/flask b/tests/test_artifacts/git/flask new file mode 160000 index 00000000..dcc02d6e --- /dev/null +++ b/tests/test_artifacts/git/flask @@ -0,0 +1 @@ +Subproject commit dcc02d6e7d4bf486e64aa5b6e55a75501c2ba2e5 diff --git a/tests/test_artifacts/git/jinja2 b/tests/test_artifacts/git/jinja2 new file mode 160000 index 00000000..a7f1f528 --- /dev/null +++ b/tests/test_artifacts/git/jinja2 @@ -0,0 +1 @@ +Subproject commit a7f1f528f5e77d5401a96aa326885508245f7c6f diff --git a/tests/test_artifacts/git/pyinstaller b/tests/test_artifacts/git/pyinstaller new file mode 160000 index 00000000..19d8a378 --- /dev/null +++ b/tests/test_artifacts/git/pyinstaller @@ -0,0 +1 @@ +Subproject commit 19d8a378987d6115c0acb72ef954bbf3bca3c61b diff --git a/tests/test_artifacts/git/requests-2.18.4 b/tests/test_artifacts/git/requests-2.18.4 new file mode 160000 index 00000000..a3d7cf3f --- /dev/null +++ b/tests/test_artifacts/git/requests-2.18.4 @@ -0,0 +1 @@ +Subproject commit a3d7cf3f27e74c28ef30f01e9f2e483570ab042e