From 1f595b893bbc10a9091910a85840b0ae42ea2ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 2 Oct 2021 18:06:07 +0200 Subject: [PATCH] Reject project that have neither a pyproject.toml nor a setup.py Before it sometimes errored out. --- news/10531.bugfix.rst | 2 ++ src/pip/_internal/pyproject.py | 6 ++++++ src/pip/_internal/req/constructors.py | 8 +------- tests/data/src/pep517_setup_cfg_only/setup.cfg | 3 +++ tests/functional/test_install.py | 15 ++++++++------- tests/unit/test_pep517.py | 13 +++++++++++++ tests/unit/test_req.py | 11 ----------- 7 files changed, 33 insertions(+), 25 deletions(-) create mode 100644 news/10531.bugfix.rst create mode 100644 tests/data/src/pep517_setup_cfg_only/setup.cfg diff --git a/news/10531.bugfix.rst b/news/10531.bugfix.rst new file mode 100644 index 00000000000..0a33944a10d --- /dev/null +++ b/news/10531.bugfix.rst @@ -0,0 +1,2 @@ +Always refuse installing or building projects that have no ``pyproject.toml`` nor +``setup.py``. diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index 0b3a6cde64f..34511d9c408 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -48,6 +48,12 @@ def load_pyproject_toml( has_pyproject = os.path.isfile(pyproject_toml) has_setup = os.path.isfile(setup_py) + if not has_pyproject and not has_setup: + raise InstallationError( + f"{req_name} does not appear to be a Python project: " + f"no pyproject.toml or setup.py" + ) + if has_pyproject: with open(pyproject_toml, encoding="utf-8") as f: pp_toml = tomli.load(f) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 5cf923515d7..b5037d64c95 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -25,7 +25,6 @@ from pip._internal.req.req_file import ParsedRequirement from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.filetypes import is_archive_file -from pip._internal.utils.misc import is_installable_dir from pip._internal.utils.packaging import get_requirement from pip._internal.utils.urls import path_to_url from pip._internal.vcs import is_url, vcs @@ -233,12 +232,7 @@ def _get_url_from_path(path: str, name: str) -> Optional[str]: an @, it will treat it as a PEP 440 URL requirement and return the path. """ if _looks_like_path(name) and os.path.isdir(path): - if is_installable_dir(path): - return path_to_url(path) - raise InstallationError( - f"Directory {name!r} is not installable. Neither 'setup.py' " - "nor 'pyproject.toml' found." - ) + return path_to_url(path) if not is_archive_file(path): return None if os.path.isfile(path): diff --git a/tests/data/src/pep517_setup_cfg_only/setup.cfg b/tests/data/src/pep517_setup_cfg_only/setup.cfg new file mode 100644 index 00000000000..4d62ef58d5a --- /dev/null +++ b/tests/data/src/pep517_setup_cfg_only/setup.cfg @@ -0,0 +1,3 @@ +[metadata] +name = "dummy" +version = "0.1" diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 355113e37d3..9f77f859315 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -653,8 +653,10 @@ def test_install_from_local_directory_with_no_setup_py(script, data): """ result = script.pip("install", data.root, expect_error=True) assert not result.files_created - assert "is not installable." in result.stderr - assert "Neither 'setup.py' nor 'pyproject.toml' found." in result.stderr + assert ( + "does not appear to be a Python project: no pyproject.toml or setup.py" + in result.stderr + ) def test_editable_install__local_dir_no_setup_py(script, data): @@ -663,11 +665,10 @@ def test_editable_install__local_dir_no_setup_py(script, data): """ result = script.pip("install", "-e", data.root, expect_error=True) assert not result.files_created - - msg = result.stderr - assert msg.startswith("ERROR: File 'setup.py' or 'setup.cfg' not found ") - assert "cannot be installed in editable mode" in msg - assert "pyproject.toml" not in msg + assert ( + "does not appear to be a Python project: no pyproject.toml or setup.py" + in result.stderr + ) def test_editable_install__local_dir_no_setup_py_with_pyproject(script): diff --git a/tests/unit/test_pep517.py b/tests/unit/test_pep517.py index b18299d7039..964bea1b43f 100644 --- a/tests/unit/test_pep517.py +++ b/tests/unit/test_pep517.py @@ -27,6 +27,19 @@ def test_use_pep517(shared_data: TestData, source: str, expected: bool) -> None: assert req.use_pep517 is expected +def test_use_pep517_rejects_setup_cfg_only(shared_data: TestData) -> None: + """ + Test that projects with setup.cfg but no pyproject.toml are rejected. + """ + src = shared_data.src.joinpath("pep517_setup_cfg_only") + req = InstallRequirement(None, None) + req.source_dir = src # make req believe it has been unpacked + with pytest.raises(InstallationError) as e: + req.load_pyproject_toml() + err_msg = e.value.args[0] + assert "no pyproject.toml or setup.py" in err_msg + + @pytest.mark.parametrize( ("source", "msg"), [ diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index c4b97cf1c30..f5d393ff458 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -799,14 +799,3 @@ def test_get_url_from_path__installable_dir( path = os.path.join("/path/to/" + name) url = path_to_url(path) assert _get_url_from_path(path, name) == url - - -@mock.patch("pip._internal.req.req_install.os.path.isdir") -def test_get_url_from_path__installable_error(isdir_mock: mock.Mock) -> None: - isdir_mock.return_value = True - name = "some/setuptools/project" - path = os.path.join("/path/to/" + name) - with pytest.raises(InstallationError) as e: - _get_url_from_path(path, name) - err_msg = e.value.args[0] - assert "Neither 'setup.py' nor 'pyproject.toml' found" in err_msg