1from __future__ import annotations
2
3from typing import Any, Callable, Mapping
4import numbers
5
6from attrs import evolve, field, frozen
7from rpds import HashTrieMap
8
9from jsonschema.exceptions import UndefinedTypeCheck
10
11
12# unfortunately, the type of HashTrieMap is generic, and if used as an attrs
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
16def _typed_map_converter(
17 init_val: Mapping[str, Callable[[TypeChecker, Any], bool]],
18) -> HashTrieMap[str, Callable[[TypeChecker, Any], bool]]:
19 return HashTrieMap.convert(init_val)
20
21
22def is_array(checker, instance):
23 return isinstance(instance, list)
24
25
26def is_bool(checker, instance):
27 return isinstance(instance, bool)
28
29
30def is_integer(checker, instance):
31 # bool inherits from int, so ensure bools aren't reported as ints
32 if isinstance(instance, bool):
33 return False
34 return isinstance(instance, int)
35
36
37def is_null(checker, instance):
38 return instance is None
39
40
41def is_number(checker, instance):
42 # bool inherits from int, so ensure bools aren't reported as ints
43 if isinstance(instance, bool):
44 return False
45 return isinstance(instance, numbers.Number)
46
47
48def is_object(checker, instance):
49 return isinstance(instance, dict)
50
51
52def is_string(checker, instance):
53 return isinstance(instance, str)
54
55
56def is_any(checker, instance):
57 return True
58
59
60@frozen(repr=False)
61class TypeChecker:
62 """
63 A :kw:`type` property checker.
64
65 A `TypeChecker` performs type checking for a `Validator`, converting
66 between the defined JSON Schema types and some associated Python types or
67 objects.
68
69 Modifying the behavior just mentioned by redefining which Python objects
70 are considered to be of which JSON Schema types can be done using
71 `TypeChecker.redefine` or `TypeChecker.redefine_many`, and types can be
72 removed via `TypeChecker.remove`. Each of these return a new `TypeChecker`.
73
74 Arguments:
75
76 type_checkers:
77
78 The initial mapping of types to their checking functions.
79
80 """
81
82 _type_checkers: HashTrieMap[
83 str, Callable[[TypeChecker, Any], bool],
84 ] = field(default=HashTrieMap(), converter=_typed_map_converter)
85
86 def __repr__(self):
87 types = ", ".join(repr(k) for k in sorted(self._type_checkers))
88 return f"<{self.__class__.__name__} types={{{types}}}>"
89
90 def is_type(self, instance, type: str) -> bool:
91 """
92 Check if the instance is of the appropriate type.
93
94 Arguments:
95
96 instance:
97
98 The instance to check
99
100 type:
101
102 The name of the type that is expected.
103
104 Raises:
105
106 `jsonschema.exceptions.UndefinedTypeCheck`:
107
108 if ``type`` is unknown to this object.
109
110 """
111 try:
112 fn = self._type_checkers[type]
113 except KeyError:
114 raise UndefinedTypeCheck(type) from None
115
116 return fn(self, instance)
117
118 def redefine(self, type: str, fn) -> TypeChecker:
119 """
120 Produce a new checker with the given type redefined.
121
122 Arguments:
123
124 type:
125
126 The name of the type to check.
127
128 fn (collections.abc.Callable):
129
130 A callable taking exactly two parameters - the type
131 checker calling the function and the instance to check.
132 The function should return true if instance is of this
133 type and false otherwise.
134
135 """
136 return self.redefine_many({type: fn})
137
138 def redefine_many(self, definitions=()) -> TypeChecker:
139 """
140 Produce a new checker with the given types redefined.
141
142 Arguments:
143
144 definitions (dict):
145
146 A dictionary mapping types to their checking functions.
147
148 """
149 type_checkers = self._type_checkers.update(definitions)
150 return evolve(self, type_checkers=type_checkers)
151
152 def remove(self, *types) -> TypeChecker:
153 """
154 Produce a new checker with the given types forgotten.
155
156 Arguments:
157
158 types:
159
160 the names of the types to remove.
161
162 Raises:
163
164 `jsonschema.exceptions.UndefinedTypeCheck`:
165
166 if any given type is unknown to this object
167
168 """
169 type_checkers = self._type_checkers
170 for each in types:
171 try:
172 type_checkers = type_checkers.remove(each)
173 except KeyError:
174 raise UndefinedTypeCheck(each) from None
175 return evolve(self, type_checkers=type_checkers)
176
177
178draft3_type_checker = TypeChecker(
179 {
180 "any": is_any,
181 "array": is_array,
182 "boolean": is_bool,
183 "integer": is_integer,
184 "object": is_object,
185 "null": is_null,
186 "number": is_number,
187 "string": is_string,
188 },
189)
190draft4_type_checker = draft3_type_checker.remove("any")
191draft6_type_checker = draft4_type_checker.redefine(
192 "integer",
193 lambda checker, instance: (
194 is_integer(checker, instance)
195 or isinstance(instance, float) and instance.is_integer()
196 ),
197)
198draft7_type_checker = draft6_type_checker
199draft201909_type_checker = draft7_type_checker
200draft202012_type_checker = draft201909_type_checker