Skip to content

Commit eed6d8b

Browse files
committed
Merge branch 'add-mypy-config'
* add-mypy-config: Bump doc requirements. Omit 'if TYPE_CHECKING' blocks from coverage Cleanup internal type annotation variables Fix sphinx nitpick error arising from annotations Fix nitpick error on type annotation Use future import for type annotations Parenthesize dict-tuple to pacify pypy3.7 parser Fix typing_extensions import handling for mypy Setup mypy in `tox -e typing` and get it to pass
2 parents fc0990a + bc4f2d5 commit eed6d8b

File tree

14 files changed

+111
-33
lines changed

14 files changed

+111
-33
lines changed

.coveragerc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33
branch = True
44
source = jsonschema
55
omit = */jsonschema/_reflect.py,*/jsonschema/__main__.py,*/jsonschema/benchmarks/*,*/jsonschema/tests/fuzz_validate.py
6+
7+
[report]
8+
exclude_lines =
9+
if TYPE_CHECKING:

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ jobs:
102102
toxenv: secrets
103103
- name: 3.9
104104
toxenv: style
105+
- name: 3.9
106+
toxenv: typing
105107
exclude:
106108
- os: windows-latest
107109
python-version:

docs/requirements.txt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@
66
#
77
alabaster==0.7.12
88
# via sphinx
9-
attrs==21.2.0
9+
attrs==21.4.0
1010
# via jsonschema
1111
babel==2.9.1
1212
# via sphinx
1313
beautifulsoup4==4.10.0
1414
# via furo
1515
certifi==2021.10.8
1616
# via requests
17-
charset-normalizer==2.0.9
17+
charset-normalizer==2.0.10
1818
# via requests
1919
docutils==0.17.1
2020
# via sphinx
21-
furo==2021.11.23
21+
furo==2022.1.2
2222
# via -r docs/requirements.in
2323
idna==3.3
2424
# via requests
@@ -28,15 +28,15 @@ jinja2==3.0.3
2828
# via sphinx
2929
file:.#egg=jsonschema
3030
# via -r docs/requirements.in
31-
lxml==4.6.5
31+
lxml==4.7.1
3232
# via -r docs/requirements.in
3333
markupsafe==2.0.1
3434
# via jinja2
3535
packaging==21.3
3636
# via sphinx
3737
pyenchant==3.2.2
3838
# via sphinxcontrib-spelling
39-
pygments==2.10.0
39+
pygments==2.11.2
4040
# via
4141
# furo
4242
# sphinx
@@ -46,13 +46,13 @@ pyrsistent==0.18.0
4646
# via jsonschema
4747
pytz==2021.3
4848
# via babel
49-
requests==2.26.0
49+
requests==2.27.1
5050
# via sphinx
5151
snowballstemmer==2.2.0
5252
# via sphinx
5353
soupsieve==2.3.1
5454
# via beautifulsoup4
55-
sphinx==4.3.1
55+
sphinx==4.3.2
5656
# via
5757
# -r docs/requirements.in
5858
# furo
@@ -69,7 +69,7 @@ sphinxcontrib-qthelp==1.0.3
6969
# via sphinx
7070
sphinxcontrib-serializinghtml==1.1.5
7171
# via sphinx
72-
sphinxcontrib-spelling==7.3.0
72+
sphinxcontrib-spelling==7.3.2
7373
# via -r docs/requirements.in
7474
urllib3==1.26.7
7575
# via requests

jsonschema/_format.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
from __future__ import annotations
2+
13
from contextlib import suppress
24
from uuid import UUID
35
import datetime
46
import ipaddress
57
import re
8+
import typing
69

710
from jsonschema.exceptions import FormatError
811

@@ -30,7 +33,13 @@ class FormatChecker(object):
3033
limit which formats will be used during validation.
3134
"""
3235

33-
checkers = {}
36+
checkers: dict[
37+
str,
38+
tuple[
39+
typing.Callable[[typing.Any], bool],
40+
Exception | tuple[Exception, ...],
41+
],
42+
] = {}
3443

3544
def __init__(self, formats=None):
3645
if formats is None:

jsonschema/_types.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
1+
from __future__ import annotations
2+
13
import numbers
4+
import typing
25

36
from pyrsistent import pmap
47
import attr
58

69
from jsonschema.exceptions import UndefinedTypeCheck
710

811

12+
# unfortunately, the type of pmap is generic, and if used as the attr.ib
13+
# converter, the generic type is presented to mypy, which then fails to match
14+
# the concrete type of a type checker mapping
15+
# this "do nothing" wrapper presents the correct information to mypy
16+
def _typed_pmap_converter(
17+
init_val: typing.Mapping[
18+
str,
19+
typing.Callable[["TypeChecker", typing.Any], bool],
20+
],
21+
) -> typing.Mapping[str, typing.Callable[["TypeChecker", typing.Any], bool]]:
22+
return typing.cast(
23+
typing.Mapping[
24+
str,
25+
typing.Callable[["TypeChecker", typing.Any], bool],
26+
],
27+
pmap(init_val),
28+
)
29+
30+
931
def is_array(checker, instance):
1032
return isinstance(instance, list)
1133

@@ -60,7 +82,13 @@ class TypeChecker(object):
6082
6183
The initial mapping of types to their checking functions.
6284
"""
63-
_type_checkers = attr.ib(default=pmap(), converter=pmap)
85+
86+
_type_checkers: typing.Mapping[
87+
str, typing.Callable[["TypeChecker", typing.Any], bool],
88+
] = attr.ib(
89+
default=pmap(),
90+
converter=_typed_pmap_converter,
91+
)
6492

6593
def is_type(self, instance, type):
6694
"""

jsonschema/_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
if sys.version_info >= (3, 9): # pragma: no cover
1010
from importlib import resources
1111
else: # pragma: no cover
12-
import importlib_resources as resources
12+
import importlib_resources as resources # type: ignore
1313

1414

1515
class URIDict(MutableMapping):

jsonschema/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
try:
1313
from importlib import metadata
1414
except ImportError:
15-
import importlib_metadata as metadata
15+
import importlib_metadata as metadata # type: ignore
1616

1717
import attr
1818

jsonschema/exceptions.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""
22
Validation errors, and some surrounding helpers.
33
"""
4+
from __future__ import annotations
5+
46
from collections import defaultdict, deque
57
from pprint import pformat
68
from textwrap import dedent, indent
@@ -10,8 +12,8 @@
1012

1113
from jsonschema import _utils
1214

13-
WEAK_MATCHES = frozenset(["anyOf", "oneOf"])
14-
STRONG_MATCHES = frozenset()
15+
WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"])
16+
STRONG_MATCHES: frozenset[str] = frozenset()
1517

1618
_unset = _utils.Unset()
1719

jsonschema/protocols.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,30 @@
55
# for reference material on Protocols, see
66
# https://www.python.org/dev/peps/pep-0544/
77

8-
from typing import Any, ClassVar, Iterator, Optional, Union
8+
from __future__ import annotations
99

10-
try:
10+
from typing import TYPE_CHECKING, Any, ClassVar, Iterator
11+
import sys
12+
13+
# doing these imports with `try ... except ImportError` doesn't pass mypy
14+
# checking because mypy sees `typing._SpecialForm` and
15+
# `typing_extensions._SpecialForm` as incompatible
16+
#
17+
# see:
18+
# https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-new-additions-to-the-typing-module
19+
# https://github.com/python/mypy/issues/4427
20+
if sys.version_info >= (3, 8):
1121
from typing import Protocol, runtime_checkable
12-
except ImportError:
22+
else:
1323
from typing_extensions import Protocol, runtime_checkable
1424

15-
from jsonschema._format import FormatChecker
16-
from jsonschema._types import TypeChecker
25+
# in order for Sphinx to resolve references accurately from type annotations,
26+
# it needs to see names like `jsonschema.TypeChecker`
27+
# therefore, only import at type-checking time (to avoid circular references),
28+
# but use `jsonschema` for any types which will otherwise not be resolvable
29+
if TYPE_CHECKING:
30+
import jsonschema
31+
1732
from jsonschema.exceptions import ValidationError
1833
from jsonschema.validators import RefResolver
1934

@@ -62,16 +77,16 @@ class Validator(Protocol):
6277

6378
#: A `jsonschema.TypeChecker` that will be used when validating
6479
#: :validator:`type` properties in JSON schemas.
65-
TYPE_CHECKER: ClassVar[TypeChecker]
80+
TYPE_CHECKER: ClassVar[jsonschema.TypeChecker]
6681

6782
#: The schema that was passed in when initializing the object.
68-
schema: Union[dict, bool]
83+
schema: dict | bool
6984

7085
def __init__(
7186
self,
72-
schema: Union[dict, bool],
73-
resolver: Optional[RefResolver] = None,
74-
format_checker: Optional[FormatChecker] = None,
87+
schema: dict | bool,
88+
resolver: RefResolver | None = None,
89+
format_checker: jsonschema.FormatChecker | None = None,
7590
) -> None:
7691
...
7792

jsonschema/tests/test_cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
try: # pragma: no cover
1414
from importlib import metadata
1515
except ImportError: # pragma: no cover
16-
import importlib_metadata as metadata
16+
import importlib_metadata as metadata # type: ignore
1717

1818
from pyrsistent import m
1919

jsonschema/tests/test_validators.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from collections import deque, namedtuple
24
from contextlib import contextmanager
35
from decimal import Decimal
@@ -1662,7 +1664,7 @@ def test_False_is_not_a_schema_even_if_you_forget_to_check(self):
16621664

16631665
class TestDraft3Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase):
16641666
Validator = validators.Draft3Validator
1665-
valid = {}, {}
1667+
valid: tuple[dict, dict] = ({}, {})
16661668
invalid = {"type": "integer"}, "foo"
16671669

16681670
def test_any_type_is_valid_for_type_any(self):
@@ -1694,31 +1696,31 @@ def test_is_type_does_not_evade_bool_if_it_is_being_tested(self):
16941696

16951697
class TestDraft4Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase):
16961698
Validator = validators.Draft4Validator
1697-
valid = {}, {}
1699+
valid: tuple[dict, dict] = ({}, {})
16981700
invalid = {"type": "integer"}, "foo"
16991701

17001702

17011703
class TestDraft6Validator(ValidatorTestMixin, TestCase):
17021704
Validator = validators.Draft6Validator
1703-
valid = {}, {}
1705+
valid: tuple[dict, dict] = ({}, {})
17041706
invalid = {"type": "integer"}, "foo"
17051707

17061708

17071709
class TestDraft7Validator(ValidatorTestMixin, TestCase):
17081710
Validator = validators.Draft7Validator
1709-
valid = {}, {}
1711+
valid: tuple[dict, dict] = ({}, {})
17101712
invalid = {"type": "integer"}, "foo"
17111713

17121714

17131715
class TestDraft201909Validator(ValidatorTestMixin, TestCase):
17141716
Validator = validators.Draft201909Validator
1715-
valid = {}, {}
1717+
valid: tuple[dict, dict] = ({}, {})
17161718
invalid = {"type": "integer"}, "foo"
17171719

17181720

17191721
class TestDraft202012Validator(ValidatorTestMixin, TestCase):
17201722
Validator = validators.Draft202012Validator
1721-
valid = {}, {}
1723+
valid: tuple[dict, dict] = ({}, {})
17221724
invalid = {"type": "integer"}, "foo"
17231725

17241726

jsonschema/validators.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""
22
Creation and extension of validators, with implementations for existing drafts.
33
"""
4+
from __future__ import annotations
5+
46
from collections import deque
57
from collections.abc import Sequence
68
from functools import lru_cache
@@ -10,6 +12,7 @@
1012
import contextlib
1113
import json
1214
import reprlib
15+
import typing
1316
import warnings
1417

1518
import attr
@@ -22,9 +25,9 @@
2225
exceptions,
2326
)
2427

25-
_VALIDATORS = {}
28+
_VALIDATORS: dict[str, typing.Any] = {}
2629
_META_SCHEMAS = _utils.URIDict()
27-
_VOCABULARIES = []
30+
_VOCABULARIES: list[tuple[str, typing.Any]] = []
2831

2932

3033
def __getattr__(name):

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ ignore =
7272
B306, # See https://github.com/PyCQA/flake8-bugbear/issues/131
7373
W503, # (flake8 default) old PEP8 boolean operator line breaks
7474
75+
[mypy]
76+
ignore_missing_imports = true
77+
7578
[pydocstyle]
7679
match = (?!(test_|_|compat|cli)).*\.py # see PyCQA/pydocstyle#323
7780
add-select =

tox.ini

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ envlist =
55
safety
66
secrets
77
style
8+
typing
89
docs-{html,doctest,linkcheck,spelling,style}
910
skipsdist = True
1011

@@ -89,6 +90,15 @@ deps =
8990
commands =
9091
{envpython} -m flake8 {posargs} {toxinidir}/jsonschema {toxinidir}/docs
9192

93+
[testenv:typing]
94+
skip_install = true
95+
deps =
96+
mypy
97+
pyrsistent
98+
types-attrs
99+
types-requests
100+
commands = {envpython} -m mypy --config {toxinidir}/setup.cfg {posargs} {toxinidir}/jsonschema
101+
92102
[testenv:docs-dirhtml]
93103
commands = {envpython} -m sphinx -b dirhtml {toxinidir}/docs/ {envtmpdir}/build {posargs:-a -n -q -T -W}
94104
deps =

0 commit comments

Comments
 (0)