Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/jsonschema/exceptions.py: 41%

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

180 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 

10import heapq 

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 Iterable, Mapping, MutableMapping, Sequence 

20 

21 from jsonschema import _types 

22 

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

24STRONG_MATCHES: frozenset[str] = frozenset() 

25 

26_unset = _utils.Unset() 

27 

28 

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

30 """ 

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

32 """ 

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

34 

35 

36def __getattr__(name): 

37 if name == "RefResolutionError": 

38 warnings.warn( 

39 _RefResolutionError._DEPRECATION_MESSAGE, 

40 DeprecationWarning, 

41 stacklevel=2, 

42 ) 

43 return _RefResolutionError 

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

45 

46 

47class _Error(Exception): 

48 

49 _word_for_schema_in_error_message: ClassVar[str] 

50 _word_for_instance_in_error_message: ClassVar[str] 

51 

52 def __init__( 

53 self, 

54 message: str, 

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

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

57 cause: Exception | None = None, 

58 context=(), 

59 validator_value: Any = _unset, 

60 instance: Any = _unset, 

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

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

63 parent: _Error | None = None, 

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

65 ) -> None: 

66 super().__init__( 

67 message, 

68 validator, 

69 path, 

70 cause, 

71 context, 

72 validator_value, 

73 instance, 

74 schema, 

75 schema_path, 

76 parent, 

77 ) 

78 self.message = message 

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

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

81 self.context = list(context) 

82 self.cause = self.__cause__ = cause 

83 self.validator = validator 

84 self.validator_value = validator_value 

85 self.instance = instance 

86 self.schema = schema 

87 self.parent = parent 

88 self._type_checker = type_checker 

89 

90 for error in context: 

91 error.parent = self 

92 

93 def __repr__(self) -> str: 

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

95 

96 def __str__(self) -> str: 

97 essential_for_verbose = ( 

98 self.validator, self.validator_value, self.instance, self.schema, 

99 ) 

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

101 return self.message 

102 

103 schema_path = _utils.format_as_index( 

104 container=self._word_for_schema_in_error_message, 

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

106 ) 

107 instance_path = _utils.format_as_index( 

108 container=self._word_for_instance_in_error_message, 

109 indices=self.relative_path, 

110 ) 

111 prefix = 16 * " " 

112 

113 return dedent( 

114 f"""\ 

115 {self.message} 

116 

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

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

119 

120 On {instance_path}: 

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

122 """.rstrip(), 

123 ) 

124 

125 @classmethod 

126 def create_from(cls, other: _Error): 

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

128 

129 @property 

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

131 parent = self.parent 

132 if parent is None: 

133 return self.relative_path 

134 

135 path = deque(self.relative_path) 

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

137 return path 

138 

139 @property 

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

141 parent = self.parent 

142 if parent is None: 

143 return self.relative_schema_path 

144 

145 path = deque(self.relative_schema_path) 

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

147 return path 

148 

149 @property 

150 def json_path(self) -> str: 

151 path = "$" 

152 for elem in self.absolute_path: 

153 if isinstance(elem, int): 

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

155 else: 

156 path += "." + elem 

157 return path 

158 

159 def _set( 

160 self, 

161 type_checker: _types.TypeChecker | None = None, 

162 **kwargs: Any, 

163 ) -> None: 

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

165 self._type_checker = type_checker 

166 

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

168 if getattr(self, k) is _unset: 

169 setattr(self, k, v) 

170 

171 def _contents(self): 

172 attrs = ( 

173 "message", "cause", "context", "validator", "validator_value", 

174 "path", "schema_path", "instance", "schema", "parent", 

175 ) 

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

177 

178 def _matches_type(self) -> bool: 

179 try: 

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

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

182 except (KeyError, TypeError): 

183 return False 

184 

185 if isinstance(expected, str): 

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

187 

188 return any( 

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

190 for expected_type in expected 

191 ) 

192 

193 

194class ValidationError(_Error): 

195 """ 

196 An instance was invalid under a provided schema. 

197 """ 

198 

199 _word_for_schema_in_error_message = "schema" 

200 _word_for_instance_in_error_message = "instance" 

201 

202 

203class SchemaError(_Error): 

204 """ 

205 A schema was invalid under its corresponding metaschema. 

206 """ 

207 

208 _word_for_schema_in_error_message = "metaschema" 

209 _word_for_instance_in_error_message = "schema" 

210 

211 

212@define(slots=False) 

213class _RefResolutionError(Exception): 

214 """ 

215 A ref could not be resolved. 

216 """ 

217 

218 _DEPRECATION_MESSAGE = ( 

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

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

221 "directly catch referencing.exceptions.Unresolvable." 

222 ) 

223 

224 _cause: Exception 

225 

226 def __eq__(self, other): 

227 if self.__class__ is not other.__class__: 

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

229 return self._cause == other._cause 

230 

231 def __str__(self) -> str: 

232 return str(self._cause) 

233 

234 

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

236 def __init__(self, cause: _Unresolvable): 

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

238 

239 def __eq__(self, other): 

240 if other.__class__ is self.__class__: 

241 return self._wrapped == other._wrapped 

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

243 return self._wrapped == other 

244 return NotImplemented 

245 

246 def __getattr__(self, attr): 

247 return getattr(self._wrapped, attr) 

248 

249 def __hash__(self): 

250 return hash(self._wrapped) 

251 

252 def __repr__(self): 

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

254 

255 def __str__(self): 

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

257 

258 

259class UndefinedTypeCheck(Exception): 

260 """ 

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

262 """ 

263 

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

265 self.type = type 

266 

267 def __str__(self) -> str: 

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

269 

270 

271class UnknownType(Exception): 

272 """ 

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

274 """ 

275 

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

277 self.type = type 

278 self.instance = instance 

279 self.schema = schema 

280 

281 def __str__(self): 

282 prefix = 16 * " " 

283 

284 return dedent( 

285 f"""\ 

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

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

288 

289 While checking instance: 

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

291 """.rstrip(), 

292 ) 

293 

294 

295class FormatError(Exception): 

296 """ 

297 Validating a format failed. 

298 """ 

299 

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

301 super().__init__(message, cause) 

302 self.message = message 

303 self.cause = self.__cause__ = cause 

304 

305 def __str__(self): 

306 return self.message 

307 

308 

309class ErrorTree: 

310 """ 

311 ErrorTrees make it easier to check which validations failed. 

312 """ 

313 

314 _instance = _unset 

315 

316 def __init__(self, errors: Iterable[ValidationError] = ()): 

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

318 self._contents: Mapping[str, ErrorTree] = defaultdict(self.__class__) 

319 

320 for error in errors: 

321 container = self 

322 for element in error.path: 

323 container = container[element] 

324 container.errors[error.validator] = error 

325 

326 container._instance = error.instance 

327 

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

329 """ 

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

331 """ 

332 return index in self._contents 

333 

334 def __getitem__(self, index): 

335 """ 

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

337 

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

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

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

341 some subclass of `LookupError`. 

342 """ 

343 if self._instance is not _unset and index not in self: 

344 self._instance[index] 

345 return self._contents[index] 

346 

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

348 """ 

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

350 

351 .. deprecated:: v4.20.0 

352 

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

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

355 construct the tree. 

356 """ 

357 warnings.warn( 

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

359 DeprecationWarning, 

360 stacklevel=2, 

361 ) 

362 self._contents[index] = value # type: ignore[index] 

363 

364 def __iter__(self): 

365 """ 

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

367 """ 

368 return iter(self._contents) 

369 

370 def __len__(self): 

371 """ 

372 Return the `total_errors`. 

373 """ 

374 return self.total_errors 

375 

376 def __repr__(self): 

377 total = len(self) 

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

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

380 

381 @property 

382 def total_errors(self): 

383 """ 

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

385 """ 

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

387 return len(self.errors) + child_errors 

388 

389 

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

391 """ 

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

393 

394 Arguments: 

395 weak (set): 

396 a collection of validation keywords to consider to be 

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

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

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

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

401 superseded by other same-level validation errors. 

402 

403 strong (set): 

404 a collection of validation keywords to consider to be 

405 "strong" 

406 

407 """ 

408 

409 def relevance(error): 

410 validator = error.validator 

411 return ( # prefer errors which are ... 

412 -len(error.path), # 'deeper' and thereby more specific 

413 error.path, # earlier (for sibling errors) 

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

415 validator in strong, # for a high priority keyword 

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

417 ) # otherwise we'll treat them the same 

418 

419 return relevance 

420 

421 

422relevance = by_relevance() 

423""" 

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

425 

426Example: 

427 

428.. code:: python 

429 

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

431""" 

432 

433 

434def best_match(errors, key=relevance): 

435 """ 

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

437 

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

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

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

441 

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

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

444 since these keywords only need to match once, and any other errors 

445 may not be relevant. 

446 

447 Arguments: 

448 errors (collections.abc.Iterable): 

449 

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

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

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

453 sensical output. 

454 

455 key (collections.abc.Callable): 

456 

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

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

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

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

461 that rates errors but still want the error context descent 

462 done by this function. 

463 

464 Returns: 

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

466 

467 .. note:: 

468 

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

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

471 

472 """ 

473 best = max(errors, key=key, default=None) 

474 if best is None: 

475 return 

476 

477 while best.context: 

478 # Calculate the minimum via nsmallest, because we don't recurse if 

479 # all nested errors have the same relevance (i.e. if min == max == all) 

480 smallest = heapq.nsmallest(2, best.context, key=key) 

481 if len(smallest) == 2 and key(smallest[0]) == key(smallest[1]): # noqa: PLR2004 

482 return best 

483 best = smallest[0] 

484 return best