1from collections.abc import Mapping, MutableMapping, Sequence
2from urllib.parse import urlsplit
3import itertools
4import re
5
6
7class URIDict(MutableMapping):
8 """
9 Dictionary which uses normalized URIs as keys.
10 """
11
12 def normalize(self, uri):
13 return urlsplit(uri).geturl()
14
15 def __init__(self, *args, **kwargs):
16 self.store = dict()
17 self.store.update(*args, **kwargs)
18
19 def __getitem__(self, uri):
20 return self.store[self.normalize(uri)]
21
22 def __setitem__(self, uri, value):
23 self.store[self.normalize(uri)] = value
24
25 def __delitem__(self, uri):
26 del self.store[self.normalize(uri)]
27
28 def __iter__(self):
29 return iter(self.store)
30
31 def __len__(self): # pragma: no cover -- untested, but to be removed
32 return len(self.store)
33
34 def __repr__(self): # pragma: no cover -- untested, but to be removed
35 return repr(self.store)
36
37
38class Unset:
39 """
40 An as-of-yet unset attribute or unprovided default parameter.
41 """
42
43 def __repr__(self): # pragma: no cover
44 return "<unset>"
45
46
47def format_as_index(container, indices):
48 """
49 Construct a single string containing indexing operations for the indices.
50
51 For example for a container ``bar``, [1, 2, "foo"] -> bar[1][2]["foo"]
52
53 Arguments:
54
55 container (str):
56
57 A word to use for the thing being indexed
58
59 indices (sequence):
60
61 The indices to format.
62
63 """
64 if not indices:
65 return container
66 return f"{container}[{']['.join(repr(index) for index in indices)}]"
67
68
69def find_additional_properties(instance, schema):
70 """
71 Return the set of additional properties for the given ``instance``.
72
73 Weeds out properties that should have been validated by ``properties`` and
74 / or ``patternProperties``.
75
76 Assumes ``instance`` is dict-like already.
77 """
78 properties = schema.get("properties", {})
79 patterns = "|".join(schema.get("patternProperties", {}))
80 for property in instance:
81 if property not in properties:
82 if patterns and re.search(patterns, property):
83 continue
84 yield property
85
86
87def extras_msg(extras):
88 """
89 Create an error message for extra items or properties.
90 """
91 verb = "was" if len(extras) == 1 else "were"
92 return ", ".join(repr(extra) for extra in extras), verb
93
94
95def ensure_list(thing):
96 """
97 Wrap ``thing`` in a list if it's a single str.
98
99 Otherwise, return it unchanged.
100 """
101 if isinstance(thing, str):
102 return [thing]
103 return thing
104
105
106def _mapping_equal(one, two):
107 """
108 Check if two mappings are equal using the semantics of `equal`.
109 """
110 if len(one) != len(two):
111 return False
112 return all(
113 key in two and equal(value, two[key])
114 for key, value in one.items()
115 )
116
117
118def _sequence_equal(one, two):
119 """
120 Check if two sequences are equal using the semantics of `equal`.
121 """
122 if len(one) != len(two):
123 return False
124 return all(equal(i, j) for i, j in zip(one, two))
125
126
127def equal(one, two):
128 """
129 Check if two things are equal evading some Python type hierarchy semantics.
130
131 Specifically in JSON Schema, evade `bool` inheriting from `int`,
132 recursing into sequences to do the same.
133 """
134 if one is two:
135 return True
136 if isinstance(one, str) or isinstance(two, str):
137 return one == two
138 if isinstance(one, Sequence) and isinstance(two, Sequence):
139 return _sequence_equal(one, two)
140 if isinstance(one, Mapping) and isinstance(two, Mapping):
141 return _mapping_equal(one, two)
142 return unbool(one) == unbool(two)
143
144
145def unbool(element, true=object(), false=object()):
146 """
147 A hack to make True and 1 and False and 0 unique for ``uniq``.
148 """
149 if element is True:
150 return true
151 elif element is False:
152 return false
153 return element
154
155
156def uniq(container):
157 """
158 Check if all of a container's elements are unique.
159
160 Tries to rely on the container being recursively sortable, or otherwise
161 falls back on (slow) brute force.
162 """
163 try:
164 sort = sorted(unbool(i) for i in container)
165 sliced = itertools.islice(sort, 1, None)
166
167 for i, j in zip(sort, sliced):
168 if equal(i, j):
169 return False
170
171 except (NotImplementedError, TypeError):
172 seen = []
173 for e in container:
174 e = unbool(e)
175
176 for i in seen:
177 if equal(i, e):
178 return False
179
180 seen.append(e)
181 return True
182
183
184def find_evaluated_item_indexes_by_schema(validator, instance, schema):
185 """
186 Get all indexes of items that get evaluated under the current schema.
187
188 Covers all keywords related to unevaluatedItems: items, prefixItems, if,
189 then, else, contains, unevaluatedItems, allOf, oneOf, anyOf
190 """
191 if validator.is_type(schema, "boolean"):
192 return []
193 evaluated_indexes = []
194
195 if "items" in schema:
196 return list(range(len(instance)))
197
198 ref = schema.get("$ref")
199 if ref is not None:
200 resolved = validator._resolver.lookup(ref)
201 evaluated_indexes.extend(
202 find_evaluated_item_indexes_by_schema(
203 validator.evolve(
204 schema=resolved.contents,
205 _resolver=resolved.resolver,
206 ),
207 instance,
208 resolved.contents,
209 ),
210 )
211
212 dynamicRef = schema.get("$dynamicRef")
213 if dynamicRef is not None:
214 resolved = validator._resolver.lookup(dynamicRef)
215 evaluated_indexes.extend(
216 find_evaluated_item_indexes_by_schema(
217 validator.evolve(
218 schema=resolved.contents,
219 _resolver=resolved.resolver,
220 ),
221 instance,
222 resolved.contents,
223 ),
224 )
225
226 if "prefixItems" in schema:
227 evaluated_indexes += list(range(len(schema["prefixItems"])))
228
229 if "if" in schema:
230 if validator.evolve(schema=schema["if"]).is_valid(instance):
231 evaluated_indexes += find_evaluated_item_indexes_by_schema(
232 validator, instance, schema["if"],
233 )
234 if "then" in schema:
235 evaluated_indexes += find_evaluated_item_indexes_by_schema(
236 validator, instance, schema["then"],
237 )
238 elif "else" in schema:
239 evaluated_indexes += find_evaluated_item_indexes_by_schema(
240 validator, instance, schema["else"],
241 )
242
243 for keyword in ["contains", "unevaluatedItems"]:
244 if keyword in schema:
245 for k, v in enumerate(instance):
246 if validator.evolve(schema=schema[keyword]).is_valid(v):
247 evaluated_indexes.append(k)
248
249 for keyword in ["allOf", "oneOf", "anyOf"]:
250 if keyword in schema:
251 for subschema in schema[keyword]:
252 errs = next(validator.descend(instance, subschema), None)
253 if errs is None:
254 evaluated_indexes += find_evaluated_item_indexes_by_schema(
255 validator, instance, subschema,
256 )
257
258 return evaluated_indexes
259
260
261def find_evaluated_property_keys_by_schema(validator, instance, schema):
262 """
263 Get all keys of items that get evaluated under the current schema.
264
265 Covers all keywords related to unevaluatedProperties: properties,
266 additionalProperties, unevaluatedProperties, patternProperties,
267 dependentSchemas, allOf, oneOf, anyOf, if, then, else
268 """
269 if validator.is_type(schema, "boolean"):
270 return []
271 evaluated_keys = []
272
273 ref = schema.get("$ref")
274 if ref is not None:
275 resolved = validator._resolver.lookup(ref)
276 evaluated_keys.extend(
277 find_evaluated_property_keys_by_schema(
278 validator.evolve(
279 schema=resolved.contents,
280 _resolver=resolved.resolver,
281 ),
282 instance,
283 resolved.contents,
284 ),
285 )
286
287 dynamicRef = schema.get("$dynamicRef")
288 if dynamicRef is not None:
289 resolved = validator._resolver.lookup(dynamicRef)
290 evaluated_keys.extend(
291 find_evaluated_property_keys_by_schema(
292 validator.evolve(
293 schema=resolved.contents,
294 _resolver=resolved.resolver,
295 ),
296 instance,
297 resolved.contents,
298 ),
299 )
300
301 for keyword in [
302 "properties", "additionalProperties", "unevaluatedProperties",
303 ]:
304 if keyword in schema:
305 schema_value = schema[keyword]
306 if validator.is_type(schema_value, "boolean") and schema_value:
307 evaluated_keys += instance.keys()
308
309 elif validator.is_type(schema_value, "object"):
310 for property in schema_value:
311 if property in instance:
312 evaluated_keys.append(property)
313
314 if "patternProperties" in schema:
315 for property in instance:
316 for pattern in schema["patternProperties"]:
317 if re.search(pattern, property):
318 evaluated_keys.append(property)
319
320 if "dependentSchemas" in schema:
321 for property, subschema in schema["dependentSchemas"].items():
322 if property not in instance:
323 continue
324 evaluated_keys += find_evaluated_property_keys_by_schema(
325 validator, instance, subschema,
326 )
327
328 for keyword in ["allOf", "oneOf", "anyOf"]:
329 if keyword in schema:
330 for subschema in schema[keyword]:
331 errs = next(validator.descend(instance, subschema), None)
332 if errs is None:
333 evaluated_keys += find_evaluated_property_keys_by_schema(
334 validator, instance, subschema,
335 )
336
337 if "if" in schema:
338 if validator.evolve(schema=schema["if"]).is_valid(instance):
339 evaluated_keys += find_evaluated_property_keys_by_schema(
340 validator, instance, schema["if"],
341 )
342 if "then" in schema:
343 evaluated_keys += find_evaluated_property_keys_by_schema(
344 validator, instance, schema["then"],
345 )
346 elif "else" in schema:
347 evaluated_keys += find_evaluated_property_keys_by_schema(
348 validator, instance, schema["else"],
349 )
350
351 return evaluated_keys