Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better detection of invalid __legacy__ PEP 517 projects #10534

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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``.
1 change: 1 addition & 0 deletions src/pip/_internal/operations/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ def _ensure_link_req_src_dir(
# installation.
# FIXME: this won't upgrade when there's an existing
# package unpacked in `req.source_dir`
# TODO: this check is now probably dead code
if is_installable_dir(req.source_dir):
raise PreviousBuildDirError(
"pip can't proceed with requirements '{}' due to a"
Expand Down
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)
pradyunsg marked this conversation as resolved.
Show resolved Hide resolved
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