1""" 2Validation errors, and some surrounding helpers. 3""" 4from collections import defaultdict, deque 5from pprint import pformat 6from textwrap import dedent, indent 7import itertools 8 9import attr 10 11from jsonschema import _utils 12 13WEAK_MATCHES = frozenset(["anyOf", "oneOf"]) 14STRONG_MATCHES = frozenset() 15 16_unset = _utils.Unset() 17 18 19class _Error(Exception): 20 def __init__( 21 self, 22 message, 23 validator=_unset, 24 path=(), 25 cause=None, 26 context=(), 27 validator_value=_unset, 28 instance=_unset, 29 schema=_unset, 30 schema_path=(), 31 parent=None, 32 ): 33 super(_Error, self).__init__( 34 message, 35 validator, 36 path, 37 cause, 38 context, 39 validator_value, 40 instance, 41 schema, 42 schema_path, 43 parent, 44 ) 45 self.message = message 46 self.path = self.relative_path = deque(path) 47 self.schema_path = self.relative_schema_path = deque(schema_path) 48 self.context = list(context) 49 self.cause = self.__cause__ = cause 50 self.validator = validator 51 self.validator_value = validator_value 52 self.instance = instance 53 self.schema = schema 54 self.parent = parent 55 56 for error in context: 57 error.parent = self 58 59 def __repr__(self): 60 return f"<{self.__class__.__name__}: {self.message!r}>" 61 62 def __str__(self): 63 essential_for_verbose = ( 64 self.validator, self.validator_value, self.instance, self.schema, 65 ) 66 if any(m is _unset for m in essential_for_verbose): 67 return self.message 68 69 schema_path = _utils.format_as_index( 70 container=self._word_for_schema_in_error_message, 71 indices=list(self.relative_schema_path)[:-1], 72 ) 73 instance_path = _utils.format_as_index( 74 container=self._word_for_instance_in_error_message, 75 indices=self.relative_path, 76 ) 77 prefix = 16 * " " 78 79 return dedent( 80 f"""\ 81 {self.message} 82 83 Failed validating {self.validator!r} in {schema_path}: 84 {indent(pformat(self.schema, width=72), prefix).lstrip()} 85 86 On {instance_path}: 87 {indent(pformat(self.instance, width=72), prefix).lstrip()} 88 """.rstrip(), 89 ) 90 91 @classmethod 92 def create_from(cls, other): 93 return cls(**other._contents()) 94 95 @property 96 def absolute_path(self): 97 parent = self.parent 98 if parent is None: 99 return self.relative_path 100 101 path = deque(self.relative_path) 102 path.extendleft(reversed(parent.absolute_path)) 103 return path 104 105 @property 106 def absolute_schema_path(self): 107 parent = self.parent 108 if parent is None: 109 return self.relative_schema_path 110 111 path = deque(self.relative_schema_path) 112 path.extendleft(reversed(parent.absolute_schema_path)) 113 return path 114 115 @property 116 def json_path(self): 117 path = "$" 118 for elem in self.absolute_path: 119 if isinstance(elem, int): 120 path += "[" + str(elem) + "]" 121 else: 122 path += "." + elem 123 return path 124 125 def _set(self, **kwargs): 126 for k, v in kwargs.items(): 127 if getattr(self, k) is _unset: 128 setattr(self, k, v) 129 130 def _contents(self): 131 attrs = ( 132 "message", "cause", "context", "validator", "validator_value", 133 "path", "schema_path", "instance", "schema", "parent", 134 ) 135 return dict((attr, getattr(self, attr)) for attr in attrs) 136 137 138class ValidationError(_Error): 139 """ 140 An instance was invalid under a provided schema. 141 """ 142 143 _word_for_schema_in_error_message = "schema" 144 _word_for_instance_in_error_message = "instance" 145 146 147class SchemaError(_Error): 148 """ 149 A schema was invalid under its corresponding metaschema. 150 """ 151 152 _word_for_schema_in_error_message = "metaschema" 153 _word_for_instance_in_error_message = "schema" 154 155 156@attr.s(hash=True) 157class RefResolutionError(Exception): 158 """ 159 A ref could not be resolved. 160 """ 161 162 _cause = attr.ib() 163 164 def __str__(self): 165 return str(self._cause) 166 167 168class UndefinedTypeCheck(Exception): 169 """ 170 A type checker was asked to check a type it did not have registered. 171 """ 172 173 def __init__(self, type): 174 self.type = type 175 176 def __str__(self): 177 return f"Type {self.type!r} is unknown to this type checker" 178 179 180class UnknownType(Exception): 181 """ 182 A validator was asked to validate an instance against an unknown type. 183 """ 184 185 def __init__(self, type, instance, schema): 186 self.type = type 187 self.instance = instance 188 self.schema = schema 189 190 def __str__(self): 191 prefix = 16 * " " 192 193 return dedent( 194 f"""\ 195 Unknown type {self.type!r} for validator with schema: 196 {indent(pformat(self.schema, width=72), prefix).lstrip()} 197 198 While checking instance: 199 {indent(pformat(self.instance, width=72), prefix).lstrip()} 200 """.rstrip(), 201 ) 202 203 204class FormatError(Exception): 205 """ 206 Validating a format failed. 207 """ 208 209 def __init__(self, message, cause=None): 210 super(FormatError, self).__init__(message, cause) 211 self.message = message 212 self.cause = self.__cause__ = cause 213 214 def __str__(self): 215 return self.message 216 217 218class ErrorTree(object): 219 """ 220 ErrorTrees make it easier to check which validations failed. 221 """ 222 223 _instance = _unset 224 225 def __init__(self, errors=()): 226 self.errors = {} 227 self._contents = defaultdict(self.__class__) 228 229 for error in errors: 230 container = self 231 for element in error.path: 232 container = container[element] 233 container.errors[error.validator] = error 234 235 container._instance = error.instance 236 237 def __contains__(self, index): 238 """ 239 Check whether ``instance[index]`` has any errors. 240 """ 241 242 return index in self._contents 243 244 def __getitem__(self, index): 245 """ 246 Retrieve the child tree one level down at the given ``index``. 247 248 If the index is not in the instance that this tree corresponds 249 to and is not known by this tree, whatever error would be raised 250 by ``instance.__getitem__`` will be propagated (usually this is 251 some subclass of `LookupError`. 252 """ 253 254 if self._instance is not _unset and index not in self: 255 self._instance[index] 256 return self._contents[index] 257 258 def __setitem__(self, index, value): 259 """ 260 Add an error to the tree at the given ``index``. 261 """ 262 self._contents[index] = value 263 264 def __iter__(self): 265 """ 266 Iterate (non-recursively) over the indices in the instance with errors. 267 """ 268 269 return iter(self._contents) 270 271 def __len__(self): 272 """ 273 Return the `total_errors`. 274 """ 275 return self.total_errors 276 277 def __repr__(self): 278 return f"<{self.__class__.__name__} ({len(self)} total errors)>" 279 280 @property 281 def total_errors(self): 282 """ 283 The total number of errors in the entire tree, including children. 284 """ 285 286 child_errors = sum(len(tree) for _, tree in self._contents.items()) 287 return len(self.errors) + child_errors 288 289 290def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES): 291 """ 292 Create a key function that can be used to sort errors by relevance. 293 294 Arguments: 295 weak (set): 296 a collection of validator names to consider to be "weak". 297 If there are two errors at the same level of the instance 298 and one is in the set of weak validator names, the other 299 error will take priority. By default, :validator:`anyOf` and 300 :validator:`oneOf` are considered weak validators and will 301 be superseded by other same-level validation errors. 302 303 strong (set): 304 a collection of validator names to consider to be "strong" 305 """ 306 def relevance(error): 307 validator = error.validator 308 return -len(error.path), validator not in weak, validator in strong 309 return relevance 310 311 312relevance = by_relevance() 313 314 315def best_match(errors, key=relevance): 316 """ 317 Try to find an error that appears to be the best match among given errors. 318 319 In general, errors that are higher up in the instance (i.e. for which 320 `ValidationError.path` is shorter) are considered better matches, 321 since they indicate "more" is wrong with the instance. 322 323 If the resulting match is either :validator:`oneOf` or :validator:`anyOf`, 324 the *opposite* assumption is made -- i.e. the deepest error is picked, 325 since these validators only need to match once, and any other errors may 326 not be relevant. 327 328 Arguments: 329 errors (collections.abc.Iterable): 330 331 the errors to select from. Do not provide a mixture of 332 errors from different validation attempts (i.e. from 333 different instances or schemas), since it won't produce 334 sensical output. 335 336 key (collections.abc.Callable): 337 338 the key to use when sorting errors. See `relevance` and 339 transitively `by_relevance` for more details (the default is 340 to sort with the defaults of that function). Changing the 341 default is only useful if you want to change the function 342 that rates errors but still want the error context descent 343 done by this function. 344 345 Returns: 346 the best matching error, or ``None`` if the iterable was empty 347 348 .. note:: 349 350 This function is a heuristic. Its return value may change for a given 351 set of inputs from version to version if better heuristics are added. 352 """ 353 errors = iter(errors) 354 best = next(errors, None) 355 if best is None: 356 return 357 best = max(itertools.chain([best], errors), key=key) 358 359 while best.context: 360 best = min(best.context, key=key) 361 return best 362