From 5a2f8ee5a4b428582d7224b79da17277648820a2 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 10 Dec 2021 23:30:32 +0000 Subject: [PATCH 1/9] Setup mypy in `tox -e typing` and get it to pass This is the smallest possible change to get mypy passing on the jsonschema codebase. The goal of this configuration is to enforce type annotations anywhere that they appear. That is, if a method is added to the codebase, def foo(x: int) -> str: return str(x) then usages of `foo` will by type checked. If no annotations are added, `mypy` will not type check functions. For the most part, this keeps the impact low. The one exceptional case is the use of `pyrsistent.pmap` as an argument to `attr.ib(converter=...)`. Unfortunately, it causes `mypy` to incorrectly deduce the type of the init parameter created by attrs. We need to "explain the type of init" to mypy by creating a callable with a concrete type to act as the converter. The callable in question simply wraps `pmap` with a cast and presents the desired type information to mypy. --- .github/workflows/ci.yml | 2 ++ jsonschema/_format.py | 9 ++++++++- jsonschema/_types.py | 20 +++++++++++++++++++- jsonschema/_utils.py | 2 +- jsonschema/cli.py | 2 +- jsonschema/exceptions.py | 5 +++-- jsonschema/tests/test_cli.py | 2 +- jsonschema/tests/test_validators.py | 13 +++++++------ jsonschema/validators.py | 5 +++-- setup.cfg | 3 +++ tox.ini | 10 ++++++++++ 11 files changed, 58 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f5c8cee8..2b9e7e89d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,6 +102,8 @@ jobs: toxenv: secrets - name: 3.9 toxenv: style + - name: 3.9 + toxenv: typing exclude: - os: windows-latest python-version: diff --git a/jsonschema/_format.py b/jsonschema/_format.py index c6a0d05f6..dba54da25 100644 --- a/jsonschema/_format.py +++ b/jsonschema/_format.py @@ -3,9 +3,13 @@ import datetime import ipaddress import re +import typing from jsonschema.exceptions import FormatError +_FormatCheckerFunc = typing.Callable[[typing.Any], bool] +_CheckerRaises = typing.Union[Exception, typing.Tuple[Exception, ...]] + class FormatChecker(object): """ @@ -30,7 +34,10 @@ class FormatChecker(object): limit which formats will be used during validation. """ - checkers = {} + checkers: typing.Dict[ + str, + typing.Tuple[_FormatCheckerFunc, _CheckerRaises], + ] = {} def __init__(self, formats=None): if formats is None: diff --git a/jsonschema/_types.py b/jsonschema/_types.py index f24e433c6..b024f7e52 100644 --- a/jsonschema/_types.py +++ b/jsonschema/_types.py @@ -1,10 +1,25 @@ import numbers +import typing from pyrsistent import pmap import attr from jsonschema.exceptions import UndefinedTypeCheck +# internal type declarations for annotations +_TypeCheckerFunc = typing.Callable[["TypeChecker", typing.Any], bool] +_TypeCheckerMapping = typing.Mapping[str, _TypeCheckerFunc] + + +# unfortunately, the type of pmap is generic, and if used as the attr.ib +# converter, the generic type is presented to mypy, which then fails to match +# the concrete type of a type checker mapping +# this "do nothing" wrapper presents the correct information to mypy +def _typed_pmap_converter( + init_val: _TypeCheckerMapping, +) -> _TypeCheckerMapping: + return typing.cast(_TypeCheckerMapping, pmap(init_val)) + def is_array(checker, instance): return isinstance(instance, list) @@ -60,7 +75,10 @@ class TypeChecker(object): The initial mapping of types to their checking functions. """ - _type_checkers = attr.ib(default=pmap(), converter=pmap) + + _type_checkers: _TypeCheckerMapping = attr.ib( + default=pmap(), converter=_typed_pmap_converter, + ) def is_type(self, instance, type): """ diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py index 1d62545bf..c66b07dc9 100644 --- a/jsonschema/_utils.py +++ b/jsonschema/_utils.py @@ -9,7 +9,7 @@ if sys.version_info >= (3, 9): # pragma: no cover from importlib import resources else: # pragma: no cover - import importlib_resources as resources + import importlib_resources as resources # type: ignore class URIDict(MutableMapping): diff --git a/jsonschema/cli.py b/jsonschema/cli.py index ce3568466..f5f6aef89 100644 --- a/jsonschema/cli.py +++ b/jsonschema/cli.py @@ -12,7 +12,7 @@ try: from importlib import metadata except ImportError: - import importlib_metadata as metadata + import importlib_metadata as metadata # type: ignore import attr diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index e2ec35fa1..70e6372b2 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -5,13 +5,14 @@ from pprint import pformat from textwrap import dedent, indent import itertools +import typing import attr from jsonschema import _utils -WEAK_MATCHES = frozenset(["anyOf", "oneOf"]) -STRONG_MATCHES = frozenset() +WEAK_MATCHES: typing.FrozenSet[str] = frozenset(["anyOf", "oneOf"]) +STRONG_MATCHES: typing.FrozenSet[str] = frozenset() _unset = _utils.Unset() diff --git a/jsonschema/tests/test_cli.py b/jsonschema/tests/test_cli.py index f9c8c621e..6d42af614 100644 --- a/jsonschema/tests/test_cli.py +++ b/jsonschema/tests/test_cli.py @@ -13,7 +13,7 @@ try: # pragma: no cover from importlib import metadata except ImportError: # pragma: no cover - import importlib_metadata as metadata + import importlib_metadata as metadata # type: ignore from pyrsistent import m diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index 67082d5b3..aae9faaf0 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -8,6 +8,7 @@ import os import sys import tempfile +import typing import unittest import warnings @@ -1662,7 +1663,7 @@ def test_False_is_not_a_schema_even_if_you_forget_to_check(self): class TestDraft3Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase): Validator = validators.Draft3Validator - valid = {}, {} + valid: typing.Tuple[dict, dict] = {}, {} invalid = {"type": "integer"}, "foo" def test_any_type_is_valid_for_type_any(self): @@ -1694,31 +1695,31 @@ def test_is_type_does_not_evade_bool_if_it_is_being_tested(self): class TestDraft4Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase): Validator = validators.Draft4Validator - valid = {}, {} + valid: typing.Tuple[dict, dict] = {}, {} invalid = {"type": "integer"}, "foo" class TestDraft6Validator(ValidatorTestMixin, TestCase): Validator = validators.Draft6Validator - valid = {}, {} + valid: typing.Tuple[dict, dict] = {}, {} invalid = {"type": "integer"}, "foo" class TestDraft7Validator(ValidatorTestMixin, TestCase): Validator = validators.Draft7Validator - valid = {}, {} + valid: typing.Tuple[dict, dict] = {}, {} invalid = {"type": "integer"}, "foo" class TestDraft201909Validator(ValidatorTestMixin, TestCase): Validator = validators.Draft201909Validator - valid = {}, {} + valid: typing.Tuple[dict, dict] = {}, {} invalid = {"type": "integer"}, "foo" class TestDraft202012Validator(ValidatorTestMixin, TestCase): Validator = validators.Draft202012Validator - valid = {}, {} + valid: typing.Tuple[dict, dict] = {}, {} invalid = {"type": "integer"}, "foo" diff --git a/jsonschema/validators.py b/jsonschema/validators.py index f38503f10..b6d850dcf 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -10,6 +10,7 @@ import contextlib import json import reprlib +import typing import warnings import attr @@ -22,9 +23,9 @@ exceptions, ) -_VALIDATORS = {} +_VALIDATORS: typing.Dict[str, typing.Any] = {} _META_SCHEMAS = _utils.URIDict() -_VOCABULARIES = [] +_VOCABULARIES: typing.List[typing.Tuple[str, typing.Any]] = [] def __getattr__(name): diff --git a/setup.cfg b/setup.cfg index dacd87754..3d2924af0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -72,6 +72,9 @@ ignore = B306, # See https://github.com/PyCQA/flake8-bugbear/issues/131 W503, # (flake8 default) old PEP8 boolean operator line breaks +[mypy] +ignore_missing_imports = true + [pydocstyle] match = (?!(test_|_|compat|cli)).*\.py # see PyCQA/pydocstyle#323 add-select = diff --git a/tox.ini b/tox.ini index eea61c689..ced13cc11 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = safety secrets style + typing docs-{html,doctest,linkcheck,spelling,style} skipsdist = True @@ -89,6 +90,15 @@ deps = commands = {envpython} -m flake8 {posargs} {toxinidir}/jsonschema {toxinidir}/docs +[testenv:typing] +skip_install = true +deps = + mypy + pyrsistent + types-attrs + types-requests +commands = {envpython} -m mypy --config {toxinidir}/setup.cfg {posargs} {toxinidir}/jsonschema + [testenv:docs-dirhtml] commands = {envpython} -m sphinx -b dirhtml {toxinidir}/docs/ {envtmpdir}/build {posargs:-a -n -q -T -W} deps = From a8c131baafebcf27b0245ba8916b351ce42807f2 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 16 Dec 2021 19:18:30 +0000 Subject: [PATCH 2/9] Fix typing_extensions import handling for mypy mypy can handle `if sys.version_info` checking better than `try ... except ImportError`. This is somewhat disappointing, but the rewrite of these import lines isn't that bad and aligns with the recommendations of the mypy docs. --- jsonschema/protocols.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/jsonschema/protocols.py b/jsonschema/protocols.py index 2e177b3ae..aebba3189 100644 --- a/jsonschema/protocols.py +++ b/jsonschema/protocols.py @@ -6,10 +6,18 @@ # https://www.python.org/dev/peps/pep-0544/ from typing import Any, ClassVar, Iterator, Optional, Union +import sys -try: +# doing these imports with `try ... except ImportError` doesn't pass mypy +# checking because mypy sees `typing._SpecialForm` and +# `typing_extensions._SpecialForm` as incompatible +# +# see: +# https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-new-additions-to-the-typing-module +# https://github.com/python/mypy/issues/4427 +if sys.version_info >= (3, 8): from typing import Protocol, runtime_checkable -except ImportError: +else: from typing_extensions import Protocol, runtime_checkable from jsonschema._format import FormatChecker From ec8dab1ca46974a9ecc99de454784854d862f4bc Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 16 Dec 2021 19:58:01 +0000 Subject: [PATCH 3/9] Parenthesize dict-tuple to pacify pypy3.7 parser In pypy3, the following line is invalid: x: Tuple[dict, dict] = {}, {} but this variant *is* valid: x: Tuple[dict, dict] = ({}, {}) Apply this correction where it occurs in test_validators.py --- jsonschema/tests/test_validators.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index aae9faaf0..03af7edc7 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -1663,7 +1663,7 @@ def test_False_is_not_a_schema_even_if_you_forget_to_check(self): class TestDraft3Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase): Validator = validators.Draft3Validator - valid: typing.Tuple[dict, dict] = {}, {} + valid: typing.Tuple[dict, dict] = ({}, {}) invalid = {"type": "integer"}, "foo" def test_any_type_is_valid_for_type_any(self): @@ -1695,31 +1695,31 @@ def test_is_type_does_not_evade_bool_if_it_is_being_tested(self): class TestDraft4Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase): Validator = validators.Draft4Validator - valid: typing.Tuple[dict, dict] = {}, {} + valid: typing.Tuple[dict, dict] = ({}, {}) invalid = {"type": "integer"}, "foo" class TestDraft6Validator(ValidatorTestMixin, TestCase): Validator = validators.Draft6Validator - valid: typing.Tuple[dict, dict] = {}, {} + valid: typing.Tuple[dict, dict] = ({}, {}) invalid = {"type": "integer"}, "foo" class TestDraft7Validator(ValidatorTestMixin, TestCase): Validator = validators.Draft7Validator - valid: typing.Tuple[dict, dict] = {}, {} + valid: typing.Tuple[dict, dict] = ({}, {}) invalid = {"type": "integer"}, "foo" class TestDraft201909Validator(ValidatorTestMixin, TestCase): Validator = validators.Draft201909Validator - valid: typing.Tuple[dict, dict] = {}, {} + valid: typing.Tuple[dict, dict] = ({}, {}) invalid = {"type": "integer"}, "foo" class TestDraft202012Validator(ValidatorTestMixin, TestCase): Validator = validators.Draft202012Validator - valid: typing.Tuple[dict, dict] = {}, {} + valid: typing.Tuple[dict, dict] = ({}, {}) invalid = {"type": "integer"}, "foo" From 2474242192af8108086410d32b4d1307675d471a Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 3 Jan 2022 20:31:07 +0000 Subject: [PATCH 4/9] Use future import for type annotations Use of the `__future__` import of annotations allows several niceties, in particular: - parametrization of builtin types as generics - `|` syntax for unions (including `| None` for optionals) Update to use the future import wherever it improves or simplifies annotations. Avoid using new typing features outside of annotations (e.g. in assignment), which fails on older pythons. This is an unfortunate wart in the way that the future import works. --- jsonschema/_format.py | 6 ++++-- jsonschema/exceptions.py | 7 ++++--- jsonschema/protocols.py | 12 +++++++----- jsonschema/tests/test_validators.py | 15 ++++++++------- jsonschema/validators.py | 6 ++++-- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/jsonschema/_format.py b/jsonschema/_format.py index dba54da25..a842307f5 100644 --- a/jsonschema/_format.py +++ b/jsonschema/_format.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from contextlib import suppress from uuid import UUID import datetime @@ -34,9 +36,9 @@ class FormatChecker(object): limit which formats will be used during validation. """ - checkers: typing.Dict[ + checkers: dict[ str, - typing.Tuple[_FormatCheckerFunc, _CheckerRaises], + tuple[_FormatCheckerFunc, _CheckerRaises], ] = {} def __init__(self, formats=None): diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index 70e6372b2..274e6c58d 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -1,18 +1,19 @@ """ Validation errors, and some surrounding helpers. """ +from __future__ import annotations + from collections import defaultdict, deque from pprint import pformat from textwrap import dedent, indent import itertools -import typing import attr from jsonschema import _utils -WEAK_MATCHES: typing.FrozenSet[str] = frozenset(["anyOf", "oneOf"]) -STRONG_MATCHES: typing.FrozenSet[str] = frozenset() +WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"]) +STRONG_MATCHES: frozenset[str] = frozenset() _unset = _utils.Unset() diff --git a/jsonschema/protocols.py b/jsonschema/protocols.py index aebba3189..ee00b8477 100644 --- a/jsonschema/protocols.py +++ b/jsonschema/protocols.py @@ -5,7 +5,9 @@ # for reference material on Protocols, see # https://www.python.org/dev/peps/pep-0544/ -from typing import Any, ClassVar, Iterator, Optional, Union +from __future__ import annotations + +from typing import Any, ClassVar, Iterator import sys # doing these imports with `try ... except ImportError` doesn't pass mypy @@ -73,13 +75,13 @@ class Validator(Protocol): TYPE_CHECKER: ClassVar[TypeChecker] #: The schema that was passed in when initializing the object. - schema: Union[dict, bool] + schema: dict | bool def __init__( self, - schema: Union[dict, bool], - resolver: Optional[RefResolver] = None, - format_checker: Optional[FormatChecker] = None, + schema: dict | bool, + resolver: RefResolver | None = None, + format_checker: FormatChecker | None = None, ) -> None: ... diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index 03af7edc7..8b69a69bc 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections import deque, namedtuple from contextlib import contextmanager from decimal import Decimal @@ -8,7 +10,6 @@ import os import sys import tempfile -import typing import unittest import warnings @@ -1663,7 +1664,7 @@ def test_False_is_not_a_schema_even_if_you_forget_to_check(self): class TestDraft3Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase): Validator = validators.Draft3Validator - valid: typing.Tuple[dict, dict] = ({}, {}) + valid: tuple[dict, dict] = ({}, {}) invalid = {"type": "integer"}, "foo" def test_any_type_is_valid_for_type_any(self): @@ -1695,31 +1696,31 @@ def test_is_type_does_not_evade_bool_if_it_is_being_tested(self): class TestDraft4Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase): Validator = validators.Draft4Validator - valid: typing.Tuple[dict, dict] = ({}, {}) + valid: tuple[dict, dict] = ({}, {}) invalid = {"type": "integer"}, "foo" class TestDraft6Validator(ValidatorTestMixin, TestCase): Validator = validators.Draft6Validator - valid: typing.Tuple[dict, dict] = ({}, {}) + valid: tuple[dict, dict] = ({}, {}) invalid = {"type": "integer"}, "foo" class TestDraft7Validator(ValidatorTestMixin, TestCase): Validator = validators.Draft7Validator - valid: typing.Tuple[dict, dict] = ({}, {}) + valid: tuple[dict, dict] = ({}, {}) invalid = {"type": "integer"}, "foo" class TestDraft201909Validator(ValidatorTestMixin, TestCase): Validator = validators.Draft201909Validator - valid: typing.Tuple[dict, dict] = ({}, {}) + valid: tuple[dict, dict] = ({}, {}) invalid = {"type": "integer"}, "foo" class TestDraft202012Validator(ValidatorTestMixin, TestCase): Validator = validators.Draft202012Validator - valid: typing.Tuple[dict, dict] = ({}, {}) + valid: tuple[dict, dict] = ({}, {}) invalid = {"type": "integer"}, "foo" diff --git a/jsonschema/validators.py b/jsonschema/validators.py index b6d850dcf..a689e513e 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -1,6 +1,8 @@ """ Creation and extension of validators, with implementations for existing drafts. """ +from __future__ import annotations + from collections import deque from collections.abc import Sequence from functools import lru_cache @@ -23,9 +25,9 @@ exceptions, ) -_VALIDATORS: typing.Dict[str, typing.Any] = {} +_VALIDATORS: dict[str, typing.Any] = {} _META_SCHEMAS = _utils.URIDict() -_VOCABULARIES: typing.List[typing.Tuple[str, typing.Any]] = [] +_VOCABULARIES: list[tuple[str, typing.Any]] = [] def __getattr__(name): From 631fba10862dca89f032231e1aba70225379f314 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 5 Jan 2022 23:00:09 +0000 Subject: [PATCH 5/9] Fix nitpick error on type annotation Nitpick was failing on TYPE_CHECKER: ClassVar[TypeChecker] It's not clear what to do about this. `TypeChecker` was imported correctly from `jsonschema._types` and is noted in the doc as `jsonschema.TypeChecker`. We can't import the `jsonschema` name at runtime because that would be circular. To resolve, use `typing.TYPE_CHECKING` to conditionally import `jsonschema` at type-checking time. This avoids the circular import but allows us to write TYPE_CHECKER: ClassVar[jsonschema.TypeChecker] As a result, Sphinx correctly builds a cross-reference from the annotation, the annotation is still accurate, and runtime behavior is left untouched. --- jsonschema/_types.py | 2 ++ jsonschema/protocols.py | 15 ++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/jsonschema/_types.py b/jsonschema/_types.py index b024f7e52..d0525bde6 100644 --- a/jsonschema/_types.py +++ b/jsonschema/_types.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import numbers import typing diff --git a/jsonschema/protocols.py b/jsonschema/protocols.py index ee00b8477..ea0044696 100644 --- a/jsonschema/protocols.py +++ b/jsonschema/protocols.py @@ -7,7 +7,7 @@ from __future__ import annotations -from typing import Any, ClassVar, Iterator +from typing import TYPE_CHECKING, Any, ClassVar, Iterator import sys # doing these imports with `try ... except ImportError` doesn't pass mypy @@ -22,8 +22,13 @@ else: from typing_extensions import Protocol, runtime_checkable -from jsonschema._format import FormatChecker -from jsonschema._types import TypeChecker +# in order for Sphinx to resolve references accurately from type annotations, +# it needs to see names like `jsonschema.TypeChecker` +# therefore, only import at type-checking time (to avoid circular references), +# but use `jsonschema` for any types which will otherwise not be resolvable +if TYPE_CHECKING: + import jsonschema + from jsonschema.exceptions import ValidationError from jsonschema.validators import RefResolver @@ -72,7 +77,7 @@ class Validator(Protocol): #: A `jsonschema.TypeChecker` that will be used when validating #: :validator:`type` properties in JSON schemas. - TYPE_CHECKER: ClassVar[TypeChecker] + TYPE_CHECKER: ClassVar[jsonschema.TypeChecker] #: The schema that was passed in when initializing the object. schema: dict | bool @@ -81,7 +86,7 @@ def __init__( self, schema: dict | bool, resolver: RefResolver | None = None, - format_checker: FormatChecker | None = None, + format_checker: jsonschema.FormatChecker | None = None, ) -> None: ... From 17384e735ca7411c0199cd39f3375e0b25a8ca81 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 5 Jan 2022 23:21:06 +0000 Subject: [PATCH 6/9] Fix sphinx nitpick error arising from annotations Type annotations can use variable names in order to support easily named constructs (e.g. `FooType = Union[int, str]; x: FooType`). However, such variable names are then seen by sphinx autodoc, which does not evaluate them. As a result, for such a name to avoid tripping nitpick warnings, these names need to be resolvable. The simplest resolution is to remove the use of any internal variables for this purpose (at least where they would be seen by sphinx), and use the longhand description of types in such cases. --- jsonschema/_types.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/jsonschema/_types.py b/jsonschema/_types.py index d0525bde6..9d59eb325 100644 --- a/jsonschema/_types.py +++ b/jsonschema/_types.py @@ -8,19 +8,24 @@ from jsonschema.exceptions import UndefinedTypeCheck -# internal type declarations for annotations -_TypeCheckerFunc = typing.Callable[["TypeChecker", typing.Any], bool] -_TypeCheckerMapping = typing.Mapping[str, _TypeCheckerFunc] - # unfortunately, the type of pmap is generic, and if used as the attr.ib # converter, the generic type is presented to mypy, which then fails to match # the concrete type of a type checker mapping # this "do nothing" wrapper presents the correct information to mypy def _typed_pmap_converter( - init_val: _TypeCheckerMapping, -) -> _TypeCheckerMapping: - return typing.cast(_TypeCheckerMapping, pmap(init_val)) + init_val: typing.Mapping[ + str, + typing.Callable[["TypeChecker", typing.Any], bool], + ], +) -> typing.Mapping[str, typing.Callable[["TypeChecker", typing.Any], bool]]: + return typing.cast( + typing.Mapping[ + str, + typing.Callable[["TypeChecker", typing.Any], bool], + ], + pmap(init_val), + ) def is_array(checker, instance): @@ -78,8 +83,11 @@ class TypeChecker(object): The initial mapping of types to their checking functions. """ - _type_checkers: _TypeCheckerMapping = attr.ib( - default=pmap(), converter=_typed_pmap_converter, + _type_checkers: typing.Mapping[ + str, typing.Callable[["TypeChecker", typing.Any], bool], + ] = attr.ib( + default=pmap(), + converter=_typed_pmap_converter, ) def is_type(self, instance, type): From cdc2807c0d14a58947be9508566af8263b77fccb Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 5 Jan 2022 23:37:50 +0000 Subject: [PATCH 7/9] Cleanup internal type annotation variables `_format.py` had a few internal variables to make type annotations shorter to write. However, these can cause issues with sphinx nitpick. Even though the variables removed in this commit aren't causing a build failure, removing them is a good precaution. --- jsonschema/_format.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jsonschema/_format.py b/jsonschema/_format.py index a842307f5..5f99c6599 100644 --- a/jsonschema/_format.py +++ b/jsonschema/_format.py @@ -9,9 +9,6 @@ from jsonschema.exceptions import FormatError -_FormatCheckerFunc = typing.Callable[[typing.Any], bool] -_CheckerRaises = typing.Union[Exception, typing.Tuple[Exception, ...]] - class FormatChecker(object): """ @@ -38,7 +35,10 @@ class FormatChecker(object): checkers: dict[ str, - tuple[_FormatCheckerFunc, _CheckerRaises], + tuple[ + typing.Callable[[typing.Any], bool], + Exception | tuple[Exception, ...], + ], ] = {} def __init__(self, formats=None): From 811bab2efdea4a2d5c29c5246480bf809a9a9a35 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 5 Jan 2022 23:43:00 +0000 Subject: [PATCH 8/9] Omit 'if TYPE_CHECKING' blocks from coverage These blocks only exist to create code which evaluates under mypy but does not run at runtime. As a result, they are impossible to cover with `coverage` on a testsuite, and make sense to exclude from coverage reporting. --- .coveragerc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.coveragerc b/.coveragerc index 0d30ffb5c..ad7c06fc9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,3 +3,7 @@ branch = True source = jsonschema omit = */jsonschema/_reflect.py,*/jsonschema/__main__.py,*/jsonschema/benchmarks/*,*/jsonschema/tests/fuzz_validate.py + +[report] +exclude_lines = + if TYPE_CHECKING: From bc4f2d59a597fa2ee6c1e743c2b535a67e98f2a0 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Thu, 6 Jan 2022 17:44:17 +0000 Subject: [PATCH 9/9] Bump doc requirements. --- docs/requirements.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 3198a2071..6bc490f28 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,7 +6,7 @@ # alabaster==0.7.12 # via sphinx -attrs==21.2.0 +attrs==21.4.0 # via jsonschema babel==2.9.1 # via sphinx @@ -14,11 +14,11 @@ beautifulsoup4==4.10.0 # via furo certifi==2021.10.8 # via requests -charset-normalizer==2.0.9 +charset-normalizer==2.0.10 # via requests docutils==0.17.1 # via sphinx -furo==2021.11.23 +furo==2022.1.2 # via -r docs/requirements.in idna==3.3 # via requests @@ -28,7 +28,7 @@ jinja2==3.0.3 # via sphinx file:.#egg=jsonschema # via -r docs/requirements.in -lxml==4.6.5 +lxml==4.7.1 # via -r docs/requirements.in markupsafe==2.0.1 # via jinja2 @@ -36,7 +36,7 @@ packaging==21.3 # via sphinx pyenchant==3.2.2 # via sphinxcontrib-spelling -pygments==2.10.0 +pygments==2.11.2 # via # furo # sphinx @@ -46,13 +46,13 @@ pyrsistent==0.18.0 # via jsonschema pytz==2021.3 # via babel -requests==2.26.0 +requests==2.27.1 # via sphinx snowballstemmer==2.2.0 # via sphinx soupsieve==2.3.1 # via beautifulsoup4 -sphinx==4.3.1 +sphinx==4.3.2 # via # -r docs/requirements.in # furo @@ -69,7 +69,7 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -sphinxcontrib-spelling==7.3.0 +sphinxcontrib-spelling==7.3.2 # via -r docs/requirements.in urllib3==1.26.7 # via requests