diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/setuptools/config/_validate_pyproject/formats.py')
-rw-r--r-- | .venv/lib/python3.12/site-packages/setuptools/config/_validate_pyproject/formats.py | 402 |
1 files changed, 402 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/setuptools/config/_validate_pyproject/formats.py b/.venv/lib/python3.12/site-packages/setuptools/config/_validate_pyproject/formats.py new file mode 100644 index 00000000..1cf4a465 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/setuptools/config/_validate_pyproject/formats.py @@ -0,0 +1,402 @@ +""" +The functions in this module are used to validate schemas with the +`format JSON Schema keyword +<https://json-schema.org/understanding-json-schema/reference/string#format>`_. + +The correspondence is given by replacing the ``_`` character in the name of the +function with a ``-`` to obtain the format name and vice versa. +""" + +import builtins +import logging +import os +import re +import string +import typing +from itertools import chain as _chain + +if typing.TYPE_CHECKING: + from typing_extensions import Literal + +_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>alpha|a|beta|b|preview|pre|c|rc) + [-_\.]? + (?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: + """See :ref:`PyPA's version specification <pypa:version-specifiers>` + (initially introduced in :pep:`440`). + """ + 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: + """See :ref:`PyPA's name specification <pypa:name-format>` + (initially introduced in :pep:`508#names`). + """ + 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 ( # type: ignore[no-redef] + requirements as _req, + ) + + def pep508(value: str) -> bool: + """See :ref:`PyPA's dependency specifiers <pypa:dependency-specifiers>` + (initially introduced in :pep:`508`). + """ + 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) + See ``versionspec`` in :ref:`PyPA's dependency specifiers + <pypa:dependency-specifiers>` (initially introduced in :pep:`508`). + """ + 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 reuse the pep508 function for validation: + return pep508(f"requirement{value}") + + +# ------------------------------------------------------------------------------------- +# PEP 517 + + +def pep517_backend_reference(value: str) -> bool: + """See PyPA's specification for defining build-backend references + introduced in :pep:`517#source-trees`. + + This is similar to an entry-point reference (e.g., ``package.module:object``). + """ + 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 + + +def _download_classifiers() -> str: + import ssl + from email.message import Message + from urllib.request import urlopen + + url = "https://pypi.org/pypi?:action=list_classifiers" + context = ssl.create_default_context() + with urlopen(url, context=context) as response: # noqa: S310 (audit URLs) + headers = Message() + headers["content_type"] = response.getheader("content-type", "text/plain") + return response.read().decode(headers.get_param("charset", "utf-8")) # type: ignore[no-any-return] + + +class _TroveClassifier: + """The ``trove_classifiers`` package is the official way of validating classifiers, + however this package might not be always available. + As a workaround we can still download a list from PyPI. + We also don't want to be over strict about it, so simply skipping silently is an + option (classifiers will be validated anyway during the upload to PyPI). + """ + + downloaded: typing.Union[None, "Literal[False]", typing.Set[str]] + """ + None => not cached yet + False => unavailable + set => cached values + """ + + def __init__(self) -> None: + self.downloaded = None + self._skip_download = False + self.__name__ = "trove_classifier" # Emulate a public function + + def _disable_download(self) -> None: + # This is a private API. Only setuptools has the consent of using it. + self._skip_download = True + + def __call__(self, value: str) -> bool: + if self.downloaded is False or self._skip_download is True: + return True + + if os.getenv("NO_NETWORK") or os.getenv("VALIDATE_PYPROJECT_NO_NETWORK"): + self.downloaded = False + msg = ( + "Install ``trove-classifiers`` to ensure proper validation. " + "Skipping download of classifiers list from PyPI (NO_NETWORK)." + ) + _logger.debug(msg) + return True + + if self.downloaded is None: + msg = ( + "Install ``trove-classifiers`` to ensure proper validation. " + "Meanwhile a list of classifiers will be downloaded from PyPI." + ) + _logger.debug(msg) + try: + self.downloaded = set(_download_classifiers().splitlines()) + except Exception: + self.downloaded = False + _logger.debug("Problem with download, skipping validation") + return True + + return value in self.downloaded or value.lower().startswith("private ::") + + +try: + from trove_classifiers import classifiers as _trove_classifiers + + def trove_classifier(value: str) -> bool: + """See https://pypi.org/classifiers/""" + return value in _trove_classifiers or value.lower().startswith("private ::") + +except ImportError: # pragma: no cover + trove_classifier = _TroveClassifier() + + +# ------------------------------------------------------------------------------------- +# Stub packages - PEP 561 + + +def pep561_stub_name(value: str) -> bool: + """Name of a directory containing type stubs. + It must follow the name scheme ``<package>-stubs`` as defined in + :pep:`561#stub-only-packages`. + """ + top, *children = value.split(".") + if not top.endswith("-stubs"): + return False + return python_module_name(".".join([top[: -len("-stubs")], *children])) + + +# ------------------------------------------------------------------------------------- +# Non-PEP related + + +def url(value: str) -> bool: + """Valid URL (validation uses :obj:`urllib.parse`). + For maximum compatibility please make sure to include a ``scheme`` prefix + in your URL (e.g. ``http://``). + """ + from urllib.parse import urlparse + + try: + parts = urlparse(value) + if not parts.scheme: + _logger.warning( + "For maximum compatibility please make sure to include a " + "`scheme` prefix in your URL (e.g. 'http://'). " + f"Given value: {value}" + ) + if not (value.startswith("/") or value.startswith("\\") or "@" in value): + parts = urlparse(f"http://{value}") + + return bool(parts.scheme and parts.netloc) + 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: + """Can be used as identifier in Python. + (Validation uses :obj:`str.isidentifier`). + """ + return value.isidentifier() + + +def python_qualified_identifier(value: str) -> bool: + """ + Python "dotted identifier", i.e. a sequence of :obj:`python_identifier` + concatenated with ``"."`` (e.g.: ``package.module.submodule``). + """ + 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: + """Module name that can be used in an ``import``-statement in Python. + See :obj:`python_qualified_identifier`. + """ + return python_qualified_identifier(value) + + +def python_module_name_relaxed(value: str) -> bool: + """Similar to :obj:`python_module_name`, but relaxed to also accept + dash characters (``-``) and cover special cases like ``pip-run``. + + It is recommended, however, that beginners avoid dash characters, + as they require advanced knowledge about Python internals. + + The following are disallowed: + + * names starting/ending in dashes, + * names ending in ``-stubs`` (potentially collide with :obj:`pep561_stub_name`). + """ + if value.startswith("-") or value.endswith("-"): + return False + if value.endswith("-stubs"): + return False # Avoid collision with PEP 561 + return python_module_name(value.replace("-", "_")) + + +def python_entrypoint_group(value: str) -> bool: + """See ``Data model > group`` in the :ref:`PyPA's entry-points specification + <pypa:entry-points>`. + """ + return ENTRYPOINT_GROUP_REGEX.match(value) is not None + + +def python_entrypoint_name(value: str) -> bool: + """See ``Data model > name`` in the :ref:`PyPA's entry-points specification + <pypa:entry-points>`. + """ + 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: + """Reference to a Python object using in the format:: + + importable.module:object.attr + + See ``Data model >object reference`` in the :ref:`PyPA's entry-points specification + <pypa:entry-points>`. + """ + 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 + + module_parts = module.split(".") + identifiers = _chain(module_parts, obj.split(".")) if rest else iter(module_parts) + return all(python_identifier(i.strip()) for i in identifiers) + + +def uint8(value: builtins.int) -> bool: + r"""Unsigned 8-bit integer (:math:`0 \leq x < 2^8`)""" + return 0 <= value < 2**8 + + +def uint16(value: builtins.int) -> bool: + r"""Unsigned 16-bit integer (:math:`0 \leq x < 2^{16}`)""" + return 0 <= value < 2**16 + + +def uint(value: builtins.int) -> bool: + r"""Unsigned 64-bit integer (:math:`0 \leq x < 2^{64}`)""" + return 0 <= value < 2**64 + + +def int(value: builtins.int) -> bool: + r"""Signed 64-bit integer (:math:`-2^{63} \leq x < 2^{63}`)""" + return -(2**63) <= value < 2**63 + + +try: + from packaging import licenses as _licenses + + def SPDX(value: str) -> bool: + """See :ref:`PyPA's License-Expression specification + <pypa:core-metadata-license-expression>` (added in :pep:`639`). + """ + try: + _licenses.canonicalize_license_expression(value) + return True + except _licenses.InvalidLicenseExpression: + return False + +except ImportError: # pragma: no cover + _logger.warning( + "Could not find an up-to-date installation of `packaging`. " + "License expressions might not be validated. " + "To enforce validation, please install `packaging>=24.2`." + ) + + def SPDX(value: str) -> bool: + return True |