Skip to content

Commit

Permalink
Reject project that have neither a pyproject.toml nor a setup.py
Browse files Browse the repository at this point in the history
Before it sometimes errored out.
  • Loading branch information
sbidoul committed Oct 12, 2021
1 parent d85178f commit 1f595b8
Show file tree
Hide file tree
Showing 7 changed files with 33 additions and 25 deletions.
2 changes: 2 additions & 0 deletions news/10531.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Always refuse installing or building projects that have no ``pyproject.toml`` nor
``setup.py``.
6 changes: 6 additions & 0 deletions src/pip/_internal/pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 1 addition & 7 deletions src/pip/_internal/req/constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
3 changes: 3 additions & 0 deletions tests/data/src/pep517_setup_cfg_only/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[metadata]
name = "dummy"
version = "0.1"
15 changes: 8 additions & 7 deletions tests/functional/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/test_pep517.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
[
Expand Down
11 changes: 0 additions & 11 deletions tests/unit/test_req.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 1f595b8

Please sign in to comment.