From d619aba150ff913aa36d3ef3a02f6b2016c652f3 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 26 Jan 2019 06:15:23 -0800 Subject: [PATCH] Provide a better error message for a pyproject.toml editable install. The message looks like this: File "setup.py" not found. Directory cannot be installed in editable mode: (A "pyproject.toml" file was found, but editable mode currently requires a setup.py based build.) --- news/6170.feature | 2 ++ src/pip/_internal/pyproject.py | 12 ++++++++++ src/pip/_internal/req/constructors.py | 15 +++++++++--- src/pip/_internal/req/req_install.py | 10 ++------ tests/functional/test_install.py | 34 ++++++++++++++++++++++++--- 5 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 news/6170.feature diff --git a/news/6170.feature b/news/6170.feature new file mode 100644 index 00000000000..34ce4b5358e --- /dev/null +++ b/news/6170.feature @@ -0,0 +1,2 @@ +Provide a better error message if attempting an editable install of a +directory with a ``pyproject.toml`` but no ``setup.py``. diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index d3e1bbe7ded..1de4b62a75a 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -2,6 +2,7 @@ import io import os +import sys from pip._vendor import pytoml, six @@ -20,6 +21,17 @@ def _is_list_of_str(obj): ) +def make_pyproject_path(setup_py_dir): + # type: (str) -> str + path = os.path.join(setup_py_dir, 'pyproject.toml') + + # Python2 __file__ should not be unicode + if six.PY2 and isinstance(path, six.text_type): + path = path.encode(sys.getfilesystemencoding()) + + return path + + def load_pyproject_toml( use_pep517, # type: Optional[bool] pyproject_toml, # type: str diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 755c10d576f..1eed1dd38da 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -23,6 +23,7 @@ from pip._internal.exceptions import InstallationError from pip._internal.models.index import PyPI, TestPyPI from pip._internal.models.link import Link +from pip._internal.pyproject import make_pyproject_path from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.misc import is_installable_dir from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -77,10 +78,18 @@ def parse_editable(editable_req): if os.path.isdir(url_no_extras): if not os.path.exists(os.path.join(url_no_extras, 'setup.py')): - raise InstallationError( - "Directory %r is not installable. File 'setup.py' not found." % - url_no_extras + msg = ( + 'File "setup.py" not found. Directory cannot be installed ' + 'in editable mode: {}'.format(os.path.abspath(url_no_extras)) ) + pyproject_path = make_pyproject_path(url_no_extras) + if os.path.isfile(pyproject_path): + msg += ( + '\n(A "pyproject.toml" file was found, but editable ' + 'mode currently requires a setup.py based build.)' + ) + raise InstallationError(msg) + # Treating it as code that has already been checked out url_no_extras = path_to_url(url_no_extras) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index c5dd2bd524f..a4834b00c63 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -22,7 +22,7 @@ PIP_DELETE_MARKER_FILENAME, running_under_virtualenv, ) from pip._internal.models.link import Link -from pip._internal.pyproject import load_pyproject_toml +from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet from pip._internal.utils.compat import native_str from pip._internal.utils.hashes import Hashes @@ -471,13 +471,7 @@ def pyproject_toml(self): # type: () -> str assert self.source_dir, "No source dir for %s" % self - pp_toml = os.path.join(self.setup_py_dir, 'pyproject.toml') - - # Python2 __file__ should not be unicode - if six.PY2 and isinstance(pp_toml, six.text_type): - pp_toml = pp_toml.encode(sys.getfilesystemencoding()) - - return pp_toml + return make_pyproject_path(self.setup_py_dir) def load_pyproject_toml(self): # type: () -> None diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 19794ef55d0..38a771d998a 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -491,13 +491,41 @@ def test_install_from_local_directory_with_no_setup_py(script, data): assert "Neither 'setup.py' nor 'pyproject.toml' found." in result.stderr -def test_editable_install_from_local_directory_with_no_setup_py(script, data): +def test_editable_install__local_dir_no_setup_py( + script, data, deprecated_python): """ - Test installing from a local directory with no 'setup.py'. + Test installing in editable mode from a local directory with no setup.py. """ result = script.pip('install', '-e', data.root, expect_error=True) assert not result.files_created - assert "is not installable. File 'setup.py' not found." in result.stderr + + msg = result.stderr + if deprecated_python: + assert 'File "setup.py" not found. ' in msg + else: + assert msg.startswith('File "setup.py" not found. ') + assert 'pyproject.toml' not in msg + + +def test_editable_install__local_dir_no_setup_py_with_pyproject( + script, deprecated_python): + """ + Test installing in editable mode from a local directory with no setup.py + but that does have pyproject.toml. + """ + local_dir = script.scratch_path.join('temp').mkdir() + pyproject_path = local_dir.join('pyproject.toml') + pyproject_path.write('') + + result = script.pip('install', '-e', local_dir, expect_error=True) + assert not result.files_created + + msg = result.stderr + if deprecated_python: + assert 'File "setup.py" not found. ' in msg + else: + assert msg.startswith('File "setup.py" not found. ') + assert 'A "pyproject.toml" file was found' in msg @pytest.mark.skipif("sys.version_info >= (3,4)")