Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setup mypy in tox -e typing and get it to pass #892

Merged
merged 9 commits into from
Jan 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -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:
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ jobs:
toxenv: secrets
- name: 3.9
toxenv: style
- name: 3.9
toxenv: typing
exclude:
- os: windows-latest
python-version:
Expand Down
16 changes: 8 additions & 8 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@
#
alabaster==0.7.12
# via sphinx
attrs==21.2.0
attrs==21.4.0
# via jsonschema
babel==2.9.1
# via sphinx
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
Expand All @@ -28,15 +28,15 @@ 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
packaging==21.3
# via sphinx
pyenchant==3.2.2
# via sphinxcontrib-spelling
pygments==2.10.0
pygments==2.11.2
# via
# furo
# sphinx
Expand All @@ -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
Expand All @@ -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
Expand Down
11 changes: 10 additions & 1 deletion jsonschema/_format.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from __future__ import annotations

from contextlib import suppress
from uuid import UUID
import datetime
import ipaddress
import re
import typing

from jsonschema.exceptions import FormatError

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

checkers = {}
checkers: dict[
str,
tuple[
typing.Callable[[typing.Any], bool],
Exception | tuple[Exception, ...],
],
] = {}

def __init__(self, formats=None):
if formats is None:
Expand Down
30 changes: 29 additions & 1 deletion jsonschema/_types.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
from __future__ import annotations

import numbers
import typing

from pyrsistent import pmap
import attr

from jsonschema.exceptions import UndefinedTypeCheck


# 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: 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):
return isinstance(instance, list)

Expand Down Expand Up @@ -60,7 +82,13 @@ class TypeChecker(object):

The initial mapping of types to their checking functions.
"""
_type_checkers = attr.ib(default=pmap(), converter=pmap)

_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):
"""
Expand Down
2 changes: 1 addition & 1 deletion jsonschema/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion jsonschema/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 4 additions & 2 deletions jsonschema/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
Validation errors, and some surrounding helpers.
"""
from __future__ import annotations

from collections import defaultdict, deque
from pprint import pformat
from textwrap import dedent, indent
Expand All @@ -10,8 +12,8 @@

from jsonschema import _utils

WEAK_MATCHES = frozenset(["anyOf", "oneOf"])
STRONG_MATCHES = frozenset()
WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"])
STRONG_MATCHES: frozenset[str] = frozenset()

_unset = _utils.Unset()

Expand Down
35 changes: 25 additions & 10 deletions jsonschema/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,30 @@
# 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

try:
from typing import TYPE_CHECKING, Any, ClassVar, Iterator
import sys

# 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 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

Expand Down Expand Up @@ -62,16 +77,16 @@ 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: 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: jsonschema.FormatChecker | None = None,
) -> None:
...

Expand Down
2 changes: 1 addition & 1 deletion jsonschema/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 8 additions & 6 deletions jsonschema/tests/test_validators.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from collections import deque, namedtuple
from contextlib import contextmanager
from decimal import Decimal
Expand Down Expand Up @@ -1662,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 = {}, {}
valid: tuple[dict, dict] = ({}, {})
invalid = {"type": "integer"}, "foo"

def test_any_type_is_valid_for_type_any(self):
Expand Down Expand Up @@ -1694,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 = {}, {}
valid: tuple[dict, dict] = ({}, {})
invalid = {"type": "integer"}, "foo"


class TestDraft6Validator(ValidatorTestMixin, TestCase):
Validator = validators.Draft6Validator
valid = {}, {}
valid: tuple[dict, dict] = ({}, {})
invalid = {"type": "integer"}, "foo"


class TestDraft7Validator(ValidatorTestMixin, TestCase):
Validator = validators.Draft7Validator
valid = {}, {}
valid: tuple[dict, dict] = ({}, {})
invalid = {"type": "integer"}, "foo"


class TestDraft201909Validator(ValidatorTestMixin, TestCase):
Validator = validators.Draft201909Validator
valid = {}, {}
valid: tuple[dict, dict] = ({}, {})
invalid = {"type": "integer"}, "foo"


class TestDraft202012Validator(ValidatorTestMixin, TestCase):
Validator = validators.Draft202012Validator
valid = {}, {}
valid: tuple[dict, dict] = ({}, {})
invalid = {"type": "integer"}, "foo"


Expand Down
7 changes: 5 additions & 2 deletions jsonschema/validators.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,6 +12,7 @@
import contextlib
import json
import reprlib
import typing
import warnings

import attr
Expand All @@ -22,9 +25,9 @@
exceptions,
)

_VALIDATORS = {}
_VALIDATORS: dict[str, typing.Any] = {}
_META_SCHEMAS = _utils.URIDict()
_VOCABULARIES = []
_VOCABULARIES: list[tuple[str, typing.Any]] = []


def __getattr__(name):
Expand Down
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
10 changes: 10 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ envlist =
safety
secrets
style
typing
docs-{html,doctest,linkcheck,spelling,style}
skipsdist = True

Expand Down Expand Up @@ -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 =
Expand Down