Skip to content

Commit 5a2f8ee

Browse files
committed
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.
1 parent fc0990a commit 5a2f8ee

File tree

11 files changed

+58
-15
lines changed

11 files changed

+58
-15
lines changed

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

jsonschema/_format.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
import datetime
44
import ipaddress
55
import re
6+
import typing
67

78
from jsonschema.exceptions import FormatError
89

10+
_FormatCheckerFunc = typing.Callable[[typing.Any], bool]
11+
_CheckerRaises = typing.Union[Exception, typing.Tuple[Exception, ...]]
12+
913

1014
class FormatChecker(object):
1115
"""
@@ -30,7 +34,10 @@ class FormatChecker(object):
3034
limit which formats will be used during validation.
3135
"""
3236

33-
checkers = {}
37+
checkers: typing.Dict[
38+
str,
39+
typing.Tuple[_FormatCheckerFunc, _CheckerRaises],
40+
] = {}
3441

3542
def __init__(self, formats=None):
3643
if formats is None:

jsonschema/_types.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
import numbers
2+
import typing
23

34
from pyrsistent import pmap
45
import attr
56

67
from jsonschema.exceptions import UndefinedTypeCheck
78

9+
# internal type declarations for annotations
10+
_TypeCheckerFunc = typing.Callable[["TypeChecker", typing.Any], bool]
11+
_TypeCheckerMapping = typing.Mapping[str, _TypeCheckerFunc]
12+
13+
14+
# unfortunately, the type of pmap is generic, and if used as the attr.ib
15+
# converter, the generic type is presented to mypy, which then fails to match
16+
# the concrete type of a type checker mapping
17+
# this "do nothing" wrapper presents the correct information to mypy
18+
def _typed_pmap_converter(
19+
init_val: _TypeCheckerMapping,
20+
) -> _TypeCheckerMapping:
21+
return typing.cast(_TypeCheckerMapping, pmap(init_val))
22+
823

924
def is_array(checker, instance):
1025
return isinstance(instance, list)
@@ -60,7 +75,10 @@ class TypeChecker(object):
6075
6176
The initial mapping of types to their checking functions.
6277
"""
63-
_type_checkers = attr.ib(default=pmap(), converter=pmap)
78+
79+
_type_checkers: _TypeCheckerMapping = attr.ib(
80+
default=pmap(), converter=_typed_pmap_converter,
81+
)
6482

6583
def is_type(self, instance, type):
6684
"""

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: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
from pprint import pformat
66
from textwrap import dedent, indent
77
import itertools
8+
import typing
89

910
import attr
1011

1112
from jsonschema import _utils
1213

13-
WEAK_MATCHES = frozenset(["anyOf", "oneOf"])
14-
STRONG_MATCHES = frozenset()
14+
WEAK_MATCHES: typing.FrozenSet[str] = frozenset(["anyOf", "oneOf"])
15+
STRONG_MATCHES: typing.FrozenSet[str] = frozenset()
1516

1617
_unset = _utils.Unset()
1718

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: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import os
99
import sys
1010
import tempfile
11+
import typing
1112
import unittest
1213
import warnings
1314

@@ -1662,7 +1663,7 @@ def test_False_is_not_a_schema_even_if_you_forget_to_check(self):
16621663

16631664
class TestDraft3Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase):
16641665
Validator = validators.Draft3Validator
1665-
valid = {}, {}
1666+
valid: typing.Tuple[dict, dict] = {}, {}
16661667
invalid = {"type": "integer"}, "foo"
16671668

16681669
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):
16941695

16951696
class TestDraft4Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase):
16961697
Validator = validators.Draft4Validator
1697-
valid = {}, {}
1698+
valid: typing.Tuple[dict, dict] = {}, {}
16981699
invalid = {"type": "integer"}, "foo"
16991700

17001701

17011702
class TestDraft6Validator(ValidatorTestMixin, TestCase):
17021703
Validator = validators.Draft6Validator
1703-
valid = {}, {}
1704+
valid: typing.Tuple[dict, dict] = {}, {}
17041705
invalid = {"type": "integer"}, "foo"
17051706

17061707

17071708
class TestDraft7Validator(ValidatorTestMixin, TestCase):
17081709
Validator = validators.Draft7Validator
1709-
valid = {}, {}
1710+
valid: typing.Tuple[dict, dict] = {}, {}
17101711
invalid = {"type": "integer"}, "foo"
17111712

17121713

17131714
class TestDraft201909Validator(ValidatorTestMixin, TestCase):
17141715
Validator = validators.Draft201909Validator
1715-
valid = {}, {}
1716+
valid: typing.Tuple[dict, dict] = {}, {}
17161717
invalid = {"type": "integer"}, "foo"
17171718

17181719

17191720
class TestDraft202012Validator(ValidatorTestMixin, TestCase):
17201721
Validator = validators.Draft202012Validator
1721-
valid = {}, {}
1722+
valid: typing.Tuple[dict, dict] = {}, {}
17221723
invalid = {"type": "integer"}, "foo"
17231724

17241725

jsonschema/validators.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import contextlib
1111
import json
1212
import reprlib
13+
import typing
1314
import warnings
1415

1516
import attr
@@ -22,9 +23,9 @@
2223
exceptions,
2324
)
2425

25-
_VALIDATORS = {}
26+
_VALIDATORS: typing.Dict[str, typing.Any] = {}
2627
_META_SCHEMAS = _utils.URIDict()
27-
_VOCABULARIES = []
28+
_VOCABULARIES: typing.List[typing.Tuple[str, typing.Any]] = []
2829

2930

3031
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 =

0 commit comments

Comments
 (0)