Skip to content

Commit fca233c

Browse files
committed
Implement diagnostic errors for build-system.requires issues
This demonstrates how the new diagnostic errors are to implement, and how they get presented to users.
1 parent 3a02455 commit fca233c

File tree

4 files changed

+76
-42
lines changed

4 files changed

+76
-42
lines changed

src/pip/_internal/exceptions.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,41 @@ class UninstallationError(PipError):
109109
"""General exception during uninstallation"""
110110

111111

112+
class MissingPyProjectBuildRequires(DiagnosticPipError):
113+
"""Raised when pyproject.toml has `build-system`, but no `build-system.requires`."""
114+
115+
reference = "missing-pyproject-build-system-requires"
116+
117+
def __init__(self, *, package: str) -> None:
118+
super().__init__(
119+
message=f"Can not process {package}",
120+
context=(
121+
"This package has an invalid pyproject.toml file.\n"
122+
"The [build-system] table is missing the mandatory `requires` key."
123+
),
124+
attention_stmt="This is an issue with the package noted above, not pip.",
125+
hint_stmt="See PEP 518 for the detailed specification.",
126+
)
127+
128+
129+
class InvalidPyProjectBuildRequires(DiagnosticPipError):
130+
"""Raised when pyproject.toml an invalid `build-system.requires`."""
131+
132+
reference = "invalid-pyproject-build-system-requires"
133+
134+
def __init__(self, *, package: str, reason: str) -> None:
135+
super().__init__(
136+
message=f"Can not process {package}",
137+
context=(
138+
"This package has an invalid `build-system.requires` key in "
139+
"pyproject.toml.\n"
140+
f"{reason}"
141+
),
142+
hint_stmt="See PEP 518 for the detailed specification.",
143+
attention_stmt="This is an issue with the package noted above, not pip.",
144+
)
145+
146+
112147
class NoneMetadataError(PipError):
113148
"""
114149
Raised when accessing "METADATA" or "PKG-INFO" metadata for a

src/pip/_internal/pyproject.py

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
from pip._vendor import tomli
66
from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
77

8-
from pip._internal.exceptions import InstallationError
8+
from pip._internal.exceptions import (
9+
InstallationError,
10+
InvalidPyProjectBuildRequires,
11+
MissingPyProjectBuildRequires,
12+
)
913

1014

1115
def _is_list_of_str(obj: Any) -> bool:
@@ -119,47 +123,28 @@ def load_pyproject_toml(
119123

120124
# Ensure that the build-system section in pyproject.toml conforms
121125
# to PEP 518.
122-
error_template = (
123-
"{package} has a pyproject.toml file that does not comply "
124-
"with PEP 518: {reason}"
125-
)
126126

127127
# Specifying the build-system table but not the requires key is invalid
128128
if "requires" not in build_system:
129-
raise InstallationError(
130-
error_template.format(
131-
package=req_name,
132-
reason=(
133-
"it has a 'build-system' table but not "
134-
"'build-system.requires' which is mandatory in the table"
135-
),
136-
)
137-
)
129+
raise MissingPyProjectBuildRequires(package=req_name)
138130

139131
# Error out if requires is not a list of strings
140132
requires = build_system["requires"]
141133
if not _is_list_of_str(requires):
142-
raise InstallationError(
143-
error_template.format(
144-
package=req_name,
145-
reason="'build-system.requires' is not a list of strings.",
146-
)
134+
raise InvalidPyProjectBuildRequires(
135+
package=req_name,
136+
reason="It is not a list of strings.",
147137
)
148138

149139
# Each requirement must be valid as per PEP 508
150140
for requirement in requires:
151141
try:
152142
Requirement(requirement)
153-
except InvalidRequirement:
154-
raise InstallationError(
155-
error_template.format(
156-
package=req_name,
157-
reason=(
158-
"'build-system.requires' contains an invalid "
159-
"requirement: {!r}".format(requirement)
160-
),
161-
)
162-
)
143+
except InvalidRequirement as error:
144+
raise InvalidPyProjectBuildRequires(
145+
package=req_name,
146+
reason=f"It contains an invalid requirement: {requirement!r}",
147+
) from error
163148

164149
backend = build_system.get("build-backend")
165150
backend_path = build_system.get("backend-path", [])

tests/functional/test_install.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,12 @@ def test_pep518_refuses_invalid_requires(script, data, common_wheels):
108108
expect_error=True,
109109
)
110110
assert result.returncode == 1
111-
assert "does not comply with PEP 518" in result.stderr
111+
112+
# Ensure the relevant things are mentioned.
113+
assert "PEP 518" in result.stderr
114+
assert "not a list of strings" in result.stderr
115+
assert "build-system.requires" in result.stderr
116+
assert "pyproject.toml" in result.stderr
112117

113118

114119
def test_pep518_refuses_invalid_build_system(script, data, common_wheels):
@@ -120,7 +125,12 @@ def test_pep518_refuses_invalid_build_system(script, data, common_wheels):
120125
expect_error=True,
121126
)
122127
assert result.returncode == 1
123-
assert "does not comply with PEP 518" in result.stderr
128+
129+
# Ensure the relevant things are mentioned.
130+
assert "PEP 518" in result.stderr
131+
assert "mandatory `requires` key" in result.stderr
132+
assert "[build-system] table" in result.stderr
133+
assert "pyproject.toml" in result.stderr
124134

125135

126136
def test_pep518_allows_missing_requires(script, data, common_wheels):
@@ -132,7 +142,7 @@ def test_pep518_allows_missing_requires(script, data, common_wheels):
132142
expect_stderr=True,
133143
)
134144
# Make sure we don't warn when this occurs.
135-
assert "does not comply with PEP 518" not in result.stderr
145+
assert "PEP 518" not in result.stderr
136146

137147
# We want it to go through isolation for now.
138148
assert "Installing build dependencies" in result.stdout, result.stdout

tests/unit/test_pep517.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from pip._internal.exceptions import InstallationError
5+
from pip._internal.exceptions import InstallationError, InvalidPyProjectBuildRequires
66
from pip._internal.req import InstallRequirement
77
from tests.lib import TestData
88
from tests.lib.path import Path
@@ -75,20 +75,24 @@ def test_disabling_pep517_invalid(shared_data: TestData, source: str, msg: str)
7575
def test_pep517_parsing_checks_requirements(tmpdir: Path, spec: str) -> None:
7676
tmpdir.joinpath("pyproject.toml").write_text(
7777
dedent(
78+
f"""
79+
[build-system]
80+
requires = [{spec!r}]
81+
build-backend = "foo"
7882
"""
79-
[build-system]
80-
requires = [{!r}]
81-
build-backend = "foo"
82-
""".format(
83-
spec
84-
)
8583
)
8684
)
8785
req = InstallRequirement(None, None)
8886
req.source_dir = tmpdir # make req believe it has been unpacked
8987

90-
with pytest.raises(InstallationError) as e:
88+
with pytest.raises(InvalidPyProjectBuildRequires) as e:
9189
req.load_pyproject_toml()
9290

93-
err_msg = e.value.args[0]
94-
assert "contains an invalid requirement" in err_msg
91+
error = e.value
92+
93+
assert str(req) in error.message
94+
assert error.context
95+
assert "build-system.requires" in error.context
96+
assert "contains an invalid requirement" in error.context
97+
assert error.hint_stmt
98+
assert "PEP 518" in error.hint_stmt

0 commit comments

Comments
 (0)