Skip to content

Commit

Permalink
Update pyproject.toml validation (#4344)
Browse files Browse the repository at this point in the history
  • Loading branch information
abravalheri committed May 9, 2024
2 parents 804ccd2 + 5c9d37a commit 6d4902a
Show file tree
Hide file tree
Showing 8 changed files with 372 additions and 201 deletions.
14 changes: 9 additions & 5 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ exclude = (?x)(
# *.extern modules that actually live in *._vendor will also cause attr-defined issues on import
disable_error_code = attr-defined

# - pkg_resources tests create modules that won't exists statically before the test is run.
# Let's ignore all "import-not-found" since, if an import really wasn't found, then the test would fail.
[mypy-pkg_resources.tests.*]
disable_error_code = import-not-found

# - Avoid raising issues when importing from "extern" modules, as those are added to path dynamically.
# https://github.com/pypa/setuptools/pull/3979#discussion_r1367968993
# - distutils._modified has different errors on Python 3.8 [import-untyped], on Python 3.9+ [import-not-found]
Expand All @@ -29,8 +34,7 @@ disable_error_code = attr-defined
[mypy-pkg_resources.extern.*,setuptools.extern.*,distutils._modified,jaraco.*,trove_classifiers]
ignore_missing_imports = True

# - pkg_resources tests create modules that won't exists statically before the test is run.
# Let's ignore all "import-not-found" since, if an import really wasn't found, then the test would fail.
# - setuptools._vendor.packaging._manylinux: Mypy issue, this vendored module is already excluded!
[mypy-pkg_resources.tests.*,setuptools._vendor.packaging._manylinux,setuptools.config._validate_pyproject.*]
disable_error_code = import-not-found
# Even when excluding vendored/generated modules, there might be problems: https://github.com/python/mypy/issues/11936#issuecomment-1466764006
[mypy-setuptools._vendor.packaging._manylinux,setuptools.config._validate_pyproject.*]
follow_imports = silent
# silent => ignore errors when following imports
1 change: 0 additions & 1 deletion setuptools/config/_validate_pyproject/NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -436,4 +436,3 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice
This Source Code Form is "Incompatible
With Secondary Licenses", as defined by
the Mozilla Public License, v. 2.0.

2 changes: 1 addition & 1 deletion setuptools/config/_validate_pyproject/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ def validate(data: Any) -> bool:
"""
with detailed_errors():
_validate(data, custom_formats=FORMAT_FUNCTIONS)
reduce(lambda acc, fn: fn(acc), EXTRA_VALIDATIONS, data)
reduce(lambda acc, fn: fn(acc), EXTRA_VALIDATIONS, data)
return True
46 changes: 33 additions & 13 deletions setuptools/config/_validate_pyproject/error_reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@
import logging
import os
import re
import typing
from contextlib import contextmanager
from textwrap import indent, wrap
from typing import Any, Dict, Iterator, List, Optional, Sequence, Union, cast
from typing import Any, Dict, Generator, Iterator, List, Optional, Sequence, Union, cast

from .fastjsonschema_exceptions import JsonSchemaValueException

if typing.TYPE_CHECKING:
import sys

if sys.version_info < (3, 11):
from typing_extensions import Self
else:
from typing import Self

_logger = logging.getLogger(__name__)

_MESSAGE_REPLACEMENTS = {
Expand Down Expand Up @@ -36,6 +45,11 @@
"property names": "keys",
}

_FORMATS_HELP = """
For more details about `format` see
https://validate-pyproject.readthedocs.io/en/latest/api/validate_pyproject.formats.html
"""


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

@classmethod
def _from_jsonschema(cls, ex: JsonSchemaValueException):
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 All @@ -72,7 +86,7 @@ def _from_jsonschema(cls, ex: JsonSchemaValueException):


@contextmanager
def detailed_errors():
def detailed_errors() -> Generator[None, None, None]:
try:
yield
except JsonSchemaValueException as ex:
Expand All @@ -83,7 +97,7 @@ class _ErrorFormatting:
def __init__(self, ex: JsonSchemaValueException):
self.ex = ex
self.name = f"`{self._simplify_name(ex.name)}`"
self._original_message = self.ex.message.replace(ex.name, self.name)
self._original_message: str = self.ex.message.replace(ex.name, self.name)
self._summary = ""
self._details = ""

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

return self._details

def _simplify_name(self, name):
@staticmethod
def _simplify_name(name: str) -> str:
x = len("data.")
return name[x:] if name.startswith("data.") else name

def _expand_summary(self):
def _expand_summary(self) -> str:
msg = self._original_message

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

def _expand_details(self) -> str:
optional = []
desc_lines = self.ex.definition.pop("$$description", [])
desc = self.ex.definition.pop("description", None) or " ".join(desc_lines)
definition = self.ex.definition or {}
desc_lines = definition.pop("$$description", [])
desc = definition.pop("description", None) or " ".join(desc_lines)
if desc:
description = "\n".join(
wrap(
Expand All @@ -142,18 +158,20 @@ def _expand_details(self) -> str:
)
)
optional.append(f"DESCRIPTION:\n{description}")
schema = json.dumps(self.ex.definition, indent=4)
schema = json.dumps(definition, indent=4)
value = json.dumps(self.ex.value, indent=4)
defaults = [
f"GIVEN VALUE:\n{indent(value, ' ')}",
f"OFFENDING RULE: {self.ex.rule!r}",
f"DEFINITION:\n{indent(schema, ' ')}",
]
return "\n\n".join(optional + defaults)
msg = "\n\n".join(optional + defaults)
epilog = f"\n{_FORMATS_HELP}" if "format" in msg.lower() else ""
return msg + epilog


class _SummaryWriter:
_IGNORE = {"description", "default", "title", "examples"}
_IGNORE = frozenset(("description", "default", "title", "examples"))

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

def _filter_unecessary(self, schema: dict, path: Sequence[str]):
def _filter_unecessary(
self, schema: Dict[str, Any], path: Sequence[str]
) -> Dict[str, Any]:
return {
key: value
for key, value in schema.items()
Expand Down Expand Up @@ -271,7 +291,7 @@ def _handle_list(
self(v, item_prefix, _path=[*path, f"[{i}]"]) for i, v in enumerate(schemas)
)

def _is_property(self, path: Sequence[str]):
def _is_property(self, path: Sequence[str]) -> bool:
"""Check if the given path can correspond to an arbitrarily named property"""
counter = 0
for key in path[-2::-1]:
Expand Down
28 changes: 22 additions & 6 deletions setuptools/config/_validate_pyproject/extra_validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
JSON Schema library).
"""

from inspect import cleandoc
from typing import Mapping, TypeVar

from .error_reporting import ValidationError
Expand All @@ -11,11 +12,16 @@


class RedefiningStaticFieldAsDynamic(ValidationError):
"""According to PEP 621:
_DESC = """According to PEP 621:
Build back-ends MUST raise an error if the metadata specifies a field
statically as well as being listed in dynamic.
"""
__doc__ = _DESC
_URL = (
"https://packaging.python.org/en/latest/specifications/"
"pyproject-toml/#dynamic"
)


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

for field in dynamic:
if field in project_table:
msg = f"You cannot provide a value for `project.{field}` and "
msg += "list it under `project.dynamic` at the same time"
name = f"data.project.{field}"
value = {field: project_table[field], "...": " # ...", "dynamic": dynamic}
raise RedefiningStaticFieldAsDynamic(msg, value, name, rule="PEP 621")
raise RedefiningStaticFieldAsDynamic(
message=f"You cannot provide a value for `project.{field}` and "
"list it under `project.dynamic` at the same time",
value={
field: project_table[field],
"...": " # ...",
"dynamic": dynamic,
},
name=f"data.project.{field}",
definition={
"description": cleandoc(RedefiningStaticFieldAsDynamic._DESC),
"see": RedefiningStaticFieldAsDynamic._URL,
},
rule="PEP 621",
)

return pyproject

Expand Down
389 changes: 221 additions & 168 deletions setuptools/config/_validate_pyproject/fastjsonschema_validations.py

Large diffs are not rendered by default.

0 comments on commit 6d4902a

Please sign in to comment.