Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/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

185 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 re 

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_JSON_PATH_COMPATIBLE_PROPERTY_PATTERN = re.compile("^[a-zA-Z][a-zA-Z0-9_]*$") 

28 

29_unset = _utils.Unset() 

30 

31 

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

33 """ 

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

35 """ 

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

37 

38 

39def __getattr__(name): 

40 if name == "RefResolutionError": 

41 warnings.warn( 

42 _RefResolutionError._DEPRECATION_MESSAGE, 

43 DeprecationWarning, 

44 stacklevel=2, 

45 ) 

46 return _RefResolutionError 

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

48 

49 

50class _Error(Exception): 

51 

52 _word_for_schema_in_error_message: ClassVar[str] 

53 _word_for_instance_in_error_message: ClassVar[str] 

54 

55 def __init__( 

56 self, 

57 message: str, 

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

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

60 cause: Exception | None = None, 

61 context=(), 

62 validator_value: Any = _unset, 

63 instance: Any = _unset, 

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

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

66 parent: _Error | None = None, 

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

68 ) -> None: 

69 super().__init__( 

70 message, 

71 validator, 

72 path, 

73 cause, 

74 context, 

75 validator_value, 

76 instance, 

77 schema, 

78 schema_path, 

79 parent, 

80 ) 

81 self.message = message 

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

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

84 self.context = list(context) 

85 self.cause = self.__cause__ = cause 

86 self.validator = validator 

87 self.validator_value = validator_value 

88 self.instance = instance 

89 self.schema = schema 

90 self.parent = parent 

91 self._type_checker = type_checker 

92 

93 for error in context: 

94 error.parent = self 

95 

96 def __repr__(self) -> str: 

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

98 

99 def __str__(self) -> str: 

100 essential_for_verbose = ( 

101 self.validator, self.validator_value, self.instance, self.schema, 

102 ) 

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

104 return self.message 

105 

106 schema_path = _utils.format_as_index( 

107 container=self._word_for_schema_in_error_message, 

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

109 ) 

110 instance_path = _utils.format_as_index( 

111 container=self._word_for_instance_in_error_message, 

112 indices=self.relative_path, 

113 ) 

114 prefix = 16 * " " 

115 

116 return dedent( 

117 f"""\ 

118 {self.message} 

119 

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

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

122 

123 On {instance_path}: 

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

125 """.rstrip(), 

126 ) 

127 

128 @classmethod 

129 def create_from(cls, other: _Error): 

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

131 

132 @property 

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

134 parent = self.parent 

135 if parent is None: 

136 return self.relative_path 

137 

138 path = deque(self.relative_path) 

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

140 return path 

141 

142 @property 

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

144 parent = self.parent 

145 if parent is None: 

146 return self.relative_schema_path 

147 

148 path = deque(self.relative_schema_path) 

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

150 return path 

151 

152 @property 

153 def json_path(self) -> str: 

154 path = "$" 

155 for elem in self.absolute_path: 

156 if isinstance(elem, int): 

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

158 elif _JSON_PATH_COMPATIBLE_PROPERTY_PATTERN.match(elem): 

159 path += "." + elem 

160 else: 

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

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

163 return path 

164 

165 def _set( 

166 self, 

167 type_checker: _types.TypeChecker | None = None, 

168 **kwargs: Any, 

169 ) -> None: 

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

171 self._type_checker = type_checker 

172 

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

174 if getattr(self, k) is _unset: 

175 setattr(self, k, v) 

176 

177 def _contents(self): 

178 attrs = ( 

179 "message", "cause", "context", "validator", "validator_value", 

180 "path", "schema_path", "instance", "schema", "parent", 

181 ) 

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

183 

184 def _matches_type(self) -> bool: 

185 try: 

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

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

188 except (KeyError, TypeError): 

189 return False 

190 

191 if isinstance(expected, str): 

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

193 

194 return any( 

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

196 for expected_type in expected 

197 ) 

198 

199 

200class ValidationError(_Error): 

201 """ 

202 An instance was invalid under a provided schema. 

203 """ 

204 

205 _word_for_schema_in_error_message = "schema" 

206 _word_for_instance_in_error_message = "instance" 

207 

208 

209class SchemaError(_Error): 

210 """ 

211 A schema was invalid under its corresponding metaschema. 

212 """ 

213 

214 _word_for_schema_in_error_message = "metaschema" 

215 _word_for_instance_in_error_message = "schema" 

216 

217 

218@define(slots=False) 

219class _RefResolutionError(Exception): # noqa: PLW1641 

220 """ 

221 A ref could not be resolved. 

222 """ 

223 

224 _DEPRECATION_MESSAGE = ( 

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

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

227 "directly catch referencing.exceptions.Unresolvable." 

228 ) 

229 

230 _cause: Exception 

231 

232 def __eq__(self, other): 

233 if self.__class__ is not other.__class__: 

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

235 return self._cause == other._cause 

236 

237 def __str__(self) -> str: 

238 return str(self._cause) 

239 

240 

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

242 def __init__(self, cause: _Unresolvable): 

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

244 

245 def __eq__(self, other): 

246 if other.__class__ is self.__class__: 

247 return self._wrapped == other._wrapped 

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

249 return self._wrapped == other 

250 return NotImplemented 

251 

252 def __getattr__(self, attr): 

253 return getattr(self._wrapped, attr) 

254 

255 def __hash__(self): 

256 return hash(self._wrapped) 

257 

258 def __repr__(self): 

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

260 

261 def __str__(self): 

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

263 

264 

265class UndefinedTypeCheck(Exception): 

266 """ 

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

268 """ 

269 

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

271 self.type = type 

272 

273 def __str__(self) -> str: 

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

275 

276 

277class UnknownType(Exception): 

278 """ 

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

280 """ 

281 

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

283 self.type = type 

284 self.instance = instance 

285 self.schema = schema 

286 

287 def __str__(self): 

288 prefix = 16 * " " 

289 

290 return dedent( 

291 f"""\ 

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

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

294 

295 While checking instance: 

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

297 """.rstrip(), 

298 ) 

299 

300 

301class FormatError(Exception): 

302 """ 

303 Validating a format failed. 

304 """ 

305 

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

307 super().__init__(message, cause) 

308 self.message = message 

309 self.cause = self.__cause__ = cause 

310 

311 def __str__(self): 

312 return self.message 

313 

314 

315class ErrorTree: 

316 """ 

317 ErrorTrees make it easier to check which validations failed. 

318 """ 

319 

320 _instance = _unset 

321 

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

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

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

325 

326 for error in errors: 

327 container = self 

328 for element in error.path: 

329 container = container[element] 

330 container.errors[error.validator] = error 

331 

332 container._instance = error.instance 

333 

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

335 """ 

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

337 """ 

338 return index in self._contents 

339 

340 def __getitem__(self, index): 

341 """ 

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

343 

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

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

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

347 some subclass of `LookupError`. 

348 """ 

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

350 self._instance[index] 

351 return self._contents[index] 

352 

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

354 """ 

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

356 

357 .. deprecated:: v4.20.0 

358 

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

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

361 construct the tree. 

362 """ 

363 warnings.warn( 

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

365 DeprecationWarning, 

366 stacklevel=2, 

367 ) 

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

369 

370 def __iter__(self): 

371 """ 

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

373 """ 

374 return iter(self._contents) 

375 

376 def __len__(self): 

377 """ 

378 Return the `total_errors`. 

379 """ 

380 return self.total_errors 

381 

382 def __repr__(self): 

383 total = len(self) 

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

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

386 

387 @property 

388 def total_errors(self): 

389 """ 

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

391 """ 

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

393 return len(self.errors) + child_errors 

394 

395 

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

397 """ 

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

399 

400 Arguments: 

401 weak (set): 

402 a collection of validation keywords to consider to be 

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

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

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

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

407 superseded by other same-level validation errors. 

408 

409 strong (set): 

410 a collection of validation keywords to consider to be 

411 "strong" 

412 

413 """ 

414 

415 def relevance(error): 

416 validator = error.validator 

417 return ( # prefer errors which are ... 

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

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

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

421 validator in strong, # for a high priority keyword 

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

423 ) # otherwise we'll treat them the same 

424 

425 return relevance 

426 

427 

428relevance = by_relevance() 

429""" 

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

431 

432Example: 

433 

434.. code:: python 

435 

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

437""" 

438 

439 

440def best_match(errors, key=relevance): 

441 """ 

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

443 

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

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

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

447 

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

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

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

451 may not be relevant. 

452 

453 Arguments: 

454 errors (collections.abc.Iterable): 

455 

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

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

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

459 sensical output. 

460 

461 key (collections.abc.Callable): 

462 

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

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

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

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

467 that rates errors but still want the error context descent 

468 done by this function. 

469 

470 Returns: 

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

472 

473 .. note:: 

474 

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

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

477 

478 """ 

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

480 if best is None: 

481 return 

482 

483 while best.context: 

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

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

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

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

488 return best 

489 best = smallest[0] 

490 return best