diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a45d1810..78b03cce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,11 +20,11 @@ jobs: outputs: noxenvs: ${{ steps.noxenvs-matrix.outputs.noxenvs }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false - name: Set up uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 with: enable-cache: ${{ github.ref_type != 'tag' }} # zizmor: ignore[cache-poisoning] - id: noxenvs-matrix @@ -75,7 +75,7 @@ jobs: noxenv: "docs(style)" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false - name: Install dependencies @@ -85,7 +85,7 @@ jobs: run: brew install enchant if: runner.os == 'macOS' && startsWith(matrix.noxenv, 'docs') - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: | 3.9 @@ -100,7 +100,7 @@ jobs: if: runner.os == 'Windows' && startsWith(matrix.noxenv, 'tests') - name: Set up uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 with: enable-cache: true @@ -119,12 +119,12 @@ jobs: id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 persist-credentials: false - name: Set up uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 with: enable-cache: true @@ -133,10 +133,10 @@ jobs: - name: Publish to PyPI if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e - name: Create a GitHub Release if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') - uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 + uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 with: files: | dist/* diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 17f2bd11..121c14c1 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -15,12 +15,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 - name: Run zizmor 🌈 run: uvx zizmor --format=sarif .github > results.sarif diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 77b36341..5eae75bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: json/ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-ast - id: check-json @@ -16,7 +16,7 @@ repos: args: [--fix, lf] - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.12.3" + rev: "v0.12.12" hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ed0fa3b6..836fdcdc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,14 @@ +v4.25.1 +======= + +* Fix an incorrect required argument in the ``Validator`` protocol's type annotations (#1396). + +v4.25.0 +======= + +* Add support for the ``iri`` and ``iri-reference`` formats to the ``format-nongpl`` extra via the MIT-licensed ``rfc3987-syntax``. + They were alread supported by the ``format`` extra. (#1388). + v4.24.1 ======= diff --git a/docs/validate.rst b/docs/validate.rst index 91f0577b..bc740e34 100644 --- a/docs/validate.rst +++ b/docs/validate.rst @@ -206,8 +206,6 @@ Or if you want to avoid GPL dependencies, a second extra is available: $ pip install jsonschema[format-nongpl] -At the moment, it supports all the available checkers except for ``iri`` and ``iri-reference``. - .. warning:: It is your own responsibility ultimately to ensure you are license-compliant, so you should be double checking your own dependencies if you rely on this extra. @@ -230,8 +228,8 @@ Checker Notes ``idn-hostname`` requires idna_ ``ipv4`` ``ipv6`` OS must have `socket.inet_pton` function -``iri`` requires rfc3987_ -``iri-reference`` requires rfc3987_ +``iri`` requires rfc3987_ or rfc3987-syntax_ +``iri-reference`` requires rfc3987_ or rfc3987-syntax_ ``json-pointer`` requires jsonpointer_ ``regex`` ``relative-json-pointer`` requires jsonpointer_ @@ -249,6 +247,7 @@ Checker Notes .. _rfc3339-validator: https://pypi.org/project/rfc3339-validator/ .. _rfc3986-validator: https://pypi.org/project/rfc3986-validator/ .. _rfc3987: https://pypi.org/pypi/rfc3987/ +.. _rfc3987-syntax: https://pypi.org/pypi/rfc3987-syntax/ .. _uri-template: https://pypi.org/pypi/uri-template/ .. _webcolors: https://pypi.org/pypi/webcolors/ diff --git a/jsonschema/_format.py b/jsonschema/_format.py index 9b4e67b6..6fc7a01e 100644 --- a/jsonschema/_format.py +++ b/jsonschema/_format.py @@ -324,6 +324,31 @@ def is_uri_reference(instance: object) -> bool: return True return validate_rfc3986(instance, rule="URI_reference") + with suppress(ImportError): + from rfc3987_syntax import is_valid_syntax as _rfc3987_is_valid_syntax + + @_checks_drafts( + draft7="iri", + draft201909="iri", + draft202012="iri", + raises=ValueError, + ) + def is_iri(instance: object) -> bool: + if not isinstance(instance, str): + return True + return _rfc3987_is_valid_syntax("iri", instance) + + @_checks_drafts( + draft7="iri-reference", + draft201909="iri-reference", + draft202012="iri-reference", + raises=ValueError, + ) + def is_iri_reference(instance: object) -> bool: + if not isinstance(instance, str): + return True + return _rfc3987_is_valid_syntax("iri_reference", instance) + else: @_checks_drafts( diff --git a/jsonschema/protocols.py b/jsonschema/protocols.py index 0fd993ee..b6288dcc 100644 --- a/jsonschema/protocols.py +++ b/jsonschema/protocols.py @@ -108,10 +108,11 @@ class Validator(Protocol): def __init__( self, schema: Mapping | bool, - registry: referencing.jsonschema.SchemaRegistry, + resolver: Any = None, # deprecated format_checker: jsonschema.FormatChecker | None = None, - ) -> None: - ... + *, + registry: referencing.jsonschema.SchemaRegistry = ..., + ) -> None: ... @classmethod def check_schema(cls, schema: Mapping | bool) -> None: diff --git a/jsonschema/tests/typing/__init__.py b/jsonschema/tests/typing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/jsonschema/tests/typing/test_all_concrete_validators_match_protocol.py b/jsonschema/tests/typing/test_all_concrete_validators_match_protocol.py new file mode 100644 index 00000000..63e8bd40 --- /dev/null +++ b/jsonschema/tests/typing/test_all_concrete_validators_match_protocol.py @@ -0,0 +1,38 @@ +""" +This module acts as a test that type checkers will allow each validator +class to be assigned to a variable of type `type[Validator]` + +The assignation is only valid if type checkers recognize each Validator +implementation as a valid implementer of the protocol. +""" +from jsonschema.protocols import Validator +from jsonschema.validators import ( + Draft3Validator, + Draft4Validator, + Draft6Validator, + Draft7Validator, + Draft201909Validator, + Draft202012Validator, +) + +my_validator: type[Validator] + +my_validator = Draft3Validator +my_validator = Draft4Validator +my_validator = Draft6Validator +my_validator = Draft7Validator +my_validator = Draft201909Validator +my_validator = Draft202012Validator + + +# in order to confirm that none of the above were incorrectly typed as 'Any' +# ensure that each of these assignments to a non-validator variable requires an +# ignore +none_var: None + +none_var = Draft3Validator # type: ignore[assignment] +none_var = Draft4Validator # type: ignore[assignment] +none_var = Draft6Validator # type: ignore[assignment] +none_var = Draft7Validator # type: ignore[assignment] +none_var = Draft201909Validator # type: ignore[assignment] +none_var = Draft202012Validator # type: ignore[assignment] diff --git a/jsonschema/validators.py b/jsonschema/validators.py index b8ca3bd4..dbc029fc 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -147,7 +147,7 @@ def create( applicable_validators: _typing.ApplicableValidators = methodcaller( "items", ), -): +) -> type[Validator]: """ Create a new validator class. @@ -511,7 +511,7 @@ def is_valid(self, instance, _schema=None): Validator.__name__ = Validator.__qualname__ = f"{safe}Validator" Validator = validates(version)(Validator) # type: ignore[misc] - return Validator + return Validator # type: ignore[return-value] def extend( diff --git a/noxfile.py b/noxfile.py index 3a306c38..05a23845 100644 --- a/noxfile.py +++ b/noxfile.py @@ -6,6 +6,7 @@ ROOT = Path(__file__).parent PACKAGE = ROOT / "jsonschema" +TYPING_TESTS= ROOT / "jsonschema" / "tests" / "typing" BENCHMARKS = PACKAGE / "benchmarks" PYPROJECT = ROOT / "pyproject.toml" CHANGELOG = ROOT / "CHANGELOG.rst" @@ -128,6 +129,7 @@ def license_check(session): "jsonschema", "jsonschema-specifications", "referencing", + "rpds-py", "types-python-dateutil", "--allow-only", @@ -180,6 +182,9 @@ def typing(session): """ session.install("mypy", "types-requests", ROOT) session.run("mypy", "--config", PYPROJECT, PACKAGE) + session.run( + "mypy", "--config", PYPROJECT, "--warn-unused-ignores", TYPING_TESTS, + ) @session(tags=["docs"]) diff --git a/pyproject.toml b/pyproject.toml index e2c128cc..ad98e9f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ format-nongpl = [ "jsonpointer>1.13", "rfc3339-validator", "rfc3986-validator>0.1.0", + "rfc3987-syntax>=1.1.0", "uri_template", "webcolors>=24.6.0", ]