diff options
Diffstat (limited to '')
-rw-r--r-- | third_party/python/setuptools/setuptools/config/_validate_pyproject/formats.py | 275 |
1 files changed, 275 insertions, 0 deletions
diff --git a/third_party/python/setuptools/setuptools/config/_validate_pyproject/formats.py b/third_party/python/setuptools/setuptools/config/_validate_pyproject/formats.py new file mode 100644 index 0000000000..486d52602c --- /dev/null +++ b/third_party/python/setuptools/setuptools/config/_validate_pyproject/formats.py @@ -0,0 +1,275 @@ +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>(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 + + +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: + headers = Message() + headers["content_type"] = response.getheader("content-type", "text/plain") + return response.read().decode(headers.get_param("charset", "utf-8")) + + +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]] + + def __init__(self): + self.downloaded = None + self._skip_download = False + # None => not cached yet + # False => cache not available + self.__name__ = "trove_classifier" # Emulate a public function + + def _disable_download(self): + # 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: + 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: + 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: + 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: + 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: + 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 module_parts + return all(python_identifier(i.strip()) for i in identifiers) |