From 4f512dc75ccad4eb1c9b150449948dceb48eda47 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 24 Jan 2024 12:45:18 +0100 Subject: [PATCH 01/61] Keep GitHub Actions up to date with GitHub's Dependabot Automate the creation of pull requests like * microsoft/TaskWeaver#158 * 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 --- .github/dependabot.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c8efe00 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# 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: + GitHub_Actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request + schedule: + interval: weekly From 90411dcbf5f5f37b5a21db516fcdcfd51df7c53f Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 24 Jan 2024 18:36:24 +0100 Subject: [PATCH 02/61] Update .github/dependabot.yml Co-authored-by: Henry Schreiner --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c8efe00..f3267e9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,7 +6,7 @@ updates: - package-ecosystem: "github-actions" directory: "/" groups: - GitHub_Actions: + actions: patterns: - "*" # Group all Actions updates into a single larger pull request schedule: From 95a7cdc678162df411129c58010ab8fe421ac5a0 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 24 Jan 2024 20:07:02 +0100 Subject: [PATCH 03/61] Fix linkcheck --- CONTRIBUTING.rst | 1 + README.rst | 2 +- src/validate_pyproject/extra_validations.py | 2 +- src/validate_pyproject/project_metadata.schema.json | 4 ++-- src/validate_pyproject/pyproject_toml.schema.json | 4 ++-- tests/test_api.py | 6 +++--- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 0ce75c2..ad62378 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -240,6 +240,7 @@ package: Maintainer tasks ================ + Releases -------- diff --git a/README.rst b/README.rst index 768d3ad..cbb840b 100644 --- a/README.rst +++ b/README.rst @@ -181,7 +181,7 @@ For details and usage information on PyScaffold see https://pyscaffold.org/. .. _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://pypa.github.io/pipx/ +.. _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 diff --git a/src/validate_pyproject/extra_validations.py b/src/validate_pyproject/extra_validations.py index 760acf9..c4ffe65 100644 --- a/src/validate_pyproject/extra_validations.py +++ b/src/validate_pyproject/extra_validations.py @@ -20,7 +20,7 @@ class RedefiningStaticFieldAsDynamic(ValidationError): __doc__ = _DESC _URL = ( "https://packaging.python.org/en/latest/specifications/" - "declaring-project-metadata/#dynamic" + "pyproject-toml/#dynamic" ) diff --git a/src/validate_pyproject/project_metadata.schema.json b/src/validate_pyproject/project_metadata.schema.json index 8ad7199..d7874ca 100644 --- a/src/validate_pyproject/project_metadata.schema.json +++ b/src/validate_pyproject/project_metadata.schema.json @@ -1,7 +1,7 @@ -{ +pyproject-toml{ "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://packaging.python.org/en/latest/specifications/declaring-project-metadata/", + "$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``", diff --git a/src/validate_pyproject/pyproject_toml.schema.json b/src/validate_pyproject/pyproject_toml.schema.json index 2e06117..4dbaba4 100644 --- a/src/validate_pyproject/pyproject_toml.schema.json +++ b/src/validate_pyproject/pyproject_toml.schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/", + "$id": "https://packaging.python.org/en/latest/specifications/dependency-specifiers/", "title": "Data structure for ``pyproject.toml`` files", "$$description": [ "File format containing build-time configurations for the Python ecosystem. ", @@ -55,7 +55,7 @@ }, "project": { - "$ref": "https://packaging.python.org/en/latest/specifications/declaring-project-metadata/" + "$ref": "https://packaging.python.org/en/latest/specifications/pyproject-toml/" }, "tool": { diff --git a/tests/test_api.py b/tests/test_api.py index 48e7ce2..72cef0a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -14,10 +14,10 @@ def test_load(): spec = api.load("pyproject_toml") assert isinstance(spec, Mapping) - assert spec["$id"] == f"{PYPA_SPECS}/declaring-build-dependencies/" + assert spec["$id"] == f"{PYPA_SPECS}/dependency-specifiers/" spec = api.load("project_metadata") - assert spec["$id"] == f"{PYPA_SPECS}/declaring-project-metadata/" + assert spec["$id"] == f"{PYPA_SPECS}/pyproject-toml/" def test_load_plugin(): @@ -36,7 +36,7 @@ def test_with_plugins(self): registry = api.SchemaRegistry(plg) main_schema = registry[registry.main] project = main_schema["properties"]["project"] - assert project["$ref"] == f"{PYPA_SPECS}/declaring-project-metadata/" + assert project["$ref"] == f"{PYPA_SPECS}/pyproject-toml/" tool = main_schema["properties"]["tool"] assert "setuptools" in tool["properties"] assert "$ref" in tool["properties"]["setuptools"] From 2eb721efb1fedf713002499141f7087675e75497 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 24 Jan 2024 20:26:23 +0100 Subject: [PATCH 04/61] Update src/validate_pyproject/project_metadata.schema.json --- src/validate_pyproject/project_metadata.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validate_pyproject/project_metadata.schema.json b/src/validate_pyproject/project_metadata.schema.json index d7874ca..43f2411 100644 --- a/src/validate_pyproject/project_metadata.schema.json +++ b/src/validate_pyproject/project_metadata.schema.json @@ -1,4 +1,4 @@ -pyproject-toml{ +{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://packaging.python.org/en/latest/specifications/pyproject-toml/", From cc988ade29e45a6244dcdc303cf4d843ab2c0a05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 19:30:29 +0000 Subject: [PATCH 05/61] chore(deps): bump the actions group with 4 updates Bumps the actions group with 4 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-python](https://github.com/actions/setup-python), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact). Updates `actions/checkout` from 3 to 4 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) Updates `actions/setup-python` from 4 to 5 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) Updates `actions/upload-artifact` from 3 to 4 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v3...v4) Updates `actions/download-artifact` from 3 to 4 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1656e21..5533b99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,9 +25,9 @@ jobs: outputs: wheel-distribution: ${{ steps.wheel-distribution.outputs.path }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: {fetch-depth: 0} # deep clone for setuptools-scm - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: {python-version: "3.10"} - name: Run static analysis and format checkers run: pipx run --python python3.10 tox -e lint,typecheck @@ -39,7 +39,7 @@ jobs: - 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@v3 + uses: actions/upload-artifact@v4 with: name: python-distribution-files path: dist/ @@ -58,12 +58,12 @@ jobs: - windows-latest runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - name: Retrieve pre-built distribution files - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: {name: python-distribution-files, path: dist/} - name: Run tests run: >- @@ -95,11 +95,11 @@ jobs: if: ${{ github.event_name == 'push' && contains(github.ref, 'refs/tags/') }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: {python-version: "3.10"} - name: Retrieve pre-built distribution files - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: {name: python-distribution-files, path: dist/} - name: Publish Package env: From 3adaeec39a222e10a01f52802f47ceebd9ba5ce5 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 27 Jan 2024 09:59:46 -0500 Subject: [PATCH 06/61] docs: add mention of store plugin (#146) * docs: add mention of store plugin Signed-off-by: Henry Schreiner * Apply suggestions from code review Co-authored-by: Henry Schreiner --------- Signed-off-by: Henry Schreiner Co-authored-by: Anderson Bravalheri --- README.rst | 24 +++++++++++++++++++++++- setup.cfg | 7 +++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index cbb840b..dfe6f38 100644 --- a/README.rst +++ b/README.rst @@ -136,6 +136,22 @@ extending the validation with your own plugins_. 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 ========== @@ -146,9 +162,11 @@ pre-commit --- repos: - repo: https://github.com/abravalheri/validate-pyproject - rev: main + 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. @@ -158,6 +176,8 @@ 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 ==== @@ -188,3 +208,5 @@ For details and usage information on PyScaffold see https://pyscaffold.org/. .. _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 diff --git a/setup.cfg b/setup.cfg index 29361dc..ad38f9b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,11 @@ platforms = any # https://pypi.org/classifiers/ classifiers = Development Status :: 4 - Beta + Intended Audience :: Developers + Operating System :: OS Independent Programming Language :: Python + Topic :: Software Development :: Quality Assurance + Typing :: Typed [options] @@ -66,6 +70,9 @@ all = packaging>=20.4 trove-classifiers>=2021.10.20 +store = + validate-pyproject-schema-store + # Add here test requirements (semicolon/line-separated) testing = setuptools From 2ae3a8746b5a580bd9cdcd92115702a0bcd01d84 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 11:53:56 -0500 Subject: [PATCH 07/61] [pre-commit.ci] pre-commit autoupdate (#149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.14 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.14...v0.2.0) - [github.com/python-jsonschema/check-jsonschema: 0.27.3 → 0.27.4](https://github.com/python-jsonschema/check-jsonschema/compare/0.27.3...0.27.4) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bf490de..91b30ba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: args: [-w] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.14 # Ruff version + rev: v0.2.0 # Ruff version hooks: - id: ruff args: [--fix, --show-fixes] @@ -56,7 +56,7 @@ repos: - validate-pyproject[all]>=0.13 - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.27.3 + rev: 0.27.4 hooks: - id: check-metaschema files: \.schema\.json$ From 446330697b3849b953510f00bc6406950b0a5a50 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 7 Feb 2024 14:28:31 +0000 Subject: [PATCH 08/61] Return more appropriate URL for [build-system] table --- src/validate_pyproject/pyproject_toml.schema.json | 2 +- tests/test_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/validate_pyproject/pyproject_toml.schema.json b/src/validate_pyproject/pyproject_toml.schema.json index 4dbaba4..f540981 100644 --- a/src/validate_pyproject/pyproject_toml.schema.json +++ b/src/validate_pyproject/pyproject_toml.schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://packaging.python.org/en/latest/specifications/dependency-specifiers/", + "$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. ", diff --git a/tests/test_api.py b/tests/test_api.py index 72cef0a..87f1a31 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -14,7 +14,7 @@ def test_load(): spec = api.load("pyproject_toml") assert isinstance(spec, Mapping) - assert spec["$id"] == f"{PYPA_SPECS}/dependency-specifiers/" + assert spec["$id"] == f"{PYPA_SPECS}/declaring-build-dependencies/" spec = api.load("project_metadata") assert spec["$id"] == f"{PYPA_SPECS}/pyproject-toml/" From b5797b99a7310bf9982ade24a6791810b28711b6 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 7 Feb 2024 17:12:48 +0000 Subject: [PATCH 09/61] Add caching to ``remote`` --- .ruff.toml | 5 +- src/validate_pyproject/caching.py | 46 ++++++++++++++++ src/validate_pyproject/http.py | 13 +++++ src/validate_pyproject/remote.py | 26 ++++----- tests/conftest.py | 4 ++ tests/test_caching.py | 87 +++++++++++++++++++++++++++++++ tox.ini | 1 + 7 files changed, 164 insertions(+), 18 deletions(-) create mode 100644 src/validate_pyproject/caching.py create mode 100644 src/validate_pyproject/http.py create mode 100644 tests/test_caching.py diff --git a/.ruff.toml b/.ruff.toml index be8642c..e2fce3b 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -30,7 +30,10 @@ ignore = [ ] [lint.per-file-ignores] -"tests/*" = ["S"] # Assert okay in tests +"tests/*" = [ + "S", # Assert okay in tests + "PLR2004", # Magic value comparison is actually desired in tests +] # --- Tool-related config --- diff --git a/src/validate_pyproject/caching.py b/src/validate_pyproject/caching.py new file mode 100644 index 0000000..6ddd43e --- /dev/null +++ b/src/validate_pyproject/caching.py @@ -0,0 +1,46 @@ +import hashlib +import io +import logging +import os +from pathlib import Path +from typing import Callable, Optional, Union + +PathLike = Union[str, os.PathLike] +_logger = logging.getLogger(__name__) + + +def as_file( + fn: Callable[[str], io.StringIO], + arg: str, + cache_dir: Optional[PathLike] = None, +) -> Union[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") # noqa: SIM115 -- not relevant + + +def path_for(arbitrary_id: str, cache: Optional[PathLike] = None) -> Optional[Path]: + 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 diff --git a/src/validate_pyproject/http.py b/src/validate_pyproject/http.py new file mode 100644 index 0000000..09dcb11 --- /dev/null +++ b/src/validate_pyproject/http.py @@ -0,0 +1,13 @@ +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:")): + raise ValueError("URL must start with 'http:' or 'https:'") + with urlopen(url) as response: # noqa: S310 + return io.StringIO(response.read().decode("utf-8")) diff --git a/src/validate_pyproject/remote.py b/src/validate_pyproject/remote.py index c024050..2194c17 100644 --- a/src/validate_pyproject/remote.py +++ b/src/validate_pyproject/remote.py @@ -1,31 +1,20 @@ -import io import json import logging -import sys import typing import urllib.parse -import urllib.request -from typing import Generator, Tuple +from typing import Generator, Optional, Tuple -from . import errors +from . import caching, errors, http from .types import Schema if typing.TYPE_CHECKING: + import sys + if sys.version_info < (3, 11): from typing_extensions import Self else: from typing import Self -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:")): - raise ValueError("URL must start with 'http:' or 'https:'") - with urllib.request.urlopen(url) as response: # noqa: S310 - return io.StringIO(response.read().decode("utf-8")) - __all__ = ["RemotePlugin", "load_store"] @@ -33,11 +22,14 @@ def open_url(url: str) -> io.StringIO: _logger = logging.getLogger(__name__) -def load_from_uri(tool_uri: str) -> Tuple[str, Schema]: +def load_from_uri( + tool_uri: str, cache_dir: Optional[caching.PathLike] = 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}" - with open_url(url) as f: + 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: diff --git a/tests/conftest.py b/tests/conftest.py index ad76269..6ae3ccf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,10 @@ 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")] diff --git a/tests/test_caching.py b/tests/test_caching.py new file mode 100644 index 0000000..22a306c --- /dev/null +++ b/tests/test_caching.py @@ -0,0 +1,87 @@ +import 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") + + +def fn1(arg: str) -> io.StringIO: + return io.StringIO("42") + + +def fn2(arg: str) -> io.StringIO: + raise RuntimeError("should not be called") + + +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(tmp_path): + # 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", tmp_path) + + +def test_path_for_no_cache(monkeypatch): + 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() diff --git a/tox.ini b/tox.ini index 90dc453..d693c37 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ setenv = passenv = HOME SETUPTOOLS_* + VALIDATE_PYPROJECT_* extras = all testing From a656428077fc6da892bd0ae764db6f0ddc244387 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 8 Feb 2024 17:31:26 +0000 Subject: [PATCH 10/61] Use caching in Github actions --- .github/workflows/ci.yml | 17 +++++++++++ src/validate_pyproject/caching.py | 2 ++ src/validate_pyproject/http.py | 2 ++ tools/cache_urls_for_tests.py | 50 +++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 tools/cache_urls_for_tests.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5533b99..b7db5aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,10 @@ concurrency: ${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true +env: + VALIDATE_PYPROJECT_CACHE_REMOTE: ~/.cache/validate-pyproject + + jobs: prepare: runs-on: ubuntu-latest @@ -44,6 +48,14 @@ jobs: name: python-distribution-files path: dist/ retention-days: 1 + - name: Download files used for testing + run: python.10 tools/cache_urls_for_tests.py + - name: Store downloaded files + uses: actions/upload-artifact@v4 + with: + name: test-download-files + path: ${{ env.VALIDATE_PYPROJECT_CACHE_REMOTE }} + retention-days: 1 test: needs: prepare @@ -65,6 +77,11 @@ jobs: - name: Retrieve pre-built distribution files uses: actions/download-artifact@v4 with: {name: python-distribution-files, path: dist/} + - name: Retrieve test download files + uses: actions/download-artifact@v4 + with: + name: test-download-files + path: ${{ env.VALIDATE_PYPROJECT_CACHE_REMOTE }} - name: Run tests run: >- pipx run tox diff --git a/src/validate_pyproject/caching.py b/src/validate_pyproject/caching.py index 6ddd43e..2c83e3f 100644 --- a/src/validate_pyproject/caching.py +++ b/src/validate_pyproject/caching.py @@ -1,3 +1,5 @@ +# This module is intentionally kept minimal, +# so that it can be imported without triggering imports outside stdlib. import hashlib import io import logging diff --git a/src/validate_pyproject/http.py b/src/validate_pyproject/http.py index 09dcb11..248e2e7 100644 --- a/src/validate_pyproject/http.py +++ b/src/validate_pyproject/http.py @@ -1,3 +1,5 @@ +# 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 diff --git a/tools/cache_urls_for_tests.py b/tools/cache_urls_for_tests.py new file mode 100644 index 0000000..cbd7dd5 --- /dev/null +++ b/tools/cache_urls_for_tests.py @@ -0,0 +1,50 @@ +import 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"].items(): + 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", {}).items(): + 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): + with ThreadPoolExecutor(max_workers=5) as executor: + executor.map(download, set(iter_test_urls())) + + +if __name__ == "__main__": + cache = os.getenv("VALIDATE_PYPROJECT_CACHE_REMOTE") + if not cache: + raise SystemExit("Please define VALIDATE_PYPROJECT_CACHE_REMOTE") + + Path(cache).mkdir(exist_ok=True) + download_all(cache) From e163d3cb9222b2aa3cd03c6d614a6381ddabc201 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 8 Feb 2024 19:46:03 +0000 Subject: [PATCH 11/61] Fix test_caching use of mobkeypatch. --- tests/test_caching.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_caching.py b/tests/test_caching.py index 22a306c..852151e 100644 --- a/tests/test_caching.py +++ b/tests/test_caching.py @@ -9,7 +9,7 @@ @pytest.fixture(autouse=True) def no_cache_env_var(monkeypatch): - monkeypatch.delenv("VALIDATE_PYPROJECT_CACHE_REMOTE") + monkeypatch.delenv("VALIDATE_PYPROJECT_CACHE_REMOTE", raising=False) def fn1(arg: str) -> io.StringIO: From ead71cf5f53951fce64ca883889c6859867e2548 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 9 Feb 2024 10:29:43 +0000 Subject: [PATCH 12/61] Fix .github/workflows/ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7db5aa..544f8f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: path: dist/ retention-days: 1 - name: Download files used for testing - run: python.10 tools/cache_urls_for_tests.py + run: python3.10 tools/cache_urls_for_tests.py - name: Store downloaded files uses: actions/upload-artifact@v4 with: From 8a4b322705c9332fce6157a6fcc2b556f6bc19c0 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 9 Feb 2024 10:38:20 +0000 Subject: [PATCH 13/61] Fix caching script --- tools/cache_urls_for_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/cache_urls_for_tests.py b/tools/cache_urls_for_tests.py index cbd7dd5..14c5e78 100644 --- a/tools/cache_urls_for_tests.py +++ b/tools/cache_urls_for_tests.py @@ -46,5 +46,5 @@ def download_all(cache: str): if not cache: raise SystemExit("Please define VALIDATE_PYPROJECT_CACHE_REMOTE") - Path(cache).mkdir(exist_ok=True) + Path(cache).mkdir(parents=True, exist_ok=True) download_all(cache) From c1f3abd247be31632c7d9f228d98a96b407dcd5d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 9 Feb 2024 11:04:51 +0000 Subject: [PATCH 14/61] Attempt to add caching to cirrus --- .cirrus.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.cirrus.yml b/.cirrus.yml index f9d2941..023c62a 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -14,6 +14,8 @@ env: 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 ---- @@ -40,6 +42,11 @@ env: depends_on: [build] <<: *task-template dist_cache: {folder: dist, fingerprint_script: echo $CIRRUS_BUILD_ID} # download + 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 test_script: > tox --installpkg dist/*.whl -- -n 5 --randomly-seed=42 -rfEx --durations 10 --color yes From 3c77d53b2990af0bb48a65ea163f063fa16c485e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 9 Feb 2024 13:04:30 +0000 Subject: [PATCH 15/61] Fix caching test --- tests/test_caching.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_caching.py b/tests/test_caching.py index 852151e..65844a9 100644 --- a/tests/test_caching.py +++ b/tests/test_caching.py @@ -42,11 +42,11 @@ def test_as_file(tmp_path): caching.as_file(fn2, "hello-world", tmp_path) -def test_as_file_no_cache(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", tmp_path) + caching.as_file(fn2, "hello-world") def test_path_for_no_cache(monkeypatch): From 2a3789fd90cdef88806ac8aafe62621063b9f77e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 9 Feb 2024 13:08:25 +0000 Subject: [PATCH 16/61] Update version regex according to latest packaging version According to `pypa/packaging#705`. --- src/validate_pyproject/formats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validate_pyproject/formats.py b/src/validate_pyproject/formats.py index 084cb83..fbf8364 100644 --- a/src/validate_pyproject/formats.py +++ b/src/validate_pyproject/formats.py @@ -21,7 +21,7 @@ (?P[0-9]+(?:\.[0-9]+)*) # release segment (?P
                                          # pre-release
             [-_\.]?
-            (?P(a|b|c|rc|alpha|beta|pre|preview))
+            (?Palpha|a|beta|b|preview|pre|c|rc)
             [-_\.]?
             (?P[0-9]+)?
         )?

From 8c68832edd4cf10d3e7093f03b65837896339a17 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 12 Feb 2024 10:52:41 +0000
Subject: [PATCH 17/61] Improve syntax parity in `.cirrus.yml` for environment
 variables.

---
 .cirrus.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.cirrus.yml b/.cirrus.yml
index 023c62a..95448bf 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -43,7 +43,7 @@ env:
   <<: *task-template
   dist_cache: {folder: dist, fingerprint_script: echo $CIRRUS_BUILD_ID}  # download
   test_files_cache:
-    folder: $VALIDATE_PYPROJECT_CACHE_REMOTE
+    folder: ${VALIDATE_PYPROJECT_CACHE_REMOTE}
     fingerprint_script: echo $CIRRUS_BUILD_ID
     populate_script: python tools/cache_urls_for_tests.py
     reupload_on_changes: true

From cfe397b499947d44a8ae60fbf82b0555bacc8c9b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 12 Feb 2024 11:43:05 +0000
Subject: [PATCH 18/61] Activate xdist for Github actions

---
 .github/workflows/ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 544f8f0..3f97bbb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -86,7 +86,7 @@ jobs:
         run: >-
           pipx run tox
           --installpkg '${{ needs.prepare.outputs.wheel-distribution }}'
-          -- -rFEx --durations 10 --color yes
+          -- -n 5 -rFEx --durations 10 --color yes
       - name: Generate coverage report
         run: pipx run coverage lcov -o coverage.lcov
       - name: Upload partial coverage report

From e10a888844c4b31d982b2ff26931fd1a52c89138 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 12 Feb 2024 12:20:58 +0000
Subject: [PATCH 19/61] Attempt to solve CI failures by canchging cache folder

---
 .github/workflows/ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3f97bbb..8839914 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,7 +20,7 @@ concurrency:
   cancel-in-progress: true
 
 env:
-  VALIDATE_PYPROJECT_CACHE_REMOTE: ~/.cache/validate-pyproject
+  VALIDATE_PYPROJECT_CACHE_REMOTE: tests/.cache
 
 
 jobs:

From 4535fc536394f2e8e38bb523d3659ea70c103086 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 12 Feb 2024 16:39:44 +0000
Subject: [PATCH 20/61] [pre-commit.ci] pre-commit autoupdate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.2.0 → v0.2.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.0...v0.2.1)
- [github.com/python-jsonschema/check-jsonschema: 0.27.4 → 0.28.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.27.4...0.28.0)
---
 .pre-commit-config.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 91b30ba..8b952fd 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -26,7 +26,7 @@ repos:
     args: [-w]
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.2.0  # Ruff version
+  rev: v0.2.1  # Ruff version
   hooks:
   - id: ruff
     args: [--fix, --show-fixes]
@@ -56,7 +56,7 @@ repos:
       - validate-pyproject[all]>=0.13
 
 - repo: https://github.com/python-jsonschema/check-jsonschema
-  rev: 0.27.4
+  rev: 0.28.0
   hooks:
     - id: check-metaschema
       files: \.schema\.json$

From 8266383a725ecc6b787d08f965c0dbbf225d9d2c Mon Sep 17 00:00:00 2001
From: Avasam 
Date: Sun, 25 Feb 2024 16:09:07 -0500
Subject: [PATCH 21/61] Remove invalid `# type: ignore` statement

On mypy for PyPy, this causes a `[syntax]` error: https://github.com/pypa/setuptools/actions/runs/8040607909/job/21958905599?pr=4192#step:9:1517 **even if the file is excluded** (because it is imported)

On regular mypy this causes `error: Module "validate_pyproject.fastjsonschema_validations" has no attribute "validate"  [attr-defined]` (in setuptools, the exact error was: `error: Module "setuptools.config._validate_pyproject.fastjsonschema_validations" has no attribute "validate"  [attr-defined]`
---
 src/validate_pyproject/pre_compile/__init__.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/validate_pyproject/pre_compile/__init__.py b/src/validate_pyproject/pre_compile/__init__.py
index 0a597ca..4c03204 100644
--- a/src/validate_pyproject/pre_compile/__init__.py
+++ b/src/validate_pyproject/pre_compile/__init__.py
@@ -112,7 +112,6 @@ def load_licenses() -> Dict[str, str]:
 
 NOCHECK_HEADERS = (
     "# noqa",
-    "# type: ignore",
     "# ruff: noqa",
     "# flake8: noqa",
     "# pylint: skip-file",

From 0c792fde4e31af9f65e88e16805dc307f411e432 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 26 Feb 2024 11:00:56 -0500
Subject: [PATCH 22/61] [pre-commit.ci] pre-commit autoupdate (#157)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.2.1 → v0.2.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.1...v0.2.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
 .pre-commit-config.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8b952fd..85c93d3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -26,7 +26,7 @@ repos:
     args: [-w]
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.2.1  # Ruff version
+  rev: v0.2.2  # Ruff version
   hooks:
   - id: ruff
     args: [--fix, --show-fixes]

From 26ac4b6f0fb2932e9ba1d526c5408af603d55f4f Mon Sep 17 00:00:00 2001
From: Avasam 
Date: Mon, 26 Feb 2024 11:01:08 -0500
Subject: [PATCH 23/61] Remove duplicate `# ruff: noqa` (#158)

* Remove duplicate `# ruff: noqa`

* Update .cirrus.yml

* Update .cirrus.yml

---------

Co-authored-by: Henry Schreiner 
---
 .cirrus.yml                                    | 4 ++--
 src/validate_pyproject/pre_compile/__init__.py | 1 -
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/.cirrus.yml b/.cirrus.yml
index 95448bf..9b67ab8 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -140,7 +140,7 @@ freebsd_task:
   <<: *test-template
 
 windows_task:
-  name: test (Windows - 3.9.10)
+  name: test (Windows - 3.9.13)
   windows_container:
     image: "cirrusci/windowsservercore:2019"
     os_version: 2019
@@ -150,7 +150,7 @@ windows_task:
   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.9.10 --params "/NoLockdown"
+    - choco install -y --no-progress python3 --version=3.9.13 --params "/NoLockdown"
     - pip install --upgrade certifi
     - python -m pip install -U pip tox pipx
   <<: *test-template
diff --git a/src/validate_pyproject/pre_compile/__init__.py b/src/validate_pyproject/pre_compile/__init__.py
index 0a597ca..d54a341 100644
--- a/src/validate_pyproject/pre_compile/__init__.py
+++ b/src/validate_pyproject/pre_compile/__init__.py
@@ -117,7 +117,6 @@ def load_licenses() -> Dict[str, str]:
     "# flake8: noqa",
     "# pylint: skip-file",
     "# mypy: ignore-errors",
-    "# ruff: noqa",
     "# yapf: disable",
     "# pylama:skip=1",
     "\n\n# *** PLEASE DO NOT MODIFY DIRECTLY: Automatically generated code *** \n\n\n",

From 46d7ce48ab52891bf820ff8da9f8dbb8ba03a110 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 25 Mar 2024 13:16:01 -0400
Subject: [PATCH 24/61] [pre-commit.ci] pre-commit autoupdate (#160)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.2.2 → v0.3.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.2...v0.3.4)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
 .pre-commit-config.yaml                    |  2 +-
 src/validate_pyproject/api.py              |  1 +
 src/validate_pyproject/plugins/__init__.py | 15 +++++----------
 tests/conftest.py                          |  8 ++++----
 tests/test_json_schema_summary.py          |  1 +
 5 files changed, 12 insertions(+), 15 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 85c93d3..7ef8fcb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -26,7 +26,7 @@ repos:
     args: [-w]
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.2.2  # Ruff version
+  rev: v0.3.4  # Ruff version
   hooks:
   - id: ruff
     args: [--fix, --show-fixes]
diff --git a/src/validate_pyproject/api.py b/src/validate_pyproject/api.py
index 003bf78..b77fe12 100644
--- a/src/validate_pyproject/api.py
+++ b/src/validate_pyproject/api.py
@@ -1,6 +1,7 @@
 """
 Retrieve JSON schemas for validating dicts representing a ``pyproject.toml`` file.
 """
+
 import json
 import logging
 import sys
diff --git a/src/validate_pyproject/plugins/__init__.py b/src/validate_pyproject/plugins/__init__.py
index 813ac2a..16393eb 100644
--- a/src/validate_pyproject/plugins/__init__.py
+++ b/src/validate_pyproject/plugins/__init__.py
@@ -34,24 +34,19 @@
 
 class PluginProtocol(Protocol):
     @property
-    def id(self) -> str:
-        ...
+    def id(self) -> str: ...
 
     @property
-    def tool(self) -> str:
-        ...
+    def tool(self) -> str: ...
 
     @property
-    def schema(self) -> "Schema":
-        ...
+    def schema(self) -> "Schema": ...
 
     @property
-    def help_text(self) -> str:
-        ...
+    def help_text(self) -> str: ...
 
     @property
-    def fragment(self) -> str:
-        ...
+    def fragment(self) -> str: ...
 
 
 class PluginWrapper:
diff --git a/tests/conftest.py b/tests/conftest.py
index 6ae3ccf..dbeb13b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,9 +1,9 @@
 """
-    conftest.py for validate_pyproject.
+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
+Read more about conftest.py under:
+- https://docs.pytest.org/en/stable/fixture.html
+- https://docs.pytest.org/en/stable/writing_plugins.html
 """
 
 from pathlib import Path
diff --git a/tests/test_json_schema_summary.py b/tests/test_json_schema_summary.py
index a257847..2b39360 100644
--- a/tests/test_json_schema_summary.py
+++ b/tests/test_json_schema_summary.py
@@ -1,4 +1,5 @@
 """Test summary generation from schema examples"""
+
 import json
 from pathlib import Path
 

From 4acfa0ae71945c0df7475b20a30ac773d648bc96 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 15 Apr 2024 16:39:36 +0000
Subject: [PATCH 25/61] [pre-commit.ci] pre-commit autoupdate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

updates:
- [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0)
- [github.com/astral-sh/ruff-pre-commit: v0.3.4 → v0.3.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.4...v0.3.7)
- [github.com/python-jsonschema/check-jsonschema: 0.28.0 → 0.28.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.28.0...0.28.2)
---
 .pre-commit-config.yaml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7ef8fcb..ec5a7ee 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ exclude: '^src/validate_pyproject/_vendor'
 
 repos:
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.5.0
+  rev: v4.6.0
   hooks:
   - id: check-added-large-files
   - id: check-ast
@@ -26,7 +26,7 @@ repos:
     args: [-w]
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.3.4  # Ruff version
+  rev: v0.3.7  # Ruff version
   hooks:
   - id: ruff
     args: [--fix, --show-fixes]
@@ -56,7 +56,7 @@ repos:
       - validate-pyproject[all]>=0.13
 
 - repo: https://github.com/python-jsonschema/check-jsonschema
-  rev: 0.28.0
+  rev: 0.28.2
   hooks:
     - id: check-metaschema
       files: \.schema\.json$

From 2c1ba40a7512b575486dca3f1f3bc26b4cb4ed5e Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 17:23:10 +0100
Subject: [PATCH 26/61] Only add to docs references to public API

---
 docs/_gendocs.py    | 40 ++++++++++++++++++++++++++++++++++++++++
 docs/conf.py        | 41 +++++------------------------------------
 docs/modules.rst.in | 17 +++++++++++++++++
 3 files changed, 62 insertions(+), 36 deletions(-)
 create mode 100644 docs/_gendocs.py
 create mode 100644 docs/modules.rst.in

diff --git a/docs/_gendocs.py b/docs/_gendocs.py
new file mode 100644
index 0000000..5863dc8
--- /dev/null
+++ b/docs/_gendocs.py
@@ -0,0 +1,40 @@
+"""``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:
+"""
+
+__location__ = Path(__file__).parent
+
+
+def gen_stubs(module_dir: str, output_dir: str):
+    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)
diff --git a/docs/conf.py b/docs/conf.py
index 2d3e846..21322cd 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -8,52 +8,20 @@
 # serve to show the default.
 
 import os
-import shutil
 import sys
 
 # -- Path setup --------------------------------------------------------------
 
 __location__ = os.path.dirname(__file__)
-
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, __location__)
 sys.path.insert(0, os.path.join(__location__, "../src"))
 
-# -- Run sphinx-apidoc -------------------------------------------------------
-# This hack is necessary since RTD does not issue `sphinx-apidoc` before running
-# `sphinx-build -b html . _build/html`. See Issue:
-# https://github.com/readthedocs/readthedocs.org/issues/1139
-# DON'T FORGET: Check the box "Install your project inside a virtualenv using
-# setup.py install" in the RTD Advanced Settings.
-# Additionally it helps us to avoid running apidoc manually
-
-try:  # for Sphinx >= 1.7
-    from sphinx.ext import apidoc
-except ImportError:
-    from sphinx import apidoc
+# -- Dynamically generated docs ----------------------------------------------
+import _gendocs
 
 output_dir = os.path.join(__location__, "api")
 module_dir = os.path.join(__location__, "../src/validate_pyproject")
-try:
-    shutil.rmtree(output_dir)
-except FileNotFoundError:
-    pass
-
-try:
-    import sphinx
-
-    cmd_line = f"sphinx-apidoc --implicit-namespaces -f -o {output_dir} {module_dir}"
-
-    args = cmd_line.split(" ")
-    if tuple(sphinx.__version__.split(".")) >= ("1", "7"):
-        # This is a rudimentary parse_version to avoid external dependencies
-        args = args[1:]
-
-    apidoc.main(args)
-except Exception as e:
-    print("Running `sphinx-apidoc` failed!")
-    print(e)
+_gendocs.gen_stubs(module_dir, output_dir)
 
 # -- General configuration ---------------------------------------------------
 
@@ -323,6 +291,7 @@
     "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),
 }
 extlinks = {
     "issue": (f"{repository}/issues/%s", "issue #%s"),
diff --git a/docs/modules.rst.in b/docs/modules.rst.in
new file mode 100644
index 0000000..7807366
--- /dev/null
+++ b/docs/modules.rst.in
@@ -0,0 +1,17 @@
+Module 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.format`.
+
+.. toctree::
+   :maxdepth: 2
+
+   validate_pyproject.api
+   validate_pyproject.errors
+   validate_pyproject.types
+   validate_pyproject.formats

From 31215ede0fe62820a4eb1b862cec8afc9bbb7c2f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 17:35:37 +0100
Subject: [PATCH 27/61] Avoid re-exporting jsonschema exceptions

---
 src/validate_pyproject/errors.py | 24 +++++++++++++++---------
 1 file changed, 15 insertions(+), 9 deletions(-)

diff --git a/src/validate_pyproject/errors.py b/src/validate_pyproject/errors.py
index e2d1ce2..44050b2 100644
--- a/src/validate_pyproject/errors.py
+++ b/src/validate_pyproject/errors.py
@@ -1,9 +1,18 @@
+"""
+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:`JsonSchemaException`, :obj:`JsonSchemaValueException` and
+:obj:`fastjsonschema.JsonSchemaDefinitionException`.
+)
+"""
+
 from textwrap import dedent
 
 from fastjsonschema import (
-    JsonSchemaDefinitionException,
-    JsonSchemaException,
-    JsonSchemaValueException,
+    JsonSchemaDefinitionException as _JsonSchemaDefinitionException,
 )
 
 from .error_reporting import ValidationError
@@ -24,7 +33,7 @@ def __init__(self, url: str):
         super().__init__(msg)
 
 
-class InvalidSchemaVersion(JsonSchemaDefinitionException):
+class InvalidSchemaVersion(_JsonSchemaDefinitionException):
     _DESC = """\
     All schemas used in the validator should be specified using the same version \
     as the toplevel schema ({version!r}).
@@ -39,7 +48,7 @@ def __init__(self, name: str, given_version: str, required_version: str):
         super().__init__(msg)
 
 
-class SchemaMissingId(JsonSchemaDefinitionException):
+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}.
@@ -51,7 +60,7 @@ def __init__(self, reference: str):
         super().__init__(msg.format(reference=reference))
 
 
-class SchemaWithDuplicatedId(JsonSchemaDefinitionException):
+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.
@@ -65,9 +74,6 @@ def __init__(self, schema_id: str):
 
 __all__ = [
     "InvalidSchemaVersion",
-    "JsonSchemaDefinitionException",
-    "JsonSchemaException",
-    "JsonSchemaValueException",
     "SchemaMissingId",
     "SchemaWithDuplicatedId",
     "ValidationError",

From c9142c082edfac9a2507f125d17ddc934663be41 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 18:38:26 +0100
Subject: [PATCH 28/61] Improve docs for 'validate_pyproject.format'

---
 docs/conf.py                      |  1 +
 src/validate_pyproject/formats.py | 64 ++++++++++++++++++++++++++++++-
 2 files changed, 64 insertions(+), 1 deletion(-)

diff --git a/docs/conf.py b/docs/conf.py
index 21322cd..2a9dcbd 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -292,6 +292,7 @@
     "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"),
diff --git a/src/validate_pyproject/formats.py b/src/validate_pyproject/formats.py
index fbf8364..bb5a891 100644
--- a/src/validate_pyproject/formats.py
+++ b/src/validate_pyproject/formats.py
@@ -1,3 +1,12 @@
+"""
+The functions in this module are used to validate schemas that use 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.
+"""
+
 import builtins
 import logging
 import os
@@ -49,6 +58,9 @@
 
 
 def pep440(version: str) -> bool:
+    """See :ref:`PyPA's version specification `
+    (initially introduced in :pep:`440`).
+    """
     return VERSION_REGEX.match(version) is not None
 
 
@@ -60,6 +72,9 @@ def pep440(version: str) -> bool:
 
 
 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
 
 
@@ -71,6 +86,9 @@ def pep508_identifier(name: str) -> bool:
         from setuptools._vendor.packaging import requirements as _req  # type: ignore
 
     def pep508(value: str) -> bool:
+        """See :ref:`PyPA's dependency specifiers `
+        (initially introduced in :pep:`508`).
+        """
         try:
             _req.Requirement(value)
             return True
@@ -89,7 +107,10 @@ def pep508(value: str) -> bool:
 
 
 def pep508_versionspec(value: str) -> bool:
-    """Expression that can be used to specify/lock versions (including ranges)"""
+    """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
@@ -105,6 +126,11 @@ def pep508_versionspec(value: str) -> bool:
 
 
 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)
@@ -181,6 +207,7 @@ def __call__(self, value: str) -> bool:
     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
@@ -192,6 +219,10 @@ def trove_classifier(value: str) -> bool:
 
 
 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
@@ -203,6 +234,10 @@ def pep561_stub_name(value: str) -> bool:
 
 
 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:
@@ -231,24 +266,40 @@ def url(value: str) -> bool:
 
 
 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_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):
@@ -259,6 +310,13 @@ def python_entrypoint_name(value: str) -> bool:
 
 
 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("[")
@@ -277,16 +335,20 @@ def python_entrypoint_reference(value: str) -> bool:
 
 
 def uint8(value: builtins.int) -> bool:
+    r"""Unsigned 8-bit integer (:math:`0 \leq x < 2^8`)"""
     return 0 <= value < 2**8
 
 
 def uint16(value: builtins.int) -> bool:
+    r"""Unsigned 16-bit integer (:math:`0 \leq x < 2^{16}`)"""
     return 0 <= value < 2**16
 
 
 def uint(value: builtins.int) -> bool:
+    r"""Unsigned 64-bit integer (:math:`0 \leq x < 2^{64}`)"""
     return 0 <= value < 2**64
 
 
 def int(value: builtins.int) -> bool:
+    r"""Signed 64-bit integer (:math:`-2^{63} \leq x < 2^{63}`)"""
     return -(2**63) <= value < 2**63

From 59612be5372d28e25a1645f5bc5ebadc561dca11 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 18:53:51 +0100
Subject: [PATCH 29/61] Small wording improvement

---
 src/validate_pyproject/formats.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/validate_pyproject/formats.py b/src/validate_pyproject/formats.py
index bb5a891..c45bd0c 100644
--- a/src/validate_pyproject/formats.py
+++ b/src/validate_pyproject/formats.py
@@ -1,5 +1,5 @@
 """
-The functions in this module are used to validate schemas that use the
+The functions in this module are used to validate schemas with the
 `format JSON Schema keyword
 `_.
 

From aa0661c7348ea725e782557fd4140665cac939bc Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 18:55:36 +0100
Subject: [PATCH 30/61] Fix cross-reference

---
 docs/modules.rst.in | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/modules.rst.in b/docs/modules.rst.in
index 7807366..17d370f 100644
--- a/docs/modules.rst.in
+++ b/docs/modules.rst.in
@@ -6,7 +6,7 @@ Users may also import :mod:`validate_pyproject.errors` and :mod:`validate_pyproj
 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.format`.
+that can be used in the JSON Schema definitions are implemented in :mod:`validate_pyproject.formats`.
 
 .. toctree::
    :maxdepth: 2

From 5c6fd04070f5f4b0f390d5b9b7e14d28f51d4afd Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 19:16:41 +0100
Subject: [PATCH 31/61] Improve docs for validate_pyproject.api

---
 docs/_gendocs.py              |  1 +
 src/validate_pyproject/api.py | 13 ++++++++++++-
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/docs/_gendocs.py b/docs/_gendocs.py
index 5863dc8..bea9834 100644
--- a/docs/_gendocs.py
+++ b/docs/_gendocs.py
@@ -13,6 +13,7 @@
    :members:{_members}
    :undoc-members:
    :show-inheritance:
+   :special-members: __call__
 """
 
 __location__ = Path(__file__).parent
diff --git a/src/validate_pyproject/api.py b/src/validate_pyproject/api.py
index b77fe12..2c88ef2 100644
--- a/src/validate_pyproject/api.py
+++ b/src/validate_pyproject/api.py
@@ -41,6 +41,7 @@
         from importlib.resources import files
 
     def read_text(package: Union[str, ModuleType], resource: str) -> str:
+        """:meta private:"""
         return files(package).joinpath(resource).read_text(encoding="utf-8")  # type: ignore[no-any-return]
 
 except ImportError:  # pragma: no cover
@@ -48,7 +49,7 @@ def read_text(package: Union[str, ModuleType], resource: str) -> str:
 
 
 T = TypeVar("T", bound=Mapping)
-AllPlugins = Enum("AllPlugins", "ALL_PLUGINS")
+AllPlugins = Enum("AllPlugins", "ALL_PLUGINS")  #: :meta private:
 ALL_PLUGINS = AllPlugins.ALL_PLUGINS
 
 TOP_LEVEL_SCHEMA = "pyproject_toml"
@@ -69,11 +70,14 @@ def _get_public_functions(module: ModuleType) -> Mapping[str, FormatValidationFn
 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")
 
 
@@ -86,6 +90,8 @@ class SchemaRegistry(Mapping[str, Schema]):
 
     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"] = ()):
@@ -159,6 +165,8 @@ class RefHandler(Mapping[str, Callable[[str], Schema]]):
     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]):
@@ -244,6 +252,9 @@ def __getitem__(self, schema_id: str) -> Schema:
         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))
             fn = partial(compiled, custom_formats=self._format_validators)

From 45621b19b57bd441ccd517be9ac04ea552869c80 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 19:28:10 +0100
Subject: [PATCH 32/61] Add link to formats to debug info

---
 src/validate_pyproject/error_reporting.py | 9 ++++++++-
 tests/test_error_reporting.py             | 2 ++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/validate_pyproject/error_reporting.py b/src/validate_pyproject/error_reporting.py
index b2f883d..b8dca35 100644
--- a/src/validate_pyproject/error_reporting.py
+++ b/src/validate_pyproject/error_reporting.py
@@ -45,6 +45,11 @@
     "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.
@@ -160,7 +165,9 @@ def _expand_details(self) -> str:
             f"OFFENDING RULE: {self.ex.rule!r}",
             f"DEFINITION:\n{indent(schema, '    ')}",
         ]
-        return "\n\n".join(optional + defaults)
+        msg = "\n\n".join(optional + defaults)
+        epilog = f"\n{_FORMATS_HELP}" if "format" in msg.lower() else ""
+        return msg + epilog
 
 
 class _SummaryWriter:
diff --git a/tests/test_error_reporting.py b/tests/test_error_reporting.py
index e0cc4a9..ccf6a3e 100644
--- a/tests/test_error_reporting.py
+++ b/tests/test_error_reporting.py
@@ -62,6 +62,8 @@
                         }
                     ]
                 }
+
+            For more details about `format` see
         """,
     },
     "description": {

From 10d184f5d9e23bb4bbade9c07cce2e55d1763760 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 19:34:23 +0100
Subject: [PATCH 33/61] Add entry to FAQ

---
 docs/faq.rst | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/docs/faq.rst b/docs/faq.rst
index 607257d..875729d 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -61,6 +61,12 @@ The text on the standard is:
 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/

From a62686092a08dea6f10bc4e2eb818b1e79def695 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 19:35:03 +0100
Subject: [PATCH 34/61] Fix title hierarchy in CONTRIBUTING

---
 CONTRIBUTING.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index ad62378..8c85d74 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -77,7 +77,7 @@ 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

From 8b3977bc0a236457d32696b55ac720c77abc1e71 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 21 Apr 2024 19:39:30 +0100
Subject: [PATCH 35/61] Use ValidationErrro for example in docs

---
 docs/embedding.rst | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/docs/embedding.rst b/docs/embedding.rst
index d89301a..dca619d 100644
--- a/docs/embedding.rst
+++ b/docs/embedding.rst
@@ -27,25 +27,25 @@ via CLI as indicated in the command below:
 
     # in you terminal
     $ python -m validate_pyproject.pre_compile --help
-    $ python -m validate_pyproject.pre_compile -O dir/for/genereated_files
+    $ 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 ``genereated_files`` directory, and that the value
+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 .genereated_files import validate, JsonSchemaValueException
+    from .generated_files import validate, ValidationError
 
     try:
         validate(dict_representing_the_parsed_toml_file)
-    except JsonSchemaValueException:
+    except ValidationError:
         print("Invalid File")
 
 

From 0d28c9a46d778377759169f53e02bdf4dcc5f66a Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 09:19:03 +0100
Subject: [PATCH 36/61] Fix references in docs

---
 src/validate_pyproject/errors.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/validate_pyproject/errors.py b/src/validate_pyproject/errors.py
index 44050b2..4f29011 100644
--- a/src/validate_pyproject/errors.py
+++ b/src/validate_pyproject/errors.py
@@ -4,7 +4,8 @@
 
 Note that ``validate-pyproject`` derives most of its exceptions from
 :mod:`fastjsonschema`, so it might make sense to also have a look on
-:obj:`JsonSchemaException`, :obj:`JsonSchemaValueException` and
+:obj:`fastjsonschema.JsonSchemaException`,
+:obj:`fastjsonschema.JsonSchemaValueException` and
 :obj:`fastjsonschema.JsonSchemaDefinitionException`.
 )
 """

From c0611b7823cb040c96b7c6aaa4765b770b087403 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 09:35:14 +0100
Subject: [PATCH 37/61] Fix syntax in docs

---
 src/validate_pyproject/formats.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/validate_pyproject/formats.py b/src/validate_pyproject/formats.py
index c45bd0c..5a0599c 100644
--- a/src/validate_pyproject/formats.py
+++ b/src/validate_pyproject/formats.py
@@ -310,9 +310,9 @@ def python_entrypoint_name(value: str) -> bool:
 
 
 def python_entrypoint_reference(value: str) -> bool:
-    """Reference to a Python object using in the format:
+    """Reference to a Python object using in the format::
 
-    > ``importable.module:object.attr``
+        importable.module:object.attr
 
     See ``Data model >object reference`` in the :ref:`PyPA's entry-points specification
     `.

From 03f6f178888015ac8c72e0f5a2ac3434f2fae2e6 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 22 Apr 2024 09:43:24 +0100
Subject: [PATCH 38/61] Use 'extra_plugins' instead of 'plugins' in
 documentation example

---
 docs/dev-guide.rst | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/docs/dev-guide.rst b/docs/dev-guide.rst
index cf69db5..8591987 100644
--- a/docs/dev-guide.rst
+++ b/docs/dev-guide.rst
@@ -55,14 +55,14 @@ 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 a ``plugins`` argument to the
+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, plugins
+    from validate_pyproject import api
 
 
     def your_plugin(tool_name: str) -> dict:
@@ -77,14 +77,18 @@ specify which ``tool`` subtable it would be checking:
 
 
     available_plugins = [
-        *plugins.list_from_entry_points(),
         plugins.PluginWrapper("your-tool", your_plugin),
     ]
-    validator = api.Validator(available_plugins)
+    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
 --------------------

From ef05f9da314d743e987d603b2bad0677749c7d9e Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 22 Apr 2024 16:37:24 +0000
Subject: [PATCH 39/61] [pre-commit.ci] pre-commit autoupdate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.7 → v0.4.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.7...v0.4.1)
---
 .pre-commit-config.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ec5a7ee..2077693 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -26,7 +26,7 @@ repos:
     args: [-w]
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.3.7  # Ruff version
+  rev: v0.4.1  # Ruff version
   hooks:
   - id: ruff
     args: [--fix, --show-fixes]

From e6ab23fc5c36c8397ffe170f3041d919d0a7340c Mon Sep 17 00:00:00 2001
From: Henry Schreiner 
Date: Tue, 23 Apr 2024 23:27:47 -0400
Subject: [PATCH 40/61] ci: macos-latest now points at macos-14

---
 .github/workflows/ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8839914..0304b23 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -66,7 +66,7 @@ jobs:
         - "3.11"  # newest Python that is stable
         platform:
         - ubuntu-latest
-        - macos-latest
+        - macos-13
         - windows-latest
     runs-on: ${{ matrix.platform }}
     steps:

From beb539f31b7dbf6bfe0dc2e4dd3d6ef45247fdb0 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 7 May 2024 13:51:38 +0100
Subject: [PATCH 41/61] Add temporary workaround for coveralls

---
 .cirrus.yml | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/.cirrus.yml b/.cirrus.yml
index 9b67ab8..57c953b 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -52,7 +52,8 @@ env:
     -n 5 --randomly-seed=42 -rfEx --durations 10 --color yes
   submit_coverage_script:
     - pipx run coverage xml -o coverage.xml
-    - pipx run coveralls --submit 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 |
@@ -159,7 +160,8 @@ finalize_task:
   container: {image: "python:3.10-bullseye"}
   depends_on: [test]
   <<: *task-template
-  install_script: pip install coveralls
+  install_script: pip install 'coveralls<4'
+    # ^-- https://github.com/TheKevJames/coveralls-python/issues/434
   finalize_coverage_script: coveralls --finish
 
 linkcheck_task:

From 822845504007634940b4af7c64cadd4950c8608c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 24 Apr 2024 08:51:21 +0100
Subject: [PATCH 42/61] Bump min Python version

---
 .cirrus.yml              | 9 ++-------
 .github/workflows/ci.yml | 4 ++--
 setup.cfg                | 2 +-
 3 files changed, 5 insertions(+), 10 deletions(-)

diff --git a/.cirrus.yml b/.cirrus.yml
index 57c953b..9c9550b 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -82,10 +82,10 @@ build_task:
   build_script: tox -e clean,build
 
 check_task:
-  name: check (Linux - 3.10)
+  name: check (Linux - 3.12)
   alias: check
   depends_on: [build]
-  container: {image: "python:3.10-bullseye"}  # most recent => better types
+  container: {image: "python:3.12-bullseye"}  # most recent => better types
   dist_cache: {folder: dist, fingerprint_script: echo $CIRRUS_BUILD_ID}  # download
   <<: *task-template
   install_script: pip install tox
@@ -93,11 +93,6 @@ check_task:
 
 linux_task:
   matrix:
-    - name: test (Linux - 3.6)
-      container: {image: "python:3.6-bullseye"}
-      allow_failures: true  # EoL
-    - name: test (Linux - 3.7)
-      container: {image: "python:3.7-bullseye"}
     - name: test (Linux - 3.8)
       container: {image: "python:3.8-bullseye"}
     - name: test (Linux - 3.9)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0304b23..2ad5a91 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -62,8 +62,8 @@ jobs:
     strategy:
       matrix:
         python:
-        - "3.7"  # oldest Python supported by PSF
-        - "3.11"  # newest Python that is stable
+        - "3.8"  # oldest Python supported by PSF
+        - "3.12"  # newest Python that is stable
         platform:
         - ubuntu-latest
         - macos-13
diff --git a/setup.cfg b/setup.cfg
index ad38f9b..ede5670 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -47,7 +47,7 @@ package_dir =
     =src
 
 # Require a min/specific Python version (comma-separated conditions)
-python_requires = >=3.6
+python_requires = >=3.8
 
 # Add here dependencies of your project (line-separated), e.g. requests>=2.2,<3.0.
 # Version specifiers like >=2.2,<3.0 avoid problems due to API changes in

From 26fbf024a435fd092250fb08e0b39fed3e75241b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 24 Apr 2024 09:01:50 +0100
Subject: [PATCH 43/61] Remove conditional for Python < 3.8

---
 src/validate_pyproject/plugins/__init__.py | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/src/validate_pyproject/plugins/__init__.py b/src/validate_pyproject/plugins/__init__.py
index 16393eb..f4a5a76 100644
--- a/src/validate_pyproject/plugins/__init__.py
+++ b/src/validate_pyproject/plugins/__init__.py
@@ -20,12 +20,9 @@
     from importlib_metadata import EntryPoint, entry_points
 
 if typing.TYPE_CHECKING:
-    from ..types import Plugin, Schema
+    from typing import Protocol
 
-    if sys.version_info < (3, 8):
-        from typing_extensions import Protocol
-    else:
-        from typing import Protocol
+    from ..types import Plugin, Schema
 else:
     Protocol = object
 

From e97f82952f1584e72dd3d6ffccc241fbac7e47d7 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 24 Apr 2024 09:06:29 +0100
Subject: [PATCH 44/61] Revert bump for type check

---
 .cirrus.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.cirrus.yml b/.cirrus.yml
index 9c9550b..d1f1ce9 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -82,10 +82,10 @@ build_task:
   build_script: tox -e clean,build
 
 check_task:
-  name: check (Linux - 3.12)
+  name: check (Linux - 3.10)
   alias: check
   depends_on: [build]
-  container: {image: "python:3.12-bullseye"}  # most recent => better types
+  container: {image: "python:3.10-bullseye"}  # 3.11+ don't understand some code paths
   dist_cache: {folder: dist, fingerprint_script: echo $CIRRUS_BUILD_ID}  # download
   <<: *task-template
   install_script: pip install tox

From a073a02333ebf118a6a846b5e9fe0562b8f2c103 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 24 Apr 2024 09:19:58 +0100
Subject: [PATCH 45/61] Attempt to run the workflow, when the workflow file
 changes

---
 .github/workflows/ci.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2ad5a91..5c0ffb1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,7 +6,8 @@ on:
     # 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:  # Run in every PR
+  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

From 622394338378acdbe205d75029892f92c066651b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 24 Apr 2024 09:27:12 +0100
Subject: [PATCH 46/61] Remove rc from the python3.12 container in cirrus

---
 .cirrus.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.cirrus.yml b/.cirrus.yml
index d1f1ce9..8dcda21 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -102,7 +102,7 @@ linux_task:
     - name: test (Linux - 3.11)
       container: {image: "python:3.11-bullseye"}
     - name: test (Linux - 3.12)
-      container: {image: "python:3.12-rc-bullseye"}
+      container: {image: "python:3.12-bullseye"}
       allow_failures: true  # Experimental
   install_script:
     - python -m pip install --upgrade pip tox pipx

From 33733311f59aba2975d610c8febb1c0114c38e3b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 7 May 2024 12:44:33 +0100
Subject: [PATCH 47/61] Remove unecessary typing fallbacks

---
 setup.cfg                                     |  2 --
 src/validate_pyproject/__init__.py            |  8 +------
 src/validate_pyproject/api.py                 |  6 +----
 src/validate_pyproject/plugins/__init__.py    | 24 +++++--------------
 .../pre_compile/__init__.py                   |  7 +-----
 tests/test_plugins.py                         | 10 +-------
 6 files changed, 10 insertions(+), 47 deletions(-)

diff --git a/setup.cfg b/setup.cfg
index ede5670..2863019 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -54,8 +54,6 @@ python_requires = >=3.8
 # new major versions. This works if the required packages follow Semantic Versioning.
 # For more information, check out https://semver.org/.
 install_requires =
-    importlib-metadata; python_version<"3.8"
-    importlib-resources; python_version<"3.7"
     fastjsonschema>=2.16.2,<=3
 
 
diff --git a/src/validate_pyproject/__init__.py b/src/validate_pyproject/__init__.py
index fbaff2e..be9fa4e 100644
--- a/src/validate_pyproject/__init__.py
+++ b/src/validate_pyproject/__init__.py
@@ -1,10 +1,4 @@
-import sys
-
-if sys.version_info[:2] >= (3, 8):
-    # TODO: Import directly (no need for conditional) when `python_requires = >= 3.8`
-    from importlib.metadata import PackageNotFoundError, version  # pragma: no cover
-else:
-    from importlib_metadata import PackageNotFoundError, version  # pragma: no cover
+from importlib.metadata import PackageNotFoundError, version  # pragma: no cover
 
 try:
     # Change here if project is renamed and does not equal the package name
diff --git a/src/validate_pyproject/api.py b/src/validate_pyproject/api.py
index 2c88ef2..f25e035 100644
--- a/src/validate_pyproject/api.py
+++ b/src/validate_pyproject/api.py
@@ -4,7 +4,6 @@
 
 import json
 import logging
-import sys
 import typing
 from enum import Enum
 from functools import partial, reduce
@@ -35,10 +34,7 @@
 
 
 try:  # pragma: no cover
-    if sys.version_info[:2] < (3, 7) or typing.TYPE_CHECKING:  # See #22
-        from importlib_resources import files
-    else:
-        from importlib.resources import files
+    from importlib.resources import files
 
     def read_text(package: Union[str, ModuleType], resource: str) -> str:
         """:meta private:"""
diff --git a/src/validate_pyproject/plugins/__init__.py b/src/validate_pyproject/plugins/__init__.py
index f4a5a76..f39175d 100644
--- a/src/validate_pyproject/plugins/__init__.py
+++ b/src/validate_pyproject/plugins/__init__.py
@@ -5,26 +5,14 @@
 .. _entry point: https://setuptools.readthedocs.io/en/latest/userguide/entry_point.html
 """
 
-import sys
 import typing
+from importlib.metadata import EntryPoint, entry_points
 from string import Template
 from textwrap import dedent
-from typing import Any, Callable, Iterable, List, Optional
+from typing import Any, Callable, Iterable, List, Optional, Protocol
 
 from .. import __version__
-
-if sys.version_info[:2] >= (3, 8):  # pragma: no cover
-    # TODO: Import directly (no need for conditional) when `python_requires = >= 3.8`
-    from importlib.metadata import EntryPoint, entry_points
-else:  # pragma: no cover
-    from importlib_metadata import EntryPoint, entry_points
-
-if typing.TYPE_CHECKING:
-    from typing import Protocol
-
-    from ..types import Plugin, Schema
-else:
-    Protocol = object
+from ..types import Plugin, Schema
 
 ENTRYPOINT_GROUP = "validate_pyproject.tool_schema"
 
@@ -37,7 +25,7 @@ def id(self) -> str: ...
     def tool(self) -> str: ...
 
     @property
-    def schema(self) -> "Schema": ...
+    def schema(self) -> Schema: ...
 
     @property
     def help_text(self) -> str: ...
@@ -47,7 +35,7 @@ def fragment(self) -> str: ...
 
 
 class PluginWrapper:
-    def __init__(self, tool: str, load_fn: "Plugin"):
+    def __init__(self, tool: str, load_fn: Plugin):
         self._tool = tool
         self._load_fn = load_fn
 
@@ -60,7 +48,7 @@ def tool(self) -> str:
         return self._tool
 
     @property
-    def schema(self) -> "Schema":
+    def schema(self) -> Schema:
         return self._load_fn(self.tool)
 
     @property
diff --git a/src/validate_pyproject/pre_compile/__init__.py b/src/validate_pyproject/pre_compile/__init__.py
index 9cde18e..efa6d00 100644
--- a/src/validate_pyproject/pre_compile/__init__.py
+++ b/src/validate_pyproject/pre_compile/__init__.py
@@ -1,6 +1,6 @@
 import logging
 import os
-import sys
+from importlib import metadata as _M
 from pathlib import Path
 from types import MappingProxyType
 from typing import TYPE_CHECKING, Dict, Mapping, Optional, Sequence, Union
@@ -9,11 +9,6 @@
 
 from .. import api, dist_name, types
 
-if sys.version_info[:2] >= (3, 8):  # pragma: no cover
-    from importlib import metadata as _M
-else:  # pragma: no cover
-    import importlib_metadata as _M
-
 if TYPE_CHECKING:  # pragma: no cover
     from ..plugins import PluginProtocol
 
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index a5e8d01..1f07dec 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -1,8 +1,7 @@
 # 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'
-
-import sys
+from importlib.metadata import EntryPoint  # pragma: no cover
 
 import pytest
 
@@ -15,13 +14,6 @@
 )
 
 
-if sys.version_info[:2] >= (3, 8):
-    # TODO: Import directly (no need for conditional) when `python_requires = >= 3.8`
-    from importlib.metadata import EntryPoint  # pragma: no cover
-else:
-    from importlib_metadata import EntryPoint  # pragma: no cover
-
-
 def test_load_from_entry_point__error():
     # This module does not exist, so Python will have some trouble loading it
     # EntryPoint(name, value, group)

From a1f41c93980ace0d47f6971826dbf6764c9f73fa Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 6 May 2024 16:38:19 +0000
Subject: [PATCH 48/61] [pre-commit.ci] pre-commit autoupdate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.1 → v0.4.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.1...v0.4.3)
---
 .pre-commit-config.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2077693..aaa98bd 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -26,7 +26,7 @@ repos:
     args: [-w]
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.4.1  # Ruff version
+  rev: v0.4.3  # Ruff version
   hooks:
   - id: ruff
     args: [--fix, --show-fixes]

From 3efd7d1f275c259c44ec3f47b13d9e4ead14e6e1 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 7 May 2024 14:06:10 +0100
Subject: [PATCH 49/61] Remove unused type comment

---
 src/validate_pyproject/api.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/validate_pyproject/api.py b/src/validate_pyproject/api.py
index f25e035..887eaf7 100644
--- a/src/validate_pyproject/api.py
+++ b/src/validate_pyproject/api.py
@@ -38,7 +38,7 @@
 
     def read_text(package: Union[str, ModuleType], resource: str) -> str:
         """:meta private:"""
-        return files(package).joinpath(resource).read_text(encoding="utf-8")  # type: ignore[no-any-return]
+        return files(package).joinpath(resource).read_text(encoding="utf-8")
 
 except ImportError:  # pragma: no cover
     from importlib.resources import read_text

From ba8dd19caa5dc34e777ffb9ce74c4f776fc3d2d9 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 7 May 2024 14:35:41 +0100
Subject: [PATCH 50/61] Add example capturing expectation for
 tool.setuptools.dynamic.optional-dependencies

---
 tests/examples/setuptools/10-pyproject.toml | 7 +++++++
 1 file changed, 7 insertions(+)
 create mode 100644 tests/examples/setuptools/10-pyproject.toml

diff --git a/tests/examples/setuptools/10-pyproject.toml b/tests/examples/setuptools/10-pyproject.toml
new file mode 100644
index 0000000..3c9c393
--- /dev/null
+++ b/tests/examples/setuptools/10-pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "myproj"
+version = "42"
+dynamic = ["optional-dependencies"]
+
+[tool.setuptools.dynamic.optional-dependencies]
+name-with-hyfens = {file = "extra.txt"}

From b1d99d8c06fef2b047a2145247891703bd7a64b2 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 7 May 2024 14:30:36 +0100
Subject: [PATCH 51/61] Align tool.setuptools.dynamic.optional-dependencies
 with project.optional-dependencies

---
 src/validate_pyproject/plugins/setuptools.schema.json           | 2 +-
 .../setuptools/dependencies/invalid-extra-name.errors.txt       | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/validate_pyproject/plugins/setuptools.schema.json b/src/validate_pyproject/plugins/setuptools.schema.json
index 2b029b6..adc203d 100644
--- a/src/validate_pyproject/plugins/setuptools.schema.json
+++ b/src/validate_pyproject/plugins/setuptools.schema.json
@@ -219,7 +219,7 @@
         "dependencies": {"$ref": "#/definitions/file-directive-for-dependencies"},
         "optional-dependencies": {
           "type": "object",
-          "propertyNames": {"type": "string", "format": "python-identifier"},
+          "propertyNames": {"type": "string", "format": "pep508-identifier"},
           "additionalProperties": false,
           "patternProperties": {
             ".+": {"$ref": "#/definitions/file-directive-for-dependencies"}
diff --git a/tests/invalid-examples/setuptools/dependencies/invalid-extra-name.errors.txt b/tests/invalid-examples/setuptools/dependencies/invalid-extra-name.errors.txt
index 8f03afc..8abee19 100644
--- a/tests/invalid-examples/setuptools/dependencies/invalid-extra-name.errors.txt
+++ b/tests/invalid-examples/setuptools/dependencies/invalid-extra-name.errors.txt
@@ -1,3 +1,3 @@
 `tool.setuptools.dynamic.optional-dependencies` keys must be named by:
 
-    {type: string, format: 'python-identifier'}
+    {type: string, format: 'pep508-identifier'}

From 16c45f61e1abd71f619c0fe1a3546e8b0baf09d3 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 7 May 2024 16:33:59 +0100
Subject: [PATCH 52/61] Update ruff target

---
 .ruff.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.ruff.toml b/.ruff.toml
index e2fce3b..a36942a 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -1,6 +1,6 @@
 # --- General config ---
 src = ["src"]
-target-version = "py37"
+target-version = "py38"
 
 # --- Linting config ---
 [lint]

From 3a5a8ee83606d74207564a412a65531881a072dc Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 7 May 2024 16:35:25 +0100
Subject: [PATCH 53/61] Further updates from ruff

---
 src/validate_pyproject/pre_compile/cli.py | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/src/validate_pyproject/pre_compile/cli.py b/src/validate_pyproject/pre_compile/cli.py
index 6694b7b..985ba74 100644
--- a/src/validate_pyproject/pre_compile/cli.py
+++ b/src/validate_pyproject/pre_compile/cli.py
@@ -17,13 +17,8 @@
 
 if sys.platform == "win32":  # pragma: no cover
     from subprocess import list2cmdline as arg_join
-elif sys.version_info[:2] >= (3, 8):  # pragma: no cover
-    from shlex import join as arg_join
 else:  # pragma: no cover
-    from shlex import quote
-
-    def arg_join(args: Sequence[str]) -> str:
-        return " ".join(quote(x) for x in args)
+    from shlex import join as arg_join
 
 
 _logger = logging.getLogger(__package__)

From a0904167e7fccdbabf55861365e906f8e97aac50 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 7 May 2024 17:19:25 +0100
Subject: [PATCH 54/61] Update CHANGELOG

---
 CHANGELOG.rst | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 8db34bf..61cee46 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,14 @@ Changelog
    Development Version
    ====================
 
+Version 0.16
+============
+- 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

From a069613a0a55c34ab6e84acf1aab5a939074ac1f Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
 <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 13 May 2024 16:38:14 +0000
Subject: [PATCH 55/61] [pre-commit.ci] pre-commit autoupdate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.3 → v0.4.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.3...v0.4.4)
- [github.com/python-jsonschema/check-jsonschema: 0.28.2 → 0.28.3](https://github.com/python-jsonschema/check-jsonschema/compare/0.28.2...0.28.3)
---
 .pre-commit-config.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index aaa98bd..8809e91 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -26,7 +26,7 @@ repos:
     args: [-w]
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.4.3  # Ruff version
+  rev: v0.4.4  # Ruff version
   hooks:
   - id: ruff
     args: [--fix, --show-fixes]
@@ -56,7 +56,7 @@ repos:
       - validate-pyproject[all]>=0.13
 
 - repo: https://github.com/python-jsonschema/check-jsonschema
-  rev: 0.28.2
+  rev: 0.28.3
   hooks:
     - id: check-metaschema
       files: \.schema\.json$

From 020a3429cbb2f4547ad6d29d1288973ab03b44ad Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 17 May 2024 12:41:08 +0100
Subject: [PATCH 56/61] Test expectation of allowing tools to be overwritten

---
 tests/test_api.py | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/tests/test_api.py b/tests/test_api.py
index 87f1a31..0e2f1cb 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -43,7 +43,7 @@ def test_with_plugins(self):
 
     def fake_plugin(self, name, schema_version=7, end="#"):
         schema = {
-            "$id": f"https://example.com/{name}.schema.json",
+            "$id": id or f"https://example.com/{name}.schema.json",
             "$schema": f"http://json-schema.org/draft-{schema_version:02d}/schema{end}",
             "type": "object",
         }
@@ -63,11 +63,19 @@ def test_incompatible_versions(self):
         with pytest.raises(errors.InvalidSchemaVersion):
             api.SchemaRegistry([plg])
 
-    def test_duplicated_id(self):
-        plg = [plugins.PluginWrapper("plg", self.fake_plugin) for _ in range(2)]
+    def test_duplicated_id_different_tools(self):
+        schema = self.fake_plugin("plg")
+        fn = wraps(self.fake_plugin)(lambda *_1, **_2: 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))

From 82ec7fab12267921346e5cbf19c29a1992a31a0b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 17 May 2024 13:05:56 +0100
Subject: [PATCH 57/61] Allow overwrite for the same tool

---
 src/validate_pyproject/api.py | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/src/validate_pyproject/api.py b/src/validate_pyproject/api.py
index 887eaf7..4dd7e8e 100644
--- a/src/validate_pyproject/api.py
+++ b/src/validate_pyproject/api.py
@@ -109,11 +109,16 @@ def __init__(self, plugins: Sequence["PluginProtocol"] = ()):
 
         # Add tools using Plugins
         for plugin in plugins:
+            allow_overwrite: Optional[str] = None
             if plugin.tool in tool_properties:
                 _logger.warning(f"{plugin.id} overwrites `tool.{plugin.tool}` schema")
+                allow_overwrite = plugin.schema.get("$id")
             else:
                 _logger.info(f"{plugin.id} defines `tool.{plugin.tool}` schema")
-            sid = self._ensure_compatibility(plugin.tool, plugin.schema)["$id"]
+            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)
@@ -133,11 +138,13 @@ 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) -> Schema:
-        if "$id" not in schema:
+    def _ensure_compatibility(
+        self, reference: str, schema: Schema, allow_overwrite: Optional[str] = None
+    ) -> Schema:
+        if "$id" not in schema or not schema["$id"]:
             raise errors.SchemaMissingId(reference)
         sid = schema["$id"]
-        if sid in self._schemas:
+        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)

From 64c36c93253ae86ae37c7002f842672c3056078e Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 17 May 2024 13:18:03 +0100
Subject: [PATCH 58/61] Improve reproducibility of sorting

---
 src/validate_pyproject/plugins/__init__.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/validate_pyproject/plugins/__init__.py b/src/validate_pyproject/plugins/__init__.py
index f39175d..19ca2c1 100644
--- a/src/validate_pyproject/plugins/__init__.py
+++ b/src/validate_pyproject/plugins/__init__.py
@@ -90,7 +90,9 @@ def iterate_entry_points(group: str = ENTRYPOINT_GROUP) -> Iterable[EntryPoint]:
         # TODO: Once Python 3.10 becomes the oldest version supported, this fallback and
         #       conditional statement can be removed.
         entries_ = (plugin for plugin in entries.get(group, []))
-    deduplicated = {e.name: e for e in sorted(entries_, key=lambda e: e.name)}
+    deduplicated = {
+        e.name: e for e in sorted(entries_, key=lambda e: (e.name, e.value))
+    }
     return list(deduplicated.values())
 
 

From c7ac8caa038f43a06422abeecf5582ff15b77fdd Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 17 May 2024 13:21:20 +0100
Subject: [PATCH 59/61] Fix broken fixture

---
 tests/test_api.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/test_api.py b/tests/test_api.py
index 0e2f1cb..500e067 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -43,7 +43,7 @@ def test_with_plugins(self):
 
     def fake_plugin(self, name, schema_version=7, end="#"):
         schema = {
-            "$id": id or f"https://example.com/{name}.schema.json",
+            "$id": f"https://example.com/{name}.schema.json",
             "$schema": f"http://json-schema.org/draft-{schema_version:02d}/schema{end}",
             "type": "object",
         }

From b1a973c90e988979b524826f3e21b59d97aa24b1 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 17 May 2024 16:42:56 +0100
Subject: [PATCH 60/61] Simplify lambda used in tests

---
 tests/test_api.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/test_api.py b/tests/test_api.py
index 500e067..2a49ad4 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -65,7 +65,7 @@ def test_incompatible_versions(self):
 
     def test_duplicated_id_different_tools(self):
         schema = self.fake_plugin("plg")
-        fn = wraps(self.fake_plugin)(lambda *_1, **_2: schema)  # Same ID
+        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)

From bea368871c59605bf2471441d0c6214bd3b80c44 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 20 May 2024 10:13:08 +0100
Subject: [PATCH 61/61] Update CHANGELOG

---
 CHANGELOG.rst | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 61cee46..f5ab4b2 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,10 @@ Changelog
    Development Version
    ====================
 
+Version 0.16
+============
+- Allow overwriting schemas referring to the same ``tool``, #175.
+
 Version 0.16
 ============
 - Update version regex according to latest packaging version, #153