abravalheri-validate-pyproject-4b2e70d/0000775000175000017500000000000015140154751020074 5ustar carstencarstenabravalheri-validate-pyproject-4b2e70d/CHANGELOG.rst0000664000175000017500000002056115140154751022121 0ustar carstencarsten========= Changelog ========= .. Development Version ==================== * Added support for specifying package-data for stub packages, #248. Version 0.24.1 ============== * Fixed multi plugin id was read from the wrong place by @henryiii, #240. * Implemented alternative plugin sorting, #243 Version 0.24 ============ * Fix integration with ``SchemaStore`` by loading extra/side schemas, #226, #229. * Add support for loading extra schemas, #226. * Fixed verify author dict is not empty, #232. * Added support for ``validate_pyproject.multi_schema`` plugins with extra schemas, #231. * ``validate-pyproject`` no longer communicates test dependencies via the ``tests`` extra and documentation dependencies dependencies via the ``docs/requirements.txt`` file. Instead :doc:`pypa:dependency-groups` have been adopted to support CI environments, #227. As a result, ``uv``'s high level interface also works for developers. You can use the :pypi:`dependency-groups` package on PyPI if you need to convert to a classic requirements list. Contributions by @henryiii. Version 0.23 ============ * Validate SPDX license expressions by @cdce8p in #217 Version 0.22 ============ * Prevent injecting defaults and modifying input in-place, by @henryiii in #213 Version 0.21 ============ * Added support PEP 735, #208 * Added support PEP 639, #210 * Renamed ``testing`` extra to ``test``, #212 * General updates in CI setup Version 0.20 ============ - ``setuptools`` plugin: * Update ``setuptools.schema.json``, #206 Maintenance and Minor Changes ----------------------------- - Fix misplaced comments on ``formats.py``, #184 - Adopt ``--import-mode=importlib`` for pytest to prevent errors with ``importlib.metadata``, #203 - Update CI configs, #195 #202, #204, #205 Version 0.19 ============ - Relax requirements about module names to also allow dash characters, #164 - Migrate metadata to ``pyproject.toml`` , #192 Version 0.18 ============ - Allow overwriting schemas referring to the same ``tool``, #175 Version 0.17 ============ - Update version regex according to latest packaging version, #153 - Remove duplicate ``# ruff: noqa``, #158 - Remove invalid top-of-the-file ``# type: ignore`` statement, #159 - Align ``tool.setuptools.dynamic.optional-dependencies`` with ``project.optional-dependencies``, #170 - Bump min Python version to 3.8, #167 Version 0.16 ============ - Fix setuptools ``readme`` field , #116 - Fix ``oneOf <> anyOf`` in setuptools schema, #117 - Add previously omitted type keywords for string values, #117 - Add schema validator check, #118 - Add ``SchemaStore`` conversion script, #119 - Allow tool(s) to be specified via URL (added CLI option: ``--tool``), #121 - Support ``uint`` formats (as used by Ruff's schema), #128 - Allow schemas to be loaded from ``SchemaStore`` (added CLI option: ``--store``), #133 Version 0.15 ============ - Update ``setuptools`` schema definitions, #112 - Add ``__repr__`` to plugin wrapper, by @henryiii #114 - Fix standard ``$schema`` ending ``#``, by @henryiii #113 Version 0.14 ============ - Ensure reporting show more detailed error messages for ``RedefiningStaticFieldAsDynamic``, #104 - Add support for ``repo-review``, by @henryiii in #105 Version 0.13 ============ - Make it clear when using input from ``stdin``, #96 - Fix summary for ``allOf``, #100 - ``setuptools`` plugin: - Improve validation of ``attr`` directives, #101 Version 0.12.2 ============== - ``setuptools`` plugin: - Fix problem with ``license-files`` patterns, by removing ``default`` value. Version 0.12.1 ============== - ``setuptools`` plugin: - Allow PEP 561 stub names in ``tool.setuptools.package-dir``, #87 Version 0.12 ============ - ``setuptools`` plugin: - Allow PEP 561 stub names in ``tool.setuptools.packages``, #86 Version 0.11 ============ - Improve error message for invalid replacements in the ``pre_compile`` CLI, #71 - Allow package to be build from git archive, #53 - Improve error message for invalid replacements in the ``pre_compile`` CLI, #71 - Error-out when extra keys are added to ``project.authors/maintainers``, #82 - De-vendor ``fastjsonschema``, #83 Version 0.10.1 ============== - Ensure ``LICENSE.txt`` is added to wheel. Version 0.10 ============ - Add ``NOTICE.txt`` to ``license_files``, #58 - Use default SSL context when downloading classifiers from PyPI, #57 - Remove ``setup.py``, #52 - Explicitly limit oldest supported Python version - Replace usage of ``cgi.parse_header`` with ``email.message.Message`` Version 0.9 =========== - Use ``tomllib`` from the standard library in Python 3.11+, #42 Version 0.8.1 ============= - Workaround typecheck inconsistencies between different Python versions - Publish :pep:`561` type hints, #43 Version 0.8 =========== - New :pypi:`pre-commit` hook, #40 - Allow multiple TOML files to be validated at once via **CLI** (*no changes regarding the Python API*). Version 0.7.2 ============= - ``setuptools`` plugin: - Allow ``dependencies``/``optional-dependencies`` to use file directives, #37 Version 0.7.1 ============= - CI: Enforced doctests - CI: Add more tests for situations when downloading classifiers is disabled Version 0.7 =========== - **Deprecated** use of ``validate_pyproject.vendoring``. This module is replaced by ``validate_pyproject.pre_compile``. Version 0.6.1 ============= - Fix validation of ``version`` to ensure it is given either statically or dynamically, #29 Version 0.6 ============= - Allow private classifiers, #26 - ``setuptools`` plugin: - Remove ``license`` and ``license-files`` from ``tool.setuptools.dynamic``, #27 Version 0.5.2 ============= - Exported ``ValidationError`` from the main file when vendored, :pr:`23` - Removed ``ValidationError`` traceback to avoid polluting the user logs with generate code, :pr:`24` Version 0.5.1 ============= - Fixed typecheck errors (only found against GitHub Actions, not Cirrus CI), :pr:`22` Version 0.5 =========== - Fixed entry-points format to allow values without the ``:obj.attr part``, :pr:`8` - Improved trove-classifier validation, even when the package is not installed, :pr:`9` - Improved URL validation when scheme prefix is not present, :pr:`14` - Vendor :pypi:`fastjsonschema` to facilitate applying patches and latest updates, :pr:`15` - Remove fixes for old version of :pypi:`fastjsonschema`, :pr:`16`, :pr:`19` - Replaced usage of :mod:`importlib.resources` legacy functions with the new API, :pr:`17` - Improved error messages, :pr:`18` - Added GitHub Actions for automatic test and release of tags, :pr:`11` Version 0.4 =========== - Validation now fails when non-standardised fields to be added to the project table (:issue:`4`, :pr:`5`) - Terminology and schema names were also updated to avoid specific PEP numbers and refer instead to living standards (:issue:`6`, :pr:`7`) Version 0.3.3 ============= - Remove upper pin from the :pypi:`tomli` dependency by :user:`hukkin` (:pr:`1`) - Fix failing :pypi:`blacken-docs` pre-commit hook by :user:`hukkin` (:pr:`2`) - Update versions of tools and containers used in the CI setup (:pr:`3`) Version 0.3.2 ============= - Updated ``fastjsonschema`` dependency version. - Removed workarounds for ``fastjsonschema`` pre 2.15.2 Version 0.3.1 ============= - ``setuptools`` plugin: - Fixed missing ``required`` properties for the ``attr:`` and ``file:`` directives (previously empty objects were allowed). Version 0.3 =========== - ``setuptools`` plugin: - Added support for ``readme``, ``license`` and ``license-files`` via ``dynamic``. .. warning:: ``license`` and ``license-files`` in ``dynamic`` are **PROVISIONAL** they are likely to change depending on :pep:`639` - Removed support for ``tool.setuptools.dynamic.{scripts,gui-scripts}``. Dynamic values for ``project.{scripts,gui-scripts}`` are expected to be dynamically derived from ``tool.setuptools.dynamic.entry-points``. Version 0.2 =========== - ``setuptools`` plugin: - Added ``cmdclass`` support Version 0.1 =========== - ``setuptools`` plugin: - Added ``data-files`` support (although this option is marked as deprecated). - Unified ``tool.setuptools.packages.find`` and ``tool.setuptools.packages.find-namespace`` options by adding a new keyword ``namespaces`` - ``tool.setuptools.packages.find.where`` now accepts a list of directories (previously only one directory was accepted). Version 0.0.1 ============= - Initial release with basic functionality abravalheri-validate-pyproject-4b2e70d/.projections.json0000664000175000017500000000102515140154751023402 0ustar carstencarsten{ "*.py": { "autoformat": true, "textwidth": 88 }, "*.json": { "textwidth": 88 }, "src/validate_pyproject/*/__init__.py" : { "alternate": "tests/test_{basename}.py", "type": "source" }, "src/validate_pyproject/*.py" : { "alternate": "tests/{dirname}/test_{basename}.py", "type": "source" }, "tests/**/test_*.py" : { "alternate": [ "src/validate_pyproject/{dirname}/{basename}.py", "src/validate_pyproject/{dirname}/{basename}/__init__.py" ], "type": "test" } } abravalheri-validate-pyproject-4b2e70d/src/0000775000175000017500000000000015140154751020663 5ustar carstencarstenabravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/0000775000175000017500000000000015140154751024553 5ustar carstencarstenabravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/errors.py0000664000175000017500000000450415140154751026444 0ustar carstencarsten""" In general, users should expect :obj:`validate_pyproject.errors.ValidationError` from :obj:`validate_pyproject.api.Validator.__call__`. Note that ``validate-pyproject`` derives most of its exceptions from :mod:`fastjsonschema`, so it might make sense to also have a look on :obj:`fastjsonschema.JsonSchemaException`, :obj:`fastjsonschema.JsonSchemaValueException` and :obj:`fastjsonschema.JsonSchemaDefinitionException`. ) """ from textwrap import dedent from fastjsonschema import ( JsonSchemaDefinitionException as _JsonSchemaDefinitionException, ) from .error_reporting import ValidationError class URLMissingTool(RuntimeError): _DESC = """\ The '--tool' option requires a tool name. Correct form is '--tool ={url}', with an optional '#json/pointer' at the end. """ __doc__ = _DESC def __init__(self, url: str): msg = dedent(self._DESC).strip() msg = msg.format(url=url) super().__init__(msg) class InvalidSchemaVersion(_JsonSchemaDefinitionException): _DESC = """\ All schemas used in the validator should be specified using the same version \ as the toplevel schema ({version!r}). Schema for {name!r} has version {given!r}. """ __doc__ = _DESC def __init__(self, name: str, given_version: str, required_version: str): msg = dedent(self._DESC).strip() msg = msg.format(name=name, version=required_version, given=given_version) super().__init__(msg) class SchemaMissingId(_JsonSchemaDefinitionException): _DESC = """\ All schemas used in the validator MUST define a unique toplevel `"$id"`. No `"$id"` was found for schema associated with {reference!r}. """ __doc__ = _DESC def __init__(self, reference: str): msg = dedent(self._DESC).strip() super().__init__(msg.format(reference=reference)) class SchemaWithDuplicatedId(_JsonSchemaDefinitionException): _DESC = """\ All schemas used in the validator MUST define a unique toplevel `"$id"`. `$id = {schema_id!r}` was found at least twice. """ __doc__ = _DESC def __init__(self, schema_id: str): msg = dedent(self._DESC).strip() super().__init__(msg.format(schema_id=schema_id)) __all__ = [ "InvalidSchemaVersion", "SchemaMissingId", "SchemaWithDuplicatedId", "ValidationError", ] abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/caching.py0000664000175000017500000000330515140154751026522 0ustar carstencarsten# This module is intentionally kept minimal, # so that it can be imported without triggering imports outside stdlib. from __future__ import annotations import hashlib import logging import os from pathlib import Path from typing import TYPE_CHECKING, Callable, Union if TYPE_CHECKING: import io PathLike = Union[str, "os.PathLike[str]"] _logger = logging.getLogger(__name__) def as_file( fn: Callable[[str], io.StringIO], arg: str, cache_dir: PathLike | None = None, ) -> io.StringIO | io.BufferedReader: """ Cache the result of calling ``fn(arg)`` into a file inside ``cache_dir``. The file name is derived from ``arg``. If no ``cache_dir`` is provided, it is equivalent to calling ``fn(arg)``. The return value can be used as a context. """ cache_path = path_for(arg, cache_dir) if not cache_path: return fn(arg) if cache_path.exists(): _logger.debug(f"Using cached {arg} from {cache_path}") else: with fn(arg) as f: cache_path.write_text(f.getvalue(), encoding="utf-8") _logger.debug(f"Caching {arg} into {cache_path}") return open(cache_path, "rb") def path_for(arbitrary_id: str, cache: PathLike | None = None) -> Path | None: cache_dir = cache or os.getenv("VALIDATE_PYPROJECT_CACHE_REMOTE") if not cache_dir: return None escaped = "".join(c if c.isalnum() else "-" for c in arbitrary_id) sha1 = hashlib.sha1(arbitrary_id.encode()) # noqa: S324 # ^-- Non-crypto context and appending `escaped` should minimise collisions return Path(os.path.expanduser(cache_dir), f"{sha1.hexdigest()}-{escaped}") # ^-- Intentionally uses `os.path` instead of `pathlib` to avoid exception abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/__init__.py0000664000175000017500000000055115140154751026665 0ustar carstencarstenfrom importlib.metadata import PackageNotFoundError, version # pragma: no cover try: # Change here if project is renamed and does not equal the package name dist_name = "validate-pyproject" __version__ = version(dist_name) except PackageNotFoundError: # pragma: no cover __version__ = "unknown" finally: del version, PackageNotFoundError abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/py.typed0000664000175000017500000000003315140154751026246 0ustar carstencarsten# Marker file for PEP 561. abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/vendoring/0000775000175000017500000000000015140154751026546 5ustar carstencarstenabravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/vendoring/__init__.py0000664000175000017500000000131015140154751030652 0ustar carstencarstenimport warnings from functools import wraps from inspect import cleandoc from typing import Any from ..pre_compile import pre_compile def _deprecated(orig: Any, repl: Any) -> Any: msg = f""" `{orig.__module__}:{orig.__name__}` is deprecated and will be removed in future versions of `validate-pyproject`. Please use `{repl.__module__}:{repl.__name__}` instead. """ @wraps(orig) def _wrapper(*args: Any, **kwargs: Any) -> Any: warnings.warn(cleandoc(msg), category=DeprecationWarning, stacklevel=2) return repl(*args, **kwargs) return _wrapper def vendorify(*args: Any, **kwargs: Any) -> Any: return _deprecated(vendorify, pre_compile)(*args, **kwargs) abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/vendoring/__main__.py0000664000175000017500000000007515140154751030642 0ustar carstencarstenfrom . import cli if __name__ == "__main__": cli.main() abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/vendoring/cli.py0000664000175000017500000000043015140154751027664 0ustar carstencarstenfrom typing import Any from ..pre_compile import cli from . import _deprecated def run(*args: Any, **kwargs: Any) -> Any: return _deprecated(run, cli.run)(*args, **kwargs) def main(*args: Any, **kwargs: Any) -> Any: return _deprecated(run, cli.main)(*args, **kwargs) abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/formats.py0000664000175000017500000003601515140154751026605 0ustar carstencarsten""" The functions in this module are used to validate schemas with the `format JSON Schema keyword `_. The correspondence is given by replacing the ``_`` character in the name of the function with a ``-`` to obtain the format name and vice versa. """ from __future__ import annotations import keyword import logging import os import re import string import typing from itertools import chain as _chain if typing.TYPE_CHECKING: import builtins from typing_extensions import Literal _logger = logging.getLogger(__name__) # ------------------------------------------------------------------------------------- # PEP 440 VERSION_PATTERN = r""" v? (?: (?:(?P[0-9]+)!)? # epoch (?P[0-9]+(?:\.[0-9]+)*) # release segment (?P
                                          # pre-release
            [-_\.]?
            (?Palpha|a|beta|b|preview|pre|c|rc)
            [-_\.]?
            (?P[0-9]+)?
        )?
        (?P                                         # post release
            (?:-(?P[0-9]+))
            |
            (?:
                [-_\.]?
                (?Ppost|rev|r)
                [-_\.]?
                (?P[0-9]+)?
            )
        )?
        (?P                                          # dev release
            [-_\.]?
            (?Pdev)
            [-_\.]?
            (?P[0-9]+)?
        )?
    )
    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
"""

VERSION_REGEX = re.compile(
    r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE
)


def pep440(version: str) -> bool:
    """See :ref:`PyPA's version specification `
    (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.IGNORECASE)


def pep508_identifier(name: str) -> bool:
    """See :ref:`PyPA's name specification `
    (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 `
        (initially introduced in :pep:`508`).
        """
        try:
            _req.Requirement(value)
        except _req.InvalidRequirement:
            return False
        return True

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:  # noqa: ARG001
        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
    ` (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: None | Literal[False] | 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:  # noqa: BLE001
                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 ``-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 "@" in value):
                parts = urlparse(f"http://{value}")

        return bool(parts.scheme and parts.netloc)
    except Exception:  # noqa: BLE001
        return False


# https://packaging.python.org/specifications/entry-points/
ENTRYPOINT_PATTERN = r"[^\[\s=]([^=]*[^\s=])?"
ENTRYPOINT_REGEX = re.compile(f"^{ENTRYPOINT_PATTERN}$", re.IGNORECASE)
RECOMMEDED_ENTRYPOINT_PATTERN = r"[\w.-]+"
RECOMMEDED_ENTRYPOINT_REGEX = re.compile(
    f"^{RECOMMEDED_ENTRYPOINT_PATTERN}$", re.IGNORECASE
)
ENTRYPOINT_GROUP_PATTERN = r"\w+(\.\w+)*"
ENTRYPOINT_GROUP_REGEX = re.compile(f"^{ENTRYPOINT_GROUP_PATTERN}$", re.IGNORECASE)


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
    `.
    """
    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
    `.
    """
    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
    `.
    """
    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 uint32(value: builtins.int) -> bool:
    r"""Unsigned 32-bit integer (:math:`0 \leq x < 2^{32}`)"""
    return 0 <= value < 2**32


def uint64(value: builtins.int) -> bool:
    r"""Unsigned 64-bit integer (:math:`0 \leq x < 2^{64}`)"""
    return 0 <= value < 2**64


def uint(value: builtins.int) -> bool:
    r"""Signed 64-bit integer (:math:`0 \leq x < 2^{64}`)"""
    return 0 <= value < 2**64


def int8(value: builtins.int) -> bool:
    r"""Signed 8-bit integer (:math:`-2^{7} \leq x < 2^{7}`)"""
    return -(2**7) <= value < 2**7


def int16(value: builtins.int) -> bool:
    r"""Signed 16-bit integer (:math:`-2^{15} \leq x < 2^{15}`)"""
    return -(2**15) <= value < 2**15


def int32(value: builtins.int) -> bool:
    r"""Signed 32-bit integer (:math:`-2^{31} \leq x < 2^{31}`)"""
    return -(2**31) <= value < 2**31


def int64(value: builtins.int) -> bool:
    r"""Signed 64-bit integer (:math:`-2^{63} \leq x < 2^{63}`)"""
    return -(2**63) <= value < 2**63


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
        ` (added in :pep:`639`).
        """
        try:
            _licenses.canonicalize_license_expression(value)
        except _licenses.InvalidLicenseExpression:
            return False
        return True

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:  # noqa: ARG001
        return True


VALID_IMPORT_NAME = re.compile(
    r"""
    ^                                  # start of string
        [A-Za-z_][A-Za-z_0-9]+         # a valid Python identifier
        (?:\.[A-Za-z_][A-Za-z_0-9]*)*  # optionally followed by .identifier's
    (?:\s*;\s*private)?                # optionally followed by ; private
    $                                  # end of string
    """,
    re.VERBOSE,
)


def import_name(value: str) -> bool:
    """This is a valid import name. It has to be series of python identifiers
    (not keywords), separated by dots, optionally followed by a semicolon and
    the keyword "private".
    """
    if VALID_IMPORT_NAME.match(value) is None:
        return False

    idents, _, _ = value.partition(";")
    return all(not keyword.iskeyword(ident) for ident in idents.rstrip().split("."))
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/http.py0000664000175000017500000000111315140154751026100 0ustar  carstencarsten# This module is intentionally kept minimal,
# so that it can be imported without triggering imports outside stdlib.
import io
import sys
from urllib.request import urlopen

if sys.platform == "emscripten" and "pyodide" in sys.modules:
    from pyodide.http import open_url
else:

    def open_url(url: str) -> io.StringIO:
        if not url.startswith(("http:", "https:")):
            msg = "URL must start with 'http:' or 'https:'"
            raise ValueError(msg)
        with urlopen(url) as response:  # noqa: S310
            return io.StringIO(response.read().decode("utf-8"))
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/__main__.py0000664000175000017500000000003615140154751026644 0ustar  carstencarstenfrom .cli import main

main()
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/_tomllib.py0000664000175000017500000000112115140154751026721 0ustar  carstencarstenimport sys

try:  # pragma: no cover
    if sys.version_info[:2] >= (3, 11):
        from tomllib import TOMLDecodeError, loads
    else:
        from tomli import TOMLDecodeError, loads
except ImportError:  # pragma: no cover
    try:
        from toml import (  # type: ignore[no-redef,import-untyped]
            TomlDecodeError as TOMLDecodeError,
        )
        from toml import loads  # type: ignore[no-redef]
    except ImportError as ex:
        msg = "Please install `tomli` (TOML parser)"
        raise ImportError(msg) from ex


__all__ = [
    "TOMLDecodeError",
    "loads",
]
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/repo_review.py0000664000175000017500000000215015140154751027451 0ustar  carstencarstenfrom __future__ import annotations

from typing import Any

import fastjsonschema

from . import api, plugins

__all__ = ["VPP001", "repo_review_checks", "repo_review_families"]


class VPP001:
    """Validate pyproject.toml"""

    family = "validate-pyproject"

    @staticmethod
    def check(pyproject: dict[str, Any]) -> str:
        validator = api.Validator()
        try:
            validator(pyproject)
        except fastjsonschema.JsonSchemaValueException as e:
            return f"Invalid pyproject.toml! Error: {e}"
        return ""


def repo_review_checks() -> dict[str, VPP001]:
    return {"VPP001": VPP001()}


def repo_review_families(pyproject: dict[str, Any]) -> dict[str, dict[str, str]]:
    has_distutils = "distutils" in pyproject.get("tool", {})
    plugin_list = plugins.list_from_entry_points(
        lambda e: e.name != "distutils" or has_distutils
    )
    plugin_names = (f"`[tool.{n.tool}]`" for n in plugin_list if n.tool)
    descr = f"Checks `[build-system]`, `[project]`, {', '.join(plugin_names)}"
    return {"validate-pyproject": {"name": "Validate-PyProject", "description": descr}}
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/pre_compile/0000775000175000017500000000000015140154751027051 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/pre_compile/__init__.py0000664000175000017500000001077015140154751031167 0ustar  carstencarstenfrom __future__ import annotations

import logging
from importlib import metadata as _M
from pathlib import Path
from types import MappingProxyType
from typing import TYPE_CHECKING, Mapping, Sequence

import fastjsonschema as FJS

from .. import api, dist_name, types

if TYPE_CHECKING:  # pragma: no cover
    import os

    from ..plugins import PluginProtocol


_logger = logging.getLogger(__name__)


TEXT_REPLACEMENTS = MappingProxyType(
    {
        "from fastjsonschema import": "from .fastjsonschema_exceptions import",
    }
)


def pre_compile(  # noqa: PLR0913
    output_dir: str | os.PathLike = ".",
    main_file: str = "__init__.py",
    original_cmd: str = "",
    plugins: api.AllPlugins | Sequence[PluginProtocol] = api.ALL_PLUGINS,
    text_replacements: Mapping[str, str] = TEXT_REPLACEMENTS,
    *,
    extra_plugins: Sequence[PluginProtocol] = (),
) -> Path:
    """Populate the given ``output_dir`` with all files necessary to perform
    the validation.
    The validation can be performed by calling the ``validate`` function inside the
    the file named with the ``main_file`` value.
    ``text_replacements`` can be used to
    """
    out = Path(output_dir)
    out.mkdir(parents=True, exist_ok=True)
    replacements = {**TEXT_REPLACEMENTS, **text_replacements}

    validator = api.Validator(plugins, extra_plugins=extra_plugins)
    header = "\n".join(NOCHECK_HEADERS)
    code = replace_text(validator.generated_code, replacements)
    _write(out / "fastjsonschema_validations.py", header + code)

    copy_fastjsonschema_exceptions(out, replacements)
    copy_module("extra_validations", out, replacements)
    copy_module("formats", out, replacements)
    copy_module("error_reporting", out, replacements)
    write_main(out / main_file, validator.schema, replacements)
    write_notice(out, main_file, original_cmd, replacements)
    (out / "__init__.py").touch()

    return out


def replace_text(text: str, replacements: dict[str, str]) -> str:
    for orig, subst in replacements.items():
        text = text.replace(orig, subst)
    return text


def copy_fastjsonschema_exceptions(
    output_dir: Path, replacements: dict[str, str]
) -> Path:
    code = replace_text(api.read_text(FJS.__name__, "exceptions.py"), replacements)
    return _write(output_dir / "fastjsonschema_exceptions.py", code)


def copy_module(name: str, output_dir: Path, replacements: dict[str, str]) -> Path:
    code = api.read_text(api.__package__, f"{name}.py")
    return _write(output_dir / f"{name}.py", replace_text(code, replacements))


def write_main(
    file_path: Path,
    schema: types.Schema,  # noqa: ARG001
    replacements: dict[str, str],
) -> Path:
    code = api.read_text(__name__, "main_file.template")
    return _write(file_path, replace_text(code, replacements))


def write_notice(
    out: Path, main_file: str, cmd: str, replacements: dict[str, str]
) -> Path:
    if cmd:
        opening = api.read_text(__name__, "cli-notice.template")
        opening = opening.format(command=cmd)
    else:
        opening = api.read_text(__name__, "api-notice.template")
    notice = api.read_text(__name__, "NOTICE.template")
    notice = notice.format(notice=opening, main_file=main_file, **load_licenses())
    notice = replace_text(notice, replacements)

    return _write(out / "NOTICE", notice)


def load_licenses() -> dict[str, str]:
    return {
        "fastjsonschema_license": _find_and_load_licence(_M.files("fastjsonschema")),
        "validate_pyproject_license": _find_and_load_licence(_M.files(dist_name)),
    }


NOCHECK_HEADERS = (
    "# noqa",
    "# ruff: noqa",
    "# flake8: noqa",
    "# pylint: skip-file",
    "# mypy: ignore-errors",
    "# yapf: disable",
    "# pylama:skip=1",
    "\n\n# *** PLEASE DO NOT MODIFY DIRECTLY: Automatically generated code *** \n\n\n",
)


def _find_and_load_licence(files: Sequence[_M.PackagePath] | None) -> str:
    if files is None:  # pragma: no cover
        msg = "Could not find LICENSE for package"
        raise ImportError(msg)
    try:
        return next(f for f in files if f.stem.upper() == "LICENSE").read_text("UTF-8")
    except FileNotFoundError:  # pragma: no cover
        msg = (
            "Please make sure to install `validate-pyproject` and `fastjsonschema` "
            "in a NON-EDITABLE way. This is necessary due to the issue #112 in "
            "python/importlib_metadata."
        )
        _logger.warning(msg)
        raise


def _write(file: Path, text: str) -> Path:
    file.write_text(text.rstrip() + "\n", encoding="utf-8")  # POSIX convention
    return file
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/pre_compile/NOTICE.template0000664000175000017500000000174515140154751031576 0ustar  carstencarsten{notice}

You can report issues or suggest changes directly to `validate-pyproject`
(or to the relevant plugin repository)

- https://github.com/abravalheri/validate-pyproject/issues


***

The following files include code from opensource projects
(either as direct copies or modified versions):

- `fastjsonschema_exceptions.py`:
    - project: `fastjsonschema` - licensed under BSD-3-Clause
      (https://github.com/horejsek/python-fastjsonschema)
- `extra_validations.py` and `format.py`, `error_reporting.py`:
    - project: `validate-pyproject` - licensed under MPL-2.0
      (https://github.com/abravalheri/validate-pyproject)


Additionally the following files are automatically generated by tools provided
by the same projects:

- `{main_file}`
- `fastjsonschema_validations.py`

The relevant copyright notes and licenses are included below.


***

`fastjsonschema`
================

{fastjsonschema_license}


***

`validate-pyproject`
====================

{validate_pyproject_license}
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/pre_compile/api-notice.template0000664000175000017500000000014515140154751032636 0ustar  carstencarstenThe code contained in this directory was automatically generated.
Please avoid changing it manually.
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/pre_compile/__main__.py0000664000175000017500000000007515140154751031145 0ustar  carstencarstenfrom . import cli

if __name__ == "__main__":
    cli.main()
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/pre_compile/cli.py0000664000175000017500000000755715140154751030210 0ustar  carstencarsten# ruff: noqa: C408
# Unnecessary `dict` call (rewrite as a literal)
from __future__ import annotations

import json
import logging
import sys
from functools import partial, wraps
from pathlib import Path
from types import MappingProxyType
from typing import Any, Mapping, NamedTuple, Sequence

from .. import cli
from ..plugins import PluginProtocol, PluginWrapper
from ..plugins import list_from_entry_points as list_plugins_from_entry_points
from ..remote import RemotePlugin, load_store
from . import pre_compile

if sys.platform == "win32":  # pragma: no cover
    from subprocess import list2cmdline as arg_join
else:  # pragma: no cover
    from shlex import join as arg_join


_logger = logging.getLogger(__package__)


def JSON_dict(name: str, value: str) -> dict[str, Any]:
    try:
        return ensure_dict(name, json.loads(value))
    except json.JSONDecodeError as ex:
        msg = f"Invalid JSON: {value}"
        raise ValueError(msg) from ex


META: dict[str, dict] = {
    "output_dir": dict(
        flags=("-O", "--output-dir"),
        default=".",
        type=Path,
        help="Path to the directory where the files for embedding will be generated "
        "(default: current working directory)",
    ),
    "main_file": dict(
        flags=("-M", "--main-file"),
        default="__init__.py",
        help="Name of the file that will contain the main `validate` function"
        "(default: `%(default)s`)",
    ),
    "replacements": dict(
        flags=("-R", "--replacements"),
        default="{}",
        type=wraps(JSON_dict)(partial(JSON_dict, "replacements")),
        help="JSON string (don't forget to quote) representing a map between strings "
        "that should be replaced in the generated files and their replacement, "
        "for example: \n"
        '-R \'{"from packaging import": "from .._vendor.packaging import"}\'',
    ),
    "tool": dict(
        flags=("-t", "--tool"),
        action="append",
        dest="tool",
        help="External tools file/url(s) to load, of the form name=URL#path",
    ),
    "store": dict(
        flags=("--store",),
        help="Load a pyproject.json file and read all the $ref's into tools "
        "(see https://json.schemastore.org/pyproject.json)",
    ),
}


def ensure_dict(name: str, value: Any) -> dict:
    if not isinstance(value, dict):
        msg = f"`{value.__class__.__name__}` given (value = {value!r})."
        msg = f"`{name}` should be a dict. {msg}"
        # Should be type error according to linter
        raise ValueError(msg)  # noqa: TRY004
    return value


class CliParams(NamedTuple):
    plugins: list[PluginWrapper]
    output_dir: Path = Path(".")
    main_file: str = "__init__.py"
    replacements: Mapping[str, str] = MappingProxyType({})
    loglevel: int = logging.WARNING
    tool: Sequence[str] = ()
    store: str = ""


def parser_spec(
    plugins: Sequence[PluginProtocol],
) -> dict[str, dict]:
    common = ("version", "enable", "disable", "verbose", "very_verbose")
    cli_spec = cli.__meta__(plugins)
    meta = {k: v.copy() for k, v in META.items()}
    meta.update({k: cli_spec[k].copy() for k in common})
    return meta


def run(args: Sequence[str] = ()) -> int:
    args = args if args else sys.argv[1:]
    cmd = f"python -m {__package__} " + arg_join(args)
    plugins = list_plugins_from_entry_points()
    desc = 'Generate files for "pre-compiling" `validate-pyproject`'
    prms = cli.parse_args(args, plugins, desc, parser_spec, CliParams)
    cli.setup_logging(prms.loglevel)

    tool_plugins: list[PluginProtocol] = [RemotePlugin.from_str(t) for t in prms.tool]
    if prms.store:
        tool_plugins.extend(load_store(prms.store))

    pre_compile(
        prms.output_dir,
        prms.main_file,
        cmd,
        prms.plugins,
        prms.replacements,
        extra_plugins=tool_plugins,
    )
    return 0


main = cli.exceptions2exit()(run)


if __name__ == "__main__":
    main()
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/pre_compile/main_file.template0000664000175000017500000000202215140154751032525 0ustar  carstencarstenfrom functools import reduce
from typing import Any, Callable, Dict

from . import formats
from .error_reporting import detailed_errors, ValidationError
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",
    "ValidationError",
    "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 ``ValidationError`` if ``data`` is invalid.
    """
    with detailed_errors():
        _validate(data, custom_formats=FORMAT_FUNCTIONS)
        reduce(lambda acc, fn: fn(acc), EXTRA_VALIDATIONS, data)
    return True
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/pre_compile/cli-notice.template0000664000175000017500000000022115140154751032627 0ustar  carstencarstenThe code contained in this directory was automatically generated using the
following command:

    {command}

Please avoid changing it manually.
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/project_metadata.schema.json0000664000175000017500000003024115140154751032213 0ustar  carstencarsten{
  "$schema": "http://json-schema.org/draft-07/schema#",

  "$id": "https://packaging.python.org/en/latest/specifications/pyproject-toml/",
  "title": "Package metadata stored in the ``project`` table",
  "$$description": [
    "Data structure for the **project** table inside ``pyproject.toml``",
    "(as initially defined in :pep:`621`)"
  ],

  "type": "object",
  "properties": {
    "name": {
       "type": "string",
       "description":
         "The name (primary identifier) of the project. MUST be statically defined.",
       "format": "pep508-identifier"
    },
    "version": {
      "type": "string",
      "description": "The version of the project as supported by :pep:`440`.",
      "format": "pep440"
    },
    "description": {
      "type": "string",
      "$$description": [
        "The `summary description of the project",
        "`_"
      ]
    },
    "readme": {
      "$$description": [
        "`Full/detailed description of the project in the form of a README",
        "`_",
        "with meaning similar to the one defined in `core metadata's Description",
        "`_"
      ],
      "oneOf": [
        {
          "type": "string",
          "$$description": [
            "Relative path to a text file (UTF-8) containing the full description",
            "of the project. If the file path ends in case-insensitive ``.md`` or",
            "``.rst`` suffixes, then the content-type is respectively",
            "``text/markdown`` or ``text/x-rst``"
          ]
        },
        {
          "type": "object",
          "allOf": [
            {
              "anyOf": [
                {
                  "properties": {
                    "file": {
                      "type": "string",
                      "$$description": [
                        "Relative path to a text file containing the full description",
                        "of the project."
                      ]
                    }
                  },
                  "required": ["file"]
                },
                {
                  "properties": {
                    "text": {
                      "type": "string",
                      "description": "Full text describing the project."
                    }
                  },
                  "required": ["text"]
                }
              ]
            },
            {
              "properties": {
                "content-type" : {
                  "type": "string",
                  "$$description": [
                    "Content-type (:rfc:`1341`) of the full description",
                    "(e.g. ``text/markdown``). The ``charset`` parameter is assumed",
                    "UTF-8 when not present."
                  ],
                  "$comment": "TODO: add regex pattern or format?"
                }
              },
              "required": ["content-type"]
            }
          ]
        }
      ]
    },
    "requires-python": {
      "type": "string",
      "format": "pep508-versionspec",
      "$$description": [
        "`The Python version requirements of the project",
        "`_."
      ]
    },
    "license": {
      "description":
        "`Project license `_.",
      "oneOf": [
        {
          "type": "string",
          "description": "An SPDX license identifier",
          "format": "SPDX"
        },
        {
          "type": "object",
          "properties": {
            "file": {
              "type": "string",
              "$$description": [
                "Relative path to the file (UTF-8) which contains the license for the",
                "project."
              ]
            }
          },
          "required": ["file"]
        },
        {
          "type": "object",
          "properties": {
            "text": {
              "type": "string",
              "$$description": [
                "The license of the project whose meaning is that of the",
                "`License field from the core metadata",
                "`_."
              ]
            }
          },
          "required": ["text"]
        }
      ]
    },
    "license-files": {
      "description": "Paths or globs to paths of license files",
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "authors": {
      "type": "array",
      "items": {"$ref": "#/definitions/author"},
      "$$description": [
        "The people or organizations considered to be the 'authors' of the project.",
        "The exact meaning is open to interpretation (e.g. original or primary authors,",
        "current maintainers, or owners of the package)."
      ]
    },
    "maintainers": {
      "type": "array",
      "items": {"$ref": "#/definitions/author"},
      "$$description": [
        "The people or organizations considered to be the 'maintainers' of the project.",
        "Similarly to ``authors``, the exact meaning is open to interpretation."
      ]
    },
    "keywords": {
      "type": "array",
      "items": {"type": "string"},
      "description":
        "List of keywords to assist searching for the distribution in a larger catalog."
    },
    "classifiers": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "trove-classifier",
        "description": "`PyPI classifier `_."
      },
      "$$description": [
        "`Trove classifiers `_",
        "which apply to the project."
      ]
    },
    "urls": {
      "type": "object",
      "description": "URLs associated with the project in the form ``label => value``.",
      "additionalProperties": false,
      "patternProperties": {
        "^.+$": {"type": "string", "format": "url"}
      }
    },
    "scripts": {
      "$ref": "#/definitions/entry-point-group",
      "$$description": [
        "Instruct the installer to create command-line wrappers for the given",
        "`entry points `_."
      ]
    },
    "gui-scripts": {
      "$ref": "#/definitions/entry-point-group",
      "$$description": [
        "Instruct the installer to create GUI wrappers for the given",
        "`entry points `_.",
        "The difference between ``scripts`` and ``gui-scripts`` is only relevant in",
        "Windows."
      ]
    },
    "entry-points": {
      "$$description": [
        "Instruct the installer to expose the given modules/functions via",
        "``entry-point`` discovery mechanism (useful for plugins).",
        "More information available in the `Python packaging guide",
        "`_."
      ],
      "propertyNames": {"format": "python-entrypoint-group"},
      "additionalProperties": false,
      "patternProperties": {
        "^.+$": {"$ref": "#/definitions/entry-point-group"}
      }
    },
    "dependencies": {
      "type": "array",
      "description": "Project (mandatory) dependencies.",
      "items": {"$ref": "#/definitions/dependency"}
    },
    "optional-dependencies": {
      "type": "object",
      "description": "Optional dependency for the project",
      "propertyNames": {"format": "pep508-identifier"},
      "additionalProperties": false,
      "patternProperties": {
        "^.+$": {
          "type": "array",
          "items": {"$ref": "#/definitions/dependency"}
        }
      }
    },
    "import-names": {
      "description": "Lists import names which a project, when installed, would exclusively provide.",
      "type": "array",
      "items": {
        "type": "string",
        "format": "import-name"
      }
    },
    "import-namespaces": {
      "description": "Lists import names that, when installed, would be provided by the project, but not exclusively.",
      "type": "array",
      "items": {
        "type": "string",
        "format": "import-name"
      }
    },
    "dynamic": {
      "type": "array",
      "$$description": [
        "Specifies which fields are intentionally unspecified and expected to be",
        "dynamically provided by build tools"
      ],
      "items": {
        "enum": [
          "version",
          "description",
          "readme",
          "requires-python",
          "license",
          "license-files",
          "authors",
          "maintainers",
          "keywords",
          "classifiers",
          "urls",
          "scripts",
          "gui-scripts",
          "entry-points",
          "dependencies",
          "optional-dependencies",
          "import-names",
          "import-namespaces"
        ]
      }
    }
  },
  "required": ["name"],
  "additionalProperties": false,
  "allOf": [
    {
      "if": {
        "not": {
          "required": ["dynamic"],
          "properties": {
            "dynamic": {
              "contains": {"const": "version"},
              "$$description": ["version is listed in ``dynamic``"]
            }
          }
        },
        "$$comment": [
          "According to :pep:`621`:",
          "    If the core metadata specification lists a field as \"Required\", then",
          "    the metadata MUST specify the field statically or list it in dynamic",
          "In turn, `core metadata`_ defines:",
          "    The required fields are: Metadata-Version, Name, Version.",
          "    All the other fields are optional.",
          "Since ``Metadata-Version`` is defined by the build back-end, ``name`` and",
          "``version`` are the only mandatory information in ``pyproject.toml``.",
          ".. _core metadata: https://packaging.python.org/specifications/core-metadata/"
        ]
      },
      "then": {
        "required": ["version"],
        "$$description": ["version should be statically defined in the ``version`` field"]
      }
    },
    {
      "if": {
        "required": ["license-files"]
      },
      "then": {
        "properties": {
          "license": {
            "type": "string"
          }
        }
      }
    }
  ],

  "definitions": {
    "author": {
      "$id": "#/definitions/author",
      "title": "Author or Maintainer",
      "$comment": "https://peps.python.org/pep-0621/#authors-maintainers",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string",
          "$$description": [
            "MUST be a valid email name, i.e. whatever can be put as a name, before an",
            "email, in :rfc:`822`."
          ]
        },
        "email": {
          "type": "string",
          "format": "idn-email",
          "description": "MUST be a valid email address"
        }
      },
      "anyOf": [
        { "required": ["name"] },
        { "required": ["email"] }
      ]
    },
    "entry-point-group": {
      "$id": "#/definitions/entry-point-group",
      "title": "Entry-points",
      "type": "object",
      "$$description": [
        "Entry-points are grouped together to indicate what sort of capabilities they",
        "provide.",
        "See the `packaging guides",
        "`_",
        "and `setuptools docs",
        "`_",
        "for more information."
      ],
      "propertyNames": {"format": "python-entrypoint-name"},
      "additionalProperties": false,
      "patternProperties": {
        "^.+$": {
          "type": "string",
          "$$description": [
            "Reference to a Python object. It is either in the form",
            "``importable.module``, or ``importable.module:object.attr``."
          ],
          "format": "python-entrypoint-reference",
          "$comment": "https://packaging.python.org/specifications/entry-points/"
        }
      }
    },
    "dependency": {
      "$id": "#/definitions/dependency",
      "title": "Dependency",
      "type": "string",
      "description": "Project dependency specification according to PEP 508",
      "format": "pep508"
    }
  }
}
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/api.py0000664000175000017500000002324415140154751025703 0ustar  carstencarsten"""
Retrieve JSON schemas for validating dicts representing a ``pyproject.toml`` file.
"""

from __future__ import annotations

import json
import logging
import sys
import typing
from enum import Enum
from functools import partial, reduce
from types import MappingProxyType, ModuleType
from typing import (
    Callable,
    Iterator,
    Mapping,
    Sequence,
    TypeVar,
)

import fastjsonschema as FJS

from . import errors, formats
from .error_reporting import detailed_errors
from .extra_validations import EXTRA_VALIDATIONS
from .types import FormatValidationFn, Schema, ValidationFn

_logger = logging.getLogger(__name__)

if typing.TYPE_CHECKING:  # pragma: no cover
    from .plugins import PluginProtocol


if sys.version_info >= (3, 9):  # pragma: no cover
    from importlib.resources import files

    def read_text(package: str | ModuleType, resource: str) -> str:
        """:meta private:"""
        return files(package).joinpath(resource).read_text(encoding="utf-8")

else:  # pragma: no cover
    from importlib.resources import read_text as read_text  # noqa: PLC0414


__all__ = ["Validator"]


T = TypeVar("T", bound=Mapping)
AllPlugins = Enum("AllPlugins", "ALL_PLUGINS")  #: :meta private:
ALL_PLUGINS = AllPlugins.ALL_PLUGINS

TOP_LEVEL_SCHEMA = "pyproject_toml"
PROJECT_TABLE_SCHEMA = "project_metadata"


def _get_public_functions(module: ModuleType) -> Mapping[str, FormatValidationFn]:
    return {
        fn.__name__.replace("_", "-"): fn
        for fn in module.__dict__.values()
        if callable(fn) and not fn.__name__.startswith("_")
    }


FORMAT_FUNCTIONS = MappingProxyType(_get_public_functions(formats))


def load(name: str, package: str = __package__, ext: str = ".schema.json") -> Schema:
    """Load the schema from a JSON Schema file.
    The returned dict-like object is immutable.

    :meta private: (low level detail)
    """
    return Schema(json.loads(read_text(package, f"{name}{ext}")))


def load_builtin_plugin(name: str) -> Schema:
    """:meta private: (low level detail)"""
    return load(name, f"{__package__}.plugins")


class SchemaRegistry(Mapping[str, Schema]):
    """Repository of parsed JSON Schemas used for validating a ``pyproject.toml``.

    During instantiation the schemas equivalent to PEP 517, PEP 518 and PEP 621
    will be combined with the schemas for the ``tool`` subtables provided by the
    plugins.

    Since this object work as a mapping between each schema ``$id`` and the schema
    itself, all schemas provided by plugins **MUST** have a top level ``$id``.

    :meta private: (low level detail)
    """

    def __init__(self, plugins: Sequence[PluginProtocol] = ()):
        self._schemas: dict[str, tuple[str, str, Schema]] = {}
        # (which part of the TOML, who defines, schema)

        top_level = typing.cast("dict", load(TOP_LEVEL_SCHEMA))  # Make it mutable
        self._spec_version: str = top_level["$schema"]
        top_properties = top_level["properties"]
        tool_properties = top_properties["tool"].setdefault("properties", {})

        # Add PEP 621
        project_table_schema = load(PROJECT_TABLE_SCHEMA)
        self._ensure_compatibility(PROJECT_TABLE_SCHEMA, project_table_schema)
        sid = project_table_schema["$id"]
        top_level["project"] = {"$ref": sid}
        origin = f"{__name__} - project metadata"
        self._schemas = {sid: ("project", origin, project_table_schema)}

        # Add tools using Plugins
        for plugin in plugins:
            if plugin.tool:
                allow_overwrite: str | None = None
                if plugin.tool in tool_properties:
                    _logger.warning(f"{plugin} overwrites `tool.{plugin.tool}` schema")
                    allow_overwrite = plugin.schema.get("$id")
                else:
                    _logger.info(f"{plugin} defines `tool.{plugin.tool}` schema")
                compatible = self._ensure_compatibility(
                    plugin.tool, plugin.schema, allow_overwrite
                )
                sid = compatible["$id"]
                sref = f"{sid}#{plugin.fragment}" if plugin.fragment else sid
                tool_properties[plugin.tool] = {"$ref": sref}
                self._schemas[sid] = (f"tool.{plugin.tool}", plugin.id, plugin.schema)
            else:
                _logger.info(f"{plugin} defines extra schema {plugin.id}")
                self._schemas[plugin.id] = (plugin.id, plugin.id, plugin.schema)

        self._main_id: str = top_level["$id"]
        main_schema = Schema(top_level)
        origin = f"{__name__} - build metadata"
        self._schemas[self._main_id] = ("<$ROOT>", origin, main_schema)

    @property
    def spec_version(self) -> str:
        """Version of the JSON Schema spec in use"""
        return self._spec_version

    @property
    def main(self) -> str:
        """Top level schema for validating a ``pyproject.toml`` file"""
        return self._main_id

    def _ensure_compatibility(
        self,
        reference: str,
        schema: Schema,
        allow_overwrite: str | None = None,
    ) -> Schema:
        if "$id" not in schema or not schema["$id"]:
            raise errors.SchemaMissingId(reference or "")
        sid = schema["$id"]
        if sid in self._schemas and sid != allow_overwrite:
            raise errors.SchemaWithDuplicatedId(sid)
        version = schema.get("$schema")
        # Support schemas with missing trailing # (incorrect, but required before 0.15)
        if version and version.rstrip("#") != self.spec_version.rstrip("#"):
            raise errors.InvalidSchemaVersion(
                reference or sid, version, self.spec_version
            )
        return schema

    def __getitem__(self, key: str) -> Schema:
        return self._schemas[key][-1]

    def __iter__(self) -> Iterator[str]:
        return iter(self._schemas)

    def __len__(self) -> int:
        return len(self._schemas)


class RefHandler(Mapping[str, Callable[[str], Schema]]):
    """:mod:`fastjsonschema` allows passing a dict-like object to load external schema
    ``$ref``s. Such objects map the URI schema (e.g. ``http``, ``https``, ``ftp``)
    into a function that receives the schema URI and returns the schema (as parsed JSON)
    (otherwise :mod:`urllib` is used and the URI is assumed to be a valid URL).
    This class will ensure all the URIs are loaded from the local registry.

    :meta private: (low level detail)
    """

    def __init__(self, registry: Mapping[str, Schema]):
        self._uri_schemas = ["http", "https"]
        self._registry = registry

    def __contains__(self, key: object) -> bool:
        if isinstance(key, str):
            if key not in self._uri_schemas:
                self._uri_schemas.append(key)
            return True
        return False

    def __iter__(self) -> Iterator[str]:
        return iter(self._uri_schemas)

    def __len__(self) -> int:
        return len(self._uri_schemas)

    def __getitem__(self, key: str) -> Callable[[str], Schema]:
        """All the references should be retrieved from the registry"""
        return self._registry.__getitem__


class Validator:
    _plugins: Sequence[PluginProtocol]

    def __init__(
        self,
        plugins: Sequence[PluginProtocol] | AllPlugins = ALL_PLUGINS,
        format_validators: Mapping[str, FormatValidationFn] = FORMAT_FUNCTIONS,
        extra_validations: Sequence[ValidationFn] = EXTRA_VALIDATIONS,
        *,
        extra_plugins: Sequence[PluginProtocol] = (),
    ):
        self._code_cache: str | None = None
        self._cache: ValidationFn | None = None
        self._schema: Schema | None = None

        # Let's make the following options readonly
        self._format_validators = MappingProxyType(format_validators)
        self._extra_validations = tuple(extra_validations)

        if plugins is ALL_PLUGINS:
            from .plugins import list_from_entry_points

            plugins = list_from_entry_points()

        self._plugins = (*plugins, *extra_plugins)

        self._schema_registry = SchemaRegistry(self._plugins)
        self.handlers = RefHandler(self._schema_registry)

    @property
    def registry(self) -> SchemaRegistry:
        return self._schema_registry

    @property
    def schema(self) -> Schema:
        """Top level ``pyproject.toml`` JSON Schema"""
        return Schema({"$ref": self._schema_registry.main})

    @property
    def extra_validations(self) -> Sequence[ValidationFn]:
        """List of extra validation functions that run after the JSON Schema check"""
        return self._extra_validations

    @property
    def formats(self) -> Mapping[str, FormatValidationFn]:
        """Mapping between JSON Schema formats and functions that validates them"""
        return self._format_validators

    @property
    def generated_code(self) -> str:
        if self._code_cache is None:
            fmts = dict(self.formats)
            self._code_cache = FJS.compile_to_code(
                self.schema, self.handlers, fmts, use_default=False
            )

        return self._code_cache

    def __getitem__(self, schema_id: str) -> Schema:
        """Retrieve a schema from registry"""
        return self._schema_registry[schema_id]

    def __call__(self, pyproject: T) -> T:
        """Checks a parsed ``pyproject.toml`` file (given as :obj:`typing.Mapping`)
        and raises an exception when it is not a valid.
        """
        if self._cache is None:
            compiled = FJS.compile(
                self.schema, self.handlers, dict(self.formats), use_default=False
            )
            fn = partial(compiled, custom_formats=self._format_validators)
            self._cache = typing.cast("ValidationFn", fn)

        with detailed_errors():
            self._cache(pyproject)
            return reduce(lambda acc, fn: fn(acc), self.extra_validations, pyproject)
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/pyproject_toml.schema.json0000664000175000017500000000602115140154751031756 0ustar  carstencarsten{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/",
  "title": "Data structure for ``pyproject.toml`` files",
  "$$description": [
    "File format containing build-time configurations for the Python ecosystem. ",
    ":pep:`517` initially defined a build-system independent format for source trees",
    "which was complemented by :pep:`518` to provide a way of specifying dependencies ",
    "for building Python projects.",
    "Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included",
    "in this schema and should be considered separately."
  ],

  "type": "object",
  "additionalProperties": false,

  "properties": {
    "build-system": {
      "type": "object",
      "description": "Table used to store build-related data",
      "additionalProperties": false,

      "properties": {
        "requires": {
          "type": "array",
          "$$description": [
            "List of dependencies in the :pep:`508` format required to execute the build",
            "system. Please notice that the resulting dependency graph",
            "**MUST NOT contain cycles**"
          ],
          "items": {
            "type": "string"
          }
        },
        "build-backend": {
          "type": "string",
          "description":
            "Python object that will be used to perform the build according to :pep:`517`",
          "format": "pep517-backend-reference"
        },
        "backend-path": {
          "type": "array",
          "$$description": [
            "List of directories to be prepended to ``sys.path`` when loading the",
            "back-end, and running its hooks"
          ],
          "items": {
            "type": "string",
            "$comment": "Should be a path (TODO: enforce it with format?)"
          }
        }
      },
      "required": ["requires"]
    },

    "project": {
      "$ref": "https://packaging.python.org/en/latest/specifications/pyproject-toml/"
    },

    "tool": {
      "type": "object"
    },
    "dependency-groups": {
      "type": "object",
      "description": "Dependency groups following PEP 735",
      "additionalProperties": false,
      "patternProperties": {
        "^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$": {
          "type": "array",
          "items": {
            "oneOf": [
              {
                "type": "string",
                "description": "Python package specifiers following PEP 508",
                "format": "pep508"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "include-group": {
                    "description": "Another dependency group to include in this one",
                    "type": "string",
                    "pattern": "^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$"
                  }
                }
              }
            ]
          }
        }
      }
    }
  }
}
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/plugins/0000775000175000017500000000000015140154751026234 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/plugins/__init__.py0000664000175000017500000001722515140154751030354 0ustar  carstencarsten# The code in this module is mostly borrowed/adapted from PyScaffold and was originally
# published under the MIT license
# The original PyScaffold license can be found in 'NOTICE.txt'
"""
.. _entry point: https://setuptools.readthedocs.io/en/latest/userguide/entry_point.html
"""

from __future__ import annotations

import typing
from importlib.metadata import EntryPoint, entry_points
from itertools import chain
from string import Template
from textwrap import dedent
from typing import (
    Any,
    Callable,
    Generator,
    Iterable,
    NamedTuple,
    Protocol,
)

from .. import __version__

if typing.TYPE_CHECKING:
    from ..types import Plugin, Schema

_DEFAULT_MULTI_PRIORITY = 0
_DEFAULT_TOOL_PRIORITY = 1


class PluginProtocol(Protocol):
    @property
    def id(self) -> str: ...

    @property
    def tool(self) -> str: ...

    @property
    def schema(self) -> Schema: ...

    @property
    def help_text(self) -> str: ...

    @property
    def fragment(self) -> str: ...


class PluginWrapper:
    def __init__(self, tool: str, load_fn: Plugin):
        self._tool = tool
        self._load_fn = load_fn

    @property
    def id(self) -> str:
        return f"{self._load_fn.__module__}.{self._load_fn.__name__}"

    @property
    def tool(self) -> str:
        return self._tool

    @property
    def schema(self) -> Schema:
        return self._load_fn(self.tool)

    @property
    def fragment(self) -> str:
        return ""

    @property
    def priority(self) -> float:
        return getattr(self._load_fn, "priority", _DEFAULT_TOOL_PRIORITY)

    @property
    def help_text(self) -> str:
        tpl = self._load_fn.__doc__
        if not tpl:
            return ""
        return Template(tpl).safe_substitute(tool=self.tool, id=self.id)

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}({self.tool!r}, {self.id})"

    def __str__(self) -> str:
        return self.id


class StoredPlugin:
    def __init__(self, tool: str, schema: Schema, source: str, priority: float):
        self._tool, _, self._fragment = tool.partition("#")
        self._schema = schema
        self._source = source
        self._priority = priority

    @property
    def id(self) -> str:
        return self._schema["$id"]  # type: ignore[no-any-return]

    @property
    def tool(self) -> str:
        return self._tool

    @property
    def schema(self) -> Schema:
        return self._schema

    @property
    def fragment(self) -> str:
        return self._fragment

    @property
    def priority(self) -> float:
        return self._priority

    @property
    def help_text(self) -> str:
        return self.schema.get("description", "")

    def __str__(self) -> str:
        return self._source

    def __repr__(self) -> str:
        args = [repr(self.tool), self.id]
        if self.fragment:
            args.append(f"fragment={self.fragment!r}")
        return f"{self.__class__.__name__}({', '.join(args)}, )"


if typing.TYPE_CHECKING:
    _: PluginProtocol = typing.cast("PluginWrapper", None)


def iterate_entry_points(group: str) -> Iterable[EntryPoint]:
    """Produces an iterable yielding an EntryPoint object for each plugin registered
    via ``setuptools`` `entry point`_ mechanism.

    This method can be used in conjunction with :obj:`load_from_entry_point` to filter
    the plugins before actually loading them. The entry points are not
    deduplicated.
    """
    entries = entry_points()
    if hasattr(entries, "select"):  # pragma: no cover
        # The select method was introduced in importlib_metadata 3.9 (and Python 3.10)
        # and the previous dict interface was declared deprecated
        select = typing.cast(
            "Callable[..., Iterable[EntryPoint]]",
            getattr(entries, "select"),  # noqa: B009
        )  # typecheck gymnastics
        return select(group=group)
    # pragma: no cover
    # TODO: Once Python 3.10 becomes the oldest version supported, this fallback and
    #       conditional statement can be removed.
    return (plugin for plugin in entries.get(group, []))


def load_from_entry_point(entry_point: EntryPoint) -> PluginWrapper:
    """Carefully load the plugin, raising a meaningful message in case of errors"""
    try:
        fn = entry_point.load()
        return PluginWrapper(entry_point.name, fn)
    except Exception as ex:
        raise ErrorLoadingPlugin(entry_point=entry_point) from ex


def load_from_multi_entry_point(
    entry_point: EntryPoint,
) -> Generator[StoredPlugin, None, None]:
    """Carefully load the plugin, raising a meaningful message in case of errors"""
    try:
        fn = entry_point.load()
        output = fn()
        id_ = f"{fn.__module__}.{fn.__name__}"
    except Exception as ex:
        raise ErrorLoadingPlugin(entry_point=entry_point) from ex

    priority = output.get("priority", _DEFAULT_MULTI_PRIORITY)
    for tool, schema in output["tools"].items():
        yield StoredPlugin(tool, schema, f"{id_}:{tool}", priority)
    for i, schema in enumerate(output.get("schemas", [])):
        yield StoredPlugin("", schema, f"{id_}:{i}", priority)


class _SortablePlugin(NamedTuple):
    name: str
    plugin: PluginWrapper | StoredPlugin

    def key(self) -> str:
        return self.plugin.tool or self.plugin.id

    def __lt__(self, other: Any) -> bool:
        # **Major concern**:
        # Consistency and reproducibility on which entry-points have priority
        # for a given environment.
        # The plugin with higher priority overwrites the schema definition.
        # The exact order that they are listed itself is not important for now.
        # **Implementation detail**:
        # By default, "single tool plugins" have priority 1 and "multi plugins"
        # have priority 0.
        # The order that the plugins will be listed is inverse to the priority.
        # If 2 plugins have the same numerical priority, the one whose
        # entry-point name is "higher alphabetically" wins.
        return (self.plugin.priority, self.name, self.key()) < (
            other.plugin.priority,
            other.name,
            other.key(),
        )


def list_from_entry_points(
    filtering: Callable[[EntryPoint], bool] = lambda _: True,
) -> list[PluginWrapper | StoredPlugin]:
    """Produces a list of plugin objects for each plugin registered
    via ``setuptools`` `entry point`_ mechanism.

    Args:
        filtering: function returning a boolean deciding if the entry point should be
            loaded and included (or not) in the final list. A ``True`` return means the
            plugin should be included.
    """
    tool_eps = (
        _SortablePlugin(e.name, load_from_entry_point(e))
        for e in iterate_entry_points("validate_pyproject.tool_schema")
        if filtering(e)
    )
    multi_eps = (
        _SortablePlugin(e.name, p)
        for e in iterate_entry_points("validate_pyproject.multi_schema")
        for p in load_from_multi_entry_point(e)
        if filtering(e)
    )
    eps = chain(tool_eps, multi_eps)
    dedup = {e.key(): e.plugin for e in sorted(eps)}
    return list(dedup.values())


class ErrorLoadingPlugin(RuntimeError):
    _DESC = """There was an error loading '{plugin}'.
    Please make sure you have installed a version of the plugin that is compatible
    with {package} {version}. You can also try uninstalling it.
    """
    __doc__ = _DESC

    def __init__(self, plugin: str = "", entry_point: EntryPoint | None = None):
        if entry_point and not plugin:
            plugin = getattr(entry_point, "module", entry_point.name)

        sub = {"package": __package__, "version": __version__, "plugin": plugin}
        msg = dedent(self._DESC).format(**sub).splitlines()
        super().__init__(f"{msg[0]}\n{' '.join(msg[1:])}")
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/plugins/distutils.schema.json0000664000175000017500000000171415140154751032415 0ustar  carstencarsten{
  "$schema": "http://json-schema.org/draft-07/schema#",

  "$id": "https://setuptools.pypa.io/en/latest/deprecated/distutils/configfile.html",
  "title": "``tool.distutils`` table",
  "$$description": [
    "**EXPERIMENTAL** (NOT OFFICIALLY SUPPORTED): Use ``tool.distutils``",
    "subtables to configure arguments for ``distutils`` commands.",
    "Originally, ``distutils`` allowed developers to configure arguments for",
    "``setup.py`` commands via `distutils configuration files",
    "`_.",
    "See also `the old Python docs _`."
  ],

  "type": "object",
  "properties": {
    "global": {
      "type": "object",
      "description": "Global options applied to all ``distutils`` commands"
    }
  },
  "patternProperties": {
    ".+": {"type": "object"}
  },
  "$comment": "TODO: Is there a practical way of making this schema more specific?"
}
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/plugins/setuptools.schema.json0000664000175000017500000003725715140154751032625 0ustar  carstencarsten{
  "$schema": "http://json-schema.org/draft-07/schema#",

  "$id": "https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html",
  "title": "``tool.setuptools`` table",
  "$$description": [
    "``setuptools``-specific configurations that can be set by users that require",
    "customization.",
    "These configurations are completely optional and probably can be skipped when",
    "creating simple packages. They are equivalent to some of the `Keywords",
    "`_",
    "used by the ``setup.py`` file, and can be set via the ``tool.setuptools`` table.",
    "It considers only ``setuptools`` `parameters",
    "`_",
    "that are not covered by :pep:`621`; and intentionally excludes ``dependency_links``",
    "and ``setup_requires`` (incompatible with modern workflows/standards)."
  ],

  "type": "object",
  "additionalProperties": false,
  "properties": {
    "platforms": {
      "type": "array",
      "items": {"type": "string"}
    },
    "provides": {
      "$$description": [
        "Package and virtual package names contained within this package",
        "**(not supported by pip)**"
      ],
      "type": "array",
      "items": {"type": "string", "format": "pep508-identifier"}
    },
    "obsoletes": {
      "$$description": [
        "Packages which this package renders obsolete",
        "**(not supported by pip)**"
      ],
      "type": "array",
      "items": {"type": "string", "format": "pep508-identifier"}
    },
    "zip-safe": {
      "$$description": [
        "Whether the project can be safely installed and run from a zip file.",
        "**OBSOLETE**: only relevant for ``pkg_resources``, ``easy_install`` and",
        "``setup.py install`` in the context of ``eggs`` (**DEPRECATED**)."
      ],
      "type": "boolean"
    },
    "script-files": {
      "$$description": [
        "Legacy way of defining scripts (entry-points are preferred).",
        "Equivalent to the ``script`` keyword in ``setup.py``",
        "(it was renamed to avoid confusion with entry-point based ``project.scripts``",
        "defined in :pep:`621`).",
        "**DISCOURAGED**: generic script wrappers are tricky and may not work properly.",
        "Whenever possible, please use ``project.scripts`` instead."
      ],
      "type": "array",
      "items": {"type": "string"},
      "$comment": "TODO: is this field deprecated/should be removed?"
    },
    "eager-resources": {
      "$$description": [
        "Resources that should be extracted together, if any of them is needed,",
        "or if any C extensions included in the project are imported.",
        "**OBSOLETE**: only relevant for ``pkg_resources``, ``easy_install`` and",
        "``setup.py install`` in the context of ``eggs`` (**DEPRECATED**)."
      ],
      "type": "array",
      "items": {"type": "string"}
    },
    "packages": {
      "$$description": [
        "Packages that should be included in the distribution.",
        "It can be given either as a list of package identifiers",
        "or as a ``dict``-like structure with a single key ``find``",
        "which corresponds to a dynamic call to",
        "``setuptools.config.expand.find_packages`` function.",
        "The ``find`` key is associated with a nested ``dict``-like structure that can",
        "contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,",
        "mimicking the keyword arguments of the associated function."
      ],
      "oneOf": [
        {
          "title": "Array of Python package identifiers",
          "type": "array",
          "items": {"$ref": "#/definitions/package-name"}
        },
        {"$ref": "#/definitions/find-directive"}
      ]
    },
    "package-dir": {
      "$$description": [
        ":class:`dict`-like structure mapping from package names to directories where their",
        "code can be found.",
        "The empty string (as key) means that all packages are contained inside",
        "the given directory will be included in the distribution."
      ],
      "type": "object",
      "additionalProperties": false,
      "propertyNames": {
        "anyOf": [{"const": ""}, {"$ref": "#/definitions/package-name"}]
      },
      "patternProperties": {
        "^.*$": {"type": "string" }
      }
    },
    "package-data": {
      "$$description": [
        "Mapping from package names to lists of glob patterns.",
        "Usually this option is not needed when using ``include-package-data = true``",
        "For more information on how to include data files, check ``setuptools`` `docs",
        "`_."
      ],
      "type": "object",
      "additionalProperties": false,
      "propertyNames": {
        "anyOf": [{"$ref": "#/definitions/package-name"}, {"const": "*"}]
      },
      "patternProperties": {
        "^.*$": {"type": "array", "items": {"type": "string"}}
      }
    },
    "include-package-data": {
      "$$description": [
        "Automatically include any data files inside the package directories",
        "that are specified by ``MANIFEST.in``",
        "For more information on how to include data files, check ``setuptools`` `docs",
        "`_."
      ],
      "type": "boolean"
    },
    "exclude-package-data": {
      "$$description": [
        "Mapping from package names to lists of glob patterns that should be excluded",
        "For more information on how to include data files, check ``setuptools`` `docs",
        "`_."
      ],
      "type": "object",
      "additionalProperties": false,
      "propertyNames": {
        "anyOf": [{"$ref": "#/definitions/package-name"}, {"const": "*"}]
      },
      "patternProperties": {
          "^.*$": {"type": "array", "items": {"type": "string"}}
      }
    },
    "namespace-packages": {
      "type": "array",
      "items": {"type": "string", "format": "python-module-name-relaxed"},
      "$comment": "https://setuptools.pypa.io/en/latest/userguide/package_discovery.html",
      "description": "**DEPRECATED**: use implicit namespaces instead (:pep:`420`)."
    },
    "py-modules": {
      "description": "Modules that setuptools will manipulate",
      "type": "array",
      "items": {"type": "string", "format": "python-module-name-relaxed"},
      "$comment": "TODO: clarify the relationship with ``packages``"
    },
    "ext-modules": {
      "description": "Extension modules to be compiled by setuptools",
      "type": "array",
      "items": {"$ref": "#/definitions/ext-module"}
    },
    "data-files": {
      "$$description": [
        "``dict``-like structure where each key represents a directory and",
        "the value is a list of glob patterns that should be installed in them.",
        "**DISCOURAGED**: please notice this might not work as expected with wheels.",
        "Whenever possible, consider using data files inside the package directories",
        "(or create a new namespace package that only contains data files).",
        "See `data files support",
        "`_."
      ],
      "type": "object",
      "patternProperties": {
          "^.*$": {"type": "array", "items": {"type": "string"}}
      }
    },
    "cmdclass": {
      "$$description": [
        "Mapping of distutils-style command names to ``setuptools.Command`` subclasses",
        "which in turn should be represented by strings with a qualified class name",
        "(i.e., \"dotted\" form with module), e.g.::\n\n",
        "    cmdclass = {mycmd = \"pkg.subpkg.module.CommandClass\"}\n\n",
        "The command class should be a directly defined at the top-level of the",
        "containing module (no class nesting)."
      ],
      "type": "object",
      "patternProperties": {
          "^.*$": {"type": "string", "format": "python-qualified-identifier"}
      }
    },
    "license-files": {
      "type": "array",
      "items": {"type": "string"},
      "$$description": [
        "**PROVISIONAL**: list of glob patterns for all license files being distributed.",
        "(likely to become standard with :pep:`639`).",
        "By default: ``['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']``"
      ],
      "$comment": "TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?"
    },
    "dynamic": {
      "type": "object",
      "description": "Instructions for loading :pep:`621`-related metadata dynamically",
      "additionalProperties": false,
      "properties": {
        "version": {
          "$$description": [
            "A version dynamically loaded via either the ``attr:`` or ``file:``",
            "directives. Please make sure the given file or attribute respects :pep:`440`.",
            "Also ensure to set ``project.dynamic`` accordingly."
          ],
          "oneOf": [
            {"$ref": "#/definitions/attr-directive"},
            {"$ref": "#/definitions/file-directive"}
          ]
        },
        "classifiers": {"$ref": "#/definitions/file-directive"},
        "description": {"$ref": "#/definitions/file-directive"},
        "entry-points": {"$ref": "#/definitions/file-directive"},
        "dependencies": {"$ref": "#/definitions/file-directive-for-dependencies"},
        "optional-dependencies": {
          "type": "object",
          "propertyNames": {"type": "string", "format": "pep508-identifier"},
          "additionalProperties": false,
          "patternProperties": {
            ".+": {"$ref": "#/definitions/file-directive-for-dependencies"}
          }
        },
        "readme": {
          "type": "object",
          "anyOf": [
            {"$ref": "#/definitions/file-directive"},
            {
              "type": "object",
              "properties": {
                "content-type": {"type": "string"},
                "file": { "$ref": "#/definitions/file-directive/properties/file" }
              },
              "additionalProperties": false}
          ],
          "required": ["file"]
        }
      }
    }
  },

  "definitions": {
    "package-name": {
      "$id": "#/definitions/package-name",
      "title": "Valid package name",
      "description": "Valid package name (importable or :pep:`561`).",
      "type": "string",
      "anyOf": [
        {"type": "string", "format": "python-module-name-relaxed"},
        {"type": "string", "format": "pep561-stub-name"}
      ]
    },
    "ext-module": {
      "$id": "#/definitions/ext-module",
      "title": "Extension module",
      "description": "Parameters to construct a :class:`setuptools.Extension` object",
      "type": "object",
      "required": ["name", "sources"],
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string",
          "format": "python-module-name-relaxed"
        },
        "sources": {
          "type": "array",
          "items": {"type": "string"}
        },
        "include-dirs":{
          "type": "array",
          "items": {"type": "string"}
        },
        "define-macros": {
          "type": "array",
          "items": {
            "type": "array",
            "items": [
              {"description": "macro name", "type": "string"},
              {"description": "macro value", "oneOf": [{"type": "string"}, {"type": "null"}]}
            ],
            "additionalItems": false
          }
        },
        "undef-macros": {
          "type": "array",
          "items": {"type": "string"}
        },
        "library-dirs": {
          "type": "array",
          "items": {"type": "string"}
        },
        "libraries": {
          "type": "array",
          "items": {"type": "string"}
        },
        "runtime-library-dirs": {
          "type": "array",
          "items": {"type": "string"}
        },
        "extra-objects": {
          "type": "array",
          "items": {"type": "string"}
        },
        "extra-compile-args": {
          "type": "array",
          "items": {"type": "string"}
        },
        "extra-link-args": {
          "type": "array",
          "items": {"type": "string"}
        },
        "export-symbols": {
          "type": "array",
          "items": {"type": "string"}
        },
        "swig-opts": {
          "type": "array",
          "items": {"type": "string"}
        },
        "depends": {
          "type": "array",
          "items": {"type": "string"}
        },
        "language": {"type": "string"},
        "optional": {"type": "boolean"},
        "py-limited-api": {"type": "boolean"}
      }
    },
    "file-directive": {
      "$id": "#/definitions/file-directive",
      "title": "'file:' directive",
      "description":
        "Value is read from a file (or list of files and then concatenated)",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "file": {
          "oneOf": [
            {"type": "string"},
            {"type": "array", "items": {"type": "string"}}
          ]
        }
      },
      "required": ["file"]
    },
    "file-directive-for-dependencies": {
      "title": "'file:' directive for dependencies",
      "allOf": [
        {
          "$$description": [
            "**BETA**: subset of the ``requirements.txt`` format",
            "without ``pip`` flags and options",
            "(one :pep:`508`-compliant string per line,",
            "lines that are blank or start with ``#`` are excluded).",
            "See `dynamic metadata",
            "`_."
          ]
        },
        {"$ref": "#/definitions/file-directive"}
      ]
    },
    "attr-directive": {
      "title": "'attr:' directive",
      "$id": "#/definitions/attr-directive",
      "$$description": [
        "Value is read from a module attribute. Supports callables and iterables;",
        "unsupported types are cast via ``str()``"
      ],
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "attr": {"type": "string", "format": "python-qualified-identifier"}
      },
      "required": ["attr"]
    },
    "find-directive": {
      "$id": "#/definitions/find-directive",
      "title": "'find:' directive",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "find": {
          "type": "object",
          "$$description": [
            "Dynamic `package discovery",
            "`_."
          ],
          "additionalProperties": false,
          "properties": {
            "where": {
              "description":
                "Directories to be searched for packages (Unix-style relative path)",
              "type": "array",
              "items": {"type": "string"}
            },
            "exclude": {
              "type": "array",
              "$$description": [
                "Exclude packages that match the values listed in this field.",
                "Can container shell-style wildcards (e.g. ``'pkg.*'``)"
              ],
              "items": {"type": "string"}
            },
            "include": {
              "type": "array",
              "$$description": [
                "Restrict the found packages to just the ones listed in this field.",
                "Can container shell-style wildcards (e.g. ``'pkg.*'``)"
              ],
              "items": {"type": "string"}
            },
            "namespaces": {
              "type": "boolean",
              "$$description": [
                "When ``True``, directories without a ``__init__.py`` file will also",
                "be scanned for :pep:`420`-style implicit namespaces"
              ]
            }
          }
        }
      }
    }
  }
}
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/error_reporting.py0000664000175000017500000002701715140154751030356 0ustar  carstencarstenfrom __future__ import annotations

import io
import json
import logging
import os
import re
import typing
from contextlib import contextmanager
from textwrap import indent, wrap
from typing import Any, Generator, Iterator, Sequence

from fastjsonschema 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 = {
    "must be named by propertyName definition": "keys must be named by",
    "one of contains definition": "at least one item that matches",
    " same as const definition:": "",
    "only specified items": "only items matching the definition",
}

_SKIP_DETAILS = (
    "must not be empty",
    "is always invalid",
    "must not be there",
)

_NEED_DETAILS = {"anyOf", "oneOf", "allOf", "contains", "propertyNames", "not", "items"}

_CAMEL_CASE_SPLITTER = re.compile(r"\W+|([A-Z][^A-Z\W]*)")
_IDENTIFIER = re.compile(r"^[\w_]+$", re.IGNORECASE)

_TOML_JARGON = {
    "object": "table",
    "property": "key",
    "properties": "keys",
    "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.

    This class extends :exc:`~fastjsonschema.JsonSchemaValueException`
    by adding the following properties:

    - ``summary``: an improved version of the ``JsonSchemaValueException`` error message
      with only the necessary information)

    - ``details``: more contextual information about the error like the failing schema
      itself and the value that violates the schema.

    Depending on the level of the verbosity of the ``logging`` configuration
    the exception message will be only ``summary`` (default) or a combination of
    ``summary`` and ``details`` (when the logging level is set to :obj:`logging.DEBUG`).
    """

    summary = ""
    details = ""
    _original_message = ""

    @classmethod
    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()
        if debug_code != "false":  # pragma: no cover
            obj.__cause__, obj.__traceback__ = ex.__cause__, ex.__traceback__
        obj._original_message = ex.message
        obj.summary = formatter.summary
        obj.details = formatter.details
        return obj


@contextmanager
def detailed_errors() -> Generator[None, None, None]:
    try:
        yield
    except JsonSchemaValueException as ex:
        raise ValidationError._from_jsonschema(ex) from None


class _ErrorFormatting:
    def __init__(self, ex: JsonSchemaValueException):
        self.ex = ex
        self.name = f"`{self._simplify_name(ex.name)}`"
        self._original_message: str = self.ex.message.replace(ex.name, self.name)
        self._summary = ""
        self._details = ""

    def __str__(self) -> str:
        if _logger.getEffectiveLevel() <= logging.DEBUG and self.details:
            return f"{self.summary}\n\n{self.details}"

        return self.summary

    @property
    def summary(self) -> str:
        if not self._summary:
            self._summary = self._expand_summary()

        return self._summary

    @property
    def details(self) -> str:
        if not self._details:
            self._details = self._expand_details()

        return self._details

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

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

        for bad, repl in _MESSAGE_REPLACEMENTS.items():
            msg = msg.replace(bad, repl)

        if any(substring in msg for substring in _SKIP_DETAILS):
            return msg

        schema = self.ex.rule_definition
        if self.ex.rule in _NEED_DETAILS and schema:
            summary = _SummaryWriter(_TOML_JARGON)
            return f"{msg}:\n\n{indent(summary(schema), '    ')}"

        return msg

    def _expand_details(self) -> str:
        optional = []
        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(
                    desc,
                    width=80,
                    initial_indent="    ",
                    subsequent_indent="    ",
                    break_long_words=False,
                )
            )
            optional.append(f"DESCRIPTION:\n{description}")
        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, '    ')}",
        ]
        msg = "\n\n".join(optional + defaults)
        epilog = f"\n{_FORMATS_HELP}" if "format" in msg.lower() else ""
        return msg + epilog


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

    def __init__(self, jargon: dict[str, str] | None = None):
        self.jargon: dict[str, str] = jargon or {}
        # Clarify confusing terms
        self._terms = {
            "anyOf": "at least one of the following",
            "oneOf": "exactly one of the following",
            "allOf": "all of the following",
            "not": "(*NOT* the following)",
            "prefixItems": f"{self._jargon('items')} (in order)",
            "items": "items",
            "contains": "contains at least one of",
            "propertyNames": (
                f"non-predefined acceptable {self._jargon('property names')}"
            ),
            "patternProperties": f"{self._jargon('properties')} named via pattern",
            "const": "predefined value",
            "enum": "one of",
        }
        # Attributes that indicate that the definition is easy and can be done
        # inline (e.g. string and number)
        self._guess_inline_defs = [
            "enum",
            "const",
            "maxLength",
            "minLength",
            "pattern",
            "format",
            "minimum",
            "maximum",
            "exclusiveMinimum",
            "exclusiveMaximum",
            "multipleOf",
        ]

    def _jargon(self, term: str | list[str]) -> str | list[str]:
        if isinstance(term, list):
            return [self.jargon.get(t, t) for t in term]
        return self.jargon.get(term, term)

    def __call__(
        self,
        schema: dict | list[dict],
        prefix: str = "",
        *,
        _path: Sequence[str] = (),
    ) -> str:
        if isinstance(schema, list):
            return self._handle_list(schema, prefix, _path)

        filtered = self._filter_unecessary(schema, _path)
        simple = self._handle_simple_dict(filtered, _path)
        if simple:
            return f"{prefix}{simple}"

        child_prefix = self._child_prefix(prefix, "  ")
        item_prefix = self._child_prefix(prefix, "- ")
        indent = len(prefix) * " "
        with io.StringIO() as buffer:
            for i, (key, value) in enumerate(filtered.items()):
                child_path = [*_path, key]
                line_prefix = prefix if i == 0 else indent
                buffer.write(f"{line_prefix}{self._label(child_path)}:")
                # ^  just the first item should receive the complete prefix
                if isinstance(value, dict):
                    filtered = self._filter_unecessary(value, child_path)
                    simple = self._handle_simple_dict(filtered, child_path)
                    buffer.write(
                        f" {simple}"
                        if simple
                        else f"\n{self(value, child_prefix, _path=child_path)}"
                    )
                elif isinstance(value, list) and (
                    key != "type" or self._is_property(child_path)
                ):
                    children = self._handle_list(value, item_prefix, child_path)
                    sep = " " if children.startswith("[") else "\n"
                    buffer.write(f"{sep}{children}")
                else:
                    buffer.write(f" {self._value(value, child_path)}\n")
            return buffer.getvalue()

    def _is_unecessary(self, path: Sequence[str]) -> bool:
        if self._is_property(path) or not path:  # empty path => instruction @ root
            return False
        key = path[-1]
        return any(key.startswith(k) for k in "$_") or key in self._IGNORE

    def _filter_unecessary(
        self, schema: dict[str, Any], path: Sequence[str]
    ) -> dict[str, Any]:
        return {
            key: value
            for key, value in schema.items()
            if not self._is_unecessary([*path, key])
        }

    def _handle_simple_dict(self, value: dict, path: Sequence[str]) -> str | None:
        inline = any(p in value for p in self._guess_inline_defs)
        simple = not any(isinstance(v, (list, dict)) for v in value.values())
        if inline or simple:
            return f"{{{', '.join(self._inline_attrs(value, path))}}}\n"
        return None

    def _handle_list(
        self, schemas: list, prefix: str = "", path: Sequence[str] = ()
    ) -> str:
        if self._is_unecessary(path):
            return ""

        repr_ = repr(schemas)
        if all(not isinstance(e, (dict, list)) for e in schemas) and len(repr_) < 60:
            return f"{repr_}\n"

        item_prefix = self._child_prefix(prefix, "- ")
        return "".join(
            self(v, item_prefix, _path=[*path, f"[{i}]"]) for i, v in enumerate(schemas)
        )

    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]:
            if key not in {"properties", "patternProperties"}:
                break
            counter += 1

        # If the counter if even, the path correspond to a JSON Schema keyword
        # otherwise it can be any arbitrary string naming a property
        return counter % 2 == 1

    def _label(self, path: Sequence[str]) -> str:
        *parents, key = path
        if not self._is_property(path):
            norm_key = _separate_terms(key)
            return self._terms.get(key) or " ".join(self._jargon(norm_key))

        if parents[-1] == "patternProperties":
            return f"(regex {key!r})"
        return repr(key)  # property name

    def _value(self, value: Any, path: Sequence[str]) -> str:
        if path[-1] == "type" and not self._is_property(path):
            type_ = self._jargon(value)
            return f"[{', '.join(type_)}]" if isinstance(type_, list) else type_
        return repr(value)

    def _inline_attrs(self, schema: dict, path: Sequence[str]) -> Iterator[str]:
        for key, value in schema.items():
            child_path = [*path, key]
            yield f"{self._label(child_path)}: {self._value(value, child_path)}"

    def _child_prefix(self, parent_prefix: str, child_prefix: str) -> str:
        return len(parent_prefix) * " " + child_prefix


def _separate_terms(word: str) -> list[str]:
    """
    >>> _separate_terms("FooBar-foo")
    ['foo', 'bar', 'foo']
    """
    return [w.lower() for w in _CAMEL_CASE_SPLITTER.split(word) if w]
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/extra_validations.py0000664000175000017500000001171215140154751030647 0ustar  carstencarsten"""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).
"""

import collections
import itertools
from inspect import cleandoc
from typing import Generator, Iterable, Mapping, TypeVar

from .error_reporting import ValidationError

T = TypeVar("T", bound=Mapping)


class RedefiningStaticFieldAsDynamic(ValidationError):
    _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"
    )


class IncludedDependencyGroupMustExist(ValidationError):
    _DESC = """An included dependency group must exist and must not be cyclic.
    """
    __doc__ = _DESC
    _URL = "https://peps.python.org/pep-0735/"


class ImportNameCollision(ValidationError):
    _DESC = """According to PEP 794:

    All import-names and import-namespaces items must be unique.
    """
    __doc__ = _DESC
    _URL = "https://peps.python.org/pep-0794/"


class ImportNameMissing(ValidationError):
    _DESC = """According to PEP 794:

    An import name must have all parents listed.
    """
    __doc__ = _DESC
    _URL = "https://peps.python.org/pep-0794/"


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:
            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


def validate_include_depenency(pyproject: T) -> T:
    dependency_groups = pyproject.get("dependency-groups", {})
    for key, value in dependency_groups.items():
        for each in value:
            if (
                isinstance(each, dict)
                and (include_group := each.get("include-group"))
                and include_group not in dependency_groups
            ):
                raise IncludedDependencyGroupMustExist(
                    message=f"The included dependency group {include_group} doesn't exist",
                    value=each,
                    name=f"data.dependency_groups.{key}",
                    definition={
                        "description": cleandoc(IncludedDependencyGroupMustExist._DESC),
                        "see": IncludedDependencyGroupMustExist._URL,
                    },
                    rule="PEP 735",
                )
    # TODO: check for `include-group` cycles (can be conditional to graphlib)
    return pyproject


def _remove_private(items: Iterable[str]) -> Generator[str, None, None]:
    for item in items:
        yield item.partition(";")[0].rstrip()


def validate_import_name_issues(pyproject: T) -> T:
    project = pyproject.get("project", {})
    import_names = collections.Counter(_remove_private(project.get("import-names", [])))
    import_namespaces = collections.Counter(
        _remove_private(project.get("import-namespaces", []))
    )

    duplicated = [k for k, v in (import_names + import_namespaces).items() if v > 1]

    if duplicated:
        raise ImportNameCollision(
            message="Duplicated names are not allowed in import-names/import-namespaces",
            value=duplicated,
            name="data.project.importnames(paces)",
            definition={
                "description": cleandoc(ImportNameCollision._DESC),
                "see": ImportNameCollision._URL,
            },
            rule="PEP 794",
        )

    names = frozenset(import_names + import_namespaces)
    for name in names:
        for parent in itertools.accumulate(
            name.split(".")[:-1], lambda a, b: f"{a}.{b}"
        ):
            if parent not in names:
                raise ImportNameMissing(
                    message="All parents of an import name must also be listed in import-namespace/import-names",
                    value=name,
                    name="data.project.importnames(paces)",
                    definition={
                        "description": cleandoc(ImportNameMissing._DESC),
                        "see": ImportNameMissing._URL,
                    },
                    rule="PEP 794",
                )

    return pyproject


EXTRA_VALIDATIONS = (
    validate_project_dynamic,
    validate_include_depenency,
    validate_import_name_issues,
)
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/cli.py0000664000175000017500000002256515140154751025706 0ustar  carstencarsten# The code in this module is based on a similar code from `ini2toml` (originally
# published under the MPL-2.0 license)
# https://github.com/abravalheri/ini2toml/blob/49897590a9254646434b7341225932e54f9626a3/LICENSE.txt

# ruff: noqa: C408
# Unnecessary `dict` call (rewrite as a literal)
from __future__ import annotations

import argparse
import json
import logging
import sys
from contextlib import contextmanager
from itertools import chain
from textwrap import dedent, wrap
from typing import (
    TYPE_CHECKING,
    Callable,
    Generator,
    Iterator,
    NamedTuple,
    Sequence,
    TypeVar,
)

from . import __version__
from . import _tomllib as tomllib
from .api import Validator
from .errors import ValidationError
from .plugins import PluginProtocol, PluginWrapper
from .plugins import list_from_entry_points as list_plugins_from_entry_points
from .remote import RemotePlugin, load_store

if TYPE_CHECKING:
    import io

_logger = logging.getLogger(__package__)
T = TypeVar("T", bound=NamedTuple)

_REGULAR_EXCEPTIONS = (ValidationError, tomllib.TOMLDecodeError)


@contextmanager
def critical_logging() -> Generator[None, None, None]:
    """Make sure the logging level is set even before parsing the CLI args"""
    try:
        yield
    except Exception:  # pragma: no cover
        if "-vv" in sys.argv or "--very-verbose" in sys.argv:
            setup_logging(logging.DEBUG)
        raise


_STDIN = argparse.FileType("r")("-")

META: dict[str, dict] = {
    "version": dict(
        flags=("-V", "--version"),
        action="version",
        version=f"{__package__} {__version__}",
    ),
    "input_file": dict(
        dest="input_file",
        nargs="*",
        # default=[_STDIN],  # postponed to facilitate testing
        type=argparse.FileType("r"),
        help="TOML file to be verified (`stdin` by default)",
    ),
    "enable": dict(
        flags=("-E", "--enable-plugins"),
        nargs="+",
        default=(),
        dest="enable",
        metavar="PLUGINS",
        help="Enable ONLY the given plugins (ALL plugins are enabled by default).",
    ),
    "disable": dict(
        flags=("-D", "--disable-plugins"),
        nargs="+",
        dest="disable",
        default=(),
        metavar="PLUGINS",
        help="Enable ALL plugins, EXCEPT the ones given.",
    ),
    "verbose": dict(
        flags=("-v", "--verbose"),
        dest="loglevel",
        action="store_const",
        const=logging.INFO,
        help="set logging level to INFO",
    ),
    "very_verbose": dict(
        flags=("-vv", "--very-verbose"),
        dest="loglevel",
        action="store_const",
        const=logging.DEBUG,
        help="set logging level to DEBUG",
    ),
    "dump_json": dict(
        flags=("--dump-json",),
        action="store_true",
        help="Print the JSON equivalent to the given TOML",
    ),
    "tool": dict(
        flags=("-t", "--tool"),
        action="append",
        dest="tool",
        help="External tools file/url(s) to load, of the form name=URL#path",
    ),
    "store": dict(
        flags=("--store",),
        help="Load a pyproject.json file and read all the $ref's into tools "
        "(see https://json.schemastore.org/pyproject.json)",
    ),
}


class CliParams(NamedTuple):
    input_file: list[io.TextIOBase]
    plugins: list[PluginWrapper]
    tool: list[str]
    store: str
    loglevel: int = logging.WARNING
    dump_json: bool = False


def __meta__(plugins: Sequence[PluginProtocol]) -> dict[str, dict]:
    """'Hyper parameters' to instruct :mod:`argparse` how to create the CLI"""
    meta = {k: v.copy() for k, v in META.items()}
    meta["enable"]["choices"] = {p.tool for p in plugins}
    meta["input_file"]["default"] = [_STDIN]  # lazily defined to facilitate testing
    return meta


@critical_logging()
def parse_args(
    args: Sequence[str],
    plugins: Sequence[PluginProtocol],
    description: str = "Validate a given TOML file",
    get_parser_spec: Callable[[Sequence[PluginProtocol]], dict[str, dict]] = __meta__,
    params_class: type[T] = CliParams,  # type: ignore[assignment]
) -> T:
    """Parse command line parameters

    Args:
      args: command line parameters as list of strings (for example  ``["--help"]``).

    Returns: command line parameters namespace
    """
    epilog = ""
    if plugins:
        epilog = f"The following plugins are available:\n\n{plugins_help(plugins)}"

    parser = argparse.ArgumentParser(
        description=description, epilog=epilog, formatter_class=Formatter
    )
    for cli_opts in get_parser_spec(plugins).values():
        parser.add_argument(*cli_opts.pop("flags", ()), **cli_opts)

    parser.set_defaults(loglevel=logging.WARNING)
    params = vars(parser.parse_args(args))
    enabled = params.pop("enable", ())
    disabled = params.pop("disable", ())
    params["tool"] = params["tool"] or []
    params["store"] = params["store"] or ""
    params["plugins"] = select_plugins(plugins, enabled, disabled)
    return params_class(**params)  # type: ignore[call-overload, no-any-return]


Plugins = TypeVar("Plugins", bound=PluginProtocol)


def select_plugins(
    plugins: Sequence[Plugins],
    enabled: Sequence[str] = (),
    disabled: Sequence[str] = (),
) -> list[Plugins]:
    available = list(plugins)
    if enabled:
        available = [p for p in available if p.tool in enabled]
    if disabled:
        available = [p for p in available if p.tool not in disabled]
    return available


def setup_logging(loglevel: int) -> None:
    """Setup basic logging

    Args:
      loglevel: minimum loglevel for emitting messages
    """
    logformat = "[%(levelname)s] %(message)s"
    logging.basicConfig(level=loglevel, stream=sys.stderr, format=logformat)


@contextmanager
def exceptions2exit() -> Generator[None, None, None]:
    try:
        yield
    except _ExceptionGroup as group:
        for prefix, ex in group:
            print(prefix)
            _logger.exception(str(ex) + "\n")
        raise SystemExit(1) from None
    except _REGULAR_EXCEPTIONS as ex:
        _logger.exception(str(ex))
        raise SystemExit(1) from None
    except Exception as ex:  # pragma: no cover
        _logger.exception(f"{ex.__class__.__name__}: {ex}\n")
        _logger.debug("Please check the following information:", exc_info=True)
        raise SystemExit(1) from None


def run(args: Sequence[str] = ()) -> int:
    """Wrapper allowing :obj:`Translator` to be called in a CLI fashion.

    Instead of returning the value from :func:`Translator.translate`, it prints the
    result to the given ``output_file`` or ``stdout``.

    Args:
      args (List[str]): command line parameters as list of strings
          (for example  ``["--verbose", "setup.cfg"]``).
    """
    args = args or sys.argv[1:]
    plugins = list_plugins_from_entry_points()
    params: CliParams = parse_args(args, plugins)
    setup_logging(params.loglevel)
    tool_plugins = [RemotePlugin.from_str(t) for t in params.tool]
    if params.store:
        tool_plugins.extend(load_store(params.store))
    validator = Validator(params.plugins, extra_plugins=tool_plugins)

    exceptions = _ExceptionGroup()
    for file in params.input_file:
        try:
            _run_on_file(validator, params, file)
        except _REGULAR_EXCEPTIONS as ex:  # noqa: PERF203
            exceptions.add(f"Invalid {_format_file(file)}", ex)

    exceptions.raise_if_any()

    return 0


def _run_on_file(validator: Validator, params: CliParams, file: io.TextIOBase) -> None:
    if file in (sys.stdin, _STDIN):
        print("Expecting input via `stdin`...", file=sys.stderr, flush=True)

    toml_equivalent = tomllib.loads(file.read())
    validator(toml_equivalent)
    if params.dump_json:
        print(json.dumps(toml_equivalent, indent=2))
    else:
        print(f"Valid {_format_file(file)}")


main = exceptions2exit()(run)


class Formatter(argparse.RawTextHelpFormatter):
    # Since the stdlib does not specify what is the signature we need to implement in
    # order to create our own formatter, we are left no choice other then overwrite a
    # "private" method considered to be an implementation detail.

    def _split_lines(self, text: str, width: int) -> list[str]:
        return list(chain.from_iterable(wrap(x, width) for x in text.splitlines()))


def plugins_help(plugins: Sequence[PluginProtocol]) -> str:
    return "\n".join(_format_plugin_help(p) for p in plugins)


def _flatten_str(text: str) -> str:
    text = " ".join(x.strip() for x in dedent(text).splitlines()).strip()
    text = text.rstrip(".,;").strip()
    return (text[0].lower() + text[1:]).strip()


def _format_plugin_help(plugin: PluginProtocol) -> str:
    help_text = plugin.help_text
    help_text = f": {_flatten_str(help_text)}" if help_text else ""
    return f"* {plugin.tool!r}{help_text}"


def _format_file(file: io.TextIOBase) -> str:
    if hasattr(file, "name") and file.name:
        return f"file: {file.name}"
    return "file"  # pragma: no cover


class _ExceptionGroup(Exception):
    _members: list[tuple[str, Exception]]

    def __init__(self) -> None:
        self._members = []
        super().__init__()

    def add(self, prefix: str, ex: Exception) -> None:
        self._members.append((prefix, ex))

    def __iter__(self) -> Iterator[tuple[str, Exception]]:
        return iter(self._members)

    def raise_if_any(self) -> None:
        number = len(self._members)
        if number == 1:
            print(self._members[0][0])
            raise self._members[0][1]
        if number > 0:
            raise self
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/remote.py0000664000175000017500000000566715140154751026436 0ustar  carstencarstenfrom __future__ import annotations

import json
import logging
import typing
import urllib.parse
from typing import Generator

from . import caching, errors, http

if typing.TYPE_CHECKING:
    import sys

    from .types import Schema

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


__all__ = ["RemotePlugin", "load_store"]


_logger = logging.getLogger(__name__)


def load_from_uri(
    tool_uri: str, cache_dir: caching.PathLike | None = None
) -> tuple[str, Schema]:
    tool_info = urllib.parse.urlparse(tool_uri)
    if tool_info.netloc:
        url = f"{tool_info.scheme}://{tool_info.netloc}{tool_info.path}"
        download = caching.as_file(http.open_url, url, cache_dir)
        with download as f:
            contents = json.load(f)
    else:
        with open(tool_info.path, "rb") as f:
            contents = json.load(f)
    return tool_info.fragment, contents


class RemotePlugin:
    def __init__(self, *, tool: str, schema: Schema, fragment: str = ""):
        self.tool = tool
        self.schema = schema
        self.fragment = fragment
        self.id = self.schema["$id"]
        self.help_text = f"{tool} "

    @classmethod
    def from_url(cls, tool: str, url: str) -> Self:
        fragment, schema = load_from_uri(url)
        return cls(tool=tool, schema=schema, fragment=fragment)

    @classmethod
    def from_str(cls, tool_url: str) -> Self:
        tool, _, url = tool_url.partition("=")
        if not url:
            raise errors.URLMissingTool(tool)
        return cls.from_url(tool, url)


def load_store(pyproject_url: str) -> Generator[RemotePlugin, None, None]:
    """
    Takes a URL / Path and loads the tool table, assuming it is a set of ref's.
    Currently ignores "inline" sections. This is the format that SchemaStore
    (https://json.schemastore.org/pyproject.json) is in.
    """

    fragment, contents = load_from_uri(pyproject_url)
    if fragment:
        _logger.error(
            f"Must not be called with a fragment, got {fragment!r}"
        )  # pragma: no cover
    table = contents["properties"]["tool"]["properties"]
    for tool, info in table.items():
        if tool in {"setuptools", "distutils"}:
            pass  # built-in
        elif "$ref" in info:
            _logger.info(f"Loading {tool} from store: {pyproject_url}")
            rp = RemotePlugin.from_url(tool, info["$ref"])
            yield rp
            # Does not support anyOf and similar with properties inside them
            for values in rp.schema.get("properties", {}).values():
                url = values.get("$ref", "")
                if url.startswith(("https://", "https://")):
                    yield RemotePlugin.from_url("", url)
        else:
            _logger.warning(f"{tool!r} does not contain $ref")  # pragma: no cover


if typing.TYPE_CHECKING:
    from .plugins import PluginProtocol

    _: PluginProtocol = typing.cast("RemotePlugin", None)
abravalheri-validate-pyproject-4b2e70d/src/validate_pyproject/types.py0000664000175000017500000000146515140154751026277 0ustar  carstencarstenfrom typing import Callable, Mapping, NewType, TypeVar

T = TypeVar("T", bound=Mapping)

Schema = NewType("Schema", Mapping)
"""JSON Schema represented as a Python dict"""

ValidationFn = Callable[[T], T]
"""Custom validation function.
It should receive as input a mapping corresponding to the whole
``pyproject.toml`` file and raise a :exc:`fastjsonschema.JsonSchemaValueException`
if it is not valid.
"""

FormatValidationFn = Callable[[str], bool]
"""Should return ``True`` when the input string satisfies the format"""

Plugin = Callable[[str], Schema]
"""A plugin is something that receives the name of a `tool` sub-table
(as defined  in PEPPEP621) and returns a :obj:`Schema`.

For example ``plugin("setuptools")`` should return the JSON schema for the
``[tool.setuptools]`` table of a ``pyproject.toml`` file.
"""
abravalheri-validate-pyproject-4b2e70d/.cirrus.yml0000664000175000017500000001544115140154751022211 0ustar  carstencarsten---
# ---- Default values to be merged into tasks ----

env:
  LC_ALL: C.UTF-8
  LANG: C.UTF-8
  PIP_CACHE_DIR: ${CIRRUS_WORKING_DIR}/.cache/pip
  PRE_COMMIT_HOME: ${CIRRUS_WORKING_DIR}/.cache/pre-commit
  CIRRUS_ARTIFACT_URL: https://api.cirrus-ci.com/v1/artifact/build/${CIRRUS_BUILD_ID}
  # Coveralls configuration
  CI_NAME: cirrus-ci
  CI_BRANCH: ${CIRRUS_BRANCH}
  CI_PULL_REQUEST: ${CIRRUS_PR}
  CI_BUILD_NUMBER: ${CIRRUS_BUILD_ID}
  CI_BUILD_URL: https://cirrus-ci.com/build/${CIRRUS_BUILD_ID}
  COVERALLS_PARALLEL: "true"
  COVERALLS_FLAG_NAME: ${CIRRUS_TASK_NAME}
  # Project-specific
  VALIDATE_PYPROJECT_CACHE_REMOTE: tests/.cache

# ---- Templates ----

.task_template: &task-template
  debug_information_script:
    - echo "$(which python) -- $(python -VV)"
    - echo "$(which pip) -- $(pip -VV)"
    - python -c 'import os, sys; print(os.name, sys.platform, getattr(sys, "abiflags", None))'
  prepare_script:  # avoid git failing with setuptools-scm
    - git config --global user.email "you@example.com"
    - git config --global user.name "Your Name"
  pip_cache:
    folder: "${CIRRUS_WORKING_DIR}/.cache/pip"
    fingerprint_script: echo "${CIRRUS_OS}-${CIRRUS_TASK_NAME}"
    reupload_on_changes: true
  pre_commit_cache:
    folder: "${CIRRUS_WORKING_DIR}/.cache/pre-commit"
    fingerprint_script: echo "${CIRRUS_OS}-${CIRRUS_TASK_NAME}" | cat - .pre-commit-config.yaml
    reupload_on_changes: true

.test_template: &test-template
  # Requires pip, tox, and pipx to be installed via OS/pip
  alias: test
  depends_on: [build]
  <<: *task-template
  test_files_cache:
    folder: ${VALIDATE_PYPROJECT_CACHE_REMOTE}
    fingerprint_script: echo $CIRRUS_BUILD_ID
    populate_script: python tools/cache_urls_for_tests.py
    reupload_on_changes: true
  download_artifact_script: &download-artifact
    - curl -L -O ${CIRRUS_ARTIFACT_URL}/build/upload/dist.tar.gz
    - tar xzf dist.tar.gz
    - rm dist.tar.gz
  test_script: >
    tox --installpkg dist/*.whl --
    -n 5 --randomly-seed=42 -rfEx --durations 10 --color yes
  submit_coverage_script:
    - pipx run coverage xml -o coverage.xml
    - pipx run 'coveralls<4' --submit coverage.xml
      # ^-- https://github.com/TheKevJames/coveralls-python/issues/434

# Deep clone script for POSIX environments (required for setuptools-scm)
.clone_script: &clone |
  if [ -z "$CIRRUS_PR" ]; then
    git clone --recursive --branch=$CIRRUS_BRANCH https://x-access-token:${CIRRUS_REPO_CLONE_TOKEN}@github.com/${CIRRUS_REPO_FULL_NAME}.git $CIRRUS_WORKING_DIR
    git reset --hard $CIRRUS_CHANGE_IN_REPO
  else
    git clone --recursive https://x-access-token:${CIRRUS_REPO_CLONE_TOKEN}@github.com/${CIRRUS_REPO_FULL_NAME}.git $CIRRUS_WORKING_DIR
    git fetch origin pull/$CIRRUS_PR/head:pull/$CIRRUS_PR
    git reset --hard $CIRRUS_CHANGE_IN_REPO
  fi

# ---- CI Pipeline ----

build_task:
  name: build and check (Linux - 3.12)
  alias: build
  container: {image: "python:3.12-trixie"}
  clone_script: *clone
  <<: *task-template
  install_script: pip install tox tox-uv
  build_script:
    - tox -e clean,lint,typecheck,build
    - tar czf dist.tar.gz dist
  upload_artifacts:
    path: dist.tar.gz


linux_task:
  matrix:
    - name: test (Linux - 3.8)
      container: {image: "python:3.8-bookworm"}
    - name: test (Linux - 3.10)
      container: {image: "python:3.10-trixie"}
      skip: $BRANCH !=~ "^(main|master)$"
    - name: test (Linux - 3.12)
      container: {image: "python:3.12-trixie"}
      skip: $BRANCH !=~ "^(main|master)$"
    - name: test (Linux - 3.14)
      container: {image: "python:3.14-trixie"}
    - name: test (Linux - 3.15)
      container: {image: "python:3.15-rc-trixie"}
      allow_failures: true  # RC
  install_script:
    - python -m pip install --upgrade pip tox tox-uv pipx
  <<: *test-template
  alias: base-test

mamba_task:
  name: test (Linux - mambaforge)
  container: {image: "condaforge/mambaforge"}
  install_script:  # Overwrite template
    - mamba install -y pip pipx tox curl
  <<: *test-template
  depends_on: [base-test]

macos_task:
  name: test (macOS - brew)
  macos_instance:
    image: ghcr.io/cirruslabs/macos-runner:sonoma
  env:
    PATH: "/opt/homebrew/opt/python/libexec/bin:${PATH}"
  brew_cache: {folder: "$HOME/Library/Caches/Homebrew"}
  install_script:
    - brew reinstall python
    - brew install tox pipx
  <<: *test-template
  depends_on: [build, base-test]

freebsd_task:
  name: test (freebsd - 3.11)
  freebsd_instance: {image_family: freebsd-14-3}
  install_script:
    - pkg remove -y python lang/python
    - pkg install -y git python311 py311-pip py311-gdbm py311-sqlite3 py311-tox py311-tomli py311-pipx
    - ln -s /usr/local/bin/python3.11 /usr/local/bin/python
  <<: *test-template
  depends_on: [build, base-test]

windows_task:
  name: test (Windows - 3.12.10)
  windows_container:
    image: "cirrusci/windowsservercore:2019"
    os_version: 2019
  env:
    CIRRUS_SHELL: bash
    PATH: /c/Python312:/c/Python312/Scripts:/c/tools:${PATH}
  install_script:
    # Activate long file paths to avoid some errors
    - ps: New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
    - choco install -y --no-progress python3 --version=3.12.10 --params "/NoLockdown"
    - choco install -y --no-progress curl
    - python -m pip install --upgrade certifi
    - python -m pip install -U pip tox tox-uv pipx
  configure_certs_script: |
    CERT_PATH="$(python -m certifi | tr -d '\r\n')"
    echo "SSL_CERT_FILE=$CERT_PATH" >> "$CIRRUS_ENV"
  <<: *test-template
  depends_on: [build, base-test]

finalize_task:
  container: {image: "python:3.12-trixie"}
  depends_on: [test]
  <<: *task-template
  install_script: pip install 'coveralls<4'
    # ^-- https://github.com/TheKevJames/coveralls-python/issues/434
  finalize_coverage_script: coveralls --finish

linkcheck_task:
  name: linkcheck (Linux - 3.12)
  only_if: $BRANCH =~ "^(main|master)$"
  container: {image: "python:3.12-trixie"}
  depends_on: [finalize]
  allow_failures: true
  <<: *task-template
  install_script: pip install tox tox-uv
  download_artifact_script: *download-artifact
  linkcheck_script: tox --installpkg dist/*.whl -e linkcheck -- -q

# # The following task is already covered by a GitHub Action,
# # (commented to avoid errors when publishing duplicated packages to PyPI)
# publish_task:
#   name: publish (Linux - 3.12)
#   container: {image: "python:3.12-trixie"}
#   depends_on: [build, base-test, test]
#   only_if: $CIRRUS_TAG =~ 'v\d.*' && $CIRRUS_USER_PERMISSION == "admin"
#   <<: *task-template
#   env:
#     TWINE_REPOSITORY: pypi
#     TWINE_USERNAME: __token__
#     TWINE_PASSWORD: $PYPI_TOKEN
#     # See: https://cirrus-ci.org/guide/writing-tasks/#encrypted-variables
#   install_script: pip install tox tox-uv
#   download_artifact_script: *download-artifact
#   publish_script:
#     - ls dist/*
#     - tox -e publish
abravalheri-validate-pyproject-4b2e70d/tox.ini0000664000175000017500000000534315140154751021414 0ustar  carstencarsten# Tox configuration file
# Read more under https://tox.wiki/
# THIS SCRIPT IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS!

[tox]
minversion = 4.22
envlist = default
isolated_build = True


[testenv]
description = Invoke pytest to run automated tests
setenv =
    TOXINIDIR = {toxinidir}
passenv =
    HOME
    SETUPTOOLS_*
    VALIDATE_PYPROJECT_*
    SSL_CERT_FILE
dependency_groups = test
extras = all
commands =
    pytest {posargs}


[testenv:lint]
description = Perform static analysis and style checks
skip_install = True
deps = pre-commit
passenv =
    HOMEPATH
    PROGRAMDATA
    SETUPTOOLS_*
commands =
    pre-commit run --all-files {posargs:--show-diff-on-failure}


[testenv:typecheck]
base_python = 3.8
description = Invoke mypy to typecheck the source code
changedir = {toxinidir}
passenv =
    TERM
    # ^ ensure colors
extras = all
dependency_groups = typecheck
commands =
    python -m mypy {posargs:--pretty --show-error-context src}


[testenv:{build,clean}]
description =
    build: Build the package in isolation according to PEP517, see https://github.com/pypa/build
    clean: Remove old distribution files and temporary build artifacts (./build and ./dist)
# https://setuptools.pypa.io/en/stable/build_meta.html#how-to-use-it
skip_install = True
changedir = {toxinidir}
deps =
    build: build[virtualenv]
passenv =
    SETUPTOOLS_*
commands =
    clean: python -c 'import shutil; [shutil.rmtree(p, True) for p in ("build", "dist", "docs/_build")]'
    clean: python -c 'import pathlib, shutil; [shutil.rmtree(p, True) for p in pathlib.Path("src").glob("*.egg-info")]'
    build: python -m build {posargs}


[testenv:{docs,doctests,linkcheck}]
description =
    docs: Invoke sphinx-build to build the docs
    doctests: Invoke sphinx-build to run doctests
    linkcheck: Check for broken links in the documentation
setenv =
    DOCSDIR = {toxinidir}/docs
    BUILDDIR = {toxinidir}/docs/_build
    docs: BUILD = html
    doctests: BUILD = doctest
    linkcheck: BUILD = linkcheck
passenv =
    SETUPTOOLS_*
extras = all
dependency_groups = docs
commands =
    sphinx-build -v -T -j auto --color -b {env:BUILD} -d "{env:BUILDDIR}/doctrees" "{env:DOCSDIR}" "{env:BUILDDIR}/{env:BUILD}" {posargs}


[testenv:publish]
description =
    Publish the package you have been developing to a package index server.
    By default, it uses testpypi. If you really want to publish your package
    to be publicly accessible in PyPI, use the `-- --repository pypi` option.
skip_install = True
changedir = {toxinidir}
passenv =
    TWINE_USERNAME
    TWINE_PASSWORD
    TWINE_REPOSITORY
    TWINE_REPOSITORY_URL
deps = twine
commands =
    python -m twine check dist/*
    python -m twine upload {posargs:--repository {env:TWINE_REPOSITORY:testpypi}} dist/*
abravalheri-validate-pyproject-4b2e70d/.pre-commit-config.yaml0000664000175000017500000000406015140154751024355 0ustar  carstencarstenci:
  autoupdate_commit_msg: "chore(deps): update pre-commit hooks"
  autofix_commit_msg: "style: pre-commit fixes"
  autoupdate_schedule: "monthly"

exclude: '^src/validate_pyproject/_vendor'

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v6.0.0
  hooks:
  - id: check-added-large-files
  - id: check-ast
  - id: check-json
  - id: check-merge-conflict
  - id: check-symlinks
  - id: check-toml
  - id: check-xml
  - id: check-yaml
  - id: debug-statements
  - id: end-of-file-fixer
  - id: requirements-txt-fixer
  - id: trailing-whitespace
  - id: mixed-line-ending
    args: ['--fix=auto']  # replace 'auto' with 'lf' to enforce Linux/Mac line endings or 'crlf' for Windows

- repo: https://github.com/codespell-project/codespell
  rev: v2.4.1
  hooks:
  - id: codespell
    args: ["--ignore-words-list=THIRDPARTY", --write-changes]

- repo: https://github.com/astral-sh/ruff-pre-commit
  rev: v0.14.14
  hooks:
  - id: ruff-check
    args: [--fix, --show-fixes]
  - id: ruff-format

- repo: https://github.com/adamchainz/blacken-docs
  rev: 1.20.0
  hooks:
  - id: blacken-docs
    additional_dependencies: [black==23.*]

- repo: https://github.com/pre-commit/pygrep-hooks
  rev: "v1.10.0"
  hooks:
    - id: rst-backticks
    - id: rst-directive-colons
    - id: rst-inline-touching-normal

- repo: local  # self-test for `validate-pyproject` hook
  hooks:
  - id: validate-pyproject
    name: Validate pyproject.toml
    language: python
    files: ^tests/examples/.*pyproject\.toml$
    entry: python
    args:
      - -c
      - >
        import sys;
        sys.path.insert(0, "src");
        from validate_pyproject.cli import main;
        main()
    additional_dependencies:
      - validate-pyproject[all]>=0.13

- repo: https://github.com/python-jsonschema/check-jsonschema
  rev: 0.36.1
  hooks:
    - id: check-metaschema
      files: \.schema\.json$
    - id: check-readthedocs
    - id: check-github-workflows

- repo: https://github.com/scientific-python/cookie
  rev: 2025.11.21
  hooks:
    - id: sp-repo-review
      name: Validate Python repository
abravalheri-validate-pyproject-4b2e70d/pyproject.toml0000664000175000017500000000630415140154751023013 0ustar  carstencarsten[build-system]
requires = ["setuptools>=61.2", "setuptools_scm[toml]>=7.1"]
build-backend = "setuptools.build_meta"

[project]
name = "validate-pyproject"
description = "Validation library and CLI tool for checking on 'pyproject.toml' files using JSON Schema"
authors = [{name = "Anderson Bravalheri", email = "andersonbravalheri@gmail.com"}]
readme ="README.rst"
license = {text = "MPL-2.0 and MIT and BSD-3-Clause"}
requires-python = ">=3.8"
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "Operating System :: OS Independent",
    "Programming Language :: Python",
    "Topic :: Software Development :: Quality Assurance",
    "Typing :: Typed",
]
dependencies = ["fastjsonschema>=2.16.2,<=3"]
dynamic = ["version"]

[project.urls]
Homepage = "https://github.com/abravalheri/validate-pyproject/"
Documentation = "https://validate-pyproject.readthedocs.io/"
Source = "https://github.com/abravalheri/validate-pyproject"
Tracker = "https://github.com/abravalheri/validate-pyproject/issues"
Changelog = "https://validate-pyproject.readthedocs.io/en/latest/changelog.html"
Download = "https://pypi.org/project/validate-pyproject/#files"

[project.optional-dependencies]
all = [
    "packaging>=24.2",
    "tomli>=1.2.1; python_version<'3.11'",
    "trove-classifiers>=2021.10.20",
]
store = ["validate-pyproject-schema-store"]

[project.scripts]
validate-pyproject = "validate_pyproject.cli:main"

[project.entry-points."validate_pyproject.tool_schema"]
setuptools = "validate_pyproject.api:load_builtin_plugin"
distutils = "validate_pyproject.api:load_builtin_plugin"

[project.entry-points."repo_review.checks"]
validate_pyproject = "validate_pyproject.repo_review:repo_review_checks"

[project.entry-points."repo_review.families"]
validate_pyproject = "validate_pyproject.repo_review:repo_review_families"

[dependency-groups]
dev = [
    { include-group = "test" },
    "validate_pyproject[all]",
]
docs = [
    "furo>=2023.08.17",
    "sphinx>=7.2.2",
    "sphinx-argparse>=0.3.1",
    "sphinx-copybutton",
    "sphinx-jsonschema>=1.16.11",
    "sphinxemoji",
]
test = [
    "setuptools",
    "pytest>=8.3.3",
    "pytest-cov",
    "pytest-xdist",
    "pytest-randomly",
    "repo-review; python_version>='3.10'",
    "tomli>=1.2.1; python_version<'3.11'",
]
typecheck = [
    "mypy",
    "importlib-resources",
]

[tool.uv]
environments = [
  "python_version >= '3.9'",
]

[tool.setuptools_scm]
version_scheme = "no-guess-dev"

[tool.pytest.ini_options]
addopts = """
    --import-mode importlib
    --cov validate_pyproject
    --cov-report term-missing
    --doctest-modules
    --strict-markers
    --verbose
"""
norecursedirs = ["dist", "build", ".*"]
testpaths = ["src", "tests"]
log_level = "INFO"

[tool.mypy]
python_version = "3.8"
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
show_traceback = true
warn_unreachable = true
strict = true
# Scaling back on some of the strictness for now
disallow_any_generics = false
disallow_subclassing_any = false

[[tool.mypy.overrides]]
module = ["fastjsonschema", "setuptools._vendor.packaging"]
ignore_missing_imports = true

[tool.repo-review]
ignore = ["PP302", "PP304", "PP305", "PP306", "PP308", "PP309", "PC140", "PC180", "PC901"]
abravalheri-validate-pyproject-4b2e70d/.gitattributes0000664000175000017500000000004015140154751022761 0ustar  carstencarsten.git_archival.txt  export-subst
abravalheri-validate-pyproject-4b2e70d/docs/0000775000175000017500000000000015140154751021024 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/docs/conf.py0000664000175000017500000002365415140154751022335 0ustar  carstencarsten# This file is execfile()d with the current directory set to its containing dir.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
#
# All configuration values have a default; values that are commented out
# serve to show the default.

import os
import sys

# -- Path setup --------------------------------------------------------------

__location__ = os.path.dirname(__file__)
sys.path.insert(0, __location__)
sys.path.insert(0, os.path.join(__location__, "../src"))

# -- Dynamically generated docs ----------------------------------------------
import _gendocs

output_dir = os.path.join(__location__, "api")
module_dir = os.path.join(__location__, "../src/validate_pyproject")
_gendocs.gen_stubs(module_dir, output_dir)

# -- General configuration ---------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'

# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
    "sphinx.ext.autodoc",
    "sphinx.ext.intersphinx",
    "sphinx.ext.todo",
    "sphinx.ext.autosummary",
    "sphinx.ext.viewcode",
    "sphinx.ext.coverage",
    "sphinx.ext.doctest",
    "sphinx.ext.ifconfig",
    "sphinx.ext.mathjax",
    "sphinx.ext.napoleon",
    "sphinx.ext.extlinks",
    "sphinx_copybutton",
    "sphinxemoji.sphinxemoji",
    "sphinx-jsonschema",
    "sphinxarg.ext",
]

# ----------------------------------
# JSON Schema settings
jsonschema_options = {
    "lift_title": True,
    "lift_description": True,
    "lift_definitions": True,
    "auto_reference": True,
    "auto_target": True,
}
# ----------------------------------

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]

# The suffix of source filenames.
source_suffix = ".rst"

# The encoding of source files.
# source_encoding = 'utf-8-sig'

# The master toctree document.
master_doc = "index"

try:
    from validate_pyproject import __version__, dist_name
except ImportError:
    __version__, dist_name = "", "validate-pyproject"


# General information about the project.
project = dist_name
copyright = "2021, Anderson Bravalheri"
repository = "https://github.com/abravalheri/validate-pyproject"

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# version: The short X.Y version.
# release: The full version, including alpha/beta/rc tags.
# If you don't need the separation provided between version and release,
# just set them both to the same value.
version = __version__

if not version or version.lower() == "unknown":
    version = os.getenv("READTHEDOCS_VERSION", "unknown")  # automatically set by RTD

release = version

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# language = None

# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".venv"]

# The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None

# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True

# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True

# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
pygments_dark_style = "monokai"

# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []

# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False

# If this is True, todo emits a warning for each TODO entries. The default is False.
todo_emit_warnings = True


# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
# html_theme = "alabaster"
html_theme = "furo"

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
html_theme_options = {
    "navigation_with_keys": True,
    "light_css_variables": {
        "color-brand-primary": "#2980B9",
        "color-brand-content": "#005CA0",
        "color-brand-muted": "#E7F2FA",
        "color-brand-logo-background": "#156EAD",
    },
    "dark_css_variables": {
        "color-brand-content": "#0A93FB",
        "color-brand-muted": "#00091A",
    },
}

# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []

# The name for this set of Sphinx documents.  If None, it defaults to
# " v documentation".
html_title = project

# A shorter title for the navigation bar.  Default is the same as html_title.
html_short_title = project

# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
# html_logo = ""

# The name of an image file (within the static path) to use as favicon of the
# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
# html_favicon = None

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]

html_css_files = [
    "custom-adjustments.css",  # Avoid name clashes with the theme
]

# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'

# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True

# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}

# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}

# If false, no module index is generated.
# html_domain_indices = True

# If false, no index is generated.
# html_use_index = True

# If true, the index is split into individual pages for each letter.
# html_split_index = False

# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True

# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True

# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True

# If true, an OpenSearch description file will be output, and all pages will
# contain a  tag referring to it.  The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''

# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None

# Output file base name for HTML help builder.
htmlhelp_basename = "validate-pyproject-doc"


# -- Options for LaTeX output ------------------------------------------------

latex_elements = {
    # The paper size ("letterpaper" or "a4paper").
    # "papersize": "letterpaper",
    # The font size ("10pt", "11pt" or "12pt").
    # "pointsize": "10pt",
    # Additional stuff for the LaTeX preamble.
    # "preamble": "",
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
    (
        "index",
        "user_guide.tex",
        "validate-pyproject Documentation",
        "Anderson Bravalheri",
        "manual",
    )
]

# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = ""

# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False

# If true, show page references after internal links.
# latex_show_pagerefs = False

# If true, show URL addresses after external links.
# latex_show_urls = False

# Documents to append as an appendix to all manuals.
# latex_appendices = []

# If false, no module index is generated.
# latex_domain_indices = True

# -- External mapping --------------------------------------------------------
python_version = ".".join(map(str, sys.version_info[0:2]))
intersphinx_mapping = {
    "sphinx": ("https://www.sphinx-doc.org/en/master", None),
    "python": ("https://docs.python.org/" + python_version, None),
    "matplotlib": ("https://matplotlib.org", None),
    "numpy": ("https://numpy.org/doc/stable", None),
    "sklearn": ("https://scikit-learn.org/stable", None),
    "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None),
    "scipy": ("https://docs.scipy.org/doc/scipy/reference", None),
    "setuptools": ("https://setuptools.pypa.io/en/stable/", None),
    "pyscaffold": ("https://pyscaffold.org/en/stable", None),
    "fastjsonschema": ("https://horejsek.github.io/python-fastjsonschema/", None),
    "pypa": ("https://packaging.python.org/en/latest/", None),
}
extlinks = {
    "issue": (f"{repository}/issues/%s", "issue #%s"),
    "pr": (f"{repository}/pull/%s", "PR #%s"),
    "discussion": (f"{repository}/discussions/%s", "discussion #%s"),
    "pypi": ("https://pypi.org/project/%s", "%s"),
    "github": ("https://github.com/%s", "%s"),
    "user": ("https://github.com/sponsors/%s", "@%s"),
}

print(f"loading configurations for {project} {version} ...", file=sys.stderr)
abravalheri-validate-pyproject-4b2e70d/docs/index.rst0000664000175000017500000000134315140154751022666 0ustar  carstencarsten==================
validate-pyproject
==================

**validate-pyproject** is a command line tool and Python library for validating
``pyproject.toml`` files based on JSON Schema, and includes checks for
:pep:`517`, :pep:`518`, :pep:`621`, :pep:`639`, and :pep:`735`.


Contents
========

.. toctree::
   :maxdepth: 2

   Overview 
   Schemas 
   Embedding it in your project 
   FAQ 

.. toctree::
   :caption: Project
   :maxdepth: 2

   Contributions & Help 
   Developer Guide 
   License 
   Authors 
   Changelog 
   Module Reference 


Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
abravalheri-validate-pyproject-4b2e70d/docs/_static/0000775000175000017500000000000015140154751022452 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/docs/_static/.gitignore0000664000175000017500000000002215140154751024434 0ustar  carstencarsten# Empty directory
abravalheri-validate-pyproject-4b2e70d/docs/_static/custom-adjustments.css0000664000175000017500000000262215140154751027037 0ustar  carstencarsten/**
 * The code in this module is mostly borrowed/adapted from PyScaffold and was originally
 * published under the MIT license
 * The original PyScaffold license can be found in 'NOTICE.txt'
 */

/* .row-odd td { */
/*   background-color: #f3f6f6 !important; */
/* } */

article .align-center:not(table) {
  display: block;
}

dl:not([class]) dt {
  color: var(--color-brand-content);
}

ol > li::marker {
  /* font-weight: bold; */
  color: var(--color-foreground-muted);
}

blockquote {
  background-color: var(--color-sidebar-background);
  border-left: solid 0.2rem var(--color-foreground-border);
  padding-left: 1rem;
}

blockquote p:first-child {
  margin-top: 0.1rem;
}

blockquote p:last-child {
  margin-bottom: 0.1rem;
}

.mobile-header,
.mobile-header.scrolled {
  border-bottom: solid 1px var(--color-background-border);
  box-shadow: none;
}

.section[id$="package"] h1 {
  color: var(--color-brand-content);
}

.section[id^="module"] h2 {
  color: var(--color-brand-primary);
  background-color: var(--color-brand-muted);
  border-top: solid 0.2rem var(--color-brand-primary);
  padding: 0.2rem 0.5rem;
  /* font-family: var(--font-stack--monospace); */
}

.section[id^="module"] h2:last-child {
  display: none;
}

.sidebar-tree .current-page > .reference {
    background: var(--color-brand-muted);
}

.py.class,
.py.exception,
.py.function,
.py.data {
  border-top: solid 0.2rem var(--color-brand-muted);
}
abravalheri-validate-pyproject-4b2e70d/docs/authors.rst0000664000175000017500000000005115140154751023237 0ustar  carstencarsten.. _authors:
.. include:: ../AUTHORS.rst
abravalheri-validate-pyproject-4b2e70d/docs/readme.rst0000664000175000017500000000004715140154751023014 0ustar  carstencarsten.. _readme:
.. include:: ../README.rst
abravalheri-validate-pyproject-4b2e70d/docs/dev-guide.rst0000664000175000017500000001443215140154751023433 0ustar  carstencarsten.. _dev-guide:

===============
Developer Guide
===============

This document describes the internal architecture and main concepts behind
``validate-pyproject`` and targets contributors and plugin writers.


.. _how-it-works:

How it works
============

``validate-pyproject`` relies mostly on a set of :doc:`specification documents
` represented as `JSON Schema`_.
To run the checks encoded under these schema files ``validate-pyproject``
uses the :pypi:`fastjsonschema` package.

This procedure is defined in the :mod:`~validate_pyproject.api` module,
specifically under the :class:`~validate_pyproject.api.Validator` class.
:class:`~validate_pyproject.api.Validator` objects use
:class:`~validate_pyproject.api.SchemaRegistry` instances to store references
to the JSON schema documents being used for the validation.
The :mod:`~validate_pyproject.formats` module is also important to this
process, since it defines how to validate the custom values for the
``"format"`` field defined in JSON Schema, for ``"string"`` values.

Checks for :pep:`517`, :pep:`518` and :pep:`621` are performed by default,
however these standards do not specify how the ``tool`` table and its subtables
are populated.

Since different tools allow different configurations, it would be impractical
to try to create schemas for all of them inside the same project.
Instead, ``validate-pyproject`` allows :ref:`plugins` to provide extra JSON Schemas,
against which ``tool`` subtables can be checked.


.. _plugins:

Plugins
=======

Plugins are a way of extending the built-in functionality of
``validate-pyproject``, can be simply described as functions that return
a JSON schema parsed as a Python :obj:`dict`:

.. code-block:: python

   def plugin(tool_name: str) -> dict:
       ...

These functions receive as argument the name of the tool subtable and should
return a JSON schema for the data structure **under** this table (it **should**
not include the table name itself as a property).

To use a plugin you can pass an ``extra_plugins`` argument to the
:class:`~validate_pyproject.api.Validator` constructor, but you will need to
wrap it with :class:`~validate_pyproject.plugins.PluginWrapper` to be able to
specify which ``tool`` subtable it would be checking:

.. code-block:: python

    from validate_pyproject import api


    def your_plugin(tool_name: str) -> dict:
        return {
            "$id": "https://your-urn-or-url",  # $id is mandatory
            "type": "object",
            "description": "Your tool configuration description",
            "properties": {
                "your-config-field": {"type": "string", "format": "python-module-name"}
            },
        }


    available_plugins = [
        plugins.PluginWrapper("your-tool", your_plugin),
    ]
    validator = api.Validator(extra_plugins=available_plugins)

Please notice that you can also make your plugin "autoloadable" by creating and
distributing your own Python package as described in the following section.

If you want to disable the automatic discovery of all "autoloadable" plugins you
can pass ``plugins=[]`` to the constructor; or, for example in the snippet
above, we could have used ``plugins=...`` instead of ``extra_plugins=...``
to ensure only the explicitly given plugins are loaded.


Distributing Plugins
--------------------

To distribute plugins, it is necessary to create a `Python package`_ with
a ``validate_pyproject.tool_schema`` entry-point_.

For the time being, if using setuptools_, this can be achieved by adding the following to your
``setup.cfg`` file:

.. code-block:: cfg

   # in setup.cfg
   [options.entry_points]
   validate_pyproject.tool_schema =
       your-tool = your_package.your_module:your_plugin

When using a :pep:`621`-compliant backend, the following can be add to your
``pyproject.toml`` file:

.. code-block:: toml

   # in pyproject.toml
   [project.entry-points."validate_pyproject.tool_schema"]
   your-tool = "your_package.your_module:your_plugin"

The plugin function will be automatically called with the ``tool_name``
argument as same name as given to the entrypoint (e.g. :samp:`your_plugin({"your-tool"})`).


Providing multiple schemas
--------------------------

A second system is defined for providing multiple schemas in a single plugin.
This is useful when a single plugin is responsible for multiple subtables
under the ``tool`` table, or if you need to provide multiple schemas for a
a single subtable.

To use this system, the plugin function, which does not take any arguments,
should return a dictionary with two keys: ``tools``, which is a dictionary of
tool names to schemas, and optionally ``schemas``, which is a list of schemas
that are not associated with any specific tool, but are loaded via ref's from
the other tools.

When using a :pep:`621`-compliant backend, the following can be add to your
``pyproject.toml`` file:

.. code-block:: toml

    # in pyproject.toml
    [project.entry-points."validate_pyproject.multi_schema"]
    arbitrary = "your_package.your_module:your_plugin"

An example of the plugin structure needed for this system is shown below:

.. code-block:: python

    def your_plugin(tool_name: str) -> dict:
        return {
            "tools": {"my-tool": my_schema},
            "schemas": [my_extra_schema],
        }

Fragments for schemas are also supported with this system; use ``#`` to split
the tool name and fragment path in the dictionary key.


.. admonition:: Experimental: Conflict Resolution

   Please notice that when two plugins define the same ``tool``
   (or auxiliary schemas with the same ``$id``),
   an internal conflict resolution heuristic is employed to decide
   which schema will take effect.

   To influence this heuristic you can:

   - Define a numeric ``.priority`` property in the functions
     pointed by the ``validate_pyproject.tool_schema`` entry-points.
   - Add a ``"priority"`` key with a numeric value into the dictionary
     returned by the ``validate_pyproject.multi_schema`` plugins.

   Typical values for ``priority`` are ``0`` and ``1``.

   The exact order in which the plugins are loaded is considered an
   implementation detail.


.. _entry-point: https://setuptools.pypa.io/en/stable/userguide/entry_point.html#entry-points
.. _JSON Schema: https://json-schema.org/
.. _Python package: https://packaging.python.org/
.. _setuptools: https://setuptools.pypa.io/en/stable/
abravalheri-validate-pyproject-4b2e70d/docs/faq.rst0000664000175000017500000000555515140154751022337 0ustar  carstencarsten===
FAQ
===


Why JSON Schema?
================

This design was initially inspired by an issue_ in the ``setuptools`` repository,
and brings a series of advantages and disadvantages.

Disadvantages include the fact that `JSON Schema`_ might be limited at times and
incapable of describing more complex checks. Additionally, error messages
produced by JSON Schema libraries might not be as pretty as the ones used
when bespoke validation is in place.

On the other hand, the fact that JSON Schema is standardised and have a
widespread usage among several programming language communities, means that a
bigger number of people can easily understand the schemas and modify them if
necessary.

Additionally, :pep:`518` already includes a JSON Schema representation, which
suggests that it can be used at the same time as specification language and
validation tool.


Why ``fastjsonschema``?
=======================

While there are other (more popular) `JSON Schema`_ libraries in the Python
community, none of the ones the original author of this package investigated
(other than :pypi:`fastjsonschema`) fulfilled the following requirements:

- Minimal number of dependencies (ideally 0)
- Easy to "vendorise", i.e. copy the source code of the package to be used
  directly without requiring installation.

:pypi:`fastjsonschema` has no dependency and can generate validation code directly,
which bypass the need for copying most of the files when :doc:`"embedding"
`.


Why draft-07 of JSON Schema and not a more modern version?
==========================================================

The most modern version of JSON Schema supported by :pypi:`fastjsonschema` is Draft 07.
It is not as bad as it may sound, it even supports `if-then-else`_-style conditions…


Why the URLs used as ``$id`` do not point to the schemas themselves?
====================================================================

According to the JSON Schema, the `$id keyword`_ is just a unique identifier
to differentiate between schemas and is not required to match a real URL.
The text on the standard is:

    Note that this URI is an identifier and not necessarily a network locator.
    In the case of a network-addressable URL, a schema need not be downloadable
    from its canonical URI.

This information is confirmed in a `similar document submitted to the IETF`_.


Where do I find information about *format* X?
=============================================

Please check :doc:`/api/validate_pyproject.formats`.


.. _if-then-else: https://json-schema.org/understanding-json-schema/reference/conditionals.html
.. _issue: https://github.com/pypa/setuptools/issues/2671
.. _JSON Schema: https://json-schema.org/
.. _$id keyword: https://json-schema.org/draft/2020-12/json-schema-core.html#name-the-id-keyword
.. _similar document submitted to the IETF: https://datatracker.ietf.org/doc/html/draft-wright-json-schema-01#section-8
abravalheri-validate-pyproject-4b2e70d/docs/json-schemas.rst0000664000175000017500000000107515140154751024153 0ustar  carstencarsten:orphan:

============
JSON Schemas
============

The following JSON schemas are used in ``validate-pyproject``.
Automatically generated documentation is also available on the
:doc:`schemas` page.

``pyproject.toml``
==================

.. literalinclude:: ../src/validate_pyproject/pyproject_toml.schema.json

``project`` table
=================

.. literalinclude:: ../src/validate_pyproject/project_metadata.schema.json

``tool`` table
==============

``tool.setuptools``
-------------------

.. literalinclude:: ../src/validate_pyproject/plugins/setuptools.schema.json
abravalheri-validate-pyproject-4b2e70d/docs/Makefile0000664000175000017500000000220215140154751022460 0ustar  carstencarsten# Makefile for Sphinx documentation
#

# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS    ?=
SPHINXBUILD   ?= sphinx-build
SOURCEDIR     = .
BUILDDIR      = _build
AUTODOCDIR    = api

# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $?), 1)
$(error "The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://sphinx-doc.org/")
endif

.PHONY: help clean Makefile

# Put it first so that "make" without argument is like "make help".
help:
	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

clean:
	rm -rf $(BUILDDIR)/* $(AUTODOCDIR)

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
abravalheri-validate-pyproject-4b2e70d/docs/schemas.rst0000664000175000017500000000201515140154751023177 0ustar  carstencarsten=======
Schemas
=======

The following sections represent the schemas used in ``validate-pyproject``.
They were automatically rendered via `sphinx-jsonschema`_ for quick reference.
In case of doubts or confusion, you can also have a look on the raw JSON files
in :doc:`json-schemas`.

.. _pyproject.toml:
.. jsonschema:: ../src/validate_pyproject/pyproject_toml.schema.json


.. _project_table:
.. jsonschema:: ../src/validate_pyproject/project_metadata.schema.json


``tool`` table
==============

According to :pep:`518`, tools can define their own configuration inside
``pyproject.toml`` by using custom subtables under ``tool``.

In ``validate-pyproject``, schemas for these subtables can be specified
via :ref:`plugins`. The following subtables are defined by *built-in* plugins
(i.e.  plugins that are included in the default distribution of
``validate-pyproject``):

.. _tool.setuptools:
.. jsonschema:: ../src/validate_pyproject/plugins/setuptools.schema.json


.. _sphinx-jsonschema: https://pypi.org/project/sphinx-jsonschema/
abravalheri-validate-pyproject-4b2e70d/docs/embedding.rst0000664000175000017500000000507715140154751023505 0ustar  carstencarsten=====================================
Embedding validations in your project
=====================================

``validate-pyproject`` can be used as a dependency in your project
in the same way you would use any other Python library,
i.e. by adding it to the same `virtual environment`_ you run your code in, or
by specifying it as a `project`_ or `library dependency`_ that
is automatically retrieved every time your project is installed.
Please check :ref:`this example ` for a quick overview on how to
use the Python API.

Alternatively, if you cannot afford having external dependencies in your
project you can also opt to *"vendorise"* [#vend1]_ ``validate-pyproject``.
This can be done automatically via tools such as :pypi:`vendoring` or
:pypi:`vendorize` and many others others, however this technique will copy
several files into your project.

However, if you want to keep the amount of files to a minimum,
``validate-pyproject`` offers a different solution that consists in
pre-compiling the JSON Schemas (thanks to :pypi:`fastjsonschema`).

After :ref:`installing ` ``validate-pyproject`` this can be done
via CLI as indicated in the command below:

.. code-block:: bash

    # in you terminal
    $ python -m validate_pyproject.pre_compile --help
    $ python -m validate_pyproject.pre_compile -O dir/for/generated_files

This command will generate a few files under the directory given to the CLI.
Please notice this directory should, ideally, be empty, and will correspond to
a "sub-package" in your package (a ``__init__.py`` file will be generated,
together with a few other ones).

Assuming you have created a ``generated_files`` directory, and that the value
for the ``--main-file`` option in the CLI was kept as the default
``__init__.py``, you should be able to invoke the validation function in your
code by doing:

.. code-block:: python

    from .generated_files import validate, ValidationError

    try:
        validate(dict_representing_the_parsed_toml_file)
    except ValidationError:
        print("Invalid File")


.. [#vend1] The words "vendorise" or "vendoring" in this text refer to the act
   of copying external dependencies to a folder inside your project, so they
   are distributed in the same package and can be used directly without relying
   on installation tools, such as :pypi:`pip`.


.. _project: https://packaging.python.org/tutorials/managing-dependencies/
.. _library dependency: https://setuptools.pypa.io/en/latest/userguide/dependency_management.html
.. _virtual environment: https://realpython.com/python-virtual-environments-a-primer/
abravalheri-validate-pyproject-4b2e70d/docs/contributing.rst0000664000175000017500000000004115140154751024260 0ustar  carstencarsten.. include:: ../CONTRIBUTING.rst
abravalheri-validate-pyproject-4b2e70d/docs/modules.rst.in0000664000175000017500000000117515140154751023637 0ustar  carstencarstenModule Reference
================

The public API of ``validate-pyproject`` is exposed in the :mod:`validate_pyproject.api` module.
Users may also import :mod:`validate_pyproject.errors` and :mod:`validate_pyproject.types`
when handling exceptions or specifying type hints.

In addition to that, special `formats `_
that can be used in the JSON Schema definitions are implemented in :mod:`validate_pyproject.formats`.

.. toctree::
   :maxdepth: 2

   validate_pyproject.api
   validate_pyproject.errors
   validate_pyproject.types
   validate_pyproject.formats
abravalheri-validate-pyproject-4b2e70d/docs/changelog.rst0000664000175000017500000000005315140154751023503 0ustar  carstencarsten.. _changes:
.. include:: ../CHANGELOG.rst
abravalheri-validate-pyproject-4b2e70d/docs/license.rst0000664000175000017500000000010315140154751023172 0ustar  carstencarsten.. _license:

=======
License
=======

.. include:: ../LICENSE.txt
abravalheri-validate-pyproject-4b2e70d/docs/_gendocs.py0000664000175000017500000000235615140154751023165 0ustar  carstencarsten"""``sphinx-apidoc`` only allows users to specify "exclude patterns" but not
"include patterns". This module solves that gap.
"""

import shutil
from pathlib import Path

MODULE_TEMPLATE = """
``{name}``
~~{underline}~~

.. automodule:: {name}
   :members:{_members}
   :undoc-members:
   :show-inheritance:
   :special-members: __call__
"""

__location__ = Path(__file__).parent


def gen_stubs(module_dir: str, output_dir: str):  # noqa: ARG001
    shutil.rmtree(output_dir, ignore_errors=True)  # Always start fresh
    out = Path(output_dir)
    out.mkdir(parents=True, exist_ok=True)
    manifest = shutil.copy(__location__ / "modules.rst.in", out / "modules.rst")
    for module in iter_public(manifest):
        text = module_template(module)
        Path(output_dir, f"{module}.rst").write_text(text, encoding="utf-8")


def iter_public(manifest):
    toc = Path(manifest).read_text(encoding="utf-8")
    lines = (x.strip() for x in toc.splitlines())
    return (x for x in lines if x.startswith("validate_pyproject."))


def module_template(name: str, *members: str) -> str:
    underline = "~" * len(name)
    _members = (" " + ", ".join(members)) if members else ""
    return MODULE_TEMPLATE.format(name=name, underline=underline, _members=_members)
abravalheri-validate-pyproject-4b2e70d/.ruff.toml0000664000175000017500000000357715140154751022025 0ustar  carstencarsten# --- General config ---
target-version = "py38"
exclude = ["tools/to_schemastore.py", "tests/invalid-examples"]

# --- Linting config ---
[lint]
extend-select = [
  "ARG",         # flake8-unused-arguments
  "B",           # flake8-bugbear
  "BLE",         # flake8-blind-except
  "C4",          # flake8-comprehensions
  "C90",         # McCabe cyclomatic complexity
  "DTZ",         # flake8-datetimez
  "EM",          # flake8-errmsg
  "EXE",         # flake8-executable
  "FA",          # flake8-future-annotations
  "FBT",         # flake8-boolean-trap
  "FLY",         # flynt
  "FURB",        # refurb
  "I",           # isort
  "ICN",         # flake8-import-conventions
  "INT",         # flake8-gettext
  "ISC",         # flake8-implicit-str-concat
  "LOG",         # flake8-logging
  "PERF",        # Perflint
  "PGH",         # pygrep-hooks
  "PIE",         # flake8-pie
  "PL",          # Pylint
  "PT",          # flake8-pytest-style
  "PYI",         # flake8-pyi
  "Q",           # flake8-quotes
  "RET",         # flake8-return
  "RSE",         # flake8-raise
  "RUF",         # Ruff-specific rules
  "S",           # flake8-bandit
  "SIM",         # flake8-simplify
  "SLOT",        # flake8-slots
  "T10",         # flake8-debugger
  "TC",          # flake8-type-checking
  "TCH",         # flake8-type-checking
  "TRY",         # tryceratops
  "UP",          # pyupgrade
  "YTT",         # flake8-2020
]
ignore = [
  "PLC0415",  # import at top of file
  "RSE102",   # parens on exception raise
  "S101",     # assert is used by mypy and pytest
  "TRY401",   # redundant logging message, TODO check
]

[lint.per-file-ignores]
"tests/*" = [
  "S",        # Assert okay in tests
  "PLR2004",  # Magic value comparison is actually desired in tests
]

# --- Tool-related config ---

[lint.isort]
known-third-party = ["validate_pyproject._vendor"]

[lint.pylint]
allow-magic-value-types = ["int", "str"]
abravalheri-validate-pyproject-4b2e70d/tests/0000775000175000017500000000000015140154751021236 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/test_formats.py0000664000175000017500000003514615140154751024333 0ustar  carstencarstenimport logging
import os
from itertools import chain
from unittest.mock import Mock

import pytest

from validate_pyproject import api, formats

_chain_iter = chain.from_iterable

# The following examples were taken by inspecting some opensource projects in the python
# community
ENTRYPOINT_EXAMPLES = {
    "django": {
        "console_scripts": {
            "django-admin": "django.core.management:execute_from_command_line"
        }
    },
    "pandas": {
        "pandas_plotting_backends": {"matplotlib": "pandas:plotting._matplotlib"},
    },
    "PyScaffold": {
        "console_scripts": {"putup": "pyscaffold.cli:run"},
        "pyscaffold.cli": {
            "config": "pyscaffold.extensions.config:Config",
            "interactive": "pyscaffold.extensions.interactive:Interactive",
            "venv": "pyscaffold.extensions.venv:Venv",
            "namespace": "pyscaffold.extensions.namespace:Namespace",
            "no_skeleton": "pyscaffold.extensions.no_skeleton:NoSkeleton",
            "pre_commit": "pyscaffold.extensions.pre_commit:PreCommit",
            "no_tox": "pyscaffold.extensions.no_tox:NoTox",
            "gitlab": "pyscaffold.extensions.gitlab_ci:GitLab",
            "cirrus": "pyscaffold.extensions.cirrus:Cirrus",
            "no_pyproject": "pyscaffold.extensions.no_pyproject:NoPyProject",
        },
    },
    "setuptools-scm": {
        "distutils.setup_keywords": {
            "use_scm_version": "setuptools_scm.integration:version_keyword",
        },
        "setuptools.file_finders": {
            "setuptools_scm": "setuptools_scm.integration:find_files",
        },
        "setuptools.finalize_distribution_options": {
            "setuptools_scm": "setuptools_scm.integration:infer_version",
        },
        "setuptools_scm.files_command": {
            ".hg": "setuptools_scm.file_finder_hg:hg_find_files",
            ".git": "setuptools_scm.file_finder_git:git_find_files",
        },
        "setuptools_scm.local_scheme": {
            "node-and-date": "setuptools_scm.version:get_local_node_and_date",
            "node-and-timestamp": "setuptools_scm.version:get_local_node_and_timestamp",
            "dirty-tag": "setuptools_scm.version:get_local_dirty_tag",
            "no-local-version": "setuptools_scm.version:get_no_local_node",
        },
        "setuptools_scm.parse_scm": {
            ".hg": "setuptools_scm.hg:parse",
            ".git": "setuptools_scm.git:parse",
        },
        "setuptools_scm.parse_scm_fallback": {
            ".hg_archival.txt": "setuptools_scm.hg:parse_archival",
            "PKG-INFO": "setuptools_scm.hacks:parse_pkginfo",
            "pip-egg-info": "setuptools_scm.hacks:parse_pip_egg_info",
            "setup.py": "setuptools_scm.hacks:fallback_version",
        },
        "setuptools_scm.version_scheme": {
            "guess-next-dev": "setuptools_scm.version:guess_next_dev_version",
            "post-release": "setuptools_scm.version:postrelease_version",
            "python-simplified-semver": "setuptools_scm.version:simplified_semver_version",
            "release-branch-semver": "setuptools_scm.version:release_branch_semver_version",
            "no-guess-dev": "setuptools_scm.version:no_guess_dev_version",
            "calver-by-date": "setuptools_scm.version:calver_by_date",
        },
    },
    "anyio": {
        "pytest11": {
            "anyio": "anyio.pytest_plugin",
        },
    },
}


@pytest.mark.parametrize(
    "example", _chain_iter(v.keys() for v in ENTRYPOINT_EXAMPLES.values())
)
def test_entrypoint_group(example):
    assert formats.python_entrypoint_group(example)


@pytest.mark.parametrize(
    "example",
    _chain_iter(
        _chain_iter(e.keys() for e in v.values()) for v in ENTRYPOINT_EXAMPLES.values()
    ),
)
def test_entrypoint_name(example):
    assert formats.python_entrypoint_name(example)


@pytest.mark.parametrize("example", [" invalid", "=invalid", "[invalid]", "[invalid"])
def test_entrypoint_invalid_name(example):
    assert formats.python_entrypoint_name(example) is False


@pytest.mark.parametrize("example", ["val[id", "also valid"])
def test_entrypoint_name_not_recommended(example, caplog):
    caplog.set_level(logging.WARNING)
    assert formats.python_entrypoint_name(example) is True
    assert "does not follow recommended pattern" in caplog.text


@pytest.mark.parametrize(
    "example",
    _chain_iter(
        _chain_iter(e.values() for e in v.values())
        for v in ENTRYPOINT_EXAMPLES.values()
    ),
)
def test_entrypoint_references(example):
    assert formats.python_entrypoint_reference(example)
    assert formats.pep517_backend_reference(example)
    assert formats.pep517_backend_reference(example.replace(":", "."))


def test_entrypoint_references_with_extras():
    example = "test.module:func [invalid"
    assert formats.python_entrypoint_reference(example) is False

    example = "test.module:func [valid]"
    assert formats.python_entrypoint_reference(example)
    assert formats.pep517_backend_reference(example) is False

    example = "test.module:func [valid, extras]"
    assert formats.python_entrypoint_reference(example)

    example = "test.module:func [??inva#%@!lid??]"
    assert formats.python_entrypoint_reference(example) is False


@pytest.mark.parametrize("example", ["module", "invalid-module"])
def test_invalid_entrypoint_references(example):
    result = example == "module"
    assert formats.python_entrypoint_reference(example) is result


@pytest.mark.parametrize("example", ["λ", "a", "_"])
def test_valid_python_identifier(example):
    assert formats.python_identifier(example)


@pytest.mark.parametrize("example", ["a.b", "x+y", " a", "☺"])
def test_invalid_python_identifier(example):
    assert formats.python_identifier(example) is False


@pytest.mark.parametrize(
    "example",
    [
        "0.9.10",
        "1988.12",
        "1.01rc1",
        "0.99a9",
        "3.14b5",
        "1.42.post0",
        "1.73a2.post0",
        "2.23.post6.dev0",
        "3!6.0",
        "1.0+abc.7",
        "v4.0.1",
    ],
)
def test_valid_pep440(example):
    assert formats.pep440(example)


@pytest.mark.parametrize(
    "example",
    [
        "0-9-10",
        "v4.0.1.mysuffix",
        "p4.0.2",
    ],
)
def test_invalid_pep440(example):
    assert formats.pep440(example) is False


@pytest.mark.parametrize(
    "example",
    [
        "~= 0.9, >= 1.0, != 1.3.4.*, < 2.0",
        ">= 1.4.5, == 1.4.*",
        "~= 2.2.post3",
        "!= 1.1.post1",
    ],
)
def test_valid_pep508_versionspec(example):
    assert formats.pep508_versionspec(example)


@pytest.mark.parametrize(
    "example",
    [
        "~ 0.9, ~> 1.0, - 1.3.4.*",
        "- 1.3.4.*",
        "~> 1.0",
        "~ 0.9",
        "@ file:///localbuilds/pip-1.3.1.zip",
        'v1.0; python_version<"2.7"',
    ],
)
def test_invalid_pep508_versionspec(example):
    assert formats.pep508_versionspec(example) is False


@pytest.mark.parametrize(
    "example",
    [
        "https://python.org",
        "http://python.org",
        "http://localhost:8000",
        "ftp://python.org",
        "scheme://netloc/path;parameters?query#fragment",
    ],
)
def test_valid_url(example):
    assert formats.url(example)


@pytest.mark.parametrize(
    "example",
    [
        "",
        42,
        "p@python.org",
        "http:python.org",
        "/python.org",
    ],
)
def test_invalid_url(example):
    assert formats.url(example) is False


@pytest.mark.parametrize(
    "example",
    [
        "ab",
        "ab.c.d",
        "abc._d.λ",
    ],
)
def test_valid_module_name(example):
    assert formats.python_module_name(example) is True


@pytest.mark.parametrize(
    "example",
    [
        "-",
        " ",
        "ab-cd",
        ".example",
    ],
)
def test_invalid_module_name(example):
    assert formats.python_module_name(example) is False


@pytest.mark.parametrize(
    "example",
    [
        "pip-run",
        "abc-d-λ",
        "abc-d-λ.xyz-e",
        "abc-d.λ-xyz-e",
    ],
)
def test_valid_module_name_relaxed(example):
    assert formats.python_module_name_relaxed(example) is True


@pytest.mark.parametrize(
    "example",
    [
        "pip run",
        "-pip-run",
        "pip-run-",
        "pip-run-stubs",
    ],
)
def test_invalid_module_name_relaxed(example):
    assert formats.python_module_name_relaxed(example) is False


@pytest.mark.parametrize(
    "example",
    [
        "MIT",
        "Bsd-3-clause",
        "mit and (apache-2.0 or bsd-2-clause)",
        "MIT OR GPL-2.0-or-later OR (FSFUL AND BSD-2-Clause)",
        "GPL-3.0-only WITH Classpath-exception-2.0 OR BSD-3-Clause",
        "LicenseRef-Special-License OR CC0-1.0 OR Unlicense",
        "LicenseRef-Public-Domain",
        "licenseref-proprietary",
        "LicenseRef-Beerware-4.2",
        "(LicenseRef-Special-License OR LicenseRef-OtherLicense) OR Unlicense",
    ],
)
def test_valid_pep639_license_expression(example):
    assert formats.SPDX(example) is True


@pytest.mark.parametrize(
    "example",
    [
        "",
        "Use-it-after-midnight",
        "LicenseRef-License with spaces",
        "LicenseRef-License_with_underscores",
        "or",
        "and",
        "with",
        "mit or",
        "mit and",
        "mit with",
        "or mit",
        "and mit",
        "with mit",
        "(mit",
        "mit)",
        "mit or or apache-2.0",
        # Missing an operator before `(`.
        "mit or apache-2.0 (bsd-3-clause and MPL-2.0)",
        # "2-BSD-Clause is not a valid license.
        "Apache-2.0 OR 2-BSD-Clause",
    ],
)
def test_invalid_pep639_license_expression(example):
    assert formats.SPDX(example) is False


class TestClassifiers:
    """The ``_TroveClassifier`` class and ``_download_classifiers`` are part of the
    private API and therefore need to be tested.

    By constantly testing them we can make sure the URL used to download classifiers and
    the format they are presented are still supported by PyPI.

    If at any point these tests start to fail, we know that we need to change strategy.
    """

    VALID_CLASSIFIERS = (
        "Development Status :: 5 - Production/Stable",
        "Framework :: Django",
        "Operating System :: POSIX",
        "Programming Language :: Python :: 3 :: Only",
        "private :: not really a classifier",
    )

    def test_does_not_break_public_function_detection(self):
        # See https://github.com/abravalheri/validate-pyproject/issues/12

        # When `trove_classifiers` is defined from the dependency package
        # it will be a function.
        # When it is defined based on the download, it will be a custom object.

        # In both cases the `_TroveClassifier` class should not be made public,
        # but the instance should

        trove_classifier = formats._TroveClassifier()
        _formats = Mock(
            _TroveClassifier=formats._TroveClassifier,
            trove_classifiers=trove_classifier,
        )
        fns = api._get_public_functions(_formats)
        assert fns == {"trove-classifier": trove_classifier}

        # Make sure the object and the function have the same name
        assert "trove-classifier" in api.FORMAT_FUNCTIONS
        normalized_name = trove_classifier.__name__.replace("_", "-")
        assert normalized_name == "trove-classifier"
        assert normalized_name in api.FORMAT_FUNCTIONS

        func_name = trove_classifier.__name__
        assert getattr(formats, func_name) in api.FORMAT_FUNCTIONS.values()

    def test_download(self):
        try:
            classifiers = formats._download_classifiers()
        except Exception as ex:  # noqa: BLE001
            pytest.xfail(f"Error with download: {ex.__class__.__name__} - {ex}")
        assert isinstance(classifiers, str)
        assert bytes(classifiers, "utf-8")

    def test_downloaded(self, monkeypatch):
        if os.name != "posix":
            # Mock on Windows (problems with SSL)
            downloader = Mock(return_value="\n".join(self.VALID_CLASSIFIERS))
            monkeypatch.setattr(formats, "_download_classifiers", downloader)

        validator = formats._TroveClassifier()
        assert validator("Made Up :: Classifier") is False
        assert validator.downloaded is not None
        assert validator.downloaded is not False
        assert len(validator.downloaded) > 3

    def test_valid_download_only_once(self, monkeypatch):
        if os.name == "posix":
            # Really download to make sure the API is still exposed by PyPI
            downloader = Mock(side_effect=formats._download_classifiers)
        else:
            # Mock on Windows (problems with SSL)
            downloader = Mock(return_value="\n".join(self.VALID_CLASSIFIERS))

        monkeypatch.setattr(formats, "_download_classifiers", downloader)
        validator = formats._TroveClassifier()
        for classifier in self.VALID_CLASSIFIERS:
            assert validator(classifier) is True
        downloader.assert_called_once()

    @pytest.mark.parametrize(
        "no_network", ["NO_NETWORK", "VALIDATE_PYPROJECT_NO_NETWORK"]
    )
    def test_always_valid_with_no_network(self, monkeypatch, no_network):
        monkeypatch.setenv(no_network, "1")
        validator = formats._TroveClassifier()
        assert validator("Made Up :: Classifier") is True
        assert not validator.downloaded
        assert validator("Other Made Up :: Classifier") is True
        assert not validator.downloaded

    def test_always_valid_with_skip_download(self):
        validator = formats._TroveClassifier()
        validator._disable_download()
        assert validator("Made Up :: Classifier") is True
        assert not validator.downloaded
        assert validator("Other Made Up :: Classifier") is True
        assert not validator.downloaded

    def test_always_valid_after_download_error(self, monkeypatch):
        def _failed_download():
            raise OSError()

        monkeypatch.setattr(formats, "_download_classifiers", _failed_download)
        validator = formats._TroveClassifier()
        assert validator("Made Up :: Classifier") is True
        assert not validator.downloaded
        assert validator("Other Made Up :: Classifier") is True
        assert not validator.downloaded


def test_private_classifier():
    assert formats.trove_classifier("private :: Keep Off PyPI") is True
    assert formats.trove_classifier("private:: Keep Off PyPI") is False


def test_import_name():
    assert formats.import_name("simple")
    assert formats.import_name("some1; private")
    assert formats.import_name("other.thing ; private")
    assert formats.import_name("_other._thing ; private")

    assert not formats.import_name("one two")
    assert not formats.import_name("one; two")
    assert not formats.import_name("1thing")
    assert not formats.import_name("for")
    assert not formats.import_name("thing.is.keyword")
abravalheri-validate-pyproject-4b2e70d/tests/__init__.py0000664000175000017500000000000015140154751023335 0ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/helpers.py0000664000175000017500000000270715140154751023260 0ustar  carstencarstenfrom __future__ import annotations

import functools
import json
from pathlib import Path

from validate_pyproject.remote import RemotePlugin, load_store

HERE = Path(__file__).parent.resolve()


def error_file(p: Path) -> Path:
    try:
        files = (p.with_name("errors.txt"), p.with_suffix(".errors.txt"))
        return next(f for f in files if f.exists())
    except StopIteration:
        msg = f"No error file found for {p}"
        raise FileNotFoundError(msg) from None


def get_test_config(example: Path) -> dict[str, str | dict[str, str]]:
    test_config = example.with_name("test_config.json")
    if test_config.is_file():
        with test_config.open(encoding="utf-8") as f:
            return json.load(f)
    return {}


@functools.lru_cache(maxsize=None)
def get_tools(example: Path) -> list[RemotePlugin]:
    config = get_test_config(example)
    tools: dict[str, str] = config.get("tools", {})
    load_tools = [RemotePlugin.from_url(k, v) for k, v in tools.items()]
    store: str = config.get("store", "")
    if store:
        load_tools.extend(load_store(store))
    return load_tools


@functools.lru_cache(maxsize=None)
def get_tools_as_args(example: Path) -> list[str]:
    config = get_test_config(example)
    tools: dict[str, str] = config.get("tools", {})
    load_tools = [f"--tool={k}={v}" for k, v in tools.items()]
    store: str = config.get("store", "")
    if store:
        load_tools.append(f"--store={store}")
    return load_tools
abravalheri-validate-pyproject-4b2e70d/tests/test_vendoring.py0000664000175000017500000000071515140154751024645 0ustar  carstencarstenimport pytest

from validate_pyproject.vendoring import cli, vendorify


def test_api(tmp_path):
    with pytest.warns(DeprecationWarning, match="will be removed"):
        vendorify(tmp_path)


def test_cli(tmp_path):
    with pytest.warns(DeprecationWarning, match="will be removed"):
        cli.run(["-O", str(tmp_path)])


def test_main(tmp_path):
    with pytest.warns(DeprecationWarning, match="will be removed"):
        cli.main(["-O", str(tmp_path)])
abravalheri-validate-pyproject-4b2e70d/tests/test_cli.py0000664000175000017500000001576015140154751023427 0ustar  carstencarstenimport inspect
import io
import logging
import sys
from pathlib import Path
from unittest.mock import Mock
from uuid import uuid4

import pytest
from fastjsonschema import JsonSchemaValueException

from validate_pyproject import cli, errors, plugins


class TestHelp:
    def test_list_default_plugins(self, capsys):
        with pytest.raises(SystemExit):
            cli.main(["--help"])
        captured = capsys.readouterr()
        assert "setuptools" in captured.out
        assert "distutils" in captured.out

    def test_no_plugins(self, capsys):
        with pytest.raises(SystemExit):
            cli.parse_args(["--help"], plugins=[])
        captured = capsys.readouterr()
        assert "setuptools" not in captured.out
        assert "distutils" not in captured.out

    def test_custom_plugins(self, capsys):
        fake_plugin = plugins.PluginWrapper("my42", lambda _: {})
        with pytest.raises(SystemExit):
            cli.parse_args(["--help"], plugins=[fake_plugin])
        captured = capsys.readouterr()
        assert "my42" in captured.out


def parse_args(args):
    plg = plugins.list_from_entry_points()
    return cli.parse_args(args, plg)


simple_example = """\
[project]
name = "myproj"
version = "0"

[tool.setuptools]
zip-safe = false
packages = {find = {}}
"""


def write_example(dir_path, *, name="pyproject.toml", _text=simple_example):
    path = Path(dir_path, name)
    path.write_text(_text, "UTF-8")
    return path


def write_invalid_example(dir_path, *, name="pyproject.toml"):
    text = simple_example.replace("zip-safe = false", "zip-safe = { hello = 'world' }")
    return write_example(dir_path, name=name, _text=text)


@pytest.fixture
def valid_example(tmp_path):
    return write_example(tmp_path)


@pytest.fixture
def invalid_example(tmp_path):
    return write_invalid_example(tmp_path)


class TestEnable:
    TOOLS = ("setuptools", "distutils")

    @pytest.mark.parametrize("tool", TOOLS)
    def test_parse(self, valid_example, tool):
        params = parse_args([str(valid_example), "-E", tool])
        assert len(params.plugins) == 1
        assert params.plugins[0].tool == tool

        # Meta test:
        schema = params.plugins[0].schema
        if tool == "setuptools":
            assert "zip-safe" in schema["properties"]
            assert schema["properties"]["zip-safe"]["type"] == "boolean"

    def test_valid(self, valid_example):
        assert cli.main([str(valid_example), "-E", "setuptools"]) == 0

    def test_invalid(self, invalid_example):
        print(invalid_example.read_text())
        with pytest.raises(JsonSchemaValueException):
            cli.run([str(invalid_example), "-E", "setuptools"])

    def test_invalid_not_enabled(self, invalid_example):
        # When the plugin is not enabled, the validator should ignore the tool
        assert cli.main([str(invalid_example), "-E", "distutils"]) == 0


class TestDisable:
    TOOLS = ("setuptools", "distutils")

    @pytest.mark.parametrize(("tool", "other_tool"), zip(TOOLS, reversed(TOOLS)))
    def test_parse(self, valid_example, tool, other_tool):
        all_plugins = parse_args([str(valid_example), "-D", tool]).plugins
        our_plugins = [p for p in all_plugins if p.id.startswith("validate_pyproject")]
        assert len(our_plugins) == 1
        assert our_plugins[0].tool == other_tool

    def test_valid(self, valid_example):
        assert cli.run([str(valid_example), "-D", "distutils"]) == 0

    def test_invalid(self, invalid_example):
        print(invalid_example.read_text())
        with pytest.raises(JsonSchemaValueException):
            cli.run([str(invalid_example), "-D", "distutils"])

    def test_invalid_disabled(self, invalid_example):
        # When the plugin is disabled, the validator should ignore the tool
        assert cli.main([str(invalid_example), "-D", "setuptools"]) == 0


class TestInput:
    def test_inform_user_about_stdin(self, monkeypatch):
        print_mock = Mock()
        fake_stdin = io.StringIO('[project]\nname="test"\nversion="0.42"\n')
        with monkeypatch.context() as ctx:
            ctx.setattr("validate_pyproject.cli._STDIN", fake_stdin)
            ctx.setattr("sys.argv", ["validate-pyproject"])
            ctx.setattr("builtins.print", print_mock)
            cli.run()
        calls = print_mock.call_args_list
        assert any("input via `stdin`" in str(args[0]) for args, _kwargs in calls)


class TestOutput:
    def test_valid(self, capsys, valid_example):
        cli.main([str(valid_example)])
        captured = capsys.readouterr()
        assert "valid" in captured.out.lower()

    def test_invalid(self, caplog, invalid_example):
        caplog.set_level(logging.DEBUG)
        with pytest.raises(SystemExit):
            cli.main([str(invalid_example)])
        captured = caplog.text.lower()
        assert "`tool.setuptools.zip-safe` must be boolean" in captured
        assert "offending rule" in captured
        assert "given value" in captured
        assert '"type": "boolean"' in captured


def test_multiple_files(tmp_path, capsys):
    N = 3

    valid_files = [
        write_example(tmp_path, name=f"valid-pyproject{i}.toml") for i in range(N)
    ]
    cli.run(map(str, valid_files))
    captured = capsys.readouterr().out.lower()
    number_valid = captured.count("valid file:")
    assert number_valid == N

    invalid_files = [
        write_invalid_example(tmp_path, name=f"invalid-pyproject{i}.toml")
        for i in range(N + 3)
    ]
    with pytest.raises(SystemExit):
        cli.main(map(str, valid_files + invalid_files))

    repl = str(uuid4())
    captured = capsys.readouterr().out.lower()
    captured = captured.replace("invalid file:", repl)
    number_invalid = captured.count(repl)
    number_valid = captured.count("valid file:")
    captured = captured.replace(repl, "invalid file:")
    assert number_valid == N
    assert number_invalid == N + 3


def test_missing_toolname(tmp_path):
    example = write_example(tmp_path, name="valid-pyproject.toml")
    with pytest.raises(
        errors.URLMissingTool,
        match=r"Correct form is '--tool =http://json\.schemastore\.org/poetry\.toml', with an optional",
    ):
        cli.run(["--tool=http://json.schemastore.org/poetry.toml", str(example)])


def test_bad_url(tmp_path):
    example = write_example(tmp_path, name="valid-pyproject.toml")
    with pytest.raises(ValueError, match="URL must start with 'http:' or 'https:'"):
        cli.run(
            ["--tool", "poetry=file://json.schemastore.org/poetry.toml", str(example)]
        )


def test_bad_extra_url(tmp_path):
    example = write_example(tmp_path, name="valid-pyproject.toml")
    with pytest.raises(ValueError, match="URL must start with 'http:' or 'https:'"):
        cli.run(["--tool", "=file://json.schemastore.org/poetry.toml", str(example)])


@pytest.mark.skipif(sys.version_info[:2] < (3, 11), reason="requires 3.11+")
def test_parser_is_tomllib():
    """Make sure Python >= 3.11 uses tomllib instead of tomli"""
    module_name = inspect.getmodule(cli.tomllib.loads).__name__
    assert module_name.startswith("tomllib")
abravalheri-validate-pyproject-4b2e70d/tests/test_json_schema_summary.py0000664000175000017500000000151715140154751026721 0ustar  carstencarsten"""Test summary generation from schema examples"""

import json
from pathlib import Path

import pytest

from validate_pyproject.error_reporting import _SummaryWriter

EXAMPLE_FOLDER = Path(__file__).parent / "json_schema_summary"
EXAMPLES = (p.name for p in EXAMPLE_FOLDER.glob("*"))


def load_example(file):
    text = file.read_text(encoding="utf-8")
    schema, _, summary = text.partition("# - # - # - #\n")

    # # Auto fix examples:
    # fixed = _SummaryWriter()(json.loads(schema))
    # file.write_text(text.replace(summary, fixed), encoding="utf-8")

    return json.loads(schema), summary


@pytest.mark.parametrize("example", EXAMPLES)
def test_summary_generation(example):
    schema, expected = load_example(EXAMPLE_FOLDER / example)
    summarize = _SummaryWriter()
    summary = summarize(schema)
    assert summary == expected
abravalheri-validate-pyproject-4b2e70d/tests/examples/0000775000175000017500000000000015140154751023054 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/examples/flit/0000775000175000017500000000000015140154751024012 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/examples/flit/LICENSE0000664000175000017500000000276515140154751025031 0ustar  carstencarstenCopyright (c) 2015, Thomas Kluyver and contributors
All rights reserved.

BSD 3-clause license:

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
abravalheri-validate-pyproject-4b2e70d/tests/examples/flit/pyproject.toml0000664000175000017500000000166215140154751026733 0ustar  carstencarsten[build-system]
requires = ["flit_core >=3.4.0,<4"]
build-backend = "flit_core.buildapi"

[project]
name = "flit"
authors = [
    {name = "Thomas Kluyver", email = "thomas@kluyver.me.uk"},
]
dependencies = [
    "flit_core >=3.4.0",
    "requests",
    "docutils",
    "tomli",
    "tomli-w",
]
requires-python = ">=3.6"
readme = "README.rst"
classifiers = ["Intended Audience :: Developers",
    "License :: OSI Approved :: BSD License",
    "Programming Language :: Python :: 3",
    "Topic :: Software Development :: Libraries :: Python Modules",
]
dynamic = ['version', 'description']

[project.optional-dependencies]
test = [
	"testpath",
	"responses",
	"pytest>=2.7.3",
	"pytest-cov",
]
doc = [
	"sphinx",
	"sphinxcontrib_github_alt",
	"pygments-github-lexers",  # TOML highlighting
]

[project.urls]
Documentation = "https://flit.readthedocs.io/en/latest/"
Source = "https://github.com/takluyver/flit"

[project.scripts]
flit = "flit:main"
abravalheri-validate-pyproject-4b2e70d/tests/examples/simple/0000775000175000017500000000000015140154751024345 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/examples/simple/pep639.toml0000664000175000017500000000025615140154751026273 0ustar  carstencarsten[project]
name = "example"
version = "1.2.3"
license = "MIT OR GPL-2.0-or-later OR (FSFUL AND BSD-2-Clause)"
license-files = ["licenses/LICENSE.MIT", "licenses/LICENSE.CC0"]
abravalheri-validate-pyproject-4b2e70d/tests/examples/simple/depgroups.toml0000664000175000017500000000012415140154751027247 0ustar  carstencarsten[dependency-groups]
test = ["one", "two"]
other = ["one", {include-group = "test"}]
abravalheri-validate-pyproject-4b2e70d/tests/examples/simple/import_names.toml0000664000175000017500000000020315140154751027732 0ustar  carstencarsten[project]
name = "hi"
version = "1.0.0"
import-names = ["one", "one.two", "two; private"]
import-namespaces = ["_other ; private"]
abravalheri-validate-pyproject-4b2e70d/tests/examples/simple/dynamic-version.toml0000664000175000017500000000005615140154751030352 0ustar  carstencarsten[project]
name = "spam"
dynamic = ["version"]
abravalheri-validate-pyproject-4b2e70d/tests/examples/simple/empty-author.toml0000664000175000017500000000006415140154751027700 0ustar  carstencarsten[project]
name = 'foo'
version = '1.0'
authors = []
abravalheri-validate-pyproject-4b2e70d/tests/examples/simple/minimal.toml0000664000175000017500000000005515140154751026670 0ustar  carstencarsten[project]
name = "spam"
version = "2020.0.0"
abravalheri-validate-pyproject-4b2e70d/tests/examples/store/0000775000175000017500000000000015140154751024210 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/examples/store/test_config.json0000664000175000017500000000007715140154751027413 0ustar  carstencarsten{
    "store": "https://json.schemastore.org/pyproject.json"
}
abravalheri-validate-pyproject-4b2e70d/tests/examples/store/example.toml0000664000175000017500000000427715140154751026552 0ustar  carstencarsten[tool.ruff]
src = ["src"]

[tool.ruff.lint]
extend-select = [
  "B",           # flake8-bugbear
  "I",           # isort
  "ARG",         # flake8-unused-arguments
  "C4",          # flake8-comprehensions
  "EM",          # flake8-errmsg
  "ICN",         # flake8-import-conventions
  "G",           # flake8-logging-format
  "PGH",         # pygrep-hooks
  "PIE",         # flake8-pie
  "PL",          # pylint
  "PT",          # flake8-pytest-style
  "PTH",         # flake8-use-pathlib
  "RET",         # flake8-return
  "RUF",         # Ruff-specific
  "SIM",         # flake8-simplify
  "T20",         # flake8-print
  "UP",          # pyupgrade
  "YTT",         # flake8-2020
  "EXE",         # flake8-executable
  "NPY",         # NumPy specific rules
  "PD",          # pandas-vet
  "FURB",        # refurb
  "PYI",         # flake8-pyi
]
ignore = [
  "PLR",    # Design related pylint codes
]
typing-modules = ["mypackage._compat.typing"]
isort.required-imports = ["from __future__ import annotations"]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["T20"]


[tool.cibuildwheel]
build = "*"
skip = ""
test-skip = ""

archs = ["auto"]
build-frontend = "default"
config-settings = {}
dependency-versions = "pinned"
environment = {}
environment-pass = []
build-verbosity = 0

before-all = ""
before-build = ""
repair-wheel-command = ""

test-command = ""
before-test = ""
test-requires = []
test-extras = []

container-engine = "docker"

manylinux-x86_64-image = "manylinux2014"
manylinux-i686-image = "manylinux2014"
manylinux-aarch64-image = "manylinux2014"
manylinux-ppc64le-image = "manylinux2014"
manylinux-s390x-image = "manylinux2014"
manylinux-pypy_x86_64-image = "manylinux2014"
manylinux-pypy_i686-image = "manylinux2014"
manylinux-pypy_aarch64-image = "manylinux2014"

musllinux-x86_64-image = "musllinux_1_1"
musllinux-i686-image = "musllinux_1_1"
musllinux-aarch64-image = "musllinux_1_1"
musllinux-ppc64le-image = "musllinux_1_1"
musllinux-s390x-image = "musllinux_1_1"


[tool.cibuildwheel.linux]
repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}"

[tool.cibuildwheel.macos]
repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}"

[tool.cibuildwheel.windows]
abravalheri-validate-pyproject-4b2e70d/tests/examples/setuptools/0000775000175000017500000000000015140154751025275 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/examples/setuptools/10-pyproject.toml0000664000175000017500000000024615140154751030431 0ustar  carstencarsten[project]
name = "myproj"
version = "42"
dynamic = ["optional-dependencies"]

[tool.setuptools.dynamic.optional-dependencies]
name-with-hyfens = {file = "extra.txt"}
abravalheri-validate-pyproject-4b2e70d/tests/examples/setuptools/12-pyproject.toml0000664000175000017500000000012115140154751030423 0ustar  carstencarsten[[tool.setuptools.ext-modules]]
name = "my.ext"
sources = ["hello.c", "world.c"]
abravalheri-validate-pyproject-4b2e70d/tests/examples/setuptools/08-pyproject.toml0000664000175000017500000000073715140154751030445 0ustar  carstencarsten# Setuptools should allow stub-only package names in `packages` (PEP 561)
[build-system]
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"

[project]
name = "mypkg-stubs"
version = "0.0.0"

[tool.setuptools]
platforms = ["any"]
packages = ["mypkg-stubs"]

[tool.setuptools.package-dir]
"" = "src"

[tool.setuptools.package-data]
"*" = ["*.pyi"]
"mypkg-stubs" = ["METADATA.toml"]

[tool.setuptools.exclude-package-data]
"mypkg-stubs" = ["*.rst"]
abravalheri-validate-pyproject-4b2e70d/tests/examples/setuptools/06-pyproject.toml0000664000175000017500000000212115140154751030430 0ustar  carstencarsten[project]
name = "myproj"
keywords = ["some", "key", "words"]
dynamic = ["version"]
requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
dependencies = [
    "importlib-metadata>=0.12;python_version<\"3.8\"",
    "importlib-resources>=1.0;python_version<\"3.7\"",
    "pathlib2>=2.3.3,<3;python_version < '3.4' and sys.platform != 'win32'",
]

[project.readme]
file = "README.md"
content-type = "text/markdown"

[project.optional-dependencies]
docs = [
    "sphinx>=3",
    "sphinx-argparse>=0.2.5",
    "sphinx-rtd-theme>=0.4.3",
]
testing = [
    "pytest>=1",
    "coverage>=3,<5",
]

[project.scripts]
exec = "myproj.__main__:exec"

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
package-dir = {"" = "src"}
zip-safe = true
platforms = ["any"]

[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.cmdclass]
sdist = "pkg.mod.CustomSdist"

[tool.setuptools.package-data]
"myproj.bash" = ["*.sh"]
"myproj.yaml" = ["*.yml"]

[tool.distutils.sdist]
formats = "gztar"

[tool.distutils.bdist_wheel]
universal = true
abravalheri-validate-pyproject-4b2e70d/tests/examples/setuptools/readme-pyproject.toml0000664000175000017500000000007615140154751031447 0ustar  carstencarsten[tool.setuptools]
dynamic.readme = { "file" = ["README.md"] }
abravalheri-validate-pyproject-4b2e70d/tests/examples/setuptools/05-pyproject.toml0000664000175000017500000000103215140154751030427 0ustar  carstencarsten[project]
name = "myproj"
version = "3.8"
readme = "README.rst"
urls = {Homepage = "https://github.com/me/myproj/"}
requires-python = ">=3.6"
dependencies = ["dep>=1.0.0"]

[project.entry-points]
"distutils.setup_keywords" = {use_scm_version = "myproj.setuptools:version_keyword"}

[project.optional-dependencies]
toml = ["dep>=1.0.0"]

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
package-dir = {"" = "src"}
zip-safe = true

[tool.setuptools.packages.find]
where = ["src"]
abravalheri-validate-pyproject-4b2e70d/tests/examples/setuptools/07-pyproject.toml0000664000175000017500000000255015140154751030437 0ustar  carstencarsten[project]
name = "myproj"
keywords = ["some", "key", "words"]
license = {text = "MIT"}
dynamic = [
    "version",
    "description",
    "readme",
    "entry-points",
    "gui-scripts"
]
requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
dependencies = [
    'importlib-metadata>=0.12;python_version<"3.8"',
    'importlib-resources>=1.0;python_version<"3.7"',
    'pathlib2>=2.3.3,<3;python_version < "3.4" and sys.platform != "win32"',
]

[project.optional-dependencies]
docs = [
    "sphinx>=3",
    "sphinx-argparse>=0.2.5",
    "sphinx-rtd-theme>=0.4.3",
]
testing = [
    "pytest>=1",
    "coverage>=3,<5",
]

[project.scripts]
exec = "pkg.__main__:exec"

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
package-dir = {"" = "src"}
zip-safe = true
platforms = ["any"]
license-files = ["LICENSE*", "NOTICE*"]

[tool.setuptools.packages.find]
where = ["src"]
namespaces = true

[tool.setuptools.cmdclass]
sdist = "pkg.mod.CustomSdist"

[tool.setuptools.dynamic]
version = {attr = "pkg.__version__.VERSION"}
description = {file = ["README.md"]}
readme = {file = ["README.md"], content-type = "text/markdown"}

[tool.setuptools.package-data]
"*" = ["*.txt"]

[tool.setuptools.data-files]
"data" = ["files/*.txt"]

[tool.distutils.sdist]
formats = "gztar"

[tool.distutils.bdist_wheel]
universal = true
abravalheri-validate-pyproject-4b2e70d/tests/examples/setuptools/02-pyproject.toml0000664000175000017500000000232115140154751030426 0ustar  carstencarsten[project]
name = "package"
description = "description"
authors = [{ name = "Name", email = "email@example.com" }]
readme = "README.rst"
classifiers = [
    "Development Status :: 2 - Pre-Alpha",
    "Environment :: Web Environment",
]
dynamic = ["version"]
requires-python = ">=3.8"
dependencies = [
    "backports.zoneinfo; python_version<\"3.9\"",
    "tzdata; sys_platform == 'win32'",
]

[project.license]
text = "BSD-3-Clause"

[project.urls]
Homepage = "https://www.example.com/"
Documentation = "https://docs.example.com/"

[project.optional-dependencies]
argon2 = ["argon2-cffi >= 19.1.0"]

[project.scripts]
run = "project.__main__:main"

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
packages = {find = {}}
include-package-data = true
zip-safe = false

[tool.setuptools.dynamic]
version = {attr = "project.__version__"}

[tool.distutils.bdist_rpm]
doc-files = "docs extras AUTHORS INSTALL LICENSE README.rst"
install-script = "scripts/rpm-install.sh"

[tool.flake8]
exclude = "build,.git,.tox,./tests/.env"
ignore = "W504"
max-line-length = "999"

[tool.isort]
default_section = "THIRDPARTY"
include_trailing_comma = true
line_length = 4
multi_line_output = 6
abravalheri-validate-pyproject-4b2e70d/tests/examples/setuptools/01-pyproject.toml0000664000175000017500000000210315140154751030423 0ustar  carstencarsten[project]
name = "some-project"
authors = [{ name = "Anderson Bravalheri" }]
description = "Some description"
license = { text = "MIT" }
readme = "README.rst"
classifiers = [
    "Development Status :: 5 - Production/Stable",
    "Topic :: Utilities",
    "Programming Language :: Python",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3 :: Only",
    "Environment :: Console",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Operating System :: POSIX :: Linux",
    "Operating System :: Unix",
    "Operating System :: MacOS",
    "Operating System :: Microsoft :: Windows",
]
dynamic = ["version"]
requires-python = ">=3.6"
dependencies = [
    "importlib-metadata; python_version<\"3.8\"",
    "appdirs>=1.4.4,<2",
]

[tool.setuptools]
zip-safe = false
include-package-data = true
exclude-package-data = { "pkg1" = ["*.yaml"] }
package-dir = {"" = "src"} # all the packages under the src folder
platforms = ["any"]

[tool.setuptools.packages]
find = { where = ["src"], exclude = ["tests"], namespaces = true }
abravalheri-validate-pyproject-4b2e70d/tests/examples/setuptools/09-pyproject.toml0000664000175000017500000000056015140154751030440 0ustar  carstencarsten# Setuptools should allow stub-only package names in `package-dir` (PEP 561)
[build-system]
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"

[project]
name = "mypkg-stubs"
version = "0.0.0"

[tool.setuptools]
packages = ["otherpkg-stubs", "namespace.mod.stubs"]

[tool.setuptools.package-dir]
otherpkg-stubs = "namespace/mod/stubs"
abravalheri-validate-pyproject-4b2e70d/tests/examples/setuptools/11-pyproject.toml0000664000175000017500000000013215140154751030424 0ustar  carstencarsten[tool.setuptools]
ext-modules = [
  {name = "my.ext", sources = ["hello.c", "world.c"]}
]
abravalheri-validate-pyproject-4b2e70d/tests/examples/setuptools/04-pyproject.toml0000664000175000017500000000125315140154751030433 0ustar  carstencarsten[project]
name = "project"
readme = "README.md"
dynamic = ["version"]
requires-python = ">=3.8"
dependencies = ["numpy>=1.18.5"]
license.file = "LICENSE.txt"

[project.entry-points]
pandas_plotting_backends = {matplotlib = "project.plotting:matplotlib_plot"}

[project.optional-dependencies]
test = [
    "pytest",
    "pytest-xdist",
]

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
include-package-data = true
zip-safe = false
platforms = ["any"]

[tool.setuptools.package-data]
"*" = ["data/*", "files/**/*.json"]

[tool.setuptools.packages.find]
include = ["pkg", "pkg.*"]

[tool.distutils.build_ext]
inplace = true
abravalheri-validate-pyproject-4b2e70d/tests/examples/setuptools/03-pyproject.toml0000664000175000017500000000140315140154751030427 0ustar  carstencarsten[project]
name = "project"
description = "description"
license = { text = "BSD-3-Clause" }
dynamic = ["version"]
requires-python = ">= 3.6"

[[project.authors]]
name = "Name 1"
email = "name1@example1.com"

[[project.authors]]
name = "Name 2"
email = "name2@example2.com"

[project.readme]
file = "README.rst"
content-type = "text/x-rst"

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
package-dir = {"" = "src"}
include-package-data = true
script-files = [
    "bin/run.py"
]

[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.dynamic]
version = {file = "__version__.txt"}

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.coverage.paths]
source = [
    "src",
    "*/site-packages",
]
abravalheri-validate-pyproject-4b2e70d/tests/examples/atoml/0000775000175000017500000000000015140154751024170 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/examples/atoml/LICENSE0000664000175000017500000000204615140154751025177 0ustar  carstencarstenCopyright (c) 2018 Sébastien Eustace

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.
abravalheri-validate-pyproject-4b2e70d/tests/examples/atoml/pyproject.toml0000664000175000017500000000213115140154751027101 0ustar  carstencarsten[tool.pdm]

[tool.pdm.dev-dependencies]
test = [
    "pytest",
    "pytest-cov",
    "pyyaml~=5.4",
]

[build-system]
requires = ["pdm-pep517"]
build-backend = "pdm.pep517.api"


[project]
# PEP 621 project metadata
# See https://peps.python.org/pep-0621/
name = "atoml"
# version = {use_scm = true}  ->  invalid, must be string
authors = [
    {name = "Frost Ming", email = "mianghong@gmail.com"},
    {name = "Sébastien Eustace", email = "sebastien@eustace.io"},
]
license = {text = "MIT"}
requires-python = ">=3.6"
dependencies = []
description = "Yet another style preserving TOML library"
readme = "README.md"
dynamic = ["classifiers", "version"]

[project.urls]
Homepage = "https://github.com/frostming/atoml.git"
Repository = "https://github.com/frostming/atoml.git"

[tool.black]
line-length = 88
include = '\.pyi?$'
exclude = '''
/(
    \.git
  | \.hg
  | \.mypy_cache
  | \.tox
  | \.venv
  | _build
  | build
  | dist
  | tests/toml-test
)/
'''

[tool.isort]
profile = "black"
atomic = true
lines_after_imports = 2
lines_between_types = 1

known_first_party = ["atoml"]
known_third_party = ["pytest"]
abravalheri-validate-pyproject-4b2e70d/tests/examples/ruff/0000775000175000017500000000000015140154751024016 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/examples/ruff/test_config.json0000664000175000017500000000012215140154751027210 0ustar  carstencarsten{
    "tools": {
        "ruff": "https://json.schemastore.org/ruff.json"
    }
}
abravalheri-validate-pyproject-4b2e70d/tests/examples/ruff/modern.toml0000664000175000017500000000205415140154751026200 0ustar  carstencarsten[tool.ruff]
src = ["src"]

[tool.ruff.lint]
extend-select = [
  "B",           # flake8-bugbear
  "I",           # isort
  "ARG",         # flake8-unused-arguments
  "C4",          # flake8-comprehensions
  "EM",          # flake8-errmsg
  "ICN",         # flake8-import-conventions
  "G",           # flake8-logging-format
  "PGH",         # pygrep-hooks
  "PIE",         # flake8-pie
  "PL",          # pylint
  "PT",          # flake8-pytest-style
  "PTH",         # flake8-use-pathlib
  "RET",         # flake8-return
  "RUF",         # Ruff-specific
  "SIM",         # flake8-simplify
  "T20",         # flake8-print
  "UP",          # pyupgrade
  "YTT",         # flake8-2020
  "EXE",         # flake8-executable
  "NPY",         # NumPy specific rules
  "PD",          # pandas-vet
  "FURB",        # refurb
  "PYI",         # flake8-pyi
]
ignore = [
  "PLR",    # Design related pylint codes
]
typing-modules = ["mypackage._compat.typing"]
isort.required-imports = ["from __future__ import annotations"]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["T20"]
abravalheri-validate-pyproject-4b2e70d/tests/examples/pdm/0000775000175000017500000000000015140154751023634 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/examples/pdm/LICENSE0000664000175000017500000000206015140154751024637 0ustar  carstencarstenMIT License

Copyright (c) 2019-2021 Frost Ming

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.
abravalheri-validate-pyproject-4b2e70d/tests/examples/pdm/test_config.json0000664000175000017500000000027415140154751027036 0ustar  carstencarsten{
    "tools": {
        "partial-pdm": "https://json.schemastore.org/partial-pdm.json",
        "partial-pdm-dockerize": "https://json.schemastore.org/partial-pdm-dockerize.json"
    }
}
abravalheri-validate-pyproject-4b2e70d/tests/examples/pdm/pyproject.toml0000664000175000017500000000625615140154751026561 0ustar  carstencarsten[project]
# PEP 621 project metadata
# https://www.python.org/dev/peps/pep-0621
name = "pdm-backend"
description = "The build backend used by PDM that supports latest packaging standards"
authors = [
  { name = "Frost Ming", email = "me@frostming.com" }
]
license = {text = "MIT"}
requires-python = ">=3.9"
readme = "README.md"
keywords = ["packaging", "PEP 517", "build"]
dynamic = ["version"]
import-names = ["pdm.backend"]
import-namespaces = ["pdm"]

classifiers = [
    "Development Status :: 5 - Production/Stable",
    "Topic :: Software Development :: Build Tools",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "Programming Language :: Python :: 3.14",
]

dependencies = [
    "importlib-metadata>=3.6; python_version < \"3.10\""
]

[project.urls]
Homepage = "https://github.com/pdm-project/pdm-backend"
Repository = "https://github.com/pdm-project/pdm-backend"
Documentation = "https://backend.pdm-project.org"

[build-system]
requires = []
build-backend = "pdm.backend.intree"
backend-path = ["src"]

[tool.ruff]
src = ["src"]
target-version = "py38"
exclude = ["tests/fixtures"]

[tool.ruff.lint]
extend-select = [
  "I",    # isort
  "C4",   # flake8-comprehensions
  "W",    # pycodestyle
  "YTT",  # flake8-2020
  "UP",   # pyupgrade
  "FA",   # flake8-annotations
]

[tool.ruff.lint.mccabe]
max-complexity = 10

[tool.ruff.lint.isort]
known-first-party = ["pdm.backend"]

[tool.vendoring]
destination = "src/pdm/backend/_vendor/"
requirements = "src/pdm/backend/_vendor/vendor.txt"
namespace = "pdm.backend._vendor"
patches-dir = "scripts/patches"
protected-files = ["__init__.py", "README.md", "vendor.txt"]

[tool.vendoring.transformations]
substitute = [
    {match = "import packaging", replace = "import pdm.backend._vendor.packaging"},
]
drop = [
    "bin/",
    "*.so",
    "typing.*",
    "*/tests/",
    "**/test_*.py",
    "**/*_test.py"
]

[tool.pdm.version]
source = "scm"

[tool.pdm.build]
includes = ["src"]
package-dir = "src"
source-includes = ["tests"]

[tool.pdm.dev-dependencies]
test = [
    "pytest",
    "pytest-cov",
    "pytest-gitconfig",
    "pytest-xdist",
    "setuptools",
]

dev = [
    "editables>=0.3",
    "pre-commit>=2.21.0",
    "vendoring>=1.2.0; python_version ~= \"3.8\"",
]
docs = [
    "mkdocs>=1.4.2",
    "mkdocstrings[python]>=0.19.0",
    "mkdocs-material>=8.5.10",
    "mkdocs-version-annotations>=1.0.0",
]

[tool.pdm.scripts]
build = "python scripts/build.py"
docs = "mkdocs serve"
test = "pytest"

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "if self.debug",
    "raise AssertionError",
    "raise NotImplementedError",
    "if __name__ == .__main__.:",
    "if TYPE_CHECKING:",
]
ignore_errors = true

[tool.mypy]
ignore_missing_imports = true
disallow_incomplete_defs = true
disallow_untyped_calls = true
disallow_untyped_defs = true
disallow_untyped_decorators = true
explicit_package_bases = true
namespace_packages = true

[[tool.mypy.overrides]]
module = "pdm.backend._vendor.*"
ignore_errors = true
abravalheri-validate-pyproject-4b2e70d/tests/examples/poetry/0000775000175000017500000000000015140154751024376 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/examples/poetry/test_config.json0000664000175000017500000000013615140154751027575 0ustar  carstencarsten{
    "tools": {
        "poetry": "https://json.schemastore.org/partial-poetry.json"
    }
}
abravalheri-validate-pyproject-4b2e70d/tests/examples/poetry/poetry-sample-project.toml0000664000175000017500000000321415140154751031540 0ustar  carstencarsten[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "Some description."
authors = ["Sébastien Eustace "]
license = "MIT"

readme = "README.rst"

homepage = "https://python-poetry.org"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"

keywords = ["packaging", "dependency", "poetry"]

classifiers = [
  "Topic :: Software Development :: Build Tools",
  "Topic :: Software Development :: Libraries :: Python Modules",
]

# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
cleo = "^0.6"
pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" }
tomlkit = { git = "https://github.com/sdispater/tomlkit.git", rev = "3bff550", develop = false }
requests = { version = "^2.18", optional = true, extras = ["security"] }
pathlib2 = { version = "^2.2", python = "~2.7" }

orator = { version = "^0.9", optional = true }

# File dependency
demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" }

# Dir dependency with setup.py
my-package = { path = "../project_with_setup/" }

# Dir dependency with pyproject.toml
simple-project = { path = "../simple_project/" }

# Dependency with markers
functools32 = { version = "^3.2.3", markers = "python_version ~= '2.7' and sys_platform == 'win32' or python_version in '3.4 3.5'" }

# Dependency with python constraint
dataclasses = { version = "^0.7", python = ">=3.6.1,<3.7" }


[tool.poetry.extras]
db = ["orator"]

[tool.poetry.group.dev.dependencies]
pytest = "~3.4"


[tool.poetry.scripts]
my-script = "my_package:main"


[tool.poetry.plugins."blogtool.parsers"]
".rst" = "some_module::SomeClass"
abravalheri-validate-pyproject-4b2e70d/tests/examples/poetry/poetry-complete.toml0000664000175000017500000000255315140154751030430 0ustar  carstencarsten[tool.poetry]
name = "poetry"
version = "0.5.0"
description = "Python dependency management and packaging made easy."
authors = ["Sébastien Eustace "]
license = "MIT"

readme = "README.rst"

homepage = "https://python-poetry.org/"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"

keywords = ["packaging", "dependency", "poetry"]

# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.2" # Compatible python versions must be declared here
toml = "^0.9"
# Dependencies with extras
requests = { version = "^2.13", extras = ["security"] }
# Python specific dependencies with prereleases allowed
pathlib2 = { version = "^2.2", python = "~2.7", allows-prereleases = true }
# Git dependencies
cleo = { git = "https://github.com/sdispater/cleo.git", branch = "master" }

# Optional dependencies (extras)
pendulum = { version = "^1.4", optional = true }

[tool.poetry."this key is not in the schema"]
"but that's" = "ok"

[tool.poetry.extras]
time = ["pendulum"]

[tool.poetry.dev-dependencies]
pytest = "^3.0"
pytest-cov = "^2.4"

[tool.poetry.scripts]
my-script = 'my_package:main'
sample_pyscript = { reference = "script-files/sample_script.py", type = "file" }
sample_shscript = { reference = "script-files/sample_script.sh", type = "file" }


[[tool.poetry.source]]
name = "foo"
url = "https://bar.com"
abravalheri-validate-pyproject-4b2e70d/tests/examples/poetry/poetry-inline-table.toml0000664000175000017500000000175415140154751031165 0ustar  carstencarsten[tool.poetry]
name = "with-include"
version = "1.2.3"
description = "Some description."
authors = ["Sébastien Eustace "]
license = "MIT"

homepage = "https://python-poetry.org/"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"

keywords = ["packaging", "dependency", "poetry"]

classifiers = [
  "Topic :: Software Development :: Build Tools",
  "Topic :: Software Development :: Libraries :: Python Modules",
]

packages = [{ include = "src_package", from = "src" }]

include = [
  { path = "tests", format = "sdist" },
  { path = "wheel_only.txt", format = "wheel" },
]

# Requirements
[tool.poetry.dependencies]
python = "^3.6"
cleo = "^0.6"
cachy = { version = "^0.2.0", extras = ["msgpack"] }

pendulum = { version = "^1.4", optional = true }

[tool.poetry.dev-dependencies]
pytest = "~3.4"

[tool.poetry.extras]
time = ["pendulum"]

[tool.poetry.scripts]
my-script = "my_package:main"
my-2nd-script = "my_package:main2"
abravalheri-validate-pyproject-4b2e70d/tests/examples/poetry/poetry-author-no-email.toml0000664000175000017500000000043615140154751031617 0ustar  carstencarsten[tool.poetry]
name = "single-python"
version = "0.1"
description = "Some description."
authors = ["Wagner Macedo "]
license = "MIT"

readme = ["README-1.rst", "README-2.rst"]

homepage = "https://python-poetry.org/"


[tool.poetry.dependencies]
python = "2.7.15"
abravalheri-validate-pyproject-4b2e70d/tests/examples/poetry/poetry-capital-in-author-email.toml0000664000175000017500000000040615140154751033221 0ustar  carstencarsten[tool.poetry]
name = "single-python"
version = "0.1"
description = "Some description."
authors = ["Wagner Macedo"]
license = "MIT"

readme = ["README-1.rst", "README-2.rst"]

homepage = "https://python-poetry.org/"


[tool.poetry.dependencies]
python = "2.7.15"
abravalheri-validate-pyproject-4b2e70d/tests/examples/poetry/poetry-readme-files.toml0000664000175000017500000000044115140154751031147 0ustar  carstencarsten[tool.poetry]
name = "single-python"
version = "0.1"
description = "Some description."
authors = ["Wagner Macedo "]
license = "MIT"

readme = ["README-1.rst", "README-2.rst"]

homepage = "https://python-poetry.org/"


[tool.poetry.dependencies]
python = "2.7.15"
abravalheri-validate-pyproject-4b2e70d/tests/examples/cibuildwheel/0000775000175000017500000000000015140154751025514 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/examples/cibuildwheel/test_config.json0000664000175000017500000000015215140154751030711 0ustar  carstencarsten{
    "tools": {
        "cibuildwheel": "https://json.schemastore.org/partial-cibuildwheel.json"
    }
}
abravalheri-validate-pyproject-4b2e70d/tests/examples/cibuildwheel/overrides.toml0000664000175000017500000000015415140154751030413 0ustar  carstencarsten[tool.cibuildwheel]
build = "*"

[[tool.cibuildwheel.overrides]]
select = "cp312-*"
test-command = "pytest"
abravalheri-validate-pyproject-4b2e70d/tests/examples/cibuildwheel/default.toml0000664000175000017500000000222115140154751030032 0ustar  carstencarsten[tool.cibuildwheel]
build = "*"
skip = ""
test-skip = ""

archs = ["auto"]
build-frontend = "default"
config-settings = {}
dependency-versions = "pinned"
environment = {}
environment-pass = []
build-verbosity = 0

before-all = ""
before-build = ""
repair-wheel-command = ""

test-command = ""
before-test = ""
test-requires = []
test-extras = []

container-engine = "docker"

manylinux-x86_64-image = "manylinux2014"
manylinux-i686-image = "manylinux2014"
manylinux-aarch64-image = "manylinux2014"
manylinux-ppc64le-image = "manylinux2014"
manylinux-s390x-image = "manylinux2014"
manylinux-pypy_x86_64-image = "manylinux2014"
manylinux-pypy_i686-image = "manylinux2014"
manylinux-pypy_aarch64-image = "manylinux2014"

musllinux-x86_64-image = "musllinux_1_1"
musllinux-i686-image = "musllinux_1_1"
musllinux-aarch64-image = "musllinux_1_1"
musllinux-ppc64le-image = "musllinux_1_1"
musllinux-s390x-image = "musllinux_1_1"


[tool.cibuildwheel.linux]
repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}"

[tool.cibuildwheel.macos]
repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}"

[tool.cibuildwheel.windows]
abravalheri-validate-pyproject-4b2e70d/tests/examples/trampolim/0000775000175000017500000000000015140154751025060 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/examples/trampolim/LICENSE0000664000175000017500000000213115140154751026062 0ustar  carstencarstenCopyright © 2019 Filipe Laíns 

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 (including the next
paragraph) 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.
abravalheri-validate-pyproject-4b2e70d/tests/examples/trampolim/pyproject.toml0000664000175000017500000000232015140154751027771 0ustar  carstencarsten[build-system]
build-backend = 'trampolim'
backend-path = ['.']
requires = [
  'tomli>=1.0.0',
  'packaging',
  'pep621>=0.4.0',
  'backports.cached-property ; python_version < "3.8"',
]

[project]
name = 'trampolim'
version = '0.1.0'
description = 'A modern Python build backend'
readme = 'README.md'
requires-python = '>=3.7'
license = { file = 'LICENSE' }
keywords = ['build', 'pep517', 'package', 'packaging']
authors = [
  { name = 'Filipe Laíns', email = 'lains@riseup.net' },
]
classifiers = [
  'Development Status :: 4 - Beta',
  'Programming Language :: Python'
]
import-namespaces = ["trampolim"]
dependencies = [
  'tomli>=1.0.0',
  'packaging',
  'pep621>=0.4.0',
  'backports.cached-property ; python_version < "3.8"',
]

[project.optional-dependencies]
test = [
  'wheel',
  'pytest>=3.9.1',
  'pytest-cov',
  'pytest-mock',
]
docs = [
  'furo>=2021.04.11b34',
  'sphinx~=3.0',
  'sphinx-autodoc-typehints>=1.10',
]

[project.scripts]
trampolim = 'trampolim.__main__:entrypoint'

[project.urls]
homepage = 'https://github.com/FFY00/trampolim'
repository = 'https://github.com/FFY00/trampolim'
documentation = 'https://trampolim.readthedocs.io'
changelog = 'https://trampolim.readthedocs.io/en/latest/changelog.html'
abravalheri-validate-pyproject-4b2e70d/tests/examples/localtool/0000775000175000017500000000000015140154751025044 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/examples/localtool/test_config.json0000664000175000017500000000027715140154751030251 0ustar  carstencarsten{
    "tools": {
        "localtool": "tests/examples/localtool/localtool.schema.json",
        "nestedtool": "tests/examples/localtool/nestedtool.schema.json#/properties/nestedtool"
    }
}
abravalheri-validate-pyproject-4b2e70d/tests/examples/localtool/nestedtool.schema.json0000664000175000017500000000057215140154751031362 0ustar  carstencarsten{
    "$id": "http://nested-id.example",
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "properties": {
        "nestedtool": {
            "type": "object",
            "properties": {
                "two": { "type": "integer" }
            },
            "additionalProperties": false
        }
    },
    "additionalProperties": false
}
abravalheri-validate-pyproject-4b2e70d/tests/examples/localtool/localtool.schema.json0000664000175000017500000000033615140154751031170 0ustar  carstencarsten{
    "$id": "https://simple-id.example",
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "properties": {
        "one": { "type": "integer" }
    },
    "additionalProperties": false
}
abravalheri-validate-pyproject-4b2e70d/tests/examples/localtool/working.toml0000664000175000017500000000006415140154751027421 0ustar  carstencarsten[tool.localtool]
one = 1

[tool.nestedtool]
two = 2
abravalheri-validate-pyproject-4b2e70d/tests/examples/pep_text/0000775000175000017500000000000015140154751024704 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/examples/pep_text/pyproject.toml0000664000175000017500000000232715140154751027624 0ustar  carstencarsten# This example was creating by joining examples from PEP 621 and PEP 517 text
# + some minor changes
[project]
name = "spam"
version = "2020.0.0"
description = "Lovely Spam! Wonderful Spam!"
readme = "README.rst"
requires-python = ">=3.8"
license = {file = "LICENSE.txt"}
keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"]
authors = [
  {email = "hi@pradyunsg.me"},
  {name = "Tzu-Ping Chung"}
]
maintainers = [
  {name = "Brett Cannon", email = "brett@python.org"}
]
classifiers = [
  "Development Status :: 4 - Beta",
  "Programming Language :: Python"
]

dependencies = [
  "httpx",
  "gidgethub[httpx]>4.0.0",
  "django>2.1; os_name != 'nt'",
  "django>2.0; os_name == 'nt'"
]

[project.optional-dependencies]
test = [
  "pytest < 5.0.0",
  "pytest-cov[all]"
]

[project.urls]
homepage = "https://example.com"
documentation = "https://readthedocs.org"
repository = "https://github.com"
changelog = "https://github.com/me/spam/blob/master/CHANGELOG.md"

[project.scripts]
spam-cli = "spam:main_cli"

[project.gui-scripts]
spam-gui = "spam:main_gui"

[project.entry-points."spam.magical"]
tomatoes = "spam:main_tomatoes"

[build-system]
requires = ["flit"]
build-backend = "local_backend"
backend-path = ["backend"]
abravalheri-validate-pyproject-4b2e70d/tests/test_error_reporting.py0000664000175000017500000001046515140154751026077 0ustar  carstencarstenimport logging
from inspect import cleandoc

import pytest
from fastjsonschema import validate

from validate_pyproject.api import FORMAT_FUNCTIONS
from validate_pyproject.error_reporting import ValidationError, detailed_errors

EXAMPLES = {
    "const": {
        "schema": {"const": 42},
        "value": 13,
        "message": "`data` must be 42",
        "debug_info": "**SKIP-TEST**",
    },
    "container": {
        "schema": {"type": "array", "contains": True},
        "value": [],
        "message": "`data` must not be empty",
        "debug_info": "**SKIP-TEST**",
    },
    "type": {
        "schema": {"anyOf": [{"not": {"type": ["string", "number"]}}]},
        "value": 42,
        "message": """
            `data` cannot be validated by any definition:

                - (*NOT* the following):
                    type: [string, number]
        """,
        "debug_info": "**SKIP-TEST**",
    },
    "oneOf": {
        "schema": {
            "oneOf": [{"type": "string", "format": "pep440"}, {"type": "integer"}]
        },
        "value": {"use_scm": True},
        "message": """
            `data` must be valid exactly by one definition (0 matches found):

                - {type: string, format: 'pep440'}
                - {type: integer}
        """,
        "debug_info": """
            GIVEN VALUE:
                {
                    "use_scm": true
                }

            OFFENDING RULE: 'oneOf'

            DEFINITION:
                {
                    "oneOf": [
                        {
                            "type": "string",
                            "format": "pep440"
                        },
                        {
                            "type": "integer"
                        }
                    ]
                }

            For more details about `format` see
        """,
    },
    "description": {
        "schema": {"type": "string", "description": "Lorem ipsum dolor sit amet"},
        "value": {"name": 42},
        "message": "`data` must be string",
        "debug_info": """
            DESCRIPTION:
                Lorem ipsum dolor sit amet
        """,
    },
    "$$description": {
        "schema": {
            "properties": {
                "name": {
                    "type": "string",
                    "$$description": [
                        "Lorem ipsum dolor sit amet, consectetur adipiscing elit,",
                        "sed do eiusmod tempor incididunt ut labore et dolore magna",
                        "aliqua. Ut enim ad minim veniam, quis nostrud exercitation",
                        "ullamco laboris nisi ut aliquip ex ea commodo consequat.",
                    ],
                }
            }
        },
        "value": {"name": 42},
        "message": "`name` must be string",
        "debug_info": """
            DESCRIPTION:
                Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
                tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
                quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
                consequat.

            GIVEN VALUE:
                42

            OFFENDING RULE: 'type'

            DEFINITION:
                {
                    "type": "string"
                }
        """,
    },
}


@pytest.mark.parametrize("example", EXAMPLES.keys())
def test_error_reporting(caplog, example):
    schema = EXAMPLES[example]["schema"]
    value = EXAMPLES[example]["value"]
    message = cleandoc(EXAMPLES[example]["message"])
    debug_info = cleandoc(EXAMPLES[example]["debug_info"])

    with pytest.raises(ValidationError) as excinfo, caplog.at_level(
        logging.CRITICAL
    ), detailed_errors():
        validate(schema, value, formats=FORMAT_FUNCTIONS)
    ex = excinfo.value
    assert ex.message.strip() == message
    assert ex.message == ex.summary
    assert "GIVEN VALUE:" in ex.details
    assert "DEFINITION:" in ex.details

    with pytest.raises(ValidationError) as excinfo, caplog.at_level(
        logging.DEBUG
    ), detailed_errors():
        validate(schema, value, formats=FORMAT_FUNCTIONS)
    ex = excinfo.value
    assert "GIVEN VALUE:" in ex.message
    assert "DEFINITION:" in ex.message
    assert ex.summary in ex.message
    if debug_info != "**SKIP-TEST**":
        assert debug_info in ex.details
abravalheri-validate-pyproject-4b2e70d/tests/conftest.py0000664000175000017500000000215715140154751023442 0ustar  carstencarsten"""
conftest.py for validate_pyproject.

Read more about conftest.py under:
- https://docs.pytest.org/en/stable/fixture.html
- https://docs.pytest.org/en/stable/writing_plugins.html
"""

from __future__ import annotations

from pathlib import Path

import pytest

HERE = Path(__file__).parent.resolve()


def pytest_configure(config):
    config.addinivalue_line("markers", "uses_network: tests may try to download files")


def collect(base: Path) -> list[str]:
    return [str(f.relative_to(base)) for f in base.glob("**/*.toml")]


@pytest.fixture(params=collect(HERE / "examples"))
def example(request) -> Path:
    return HERE / "examples" / request.param


@pytest.fixture(params=collect(HERE / "invalid-examples"))
def invalid_example(request) -> Path:
    return HERE / "invalid-examples" / request.param


@pytest.fixture(params=collect(HERE / "remote/examples"))
def remote_example(request) -> Path:
    return HERE / "remote/examples" / request.param


@pytest.fixture(params=collect(HERE / "remote/invalid-examples"))
def remote_invalid_example(request) -> Path:
    return HERE / "remote/invalid-examples" / request.param
abravalheri-validate-pyproject-4b2e70d/tests/pre_compile/0000775000175000017500000000000015140154751023534 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/pre_compile/__init__.py0000664000175000017500000000000015140154751025633 0ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/pre_compile/test_cli.py0000664000175000017500000000075615140154751025724 0ustar  carstencarstenimport traceback

import pytest

from validate_pyproject.pre_compile import cli


@pytest.mark.parametrize("replacements", ['["a", "b"]', "{invalid: json}"])
def test_invalid_replacements(tmp_path, replacements):
    with pytest.raises(SystemExit) as exc:
        cli.run(["-O", str(tmp_path), "-R", replacements])

    e = exc.value
    trace = "".join(traceback.format_exception(e.__class__, e, e.__traceback__))
    assert "--replacements: invalid" in trace
    assert replacements in trace
abravalheri-validate-pyproject-4b2e70d/tests/test_plugins.py0000664000175000017500000002042615140154751024334 0ustar  carstencarsten# The code in this module is mostly borrowed/adapted from PyScaffold and was originally
# published under the MIT license
# The original PyScaffold license can be found in 'NOTICE.txt'
from __future__ import annotations

import sys
from collections import defaultdict
from importlib.metadata import EntryPoint
from types import ModuleType
from typing import Callable, TypeVar

import pytest

from validate_pyproject import plugins
from validate_pyproject.plugins import ErrorLoadingPlugin, PluginWrapper, StoredPlugin

EXISTING = (
    "setuptools",
    "distutils",
)

T = TypeVar("T", bound=Callable)


def test_load_from_entry_point__error():
    # This module does not exist, so Python will have some trouble loading it
    # EntryPoint(name, value, group)
    entry = "mypkg.SOOOOO___fake___:activate"
    fake = EntryPoint("fake", entry, "validate_pyproject.tool_schema")
    with pytest.raises(ErrorLoadingPlugin):
        plugins.load_from_entry_point(fake)


def is_entry_point(ep):
    return all(hasattr(ep, attr) for attr in ("name", "load"))


def test_iterate_entry_points():
    plugin_iter = plugins.iterate_entry_points("validate_pyproject.tool_schema")
    assert hasattr(plugin_iter, "__iter__")
    pluging_list = list(plugin_iter)
    assert all(is_entry_point(e) for e in pluging_list)
    name_list = [e.name for e in pluging_list]
    for ext in EXISTING:
        assert ext in name_list


def test_list_from_entry_points():
    # Should return a list with all the plugins registered in the entrypoints
    pluging_list = plugins.list_from_entry_points()
    orig_len = len(pluging_list)
    plugin_names = " ".join(e.tool for e in pluging_list)
    for example in EXISTING:
        assert example in plugin_names

    # a filtering function can be passed to avoid loading plugins that are not needed
    pluging_list = plugins.list_from_entry_points(
        filtering=lambda e: e.name != "setuptools"
    )
    plugin_names = " ".join(e.tool for e in pluging_list)
    assert len(pluging_list) == orig_len - 1
    assert "setuptools" not in plugin_names


class TestPluginWrapper:
    def test_empty_help_text(self):
        def _fn1(_):
            return {}

        pw = plugins.PluginWrapper("name", _fn1)
        assert pw.help_text == ""

        def _fn2(_):
            """Help for `${tool}`"""
            return {}

        pw = plugins.PluginWrapper("name", _fn2)
        assert pw.help_text == "Help for `name`"


class TestStoredPlugin:
    def test_empty_help_text(self):
        def _fn1(_):
            return {}

        pw = plugins.StoredPlugin("name", {}, "id1", 0)
        assert pw.help_text == ""

        def _fn2(_):
            """Help for `${tool}`"""
            return {}

        pw = plugins.StoredPlugin("name", {"description": "Help for me"}, "id2", 0)
        assert pw.help_text == "Help for me"


class _FakeEntryPoints:
    def __init__(
        self,
        monkeypatch: pytest.MonkeyPatch,
        group: str = "__NOT_SPECIFIED__",
        data: defaultdict[str, list[EntryPoint]] | None = None,
    ):
        self._monkeypatch = monkeypatch
        self._group = group
        self._data = defaultdict(list) if data is None else data
        self.get = self._data.__getitem__

    def group(self, group: str) -> _FakeEntryPoints:
        return _FakeEntryPoints(self._monkeypatch, group, self._data)

    def reverse(self) -> _FakeEntryPoints:
        data = defaultdict(list, {k: list(reversed(v)) for k, v in self._data.items()})
        return _FakeEntryPoints(self._monkeypatch, self._group, data)

    def __call__(self, *, name: str, value: str) -> Callable[[T], T]:
        def fake_entry_point(impl: T) -> T:
            ep = EntryPoint(name=name, value=value, group=self._group)
            self._data[ep.group].append(ep)
            module, _, func = ep.value.partition(":")
            if module not in sys.modules:
                self._monkeypatch.setitem(sys.modules, module, ModuleType(module))
            self._monkeypatch.setattr(sys.modules[module], func, impl, raising=False)
            return impl

        return fake_entry_point


def test_multi_plugins(monkeypatch):
    fake_eps = _FakeEntryPoints(monkeypatch, group="validate_pyproject.multi_schema")
    fake_eps(name="f", value="test_module:f")(
        lambda: {
            "tools": {"example#frag": {"$id": "example1"}},
            "schemas": [
                {"$id": "example2"},
                {"$id": "example3"},
            ],
        }
    )
    monkeypatch.setattr(plugins, "iterate_entry_points", fake_eps.get)

    lst = plugins.list_from_entry_points()
    assert len(lst) == 3

    (fragmented,) = (e for e in lst if e.tool)
    assert fragmented.tool == "example"
    assert fragmented.fragment == "frag"
    assert fragmented.schema == {"$id": "example1"}


@pytest.mark.parametrize("epname", ["aaa", "zzz"])
def test_combined_plugins(monkeypatch, epname):
    fake_eps = _FakeEntryPoints(monkeypatch)
    multi_eps = fake_eps.group("validate_pyproject.multi_schema")
    tool_eps = fake_eps.group("validate_pyproject.tool_schema")
    multi_eps(name=epname, value="test_module:f")(
        lambda: {
            "tools": {
                "example1": {"$id": "example1"},
                "example2": {"$id": "example2"},
                "example3": {"$id": "example3"},
            }
        }
    )
    tool_eps(name="example1", value="test_module:f1")(lambda _: {"$id": "ztool1"})
    tool_eps(name="example2", value="test_module:f2")(lambda _: {"$id": "atool2"})
    tool_eps(name="example4", value="test_module:f2")(lambda _: {"$id": "tool4"})

    monkeypatch.setattr(plugins, "iterate_entry_points", fake_eps.get)

    lst = plugins.list_from_entry_points()
    print(lst)
    assert len(lst) == 4

    assert lst[0].tool == "example1"
    assert isinstance(lst[0], PluginWrapper)

    assert lst[1].tool == "example2"
    assert isinstance(lst[1], PluginWrapper)

    assert lst[2].tool == "example3"
    assert isinstance(lst[2], StoredPlugin)

    assert lst[3].tool == "example4"
    assert isinstance(lst[3], PluginWrapper)


def test_several_multi_plugins(monkeypatch):
    fake_eps = _FakeEntryPoints(monkeypatch, "validate_pyproject.multi_schema")
    fake_eps(name="zzz", value="test_module:f1")(
        lambda: {
            "tools": {"example": {"$id": "example1"}},
        }
    )
    fake_eps(name="aaa", value="test_module:f2")(
        lambda: {
            "tools": {"example": {"$id": "example2"}, "other": {"$id": "example3"}}
        }
    )
    for eps in (fake_eps, fake_eps.reverse()):
        monkeypatch.setattr(plugins, "iterate_entry_points", eps.get)
        # entry-point names closer to "zzzzzzzz..." have priority
        (plugin1, plugin2) = plugins.list_from_entry_points()
        print(plugin1, plugin2)
        assert plugin1.schema["$id"] == "example1"
        assert plugin2.schema["$id"] == "example3"


def test_custom_priority(monkeypatch):
    fake_eps = _FakeEntryPoints(monkeypatch)
    tool_eps = fake_eps.group("validate_pyproject.tool_schema")
    multi_eps = fake_eps.group("validate_pyproject.multi_schema")

    multi_schema = {"tools": {"example": {"$id": "multi-eps"}}}
    multi_eps(name="example", value="test_module:f")(lambda: multi_schema)

    @tool_eps(name="example", value="test_module1:f1")
    def tool_schema1(_name):
        return {"$id": "tool-eps-1"}

    @tool_eps(name="example", value="test_module2:f1")
    def tool_schema2(_name):
        return {"$id": "tool-eps-2"}

    monkeypatch.setattr(plugins, "iterate_entry_points", fake_eps.get)
    (winner,) = plugins.list_from_entry_points()  # default: tool with "higher" ep name
    assert winner.schema["$id"] == "tool-eps-2"

    tool_schema1.priority = 1.1
    (winner,) = plugins.list_from_entry_points()  # default: tool has priority
    assert winner.schema["$id"] == "tool-eps-1"

    tool_schema1.priority = 0.1
    tool_schema2.priority = 0.2
    multi_schema["priority"] = 0.9
    (winner,) = plugins.list_from_entry_points()  # custom higher priority wins
    assert winner.schema["$id"] == "multi-eps"


def test_broken_multi_plugin(monkeypatch):
    fake_eps = _FakeEntryPoints(monkeypatch, "validate_pyproject.multi_schema")
    fake_eps(name="broken", value="test_module.f")(lambda: {}["no-such-key"])
    monkeypatch.setattr(plugins, "iterate_entry_points", fake_eps.get)
    with pytest.raises(ErrorLoadingPlugin):
        plugins.list_from_entry_points()
abravalheri-validate-pyproject-4b2e70d/tests/test_repo_review.py0000664000175000017500000000460415140154751025201 0ustar  carstencarstenfrom argparse import Namespace
from pathlib import Path

import pytest

from validate_pyproject import _tomllib as tomllib
from validate_pyproject.repo_review import repo_review_checks, repo_review_families

DIR = Path(__file__).parent.resolve()
EXAMPLES = DIR / "examples"
INVALID_EXAMPLES = DIR / "invalid-examples"


@pytest.fixture
def repo_review_processor():
    try:
        from repo_review import processor
    except ImportError:

        class _Double:  # just for the sake of Python < 3.10 coverage
            @staticmethod
            def process(file: Path):
                pyproject = (file / "pyproject.toml").read_text(encoding="utf-8")
                opts = tomllib.loads(pyproject)
                checks = (
                    checker.check(opts) == ""  # No errors
                    for checker in repo_review_checks().values()
                )
                return Namespace(
                    families=repo_review_families(opts),
                    results=[Namespace(result=check) for check in checks],
                )

        return _Double

    return processor


@pytest.mark.parametrize("name", ["atoml", "flit", "pdm", "pep_text", "trampolim"])
def test_valid_example(repo_review_processor, name: str) -> None:
    processed = repo_review_processor.process(EXAMPLES / name)
    assert all(r.result for r in processed.results), f"{processed.results}"


@pytest.mark.parametrize("name", ["pdm/invalid-version", "pdm/redefining-as-dynamic"])
def test_invalid_example(repo_review_processor, name: str) -> None:
    processed = repo_review_processor.process(INVALID_EXAMPLES / name)
    assert any(not r.result and r.result is not None for r in processed.results), (
        f"{processed.results}"
    )


def test_no_distutils(repo_review_processor) -> None:
    processed = repo_review_processor.process(EXAMPLES / "pep_text")
    family = processed.families["validate-pyproject"]
    assert "[tool.setuptools]" in family["description"]
    assert "[tool.distutils]" not in family["description"]


def test_has_distutils(repo_review_processor, tmp_path: Path) -> None:
    d = tmp_path / "no-distutils"
    d.mkdir()
    d.joinpath("pyproject.toml").write_text("[tool.distutils]")
    processed = repo_review_processor.process(d)
    family = processed.families["validate-pyproject"]
    assert "[tool.setuptools]" in family["description"]
    assert "[tool.distutils]" in family["description"]
abravalheri-validate-pyproject-4b2e70d/tests/test_caching.py0000664000175000017500000000533215140154751024246 0ustar  carstencarstenimport io
import os
from unittest.mock import Mock

import pytest

from validate_pyproject import caching, http, remote


@pytest.fixture(autouse=True)
def no_cache_env_var(monkeypatch):
    monkeypatch.delenv("VALIDATE_PYPROJECT_CACHE_REMOTE", raising=False)


def fn1(_: str) -> io.StringIO:
    return io.StringIO("42")


def fn2(_: str) -> io.StringIO:
    msg = "should not be called"
    raise RuntimeError(msg)


def test_as_file(tmp_path):
    # The first call should create a file and return its contents
    cache_path = caching.path_for("hello-world", tmp_path)
    assert not cache_path.exists()

    with caching.as_file(fn1, "hello-world", tmp_path) as f:
        assert f.read() == b"42"

    assert cache_path.exists()
    assert cache_path.read_text("utf-8") == "42"

    # Any further calls using the same ``arg`` should reuse the file
    # and NOT call the function
    with caching.as_file(fn2, "hello-world", tmp_path) as f:
        assert f.read() == b"42"

    # If the file is deleted, then the function should be called
    cache_path.unlink()
    with pytest.raises(RuntimeError, match="should not be called"):
        caching.as_file(fn2, "hello-world", tmp_path)


def test_as_file_no_cache():
    # If no cache directory is passed, the orig function should
    # be called straight away:
    with pytest.raises(RuntimeError, match="should not be called"):
        caching.as_file(fn2, "hello-world")


def test_path_for_no_cache():
    cache_path = caching.path_for("hello-world", None)
    assert cache_path is None


@pytest.mark.uses_network
@pytest.mark.skipif(
    os.getenv("VALIDATE_PYPROJECT_NO_NETWORK") or os.getenv("NO_NETWORK"),
    reason="Disable tests that depend on network",
)
class TestIntegration:
    def test_cache_open_url(self, tmp_path, monkeypatch):
        open_url = Mock(wraps=http.open_url)
        monkeypatch.setattr(http, "open_url", open_url)

        # The first time it is called, it will cache the results into a file
        url = (
            "https://raw.githubusercontent.com/abravalheri/validate-pyproject/main/"
            "src/validate_pyproject/pyproject_toml.schema.json"
        )
        cache_path = caching.path_for(url, tmp_path)
        assert not cache_path.exists()

        with caching.as_file(http.open_url, url, tmp_path) as f:
            assert b"build-system" in f.read()

        open_url.assert_called_once()
        assert cache_path.exists()
        assert "build-system" in cache_path.read_text("utf-8")

        # The second time, it will not reach the network, and use the file contents
        open_url.reset_mock()
        _, contents = remote.load_from_uri(url, cache_dir=tmp_path)

        assert "build-system" in contents["properties"]
        open_url.assert_not_called()
abravalheri-validate-pyproject-4b2e70d/tests/test_pre_compile.py0000664000175000017500000001535515140154751025156 0ustar  carstencarstenimport builtins
import importlib
import re
import shutil
import subprocess
import sys
from inspect import cleandoc
from pathlib import Path

import pytest
from fastjsonschema import JsonSchemaValueException

from validate_pyproject import _tomllib as tomllib
from validate_pyproject.pre_compile import cli, pre_compile

from .helpers import error_file, get_tools, get_tools_as_args

MAIN_FILE = "hello_world.py"  # Let's use something different than `__init__.py`


def _pre_compile_checks(path: Path):
    assert (path / "__init__.py").exists()
    assert (path / "__init__.py").read_text() == ""
    assert (path / MAIN_FILE).exists()
    files = [
        (MAIN_FILE, "def validate("),
        (MAIN_FILE, "from .error_reporting import detailed_errors, ValidationError"),
        ("error_reporting.py", "def detailed_errors("),
        ("fastjsonschema_exceptions.py", "class JsonSchemaValueException"),
        ("fastjsonschema_validations.py", "def validate("),
        ("extra_validations.py", "def validate"),
        ("formats.py", "def pep508("),
        ("NOTICE", "The relevant copyright notes and licenses are included below"),
    ]
    for file, content in files:
        assert (path / file).exists()
        assert content in (path / file).read_text()

    # Make sure standard replacements work
    for file in ("fastjsonschema_validations.py", "error_reporting.py"):
        file_contents = (path / file).read_text()
        assert "from fastjsonschema" not in file_contents
        assert "from ._vendor.fastjsonschema" not in file_contents
        assert "from validate_pyproject._vendor.fastjsonschema" not in file_contents
        assert "from .fastjsonschema_exceptions" in file_contents

    # Make sure the pre-compiled lib works
    script = f"""
    from {path.stem} import {Path(MAIN_FILE).stem} as mod

    assert issubclass(mod.ValidationError, mod.JsonSchemaValueException)

    example = {{
        "project": {{"name": "proj", "version": 42}}
    }}
    assert mod.validate(example) == example
    """
    cmd = [sys.executable, "-c", cleandoc(script)]
    error = r".project\.version. must be string"
    with pytest.raises(subprocess.CalledProcessError) as exc_info:
        subprocess.check_output(cmd, cwd=path.parent, stderr=subprocess.STDOUT)

    assert re.search(error, str(exc_info.value.output, "utf-8"))


def test_pre_compile_api(tmp_path):
    path = Path(tmp_path)
    pre_compile(path, MAIN_FILE)
    _pre_compile_checks(path)
    # Let's make sure it also works for __init__
    shutil.rmtree(str(path), ignore_errors=True)
    replacements = {"from fastjsonschema import": "from _vend.fastjsonschema import"}
    pre_compile(path, text_replacements=replacements)
    assert "def validate(" in (path / "__init__.py").read_text()
    assert not (path / MAIN_FILE).exists()
    file_contents = (path / "fastjsonschema_validations.py").read_text()
    assert "from _vend" in file_contents
    assert "from fastjsonschema" not in file_contents


def test_vendoring_cli(tmp_path):
    path = Path(tmp_path)
    cli.run(["-O", str(path), "-M", MAIN_FILE])
    _pre_compile_checks(Path(path))
    # Let's also try to test JSON replacements
    shutil.rmtree(str(path), ignore_errors=True)
    replacements = '{"from fastjsonschema import": "from _vend.fastjsonschema import"}'
    cli.run(["-O", str(path), "-R", replacements])
    file_contents = (path / "fastjsonschema_validations.py").read_text()
    assert "from _vend" in file_contents
    assert "from fastjsonschema" not in file_contents


# ---- Examples ----


PRE_COMPILED_NAME = "_validation"


def api_pre_compile(tmp_path, *, example: Path) -> Path:
    plugins = get_tools(example)
    return pre_compile(Path(tmp_path / PRE_COMPILED_NAME), extra_plugins=plugins)


def cli_pre_compile(tmp_path, *, example: Path) -> Path:
    args = get_tools_as_args(example)
    path = Path(tmp_path / PRE_COMPILED_NAME)
    cli.run([*args, "-O", str(path)])
    return path


_PRE_COMPILED = (api_pre_compile, cli_pre_compile)


@pytest.fixture
def pre_compiled_validate(monkeypatch):
    def _validate(vendored_path, toml_equivalent):
        assert PRE_COMPILED_NAME not in sys.modules
        importlib.invalidate_caches()
        with monkeypatch.context() as m:
            # Make sure original imports are not used
            _disable_import(m, "fastjsonschema")
            _disable_import(m, "validate_pyproject")
            # Make newly generated package available for importing
            m.syspath_prepend(str(vendored_path.parent))
            mod = __import__(PRE_COMPILED_NAME)
            print(list(vendored_path.glob("*")))
            print(mod, "\n\n", dir(mod))
            try:
                return mod.validate(toml_equivalent)
            except mod.JsonSchemaValueException as ex:
                # Let's translate the exceptions so we have identical classes
                new_ex = JsonSchemaValueException(
                    ex.message, ex.value, ex.name, ex.definition, ex.rule
                )
                raise new_ex from ex
            finally:
                all_modules = [
                    mod
                    for mod in sys.modules
                    if mod.startswith(f"{PRE_COMPILED_NAME}.")
                ]
                for mod in all_modules:
                    del sys.modules[mod]
                del sys.modules[PRE_COMPILED_NAME]

    return _validate


@pytest.mark.parametrize("pre_compiled", _PRE_COMPILED)
def test_examples_api(tmp_path, pre_compiled_validate, example, pre_compiled):
    toml_equivalent = tomllib.loads(example.read_text())
    pre_compiled_path = pre_compiled(Path(tmp_path), example=example)
    assert pre_compiled_validate(pre_compiled_path, toml_equivalent) is not None


@pytest.mark.parametrize("pre_compiled", _PRE_COMPILED)
def test_invalid_examples_api(
    tmp_path, pre_compiled_validate, invalid_example, pre_compiled
):
    expected_error = error_file(invalid_example).read_text("utf-8")
    toml_equivalent = tomllib.loads(invalid_example.read_text())
    pre_compiled_path = pre_compiled(Path(tmp_path), example=invalid_example)
    with pytest.raises(JsonSchemaValueException) as exc_info:
        pre_compiled_validate(pre_compiled_path, toml_equivalent)
    exception_message = str(exc_info.value)
    print("rule", "=", exc_info.value.rule)
    print("rule_definition", "=", exc_info.value.rule_definition)
    print("definition", "=", exc_info.value.definition)
    for error in expected_error.splitlines():
        assert error in exception_message


def _disable_import(monkeypatch, name):
    orig = builtins.__import__

    def _import(import_name, *args, **kwargs):
        if import_name == name or import_name.startswith(f"{name}."):
            raise ImportError(name)
        return orig(import_name, *args, **kwargs)

    monkeypatch.setattr(builtins, "__import__", _import)
abravalheri-validate-pyproject-4b2e70d/tests/json_schema_summary/0000775000175000017500000000000015140154751025304 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/json_schema_summary/if-then-else.example0000664000175000017500000000376515140154751031154 0ustar  carstencarsten{
    "type": "object",
    "properties": {
        "street_address": {"type": "string"},
        "country": {
            "default": "United States of America",
            "enum": ["United States of America", "Canada", "Netherlands"]
        }
    },
    "allOf": [
        {
            "if": {
                "properties": {
                    "country": {"const": "United States of America"}
                }
            },
            "then": {
                "properties": {
                    "postal_code": {"pattern": "[0-9]{5}(-[0-9]{4})?"}
                }
            }
        },
        {
            "if": {
                "properties": {"country": {"const": "Canada"}},
                "required": ["country"]
            },
            "then": {
                "properties": {
                    "postal_code": {
                        "pattern": "[A-Z][0-9][A-Z] [0-9][A-Z][0-9]"
                    }
                }
            }
        },
        {
            "if": {
                "properties": {"country": {"const": "Netherlands"}},
                "required": ["country"]
            },
            "then": {
                "properties": {
                    "postal_code": {"pattern": "[0-9]{4} [A-Z]{2}"}
                }
            }
        }
    ]
}
# - # - # - #
type: object
properties:
  'street_address': {type: string}
  'country': {one of: ['United States of America', 'Canada', 'Netherlands']}
all of the following:
  - if:
      properties:
        'country': {predefined value: 'United States of America'}
    then:
      properties:
        'postal_code': {pattern: '[0-9]{5}(-[0-9]{4})?'}
  - if:
      properties:
        'country': {predefined value: 'Canada'}
      required: ['country']
    then:
      properties:
        'postal_code': {pattern: '[A-Z][0-9][A-Z] [0-9][A-Z][0-9]'}
  - if:
      properties:
        'country': {predefined value: 'Netherlands'}
      required: ['country']
    then:
      properties:
        'postal_code': {pattern: '[0-9]{4} [A-Z]{2}'}
abravalheri-validate-pyproject-4b2e70d/tests/json_schema_summary/object-property-names.example0000664000175000017500000000042215140154751033110 0ustar  carstencarsten{
    "type": "object",
    "properties": {"type": {"enum": ["A", "B"]}},
    "propertyNames": {"pattern": "a*", "maxLength": 8}
}
# - # - # - #
type: object
properties:
  'type': {one of: ['A', 'B']}
non-predefined acceptable property names: {pattern: 'a*', max length: 8}
abravalheri-validate-pyproject-4b2e70d/tests/json_schema_summary/oneof.example0000664000175000017500000000103615140154751027767 0ustar  carstencarsten{
    "type": "object",
    "properties": {
        "type": {"enum": ["A", "B"]}
    },
    "propertyNames": {
        "oneOf": [
            {"const": "*"},
            {"pattern": "a*", "minLength": 8}
        ]
    },
    "additionalProperties": false,
    "required": ["type"]
}
# - # - # - #
type: object
properties:
  'type': {one of: ['A', 'B']}
non-predefined acceptable property names:
  exactly one of the following:
    - {predefined value: '*'}
    - {pattern: 'a*', min length: 8}
additional properties: False
required: ['type']
abravalheri-validate-pyproject-4b2e70d/tests/json_schema_summary/if-then-else2.example0000664000175000017500000000075715140154751031234 0ustar  carstencarsten{
    "type": [
        "integer",
        "string"
    ],
    "if": {
        "type": "integer"
    },
    "then": {
        "type": "integer",
        "maximum": 9999,
        "minimum": 0
    },
    "else": {
        "type": "string",
        "maxLength": 4,
        "minLength": 1,
        "pattern": "\\d+"
    }
}
# - # - # - #
type: [integer, string]
if: {type: integer}
then: {type: integer, maximum: 9999, minimum: 0}
else: {type: string, max length: 4, min length: 1, pattern: '\\d+'}
abravalheri-validate-pyproject-4b2e70d/tests/json_schema_summary/not.example0000664000175000017500000000110615140154751027457 0ustar  carstencarsten{
    "properties": {
        "type": {"enum": ["A", "B"]}
    },
    "propertyNames": {
        "not": {
            "anyOf": [
                {"const": "*"},
                {"pattern": ".*", "minLength": 8}
            ]
        }
    },
    "additionalProperties": false,
    "required": ["type"]
}
# - # - # - #
properties:
  'type': {one of: ['A', 'B']}
non-predefined acceptable property names:
  (*NOT* the following):
    at least one of the following:
      - {predefined value: '*'}
      - {pattern: '.*', min length: 8}
additional properties: False
required: ['type']
abravalheri-validate-pyproject-4b2e70d/tests/json_schema_summary/array-prefix-items.example0000664000175000017500000000062715140154751032416 0ustar  carstencarsten{
    "type": "array",
    "prefixItems": [
        {"type": "number"},
        {"type": "boolean"}
    ],
    "contains": {"type": "string", "pattern": "a*", "maxLength": 8},
    "minItems": 5,
    "uniqueItems": true
}
# - # - # - #
type: array
items (in order):
  - {type: number}
  - {type: boolean}
contains at least one of: {type: string, pattern: 'a*', max length: 8}
min items: 5
unique items: True
abravalheri-validate-pyproject-4b2e70d/tests/json_schema_summary/object-no-properties.example0000664000175000017500000000006015140154751032727 0ustar  carstencarsten{"type": "object"}
# - # - # - #
{type: object}
abravalheri-validate-pyproject-4b2e70d/tests/json_schema_summary/array-simple.example0000664000175000017500000000016715140154751031272 0ustar  carstencarsten{
    "type": "array",
    "items": {
        "type": "number"
    }
}
# - # - # - #
type: array
items: {type: number}
abravalheri-validate-pyproject-4b2e70d/tests/json_schema_summary/object-pattern-properties.example0000664000175000017500000000044515140154751033777 0ustar  carstencarsten{
    "type": "object",
    "properties": {"number": {"type": "number"}},
    "patternProperties": {"^.*": {"not": {"type": "string"}}}
}
# - # - # - #
type: object
properties:
  'number': {type: number}
properties named via pattern:
  (regex '^.*'):
    (*NOT* the following): {type: string}
abravalheri-validate-pyproject-4b2e70d/tests/json_schema_summary/array-no-items.example0000664000175000017500000000005615140154751031531 0ustar  carstencarsten{"type": "array"}
# - # - # - #
{type: array}
abravalheri-validate-pyproject-4b2e70d/tests/json_schema_summary/array-contains.example0000664000175000017500000000036515140154751031617 0ustar  carstencarsten{
    "type": "array",
    "items": {"type": "number"},
    "contains": {"type": "string", "pattern": "a*", "maxLength": 8}
}
# - # - # - #
type: array
items: {type: number}
contains at least one of: {type: string, pattern: 'a*', max length: 8}
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/0000775000175000017500000000000015140154751024500 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/simple/0000775000175000017500000000000015140154751025771 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/simple/pep639.toml0000664000175000017500000000022015140154751027706 0ustar  carstencarsten[project]
name = "example"
version = "1.2.3"
license = "Apache Software License"  # should be "Apache-2.0"
license-files = ["licenses/LICENSE"]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/simple/pep639.errors.txt0000664000175000017500000000011415140154751031067 0ustar  carstencarsten`project.license` must be valid exactly by one definition (0 matches found)
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/simple/pep794-missing-parents.toml0000664000175000017500000000010515140154751033033 0ustar  carstencarsten[project]
name = "test"
version = "1.0.0"
import-names = ["one.two"]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/simple/pep794-not-private.toml0000664000175000017500000000011115140154751032155 0ustar  carstencarsten[project]
name = "test"
version = "1.0.0"
import-names = ["one; public"]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/simple/pep794-keyword.errors.txt0000664000175000017500000000006215140154751032555 0ustar  carstencarstenproject.import-namespaces[0]` must be import-name
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/simple/pep794-dup.toml0000664000175000017500000000014415140154751030503 0ustar  carstencarsten[project]
name = "hi"
version = "1.0.0"
import-names = ["one"]
import-namespaces = ["one; private"]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/simple/pep794-not-private.errors.txt0000664000175000017500000000005515140154751033343 0ustar  carstencarstenproject.import-names[0]` must be import-name
././@LongLink0000644000000000000000000000014700000000000011605 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/simple/pep794-missing-parents.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/simple/pep794-missing-parents.errors.t0000664000175000017500000000012315140154751033636 0ustar  carstencarstenAll parents of an import name must also be listed in import-namespace/import-names
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/simple/pep794-keyword.toml0000664000175000017500000000010615140154751031375 0ustar  carstencarsten[project]
name = "hi"
version = "1.0.0"
import-namespaces = ["class"]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/simple/pep794-dup.errors.txt0000664000175000017500000000010315140154751031655 0ustar  carstencarstenDuplicated names are not allowed in import-names/import-namespaces
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/store/0000775000175000017500000000000015140154751025634 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/store/ruff-unknown.errors.txt0000664000175000017500000000007615140154751032352 0ustar  carstencarsten`tool.ruff` must not contain {'not-a-real-option'} properties
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/store/test_config.json0000664000175000017500000000007715140154751031037 0ustar  carstencarsten{
    "store": "https://json.schemastore.org/pyproject.json"
}
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/store/cibw-overrides-noaction.toml0000664000175000017500000000012415140154751033262 0ustar  carstencarsten[tool.cibuildwheel]
build = "*"

[[tool.cibuildwheel.overrides]]
select = "cp312-*"
././@LongLink0000644000000000000000000000014700000000000011605 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/store/cibw-overrides-noselect.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/store/cibw-overrides-noselect.errors.t0000664000175000017500000000010415140154751034065 0ustar  carstencarsten`tool.cibuildwheel.overrides[0]` must contain ['select'] properties
././@LongLink0000644000000000000000000000014700000000000011605 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/store/cibw-overrides-noaction.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/store/cibw-overrides-noaction.errors.t0000664000175000017500000000010415140154751034063 0ustar  carstencarsten`tool.cibuildwheel.overrides[0]` must contain at least 2 properties
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/store/ruff-badcode.errors.txt0000664000175000017500000000006715140154751032234 0ustar  carstencarsten`tool.ruff.lint` cannot be validated by any definition
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/store/cibw-unknown-option.toml0000664000175000017500000000005715140154751032462 0ustar  carstencarsten[tool.cibuildwheel]
no-a-read-option = "error"
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/store/ruff-unknown.toml0000664000175000017500000000004515140154751031167 0ustar  carstencarsten[tool.ruff]
not-a-real-option = true
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/store/cibw-overrides-noselect.toml0000664000175000017500000000015615140154751033271 0ustar  carstencarsten[tool.cibuildwheel]
build = "*"

[[tool.cibuildwheel.overrides]]
test-command = "pytest"
test-extras = "test"
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/store/cibw-unknown-option.errors.txt0000664000175000017500000000010515140154751033633 0ustar  carstencarsten`tool.cibuildwheel` must not contain {'no-a-read-option'} properties
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/store/ruff-badcode.toml0000664000175000017500000000005615140154751031053 0ustar  carstencarsten[tool.ruff.lint]
extend-select = ["NOTACODE"]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/0000775000175000017500000000000015140154751025515 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/dynamic/0000775000175000017500000000000015140154751027141 5ustar  carstencarsten././@LongLink0000644000000000000000000000017600000000000011607 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/dynamic/static_entry_points_listed_as_dynamic.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/dynamic/static_entry_points_lis0000664000175000017500000000014715140154751034041 0ustar  carstencarstencannot provide a value for `project.entry-points` and list it under `project.dynamic` at the same time
././@LongLink0000644000000000000000000000017000000000000011601 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/dynamic/static_entry_points_listed_as_dynamic.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/dynamic/static_entry_points_lis0000664000175000017500000000035615140154751034043 0ustar  carstencarsten[build-system]
requires = ["setuptools>=67.5"]
build-backend = "setuptools.build_meta"

[project]
name = "timmins"
dynamic = ["version", "entry-points"]

[project.entry-points."timmins.display"]
excl = "timmins_plugin_fancy:excl_display"
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/pep639/0000775000175000017500000000000015140154751026543 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/pep639/bothstyles.toml0000664000175000017500000000013415140154751031636 0ustar  carstencarsten[project]
name = "x"
version = "1.2.3"
license.text = "Something"
license-files = ["value"]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/pep639/bothstyles.errors.txt0000664000175000017500000000004115140154751033012 0ustar  carstencarsten`project.license` must be string
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/incorrect-subtables/0000775000175000017500000000000015140154751031467 5ustar  carstencarsten././@LongLink0000644000000000000000000000017500000000000011606 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/incorrect-subtables/author_with_extra_fields.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/incorrect-subtables/author_with0000664000175000017500000000004715140154751033750 0ustar  carstencarstenmust not contain {'author'} properties
././@LongLink0000644000000000000000000000016700000000000011607 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/incorrect-subtables/author_with_extra_fields.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/incorrect-subtables/author_with0000664000175000017500000000120315140154751033743 0ustar  carstencarsten[build-system]
requires = ["setuptools", "setuptools_scm[toml]"]
build-backend = "setuptools.build_meta"


[project]
name = "package"
description = "Package Description"
readme = "README.rst"
authors = [{author="Author", email="author@gmail.com"}]
license = {file="LICENSE"}
requires-python = ">=3.6"
dynamic = ["version"]
dependencies = [
    "pywin32; platform_system=='Windows' and platform_python_implementation!='PyPy'",
]
classifiers = [
    "Development Status :: 5 - Production/Stable",
]
keywords = [
    "cli",
]

[project.optional-dependencies]
dev = [
    "pytest>=6.0",
]
docs = [
    "sphinx>=4.0.0",
]
ssh = [
    "paramiko",
]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/missing-fields/0000775000175000017500000000000015140154751030432 5ustar  carstencarsten././@LongLink0000644000000000000000000000017400000000000011605 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/missing-fields/missing-version-with-dynamic.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/missing-fields/missing-version-0000664000175000017500000000005615140154751033567 0ustar  carstencarsten`project` must contain ['version'] properties
././@LongLink0000644000000000000000000000015400000000000011603 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/missing-fields/empty-author.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/missing-fields/empty-author.err0000664000175000017500000000007315140154751033602 0ustar  carstencarsten`project.authors[0]` cannot be validated by any definition
././@LongLink0000644000000000000000000000015700000000000011606 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/missing-fields/missing-version.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/missing-fields/missing-version.0000664000175000017500000000005615140154751033570 0ustar  carstencarsten`project` must contain ['version'] properties
././@LongLink0000644000000000000000000000014600000000000011604 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/missing-fields/empty-author.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/missing-fields/empty-author.tom0000664000175000017500000000006615140154751033613 0ustar  carstencarsten[project]
name = 'foo'
version = '1.0'
authors = [{}]
././@LongLink0000644000000000000000000000015100000000000011600 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/missing-fields/missing-version.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/missing-fields/missing-version.0000664000175000017500000000003015140154751033560 0ustar  carstencarsten[project]
name = "proj"
././@LongLink0000644000000000000000000000016600000000000011606 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/missing-fields/missing-version-with-dynamic.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/missing-fields/missing-version-0000664000175000017500000000004515140154751033565 0ustar  carstencarsten[project]
name = "proj"
dynamic = []
././@LongLink0000644000000000000000000000014600000000000011604 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/non-standardised-project-fields/abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/non-standardised-project-fields0000775000175000017500000000000015140154751033603 5ustar  carstencarsten././@LongLink0000644000000000000000000000021300000000000011577 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/non-standardised-project-fields/requires_instead_of_dependencies.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/non-standardised-project-fields0000664000175000017500000000223615140154751033610 0ustar  carstencarsten[build-system]
requires = ["setuptools", "setuptools_scm[toml]"]
build-backend = "setuptools.build_meta"


[project]
name = "package"
description = "Package Description"
readme = "README.rst"
authors = [{name="Author", email="author@gmail.com"}]
license = {file="LICENSE"}
requires-python = ">=3.6"
dynamic = ["version"]
requires = [
    "pywin32; platform_system=='Windows' and platform_python_implementation!='PyPy'",
]
classifiers = [
    "Development Status :: 5 - Production/Stable",
]
keywords = [
    "cli",
]

[project.optional-dependencies]
dev = [
    "pytest>=6.0",
]
docs = [
    "sphinx>=4.0.0",
]
ssh = [
    "paramiko",
]

[tool.setuptools]
platforms = ["POSIX", "Windows"]

[tool.setuptools.packages.find]
include = ["plumbum"]

[tool.setuptools.package-data]
"plumbum.cli" = ["i18n/*/LC_MESSAGES/*.mo"]


[tool.setuptools_scm]
write_to = "plumbum/version.py"


[tool.mypy]
files = ["plumbum"]

[[tool.mypy.overrides]]
module = ["IPython.*"]
ignore_missing_imports = true


[tool.pytest.ini_options]
minversion = "6.0"
addopts = ["-ra", "--cov-config=setup.cfg", "--strict-markers", "--strict-config"]
filterwarnings = [
  "always"
]

[tool.isort]
profile = "black"
././@LongLink0000644000000000000000000000022100000000000011576 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/non-standardised-project-fields/requires_instead_of_dependencies.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/non-standardised-project-fields0000664000175000017500000000006315140154751033604 0ustar  carstencarsten`project` must not contain {'requires'} properties
././@LongLink0000644000000000000000000000021200000000000011576 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/non-standardised-project-fields/author_instead_of_authors.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/non-standardised-project-fields0000664000175000017500000000006115140154751033602 0ustar  carstencarsten`project` must not contain {'author'} properties
././@LongLink0000644000000000000000000000020400000000000011577 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/non-standardised-project-fields/author_instead_of_authors.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep621/non-standardised-project-fields0000664000175000017500000000223715140154751033611 0ustar  carstencarsten[build-system]
requires = ["setuptools", "setuptools_scm[toml]"]
build-backend = "setuptools.build_meta"


[project]
name = "package"
description = "Package Description"
readme = "README.rst"
author = {name="Author", email="author@gmail.com"}
license = {file="LICENSE"}
requires-python = ">=3.6"
dynamic = ["version"]
dependencies = [
    "pywin32; platform_system=='Windows' and platform_python_implementation!='PyPy'",
]
classifiers = [
    "Development Status :: 5 - Production/Stable",
]
keywords = [
    "cli",
]

[project.optional-dependencies]
dev = [
    "pytest>=6.0",
]
docs = [
    "sphinx>=4.0.0",
]
ssh = [
    "paramiko",
]

[tool.setuptools]
platforms = ["POSIX", "Windows"]

[tool.setuptools.packages.find]
include = ["plumbum"]

[tool.setuptools.package-data]
"plumbum.cli" = ["i18n/*/LC_MESSAGES/*.mo"]


[tool.setuptools_scm]
write_to = "plumbum/version.py"


[tool.mypy]
files = ["plumbum"]

[[tool.mypy.overrides]]
module = ["IPython.*"]
ignore_missing_imports = true


[tool.pytest.ini_options]
minversion = "6.0"
addopts = ["-ra", "--cov-config=setup.cfg", "--strict-markers", "--strict-config"]
filterwarnings = [
  "always"
]

[tool.isort]
profile = "black"
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/0000775000175000017500000000000015140154751026721 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/dynamic/0000775000175000017500000000000015140154751030345 5ustar  carstencarsten././@LongLink0000644000000000000000000000016000000000000011600 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/dynamic/readme-missing-file.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/dynamic/readme-missing-file0000664000175000017500000000010215140154751034102 0ustar  carstencarsten`tool.setuptools.dynamic.readme` must contain ['file'] properties
././@LongLink0000644000000000000000000000015200000000000011601 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/dynamic/readme-missing-file.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/dynamic/readme-missing-file0000664000175000017500000000007515140154751034113 0ustar  carstencarsten[tool.setuptools.dynamic.readme]
content-type = "text/plain"
././@LongLink0000644000000000000000000000015400000000000011603 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/dynamic/readme-too-many.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/dynamic/readme-too-many.err0000664000175000017500000000010715140154751034053 0ustar  carstencarsten`tool.setuptools.dynamic.readme` cannot be validated by any definition
././@LongLink0000644000000000000000000000014600000000000011604 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/dynamic/readme-too-many.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/dynamic/readme-too-many.tom0000664000175000017500000000017515140154751034067 0ustar  carstencarsten[tool.setuptools.dynamic.readme]
file = ["README.md"]
content-type = "text/plain"
something-else = "not supposed to be here"
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/0000775000175000017500000000000015140154751027736 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/license/0000775000175000017500000000000015140154751031360 5ustar  carstencarsten././@LongLink0000644000000000000000000000015100000000000011600 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/license/empty.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/license/empty.errors0000664000175000017500000000011415140154751033750 0ustar  carstencarsten`project.license` must be valid exactly by one definition (0 matches found)
././@LongLink0000644000000000000000000000016600000000000011606 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/license/both-text-and-file.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/license/both-text-an0000664000175000017500000000011415140154751033611 0ustar  carstencarsten`project.license` must be valid exactly by one definition (2 matches found)
././@LongLink0000644000000000000000000000016000000000000011600 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/license/both-text-and-file.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/license/both-text-an0000664000175000017500000000130415140154751033613 0ustar  carstencarsten[project]
name = "some-project"
author = { name = "Anderson Bravalheri" }
description = "Some description"
readme = "README.rst"
license = { text = "MIT", file = "LICENSE.txt" }
classifiers = [
    "Development Status :: 5 - Production/Stable",
    "Topic :: Utilities",
]
dynamic = ["version"]
requires-python = ">=3.6"
dependencies = [
    "importlib-metadata; python_version<\"3.8\"",
    "appdirs>=1.4.4,<2",
]

[tool.setuptools]
zip-safe = false
include-package-data = true
exclude-package-data = { "pkg1" = ["*.yaml"] }
package-dir = {"" = "src"} # all the packages under the src folder
platforms = ["any"]

[tool.setuptools.packages]
find = { where = ["src"], exclude = ["tests"], namespaces = true }
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/license/empty.toml0000664000175000017500000000124015140154751033410 0ustar  carstencarsten[project]
name = "some-project"
author = { name = "Anderson Bravalheri" }
description = "Some description"
readme = "README.rst"
license = {}
classifiers = [
    "Development Status :: 5 - Production/Stable",
    "Topic :: Utilities",
]
dynamic = ["version"]
requires-python = ">=3.6"
dependencies = [
    "importlib-metadata; python_version<\"3.8\"",
    "appdirs>=1.4.4,<2",
]

[tool.setuptools]
zip-safe = false
include-package-data = true
exclude-package-data = { "pkg1" = ["*.yaml"] }
package-dir = {"" = "src"} # all the packages under the src folder
platforms = ["any"]

[tool.setuptools.packages]
find = { where = ["src"], exclude = ["tests"], namespaces = true }
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/readme/0000775000175000017500000000000015140154751031173 5ustar  carstencarsten././@LongLink0000644000000000000000000000017000000000000011601 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/readme/readme-without-content-type.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/readme/readme-withou0000664000175000017500000000211315140154751033665 0ustar  carstencarsten[project]
name = "some-project"
author = { name = "Anderson Bravalheri" }
description = "Some description"
license = { text = "MIT" }
readme = { file = "README.rst" }
classifiers = [
    "Development Status :: 5 - Production/Stable",
    "Topic :: Utilities",
    "Programming Language :: Python",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3 :: Only",
    "Environment :: Console",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Operating System :: POSIX :: Linux",
    "Operating System :: Unix",
    "Operating System :: MacOS",
    "Operating System :: Microsoft :: Windows",
]
dynamic = ["version"]
requires-python = ">=3.6"
dependencies = [
    "importlib-metadata; python_version<\"3.8\"",
    "appdirs>=1.4.4,<2",
]

[tool.setuptools]
zip-safe = false
include-package-data = true
exclude-package-data = { "pkg1" = ["*.yaml"] }
package-dir = {"" = "src"} # all the packages under the src folder
platforms = ["any"]

[tool.setuptools.packages]
find = { where = ["src"], exclude = ["tests"], namespaces = true }
././@LongLink0000644000000000000000000000015400000000000011603 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/readme/readme-as-array.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/readme/readme-as-arr0000664000175000017500000000210215140154751033531 0ustar  carstencarsten[project]
name = "some-project"
author = { name = "Anderson Bravalheri" }
description = "Some description"
license = { text = "MIT" }
readme = ["README.rst"]
classifiers = [
    "Development Status :: 5 - Production/Stable",
    "Topic :: Utilities",
    "Programming Language :: Python",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3 :: Only",
    "Environment :: Console",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Operating System :: POSIX :: Linux",
    "Operating System :: Unix",
    "Operating System :: MacOS",
    "Operating System :: Microsoft :: Windows",
]
dynamic = ["version"]
requires-python = ">=3.6"
dependencies = [
    "importlib-metadata; python_version<\"3.8\"",
    "appdirs>=1.4.4,<2",
]

[tool.setuptools]
zip-safe = false
include-package-data = true
exclude-package-data = { "pkg1" = ["*.yaml"] }
package-dir = {"" = "src"} # all the packages under the src folder
platforms = ["any"]

[tool.setuptools.packages]
find = { where = ["src"], exclude = ["tests"], namespaces = true }
././@LongLink0000644000000000000000000000017600000000000011607 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/readme/readme-without-content-type.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/readme/readme-withou0000664000175000017500000000011315140154751033663 0ustar  carstencarsten`project.readme` must be valid exactly by one definition (0 matches found)
././@LongLink0000644000000000000000000000016200000000000011602 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/readme/readme-as-array.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/pep621/readme/readme-as-arr0000664000175000017500000000011315140154751033531 0ustar  carstencarsten`project.readme` must be valid exactly by one definition (0 matches found)
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/ext-modules/0000775000175000017500000000000015140154751031167 5ustar  carstencarsten././@LongLink0000644000000000000000000000015600000000000011605 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/ext-modules/invalid-field.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/ext-modules/invalid-field.e0000664000175000017500000000005015140154751034037 0ustar  carstencarstenmust not contain {'non-existing-field'}
././@LongLink0000644000000000000000000000015300000000000011602 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/ext-modules/missing-ext-name.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/ext-modules/missing-ext-nam0000664000175000017500000000010115140154751034122 0ustar  carstencarsten[[tool.setuptools.ext-modules]]
sources = ["hello.c", "world.c"]
././@LongLink0000644000000000000000000000015000000000000011577 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/ext-modules/invalid-field.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/ext-modules/invalid-field.t0000664000175000017500000000015015140154751034057 0ustar  carstencarsten[[tool.setuptools.ext-modules]]
name = "hello.world"
sources = ["hello.c"]
non-existing-field = "hello"
././@LongLink0000644000000000000000000000016000000000000011600 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/ext-modules/invalid-sources.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/ext-modules/invalid-sources0000664000175000017500000000001615140154751034216 0ustar  carstencarstenmust be array
././@LongLink0000644000000000000000000000015200000000000011601 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/ext-modules/invalid-sources.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/ext-modules/invalid-sources0000664000175000017500000000011115140154751034212 0ustar  carstencarsten[[tool.setuptools.ext-modules]]
name = "hello.world"
sources = "hello.c"
././@LongLink0000644000000000000000000000016100000000000011601 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/ext-modules/missing-ext-name.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/ext-modules/missing-ext-nam0000664000175000017500000000002615140154751034130 0ustar  carstencarstenmust contain ['name']
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/attr/0000775000175000017500000000000015140154751027673 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/attr/missing-attr-name.toml0000664000175000017500000000070415140154751034130 0ustar  carstencarsten# Issue pypa/setuptools#3928
# https://github.com/RonnyPfannschmidt/reproduce-setuptools-dynamic-attr
[build-system]
build-backend = "_own_version_helper"
backend-path = ["."]
requires = ["setuptools" ]

[project]
name = "ronnypfannschmidt.setuptools-build-attr-error-reproduce"
description = "reproducer for a setuptools issue"
requires-python = ">=3.7"
dynamic = [
  "version",
]

[tool.setuptools.dynamic]
version = { attr = "_own_version_helper."}
././@LongLink0000644000000000000000000000015300000000000011602 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/attr/missing-attr-name.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/attr/missing-attr-name.erro0000664000175000017500000000021015140154751034114 0ustar  carstencarsten`tool.setuptools.dynamic.version` must be valid exactly by one definition
'attr': {type: string, format: 'python-qualified-identifier'}
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/packages/0000775000175000017500000000000015140154751030477 5ustar  carstencarsten././@LongLink0000644000000000000000000000016400000000000011604 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/packages/missing-find-arguments.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/packages/missing-find-argum0000664000175000017500000000012515140154751034120 0ustar  carstencarsten`tool.setuptools.packages` must be valid exactly by one definition (0 matches found)
././@LongLink0000644000000000000000000000015600000000000011605 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/packages/missing-find-arguments.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/packages/missing-find-argum0000664000175000017500000000233215140154751034122 0ustar  carstencarsten[project]
name = "package"
description = "description"
authors = [{ name = "Name", email = "email@example.com" }]
readme = "README.rst"
classifiers = [
    "Development Status :: 2 - Pre-Alpha",
    "Environment :: Web Environment",
]
dynamic = ["version"]
requires-python = ">=3.8"
dependencies = [
    "backports.zoneinfo; python_version<\"3.9\"",
    "tzdata; sys_platform == 'win32'",
]

[project.license]
text = "BSD-3-Clause"

[project.urls]
Homepage = "https://www.example.com/"
Documentation = "https://docs.example.com/"

[project.optional-dependencies]
argon2 = ["argon2-cffi >= 19.1.0"]

[project.scripts]
run = "project.__main__:main"

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
packages = {find = ""}
include-package-data = true
zip-safe = false

[tool.setuptools.dynamic]
version = {attr = "project.__version__"}

[tool.setuptools.command.bdist-rpm]
doc-files = "docs extras AUTHORS INSTALL LICENSE README.rst"
install-script = "scripts/rpm-install.sh"

[tool.flake8]
exclude = "build,.git,.tox,./tests/.env"
ignore = "W504"
max-line-length = "999"

[tool.isort]
default_section = "THIRDPARTY"
include_trailing_comma = true
line_length = 4
multi_line_output = 6
././@LongLink0000644000000000000000000000015100000000000011600 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/packages/invalid-stub-name.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/packages/invalid-stub-name.0000664000175000017500000000046315140154751034022 0ustar  carstencarsten# Setuptools should allow stub-only package names in `packages` (PEP 561)
[build-system]
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"

[project]
name = "mypkg-stubs"
version = "0.0.0"

[tool.setuptools]
platforms = ["any"]
packages = ["should-be-an-identifier-stubs"]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/packages/invalid-name.toml0000664000175000017500000000044715140154751033745 0ustar  carstencarsten# Setuptools should allow stub-only package names in `packages` (PEP 561)
[build-system]
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"

[project]
name = "mypkg-stubs"
version = "0.0.0"

[tool.setuptools]
platforms = ["any"]
packages = ["-not-a-valid-name"]
././@LongLink0000644000000000000000000000015700000000000011606 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/packages/invalid-stub-name.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/packages/invalid-stub-name.0000664000175000017500000000024315140154751034016 0ustar  carstencarsten`tool.setuptools.packages` must be valid exactly by one definition
{type: string, format: 'python-module-name-relaxed'}
{type: string, format: 'pep561-stub-name'}
././@LongLink0000644000000000000000000000015200000000000011601 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/packages/invalid-name.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/packages/invalid-name.error0000664000175000017500000000024315140154751034115 0ustar  carstencarsten`tool.setuptools.packages` must be valid exactly by one definition
{type: string, format: 'python-module-name-relaxed'}
{type: string, format: 'pep561-stub-name'}
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/package-dir/0000775000175000017500000000000015140154751031070 5ustar  carstencarsten././@LongLink0000644000000000000000000000015500000000000011604 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/package-dir/invalid-stub.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/package-dir/invalid-stub.er0000664000175000017500000000021515140154751034017 0ustar  carstencarstenexactly one of the following:
{predefined value: ''}
{type: string, format: 'python-module-name'}
{type: string, format: 'pep561-stub-name'}
././@LongLink0000644000000000000000000000014700000000000011605 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/package-dir/invalid-name.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/package-dir/invalid-name.to0000664000175000017500000000140415140154751033777 0ustar  carstencarsten[project]
name = "project"
description = "description"
license = { text = "BSD-3-Clause" }
dynamic = ["version"]
requires-python = ">= 3.6"

[[project.authors]]
name = "Name 1"
email = "name1@example1.com"

[[project.authors]]
name = "Name 2"
email = "name2@example2.com"

[project.readme]
file = "README.rst"
content-type = "text/x-rst"

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
package-dir = {"-" = "src"}
include-package-data = true
script-files = [
    "bin/run.py"
]

[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.dynamic]
version = {file = "__version__.txt"}

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.coverage.paths]
source = [
    "src",
    "*/site-packages",
]
././@LongLink0000644000000000000000000000015500000000000011604 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/package-dir/invalid-name.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/package-dir/invalid-name.er0000664000175000017500000000006415140154751033764 0ustar  carstencarsten`tool.setuptools.package-dir` keys must be named by
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/dependencies/0000775000175000017500000000000015140154751031347 5ustar  carstencarsten././@LongLink0000644000000000000000000000015600000000000011605 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/dependencies/invalid-extra-name.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/dependencies/invalid-extra-0000664000175000017500000000025315140154751034116 0ustar  carstencarsten[project]
name = "myproj"
version = "42"
dynamic = ["optional-dependencies"]

[tool.setuptools.dynamic.optional-dependencies."not a Python identifier"]
file = "extra.txt"
././@LongLink0000644000000000000000000000016400000000000011604 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/dependencies/invalid-extra-name.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/dependencies/invalid-extra-0000664000175000017500000000017015140154751034114 0ustar  carstencarsten`tool.setuptools.dynamic.optional-dependencies` keys must be named by:

    {type: string, format: 'pep508-identifier'}
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/cmdclass/0000775000175000017500000000000015140154751030512 5ustar  carstencarsten././@LongLink0000644000000000000000000000015300000000000011602 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/cmdclass/invalid-value.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/cmdclass/invalid-value.erro0000664000175000017500000000010515140154751034137 0ustar  carstencarsten`tool.setuptools.cmdclass.sdist` must be python-qualified-identifier
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/setuptools/cmdclass/invalid-value.toml0000664000175000017500000000151115140154751034145 0ustar  carstencarsten[project]
name = "project"
description = "description"
license = { text = "BSD-3-Clause" }
dynamic = ["version"]
requires-python = ">= 3.6"

[[project.authors]]
name = "Name 1"
email = "name1@example1.com"

[[project.authors]]
name = "Name 2"
email = "name2@example2.com"

[project.readme]
file = "README.rst"
content-type = "text/x-rst"

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
package-dir = {"" = "src"}
include-package-data = true
script-files = [
    "bin/run.py"
]

[tool.setuptools.cmdclass]
sdist = "pkg.my-invalid:mod.Custom~Sdist"

[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.dynamic]
version = {file = "__version__.txt"}

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.coverage.paths]
source = [
    "src",
    "*/site-packages",
]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/ruff/0000775000175000017500000000000015140154751025442 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/ruff/unknown.toml0000664000175000017500000000004515140154751030035 0ustar  carstencarsten[tool.ruff]
not-a-real-option = true
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/ruff/test_config.json0000664000175000017500000000012215140154751030634 0ustar  carstencarsten{
    "tools": {
        "ruff": "https://json.schemastore.org/ruff.json"
    }
}
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/ruff/unknown.errors.txt0000664000175000017500000000007615140154751031220 0ustar  carstencarsten`tool.ruff` must not contain {'not-a-real-option'} properties
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/ruff/badcode.errors.txt0000664000175000017500000000006715140154751031102 0ustar  carstencarsten`tool.ruff.lint` cannot be validated by any definition
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/ruff/badcode.toml0000664000175000017500000000005615140154751027721 0ustar  carstencarsten[tool.ruff.lint]
extend-select = ["NOTACODE"]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pdm/0000775000175000017500000000000015140154751025260 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pdm/LICENSE0000664000175000017500000000206015140154751026263 0ustar  carstencarstenMIT License

Copyright (c) 2019-2021 Frost Ming

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.
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pdm/invalid-version/0000775000175000017500000000000015140154751030371 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pdm/invalid-version/pyproject.toml0000664000175000017500000001026615140154751033312 0ustar  carstencarsten[project]
# PEP 621 project metadata
# See https://peps.python.org/pep-0621/
authors = [
    {name = "frostming", email = "mianghong@gmail.com"},
]
dynamic = ["version", "classifiers"]
version = {use_scm = true}
requires-python = ">=3.7"
license = {text = "MIT"}
dependencies = [
    "appdirs",
    "atoml>=1.0.3",
    "click>=7",
    "importlib-metadata; python_version < \"3.8\"",
    "installer~=0.3.0",
    "packaging",
    "pdm-pep517>=0.8.3,<0.9",
    "pep517>=0.11.0",
    "pip>=20.1",
    "python-dotenv~=0.15",
    "pythonfinder",
    "resolvelib>=0.7.0,<0.8.0",
    "shellingham<2.0.0,>=1.3.2",
    "tomli>=1.1.0,<2.0.0",
    "typing-extensions; python_version < \"3.8\"",
    "wheel<1.0.0,>=0.36.2",
]
name = "pdm"
description = "Python Development Master"
readme = "README.md"
keywords = ["packaging", "dependency", "workflow"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Topic :: Software Development :: Build Tools",
]

[project.urls]
homepage = "https://pdm.fming.dev"
Repository = "https://github.com/pdm-project/pdm"
Documentation = "https://pdm.fming.dev"

[project.optional-dependencies]

[project.scripts]
pdm = "pdm.core:main"

[tool.pdm]
includes = ["pdm"]
source-includes = ["tests"]
# editables backend doesn't work well with namespace packages
editable-backend = "path"

[tool.pdm.scripts]
release = "python tasks/release.py"
test = "pytest tests/"
doc = {shell = "cd docs && mkdocs serve", help = "Start the dev server for doc preview"}
lint = "pre-commit run --all-files"
complete = {call = "tasks.complete:main"}

[tool.pdm.dev-dependencies]
test = [
    "pytest",
    "pytest-cov",
    "pytest-mock",
    "pytest-xdist<2.0.0,>=1.31.0"
]
doc = [
    "mkdocs<2.0.0,>=1.1",
    "mkdocs-material<7.0.0,>=6.2.4",
    "markdown-include<1.0.0,>=0.5.1"
]
workflow = [
    "parver<1.0.0,>=0.3.1",
    "towncrier<20.0.0,>=19.2.0",
    "vendoring; python_version ~= \"3.8\"",
    "mypy~=0.812",
    "pycomplete~=0.3"
]

[tool.black]
line-length = 88
exclude = '''
/(
    \.eggs
  | \.git
  | \.hg
  | \.mypy_cache
  | \.tox
  | \.venv
  | _build
  | buck-out
  | build
  | dist
  | pdm/_vendor
  | tests/fixtures
)/
'''

[tool.towncrier]
package = "pdm"
filename = "CHANGELOG.md"
issue_format = "[#{issue}](https://github.com/pdm-project/pdm/issues/{issue})"
directory = "news/"
title_format = "Release v{version} ({project_date})"
template = "news/towncrier_template.md"
underlines = "-~^"

  [[tool.towncrier.type]]
  directory = "feature"
  name = "Features & Improvements"
  showcontent = true

  [[tool.towncrier.type]]
  directory = "bugfix"
  name = "Bug Fixes"
  showcontent = true

  [[tool.towncrier.type]]
  directory = "doc"
  name = "Improved Documentation"
  showcontent = true

  [[tool.towncrier.type]]
  directory = "dep"
  name = "Dependencies"
  showcontent = true

  [[tool.towncrier.type]]
  directory = "removal"
  name = "Removals and Deprecations"
  showcontent = true

  [[tool.towncrier.type]]
  directory = "misc"
  name = "Miscellany"
  showcontent = true

  [[tool.towncrier.type]]
  directory = "refactor"
  name = "Refactor"
  showcontent = true

[build-system]
requires = ["pdm-pep517>=0.3.0"]
build-backend = "pdm.pep517.api"

[tool.isort]
profile = "black"
atomic = true
skip_glob = ["*/setup.py", "pdm/_vendor/*"]
filter_files = true
known_first_party = ["pdm"]
known_third_party = [
    "appdirs",
    "atoml",
    "click",
    "cfonts",
    "distlib",
    "halo",
    "packaging",
    "pip_shims",
    "pytest",
    "pythonfinder"
]

[tool.vendoring]
destination = "pdm/_vendor/"
requirements = "pdm/_vendor/vendors.txt"
namespace = "pdm._vendor"

protected-files = ["__init__.py", "README.md", "vendors.txt"]
patches-dir = "tasks/patches"

[tool.vendoring.transformations]
substitute = [
  {match = 'import halo\.', replace = 'import pdm._vendor.halo.'}
]
drop = [
    "bin/",
    "*.so",
    "typing.*",
    "*/tests/"
]

[tool.vendoring.typing-stubs]
halo = []
log_symbols = []
spinners = []
termcolor = []
colorama = []

[tool.vendoring.license.directories]

[tool.vendoring.license.fallback-urls]

[tool.pytest.ini_options]
filterwarnings = [
  "ignore::DeprecationWarning"
]
markers = [
    "pypi: Tests that connect to the real PyPI",
    "integration: Run with all Python versions"
]
addopts = "-ra"
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pdm/invalid-version/errors.txt0000664000175000017500000000004115140154751032441 0ustar  carstencarsten`project.version` must be string
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pdm/redefining-as-dynamic/0000775000175000017500000000000015140154751031415 5ustar  carstencarsten././@LongLink0000644000000000000000000000014700000000000011605 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pdm/redefining-as-dynamic/pyproject.tomlabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pdm/redefining-as-dynamic/pyproject.to0000664000175000017500000001032515140154751034001 0ustar  carstencarsten[project]
# PEP 621 project metadata
# See https://peps.python.org/pep-0621/
authors = [
    {name = "frostming", email = "mianghong@gmail.com"},
]
dynamic = ["version", "classifiers"]
# version = {use_scm = true}  ->  invalid, must be string
requires-python = ">=3.7"
license = {text = "MIT"}
dependencies = [
    "appdirs",
    "atoml>=1.0.3",
    "click>=7",
    "importlib-metadata; python_version < \"3.8\"",
    "installer~=0.3.0",
    "packaging",
    "pdm-pep517>=0.8.3,<0.9",
    "pep517>=0.11.0",
    "pip>=20.1",
    "python-dotenv~=0.15",
    "pythonfinder",
    "resolvelib>=0.7.0,<0.8.0",
    "shellingham<2.0.0,>=1.3.2",
    "tomli>=1.1.0,<2.0.0",
    "typing-extensions; python_version < \"3.8\"",
    "wheel<1.0.0,>=0.36.2",
]
name = "pdm"
description = "Python Development Master"
readme = "README.md"
keywords = ["packaging", "dependency", "workflow"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Topic :: Software Development :: Build Tools",
]

[project.urls]
homepage = "https://pdm.fming.dev"
Repository = "https://github.com/pdm-project/pdm"
Documentation = "https://pdm.fming.dev"

[project.optional-dependencies]

[project.scripts]
pdm = "pdm.core:main"

[tool.pdm]
includes = ["pdm"]
source-includes = ["tests"]
# editables backend doesn't work well with namespace packages
editable-backend = "path"

[tool.pdm.scripts]
release = "python tasks/release.py"
test = "pytest tests/"
doc = {shell = "cd docs && mkdocs serve", help = "Start the dev server for doc preview"}
lint = "pre-commit run --all-files"
complete = {call = "tasks.complete:main"}

[tool.pdm.dev-dependencies]
test = [
    "pytest",
    "pytest-cov",
    "pytest-mock",
    "pytest-xdist<2.0.0,>=1.31.0"
]
doc = [
    "mkdocs<2.0.0,>=1.1",
    "mkdocs-material<7.0.0,>=6.2.4",
    "markdown-include<1.0.0,>=0.5.1"
]
workflow = [
    "parver<1.0.0,>=0.3.1",
    "towncrier<20.0.0,>=19.2.0",
    "vendoring; python_version ~= \"3.8\"",
    "mypy~=0.812",
    "pycomplete~=0.3"
]

[tool.black]
line-length = 88
exclude = '''
/(
    \.eggs
  | \.git
  | \.hg
  | \.mypy_cache
  | \.tox
  | \.venv
  | _build
  | buck-out
  | build
  | dist
  | pdm/_vendor
  | tests/fixtures
)/
'''

[tool.towncrier]
package = "pdm"
filename = "CHANGELOG.md"
issue_format = "[#{issue}](https://github.com/pdm-project/pdm/issues/{issue})"
directory = "news/"
title_format = "Release v{version} ({project_date})"
template = "news/towncrier_template.md"
underlines = "-~^"

  [[tool.towncrier.type]]
  directory = "feature"
  name = "Features & Improvements"
  showcontent = true

  [[tool.towncrier.type]]
  directory = "bugfix"
  name = "Bug Fixes"
  showcontent = true

  [[tool.towncrier.type]]
  directory = "doc"
  name = "Improved Documentation"
  showcontent = true

  [[tool.towncrier.type]]
  directory = "dep"
  name = "Dependencies"
  showcontent = true

  [[tool.towncrier.type]]
  directory = "removal"
  name = "Removals and Deprecations"
  showcontent = true

  [[tool.towncrier.type]]
  directory = "misc"
  name = "Miscellany"
  showcontent = true

  [[tool.towncrier.type]]
  directory = "refactor"
  name = "Refactor"
  showcontent = true

[build-system]
requires = ["pdm-pep517>=0.3.0"]
build-backend = "pdm.pep517.api"

[tool.isort]
profile = "black"
atomic = true
skip_glob = ["*/setup.py", "pdm/_vendor/*"]
filter_files = true
known_first_party = ["pdm"]
known_third_party = [
    "appdirs",
    "atoml",
    "click",
    "cfonts",
    "distlib",
    "halo",
    "packaging",
    "pip_shims",
    "pytest",
    "pythonfinder"
]

[tool.vendoring]
destination = "pdm/_vendor/"
requirements = "pdm/_vendor/vendors.txt"
namespace = "pdm._vendor"

protected-files = ["__init__.py", "README.md", "vendors.txt"]
patches-dir = "tasks/patches"

[tool.vendoring.transformations]
substitute = [
  {match = 'import halo\.', replace = 'import pdm._vendor.halo.'}
]
drop = [
    "bin/",
    "*.so",
    "typing.*",
    "*/tests/"
]

[tool.vendoring.typing-stubs]
halo = []
log_symbols = []
spinners = []
termcolor = []
colorama = []

[tool.vendoring.license.directories]

[tool.vendoring.license.fallback-urls]

[tool.pytest.ini_options]
filterwarnings = [
  "ignore::DeprecationWarning"
]
markers = [
    "pypi: Tests that connect to the real PyPI",
    "integration: Run with all Python versions"
]
addopts = "-ra"
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pdm/redefining-as-dynamic/errors.txt0000664000175000017500000000015215140154751033470 0ustar  carstencarstenYou cannot provide a value for `project.classifiers` and list it under `project.dynamic` at the same time
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/poetry/0000775000175000017500000000000015140154751026022 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/poetry/test_config.json0000664000175000017500000000013615140154751031221 0ustar  carstencarsten{
    "tools": {
        "poetry": "https://json.schemastore.org/partial-poetry.json"
    }
}
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/poetry/poetry-bad-multiline.errors.txt0000664000175000017500000000006515140154751034145 0ustar  carstencarsten`tool.poetry.description` must match pattern ^[^
]*$
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/poetry/poetry-bad-multiline.toml0000664000175000017500000000021015140154751032756 0ustar  carstencarsten[tool.poetry]
name = "bad-multiline"
version = "1.2.3"
description = "Some multi-\nline string"
authors = ["Poetry "]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/cibuildwheel/0000775000175000017500000000000015140154751027140 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/cibuildwheel/test_config.json0000664000175000017500000000015215140154751032335 0ustar  carstencarsten{
    "tools": {
        "cibuildwheel": "https://json.schemastore.org/partial-cibuildwheel.json"
    }
}
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/cibuildwheel/overrides-noselect.toml0000664000175000017500000000015615140154751033653 0ustar  carstencarsten[tool.cibuildwheel]
build = "*"

[[tool.cibuildwheel.overrides]]
test-command = "pytest"
test-extras = "test"
././@LongLink0000644000000000000000000000015100000000000011600 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/cibuildwheel/overrides-noselect.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/cibuildwheel/overrides-noselect.errors0000664000175000017500000000010415140154751034205 0ustar  carstencarsten`tool.cibuildwheel.overrides[0]` must contain ['select'] properties
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/cibuildwheel/overrides-noaction.toml0000664000175000017500000000012415140154751033644 0ustar  carstencarsten[tool.cibuildwheel]
build = "*"

[[tool.cibuildwheel.overrides]]
select = "cp312-*"
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/cibuildwheel/unknown-option.toml0000664000175000017500000000005715140154751033044 0ustar  carstencarsten[tool.cibuildwheel]
no-a-read-option = "error"
././@LongLink0000644000000000000000000000015100000000000011600 Lustar  rootrootabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/cibuildwheel/overrides-noaction.errors.txtabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/cibuildwheel/overrides-noaction.errors0000664000175000017500000000010415140154751034203 0ustar  carstencarsten`tool.cibuildwheel.overrides[0]` must contain at least 2 properties
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/cibuildwheel/unknown-option.errors.txt0000664000175000017500000000010515140154751034215 0ustar  carstencarsten`tool.cibuildwheel` must not contain {'no-a-read-option'} properties
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep735/0000775000175000017500000000000015140154751025523 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep735/invalid-key.toml0000664000175000017500000000006515140154751030635 0ustar  carstencarsten[dependency-groups]
mydep = ["one", {other = "two"}]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep735/not-pep508.errors.txt0000664000175000017500000000012615140154751031415 0ustar  carstencarsten`dependency-groups.test[0]` must be valid exactly by one definition (0 matches found)
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep735/invalid-group.toml0000664000175000017500000000004415140154751031176 0ustar  carstencarsten[dependency-groups]
"a b" = ["one"]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep735/invalid-key.errors.txt0000664000175000017500000000010515140154751032007 0ustar  carstencarsten`dependency-groups.mydep[1]` must be valid exactly by one definition
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep735/invalid-group.errors.txt0000664000175000017500000000006715140154751032362 0ustar  carstencarstendependency-groups` must not contain {'a b'} properties
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/pep735/not-pep508.toml0000664000175000017500000000004115140154751030232 0ustar  carstencarsten[dependency-groups]
test = [" "]
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/localtool/0000775000175000017500000000000015140154751026470 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/localtool/test_config.json0000664000175000017500000000027715140154751031675 0ustar  carstencarsten{
    "tools": {
        "localtool": "tests/examples/localtool/localtool.schema.json",
        "nestedtool": "tests/examples/localtool/nestedtool.schema.json#/properties/nestedtool"
    }
}
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/localtool/fail1.errors.txt0000664000175000017500000000004515140154751031537 0ustar  carstencarsten`tool.localtool.one` must be integer
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/localtool/fail1.toml0000664000175000017500000000003515140154751030357 0ustar  carstencarsten[tool.localtool]
one = "one"
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/localtool/fail2.errors.txt0000664000175000017500000000004615140154751031541 0ustar  carstencarsten`tool.nestedtool.two` must be integer
abravalheri-validate-pyproject-4b2e70d/tests/invalid-examples/localtool/fail2.toml0000664000175000017500000000003615140154751030361 0ustar  carstencarsten[tool.nestedtool]
two = "two"
abravalheri-validate-pyproject-4b2e70d/tests/test_api.py0000664000175000017500000001233315140154751023422 0ustar  carstencarstenfrom collections.abc import Mapping
from functools import partial, wraps

import fastjsonschema as FJS
import pytest

from validate_pyproject import _tomllib as tomllib
from validate_pyproject import api, errors, plugins, types

PYPA_SPECS = "https://packaging.python.org/en/latest/specifications"


def test_load():
    spec = api.load("pyproject_toml")
    assert isinstance(spec, Mapping)

    assert spec["$id"] == f"{PYPA_SPECS}/declaring-build-dependencies/"

    spec = api.load("project_metadata")
    assert spec["$id"] == f"{PYPA_SPECS}/pyproject-toml/"


def test_load_plugin():
    spec = api.load_builtin_plugin("distutils")
    assert spec["$id"].startswith("https://setuptools.pypa.io")
    assert "deprecated/distutils" in spec["$id"]

    spec = api.load_builtin_plugin("setuptools")
    assert spec["$id"].startswith("https://setuptools.pypa.io")
    assert "pyproject" in spec["$id"]


class TestRegistry:
    def test_with_plugins(self):
        plg = plugins.list_from_entry_points()
        registry = api.SchemaRegistry(plg)
        main_schema = registry[registry.main]
        project = main_schema["properties"]["project"]
        assert project["$ref"] == f"{PYPA_SPECS}/pyproject-toml/"
        tool = main_schema["properties"]["tool"]
        assert "setuptools" in tool["properties"]
        assert "$ref" in tool["properties"]["setuptools"]

    def fake_plugin(self, name, schema_version=7, end="#"):
        schema = {
            "$id": f"https://example.com/{name}.schema.json",
            "$schema": f"http://json-schema.org/draft-{schema_version:02d}/schema{end}",
            "type": "object",
        }
        return types.Schema(schema)

    @pytest.mark.parametrize("end", ["", "#"], ids=["no#", "with#"])
    def test_schema_ending(self, end):
        fn = wraps(self.fake_plugin)(partial(self.fake_plugin, end=end))
        plg = plugins.PluginWrapper("plugin", fn)
        registry = api.SchemaRegistry([plg])
        main_schema = registry[registry.main]
        assert main_schema["$schema"] == "http://json-schema.org/draft-07/schema#"

    def test_incompatible_versions(self):
        fn = wraps(self.fake_plugin)(partial(self.fake_plugin, schema_version=8))
        plg = plugins.PluginWrapper("plugin", fn)
        with pytest.raises(errors.InvalidSchemaVersion):
            api.SchemaRegistry([plg])

    def test_duplicated_id_different_tools(self):
        schema = self.fake_plugin("plg")
        fn = wraps(self.fake_plugin)(lambda _: schema)  # Same ID
        plg = [plugins.PluginWrapper(f"plg{i}", fn) for i in range(2)]
        with pytest.raises(errors.SchemaWithDuplicatedId):
            api.SchemaRegistry(plg)

    def test_allow_overwrite_same_tool(self):
        plg = [plugins.PluginWrapper("plg", self.fake_plugin) for _ in range(2)]
        registry = api.SchemaRegistry(plg)
        sid = self.fake_plugin("plg")["$id"]
        assert sid in registry

    def test_missing_id(self):
        def _fake_plugin(name):
            plg = dict(self.fake_plugin(name))
            del plg["$id"]
            return types.Schema(plg)

        plg = plugins.PluginWrapper("plugin", _fake_plugin)
        with pytest.raises(errors.SchemaMissingId):
            api.SchemaRegistry([plg])


class TestValidator:
    example_toml = """\
    [project]
    name = "myproj"
    version = "0"

    [tool.setuptools]
    zip-safe = false
    packages = {find = {}}
    """

    @property
    def valid_example(self):
        return tomllib.loads(self.example_toml)

    @property
    def invalid_example(self):
        example = self.valid_example
        example["tool"]["setuptools"]["zip-safe"] = {"hello": "world"}
        return example

    def test_valid(self):
        validator = api.Validator()
        assert validator(self.valid_example) is not None

    def test_invalid(self):
        validator = api.Validator()
        with pytest.raises(FJS.JsonSchemaValueException):
            validator(self.invalid_example)

    # ---

    def plugin(self, tool):
        plg = plugins.list_from_entry_points(filtering=lambda e: e.name == tool)
        return plg[0]

    TOOLS = ("distutils", "setuptools")

    @pytest.mark.parametrize(("tool", "other_tool"), zip(TOOLS, reversed(TOOLS)))
    def test_plugin_not_enabled(self, tool, other_tool):
        plg = self.plugin(tool)
        validator = api.Validator([plg])
        registry = validator.registry
        main_schema = registry[registry.main]
        assert tool in main_schema["properties"]["tool"]["properties"]
        assert other_tool not in main_schema["properties"]["tool"]["properties"]
        tool_properties = main_schema["properties"]["tool"]["properties"]
        assert tool_properties[tool]["$ref"] == plg.schema["$id"]

    def test_invalid_but_plugin_not_enabled(self):
        # When the plugin is not enabled, the validator should ignore the tool
        validator = api.Validator([self.plugin("distutils")])
        try:
            assert validator(self.invalid_example) is not None
        except Exception:
            registry = validator.registry
            main_schema = registry[registry.main]
            assert "setuptools" not in main_schema["properties"]["tool"]["properties"]
            import json

            assert "setuptools" not in json.dumps(main_schema)
            raise
abravalheri-validate-pyproject-4b2e70d/tests/test_examples.py0000664000175000017500000000342115140154751024465 0ustar  carstencarstenimport copy
import logging
from pathlib import Path

import pytest

from validate_pyproject import _tomllib as tomllib
from validate_pyproject import api, cli
from validate_pyproject.error_reporting import ValidationError

from .helpers import error_file, get_tools, get_tools_as_args


def test_examples_api(example: Path) -> None:
    load_tools = get_tools(example)

    toml_equivalent = tomllib.loads(example.read_text())
    copy_toml = copy.deepcopy(toml_equivalent)
    validator = api.Validator(extra_plugins=load_tools)
    assert validator(toml_equivalent) is not None
    assert toml_equivalent == copy_toml


def test_examples_cli(example: Path) -> None:
    args = get_tools_as_args(example)

    assert cli.run(["--dump-json", str(example), *args]) == 0  # no errors


def test_invalid_examples_api(invalid_example: Path) -> None:
    load_tools = get_tools(invalid_example)

    expected_error = error_file(invalid_example).read_text("utf-8")
    toml_equivalent = tomllib.loads(invalid_example.read_text())
    validator = api.Validator(extra_plugins=load_tools)
    with pytest.raises(ValidationError) as exc_info:
        validator(toml_equivalent)
    exception_message = str(exc_info.value)
    summary = exc_info.value.summary
    for error in expected_error.splitlines():
        assert error in exception_message
        assert error in summary


def test_invalid_examples_cli(invalid_example: Path, caplog) -> None:
    args = get_tools_as_args(invalid_example)

    caplog.set_level(logging.DEBUG)
    expected_error = error_file(invalid_example).read_text("utf-8")
    with pytest.raises(SystemExit) as exc_info:
        cli.main([str(invalid_example), *args])
    assert exc_info.value.args == (1,)
    for error in expected_error.splitlines():
        assert error in caplog.text
abravalheri-validate-pyproject-4b2e70d/CONTRIBUTING.rst0000664000175000017500000002666615140154751022555 0ustar  carstencarsten============
Contributing
============

Welcome to ``validate-pyproject`` contributor's guide.

This document focuses on getting any potential contributor familiarized
with the development processes, but `other kinds of contributions`_ are also
appreciated.

If you are new to using git_ or have never collaborated in a project previously,
please have a look at `contribution-guide.org`_. Other resources are also
listed in the excellent `guide created by FreeCodeCamp`_.

Please notice, all users and contributors are expected to be **open,
considerate, reasonable, and respectful**. When in doubt, `Python Software
Foundation's Code of Conduct`_ is a good reference in terms of behavior
guidelines.


Issue Reports
=============

If you experience bugs or general issues with ``validate-pyproject``, please have a look
on the `issue tracker`_. If you don't see anything useful there, please feel
free to fire an issue report.

.. tip::
   Please don't forget to include the closed issues in your search.
   Sometimes a solution was already reported, and the problem is considered
   **solved**.

New issue reports should include information about your programming environment
(e.g., operating system, Python version) and steps to reproduce the problem.
Please try also to simplify the reproduction steps to a very minimal example
that still illustrates the problem you are facing. By removing other factors,
you help us to identify the root cause of the issue.


Documentation Improvements
==========================

You can help improve ``validate-pyproject`` docs by making them more readable and coherent, or
by adding missing information and correcting mistakes.

``validate-pyproject`` documentation uses Sphinx_ as its main documentation
compiler. This means that the docs are kept in the same repository as the
project code, in the form of reStructuredText_ files, and that any
documentation update is done in the same way was a code contribution.

.. tip::
  Please notice that the `GitHub web interface`_ provides a quick way of
  propose changes in ``validate-pyproject``'s files. While this mechanism can
  be tricky for normal code contributions, it works perfectly fine for
  contributing to the docs, and can be quite handy.

  If you are interested in trying this method out, please navigate to
  the ``docs`` folder in the source repository_, find which file you
  would like to propose changes and click in the little pencil icon at the
  top, to open `GitHub's code editor`_. Once you finish editing the file,
  please write a message in the form at the bottom of the page describing
  which changes have you made and what are the motivations behind them and
  submit your proposal.

When working on documentation changes in your local machine, you can
compile them using |tox|_::

    tox -e docs

and use Python's built-in web server for a preview in your web browser
(``http://localhost:8000``)::

    python3 -m http.server --directory 'docs/_build/html'


Code Contributions
==================

Understanding how the project works
-----------------------------------

If you have a change in mind, please have a look in our :doc:`dev-guide`.
It explains the main aspects of the project and provide a brief overview on how
it is organised and how to implement :ref:`plugins`.

Submit an issue
---------------

Before you work on any non-trivial code contribution it's best to first create
a report in the `issue tracker`_ to start a discussion on the subject.
This often provides additional considerations and avoids unnecessary work.

Create an environment
---------------------

Before you start coding, we recommend creating an isolated `virtual
environment`_ to avoid any problems with your installed Python packages.
This can easily be done via either |virtualenv|_::

    virtualenv 
    source /bin/activate

or Miniconda_::

    conda create -n validate-pyproject python=3 six virtualenv pytest pytest-cov
    conda activate validate-pyproject

Clone the repository
--------------------

#. Create an user account on |the repository service| if you do not already have one.
#. Fork the project repository_: click on the *Fork* button near the top of the
   page. This creates a copy of the code under your account on |the repository service|.
#. Clone this copy to your local disk::

    git clone git@github.com:YourLogin/validate-pyproject.git
    cd validate-pyproject

#. You should run::

    pip install -U pip setuptools -e .

   to be able to import the package under development in the Python REPL.

#. Install |pre-commit|_::

    pip install pre-commit
    pre-commit install

   ``validate-pyproject`` comes with a lot of hooks configured to automatically help the
   developer to check the code being written.

Implement your changes
----------------------

#. Create a branch to hold your changes::

    git checkout -b my-feature

   and start making changes. Never work on the main branch!

#. Start your work on this branch. Don't forget to add docstrings_ to new
   functions, modules and classes, especially if they are part of public APIs.

#. Add yourself to the list of contributors in ``AUTHORS.rst``.

#. When you’re done editing, do::

    git add 
    git commit

   to record your changes in git_.

   Please make sure to see the validation messages from |pre-commit|_ and fix
   any eventual issues.
   This should automatically use ruff_ to check/fix the code style in a way
   that is compatible with the project.

   .. important:: Don't forget to add unit tests and documentation in case your
      contribution adds an additional feature and is not just a bugfix.

      Moreover, writing a `descriptive commit message`_ is highly recommended.
      In case of doubt, you can check the commit history with::

         git log --graph --decorate --pretty=oneline --abbrev-commit --all

      to look for recurring communication patterns.

#. Please check that your changes don't break any unit tests with::

    tox

   (after having installed |tox|_ with ``pip install tox`` or ``pipx``).

   You can also use |tox|_ to run several other pre-configured tasks in the
   repository. Try ``tox -av`` to see a list of the available checks.

Submit your contribution
------------------------

#. If everything works fine, push your local branch to |the repository service| with::

    git push -u origin my-feature

#. Go to the web page of your fork and click |contribute button|
   to send your changes for review.

  Find more detailed information in `creating a PR`_. You might also want to open
  the PR as a draft first and mark it as ready for review after the feedbacks
  from the continuous integration (CI) system or any required fixes.


Troubleshooting
---------------

The following tips can be used when facing problems to build or test the
package:

#. Make sure to fetch all the tags from the upstream repository_.
   The command ``git describe --abbrev=0 --tags`` should return the version you
   are expecting. If you are trying to run CI scripts in a fork repository,
   make sure to push all the tags.
   You can also try to remove all the egg files or the complete egg folder, i.e.,
   ``.eggs``, as well as the ``*.egg-info`` folders in the ``src`` folder or
   potentially in the root of your project.

#. Sometimes |tox|_ misses out when new dependencies are added, especially to
   ``setup.cfg`` and ``docs/requirements.txt``. If you find any problems with
   missing dependencies when running a command with |tox|_, try to recreate the
   ``tox`` environment using the ``-r`` flag. For example, instead of::

    tox -e docs

   Try running::

    tox -r -e docs

#. Make sure to have a reliable |tox|_ installation that uses the correct
   Python version (e.g., 3.7+). When in doubt you can run::

    tox --version
    # OR
    which tox

   If you have trouble and are seeing weird errors upon running |tox|_, you can
   also try to create a dedicated `virtual environment`_ with a |tox|_ binary
   freshly installed. For example::

    virtualenv .venv
    source .venv/bin/activate
    .venv/bin/pip install tox
    .venv/bin/tox -e all

#. `Pytest can drop you`_ in an interactive session in the case an error occurs.
   In order to do that you need to pass a ``--pdb`` option (for example by
   running ``tox -- -k  --pdb``).
   You can also setup breakpoints manually instead of using the ``--pdb`` option.


Maintainer tasks
================


Releases
--------

If you are part of the group of maintainers and have correct user permissions
on PyPI_, the following steps can be used to release a new version for
``validate-pyproject``:

#. Make sure all unit tests are successful.
#. Tag the current commit on the main branch with a release tag, e.g., ``v1.2.3``.
#. Push the new tag to the upstream repository_, e.g., ``git push upstream v1.2.3``
#. Clean up the ``dist`` and ``build`` folders with ``tox -e clean``
   (or ``rm -rf dist build``)
   to avoid confusion with old builds and Sphinx docs.
#. Run ``tox -e build`` and check that the files in ``dist`` have
   the correct version (no ``.dirty`` or git_ hash) according to the git_ tag.
   Also check the sizes of the distributions, if they are too big (e.g., >
   500KB), unwanted clutter may have been accidentally included.
#. Run ``tox -e publish -- --repository pypi`` and check that everything was
   uploaded to PyPI_ correctly.


.. <-- start -->
.. |the repository service| replace:: GitHub
.. |contribute button| replace:: "Create pull request"

.. _repository: https://github.com/abravalheri/validate-pyproject
.. _issue tracker: https://github.com/abravalheri/validate-pyproject/issues
.. <-- end -->


.. |virtualenv| replace:: ``virtualenv``
.. |pre-commit| replace:: ``pre-commit``
.. |tox| replace:: ``tox``


.. _contribution-guide.org: https://www.contribution-guide.org/
.. _creating a PR: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request
.. _descriptive commit message: https://chris.beams.io/posts/git-commit
.. _docstrings: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html
.. _first-contributions tutorial: https://github.com/firstcontributions/first-contributions
.. _git: https://git-scm.com
.. _GitHub's fork and pull request workflow: https://guides.github.com/activities/forking/
.. _guide created by FreeCodeCamp: https://github.com/FreeCodeCamp/how-to-contribute-to-open-source
.. _Miniconda: https://docs.conda.io/en/latest/miniconda.html
.. _MyST: https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html
.. _other kinds of contributions: https://opensource.guide/how-to-contribute
.. _pre-commit: https://pre-commit.com/
.. _PyPI: https://pypi.org/
.. _PyScaffold's contributor's guide: https://pyscaffold.org/en/stable/contributing.html
.. _Pytest can drop you: https://docs.pytest.org/en/stable/how-to/failures.html#using-python-library-pdb-with-pytest
.. _Python Software Foundation's Code of Conduct: https://www.python.org/psf/conduct/
.. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/
.. _ruff: https://beta.ruff.rs/docs/
.. _Sphinx: https://www.sphinx-doc.org/en/master/
.. _tox: https://tox.wiki/en/stable/
.. _virtual environment: https://realpython.com/python-virtual-environments-a-primer/
.. _virtualenv: https://virtualenv.pypa.io/en/stable/

.. _GitHub web interface: https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files
.. _GitHub's code editor: https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files
abravalheri-validate-pyproject-4b2e70d/LICENSE.txt0000664000175000017500000003706215140154751021727 0ustar  carstencarstenMozilla Public License, version 2.0

1. Definitions

1.1. "Contributor"

     means each individual or legal entity that creates, contributes to the
     creation of, or owns Covered Software.

1.2. "Contributor Version"

     means the combination of the Contributions of others (if any) used by a
     Contributor and that particular Contributor's Contribution.

1.3. "Contribution"

     means Covered Software of a particular Contributor.

1.4. "Covered Software"

     means Source Code Form to which the initial Contributor has attached the
     notice in Exhibit A, the Executable Form of such Source Code Form, and
     Modifications of such Source Code Form, in each case including portions
     thereof.

1.5. "Incompatible With Secondary Licenses"
     means

     a. that the initial Contributor has attached the notice described in
        Exhibit B to the Covered Software; or

     b. that the Covered Software was made available under the terms of
        version 1.1 or earlier of the License, but not also under the terms of
        a Secondary License.

1.6. "Executable Form"

     means any form of the work other than Source Code Form.

1.7. "Larger Work"

     means a work that combines Covered Software with other material, in a
     separate file or files, that is not Covered Software.

1.8. "License"

     means this document.

1.9. "Licensable"

     means having the right to grant, to the maximum extent possible, whether
     at the time of the initial grant or subsequently, any and all of the
     rights conveyed by this License.

1.10. "Modifications"

     means any of the following:

     a. any file in Source Code Form that results from an addition to,
        deletion from, or modification of the contents of Covered Software; or

     b. any new file in Source Code Form that contains any Covered Software.

1.11. "Patent Claims" of a Contributor

      means any patent claim(s), including without limitation, method,
      process, and apparatus claims, in any patent Licensable by such
      Contributor that would be infringed, but for the grant of the License,
      by the making, using, selling, offering for sale, having made, import,
      or transfer of either its Contributions or its Contributor Version.

1.12. "Secondary License"

      means either the GNU General Public License, Version 2.0, the GNU Lesser
      General Public License, Version 2.1, the GNU Affero General Public
      License, Version 3.0, or any later versions of those licenses.

1.13. "Source Code Form"

      means the form of the work preferred for making modifications.

1.14. "You" (or "Your")

      means an individual or a legal entity exercising rights under this
      License. For legal entities, "You" includes any entity that controls, is
      controlled by, or is under common control with You. For purposes of this
      definition, "control" means (a) the power, direct or indirect, to cause
      the direction or management of such entity, whether by contract or
      otherwise, or (b) ownership of more than fifty percent (50%) of the
      outstanding shares or beneficial ownership of such entity.


2. License Grants and Conditions

2.1. Grants

     Each Contributor hereby grants You a world-wide, royalty-free,
     non-exclusive license:

     a. under intellectual property rights (other than patent or trademark)
        Licensable by such Contributor to use, reproduce, make available,
        modify, display, perform, distribute, and otherwise exploit its
        Contributions, either on an unmodified basis, with Modifications, or
        as part of a Larger Work; and

     b. under Patent Claims of such Contributor to make, use, sell, offer for
        sale, have made, import, and otherwise transfer either its
        Contributions or its Contributor Version.

2.2. Effective Date

     The licenses granted in Section 2.1 with respect to any Contribution
     become effective for each Contribution on the date the Contributor first
     distributes such Contribution.

2.3. Limitations on Grant Scope

     The licenses granted in this Section 2 are the only rights granted under
     this License. No additional rights or licenses will be implied from the
     distribution or licensing of Covered Software under this License.
     Notwithstanding Section 2.1(b) above, no patent license is granted by a
     Contributor:

     a. for any code that a Contributor has removed from Covered Software; or

     b. for infringements caused by: (i) Your and any other third party's
        modifications of Covered Software, or (ii) the combination of its
        Contributions with other software (except as part of its Contributor
        Version); or

     c. under Patent Claims infringed by Covered Software in the absence of
        its Contributions.

     This License does not grant any rights in the trademarks, service marks,
     or logos of any Contributor (except as may be necessary to comply with
     the notice requirements in Section 3.4).

2.4. Subsequent Licenses

     No Contributor makes additional grants as a result of Your choice to
     distribute the Covered Software under a subsequent version of this
     License (see Section 10.2) or under the terms of a Secondary License (if
     permitted under the terms of Section 3.3).

2.5. Representation

     Each Contributor represents that the Contributor believes its
     Contributions are its original creation(s) or it has sufficient rights to
     grant the rights to its Contributions conveyed by this License.

2.6. Fair Use

     This License is not intended to limit any rights You have under
     applicable copyright doctrines of fair use, fair dealing, or other
     equivalents.

2.7. Conditions

     Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
     Section 2.1.


3. Responsibilities

3.1. Distribution of Source Form

     All distribution of Covered Software in Source Code Form, including any
     Modifications that You create or to which You contribute, must be under
     the terms of this License. You must inform recipients that the Source
     Code Form of the Covered Software is governed by the terms of this
     License, and how they can obtain a copy of this License. You may not
     attempt to alter or restrict the recipients' rights in the Source Code
     Form.

3.2. Distribution of Executable Form

     If You distribute Covered Software in Executable Form then:

     a. such Covered Software must also be made available in Source Code Form,
        as described in Section 3.1, and You must inform recipients of the
        Executable Form how they can obtain a copy of such Source Code Form by
        reasonable means in a timely manner, at a charge no more than the cost
        of distribution to the recipient; and

     b. You may distribute such Executable Form under the terms of this
        License, or sublicense it under different terms, provided that the
        license for the Executable Form does not attempt to limit or alter the
        recipients' rights in the Source Code Form under this License.

3.3. Distribution of a Larger Work

     You may create and distribute a Larger Work under terms of Your choice,
     provided that You also comply with the requirements of this License for
     the Covered Software. If the Larger Work is a combination of Covered
     Software with a work governed by one or more Secondary Licenses, and the
     Covered Software is not Incompatible With Secondary Licenses, this
     License permits You to additionally distribute such Covered Software
     under the terms of such Secondary License(s), so that the recipient of
     the Larger Work may, at their option, further distribute the Covered
     Software under the terms of either this License or such Secondary
     License(s).

3.4. Notices

     You may not remove or alter the substance of any license notices
     (including copyright notices, patent notices, disclaimers of warranty, or
     limitations of liability) contained within the Source Code Form of the
     Covered Software, except that You may alter any license notices to the
     extent required to remedy known factual inaccuracies.

3.5. Application of Additional Terms

     You may choose to offer, and to charge a fee for, warranty, support,
     indemnity or liability obligations to one or more recipients of Covered
     Software. However, You may do so only on Your own behalf, and not on
     behalf of any Contributor. You must make it absolutely clear that any
     such warranty, support, indemnity, or liability obligation is offered by
     You alone, and You hereby agree to indemnify every Contributor for any
     liability incurred by such Contributor as a result of warranty, support,
     indemnity or liability terms You offer. You may include additional
     disclaimers of warranty and limitations of liability specific to any
     jurisdiction.

4. Inability to Comply Due to Statute or Regulation

   If it is impossible for You to comply with any of the terms of this License
   with respect to some or all of the Covered Software due to statute,
   judicial order, or regulation then You must: (a) comply with the terms of
   this License to the maximum extent possible; and (b) describe the
   limitations and the code they affect. Such description must be placed in a
   text file included with all distributions of the Covered Software under
   this License. Except to the extent prohibited by statute or regulation,
   such description must be sufficiently detailed for a recipient of ordinary
   skill to be able to understand it.

5. Termination

5.1. The rights granted under this License will terminate automatically if You
     fail to comply with any of its terms. However, if You become compliant,
     then the rights granted under this License from a particular Contributor
     are reinstated (a) provisionally, unless and until such Contributor
     explicitly and finally terminates Your grants, and (b) on an ongoing
     basis, if such Contributor fails to notify You of the non-compliance by
     some reasonable means prior to 60 days after You have come back into
     compliance. Moreover, Your grants from a particular Contributor are
     reinstated on an ongoing basis if such Contributor notifies You of the
     non-compliance by some reasonable means, this is the first time You have
     received notice of non-compliance with this License from such
     Contributor, and You become compliant prior to 30 days after Your receipt
     of the notice.

5.2. If You initiate litigation against any entity by asserting a patent
     infringement claim (excluding declaratory judgment actions,
     counter-claims, and cross-claims) alleging that a Contributor Version
     directly or indirectly infringes any patent, then the rights granted to
     You by any and all Contributors for the Covered Software under Section
     2.1 of this License shall terminate.

5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
     license agreements (excluding distributors and resellers) which have been
     validly granted by You or Your distributors under this License prior to
     termination shall survive termination.

6. Disclaimer of Warranty

   Covered Software is provided under this License on an "as is" basis,
   without warranty of any kind, either expressed, implied, or statutory,
   including, without limitation, warranties that the Covered Software is free
   of defects, merchantable, fit for a particular purpose or non-infringing.
   The entire risk as to the quality and performance of the Covered Software
   is with You. Should any Covered Software prove defective in any respect,
   You (not any Contributor) assume the cost of any necessary servicing,
   repair, or correction. This disclaimer of warranty constitutes an essential
   part of this License. No use of  any Covered Software is authorized under
   this License except under this disclaimer.

7. Limitation of Liability

   Under no circumstances and under no legal theory, whether tort (including
   negligence), contract, or otherwise, shall any Contributor, or anyone who
   distributes Covered Software as permitted above, be liable to You for any
   direct, indirect, special, incidental, or consequential damages of any
   character including, without limitation, damages for lost profits, loss of
   goodwill, work stoppage, computer failure or malfunction, or any and all
   other commercial damages or losses, even if such party shall have been
   informed of the possibility of such damages. This limitation of liability
   shall not apply to liability for death or personal injury resulting from
   such party's negligence to the extent applicable law prohibits such
   limitation. Some jurisdictions do not allow the exclusion or limitation of
   incidental or consequential damages, so this exclusion and limitation may
   not apply to You.

8. Litigation

   Any litigation relating to this License may be brought only in the courts
   of a jurisdiction where the defendant maintains its principal place of
   business and such litigation shall be governed by laws of that
   jurisdiction, without reference to its conflict-of-law provisions. Nothing
   in this Section shall prevent a party's ability to bring cross-claims or
   counter-claims.

9. Miscellaneous

   This License represents the complete agreement concerning the subject
   matter hereof. If any provision of this License is held to be
   unenforceable, such provision shall be reformed only to the extent
   necessary to make it enforceable. Any law or regulation which provides that
   the language of a contract shall be construed against the drafter shall not
   be used to construe this License against a Contributor.


10. Versions of the License

10.1. New Versions

      Mozilla Foundation is the license steward. Except as provided in Section
      10.3, no one other than the license steward has the right to modify or
      publish new versions of this License. Each version will be given a
      distinguishing version number.

10.2. Effect of New Versions

      You may distribute the Covered Software under the terms of the version
      of the License under which You originally received the Covered Software,
      or under the terms of any subsequent version published by the license
      steward.

10.3. Modified Versions

      If you create software not governed by this License, and you want to
      create a new license for such software, you may create and use a
      modified version of this License if you rename the license and remove
      any references to the name of the license steward (except to note that
      such modified license differs from this License).

10.4. Distributing Source Code Form that is Incompatible With Secondary
      Licenses If You choose to distribute Source Code Form that is
      Incompatible With Secondary Licenses under the terms of this version of
      the License, the notice described in Exhibit B of this License must be
      attached.

Exhibit A - Source Code Form License Notice

      This Source Code Form is subject to the
      terms of the Mozilla Public License, v.
      2.0. If a copy of the MPL was not
      distributed with this file, You can
      obtain one at
      https://mozilla.org/MPL/2.0/.

If it is not possible or desirable to put the notice in a particular file,
then You may include the notice in a location (such as a LICENSE file in a
relevant directory) where a recipient would be likely to look for such a
notice.

You may add additional accurate notices of copyright ownership.

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.
abravalheri-validate-pyproject-4b2e70d/AUTHORS.rst0000664000175000017500000000013515140154751021752 0ustar  carstencarsten============
Contributors
============

* Anderson Bravalheri 
abravalheri-validate-pyproject-4b2e70d/.readthedocs.yml0000664000175000017500000000152115140154751023161 0ustar  carstencarsten# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details

# Required
version: 2

build:
  os: ubuntu-lts-latest
  tools:
    python: latest
  jobs:
    pre_create_environment:
      - asdf plugin add uv
      - asdf install uv latest
      - asdf global uv latest
    create_environment:
      - uv venv $READTHEDOCS_VIRTUALENV_PATH
    install:
      # Use a cache dir in the same mount to halve the install time
      # pip and uv pip will gain support for groups soon
      - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH uv sync --active --cache-dir $READTHEDOCS_VIRTUALENV_PATH/../../uv_cache --group docs --extra all

# Build documentation in the docs/ directory with Sphinx
sphinx:
  configuration: docs/conf.py

# Optionally build your docs in additional formats such as PDF
formats:
  - pdf
abravalheri-validate-pyproject-4b2e70d/.git_archival.txt0000664000175000017500000000017215140154751023347 0ustar  carstencarstennode: 4b2e70d08cb2ccd26d1fba73588de41c7a5d50b7
node-date: 2026-02-02T17:07:53Z
describe-name: v0.25
ref-names: tag: v0.25
abravalheri-validate-pyproject-4b2e70d/NOTICE.txt0000664000175000017500000000460215140154751021620 0ustar  carstencarsten'validate-pyproject' is licensed under the MPL-2.0 license, with the following
copyright notice:

  Copyright (c) 2021, Anderson Bravalheri

see the LICENSE.txt file for details.

----------------------------------------------------------------------

A few extra files, derived from other opensource projects are collocated in
this code base ('tests/examples' and 'tests/invalid-examples') exclusively for
testing purposes during development:

- 'atoml/pyproject.toml' from https://github.com/forstming/atoml, licensed under MIT
- 'flit/pyproject.toml' from https://github.com/takluyver/flit, licensed under BSD-3-Clause
- 'pdm/pyproject.toml' from https://github.com/pdm-project/pdm, licensed under MIT
- 'trampolim/pyproject.toml' from https://github.com/FFY00/trampolim, licensed under MIT

These files are not part of the 'validate-pyproject' project and not meant
for distribution (as part of the 'validate-pyproject' software package).
The original licenses for each one of these files can be found inside the
respective directory under 'tests/examples'.

----------------------------------------------------------------------

'validate-project' also includes code based on/derived from the PyScaffold
project. PyScaffold is licensed under the MIT license; see below for details.

***

The MIT License (MIT)

Copyright (c) 2018-present, PyScaffold contributors
Copyright (c) 2014-2018 Blue Yonder GmbH

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.
abravalheri-validate-pyproject-4b2e70d/.github/0000775000175000017500000000000015140154751021434 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/.github/dependabot.yml0000664000175000017500000000110015140154751024254 0ustar  carstencarsten# Keep GitHub Actions up to date with GitHub's Dependabot...
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    groups:
      actions:
        patterns:
          - "*"  # Group all Actions updates into a single larger pull request
    schedule:
      interval: weekly
abravalheri-validate-pyproject-4b2e70d/.github/workflows/0000775000175000017500000000000015140154751023471 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/.github/workflows/ci.yml0000664000175000017500000001124415140154751024611 0ustar  carstencarstenname: tests

on:
  push:
    # Avoid using all the resources/limits available by checking only
    # relevant branches and tags. Other branches can be checked via PRs.
    # branches: [main]
    tags: ['v[0-9]*', '[0-9]+.[0-9]+*']  # Match tags that resemble a version
  pull_request:
    paths: ['.github/workflows/ci.yml']  # On PRs only when this file itself is changed
  workflow_dispatch:  # Allow manually triggering the workflow
  schedule:
    # Run roughly every 15 days at 00:00 UTC
    # (useful to check if updates on dependencies break the package)
    - cron: '0 0 1,16 * *'

concurrency:
  group: >-
    ${{ github.workflow }}-${{ github.ref_type }}-
    ${{ github.event.pull_request.number || github.sha }}
  cancel-in-progress: true

env:
  VALIDATE_PYPROJECT_CACHE_REMOTE: tests/.cache


jobs:
  prepare:
    runs-on: ubuntu-latest
    outputs:
      wheel-distribution: ${{ steps.wheel-distribution.outputs.path }}
    steps:
      - uses: actions/checkout@v6
        with: {fetch-depth: 0}  # deep clone for setuptools-scm
      - uses: actions/setup-python@v6
        with: {python-version: "3.x"}
      - uses: astral-sh/setup-uv@v7
      - name: Run static analysis and format checkers
        run: >-
          uvx --with tox-uv
          tox -e lint,typecheck
      - name: Build package distribution files
        run: >-
          uvx --with tox-uv
          tox -e clean,build
      - name: Record the path of wheel distribution
        id: wheel-distribution
        run: echo "path=$(ls dist/*.whl)" >> $GITHUB_OUTPUT
      - name: Store the distribution files for use in other stages
        # `tests` and `publish` will use the same pre-built distributions,
        # so we make sure to release the exact same package that was tested
        uses: actions/upload-artifact@v6
        with:
          name: python-distribution-files
          path: dist/
          retention-days: 1
      - name: Download files used for testing
        run: python tools/cache_urls_for_tests.py
      - name: Store downloaded files
        uses: actions/upload-artifact@v6
        with:
          name: test-download-files
          path: ${{ env.VALIDATE_PYPROJECT_CACHE_REMOTE }}
          include-hidden-files: true
          if-no-files-found: error
          retention-days: 1

  test:
    needs: prepare
    strategy:
      matrix:
        python:
        - "3.8"   # oldest Python supported by validate-pyproject
        - "3.x"   # newest Python that is stable
        - "3.14"
        platform:
        - ubuntu-latest
        - macos-15-intel
        - windows-latest
    runs-on: ${{ matrix.platform }}
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python }}
          allow-prereleases: true
      - uses: astral-sh/setup-uv@v7
      - name: Retrieve pre-built distribution files
        uses: actions/download-artifact@v7
        with: {name: python-distribution-files, path: dist/}
      - name: Retrieve test download files
        uses: actions/download-artifact@v7
        with:
          name: test-download-files
          path: ${{ env.VALIDATE_PYPROJECT_CACHE_REMOTE }}
      - name: Run tests
        run: >-
          uvx --with tox-uv
          tox
          --installpkg '${{ needs.prepare.outputs.wheel-distribution }}'
          -- -n 5 -rFEx --durations 10 --color yes
      - name: Generate coverage report
        run: pipx run coverage lcov -o coverage.lcov
      - name: Upload partial coverage report
        uses: coverallsapp/github-action@master
        with:
          path-to-lcov: coverage.lcov
          github-token: ${{ secrets.github_token }}
          flag-name: ${{ matrix.platform }} - py${{ matrix.python }}
          parallel: true

  finalize:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Finalize coverage report
        uses: coverallsapp/github-action@master
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          parallel-finished: true

  publish:
    needs: finalize
    if: ${{ github.event_name == 'push' && contains(github.ref, 'refs/tags/') }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with: {python-version: "3.x"}
      - uses: astral-sh/setup-uv@v7
      - name: Retrieve pre-built distribution files
        uses: actions/download-artifact@v7
        with: {name: python-distribution-files, path: dist/}
      - name: Publish Package
        env:
          # See: https://pypi.org/help/#apitoken
          TWINE_REPOSITORY: pypi
          TWINE_USERNAME: __token__
          TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
        run: >-
          uvx --with tox-uv
          tox -e publish
abravalheri-validate-pyproject-4b2e70d/.gitignore0000664000175000017500000000113115140154751022060 0ustar  carstencarsten# Temporary and binary files
*~
*.py[cod]
*.so
*.cfg
!.isort.cfg
!setup.cfg
*.orig
*.log
*.pot
__pycache__/*
.cache/*
.*.swp
*/.ipynb_checkpoints/*
.DS_Store

# Project files
.ropeproject
.project
.pydevproject
.settings
.idea
.vscode
tags

# Package files
*.egg
*.eggs/
.installed.cfg
*.egg-info

# Unittest and coverage
htmlcov/*
.coverage
.coverage.*
.tox
junit*.xml
coverage.xml
.pytest_cache/

# Build and docs folder/files
build/*
dist/*
sdist/*
docs/api/*
docs/_rst/*
docs/_build/*
cover/*
MANIFEST

# Per-project virtualenvs
.venv*/
.conda*/
.python-version

# Lock files
uv.lock
*pylock.toml
abravalheri-validate-pyproject-4b2e70d/.coveragerc0000664000175000017500000000163115140154751022216 0ustar  carstencarsten# .coveragerc to control coverage.py
[run]
branch = True
source = validate_pyproject
omit =
    */validate_pyproject/__main__.py
    */validate_pyproject/**/__main__.py
    */validate_pyproject/_vendor/*

[paths]
source =
    src/
    */site-packages/

[report]
# Regexes for lines to exclude from consideration
exclude_lines =
    # Have to re-enable the standard pragma
    # (exclude_also would be better, but not available on Python 3.6)
    pragma: no cover

    # Don't complain about missing debug-only code:
    def __repr__

    # Don't complain if tests don't hit defensive assertion code:
    raise AssertionError
    raise NotImplementedError

    # Don't complain if non-runnable code isn't run:
    if 0:
    if __name__ == .__main__.:

    if TYPE_CHECKING:
    if typing\.TYPE_CHECKING:
    ^\s+\.\.\.$

    # Support for Pyodide (WASM)
    if sys\.platform == .emscripten. and .pyodide. in sys\.modules:
abravalheri-validate-pyproject-4b2e70d/README.rst0000664000175000017500000002027315140154751021567 0ustar  carstencarsten.. These are examples of badges you might want to add to your README:
   please update the URLs accordingly

    .. image:: https://img.shields.io/conda/vn/conda-forge/validate-pyproject.svg
        :alt: Conda-Forge
        :target: https://anaconda.org/conda-forge/validate-pyproject
    .. image:: https://pepy.tech/badge/validate-pyproject/month
        :alt: Monthly Downloads
        :target: https://pepy.tech/project/validate-pyproject
    .. image:: https://img.shields.io/twitter/url/http/shields.io.svg?style=social&label=Twitter
        :alt: Twitter
        :target: https://twitter.com/validate-pyproject

.. image:: https://img.shields.io/badge/-PyScaffold-005CA0?logo=pyscaffold
    :alt: Project generated with PyScaffold
    :target: https://pyscaffold.org/
.. image:: https://api.cirrus-ci.com/github/abravalheri/validate-pyproject.svg?branch=main
    :alt: Built Status
    :target: https://cirrus-ci.com/github/abravalheri/validate-pyproject
.. image:: https://readthedocs.org/projects/validate-pyproject/badge/?version=latest
    :alt: ReadTheDocs
    :target: https://validate-pyproject.readthedocs.io
.. image:: https://img.shields.io/coveralls/github/abravalheri/validate-pyproject/main.svg
    :alt: Coveralls
    :target: https://coveralls.io/r/abravalheri/validate-pyproject
.. image:: https://img.shields.io/pypi/v/validate-pyproject.svg
    :alt: PyPI-Server
    :target: https://pypi.org/project/validate-pyproject/

|

==================
validate-pyproject
==================


    Automated checks on ``pyproject.toml`` powered by JSON Schema definitions


.. important:: This project is **experimental** and under active development.
   Issue reports and contributions are very welcome.


Description
===========

With the approval of `PEP 517`_ and `PEP 518`_, the Python community shifted
towards a strong focus on standardisation for packaging software, which allows
more freedom when choosing tools during development and make sure packages
created using different technologies can interoperate without the need for
custom installation procedures.

This shift became even more clear when `PEP 621`_ was also approved, as a
standardised way of specifying project metadata and dependencies.

``validate-pyproject`` was born in this context, with the mission of validating
``pyproject.toml`` files, and make sure they are compliant with the standards
and PEPs. Behind the scenes, ``validate-pyproject`` relies on `JSON Schema`_
files, which, in turn, are also a standardised way of checking if a given data
structure complies with a certain specification.


.. _installation:

Usage
=====

The easiest way of using ``validate-pyproject`` is via CLI.
To get started, you need to install the package, which can be easily done
using |pipx|_:

.. code-block:: bash

    $ pipx install 'validate-pyproject[all]'
    # or to install and run in a single command
    $ pipx run 'validate-pyproject[all]' --help

Now you can use ``validate-pyproject`` as a command line tool:

.. code-block:: bash

    # in you terminal
    $ validate-pyproject --help
    $ validate-pyproject path/to/your/pyproject.toml

You can also use ``validate-pyproject`` in your Python scripts or projects:

.. _example-api:

.. code-block:: python

    # in your python code
    from validate_pyproject import api, errors

    # let's assume that you have access to a `loads` function
    # responsible for parsing a string representing the TOML file
    # (you can check the `toml` or `tomli` projects for that)
    pyproject_as_dict = loads(pyproject_toml_str)

    # now we can use validate-pyproject
    validator = api.Validator()

    try:
        validator(pyproject_as_dict)
    except errors.ValidationError as ex:
        print(f"Invalid Document: {ex.message}")

To do so, don't forget to add it to your `virtual environment`_ or specify it as a
`project`_ or `library dependency`_.

.. note::
   When you install ``validate-pyproject[all]``, the packages ``tomli``,
   ``packaging`` and ``trove-classifiers`` will be automatically pulled as
   dependencies. ``tomli`` is a lightweight parser for TOML, while
   ``packaging`` and ``trove-classifiers`` are used to validate aspects of `PEP
   621`_.

   If you are only interested in using the Python API and wants to keep the
   dependencies minimal, you can also install ``validate-pyproject``
   (without the ``[all]`` extra dependencies group).

   If you don't install ``trove-classifiers``, ``validate-pyproject`` will
   try to download a list of valid classifiers directly from PyPI
   (to prevent that, set the environment variable
   ``NO_NETWORK`` or ``VALIDATE_PYPROJECT_NO_NETWORK``).

   On the other hand, if ``validate-pyproject`` cannot find a copy of
   ``packaging`` in your environment, the validation will fail.

More details about ``validate-pyproject`` and its Python API can be found in
`our docs`_, which includes a description of the `used JSON schemas`_,
instructions for using it in a |pre-compiled way|_ and information about
extending the validation with your own plugins_.

.. _pyscaffold-notes:

.. tip::
   If you consider contributing to this project, have a look on our
   `contribution guides`_.

Plugins
=======

The `validate-pyproject-schema-store`_ plugin has a vendored copy of
pyproject.toml related `SchemaStore`_ entries.  You can even install this using
the ``[store]`` extra:

    $ pipx install 'validate-pyproject[all,store]'

Some of the tools in SchemaStore also have integrated validate-pyproject
plugins, like ``cibuildwheel`` and ``scikit-build-core``. However, unless you
want to pin an exact version of those tools, the SchemaStore copy is lighter
weight than installing the entire package.

If you want to write a custom plugin for your tool, please consider also contributing a copy to SchemaStore.

pre-commit
==========

``validate-pyproject`` can be installed as a pre-commit hook:

.. code-block:: yaml

    ---
    repos:
      - repo: https://github.com/abravalheri/validate-pyproject
        rev: 
        hooks:
          - id: validate-pyproject
            # Optional extra validations from SchemaStore:
            additional_dependencies: ["validate-pyproject-schema-store[all]"]

By default, this ``pre-commit`` hook will only validate the ``pyproject.toml``
file at the root of the project repository.
You can customize that by defining a `custom regular expression pattern`_ using
the ``files`` parameter.

You can also use ``pre-commit autoupdate`` to update to the latest stable
version of ``validate-pyproject`` (recommended).

You can also use `validate-pyproject-schema-store`_ as a pre-commit hook, which
allows pre-commit to pin and update that instead of ``validate-pyproject`` itself.

Note
====

This project and its sister project ini2toml_ were initially created in the
context of PyScaffold, with the purpose of helping migrating existing projects
to `PEP 621`_-style configuration when it is made available on ``setuptools``.
For details and usage information on PyScaffold see https://pyscaffold.org/.


.. |pipx| replace:: ``pipx``
.. |pre-compiled way| replace:: *pre-compiled* way


.. _contribution guides: https://validate-pyproject.readthedocs.io/en/latest/contributing.html
.. _custom regular expression pattern: https://pre-commit.com/#regular-expressions
.. _our docs: https://validate-pyproject.readthedocs.io
.. _ini2toml: https://ini2toml.readthedocs.io
.. _JSON Schema: https://json-schema.org/
.. _library dependency: https://setuptools.pypa.io/en/latest/userguide/dependency_management.html
.. _PEP 517: https://peps.python.org/pep-0517/
.. _PEP 518: https://peps.python.org/pep-0518/
.. _PEP 621: https://peps.python.org/pep-0621/
.. _pipx: https://pipx.pypa.io/stable/
.. _project: https://packaging.python.org/tutorials/managing-dependencies/
.. _setuptools: https://setuptools.pypa.io/en/stable/
.. _used JSON schemas: https://validate-pyproject.readthedocs.io/en/latest/schemas.html
.. _pre-compiled way: https://validate-pyproject.readthedocs.io/en/latest/embedding.html
.. _plugins: https://validate-pyproject.readthedocs.io/en/latest/dev-guide.html
.. _virtual environment: https://realpython.com/python-virtual-environments-a-primer/
.. _validate-pyproject-schema-store: https://github.com/henryiii/validate-pyproject-schema-store
.. _SchemaStore: https://www.schemastore.org
abravalheri-validate-pyproject-4b2e70d/.pre-commit-hooks.yaml0000664000175000017500000000044015140154751024231 0ustar  carstencarsten---
- id: validate-pyproject
  name: Validate pyproject.toml
  description: >
    Validation library for a simple check on pyproject.toml,
    including optional dependencies
  language: python
  files: ^pyproject.toml$
  entry: validate-pyproject
  additional_dependencies:
    - .[all]
abravalheri-validate-pyproject-4b2e70d/tools/0000775000175000017500000000000015140154751021234 5ustar  carstencarstenabravalheri-validate-pyproject-4b2e70d/tools/cache_urls_for_tests.py0000664000175000017500000000332515140154751026011 0ustar  carstencarstenimport json
import logging
import os
import sys
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

HERE = Path(__file__).parent.resolve()
PROJECT = HERE.parent

sys.path.insert(0, str(PROJECT / "src"))  # <-- Use development version of library
logging.basicConfig(level=logging.DEBUG)

from validate_pyproject import caching, http  # noqa: E402

SCHEMA_STORE = "https://json.schemastore.org/pyproject.json"


def iter_test_urls():
    with caching.as_file(http.open_url, SCHEMA_STORE) as f:
        store = json.load(f)
        for tool in store["properties"]["tool"]["properties"].values():
            if "$ref" in tool and tool["$ref"].startswith(("http://", "https://")):
                yield tool["$ref"]

    files = PROJECT.glob("**/test_config.json")
    for file in files:
        content = json.loads(file.read_text("utf-8"))
        for url in content.get("tools", {}).values():
            if url.startswith(("http://", "https://")):
                yield url


def download(url):
    return caching.as_file(http.open_url, url).close()
    # ^-- side-effect only: write cached file


def download_all(cache: str):  # noqa: ARG001
    with ThreadPoolExecutor(max_workers=5) as executor:
        return list(executor.map(download, set(iter_test_urls())))  # Consume iterator


if __name__ == "__main__":
    cache = os.getenv("VALIDATE_PYPROJECT_CACHE_REMOTE")
    if not cache:
        msg = "Please define VALIDATE_PYPROJECT_CACHE_REMOTE"
        raise SystemExit(msg)

    Path(cache).mkdir(parents=True, exist_ok=True)
    downloads = download_all(cache)
    assert len(downloads) > 0, f"empty {downloads=!r}"
    files = list(map(print, Path(cache).iterdir()))
    assert len(files) > 0, f"empty {files=!r}"
abravalheri-validate-pyproject-4b2e70d/tools/to_schemastore.py0000775000175000017500000000171215140154751024631 0ustar  carstencarsten#!/usr/bin/env python3

import argparse
import json


def convert_tree(tree: dict[str, object]) -> None:
    for key, value in list(tree.items()):
        match key, value:
            case "$$description", list():
                tree["description"] = " ".join(value)
                del tree["$$description"]
            case "$id", str():
                del tree["$id"]
            case _, dict():
                convert_tree(value)
            case _, list():
                for item in value:
                    if isinstance(item, dict):
                        convert_tree(item)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("schema", help="JSONSchema to convert")
    args = parser.parse_args()

    with open(args.schema, encoding="utf-8") as f:
        schema = json.load(f)

    convert_tree(schema)
    schema["$id"] = "https://json.schemastore.org/setuptools.json"

    print(json.dumps(schema, indent=2))