Skip to content
Draft
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
1 change: 1 addition & 0 deletions newsfragments/5157.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update code generated by ``validate-pyproject``. This should allow using package-data for stubs only packages.
24 changes: 13 additions & 11 deletions setuptools/config/_validate_pyproject/error_reporting.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import io
import json
import logging
Expand All @@ -6,7 +8,7 @@
import typing
from contextlib import contextmanager
from textwrap import indent, wrap
from typing import Any, Dict, Generator, Iterator, List, Optional, Sequence, Union
from typing import Any, Generator, Iterator, Sequence

from .fastjsonschema_exceptions import JsonSchemaValueException

Expand Down Expand Up @@ -36,7 +38,7 @@
_NEED_DETAILS = {"anyOf", "oneOf", "allOf", "contains", "propertyNames", "not", "items"}

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

_TOML_JARGON = {
"object": "table",
Expand Down Expand Up @@ -73,7 +75,7 @@ class ValidationError(JsonSchemaValueException):
_original_message = ""

@classmethod
def _from_jsonschema(cls, ex: JsonSchemaValueException) -> "Self":
def _from_jsonschema(cls, ex: JsonSchemaValueException) -> Self:
formatter = _ErrorFormatting(ex)
obj = cls(str(formatter), ex.value, formatter.name, ex.definition, ex.rule)
debug_code = os.getenv("JSONSCHEMA_DEBUG_CODE_GENERATION", "false").lower()
Expand Down Expand Up @@ -173,8 +175,8 @@ def _expand_details(self) -> str:
class _SummaryWriter:
_IGNORE = frozenset(("description", "default", "title", "examples"))

def __init__(self, jargon: Optional[Dict[str, str]] = None):
self.jargon: Dict[str, str] = jargon or {}
def __init__(self, jargon: dict[str, str] | None = None):
self.jargon: dict[str, str] = jargon or {}
# Clarify confusing terms
self._terms = {
"anyOf": "at least one of the following",
Expand Down Expand Up @@ -207,14 +209,14 @@ def __init__(self, jargon: Optional[Dict[str, str]] = None):
"multipleOf",
]

def _jargon(self, term: Union[str, List[str]]) -> Union[str, List[str]]:
def _jargon(self, term: str | list[str]) -> str | list[str]:
if isinstance(term, list):
return [self.jargon.get(t, t) for t in term]
return self.jargon.get(term, term)

def __call__(
self,
schema: Union[dict, List[dict]],
schema: dict | list[dict],
prefix: str = "",
*,
_path: Sequence[str] = (),
Expand Down Expand Up @@ -261,15 +263,15 @@ def _is_unecessary(self, path: Sequence[str]) -> bool:
return any(key.startswith(k) for k in "$_") or key in self._IGNORE

def _filter_unecessary(
self, schema: Dict[str, Any], path: Sequence[str]
) -> Dict[str, Any]:
self, schema: dict[str, Any], path: Sequence[str]
) -> dict[str, Any]:
return {
key: value
for key, value in schema.items()
if not self._is_unecessary([*path, key])
}

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


def _separate_terms(word: str) -> List[str]:
def _separate_terms(word: str) -> list[str]:
"""
>>> _separate_terms("FooBar-foo")
['foo', 'bar', 'foo']
Expand Down
77 changes: 73 additions & 4 deletions setuptools/config/_validate_pyproject/extra_validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
JSON Schema library).
"""

import collections
import itertools
from inspect import cleandoc
from typing import Mapping, TypeVar
from typing import Generator, Iterable, Mapping, TypeVar

from .error_reporting import ValidationError

Expand All @@ -19,8 +21,7 @@ class RedefiningStaticFieldAsDynamic(ValidationError):
"""
__doc__ = _DESC
_URL = (
"https://packaging.python.org/en/latest/specifications/"
"pyproject-toml/#dynamic"
"https://packaging.python.org/en/latest/specifications/pyproject-toml/#dynamic"
)


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


class ImportNameCollision(ValidationError):
_DESC = """According to PEP 794:

All import-names and import-namespaces items must be unique.
"""
__doc__ = _DESC
_URL = "https://peps.python.org/pep-0794/"


class ImportNameMissing(ValidationError):
_DESC = """According to PEP 794:

An import name must have all parents listed.
"""
__doc__ = _DESC
_URL = "https://peps.python.org/pep-0794/"


def validate_project_dynamic(pyproject: T) -> T:
project_table = pyproject.get("project", {})
dynamic = project_table.get("dynamic", [])
Expand Down Expand Up @@ -79,4 +98,54 @@ def validate_include_depenency(pyproject: T) -> T:
return pyproject


EXTRA_VALIDATIONS = (validate_project_dynamic, validate_include_depenency)
def _remove_private(items: Iterable[str]) -> Generator[str, None, None]:
for item in items:
yield item.partition(";")[0].rstrip()


def validate_import_name_issues(pyproject: T) -> T:
project = pyproject.get("project", {})
import_names = collections.Counter(_remove_private(project.get("import-names", [])))
import_namespaces = collections.Counter(
_remove_private(project.get("import-namespaces", []))
)

duplicated = [k for k, v in (import_names + import_namespaces).items() if v > 1]

if duplicated:
raise ImportNameCollision(
message="Duplicated names are not allowed in import-names/import-namespaces",
value=duplicated,
name="data.project.importnames(paces)",
definition={
"description": cleandoc(ImportNameCollision._DESC),
"see": ImportNameCollision._URL,
},
rule="PEP 794",
)

names = frozenset(import_names + import_namespaces)
for name in names:
for parent in itertools.accumulate(
name.split(".")[:-1], lambda a, b: f"{a}.{b}"
):
if parent not in names:
raise ImportNameMissing(
message="All parents of an import name must also be listed in import-namespace/import-names",
value=name,
name="data.project.importnames(paces)",
definition={
"description": cleandoc(ImportNameMissing._DESC),
"see": ImportNameMissing._URL,
},
rule="PEP 794",
)

return pyproject


EXTRA_VALIDATIONS = (
validate_project_dynamic,
validate_include_depenency,
validate_import_name_issues,
)
75 changes: 62 additions & 13 deletions setuptools/config/_validate_pyproject/fastjsonschema_validations.py

Large diffs are not rendered by default.

92 changes: 77 additions & 15 deletions setuptools/config/_validate_pyproject/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
function with a ``-`` to obtain the format name and vice versa.
"""

import builtins
from __future__ import annotations

import keyword
import logging
import os
import re
Expand All @@ -16,6 +18,8 @@
from itertools import chain as _chain

if typing.TYPE_CHECKING:
import builtins

from typing_extensions import Literal

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

VERSION_REGEX = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.X | re.I)
VERSION_REGEX = re.compile(
r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE
)


def pep440(version: str) -> bool:
Expand All @@ -68,7 +74,7 @@ def pep440(version: str) -> bool:
# PEP 508

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


def pep508_identifier(name: str) -> bool:
Expand All @@ -93,9 +99,9 @@ def pep508(value: str) -> bool:
"""
try:
_req.Requirement(value)
return True
except _req.InvalidRequirement:
return False
return True

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

def pep508(value: str) -> bool:
def pep508(value: str) -> bool: # noqa: ARG001
return True


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

downloaded: typing.Union[None, "Literal[False]", typing.Set[str]]
downloaded: None | Literal[False] | set[str]
"""
None => not cached yet
False => unavailable
Expand Down Expand Up @@ -200,7 +206,7 @@ def __call__(self, value: str) -> bool:
_logger.debug(msg)
try:
self.downloaded = set(_download_classifiers().splitlines())
except Exception:
except Exception: # noqa: BLE001
self.downloaded = False
_logger.debug("Problem with download, skipping validation")
return True
Expand Down Expand Up @@ -253,21 +259,23 @@ def url(value: str) -> bool:
"`scheme` prefix in your URL (e.g. 'http://'). "
f"Given value: {value}"
)
if not (value.startswith("/") or value.startswith("\\") or "@" in value):
if not (value.startswith(("/", "\\")) or "@" in value):
parts = urlparse(f"http://{value}")

return bool(parts.scheme and parts.netloc)
except Exception:
except Exception: # noqa: BLE001
return False


# https://packaging.python.org/specifications/entry-points/
ENTRYPOINT_PATTERN = r"[^\[\s=]([^=]*[^\s=])?"
ENTRYPOINT_REGEX = re.compile(f"^{ENTRYPOINT_PATTERN}$", re.I)
ENTRYPOINT_REGEX = re.compile(f"^{ENTRYPOINT_PATTERN}$", re.IGNORECASE)
RECOMMEDED_ENTRYPOINT_PATTERN = r"[\w.-]+"
RECOMMEDED_ENTRYPOINT_REGEX = re.compile(f"^{RECOMMEDED_ENTRYPOINT_PATTERN}$", re.I)
RECOMMEDED_ENTRYPOINT_REGEX = re.compile(
f"^{RECOMMEDED_ENTRYPOINT_PATTERN}$", re.IGNORECASE
)
ENTRYPOINT_GROUP_PATTERN = r"\w+(\.\w+)*"
ENTRYPOINT_GROUP_REGEX = re.compile(f"^{ENTRYPOINT_GROUP_PATTERN}$", re.I)
ENTRYPOINT_GROUP_REGEX = re.compile(f"^{ENTRYPOINT_GROUP_PATTERN}$", re.IGNORECASE)


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


def uint(value: builtins.int) -> bool:
def uint32(value: builtins.int) -> bool:
r"""Unsigned 32-bit integer (:math:`0 \leq x < 2^{32}`)"""
return 0 <= value < 2**32


def uint64(value: builtins.int) -> bool:
r"""Unsigned 64-bit integer (:math:`0 \leq x < 2^{64}`)"""
return 0 <= value < 2**64


def uint(value: builtins.int) -> bool:
r"""Signed 64-bit integer (:math:`0 \leq x < 2^{64}`)"""
return 0 <= value < 2**64


def int8(value: builtins.int) -> bool:
r"""Signed 8-bit integer (:math:`-2^{7} \leq x < 2^{7}`)"""
return -(2**7) <= value < 2**7


def int16(value: builtins.int) -> bool:
r"""Signed 16-bit integer (:math:`-2^{15} \leq x < 2^{15}`)"""
return -(2**15) <= value < 2**15


def int32(value: builtins.int) -> bool:
r"""Signed 32-bit integer (:math:`-2^{31} \leq x < 2^{31}`)"""
return -(2**31) <= value < 2**31


def int64(value: builtins.int) -> bool:
r"""Signed 64-bit integer (:math:`-2^{63} \leq x < 2^{63}`)"""
return -(2**63) <= value < 2**63


def int(value: builtins.int) -> bool:
r"""Signed 64-bit integer (:math:`-2^{63} \leq x < 2^{63}`)"""
return -(2**63) <= value < 2**63
Expand All @@ -387,9 +425,9 @@ def SPDX(value: str) -> bool:
"""
try:
_licenses.canonicalize_license_expression(value)
return True
except _licenses.InvalidLicenseExpression:
return False
return True

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

def SPDX(value: str) -> bool:
def SPDX(value: str) -> bool: # noqa: ARG001
return True


VALID_IMPORT_NAME = re.compile(
r"""
^ # start of string
[A-Za-z_][A-Za-z_0-9]+ # a valid Python identifier
(?:\.[A-Za-z_][A-Za-z_0-9]*)* # optionally followed by .identifier's
(?:\s*;\s*private)? # optionally followed by ; private
$ # end of string
""",
re.VERBOSE,
)


def import_name(value: str) -> bool:
"""This is a valid import name. It has to be series of python identifiers
(not keywords), separated by dots, optionally followed by a semicolon and
the keyword "private".
"""
if VALID_IMPORT_NAME.match(value) is None:
return False

idents, _, _ = value.partition(";")
return all(not keyword.iskeyword(ident) for ident in idents.rstrip().split("."))
Loading