Skip to content

Commit b809c86

Browse files
authored
Sync setuptools schema with validate-pyproject (#5157)
2 parents aa99ad0 + 6f1e377 commit b809c86

File tree

10 files changed

+276
-68
lines changed

10 files changed

+276
-68
lines changed

newsfragments/5157.misc.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Sync ``setuptools.schema.json`` with ``validate-pyproject``.
2+
This will allow using ``package-data`` for stubs only packages.

setuptools/config/_validate_pyproject/NOTICE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
The code contained in this directory was automatically generated using the
22
following command:
33

4-
python -m validate_pyproject.pre_compile --output-dir=setuptools/config/_validate_pyproject --enable-plugins setuptools distutils --very-verbose -t setuptools=setuptools/config/setuptools.schema.json -t distutils=setuptools/config/distutils.schema.json
4+
python -m validate_pyproject.pre_compile --output-dir=setuptools/config/_validate_pyproject --enable-plugins setuptools distutils --very-verbose -t distutils=setuptools/config/distutils.schema.json -t setuptools=setuptools/config/setuptools.schema.json
55

66
Please avoid changing it manually.
77

setuptools/config/_validate_pyproject/error_reporting.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import io
24
import json
35
import logging
@@ -6,7 +8,7 @@
68
import typing
79
from contextlib import contextmanager
810
from textwrap import indent, wrap
9-
from typing import Any, Dict, Generator, Iterator, List, Optional, Sequence, Union
11+
from typing import Any, Generator, Iterator, Sequence
1012

1113
from .fastjsonschema_exceptions import JsonSchemaValueException
1214

@@ -36,7 +38,7 @@
3638
_NEED_DETAILS = {"anyOf", "oneOf", "allOf", "contains", "propertyNames", "not", "items"}
3739

3840
_CAMEL_CASE_SPLITTER = re.compile(r"\W+|([A-Z][^A-Z\W]*)")
39-
_IDENTIFIER = re.compile(r"^[\w_]+$", re.I)
41+
_IDENTIFIER = re.compile(r"^[\w_]+$", re.IGNORECASE)
4042

4143
_TOML_JARGON = {
4244
"object": "table",
@@ -73,7 +75,7 @@ class ValidationError(JsonSchemaValueException):
7375
_original_message = ""
7476

7577
@classmethod
76-
def _from_jsonschema(cls, ex: JsonSchemaValueException) -> "Self":
78+
def _from_jsonschema(cls, ex: JsonSchemaValueException) -> Self:
7779
formatter = _ErrorFormatting(ex)
7880
obj = cls(str(formatter), ex.value, formatter.name, ex.definition, ex.rule)
7981
debug_code = os.getenv("JSONSCHEMA_DEBUG_CODE_GENERATION", "false").lower()
@@ -173,8 +175,8 @@ def _expand_details(self) -> str:
173175
class _SummaryWriter:
174176
_IGNORE = frozenset(("description", "default", "title", "examples"))
175177

176-
def __init__(self, jargon: Optional[Dict[str, str]] = None):
177-
self.jargon: Dict[str, str] = jargon or {}
178+
def __init__(self, jargon: dict[str, str] | None = None):
179+
self.jargon: dict[str, str] = jargon or {}
178180
# Clarify confusing terms
179181
self._terms = {
180182
"anyOf": "at least one of the following",
@@ -207,14 +209,14 @@ def __init__(self, jargon: Optional[Dict[str, str]] = None):
207209
"multipleOf",
208210
]
209211

210-
def _jargon(self, term: Union[str, List[str]]) -> Union[str, List[str]]:
212+
def _jargon(self, term: str | list[str]) -> str | list[str]:
211213
if isinstance(term, list):
212214
return [self.jargon.get(t, t) for t in term]
213215
return self.jargon.get(term, term)
214216

215217
def __call__(
216218
self,
217-
schema: Union[dict, List[dict]],
219+
schema: dict | list[dict],
218220
prefix: str = "",
219221
*,
220222
_path: Sequence[str] = (),
@@ -261,15 +263,15 @@ def _is_unecessary(self, path: Sequence[str]) -> bool:
261263
return any(key.startswith(k) for k in "$_") or key in self._IGNORE
262264

263265
def _filter_unecessary(
264-
self, schema: Dict[str, Any], path: Sequence[str]
265-
) -> Dict[str, Any]:
266+
self, schema: dict[str, Any], path: Sequence[str]
267+
) -> dict[str, Any]:
266268
return {
267269
key: value
268270
for key, value in schema.items()
269271
if not self._is_unecessary([*path, key])
270272
}
271273

272-
def _handle_simple_dict(self, value: dict, path: Sequence[str]) -> Optional[str]:
274+
def _handle_simple_dict(self, value: dict, path: Sequence[str]) -> str | None:
273275
inline = any(p in value for p in self._guess_inline_defs)
274276
simple = not any(isinstance(v, (list, dict)) for v in value.values())
275277
if inline or simple:
@@ -328,7 +330,7 @@ def _child_prefix(self, parent_prefix: str, child_prefix: str) -> str:
328330
return len(parent_prefix) * " " + child_prefix
329331

330332

331-
def _separate_terms(word: str) -> List[str]:
333+
def _separate_terms(word: str) -> list[str]:
332334
"""
333335
>>> _separate_terms("FooBar-foo")
334336
['foo', 'bar', 'foo']

setuptools/config/_validate_pyproject/extra_validations.py

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
JSON Schema library).
44
"""
55

6+
import collections
7+
import itertools
68
from inspect import cleandoc
7-
from typing import Mapping, TypeVar
9+
from typing import Generator, Iterable, Mapping, TypeVar
810

911
from .error_reporting import ValidationError
1012

@@ -19,8 +21,7 @@ class RedefiningStaticFieldAsDynamic(ValidationError):
1921
"""
2022
__doc__ = _DESC
2123
_URL = (
22-
"https://packaging.python.org/en/latest/specifications/"
23-
"pyproject-toml/#dynamic"
24+
"https://packaging.python.org/en/latest/specifications/pyproject-toml/#dynamic"
2425
)
2526

2627

@@ -31,6 +32,24 @@ class IncludedDependencyGroupMustExist(ValidationError):
3132
_URL = "https://peps.python.org/pep-0735/"
3233

3334

35+
class ImportNameCollision(ValidationError):
36+
_DESC = """According to PEP 794:
37+
38+
All import-names and import-namespaces items must be unique.
39+
"""
40+
__doc__ = _DESC
41+
_URL = "https://peps.python.org/pep-0794/"
42+
43+
44+
class ImportNameMissing(ValidationError):
45+
_DESC = """According to PEP 794:
46+
47+
An import name must have all parents listed.
48+
"""
49+
__doc__ = _DESC
50+
_URL = "https://peps.python.org/pep-0794/"
51+
52+
3453
def validate_project_dynamic(pyproject: T) -> T:
3554
project_table = pyproject.get("project", {})
3655
dynamic = project_table.get("dynamic", [])
@@ -79,4 +98,54 @@ def validate_include_depenency(pyproject: T) -> T:
7998
return pyproject
8099

81100

82-
EXTRA_VALIDATIONS = (validate_project_dynamic, validate_include_depenency)
101+
def _remove_private(items: Iterable[str]) -> Generator[str, None, None]:
102+
for item in items:
103+
yield item.partition(";")[0].rstrip()
104+
105+
106+
def validate_import_name_issues(pyproject: T) -> T:
107+
project = pyproject.get("project", {})
108+
import_names = collections.Counter(_remove_private(project.get("import-names", [])))
109+
import_namespaces = collections.Counter(
110+
_remove_private(project.get("import-namespaces", []))
111+
)
112+
113+
duplicated = [k for k, v in (import_names + import_namespaces).items() if v > 1]
114+
115+
if duplicated:
116+
raise ImportNameCollision(
117+
message="Duplicated names are not allowed in import-names/import-namespaces",
118+
value=duplicated,
119+
name="data.project.importnames(paces)",
120+
definition={
121+
"description": cleandoc(ImportNameCollision._DESC),
122+
"see": ImportNameCollision._URL,
123+
},
124+
rule="PEP 794",
125+
)
126+
127+
names = frozenset(import_names + import_namespaces)
128+
for name in names:
129+
for parent in itertools.accumulate(
130+
name.split(".")[:-1], lambda a, b: f"{a}.{b}"
131+
):
132+
if parent not in names:
133+
raise ImportNameMissing(
134+
message="All parents of an import name must also be listed in import-namespace/import-names",
135+
value=name,
136+
name="data.project.importnames(paces)",
137+
definition={
138+
"description": cleandoc(ImportNameMissing._DESC),
139+
"see": ImportNameMissing._URL,
140+
},
141+
rule="PEP 794",
142+
)
143+
144+
return pyproject
145+
146+
147+
EXTRA_VALIDATIONS = (
148+
validate_project_dynamic,
149+
validate_include_depenency,
150+
validate_import_name_issues,
151+
)

setuptools/config/_validate_pyproject/fastjsonschema_validations.py

Lines changed: 75 additions & 34 deletions
Large diffs are not rendered by default.

setuptools/config/_validate_pyproject/formats.py

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
function with a ``-`` to obtain the format name and vice versa.
88
"""
99

10-
import builtins
10+
from __future__ import annotations
11+
12+
import keyword
1113
import logging
1214
import os
1315
import re
@@ -16,6 +18,8 @@
1618
from itertools import chain as _chain
1719

1820
if typing.TYPE_CHECKING:
21+
import builtins
22+
1923
from typing_extensions import Literal
2024

2125
_logger = logging.getLogger(__name__)
@@ -54,7 +58,9 @@
5458
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
5559
"""
5660

57-
VERSION_REGEX = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.X | re.I)
61+
VERSION_REGEX = re.compile(
62+
r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE
63+
)
5864

5965

6066
def pep440(version: str) -> bool:
@@ -68,7 +74,7 @@ def pep440(version: str) -> bool:
6874
# PEP 508
6975

7076
PEP508_IDENTIFIER_PATTERN = r"([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])"
71-
PEP508_IDENTIFIER_REGEX = re.compile(f"^{PEP508_IDENTIFIER_PATTERN}$", re.I)
77+
PEP508_IDENTIFIER_REGEX = re.compile(f"^{PEP508_IDENTIFIER_PATTERN}$", re.IGNORECASE)
7278

7379

7480
def pep508_identifier(name: str) -> bool:
@@ -93,9 +99,9 @@ def pep508(value: str) -> bool:
9399
"""
94100
try:
95101
_req.Requirement(value)
96-
return True
97102
except _req.InvalidRequirement:
98103
return False
104+
return True
99105

100106
except ImportError: # pragma: no cover
101107
_logger.warning(
@@ -104,7 +110,7 @@ def pep508(value: str) -> bool:
104110
"To enforce validation, please install `packaging`."
105111
)
106112

107-
def pep508(value: str) -> bool:
113+
def pep508(value: str) -> bool: # noqa: ARG001
108114
return True
109115

110116

@@ -163,7 +169,7 @@ class _TroveClassifier:
163169
option (classifiers will be validated anyway during the upload to PyPI).
164170
"""
165171

166-
downloaded: typing.Union[None, "Literal[False]", typing.Set[str]]
172+
downloaded: None | Literal[False] | set[str]
167173
"""
168174
None => not cached yet
169175
False => unavailable
@@ -200,7 +206,7 @@ def __call__(self, value: str) -> bool:
200206
_logger.debug(msg)
201207
try:
202208
self.downloaded = set(_download_classifiers().splitlines())
203-
except Exception:
209+
except Exception: # noqa: BLE001
204210
self.downloaded = False
205211
_logger.debug("Problem with download, skipping validation")
206212
return True
@@ -253,21 +259,23 @@ def url(value: str) -> bool:
253259
"`scheme` prefix in your URL (e.g. 'http://'). "
254260
f"Given value: {value}"
255261
)
256-
if not (value.startswith("/") or value.startswith("\\") or "@" in value):
262+
if not (value.startswith(("/", "\\")) or "@" in value):
257263
parts = urlparse(f"http://{value}")
258264

259265
return bool(parts.scheme and parts.netloc)
260-
except Exception:
266+
except Exception: # noqa: BLE001
261267
return False
262268

263269

264270
# https://packaging.python.org/specifications/entry-points/
265271
ENTRYPOINT_PATTERN = r"[^\[\s=]([^=]*[^\s=])?"
266-
ENTRYPOINT_REGEX = re.compile(f"^{ENTRYPOINT_PATTERN}$", re.I)
272+
ENTRYPOINT_REGEX = re.compile(f"^{ENTRYPOINT_PATTERN}$", re.IGNORECASE)
267273
RECOMMEDED_ENTRYPOINT_PATTERN = r"[\w.-]+"
268-
RECOMMEDED_ENTRYPOINT_REGEX = re.compile(f"^{RECOMMEDED_ENTRYPOINT_PATTERN}$", re.I)
274+
RECOMMEDED_ENTRYPOINT_REGEX = re.compile(
275+
f"^{RECOMMEDED_ENTRYPOINT_PATTERN}$", re.IGNORECASE
276+
)
269277
ENTRYPOINT_GROUP_PATTERN = r"\w+(\.\w+)*"
270-
ENTRYPOINT_GROUP_REGEX = re.compile(f"^{ENTRYPOINT_GROUP_PATTERN}$", re.I)
278+
ENTRYPOINT_GROUP_REGEX = re.compile(f"^{ENTRYPOINT_GROUP_PATTERN}$", re.IGNORECASE)
271279

272280

273281
def python_identifier(value: str) -> bool:
@@ -368,11 +376,41 @@ def uint16(value: builtins.int) -> bool:
368376
return 0 <= value < 2**16
369377

370378

371-
def uint(value: builtins.int) -> bool:
379+
def uint32(value: builtins.int) -> bool:
380+
r"""Unsigned 32-bit integer (:math:`0 \leq x < 2^{32}`)"""
381+
return 0 <= value < 2**32
382+
383+
384+
def uint64(value: builtins.int) -> bool:
372385
r"""Unsigned 64-bit integer (:math:`0 \leq x < 2^{64}`)"""
373386
return 0 <= value < 2**64
374387

375388

389+
def uint(value: builtins.int) -> bool:
390+
r"""Signed 64-bit integer (:math:`0 \leq x < 2^{64}`)"""
391+
return 0 <= value < 2**64
392+
393+
394+
def int8(value: builtins.int) -> bool:
395+
r"""Signed 8-bit integer (:math:`-2^{7} \leq x < 2^{7}`)"""
396+
return -(2**7) <= value < 2**7
397+
398+
399+
def int16(value: builtins.int) -> bool:
400+
r"""Signed 16-bit integer (:math:`-2^{15} \leq x < 2^{15}`)"""
401+
return -(2**15) <= value < 2**15
402+
403+
404+
def int32(value: builtins.int) -> bool:
405+
r"""Signed 32-bit integer (:math:`-2^{31} \leq x < 2^{31}`)"""
406+
return -(2**31) <= value < 2**31
407+
408+
409+
def int64(value: builtins.int) -> bool:
410+
r"""Signed 64-bit integer (:math:`-2^{63} \leq x < 2^{63}`)"""
411+
return -(2**63) <= value < 2**63
412+
413+
376414
def int(value: builtins.int) -> bool:
377415
r"""Signed 64-bit integer (:math:`-2^{63} \leq x < 2^{63}`)"""
378416
return -(2**63) <= value < 2**63
@@ -387,9 +425,9 @@ def SPDX(value: str) -> bool:
387425
"""
388426
try:
389427
_licenses.canonicalize_license_expression(value)
390-
return True
391428
except _licenses.InvalidLicenseExpression:
392429
return False
430+
return True
393431

394432
except ImportError: # pragma: no cover
395433
_logger.warning(
@@ -398,5 +436,29 @@ def SPDX(value: str) -> bool:
398436
"To enforce validation, please install `packaging>=24.2`."
399437
)
400438

401-
def SPDX(value: str) -> bool:
439+
def SPDX(value: str) -> bool: # noqa: ARG001
402440
return True
441+
442+
443+
VALID_IMPORT_NAME = re.compile(
444+
r"""
445+
^ # start of string
446+
[A-Za-z_][A-Za-z_0-9]+ # a valid Python identifier
447+
(?:\.[A-Za-z_][A-Za-z_0-9]*)* # optionally followed by .identifier's
448+
(?:\s*;\s*private)? # optionally followed by ; private
449+
$ # end of string
450+
""",
451+
re.VERBOSE,
452+
)
453+
454+
455+
def import_name(value: str) -> bool:
456+
"""This is a valid import name. It has to be series of python identifiers
457+
(not keywords), separated by dots, optionally followed by a semicolon and
458+
the keyword "private".
459+
"""
460+
if VALID_IMPORT_NAME.match(value) is None:
461+
return False
462+
463+
idents, _, _ = value.partition(";")
464+
return all(not keyword.iskeyword(ident) for ident in idents.rstrip().split("."))

0 commit comments

Comments
 (0)