Coverage for /pythoncovmergedfiles/medio/medio/src/jsonschema/jsonschema/exceptions.py: 52%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

205 statements  

1""" 

2Validation errors, and some surrounding helpers. 

3""" 

4from __future__ import annotations 

5 

6from collections import defaultdict, deque 

7from pprint import pformat 

8from textwrap import dedent, indent 

9from typing import TYPE_CHECKING, Any, ClassVar, TypeVar 

10import re 

11import warnings 

12 

13from attrs import define 

14from referencing.exceptions import Unresolvable as _Unresolvable 

15 

16from jsonschema import _utils 

17 

18if TYPE_CHECKING: 

19 from collections.abc import ( 

20 Callable, 

21 Iterable, 

22 Mapping, 

23 MutableMapping, 

24 Sequence, 

25 ) 

26 

27 from jsonschema import _types 

28 

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

30STRONG_MATCHES: frozenset[str] = frozenset() 

31 

32_JSON_PATH_COMPATIBLE_PROPERTY_PATTERN = re.compile("^[a-zA-Z][a-zA-Z0-9_]*$") 

33 

34_unset = _utils.Unset() 

35 

36 

37def _pretty(thing: Any, prefix: str): 

38 """ 

39 Format something for an error message as prettily as we currently can. 

40 """ 

41 return indent(pformat(thing, width=72, sort_dicts=False), prefix).lstrip() 

42 

43 

44def __getattr__(name): 

45 if name == "RefResolutionError": 

46 warnings.warn( 

47 _RefResolutionError._DEPRECATION_MESSAGE, 

48 DeprecationWarning, 

49 stacklevel=2, 

50 ) 

51 return _RefResolutionError 

52 raise AttributeError(f"module {__name__} has no attribute {name}") 

53 

54 

55class _Error(Exception): 

56 

57 _word_for_schema_in_error_message: ClassVar[str] 

58 _word_for_instance_in_error_message: ClassVar[str] 

59 

60 def __init__( 

61 self, 

62 message: str, 

63 validator: str = _unset, # type: ignore[assignment] 

64 path: Iterable[str | int] = (), 

65 cause: Exception | None = None, 

66 context=(), 

67 validator_value: Any = _unset, 

68 instance: Any = _unset, 

69 schema: Mapping[str, Any] | bool = _unset, # type: ignore[assignment] 

70 schema_path: Iterable[str | int] = (), 

71 parent: _Error | None = None, 

72 type_checker: _types.TypeChecker = _unset, # type: ignore[assignment] 

73 ) -> None: 

74 super().__init__( 

75 message, 

76 validator, 

77 path, 

78 cause, 

79 context, 

80 validator_value, 

81 instance, 

82 schema, 

83 schema_path, 

84 parent, 

85 ) 

86 self.message = message 

87 self.path = self.relative_path = deque(path) 

88 self.schema_path = self.relative_schema_path = deque(schema_path) 

89 self.context = list(context) 

90 self.cause = self.__cause__ = cause 

91 self.validator = validator 

92 self.validator_value = validator_value 

93 self.instance = instance 

94 self.schema = schema 

95 self.parent = parent 

96 self._type_checker = type_checker 

97 

98 for error in context: 

99 error.parent = self 

100 

101 def __repr__(self) -> str: 

102 return f"<{self.__class__.__name__}: {self.message!r}>" 

103 

104 def __str__(self) -> str: 

105 essential_for_verbose = ( 

106 self.validator, self.validator_value, self.instance, self.schema, 

107 ) 

108 if any(m is _unset for m in essential_for_verbose): 

109 return self.message 

110 

111 schema_path = _utils.format_as_index( 

112 container=self._word_for_schema_in_error_message, 

113 indices=list(self.relative_schema_path)[:-1], 

114 ) 

115 instance_path = _utils.format_as_index( 

116 container=self._word_for_instance_in_error_message, 

117 indices=self.relative_path, 

118 ) 

119 prefix = 16 * " " 

120 

121 return dedent( 

122 f"""\ 

123 {self.message} 

124 

125 Failed validating {self.validator!r} in {schema_path}: 

126 {_pretty(self.schema, prefix=prefix)} 

127 

128 On {instance_path}: 

129 {_pretty(self.instance, prefix=prefix)} 

130 """.rstrip(), 

131 ) 

132 

133 @classmethod 

134 def create_from(cls, other: _Error): 

135 return cls(**other._contents()) 

136 

137 @property 

138 def absolute_path(self) -> Sequence[str | int]: 

139 parent = self.parent 

140 if parent is None: 

141 return self.relative_path 

142 

143 path = deque(self.relative_path) 

144 path.extendleft(reversed(parent.absolute_path)) 

145 return path 

146 

147 @property 

148 def absolute_schema_path(self) -> Sequence[str | int]: 

149 parent = self.parent 

150 if parent is None: 

151 return self.relative_schema_path 

152 

153 path = deque(self.relative_schema_path) 

154 path.extendleft(reversed(parent.absolute_schema_path)) 

155 return path 

156 

157 @property 

158 def json_path(self) -> str: 

159 path = "$" 

160 for elem in self.absolute_path: 

161 if isinstance(elem, int): 

162 path += "[" + str(elem) + "]" 

163 elif _JSON_PATH_COMPATIBLE_PROPERTY_PATTERN.match(elem): 

164 path += "." + elem 

165 else: 

166 escaped_elem = elem.replace("\\", "\\\\").replace("'", r"\'") 

167 path += "['" + escaped_elem + "']" 

168 return path 

169 

170 def _set( 

171 self, 

172 type_checker: _types.TypeChecker | None = None, 

173 **kwargs: Any, 

174 ) -> None: 

175 if type_checker is not None and self._type_checker is _unset: 

176 self._type_checker = type_checker 

177 

178 for k, v in kwargs.items(): 

179 if getattr(self, k) is _unset: 

180 setattr(self, k, v) 

181 

182 def _contents(self): 

183 attrs = ( 

184 "message", "cause", "context", "validator", "validator_value", 

185 "path", "schema_path", "instance", "schema", "parent", 

186 ) 

187 return {attr: getattr(self, attr) for attr in attrs} 

188 

189 def _matches_type(self) -> bool: 

190 try: 

191 # We ignore this as we want to simply crash if this happens 

192 expected = self.schema["type"] # type: ignore[index] 

193 except (KeyError, TypeError): 

194 return False 

195 

196 if isinstance(expected, str): 

197 return self._type_checker.is_type(self.instance, expected) 

198 

199 return any( 

200 self._type_checker.is_type(self.instance, expected_type) 

201 for expected_type in expected 

202 ) 

203 

204 

205_E = TypeVar("_E", bound=_Error) 

206 

207 

208class ValidationError(_Error): 

209 """ 

210 An instance was invalid under a provided schema. 

211 """ 

212 

213 _word_for_schema_in_error_message = "schema" 

214 _word_for_instance_in_error_message = "instance" 

215 

216 

217class SchemaError(_Error): 

218 """ 

219 A schema was invalid under its corresponding metaschema. 

220 """ 

221 

222 _word_for_schema_in_error_message = "metaschema" 

223 _word_for_instance_in_error_message = "schema" 

224 

225 

226@define(slots=False) 

227class _RefResolutionError(Exception): # noqa: PLW1641 

228 """ 

229 A ref could not be resolved. 

230 """ 

231 

232 _DEPRECATION_MESSAGE = ( 

233 "jsonschema.exceptions.RefResolutionError is deprecated as of version " 

234 "4.18.0. If you wish to catch potential reference resolution errors, " 

235 "directly catch referencing.exceptions.Unresolvable." 

236 ) 

237 

238 _cause: Exception 

239 

240 def __eq__(self, other): 

241 if self.__class__ is not other.__class__: 

242 return NotImplemented # pragma: no cover -- uncovered but deprecated # noqa: E501 

243 return self._cause == other._cause 

244 

245 def __str__(self) -> str: 

246 return str(self._cause) 

247 

248 

249class _WrappedReferencingError(_RefResolutionError, _Unresolvable): # pragma: no cover -- partially uncovered but to be removed # noqa: E501 

250 def __init__(self, cause: _Unresolvable): 

251 object.__setattr__(self, "_wrapped", cause) 

252 

253 def __eq__(self, other): 

254 if other.__class__ is self.__class__: 

255 return self._wrapped == other._wrapped 

256 elif other.__class__ is self._wrapped.__class__: 

257 return self._wrapped == other 

258 return NotImplemented 

259 

260 def __getattr__(self, attr): 

261 return getattr(self._wrapped, attr) 

262 

263 def __hash__(self): 

264 return hash(self._wrapped) 

265 

266 def __repr__(self): 

267 return f"<WrappedReferencingError {self._wrapped!r}>" 

268 

269 def __str__(self): 

270 return f"{self._wrapped.__class__.__name__}: {self._wrapped}" 

271 

272 

273class UndefinedTypeCheck(Exception): 

274 """ 

275 A type checker was asked to check a type it did not have registered. 

276 """ 

277 

278 def __init__(self, type: str) -> None: 

279 self.type = type 

280 

281 def __str__(self) -> str: 

282 return f"Type {self.type!r} is unknown to this type checker" 

283 

284 

285class UnknownType(Exception): 

286 """ 

287 A validator was asked to validate an instance against an unknown type. 

288 """ 

289 

290 def __init__(self, type, instance, schema): 

291 self.type = type 

292 self.instance = instance 

293 self.schema = schema 

294 

295 def __str__(self): 

296 prefix = 16 * " " 

297 

298 return dedent( 

299 f"""\ 

300 Unknown type {self.type!r} for validator with schema: 

301 {_pretty(self.schema, prefix=prefix)} 

302 

303 While checking instance: 

304 {_pretty(self.instance, prefix=prefix)} 

305 """.rstrip(), 

306 ) 

307 

308 

309class FormatError(Exception): 

310 """ 

311 Validating a format failed. 

312 """ 

313 

314 def __init__(self, message, cause=None): 

315 super().__init__(message, cause) 

316 self.message = message 

317 self.cause = self.__cause__ = cause 

318 

319 def __str__(self): 

320 return self.message 

321 

322 

323class ErrorTree: 

324 """ 

325 ErrorTrees make it easier to check which validations failed. 

326 

327 Arguments: 

328 

329 errors: 

330 

331 the errors to populate the tree with 

332 

333 instance: 

334 

335 the instance the tree corresponds to, if known, which 

336 enables indexing the tree at indices not present in it to 

337 raise the same error that indexing the instance itself would 

338 

339 """ 

340 

341 def __init__( 

342 self, 

343 errors: Iterable[ValidationError] = (), 

344 *, 

345 instance: Any = _unset, 

346 ): 

347 self.errors: MutableMapping[str, ValidationError] = {} 

348 self._contents: MutableMapping[str | int, ErrorTree] = {} 

349 self._instance = instance 

350 

351 for error in errors: 

352 container = self 

353 for element in error.path: 

354 container = container._contents.setdefault( 

355 element, self.__class__(), 

356 ) 

357 container.errors[error.validator] = error 

358 

359 container._instance = error.instance 

360 

361 def __contains__(self, index: str | int): 

362 """ 

363 Check whether ``instance[index]`` has any errors. 

364 """ 

365 return index in self._contents 

366 

367 def __getitem__(self, index): 

368 """ 

369 Retrieve the child tree one level down at the given ``index``. 

370 

371 If the index is not in the instance that this tree corresponds 

372 to and is not known by this tree, whatever error would be raised 

373 by ``instance.__getitem__`` will be propagated (usually this is 

374 some subclass of `LookupError`. 

375 """ 

376 if index in self: 

377 return self._contents[index] 

378 if self._instance is _unset: 

379 return self.__class__() 

380 return self.__class__(instance=self._instance[index]) 

381 

382 def __setitem__(self, index: str | int, value: ErrorTree): 

383 """ 

384 Add an error to the tree at the given ``index``. 

385 

386 .. deprecated:: v4.20.0 

387 

388 Setting items on an `ErrorTree` is deprecated without replacement. 

389 To populate a tree, provide all of its sub-errors when you 

390 construct the tree. 

391 """ 

392 warnings.warn( 

393 "ErrorTree.__setitem__ is deprecated without replacement.", 

394 DeprecationWarning, 

395 stacklevel=2, 

396 ) 

397 self._contents[index] = value 

398 

399 def __iter__(self): 

400 """ 

401 Iterate (non-recursively) over the indices in the instance with errors. 

402 """ 

403 return iter(self._contents) 

404 

405 def __len__(self): 

406 """ 

407 Return the `total_errors`. 

408 """ 

409 return self.total_errors 

410 

411 def __repr__(self): 

412 total = len(self) 

413 errors = "error" if total == 1 else "errors" 

414 return f"<{self.__class__.__name__} ({total} total {errors})>" 

415 

416 @property 

417 def total_errors(self): 

418 """ 

419 The total number of errors in the entire tree, including children. 

420 """ 

421 child_errors = sum(len(tree) for _, tree in self._contents.items()) 

422 return len(self.errors) + child_errors 

423 

424 

425def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES): 

426 """ 

427 Create a key function that can be used to sort errors by relevance. 

428 

429 Arguments: 

430 weak (set): 

431 a collection of validation keywords to consider to be 

432 "weak". If there are two errors at the same level of the 

433 instance and one is in the set of weak validation keywords, 

434 the other error will take priority. By default, :kw:`anyOf` 

435 and :kw:`oneOf` are considered weak keywords and will be 

436 superseded by other same-level validation errors. 

437 

438 strong (set): 

439 a collection of validation keywords to consider to be 

440 "strong" 

441 

442 """ 

443 

444 def relevance(error): 

445 validator = error.validator 

446 return ( # prefer errors which are ... 

447 -len(error.path), # shorter path thereby more general 

448 validator not in weak, # for a non-low-priority keyword 

449 validator in strong, # for a high priority keyword 

450 not error._matches_type(), # at least match the instance's type 

451 ) # otherwise we'll treat them the same 

452 

453 return relevance 

454 

455 

456relevance = by_relevance() 

457""" 

458A key function (e.g. to use with `sorted`) which sorts errors by relevance. 

459 

460Example: 

461 

462.. code:: python 

463 

464 sorted(validator.iter_errors(12), key=jsonschema.exceptions.relevance) 

465""" 

466 

467 

468def best_match(errors, key=relevance): 

469 """ 

470 Try to find an error that appears to be the best match among given errors. 

471 

472 In general, errors that are higher up in the instance (i.e. for which 

473 `ValidationError.path` is shorter) are considered better matches, 

474 since they indicate "more" is wrong with the instance. 

475 

476 If the resulting match is either :kw:`oneOf` or :kw:`anyOf`, the 

477 *opposite* assumption is made -- i.e. the deepest error is picked 

478 among the most relevant errors in each separate subschema (preferring 

479 subschemas which produced fewer errors when tied), since these 

480 keywords only need to match once, and any other errors may not be 

481 relevant. 

482 

483 Arguments: 

484 errors (collections.abc.Iterable): 

485 

486 the errors to select from. Do not provide a mixture of 

487 errors from different validation attempts (i.e. from 

488 different instances or schemas), since it won't produce 

489 sensical output. 

490 

491 key (collections.abc.Callable): 

492 

493 the key to use when sorting errors. See `relevance` and 

494 transitively `by_relevance` for more details (the default is 

495 to sort with the defaults of that function). Changing the 

496 default is only useful if you want to change the function 

497 that rates errors but still want the error context descent 

498 done by this function. 

499 

500 Returns: 

501 the best matching error, or ``None`` if the iterable was empty 

502 

503 .. note:: 

504 

505 This function is a heuristic. Its return value may change for a given 

506 set of inputs from version to version if better heuristics are added. 

507 

508 """ 

509 _, best = _most_relevant(errors, key=key) 

510 if best is None: 

511 return 

512 

513 while best.context: 

514 # Group the errors by the subschema which produced them. 

515 by_subschema: dict[Any, list[_Error]] = defaultdict(list) 

516 for error in best.context: 

517 index = error.schema_path[0] if error.schema_path else None 

518 by_subschema[index].append(error) 

519 

520 # Rank each subschema by how deep its most relevant error is, 

521 # and amongst those equally deep, by how few errors it produced 

522 # (i.e. how close it was to being valid). Lower ranks are better. 

523 best_rank, best_in_subschema, tied = None, None, False 

524 for errors_in_subschema in by_subschema.values(): 

525 error_key, error = _most_relevant(errors_in_subschema, key=key) 

526 rank = error_key, len(errors_in_subschema) 

527 if best_rank is None or rank < best_rank: 

528 best_rank, best_in_subschema, tied = rank, error, False 

529 elif rank == best_rank: 

530 tied = True 

531 

532 # If multiple subschemas rank equally we can't tell which was 

533 # intended, so we stop here rather than descend into one of them. 

534 if tied: 

535 break 

536 best = best_in_subschema 

537 return best 

538 

539 

540def _most_relevant( 

541 errors: Iterable[_E], 

542 key: Callable[[_E], Any], 

543) -> tuple[Any, _E | None]: 

544 """ 

545 Find the most relevant error along with its key, computing each key once. 

546 

547 Equally relevant errors are settled by picking the one which appears 

548 earlier in the instance, which makes the choice independent of the 

549 order in which the errors happened to be produced. 

550 

551 Returns ``(None, None)`` if there were no errors. 

552 """ 

553 best_key, best = None, None 

554 for error in errors: 

555 error_key = key(error) 

556 if ( 

557 best is None 

558 or error_key > best_key 

559 or (error_key == best_key and error.path < best.path) 

560 ): 

561 best_key, best = error_key, error 

562 return best_key, best