_utils.py 10 KB

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