Skip to content

Commit

Permalink
Add vendored deps (tomli and validate-pyproject) (pypa#3066)
Browse files Browse the repository at this point in the history
Extra dependencies can be used to support project metadata in
`pyproject.toml`.
  • Loading branch information
abravalheri committed Feb 19, 2022
2 parents 94b3336 + 4f50d08 commit 9692cb6
Show file tree
Hide file tree
Showing 21 changed files with 2,873 additions and 1 deletion.
3 changes: 3 additions & 0 deletions changelog.d/3066.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added vendored dependencies for :pypi:`tomli`, :pypi:`validate-pyproject`.

These dependencies are used to read ``pyproject.toml`` files and validate them.
439 changes: 439 additions & 0 deletions setuptools/_vendor/_validate_pyproject/NOTICE

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions setuptools/_vendor/_validate_pyproject/__init__.py
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 setuptools/_vendor/_validate_pyproject/extra_validations.py
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,)
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,004 changes: 1,004 additions & 0 deletions setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py

Large diffs are not rendered by default.

200 changes: 200 additions & 0 deletions setuptools/_vendor/_validate_pyproject/formats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
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)
1 change: 1 addition & 0 deletions setuptools/_vendor/tomli-2.0.1.dist-info/INSTALLER
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pip
21 changes: 21 additions & 0 deletions setuptools/_vendor/tomli-2.0.1.dist-info/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Taneli Hukkinen

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading

0 comments on commit 9692cb6

Please sign in to comment.