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

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

181 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 itertools 

12import warnings 

13 

14from attrs import define 

15from referencing.exceptions import Unresolvable as _Unresolvable 

16 

17from jsonschema import _utils 

18 

19if TYPE_CHECKING: 

20 from collections.abc import Iterable, Mapping, MutableMapping, Sequence 

21 

22 from jsonschema import _types 

23 

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

25STRONG_MATCHES: frozenset[str] = frozenset() 

26 

27_unset = _utils.Unset() 

28 

29 

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

31 """ 

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

33 """ 

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

35 

36 

37def __getattr__(name): 

38 if name == "RefResolutionError": 

39 warnings.warn( 

40 _RefResolutionError._DEPRECATION_MESSAGE, 

41 DeprecationWarning, 

42 stacklevel=2, 

43 ) 

44 return _RefResolutionError 

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

46 

47 

48class _Error(Exception): 

49 

50 _word_for_schema_in_error_message: ClassVar[str] 

51 _word_for_instance_in_error_message: ClassVar[str] 

52 

53 def __init__( 

54 self, 

55 message: str, 

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

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

58 cause: Exception | None = None, 

59 context=(), 

60 validator_value: Any = _unset, 

61 instance: Any = _unset, 

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

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

64 parent: _Error | None = None, 

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

66 ) -> None: 

67 super().__init__( 

68 message, 

69 validator, 

70 path, 

71 cause, 

72 context, 

73 validator_value, 

74 instance, 

75 schema, 

76 schema_path, 

77 parent, 

78 ) 

79 self.message = message 

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

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

82 self.context = list(context) 

83 self.cause = self.__cause__ = cause 

84 self.validator = validator 

85 self.validator_value = validator_value 

86 self.instance = instance 

87 self.schema = schema 

88 self.parent = parent 

89 self._type_checker = type_checker 

90 

91 for error in context: 

92 error.parent = self 

93 

94 def __repr__(self) -> str: 

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

96 

97 def __str__(self) -> str: 

98 essential_for_verbose = ( 

99 self.validator, self.validator_value, self.instance, self.schema, 

100 ) 

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

102 return self.message 

103 

104 schema_path = _utils.format_as_index( 

105 container=self._word_for_schema_in_error_message, 

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

107 ) 

108 instance_path = _utils.format_as_index( 

109 container=self._word_for_instance_in_error_message, 

110 indices=self.relative_path, 

111 ) 

112 prefix = 16 * " " 

113 

114 return dedent( 

115 f"""\ 

116 {self.message} 

117 

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

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

120 

121 On {instance_path}: 

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

123 """.rstrip(), 

124 ) 

125 

126 @classmethod 

127 def create_from(cls, other: _Error): 

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

129 

130 @property 

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

132 parent = self.parent 

133 if parent is None: 

134 return self.relative_path 

135 

136 path = deque(self.relative_path) 

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

138 return path 

139 

140 @property 

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

142 parent = self.parent 

143 if parent is None: 

144 return self.relative_schema_path 

145 

146 path = deque(self.relative_schema_path) 

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

148 return path 

149 

150 @property 

151 def json_path(self) -> str: 

152 path = "$" 

153 for elem in self.absolute_path: 

154 if isinstance(elem, int): 

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

156 else: 

157 path += "." + elem 

158 return path 

159 

160 def _set( 

161 self, 

162 type_checker: _types.TypeChecker | None = None, 

163 **kwargs: Any, 

164 ) -> None: 

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

166 self._type_checker = type_checker 

167 

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

169 if getattr(self, k) is _unset: 

170 setattr(self, k, v) 

171 

172 def _contents(self): 

173 attrs = ( 

174 "message", "cause", "context", "validator", "validator_value", 

175 "path", "schema_path", "instance", "schema", "parent", 

176 ) 

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

178 

179 def _matches_type(self) -> bool: 

180 try: 

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

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

183 except (KeyError, TypeError): 

184 return False 

185 

186 if isinstance(expected, str): 

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

188 

189 return any( 

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

191 for expected_type in expected 

192 ) 

193 

194 

195class ValidationError(_Error): 

196 """ 

197 An instance was invalid under a provided schema. 

198 """ 

199 

200 _word_for_schema_in_error_message = "schema" 

201 _word_for_instance_in_error_message = "instance" 

202 

203 

204class SchemaError(_Error): 

205 """ 

206 A schema was invalid under its corresponding metaschema. 

207 """ 

208 

209 _word_for_schema_in_error_message = "metaschema" 

210 _word_for_instance_in_error_message = "schema" 

211 

212 

213@define(slots=False) 

214class _RefResolutionError(Exception): 

215 """ 

216 A ref could not be resolved. 

217 """ 

218 

219 _DEPRECATION_MESSAGE = ( 

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

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

222 "directly catch referencing.exceptions.Unresolvable." 

223 ) 

224 

225 _cause: Exception 

226 

227 def __eq__(self, other): 

228 if self.__class__ is not other.__class__: 

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

230 return self._cause == other._cause 

231 

232 def __str__(self) -> str: 

233 return str(self._cause) 

234 

235 

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

237 def __init__(self, cause: _Unresolvable): 

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

239 

240 def __eq__(self, other): 

241 if other.__class__ is self.__class__: 

242 return self._wrapped == other._wrapped 

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

244 return self._wrapped == other 

245 return NotImplemented 

246 

247 def __getattr__(self, attr): 

248 return getattr(self._wrapped, attr) 

249 

250 def __hash__(self): 

251 return hash(self._wrapped) 

252 

253 def __repr__(self): 

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

255 

256 def __str__(self): 

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

258 

259 

260class UndefinedTypeCheck(Exception): 

261 """ 

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

263 """ 

264 

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

266 self.type = type 

267 

268 def __str__(self) -> str: 

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

270 

271 

272class UnknownType(Exception): 

273 """ 

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

275 """ 

276 

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

278 self.type = type 

279 self.instance = instance 

280 self.schema = schema 

281 

282 def __str__(self): 

283 prefix = 16 * " " 

284 

285 return dedent( 

286 f"""\ 

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

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

289 

290 While checking instance: 

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

292 """.rstrip(), 

293 ) 

294 

295 

296class FormatError(Exception): 

297 """ 

298 Validating a format failed. 

299 """ 

300 

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

302 super().__init__(message, cause) 

303 self.message = message 

304 self.cause = self.__cause__ = cause 

305 

306 def __str__(self): 

307 return self.message 

308 

309 

310class ErrorTree: 

311 """ 

312 ErrorTrees make it easier to check which validations failed. 

313 """ 

314 

315 _instance = _unset 

316 

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

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

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

320 

321 for error in errors: 

322 container = self 

323 for element in error.path: 

324 container = container[element] 

325 container.errors[error.validator] = error 

326 

327 container._instance = error.instance 

328 

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

330 """ 

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

332 """ 

333 return index in self._contents 

334 

335 def __getitem__(self, index): 

336 """ 

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

338 

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

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

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

342 some subclass of `LookupError`. 

343 """ 

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

345 self._instance[index] 

346 return self._contents[index] 

347 

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

349 """ 

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

351 

352 .. deprecated:: v4.20.0 

353 

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

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

356 construct the tree. 

357 """ 

358 warnings.warn( 

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

360 DeprecationWarning, 

361 stacklevel=2, 

362 ) 

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

364 

365 def __iter__(self): 

366 """ 

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

368 """ 

369 return iter(self._contents) 

370 

371 def __len__(self): 

372 """ 

373 Return the `total_errors`. 

374 """ 

375 return self.total_errors 

376 

377 def __repr__(self): 

378 total = len(self) 

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

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

381 

382 @property 

383 def total_errors(self): 

384 """ 

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

386 """ 

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

388 return len(self.errors) + child_errors 

389 

390 

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

392 """ 

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

394 

395 Arguments: 

396 weak (set): 

397 a collection of validation keywords to consider to be 

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

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

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

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

402 superseded by other same-level validation errors. 

403 

404 strong (set): 

405 a collection of validation keywords to consider to be 

406 "strong" 

407 

408 """ 

409 

410 def relevance(error): 

411 validator = error.validator 

412 return ( # prefer errors which are ... 

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

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

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

416 validator in strong, # for a high priority keyword 

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

418 ) # otherwise we'll treat them the same 

419 

420 return relevance 

421 

422 

423relevance = by_relevance() 

424""" 

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

426 

427Example: 

428 

429.. code:: python 

430 

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

432""" 

433 

434 

435def best_match(errors, key=relevance): 

436 """ 

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

438 

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

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

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

442 

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

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

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

446 may not be relevant. 

447 

448 Arguments: 

449 errors (collections.abc.Iterable): 

450 

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

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

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

454 sensical output. 

455 

456 key (collections.abc.Callable): 

457 

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

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

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

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

462 that rates errors but still want the error context descent 

463 done by this function. 

464 

465 Returns: 

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

467 

468 .. note:: 

469 

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

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

472 

473 """ 

474 errors = iter(errors) 

475 best = next(errors, None) 

476 if best is None: 

477 return 

478 best = max(itertools.chain([best], errors), key=key) 

479 

480 while best.context: 

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

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

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

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

485 return best 

486 best = smallest[0] 

487 return best