diff --git a/.gitignore b/.gitignore index 5661bbf5..d35b0966 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ test_project /t.py /deb_dist/ + +.tox/ diff --git a/HISTORY.txt b/HISTORY.txt index b6eb43b8..1ef0cdac 100644 --- a/HISTORY.txt +++ b/HISTORY.txt @@ -2,6 +2,8 @@ - Resolve editable packages on the local filesystem. - Ensure lock hash does not change based on injected env vars. - Fix bug in detecting .venv at project root when in subdirectories. + - Parse quoting in [scripts] section correctly + clearer run errors. + - Fix bug resolving & locking markers correctly 11.9.0: - Vastly improve markers capabilities. - Support for environment variables in Pipfiles. diff --git a/docs/advanced.rst b/docs/advanced.rst index a97ee2de..f1abcd83 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -322,7 +322,7 @@ To prevent pipenv from loading the ``.env`` file, set the ``PIPENV_DONT_LOAD_ENV ☤ Support for Environment Variables ----------------------------------- -``pipenv`` supports the usage of environment variables in values. For example: +``pipenv`` supports the usage of environment variables in values. For example:: [[source]] url = "https://${PYPI_USERNAME}:${PYPI_PASSWORD}@my_private_repo.example.com/simple" diff --git a/pipenv/cli.py b/pipenv/cli.py index 56e62969..449cd3c8 100644 --- a/pipenv/cli.py +++ b/pipenv/cli.py @@ -503,9 +503,6 @@ def lock( # Ensure that virtualenv is available. core.ensure_project(three=three, python=python) - # Load the --pre settings from the Pipfile. - if not pre: - pre = core.project.settings.get('pre') if requirements: core.do_init(dev=dev, requirements=requirements) core.do_lock( @@ -739,9 +736,6 @@ def update( crayons.white('.', bold=True), ) ) - # Load the --pre settings from the Pipfile. - if not pre: - pre = core.project.settings.get('pre') core.do_lock( verbose=verbose, clear=clear, pre=pre, keep_outdated=keep_outdated ) diff --git a/pipenv/core.py b/pipenv/core.py index 421e80bb..a5969e58 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -4,6 +4,7 @@ import logging import os import sys import shutil +import shlex import signal import time import tempfile @@ -1007,7 +1008,11 @@ def do_lock( write=True, ): """Executes the freeze functionality.""" + from notpip._vendor.distlib.markers import Evaluator + allowed_marker_keys = ['markers'] + [k for k in Evaluator.allowed_values.keys()] cached_lockfile = {} + if not pre: + pre = project.settings.get('allow_prereleases') if keep_outdated: if not project.lockfile_exists: click.echo( @@ -1064,8 +1069,11 @@ def do_lock( # Add index metadata to lockfile. if 'index' in dep: lockfile['develop'][dep['name']]['index'] = dep['index'] - # Add PEP 508 specifier metadata to lockfile. + # Add PEP 508 specifier metadata to lockfile if dep isnt top level + # or top level dep doesn't itself have markers if 'markers' in dep: + if dep['name'] in dev_packages and not any(key in dev_packages[dep['name']] for key in allowed_marker_keys): + continue lockfile['develop'][dep['name']]['markers'] = dep['markers'] # Add refs for VCS installs. # TODO: be smarter about this. @@ -1120,8 +1128,11 @@ def do_lock( # Add index metadata to lockfile. if 'index' in dep: lockfile['default'][dep['name']]['index'] = dep['index'] - # Add PEP 508 specifier metadata to lockfile. + # Add PEP 508 specifier metadata to lockfile if dep isn't top level + # or top level dep has no specifiers itself if 'markers' in dep: + if dep['name'] in project.packages and not any(key in project.packages[dep['name']] for key in allowed_marker_keys): + continue lockfile['default'][dep['name']]['markers'] = dep['markers'] # Add refs for VCS installs. # TODO: be smarter about this. @@ -1637,7 +1648,6 @@ def warn_in_virtualenv(): def ensure_lockfile(keep_outdated=False): """Ensures that the lockfile is up–to–date.""" - pre = project.settings.get('allow_prereleases') if not keep_outdated: keep_outdated = project.settings.get('keep_outdated') # Write out the lockfile if it doesn't exist, but not if the Pipfile is being ignored @@ -1654,9 +1664,9 @@ def ensure_lockfile(keep_outdated=False): ), err=True, ) - do_lock(pre=pre, keep_outdated=keep_outdated) + do_lock(keep_outdated=keep_outdated) else: - do_lock(pre=pre, keep_outdated=keep_outdated) + do_lock(keep_outdated=keep_outdated) def do_py(system=False): @@ -2011,8 +2021,6 @@ def do_uninstall( system = True # Ensure that virtualenv is available. ensure_project(three=three, python=python) - # Load the --pre settings from the Pipfile. - pre = project.settings.get('allow_prereleases') package_names = (package_name,) + more_packages pipfile_remove = True # Un-install all dependencies, if --all was provided. @@ -2079,7 +2087,7 @@ def do_uninstall( project.remove_package_from_pipfile(package_name, dev=True) project.remove_package_from_pipfile(package_name, dev=False) if lock: - do_lock(system=system, pre=pre, keep_outdated=keep_outdated) + do_lock(system=system, keep_outdated=keep_outdated) def do_shell(three=None, python=False, fancy=False, shell_args=None): @@ -2177,7 +2185,7 @@ def inline_activate_virtualenv(): activate_this = which('activate_this.py') with open(activate_this) as f: code = compile(f.read(), activate_this, 'exec') - exec (code, dict(__file__=activate_this)) + exec(code, dict(__file__=activate_this)) # Catch all errors, just in case. except Exception: click.echo( @@ -2187,34 +2195,52 @@ def inline_activate_virtualenv(): ) -def do_run(command, args, three=None, python=False): - # Ensure that virtualenv is available. - ensure_project(three=three, python=python, validate=False) - load_dot_env() +def do_run_nt(command, args): + """Run command by appending space-joined args to it!""" + import subprocess + command = project.scripts.get(command, command) + + # if you've passed something with crazy quoting... + # ...just don't. (or put it in a script!) + p = subprocess.Popen( + command + ' '.join(args), shell=True, universal_newlines=True + ) + p.communicate() + sys.exit(p.returncode) + + +def _get_command_posix(project, command, args): + """Fully bake command into executable and args, based upon project""" # Script was found… if command in project.scripts: - command = ' '.join(project.scripts[command]) - # Separate out things that were passed in as a string. - _c = list(command.split()) - command = _c.pop(0) - if _c: - args = list(args) - for __c in reversed(_c): - args.insert(0, __c) - # Activate virtualenv under the current interpreter's environment - inline_activate_virtualenv() - # Windows! - if os.name == 'nt': - import subprocess + command = project.scripts[command] + parsed_command = shlex.split(command) + executable = parsed_command[0] + # prepend arguments + args = list(parsed_command[1:]) + list(args) + return executable, args - p = subprocess.Popen( - [command] + list(args), shell=True, universal_newlines=True - ) - p.communicate() - sys.exit(p.returncode) - else: - command_path = system_which(command) - if not command_path: + +def do_run_posix(command, args): + """Attempt to run command either pulling from project or interpreting as executable. + + Args are appended to the command in [scripts] section of project if found. + """ + executable, args = _get_command_posix(project, command, args) + command_path = system_which(executable) + if not command_path: + if command in project.scripts: + click.echo( + '{0}: the command {1} (from {2}) could not be found within {3}.' + ''.format( + crayons.red('Error', bold=True), + crayons.red(executable), + crayons.normal(command, bold=True), + crayons.normal('PATH', bold=True), + ), + err=True, + ) + else: click.echo( '{0}: the command {1} could not be found within {2} or Pipfile\'s {3}.' ''.format( @@ -2225,10 +2251,20 @@ def do_run(command, args, three=None, python=False): ), err=True, ) - sys.exit(1) - # Execute the command. - os.execl(command_path, command_path, *args) - pass + sys.exit(1) + os.execl(command_path, command_path, *args) + + +def do_run(command, args, three=None, python=False): + # Ensure that virtualenv is available. + ensure_project(three=three, python=python, validate=False) + load_dot_env() + # Activate virtualenv under the current interpreter's environment + inline_activate_virtualenv() + if os.name == 'nt': + do_run_nt(command, args) + else: + do_run_posix(command, args) def do_check(three=None, python=False, system=False, unused=False, args=None): diff --git a/pipenv/patched/contoml/file/file.py b/pipenv/patched/contoml/file/file.py index 16017b99..99ce1483 100755 --- a/pipenv/patched/contoml/file/file.py +++ b/pipenv/patched/contoml/file/file.py @@ -231,7 +231,7 @@ class TOMLFile: if has_anonymous_entry(): return items else: - return items + [('', self[''])] + return list(items) + [('', self[''])] @property def primitive(self): diff --git a/pipenv/patched/notpip/index.py b/pipenv/patched/notpip/index.py index f8f313e4..48aaa35f 100644 --- a/pipenv/patched/notpip/index.py +++ b/pipenv/patched/notpip/index.py @@ -653,7 +653,8 @@ class PackageFinder(object): if not ext: self._log_skipped_link(link, 'not a file') return - if ext not in SUPPORTED_EXTENSIONS and not ignore_compatibility: + # Always ignore unsupported extensions even when we ignore compatibility + if ext not in SUPPORTED_EXTENSIONS: self._log_skipped_link( link, 'unsupported archive format: %s' % ext) return diff --git a/pipenv/patched/pipfile/api.py b/pipenv/patched/pipfile/api.py index 08aa49c3..18a1ea23 100644 --- a/pipenv/patched/pipfile/api.py +++ b/pipenv/patched/pipfile/api.py @@ -9,6 +9,13 @@ import sys import os +DEFAULT_SOURCE = { + u'url': u'https://pypi.python.org/simple', + u'verify_ssl': True, + u'name': u'pypi', +} + + def format_full_version(info): version = '{0.major}.{0.minor}.{0.micro}'.format(info) kind = info.releaselevel @@ -89,7 +96,7 @@ class PipfileParser(object): # Load the default configuration. default_config = { - u'source': [{u'url': u'https://pypi.python.org/simple', u'verify_ssl': True, 'name': "pypi"}], + u'source': [DEFAULT_SOURCE], u'packages': {}, u'requires': {}, u'dev-packages': {} diff --git a/pipenv/patched/piptools/resolver.py b/pipenv/patched/piptools/resolver.py index 0a4a02a9..acea5d9f 100755 --- a/pipenv/patched/piptools/resolver.py +++ b/pipenv/patched/piptools/resolver.py @@ -13,7 +13,7 @@ from . import click from .cache import DependencyCache from .exceptions import UnsupportedConstraint from .logging import log -from .utils import (format_requirement, format_specifier, full_groupby, +from .utils import (format_requirement, format_specifier, full_groupby, dedup, is_pinned_requirement, key_from_ireq, key_from_req, UNSAFE_PACKAGES) green = partial(click.style, fg='green') @@ -297,9 +297,9 @@ class Resolver(object): dependencies = self.repository.get_dependencies(ireq) import sys if sys.version_info[0] == 2: - self.dependency_cache[ireq] = sorted(str(ireq.req) for ireq in dependencies) + self.dependency_cache[ireq] = sorted(format_requirement(ireq) for ireq in dependencies) else: - self.dependency_cache[ireq] = sorted('{0}; {1}'.format(str(ireq.req), str(ireq.markers)) if ireq.markers else str(ireq.req) for ireq in dependencies) + self.dependency_cache[ireq] = sorted(format_requirement(ireq) for ireq in dependencies) # Example: ['Werkzeug>=0.9', 'Jinja2>=2.4'] dependency_strings = self.dependency_cache[ireq] @@ -310,28 +310,11 @@ class Resolver(object): for dependency_string in dependency_strings: try: - markers = None + _dependency_string = dependency_string if ';' in dependency_string: # split off markers and remove any duplicates by comparing against deps - dependencies, markers = dependency_string.rsplit(';', 1) - dependency_string = ';'.join([dep for dep in dependencies.split(';') if dep.strip() != markers.strip()]) - individual_dependencies = [dep.strip() for dep in dependency_string.split(', ')] - cleaned_deps = [] - for dep in individual_dependencies: - tokens = [token.strip() for token in dep.split(';')] - cleaned_tokens = [] - dep_markers = [] - if len(tokens) == 1: - cleaned_deps.append(tokens[0]) - continue - dep_markers = list(set(tokens[1:])) - cleaned_tokens.append(tokens[0]) - if dep_markers: - cleaned_tokens.extend(dep_markers) - cleaned_deps.append('; '.join(cleaned_tokens)) - _dependency_string = ', '.join(set(cleaned_deps)) - if markers: - _dependency_string += ';{0}'.format(markers) + _dependencies = [dep.strip() for dep in dependency_string.split(';')] + _dependency_string = '; '.join([dep for dep in dedup(_dependencies)]) yield InstallRequirement.from_line(_dependency_string, constraint=ireq.constraint) except InvalidMarker: diff --git a/pipenv/patched/piptools/utils.py b/pipenv/patched/piptools/utils.py index 44d2d336..26d418eb 100755 --- a/pipenv/patched/piptools/utils.py +++ b/pipenv/patched/piptools/utils.py @@ -87,7 +87,7 @@ def format_requirement(ireq, marker=None): line = str(ireq.req).lower() if marker: - line = '{} ; {}'.format(line, marker) + line = '{}; {}'.format(line, marker) return line diff --git a/pipenv/project.py b/pipenv/project.py index f349298c..9da86876 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -12,6 +12,7 @@ import hashlib import contoml import delegator import pipfile +import pipfile.api import toml from pip9 import ConfigOptionParser @@ -51,6 +52,26 @@ if PIPENV_PIPFILE: _pipfile_cache = {} +if PIPENV_TEST_INDEX: + DEFAULT_SOURCE = { + u'url': PIPENV_TEST_INDEX, + u'verify_ssl': True, + u'name': u'custom', + } +else: + DEFAULT_SOURCE = { + u'url': u'https://pypi.python.org/simple', + u'verify_ssl': True, + u'name': u'pypi', + } + +pipfile.api.DEFAULT_SOURCE = DEFAULT_SOURCE + + +class SourceNotFound(KeyError): + pass + + class Project(object): """docstring for Project""" @@ -369,12 +390,7 @@ class Project(object): @property def scripts(self): - scripts = self.parsed_pipfile.get('scripts', {}) - posix = os.name == 'posix' - _scripts = {} - for (k, v) in scripts.items(): - _scripts[k] = shlex.split(str(v), posix=posix) - return _scripts + return dict(self.parsed_pipfile.get('scripts', {})) def update_settings(self, d): settings = self.settings @@ -495,35 +511,20 @@ class Project(object): config_parser = ConfigOptionParser(name=self.name) install = dict(config_parser.get_config_section('install')) indexes = install.get('extra-index-url', '').lstrip('\n').split('\n') - if PIPENV_TEST_INDEX: - sources = [ - { - u'url': PIPENV_TEST_INDEX, - u'verify_ssl': True, - u'name': u'custom', - } - ] - else: - # Default source. - pypi_source = { - u'url': u'https://pypi.python.org/simple', - u'verify_ssl': True, - u'name': 'pypi', - } - sources = [pypi_source] - for i, index in enumerate(indexes): - if not index: - continue + sources = [DEFAULT_SOURCE] + for i, index in enumerate(indexes): + if not index: + continue - source_name = 'pip_index_{}'.format(i) - verify_ssl = index.startswith('https') - sources.append( - { - u'url': index, - u'verify_ssl': verify_ssl, - u'name': source_name, - } - ) + source_name = 'pip_index_{}'.format(i) + verify_ssl = index.startswith('https') + sources.append( + { + u'url': index, + u'verify_ssl': verify_ssl, + u'name': source_name, + } + ) data = { u'source': sources, # Default packages. @@ -570,15 +571,8 @@ class Project(object): if 'source' in self.parsed_pipfile: return self.parsed_pipfile['source'] - else: - return [ - { - u'url': u'https://pypi.python.org/simple', - u'verify_ssl': True, - 'name': 'pypi', - } - ] + return [DEFAULT_SOURCE] def get_source(self, name=None, url=None): for source in self.sources: @@ -589,6 +583,7 @@ class Project(object): elif url: if source.get('url') in url: return source + raise SourceNotFound(name or url) def destroy_lockfile(self): """Deletes the lockfile.""" diff --git a/pipenv/utils.py b/pipenv/utils.py index 741826f3..7d60e440 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -1207,7 +1207,7 @@ class TemporaryDirectory(object): in it are removed. """ - def __init__(self, suffix=None, prefix=None, dir=None): + def __init__(self, suffix, prefix, dir=None): if 'RAM_DISK' in os.environ: import uuid diff --git a/tests/pypi/multidict/multidict-4.1.0-cp36-cp36m-win_amd64.whl b/tests/pypi/multidict/multidict-4.1.0-cp36-cp36m-win_amd64.whl new file mode 100644 index 00000000..6f8c38f3 Binary files /dev/null and b/tests/pypi/multidict/multidict-4.1.0-cp36-cp36m-win_amd64.whl differ diff --git a/tests/pypi/multidict/multidict-4.1.0.tar.gz b/tests/pypi/multidict/multidict-4.1.0.tar.gz new file mode 100644 index 00000000..89adbb22 Binary files /dev/null and b/tests/pypi/multidict/multidict-4.1.0.tar.gz differ diff --git a/tests/pypi/pyyaml/PyYAML-3.12-cp27-cp27m-win32.whl b/tests/pypi/pyyaml/PyYAML-3.12-cp27-cp27m-win32.whl new file mode 100644 index 00000000..d9f5e245 Binary files /dev/null and b/tests/pypi/pyyaml/PyYAML-3.12-cp27-cp27m-win32.whl differ diff --git a/tests/pypi/pyyaml/PyYAML-3.12-cp27-cp27m-win_amd64.whl b/tests/pypi/pyyaml/PyYAML-3.12-cp27-cp27m-win_amd64.whl new file mode 100644 index 00000000..2eb7094f Binary files /dev/null and b/tests/pypi/pyyaml/PyYAML-3.12-cp27-cp27m-win_amd64.whl differ diff --git a/tests/pypi/pyyaml/PyYAML-3.12-cp35-cp35m-win32.whl b/tests/pypi/pyyaml/PyYAML-3.12-cp35-cp35m-win32.whl new file mode 100644 index 00000000..f58c2f69 Binary files /dev/null and b/tests/pypi/pyyaml/PyYAML-3.12-cp35-cp35m-win32.whl differ diff --git a/tests/pypi/pyyaml/PyYAML-3.12-cp35-cp35m-win_amd64.whl b/tests/pypi/pyyaml/PyYAML-3.12-cp35-cp35m-win_amd64.whl new file mode 100644 index 00000000..c3d36a8b Binary files /dev/null and b/tests/pypi/pyyaml/PyYAML-3.12-cp35-cp35m-win_amd64.whl differ diff --git a/tests/pypi/randomwords/RandomWords-0.2.1-py2.7.egg b/tests/pypi/randomwords/RandomWords-0.2.1-py2.7.egg new file mode 100644 index 00000000..ef607e11 Binary files /dev/null and b/tests/pypi/randomwords/RandomWords-0.2.1-py2.7.egg differ diff --git a/tests/pypi/randomwords/RandomWords-0.2.1-py3.5.egg b/tests/pypi/randomwords/RandomWords-0.2.1-py3.5.egg new file mode 100644 index 00000000..e8fa3f78 Binary files /dev/null and b/tests/pypi/randomwords/RandomWords-0.2.1-py3.5.egg differ diff --git a/tests/pypi/randomwords/RandomWords-0.2.1-py3.6.egg b/tests/pypi/randomwords/RandomWords-0.2.1-py3.6.egg new file mode 100644 index 00000000..bd0b78c7 Binary files /dev/null and b/tests/pypi/randomwords/RandomWords-0.2.1-py3.6.egg differ diff --git a/tests/pypi/randomwords/RandomWords-0.2.1.tar.gz b/tests/pypi/randomwords/RandomWords-0.2.1.tar.gz new file mode 100644 index 00000000..3791a446 Binary files /dev/null and b/tests/pypi/randomwords/RandomWords-0.2.1.tar.gz differ diff --git a/tests/pypi/requests/requests-2.14.0-py2.py3-none-any.whl b/tests/pypi/requests/requests-2.14.0-py2.py3-none-any.whl new file mode 100644 index 00000000..1fb0b95a Binary files /dev/null and b/tests/pypi/requests/requests-2.14.0-py2.py3-none-any.whl differ diff --git a/tests/pypi/requests/requests-2.14.0.tar.gz b/tests/pypi/requests/requests-2.14.0.tar.gz new file mode 100644 index 00000000..33631280 Binary files /dev/null and b/tests/pypi/requests/requests-2.14.0.tar.gz differ diff --git a/tests/pypi/sqlalchemy/SQLAlchemy-1.2.0b3.tar.gz b/tests/pypi/sqlalchemy/SQLAlchemy-1.2.0b3.tar.gz new file mode 100644 index 00000000..40eafa66 Binary files /dev/null and b/tests/pypi/sqlalchemy/SQLAlchemy-1.2.0b3.tar.gz differ diff --git a/tests/pypi/tablib/tablib-0.11.5.tar.gz b/tests/pypi/tablib/tablib-0.11.5.tar.gz new file mode 100644 index 00000000..460cd0a7 Binary files /dev/null and b/tests/pypi/tablib/tablib-0.11.5.tar.gz differ diff --git a/tests/pypi/tablib/tablib-0.12.0.tar.gz b/tests/pypi/tablib/tablib-0.12.0.tar.gz new file mode 100644 index 00000000..36ab89f2 Binary files /dev/null and b/tests/pypi/tablib/tablib-0.12.0.tar.gz differ diff --git a/tests/pypi/vcrpy/vcrpy-1.11.0-py2.py3-none-any.whl b/tests/pypi/vcrpy/vcrpy-1.11.0-py2.py3-none-any.whl new file mode 100644 index 00000000..2853aafc Binary files /dev/null and b/tests/pypi/vcrpy/vcrpy-1.11.0-py2.py3-none-any.whl differ diff --git a/tests/pypi/vcrpy/vcrpy-1.11.0.tar.gz b/tests/pypi/vcrpy/vcrpy-1.11.0.tar.gz new file mode 100644 index 00000000..605bce97 Binary files /dev/null and b/tests/pypi/vcrpy/vcrpy-1.11.0.tar.gz differ diff --git a/tests/pypi/wrapt/wrapt-1.10.11.tar.gz b/tests/pypi/wrapt/wrapt-1.10.11.tar.gz new file mode 100644 index 00000000..7f6870e2 Binary files /dev/null and b/tests/pypi/wrapt/wrapt-1.10.11.tar.gz differ diff --git a/tests/pypi/yarl/yarl-1.1.1-cp36-cp36m-manylinux1_i686.whl b/tests/pypi/yarl/yarl-1.1.1-cp36-cp36m-manylinux1_i686.whl new file mode 100644 index 00000000..ab6c0e0c Binary files /dev/null and b/tests/pypi/yarl/yarl-1.1.1-cp36-cp36m-manylinux1_i686.whl differ diff --git a/tests/pypi/yarl/yarl-1.1.1-cp36-cp36m-manylinux1_x86_64.whl b/tests/pypi/yarl/yarl-1.1.1-cp36-cp36m-manylinux1_x86_64.whl new file mode 100644 index 00000000..baeeb2c1 Binary files /dev/null and b/tests/pypi/yarl/yarl-1.1.1-cp36-cp36m-manylinux1_x86_64.whl differ diff --git a/tests/pypi/yarl/yarl-1.1.1-cp36-cp36m-win32.whl b/tests/pypi/yarl/yarl-1.1.1-cp36-cp36m-win32.whl new file mode 100644 index 00000000..45fe89ed Binary files /dev/null and b/tests/pypi/yarl/yarl-1.1.1-cp36-cp36m-win32.whl differ diff --git a/tests/pypi/yarl/yarl-1.1.1-cp36-cp36m-win_amd64.whl b/tests/pypi/yarl/yarl-1.1.1-cp36-cp36m-win_amd64.whl new file mode 100644 index 00000000..e11d5c62 Binary files /dev/null and b/tests/pypi/yarl/yarl-1.1.1-cp36-cp36m-win_amd64.whl differ diff --git a/tests/pytest-pypi/pytest_pypi.egg-info/PKG-INFO b/tests/pytest-pypi/pytest_pypi.egg-info/PKG-INFO index 966cd7ad..50322734 100644 --- a/tests/pytest-pypi/pytest_pypi.egg-info/PKG-INFO +++ b/tests/pytest-pypi/pytest_pypi.egg-info/PKG-INFO @@ -6,7 +6,6 @@ Home-page: https://github.com/kennethreitz/pytest-pypi Author: Kenneth Reitz Author-email: me@kennethreitz.org License: MIT -Description-Content-Type: UNKNOWN Description: pytest-httpbin ============== diff --git a/tests/test_pipenv.py b/tests/test_pipenv.py index 0865556f..5c12a9b4 100644 --- a/tests/test_pipenv.py +++ b/tests/test_pipenv.py @@ -1,15 +1,17 @@ import os +import sys import re import shutil import json import pytest import warnings -from pipenv.core import activate_virtualenv +from pipenv.core import activate_virtualenv, _get_command_posix from pipenv.utils import ( temp_environ, get_windows_path, mkdir_p, normalize_drive, TemporaryDirectory ) from pipenv.vendor import toml from pipenv.vendor import delegator +from pipenv.vendor import requests from pipenv.patched import pipfile from pipenv.project import Project from pipenv.vendor.six import PY2 @@ -28,6 +30,25 @@ os.environ['PIPENV_VENV_IN_PROJECT'] = '1' os.environ['PYPI_VENDOR_DIR'] = os.path.sep.join([os.path.dirname(__file__), 'pypi']) +def check_internet(): + try: + # Kenneth represents the Internet LGTM. + resp = requests.get('http://httpbin.org/ip', timeout=1.0) + resp.raise_for_status() + except Exception: + warnings.warn('Cannot connect to HTTPBin...', ResourceWarning) + warnings.warn('Will skip tests requiring Internet', ResourceWarning) + return False + return True + + +WE_HAVE_INTERNET = check_internet() + +needs_internet = pytest.mark.skipif(not WE_HAVE_INTERNET, reason='requires internet') +py3_only = pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") +nix_only = pytest.mark.skipif(os.name != 'nt', reason="doesn't run on windows") + + @pytest.fixture(scope='module') def pip_src_dir(request): old_src_dir = os.environ.get('PIP_SRC', '') @@ -42,13 +63,16 @@ def pip_src_dir(request): return request -class PipenvInstance(): +VERBOSE_COMMANDS = ('install', 'lock', 'uninstall') + + +class PipenvInstance(object): """An instance of a Pipenv Project...""" def __init__(self, pypi=None, pipfile=True, chdir=False): self.pypi = pypi self.original_umask = os.umask(0o007) self.original_dir = os.path.abspath(os.curdir) - self._path = TemporaryDirectory(suffix='project', prefix='pipenv') + self._path = TemporaryDirectory(suffix='-project', prefix='pipenv-') self.path = self._path.name # set file creation perms self.pipfile_path = None @@ -87,7 +111,11 @@ class PipenvInstance(): if self.pipfile_path: os.environ['PIPENV_PIPFILE'] = self.pipfile_path - c = delegator.run('pipenv {0}'.format(cmd), block=block) + with TemporaryDirectory(prefix='pipenv-', suffix='-cache') as tempdir: + os.environ['PIPENV_CACHE_DIR'] = tempdir.name + c = delegator.run('pipenv {0}'.format(cmd), block=block) + if 'PIPENV_CACHE_DIR' in os.environ: + del os.environ['PIPENV_CACHE_DIR'] if 'PIPENV_PIPFILE' in os.environ: del os.environ['PIPENV_PIPFILE'] @@ -178,6 +206,7 @@ class TestPipenv: assert 'Warning: Using both --reverse and --json together is not supported.' in c.err @pytest.mark.cli + @needs_internet def test_pipenv_check(self, pypi): with PipenvInstance(pypi=pypi) as p: p.pipenv('install requests==1.0.0') @@ -350,7 +379,8 @@ tablib = "*" shutil.copy(source_path, os.path.join(p.path, file_name)) os.mkdir(os.path.join(p.path, "tablib")) c = p.pipenv('install {}'.format(file_name)) - c = p.pipenv('uninstall --all --verbose') + assert c.return_code == 0 + c = p.pipenv('uninstall --all') assert c.return_code == 0 assert 'tablib' in c.out assert 'tablib' not in p.pipfile['packages'] @@ -438,7 +468,8 @@ setup( @pytest.mark.vcs @pytest.mark.install - def test_basic_vcs_install(self, pypi): + @needs_internet + def test_basic_vcs_install(self, pip_src_dir, pypi): with PipenvInstance(pypi=pypi) as p: c = p.pipenv('install git+https://github.com/requests/requests.git#egg=requests') assert c.return_code == 0 @@ -453,6 +484,7 @@ setup( @pytest.mark.e @pytest.mark.vcs @pytest.mark.install + @needs_internet def test_editable_vcs_install(self, pip_src_dir, pypi): with PipenvInstance(pypi=pypi) as p: c = p.pipenv('install -e git+https://github.com/requests/requests.git#egg=requests') @@ -542,8 +574,9 @@ tpfd = "*" @pytest.mark.install @pytest.mark.resolver @pytest.mark.backup_resolver - def test_backup_resolver(self, pypi): - with PipenvInstance(pypi=pypi) as p: + @needs_internet + def test_backup_resolver(self): + with PipenvInstance() as p: with open(p.pipfile_path, 'w') as f: contents = """ [packages] @@ -557,24 +590,22 @@ tpfd = "*" @pytest.mark.run @pytest.mark.markers - @pytest.mark.install - def test_package_environment_markers(self): + def test_package_environment_markers(self, pypi): - with PipenvInstance() as p: + with PipenvInstance(pypi=pypi) as p: with open(p.pipfile_path, 'w') as f: contents = """ [packages] -requests = {version = "*", markers="os_name=='splashwear'"} +tablib = {version = "*", markers="os_name=='splashwear'"} """.strip() f.write(contents) c = p.pipenv('install') assert c.return_code == 0 - assert 'Ignoring' in c.out - assert 'markers' in p.lockfile['default']['requests'] + assert 'markers' in p.lockfile['default']['tablib'] - c = p.pipenv('run python -c "import requests;"') + c = p.pipenv('run python -c "import tablib;"') assert c.return_code == 1 @pytest.mark.run @@ -601,10 +632,10 @@ requests = {version = "*", os_name = "== 'splashwear'"} @pytest.mark.markers @pytest.mark.install - def test_top_level_overrides_environment_markers(self): + def test_top_level_overrides_environment_markers(self, pypi): """Top-level environment markers should take precedence. """ - with PipenvInstance() as p: + with PipenvInstance(pypi=pypi) as p: with open(p.pipfile_path, 'w') as f: contents = """ [packages] @@ -620,7 +651,7 @@ funcsigs = {version = "*", os_name = "== 'splashwear'"} @pytest.mark.markers @pytest.mark.install - def test_global_overrides_environment_markers(self): + def test_global_overrides_environment_markers(self, pypi): """Empty (unconditional) dependency should take precedence. If a dependency is specified without environment markers, it should @@ -628,7 +659,7 @@ funcsigs = {version = "*", os_name = "== 'splashwear'"} APScheduler requires funcsigs only on Python 2, but since funcsigs is also specified as an unconditional dep, its markers should be empty. """ - with PipenvInstance() as p: + with PipenvInstance(pypi=pypi) as p: with open(p.pipfile_path, 'w') as f: contents = """ [packages] @@ -645,8 +676,11 @@ funcsigs = "*" @pytest.mark.install @pytest.mark.vcs @pytest.mark.tablib - def test_install_editable_git_tag(self, pip_src_dir, pypi): - with PipenvInstance(pypi=pypi) as p: + @needs_internet + def test_install_editable_git_tag(self, pip_src_dir): + # This uses the real PyPI since we need Internet to access the Git + # dependency anyway. + with PipenvInstance() as p: c = p.pipenv('install -e git+https://github.com/kennethreitz/tablib.git@v0.12.1#egg=tablib') assert c.return_code == 0 assert 'tablib' in p.pipfile['packages'] @@ -873,6 +907,21 @@ import records assert command == '{0}/bin/activate'.format(venv) + @pytest.mark.lock + def test_lock_handle_eggs(self, pypi): + """Ensure locking works with packages provoding egg formats. + """ + with PipenvInstance() as p: + with open(p.pipfile_path, 'w') as f: + f.write(""" +[packages] +RandomWords = "*" + """) + c = p.pipenv('lock --verbose') + assert c.return_code == 0 + assert 'randomwords' in p.lockfile['default'] + assert p.lockfile['default']['randomwords']['version'] == '==0.2.1' + @pytest.mark.lock @pytest.mark.requirements def test_lock_requirements_file(self, pypi): @@ -904,9 +953,11 @@ flask = "==0.12.2" @pytest.mark.lock @pytest.mark.complex - def test_complex_lock_with_vcs_deps(self, pip_src_dir, pypi): - - with PipenvInstance(pypi=pypi) as p: + @needs_internet + def test_complex_lock_with_vcs_deps(self, pip_src_dir): + # This uses the real PyPI since we need Internet to access the Git + # dependency anyway. + with PipenvInstance() as p: with open(p.pipfile_path, 'w') as f: contents = """ [packages] @@ -936,14 +987,34 @@ requests = {git = "https://github.com/requests/requests", egg = "requests"} @pytest.mark.lock @pytest.mark.requirements - @pytest.mark.complex - @pytest.mark.maya - def test_complex_deps_lock_and_install_properly(self, pypi): + def test_lock_with_prereleases(self, pypi): with PipenvInstance(pypi=pypi) as p: with open(p.pipfile_path, 'w') as f: contents = """ [packages] +sqlalchemy = "==1.2.0b3" + +[pipenv] +allow_prereleases = true + """.strip() + f.write(contents) + + c = p.pipenv('lock') + assert c.return_code == 0 + assert p.lockfile['default']['sqlalchemy']['version'] == '==1.2.0b3' + + @pytest.mark.lock + @pytest.mark.requirements + @pytest.mark.complex + @pytest.mark.maya + @needs_internet + def test_complex_deps_lock_and_install_properly(self): + # This uses the real PyPI because Maya has too many dependencies... + with PipenvInstance() as p: + with open(p.pipfile_path, 'w') as f: + contents = """ +[packages] maya = "*" """.strip() f.write(contents) @@ -956,12 +1027,13 @@ maya = "*" @pytest.mark.extras @pytest.mark.lock - @pytest.mark.requirements @pytest.mark.complex - def test_complex_lock_deep_extras(self, pypi): + @needs_internet + def test_complex_lock_deep_extras(self): # records[pandas] requires tablib[pandas] which requires pandas. + # This uses the real PyPI; Pandas has too many requirements to mock. - with PipenvInstance(pypi=pypi) as p: + with PipenvInstance() as p: with open(p.pipfile_path, 'w') as f: contents = """ [packages] @@ -969,12 +1041,12 @@ records = {extras = ["pandas"], version = "==0.5.2"} """.strip() f.write(contents) + c = p.pipenv('install') + assert c.return_code == 0 c = p.pipenv('lock') assert c.return_code == 0 assert 'tablib' in p.lockfile['default'] assert 'pandas' in p.lockfile['default'] - c = p.pipenv('install') - assert c.return_code == 0 @pytest.mark.lock @pytest.mark.deploy @@ -990,9 +1062,10 @@ flask = "==0.12.2" pytest = "==3.1.1" """.strip() f.write(contents) - - p.pipenv('lock') - + c = p.pipenv('install') + assert c.return_code == 0 + c = p.pipenv('lock') + assert c.return_code == 0 with open(p.pipfile_path, 'w') as f: contents = """ [packages] @@ -1006,6 +1079,7 @@ requests = "==2.14.0" @pytest.mark.install @pytest.mark.files @pytest.mark.urls + @needs_internet def test_urls_work(self, pypi): with PipenvInstance(pypi=pypi) as p: @@ -1058,6 +1132,7 @@ requests = "==2.14.0" shutil.copy(source_path, os.path.join(p.path, file_name)) c = p.pipenv('install {}'.format(file_name)) + assert c.return_code == 0 key = [k for k in p.pipfile['packages'].keys()][0] dep = p.pipfile['packages'][key] @@ -1072,6 +1147,7 @@ requests = "==2.14.0" @pytest.mark.install @pytest.mark.files @pytest.mark.urls + @needs_internet def test_install_remote_requirements(self, pypi): with PipenvInstance(pypi=pypi) as p: # using a github hosted requirements.txt file @@ -1139,10 +1215,8 @@ requests = "==2.14.0" url = 'https://${PYPI_USERNAME}:${PYPI_PASSWORD}@pypi.python.org/simple' verify_ssl = true name = 'pypi' - [requires] python_version = '2.7' - [packages] flask = "==0.12.2" """) @@ -1164,14 +1238,16 @@ flask = "==0.12.2" assert Project().get_lockfile_hash() != Project().calculate_pipfile_hash() @pytest.mark.run - def test_scripts_basic(self): + def test_scripts(self): with PipenvInstance(chdir=True) as p: with open(p.pipfile_path, 'w') as f: - f.write(""" + f.write(r""" [scripts] -printfoo = "python -c print('foo')" +printfoo = "python -c \"print('foo')\"" +notfoundscript = "randomthingtotally" +appendscript = "cmd arg1" +multicommand = "bash -c \"cd docs && make html\"" """) - c = p.pipenv('install') assert c.return_code == 0 @@ -1179,21 +1255,34 @@ printfoo = "python -c print('foo')" assert c.return_code == 0 assert c.out == 'foo\n' assert c.err == '' + if os.name != 'nt': + c = p.pipenv('run notfoundscript') + assert c.return_code == 1 + assert c.out == '' + assert 'Error' in c.err + assert 'randomthingtotally (from notfoundscript)' in c.err + executable, argv = _get_command_posix(Project(), 'multicommand', []) + assert executable == 'bash' + assert argv == ['-c', 'cd docs && make html'] + executable, argv = _get_command_posix(Project(), 'appendscript', ['a', 'b']) + assert executable == 'cmd' + assert argv == ['arg1', 'a', 'b'] - @pytest.mark.run - @pytest.mark.skip(reason='This fails on Windows (not sure about POSIX).') - def test_scripts_quoted(self): + @pytest.mark.lock + @pytest.mark.complex + @py3_only + def test_resolver_unique_markers(self, pypi): + """vcrpy has a dependency on `yarl` which comes with a marker + of 'python version in "3.4, 3.5, 3.6" - this marker duplicates itself: + + 'yarl; python version in "3.4, 3.5, 3.6"; python version in "3.4, 3.5, 3.6"' + + This verifies that we clean that successfully. + """ with PipenvInstance(chdir=True) as p: - with open(p.pipfile_path, 'w') as f: - f.write(""" -[scripts] -printfoo = "python -c print('foo')" - """) - - c = p.pipenv('install') + c = p.pipenv('install vcrpy==1.11.0') assert c.return_code == 0 - - c = p.pipenv('run printfoo') - assert c.return_code == 0 - assert c.out == 'foo\n' - assert c.err == '' + assert 'yarl' in p.lockfile['default'] + yarl = p.lockfile['default']['yarl'] + assert 'markers' in yarl + assert yarl['markers'] == "python_version in '3.4, 3.5, 3.6'"