diff --git a/news/3005.bugfix b/news/3005.bugfix new file mode 100644 index 00000000..b4022a25 --- /dev/null +++ b/news/3005.bugfix @@ -0,0 +1 @@ +Fixed a bug which prevented installing the local directory in non-editable mode. diff --git a/pipenv/core.py b/pipenv/core.py index 610a266d..ca37bd5c 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -1315,6 +1315,12 @@ def pip_install( from .vendor.urllib3.util import parse_url src = [] + write_to_tmpfile = False + if requirement: + editable_with_markers = requirement.editable and requirement.markers + needs_hashes = not requirement.editable and not ignore_hashes and r is None + write_to_tmpfile = needs_hashes or editable_with_markers + if not trusted_hosts: trusted_hosts = [] trusted_hosts.extend(os.environ.get("PIP_TRUSTED_HOSTS", [])) @@ -1326,12 +1332,13 @@ def pip_install( err=True, ) # Create files for hash mode. - if requirement and not requirement.editable and (not ignore_hashes) and (r is None): - fd, r = tempfile.mkstemp( - prefix="pipenv-", suffix="-requirement.txt", dir=requirements_dir - ) - with os.fdopen(fd, "w") as f: - f.write(requirement.as_line()) + if write_to_tmpfile: + with vistir.compat.NamedTemporaryFile( + prefix="pipenv-", suffix="-requirement.txt", dir=requirements_dir, + delete=False + ) as f: + f.write(vistir.misc.to_bytes(requirement.as_line())) + r = f.name # Install dependencies when a package is a VCS dependency. if requirement and requirement.vcs: no_deps = False @@ -1374,7 +1381,7 @@ def pip_install( create_mirror_source(pypi_mirror) if is_pypi_url(source["url"]) else source for source in sources ] - if (requirement and requirement.editable) or not r: + if (requirement and requirement.editable) and not r: install_reqs = requirement.as_line(as_list=True) if requirement.editable and install_reqs[0].startswith("-e "): req, install_reqs = install_reqs[0], install_reqs[1:] @@ -1382,11 +1389,14 @@ def pip_install( install_reqs = [editable_opt, req] + install_reqs if not any(item.startswith("--hash") for item in install_reqs): ignore_hashes = True - else: + elif r: install_reqs = ["-r", r] with open(r) as f: if "--hash" not in f.read(): ignore_hashes = True + else: + ignore_hashes = True if not requirement.hashes else False + install_reqs = requirement.as_line(as_list=True) pip_command = [which_pip(allow_global=allow_global), "install"] if pre: pip_command.append("--pre") @@ -1804,9 +1814,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) - # Capture . argument and assign it to nothing - if len(packages) == 1 and packages[0] == ".": - packages = False # 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. @@ -2512,24 +2519,28 @@ def do_sync( def do_clean(ctx, three=None, python=None, dry_run=False, bare=False, pypi_mirror=None): # Ensure that virtualenv is available. - + from packaging.utils import canonicalize_name ensure_project(three=three, python=python, validate=False, pypi_mirror=pypi_mirror) ensure_lockfile(pypi_mirror=pypi_mirror) installed_package_names = [ - pkg.project_name for pkg in project.get_installed_packages() + canonicalize_name(pkg.project_name) for pkg in project.get_installed_packages() ] # Remove known "bad packages" from the list. for bad_package in BAD_PACKAGES: - if bad_package in installed_package_names: + if canonicalize_name(bad_package) in installed_package_names: if environments.is_verbose(): click.echo("Ignoring {0}.".format(repr(bad_package)), err=True) - del installed_package_names[installed_package_names.index(bad_package)] + del installed_package_names[installed_package_names.index( + canonicalize_name(bad_package) + )] # Intelligently detect if --dev should be used or not. - develop = [k.lower() for k in project.lockfile_content["develop"].keys()] - default = [k.lower() for k in project.lockfile_content["default"].keys()] + develop = [canonicalize_name(k) for k in project.lockfile_content["develop"].keys()] + default = [canonicalize_name(k) for k in project.lockfile_content["default"].keys()] for used_package in set(develop + default): if used_package in installed_package_names: - del installed_package_names[installed_package_names.index(used_package)] + del installed_package_names[installed_package_names.index( + canonicalize_name(used_package) + )] failure = False for apparent_bad_package in installed_package_names: if dry_run: diff --git a/pipenv/vendor/pythonfinder/__init__.py b/pipenv/vendor/pythonfinder/__init__.py index 7c9c6662..672724b4 100644 --- a/pipenv/vendor/pythonfinder/__init__.py +++ b/pipenv/vendor/pythonfinder/__init__.py @@ -1,6 +1,14 @@ from __future__ import print_function, absolute_import -__version__ = '1.1.1' +__version__ = '1.1.2' + +# Add NullHandler to "pythonfinder" logger, because Python2's default root +# logger has no handler and warnings like this would be reported: +# +# > No handlers could be found for logger "pythonfinder.models.pyenv" +import logging +logger = logging.getLogger(__name__) +logger.addHandler(logging.NullHandler()) __all__ = ["Finder", "WindowsFinder", "SystemPath", "InvalidPythonVersion"] from .pythonfinder import Finder diff --git a/pipenv/vendor/pythonfinder/cli.py b/pipenv/vendor/pythonfinder/cli.py index 64d3372d..b5aa7da3 100644 --- a/pipenv/vendor/pythonfinder/cli.py +++ b/pipenv/vendor/pythonfinder/cli.py @@ -17,9 +17,10 @@ from .pythonfinder import Finder @click.option( "--version", is_flag=True, default=False, help="Display PythonFinder version." ) -# @click.version_option(prog_name=crayons.normal('pyfinder', bold=True), version=__version__) +@click.option("--ignore-unsupported/--no-unsupported", is_flag=True, default=True, help="Ignore unsupported python versions.") +@click.version_option(prog_name='pyfinder', version=__version__) @click.pass_context -def cli(ctx, find=False, which=False, findall=False, version=False): +def cli(ctx, find=False, which=False, findall=False, version=False, ignore_unsupported=True): if version: click.echo( "{0} version {1}".format( @@ -27,7 +28,7 @@ def cli(ctx, find=False, which=False, findall=False, version=False): ) ) sys.exit(0) - finder = Finder() + finder = Finder(ignore_unsupported=ignore_unsupported) if findall: versions = finder.find_all_python_versions() if versions: @@ -46,7 +47,6 @@ def cli(ctx, find=False, which=False, findall=False, version=False): fg="red", ) if find: - if any([find.startswith("{0}".format(n)) for n in range(10)]): found = finder.find_python_version(find.strip()) else: diff --git a/pipenv/vendor/pythonfinder/models/path.py b/pipenv/vendor/pythonfinder/models/path.py index 8e38aef3..c90d9be3 100644 --- a/pipenv/vendor/pythonfinder/models/path.py +++ b/pipenv/vendor/pythonfinder/models/path.py @@ -37,6 +37,7 @@ class SystemPath(object): pyenv_finder = attr.ib(default=None, validator=optional_instance_of("PyenvPath")) system = attr.ib(default=False) _version_dict = attr.ib(default=attr.Factory(defaultdict)) + ignore_unsupported = attr.ib(default=False) __finders = attr.ib(default=attr.Factory(dict)) @@ -129,7 +130,7 @@ class SystemPath(object): pyenv_index = self.path_order.index(last_pyenv) except ValueError: return - self.pyenv_finder = PyenvFinder.create(root=PYENV_ROOT) + self.pyenv_finder = PyenvFinder.create(root=PYENV_ROOT, ignore_unsupported=self.ignore_unsupported) # paths = (v.paths.values() for v in self.pyenv_finder.versions.values()) root_paths = ( p for path in self.pyenv_finder.expanded_paths for p in path if p.is_root @@ -268,7 +269,7 @@ class SystemPath(object): return ver @classmethod - def create(cls, path=None, system=False, only_python=False, global_search=True): + def create(cls, path=None, system=False, only_python=False, global_search=True, ignore_unsupported=False): """Create a new :class:`pythonfinder.models.SystemPath` instance. :param path: Search path to prepend when searching, defaults to None @@ -277,6 +278,8 @@ class SystemPath(object): :param system: bool, optional :param only_python: Whether to search only for python executables, defaults to False :param only_python: bool, optional + :param ignore_unsupported: Whether to ignore unsupported python versions, if False, an error is raised, defaults to True + :param ignore_unsupported: bool, optional :return: A new :class:`pythonfinder.models.SystemPath` instance. :rtype: :class:`pythonfinder.models.SystemPath` """ @@ -303,6 +306,7 @@ class SystemPath(object): only_python=only_python, system=system, global_search=global_search, + ignore_unsupported=ignore_unsupported, ) diff --git a/pipenv/vendor/pythonfinder/models/pyenv.py b/pipenv/vendor/pythonfinder/models/pyenv.py index 6df47179..527c5f0a 100644 --- a/pipenv/vendor/pythonfinder/models/pyenv.py +++ b/pipenv/vendor/pythonfinder/models/pyenv.py @@ -1,35 +1,81 @@ # -*- coding=utf-8 -*- from __future__ import absolute_import, print_function +import logging + from collections import defaultdict import attr +import sysconfig from vistir.compat import Path -from ..utils import ensure_path, optional_instance_of +from ..utils import ensure_path, optional_instance_of, get_python_version, filter_pythons from .mixins import BaseFinder from .path import VersionPath from .python import PythonVersion +logger = logging.getLogger(__name__) + + @attr.s class PyenvFinder(BaseFinder): root = attr.ib(default=None, validator=optional_instance_of(Path)) + # ignore_unsupported should come before versions, because its value is used + # in versions's default initializer. + ignore_unsupported = attr.ib(default=False) versions = attr.ib() pythons = attr.ib() + @classmethod + def version_from_bin_dir(cls, base_dir): + pythons = [py for py in filter_pythons(base_dir)] + py_version = None + for py in pythons: + version = get_python_version(py.as_posix()) + try: + py_version = PythonVersion.parse(version) + except Exception: + continue + if py_version: + return py_version + return + @versions.default def get_versions(self): versions = defaultdict(VersionPath) + bin_ = sysconfig._INSTALL_SCHEMES[sysconfig._get_default_scheme()]["scripts"] for p in self.root.glob("versions/*"): - version = PythonVersion.parse(p.name) + if p.parent.name == "envs": + continue + try: + version = PythonVersion.parse(p.name) + except ValueError: + bin_dir = Path(bin_.format(base=p.as_posix())) + if bin_dir.exists(): + version = self.version_from_bin_dir(bin_dir) + if not version: + if not self.ignore_unsupported: + raise + continue + except Exception: + if not self.ignore_unsupported: + raise + logger.warning( + 'Unsupported Python version %r, ignoring...', + p.name, exc_info=True + ) + continue + if not version: + continue version_tuple = ( version.get("major"), version.get("minor"), version.get("patch"), version.get("is_prerelease"), version.get("is_devrelease"), + version.get("is_debug") ) versions[version_tuple] = VersionPath.create( path=p.resolve(), only_python=True @@ -47,6 +93,6 @@ class PyenvFinder(BaseFinder): return pythons @classmethod - def create(cls, root): + def create(cls, root, ignore_unsupported=False): root = ensure_path(root) - return cls(root=root) + return cls(root=root, ignore_unsupported=ignore_unsupported) diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index 4d8b0361..c71b9d9b 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -8,7 +8,7 @@ from collections import defaultdict import attr -from packaging.version import Version +from packaging.version import Version, LegacyVersion from packaging.version import parse as parse_version from ..environment import SYSTEM_ARCH @@ -65,10 +65,11 @@ class PythonVersion(object): self.patch, self.is_prerelease, self.is_devrelease, + self.is_debug ) def matches( - self, major=None, minor=None, patch=None, pre=False, dev=False, arch=None + self, major=None, minor=None, patch=None, pre=False, dev=False, arch=None, debug=False ): if arch and arch.isdigit(): arch = "{0}bit".format(arch) @@ -79,6 +80,7 @@ class PythonVersion(object): and (pre is None or self.is_prerelease == pre) and (dev is None or self.is_devrelease == dev) and (arch is None or self.architecture == arch) + and (debug is None or self.is_debug == debug) ) def as_major(self): @@ -108,7 +110,7 @@ class PythonVersion(object): is_debug = False if version.endswith("-debug"): is_debug = True - version, _, _ = verson.rpartition("-") + version, _, _ = version.rpartition("-") try: version = parse_version(str(version)) except TypeError: diff --git a/pipenv/vendor/pythonfinder/pythonfinder.py b/pipenv/vendor/pythonfinder/pythonfinder.py index 035842e2..e965bb51 100644 --- a/pipenv/vendor/pythonfinder/pythonfinder.py +++ b/pipenv/vendor/pythonfinder/pythonfinder.py @@ -7,27 +7,30 @@ from .models import SystemPath class Finder(object): - def __init__(self, path=None, system=False, global_search=True): + def __init__(self, path=None, system=False, global_search=True, ignore_unsupported=False): """Finder A cross-platform Finder for locating python and other executables. - + Searches for python and other specified binaries starting in `path`, if supplied, but searching the bin path of `sys.executable` if `system=True`, and then searching in the `os.environ['PATH']` if `global_search=True`. When `global_search` is `False`, this search operation is restricted to the allowed locations of `path` and `system`. - + :param path: A bin-directory search location, defaults to None :param path: str, optional :param system: Whether to include the bin-dir of `sys.executable`, defaults to False :param system: bool, optional :param global_search: Whether to search the global path from os.environ, defaults to True :param global_search: bool, optional + :param ignore_unsupported: Whether to ignore unsupported python versions, if False, an error is raised, defaults to True + :param ignore_unsupported: bool, optional :returns: a :class:`~pythonfinder.pythonfinder.Finder` object. """ self.path_prepend = path self.global_search = global_search self.system = system + self.ignore_unsupported = ignore_unsupported self._system_path = None self._windows_finder = None @@ -38,6 +41,7 @@ class Finder(object): path=self.path_prepend, system=self.system, global_search=self.global_search, + ignore_unsupported=self.ignore_unsupported, ) return self._system_path diff --git a/pipenv/vendor/pythonfinder/utils.py b/pipenv/vendor/pythonfinder/utils.py index 6494d243..dced9eab 100644 --- a/pipenv/vendor/pythonfinder/utils.py +++ b/pipenv/vendor/pythonfinder/utils.py @@ -17,7 +17,12 @@ import vistir from .exceptions import InvalidPythonVersion -PYTHON_IMPLEMENTATIONS = ("python", "ironpython", "jython", "pypy") +PYTHON_IMPLEMENTATIONS = ( + "python", "ironpython", "jython", "pypy", "anaconda", "miniconda", + "stackless", "activepython" +) +RULES_BASE = ["*{0}", "*{0}?", "*{0}?.?", "*{0}?.?m"] +RULES = [rule.format(impl) for impl in PYTHON_IMPLEMENTATIONS for rule in RULES_BASE] KNOWN_EXTS = {"exe", "py", "fish", "sh", ""} KNOWN_EXTS = KNOWN_EXTS | set( @@ -34,7 +39,7 @@ def get_python_version(path): raise InvalidPythonVersion("%s is not a valid python path" % path) if not out: raise InvalidPythonVersion("%s is not a valid python path" % path) - return out + return out.strip() def optional_instance_of(cls): @@ -58,9 +63,8 @@ def path_is_known_executable(path): def looks_like_python(name): - rules = ["*python", "*python?", "*python?.?", "*python?.?m"] match_rules = [] - for rule in rules: + for rule in RULES: match_rules.extend( [ "{0}.{1}".format(rule, ext) if ext else "{0}".format(rule) diff --git a/pipenv/vendor/vendor.txt b/pipenv/vendor/vendor.txt index a7761f66..7a574b9f 100644 --- a/pipenv/vendor/vendor.txt +++ b/pipenv/vendor/vendor.txt @@ -21,7 +21,7 @@ pipdeptree==0.13.0 pipreqs==0.4.9 docopt==0.6.2 yarg==0.1.9 -pythonfinder==1.1.1 +pythonfinder==1.1.2 requests==2.19.1 chardet==3.0.4 idna==2.7 diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 76a429e0..12f27342 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -121,6 +121,7 @@ class _PipenvInstance(object): def pipenv(self, cmd, block=True): if self.pipfile_path: os.environ['PIPENV_PIPFILE'] = self.pipfile_path + # a bit of a hack to make sure the virtualenv is created with TemporaryDirectory(prefix='pipenv-', suffix='-cache') as tempdir: os.environ['PIPENV_CACHE_DIR'] = tempdir.name @@ -136,6 +137,8 @@ class _PipenvInstance(object): print('$ pipenv {0}'.format(cmd)) print(c.out) print(c.err) + if c.return_code != 0: + print("Command failed...") # Where the action happens. return c diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index 0dd3b6ed..7ebcee1d 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -12,56 +12,81 @@ from pipenv.utils import normalize_drive @pytest.mark.cli def test_pipenv_where(PipenvInstance, pypi_secure): with PipenvInstance(pypi=pypi_secure) as p: - assert normalize_drive(p.path) in p.pipenv('--where').out + c = p.pipenv("--where") + assert c.ok + assert normalize_drive(p.path) in c.out @pytest.mark.cli def test_pipenv_venv(PipenvInstance): with PipenvInstance() as p: - p.pipenv('--python python') - venv_path = p.pipenv('--venv').out.strip() + c = p.pipenv('--python python') + assert c.ok + c = p.pipenv('--venv') + assert c.ok + venv_path = c.out.strip() assert os.path.isdir(venv_path) @pytest.mark.cli def test_pipenv_py(PipenvInstance): with PipenvInstance() as p: - p.pipenv('--python python') - python = p.pipenv('--py').out.strip() + c = p.pipenv('--python python') + assert c.ok + c = p.pipenv('--py') + assert c.ok + python = c.out.strip() assert os.path.basename(python).startswith('python') @pytest.mark.cli def test_pipenv_support(PipenvInstance): with PipenvInstance() as p: - assert p.pipenv('--support').out + c = p.pipenv('--support') + assert c.ok + assert c.out @pytest.mark.cli def test_pipenv_rm(PipenvInstance): with PipenvInstance() as p: - p.pipenv('--python python') - venv_path = p.pipenv('--venv').out.strip() + c = p.pipenv('--python python') + assert c.ok + c = p.pipenv('--venv') + assert c.ok + venv_path = c.out.strip() assert os.path.isdir(venv_path) - assert p.pipenv('--rm').out + c = p.pipenv('--rm') + assert c.ok + assert c.out assert not os.path.isdir(venv_path) @pytest.mark.cli def test_pipenv_graph(PipenvInstance, pypi): with PipenvInstance(pypi=pypi) as p: - p.pipenv('install requests') - assert 'requests' in p.pipenv('graph').out - assert 'requests' in p.pipenv('graph --json').out - assert 'requests' in p.pipenv('graph --json-tree').out + c = p.pipenv('install requests') + assert c.ok + graph = p.pipenv("graph") + assert graph.ok + assert "requests" in graph.out + graph_json = p.pipenv("graph --json") + assert graph_json.ok + assert "requests" in graph_json.out + graph_json_tree = p.pipenv("graph --json-tree") + assert graph_json_tree.ok + assert "requests" in graph_json_tree.out @pytest.mark.cli def test_pipenv_graph_reverse(PipenvInstance, pypi): with PipenvInstance(pypi=pypi) as p: - p.pipenv('install requests==2.18.4') - output = p.pipenv('graph --reverse').out + c = p.pipenv('install requests==2.18.4') + assert c.ok + c = p.pipenv('graph --reverse') + assert c.ok + output = c.out requests_dependency = [ ('certifi', 'certifi>=2017.4.17'), @@ -91,8 +116,10 @@ def test_pipenv_check(PipenvInstance, pypi): c = p.pipenv('check') assert c.return_code != 0 assert 'requests' in c.out - p.pipenv('uninstall requests') - p.pipenv('install six') + c = p.pipenv('uninstall requests') + assert c.ok + c = p.pipenv('install six') + assert c.ok c = p.pipenv('check --ignore 35015') assert c.return_code == 0 assert 'Ignoring' in c.err