-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
validate-pyproject
as a vendored dependency
In order to minimise dependencies, `validate-pyproject` has the ability to "dump" only the code necessary to run the validations to a given directory. This special strategy is used instead of the default `pip install -t`. The idea of using JSONSchema for validation was suggested in #2671, and the rationale for that approach is further discussed in https://github.com/abravalheri/validate-pyproject/blob/main/docs/faq.rst Using a library such as `validate-pyproject` has the advantage of incentive sing reuse and collaboration with other projects. Currently `validate-pyproject` ships a JSONSchema for the proposed use of `pyproject.toml` as means of configuration for setuptools. In the future, if there is interest, setuptools could also ship its own schema and just use the shared infrastructure of `validate-pyproject` (by advertising the schemas via entry-points).
- Loading branch information
1 parent
3cc64ee
commit d826bd2
Showing
9 changed files
with
1,803 additions
and
1 deletion.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from functools import reduce | ||
from typing import Any, Callable, Dict | ||
|
||
from . import formats | ||
from .extra_validations import EXTRA_VALIDATIONS | ||
from .fastjsonschema_exceptions import JsonSchemaException, JsonSchemaValueException | ||
from .fastjsonschema_validations import validate as _validate | ||
|
||
__all__ = [ | ||
"validate", | ||
"FORMAT_FUNCTIONS", | ||
"EXTRA_VALIDATIONS", | ||
"JsonSchemaException", | ||
"JsonSchemaValueException", | ||
] | ||
|
||
|
||
FORMAT_FUNCTIONS: Dict[str, Callable[[str], bool]] = { | ||
fn.__name__.replace("_", "-"): fn | ||
for fn in formats.__dict__.values() | ||
if callable(fn) and not fn.__name__.startswith("_") | ||
} | ||
|
||
|
||
def validate(data: Any) -> bool: | ||
"""Validate the given ``data`` object using JSON Schema | ||
This function raises ``JsonSchemaValueException`` if ``data`` is invalid. | ||
""" | ||
_validate(data, custom_formats=FORMAT_FUNCTIONS) | ||
reduce(lambda acc, fn: fn(acc), EXTRA_VALIDATIONS, data) | ||
return True |
36 changes: 36 additions & 0 deletions
36
setuptools/_vendor/_validate_pyproject/extra_validations.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
"""The purpose of this module is implement PEP 621 validations that are | ||
difficult to express as a JSON Schema (or that are not supported by the current | ||
JSON Schema library). | ||
""" | ||
|
||
from typing import Mapping, TypeVar | ||
|
||
from .fastjsonschema_exceptions import JsonSchemaValueException | ||
|
||
T = TypeVar("T", bound=Mapping) | ||
|
||
|
||
class RedefiningStaticFieldAsDynamic(JsonSchemaValueException): | ||
"""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. | ||
""" | ||
|
||
|
||
def validate_project_dynamic(pyproject: T) -> T: | ||
project_table = pyproject.get("project", {}) | ||
dynamic = project_table.get("dynamic", []) | ||
|
||
for field in dynamic: | ||
if field in project_table: | ||
msg = f"You cannot provided 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") | ||
|
||
return pyproject | ||
|
||
|
||
EXTRA_VALIDATIONS = (validate_project_dynamic,) |
51 changes: 51 additions & 0 deletions
51
setuptools/_vendor/_validate_pyproject/fastjsonschema_exceptions.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import re | ||
|
||
|
||
SPLIT_RE = re.compile(r'[\.\[\]]+') | ||
|
||
|
||
class JsonSchemaException(ValueError): | ||
""" | ||
Base exception of ``fastjsonschema`` library. | ||
""" | ||
|
||
|
||
class JsonSchemaValueException(JsonSchemaException): | ||
""" | ||
Exception raised by validation function. Available properties: | ||
* ``message`` containing human-readable information what is wrong (e.g. ``data.property[index] must be smaller than or equal to 42``), | ||
* invalid ``value`` (e.g. ``60``), | ||
* ``name`` of a path in the data structure (e.g. ``data.propery[index]``), | ||
* ``path`` as an array in the data structure (e.g. ``['data', 'propery', 'index']``), | ||
* the whole ``definition`` which the ``value`` has to fulfil (e.g. ``{'type': 'number', 'maximum': 42}``), | ||
* ``rule`` which the ``value`` is breaking (e.g. ``maximum``) | ||
* and ``rule_definition`` (e.g. ``42``). | ||
.. versionchanged:: 2.14.0 | ||
Added all extra properties. | ||
""" | ||
|
||
def __init__(self, message, value=None, name=None, definition=None, rule=None): | ||
super().__init__(message) | ||
self.message = message | ||
self.value = value | ||
self.name = name | ||
self.definition = definition | ||
self.rule = rule | ||
|
||
@property | ||
def path(self): | ||
return [item for item in SPLIT_RE.split(self.name) if item != ''] | ||
|
||
@property | ||
def rule_definition(self): | ||
if not self.rule or not self.definition: | ||
return None | ||
return self.definition.get(self.rule) | ||
|
||
|
||
class JsonSchemaDefinitionException(JsonSchemaException): | ||
""" | ||
Exception raised by generator of validation function. | ||
""" |
1,002 changes: 1,002 additions & 0 deletions
1,002
setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
import logging | ||
import re | ||
import string | ||
from itertools import chain | ||
from urllib.parse import urlparse | ||
|
||
_logger = logging.getLogger(__name__) | ||
|
||
# ------------------------------------------------------------------------------------- | ||
# PEP 440 | ||
|
||
VERSION_PATTERN = r""" | ||
v? | ||
(?: | ||
(?:(?P<epoch>[0-9]+)!)? # epoch | ||
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment | ||
(?P<pre> # pre-release | ||
[-_\.]? | ||
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview)) | ||
[-_\.]? | ||
(?P<pre_n>[0-9]+)? | ||
)? | ||
(?P<post> # post release | ||
(?:-(?P<post_n1>[0-9]+)) | ||
| | ||
(?: | ||
[-_\.]? | ||
(?P<post_l>post|rev|r) | ||
[-_\.]? | ||
(?P<post_n2>[0-9]+)? | ||
) | ||
)? | ||
(?P<dev> # dev release | ||
[-_\.]? | ||
(?P<dev_l>dev) | ||
[-_\.]? | ||
(?P<dev_n>[0-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) | ||
|
||
|
||
def pep440(version: str) -> bool: | ||
return VERSION_REGEX.match(version) is not None | ||
|
||
|
||
# ------------------------------------------------------------------------------------- | ||
# 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) | ||
|
||
|
||
def pep508_identifier(name: str) -> bool: | ||
return PEP508_IDENTIFIER_REGEX.match(name) is not None | ||
|
||
|
||
try: | ||
try: | ||
from packaging import requirements as _req | ||
except ImportError: # pragma: no cover | ||
# let's try setuptools vendored version | ||
from setuptools._vendor.packaging import requirements as _req # type: ignore | ||
|
||
def pep508(value: str) -> bool: | ||
try: | ||
_req.Requirement(value) | ||
return True | ||
except _req.InvalidRequirement: | ||
return False | ||
|
||
|
||
except ImportError: # pragma: no cover | ||
_logger.warning( | ||
"Could not find an installation of `packaging`. Requirements, dependencies and " | ||
"versions might not be validated. " | ||
"To enforce validation, please install `packaging`." | ||
) | ||
|
||
def pep508(value: str) -> bool: | ||
return True | ||
|
||
|
||
def pep508_versionspec(value: str) -> bool: | ||
"""Expression that can be used to specify/lock versions (including ranges)""" | ||
if any(c in value for c in (";", "]", "@")): | ||
# In PEP 508: | ||
# conditional markers, extras and URL specs are not included in the | ||
# versionspec | ||
return False | ||
# Let's pretend we have a dependency called `requirement` with the given | ||
# version spec, then we can re-use the pep508 function for validation: | ||
return pep508(f"requirement{value}") | ||
|
||
|
||
# ------------------------------------------------------------------------------------- | ||
# PEP 517 | ||
|
||
|
||
def pep517_backend_reference(value: str) -> bool: | ||
module, _, obj = value.partition(":") | ||
identifiers = (i.strip() for i in chain(module.split("."), obj.split("."))) | ||
return all(python_identifier(i) for i in identifiers if i) | ||
|
||
|
||
# ------------------------------------------------------------------------------------- | ||
# Classifiers - PEP 301 | ||
|
||
|
||
try: | ||
from trove_classifiers import classifiers as _trove_classifiers | ||
|
||
def trove_classifier(value: str) -> bool: | ||
return value in _trove_classifiers | ||
|
||
|
||
except ImportError: # pragma: no cover | ||
|
||
class _TroveClassifier: | ||
def __init__(self): | ||
self._warned = False | ||
self.__name__ = "trove-classifier" | ||
|
||
def __call__(self, value: str) -> bool: | ||
if self._warned is False: | ||
self._warned = True | ||
_logger.warning("Install ``trove-classifiers`` to ensure validation.") | ||
return True | ||
|
||
trove_classifier = _TroveClassifier() | ||
|
||
|
||
# ------------------------------------------------------------------------------------- | ||
# Non-PEP related | ||
|
||
|
||
def url(value: str) -> bool: | ||
try: | ||
parts = urlparse(value) | ||
return bool(parts.scheme and parts.netloc) | ||
# ^ TODO: should we enforce schema to be http(s)? | ||
except Exception: | ||
return False | ||
|
||
|
||
# https://packaging.python.org/specifications/entry-points/ | ||
ENTRYPOINT_PATTERN = r"[^\[\s=]([^=]*[^\s=])?" | ||
ENTRYPOINT_REGEX = re.compile(f"^{ENTRYPOINT_PATTERN}$", re.I) | ||
RECOMMEDED_ENTRYPOINT_PATTERN = r"[\w.-]+" | ||
RECOMMEDED_ENTRYPOINT_REGEX = re.compile(f"^{RECOMMEDED_ENTRYPOINT_PATTERN}$", re.I) | ||
ENTRYPOINT_GROUP_PATTERN = r"\w+(\.\w+)*" | ||
ENTRYPOINT_GROUP_REGEX = re.compile(f"^{ENTRYPOINT_GROUP_PATTERN}$", re.I) | ||
|
||
|
||
def python_identifier(value: str) -> bool: | ||
return value.isidentifier() | ||
|
||
|
||
def python_qualified_identifier(value: str) -> bool: | ||
if value.startswith(".") or value.endswith("."): | ||
return False | ||
return all(python_identifier(m) for m in value.split(".")) | ||
|
||
|
||
def python_module_name(value: str) -> bool: | ||
return python_qualified_identifier(value) | ||
|
||
|
||
def python_entrypoint_group(value: str) -> bool: | ||
return ENTRYPOINT_GROUP_REGEX.match(value) is not None | ||
|
||
|
||
def python_entrypoint_name(value: str) -> bool: | ||
if not ENTRYPOINT_REGEX.match(value): | ||
return False | ||
if not RECOMMEDED_ENTRYPOINT_REGEX.match(value): | ||
msg = f"Entry point `{value}` does not follow recommended pattern: " | ||
msg += RECOMMEDED_ENTRYPOINT_PATTERN | ||
_logger.warning(msg) | ||
return True | ||
|
||
|
||
def python_entrypoint_reference(value: str) -> bool: | ||
if ":" not in value: | ||
return False | ||
module, _, rest = value.partition(":") | ||
if "[" in rest: | ||
obj, _, extras_ = rest.partition("[") | ||
if extras_.strip()[-1] != "]": | ||
return False | ||
extras = (x.strip() for x in extras_.strip(string.whitespace + "[]").split(",")) | ||
if not all(pep508_identifier(e) for e in extras): | ||
return False | ||
_logger.warning(f"`{value}` - using extras for entry points is not recommended") | ||
else: | ||
obj = rest | ||
|
||
identifiers = chain(module.split("."), obj.split(".")) | ||
return all(python_identifier(i.strip()) for i in identifiers) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters