Skip to content

Commit 6d4902a

Browse files
authored
Update pyproject.toml validation (pypa#4344)
2 parents 804ccd2 + 5c9d37a commit 6d4902a

File tree

8 files changed

+372
-201
lines changed

8 files changed

+372
-201
lines changed

mypy.ini

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ exclude = (?x)(
2121
# *.extern modules that actually live in *._vendor will also cause attr-defined issues on import
2222
disable_error_code = attr-defined
2323

24+
# - pkg_resources tests create modules that won't exists statically before the test is run.
25+
# Let's ignore all "import-not-found" since, if an import really wasn't found, then the test would fail.
26+
[mypy-pkg_resources.tests.*]
27+
disable_error_code = import-not-found
28+
2429
# - Avoid raising issues when importing from "extern" modules, as those are added to path dynamically.
2530
# https://github.com/pypa/setuptools/pull/3979#discussion_r1367968993
2631
# - distutils._modified has different errors on Python 3.8 [import-untyped], on Python 3.9+ [import-not-found]
@@ -29,8 +34,7 @@ disable_error_code = attr-defined
2934
[mypy-pkg_resources.extern.*,setuptools.extern.*,distutils._modified,jaraco.*,trove_classifiers]
3035
ignore_missing_imports = True
3136

32-
# - pkg_resources tests create modules that won't exists statically before the test is run.
33-
# Let's ignore all "import-not-found" since, if an import really wasn't found, then the test would fail.
34-
# - setuptools._vendor.packaging._manylinux: Mypy issue, this vendored module is already excluded!
35-
[mypy-pkg_resources.tests.*,setuptools._vendor.packaging._manylinux,setuptools.config._validate_pyproject.*]
36-
disable_error_code = import-not-found
37+
# Even when excluding vendored/generated modules, there might be problems: https://github.com/python/mypy/issues/11936#issuecomment-1466764006
38+
[mypy-setuptools._vendor.packaging._manylinux,setuptools.config._validate_pyproject.*]
39+
follow_imports = silent
40+
# silent => ignore errors when following imports

setuptools/config/_validate_pyproject/NOTICE

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,4 +436,3 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice
436436
This Source Code Form is "Incompatible
437437
With Secondary Licenses", as defined by
438438
the Mozilla Public License, v. 2.0.
439-

setuptools/config/_validate_pyproject/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@ def validate(data: Any) -> bool:
3030
"""
3131
with detailed_errors():
3232
_validate(data, custom_formats=FORMAT_FUNCTIONS)
33-
reduce(lambda acc, fn: fn(acc), EXTRA_VALIDATIONS, data)
33+
reduce(lambda acc, fn: fn(acc), EXTRA_VALIDATIONS, data)
3434
return True

setuptools/config/_validate_pyproject/error_reporting.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,21 @@
33
import logging
44
import os
55
import re
6+
import typing
67
from contextlib import contextmanager
78
from textwrap import indent, wrap
8-
from typing import Any, Dict, Iterator, List, Optional, Sequence, Union, cast
9+
from typing import Any, Dict, Generator, Iterator, List, Optional, Sequence, Union, cast
910

1011
from .fastjsonschema_exceptions import JsonSchemaValueException
1112

13+
if typing.TYPE_CHECKING:
14+
import sys
15+
16+
if sys.version_info < (3, 11):
17+
from typing_extensions import Self
18+
else:
19+
from typing import Self
20+
1221
_logger = logging.getLogger(__name__)
1322

1423
_MESSAGE_REPLACEMENTS = {
@@ -36,6 +45,11 @@
3645
"property names": "keys",
3746
}
3847

48+
_FORMATS_HELP = """
49+
For more details about `format` see
50+
https://validate-pyproject.readthedocs.io/en/latest/api/validate_pyproject.formats.html
51+
"""
52+
3953

4054
class ValidationError(JsonSchemaValueException):
4155
"""Report violations of a given JSON schema.
@@ -59,7 +73,7 @@ class ValidationError(JsonSchemaValueException):
5973
_original_message = ""
6074

6175
@classmethod
62-
def _from_jsonschema(cls, ex: JsonSchemaValueException):
76+
def _from_jsonschema(cls, ex: JsonSchemaValueException) -> "Self":
6377
formatter = _ErrorFormatting(ex)
6478
obj = cls(str(formatter), ex.value, formatter.name, ex.definition, ex.rule)
6579
debug_code = os.getenv("JSONSCHEMA_DEBUG_CODE_GENERATION", "false").lower()
@@ -72,7 +86,7 @@ def _from_jsonschema(cls, ex: JsonSchemaValueException):
7286

7387

7488
@contextmanager
75-
def detailed_errors():
89+
def detailed_errors() -> Generator[None, None, None]:
7690
try:
7791
yield
7892
except JsonSchemaValueException as ex:
@@ -83,7 +97,7 @@ class _ErrorFormatting:
8397
def __init__(self, ex: JsonSchemaValueException):
8498
self.ex = ex
8599
self.name = f"`{self._simplify_name(ex.name)}`"
86-
self._original_message = self.ex.message.replace(ex.name, self.name)
100+
self._original_message: str = self.ex.message.replace(ex.name, self.name)
87101
self._summary = ""
88102
self._details = ""
89103

@@ -107,11 +121,12 @@ def details(self) -> str:
107121

108122
return self._details
109123

110-
def _simplify_name(self, name):
124+
@staticmethod
125+
def _simplify_name(name: str) -> str:
111126
x = len("data.")
112127
return name[x:] if name.startswith("data.") else name
113128

114-
def _expand_summary(self):
129+
def _expand_summary(self) -> str:
115130
msg = self._original_message
116131

117132
for bad, repl in _MESSAGE_REPLACEMENTS.items():
@@ -129,8 +144,9 @@ def _expand_summary(self):
129144

130145
def _expand_details(self) -> str:
131146
optional = []
132-
desc_lines = self.ex.definition.pop("$$description", [])
133-
desc = self.ex.definition.pop("description", None) or " ".join(desc_lines)
147+
definition = self.ex.definition or {}
148+
desc_lines = definition.pop("$$description", [])
149+
desc = definition.pop("description", None) or " ".join(desc_lines)
134150
if desc:
135151
description = "\n".join(
136152
wrap(
@@ -142,18 +158,20 @@ def _expand_details(self) -> str:
142158
)
143159
)
144160
optional.append(f"DESCRIPTION:\n{description}")
145-
schema = json.dumps(self.ex.definition, indent=4)
161+
schema = json.dumps(definition, indent=4)
146162
value = json.dumps(self.ex.value, indent=4)
147163
defaults = [
148164
f"GIVEN VALUE:\n{indent(value, ' ')}",
149165
f"OFFENDING RULE: {self.ex.rule!r}",
150166
f"DEFINITION:\n{indent(schema, ' ')}",
151167
]
152-
return "\n\n".join(optional + defaults)
168+
msg = "\n\n".join(optional + defaults)
169+
epilog = f"\n{_FORMATS_HELP}" if "format" in msg.lower() else ""
170+
return msg + epilog
153171

154172

155173
class _SummaryWriter:
156-
_IGNORE = {"description", "default", "title", "examples"}
174+
_IGNORE = frozenset(("description", "default", "title", "examples"))
157175

158176
def __init__(self, jargon: Optional[Dict[str, str]] = None):
159177
self.jargon: Dict[str, str] = jargon or {}
@@ -242,7 +260,9 @@ def _is_unecessary(self, path: Sequence[str]) -> bool:
242260
key = path[-1]
243261
return any(key.startswith(k) for k in "$_") or key in self._IGNORE
244262

245-
def _filter_unecessary(self, schema: dict, path: Sequence[str]):
263+
def _filter_unecessary(
264+
self, schema: Dict[str, Any], path: Sequence[str]
265+
) -> Dict[str, Any]:
246266
return {
247267
key: value
248268
for key, value in schema.items()
@@ -271,7 +291,7 @@ def _handle_list(
271291
self(v, item_prefix, _path=[*path, f"[{i}]"]) for i, v in enumerate(schemas)
272292
)
273293

274-
def _is_property(self, path: Sequence[str]):
294+
def _is_property(self, path: Sequence[str]) -> bool:
275295
"""Check if the given path can correspond to an arbitrarily named property"""
276296
counter = 0
277297
for key in path[-2::-1]:

setuptools/config/_validate_pyproject/extra_validations.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
JSON Schema library).
44
"""
55

6+
from inspect import cleandoc
67
from typing import Mapping, TypeVar
78

89
from .error_reporting import ValidationError
@@ -11,11 +12,16 @@
1112

1213

1314
class RedefiningStaticFieldAsDynamic(ValidationError):
14-
"""According to PEP 621:
15+
_DESC = """According to PEP 621:
1516
1617
Build back-ends MUST raise an error if the metadata specifies a field
1718
statically as well as being listed in dynamic.
1819
"""
20+
__doc__ = _DESC
21+
_URL = (
22+
"https://packaging.python.org/en/latest/specifications/"
23+
"pyproject-toml/#dynamic"
24+
)
1925

2026

2127
def validate_project_dynamic(pyproject: T) -> T:
@@ -24,11 +30,21 @@ def validate_project_dynamic(pyproject: T) -> T:
2430

2531
for field in dynamic:
2632
if field in project_table:
27-
msg = f"You cannot provide a value for `project.{field}` and "
28-
msg += "list it under `project.dynamic` at the same time"
29-
name = f"data.project.{field}"
30-
value = {field: project_table[field], "...": " # ...", "dynamic": dynamic}
31-
raise RedefiningStaticFieldAsDynamic(msg, value, name, rule="PEP 621")
33+
raise RedefiningStaticFieldAsDynamic(
34+
message=f"You cannot provide a value for `project.{field}` and "
35+
"list it under `project.dynamic` at the same time",
36+
value={
37+
field: project_table[field],
38+
"...": " # ...",
39+
"dynamic": dynamic,
40+
},
41+
name=f"data.project.{field}",
42+
definition={
43+
"description": cleandoc(RedefiningStaticFieldAsDynamic._DESC),
44+
"see": RedefiningStaticFieldAsDynamic._URL,
45+
},
46+
rule="PEP 621",
47+
)
3248

3349
return pyproject
3450

setuptools/config/_validate_pyproject/fastjsonschema_validations.py

Lines changed: 221 additions & 168 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)