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

178 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-22 06:29 +0000

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, 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 

21 

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

23STRONG_MATCHES: frozenset[str] = frozenset() 

24 

25_unset = _utils.Unset() 

26 

27 

28def __getattr__(name): 

29 if name == "RefResolutionError": 

30 warnings.warn( 

31 _RefResolutionError._DEPRECATION_MESSAGE, 

32 DeprecationWarning, 

33 stacklevel=2, 

34 ) 

35 return _RefResolutionError 

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

37 

38 

39class _Error(Exception): 

40 

41 _word_for_schema_in_error_message: ClassVar[str] 

42 _word_for_instance_in_error_message: ClassVar[str] 

43 

44 def __init__( 

45 self, 

46 message: str, 

47 validator=_unset, 

48 path=(), 

49 cause=None, 

50 context=(), 

51 validator_value=_unset, 

52 instance=_unset, 

53 schema=_unset, 

54 schema_path=(), 

55 parent=None, 

56 type_checker=_unset, 

57 ): 

58 super().__init__( 

59 message, 

60 validator, 

61 path, 

62 cause, 

63 context, 

64 validator_value, 

65 instance, 

66 schema, 

67 schema_path, 

68 parent, 

69 ) 

70 self.message = message 

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

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

73 self.context = list(context) 

74 self.cause = self.__cause__ = cause 

75 self.validator = validator 

76 self.validator_value = validator_value 

77 self.instance = instance 

78 self.schema = schema 

79 self.parent = parent 

80 self._type_checker = type_checker 

81 

82 for error in context: 

83 error.parent = self 

84 

85 def __repr__(self): 

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

87 

88 def __str__(self): 

89 essential_for_verbose = ( 

90 self.validator, self.validator_value, self.instance, self.schema, 

91 ) 

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

93 return self.message 

94 

95 schema_path = _utils.format_as_index( 

96 container=self._word_for_schema_in_error_message, 

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

98 ) 

99 instance_path = _utils.format_as_index( 

100 container=self._word_for_instance_in_error_message, 

101 indices=self.relative_path, 

102 ) 

103 prefix = 16 * " " 

104 

105 return dedent( 

106 f"""\ 

107 {self.message} 

108 

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

110 {indent(pformat(self.schema, width=72), prefix).lstrip()} 

111 

112 On {instance_path}: 

113 {indent(pformat(self.instance, width=72), prefix).lstrip()} 

114 """.rstrip(), 

115 ) 

116 

117 @classmethod 

118 def create_from(cls, other): 

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

120 

121 @property 

122 def absolute_path(self): 

123 parent = self.parent 

124 if parent is None: 

125 return self.relative_path 

126 

127 path = deque(self.relative_path) 

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

129 return path 

130 

131 @property 

132 def absolute_schema_path(self): 

133 parent = self.parent 

134 if parent is None: 

135 return self.relative_schema_path 

136 

137 path = deque(self.relative_schema_path) 

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

139 return path 

140 

141 @property 

142 def json_path(self): 

143 path = "$" 

144 for elem in self.absolute_path: 

145 if isinstance(elem, int): 

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

147 else: 

148 path += "." + elem 

149 return path 

150 

151 def _set(self, type_checker=None, **kwargs): 

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

153 self._type_checker = type_checker 

154 

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

156 if getattr(self, k) is _unset: 

157 setattr(self, k, v) 

158 

159 def _contents(self): 

160 attrs = ( 

161 "message", "cause", "context", "validator", "validator_value", 

162 "path", "schema_path", "instance", "schema", "parent", 

163 ) 

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

165 

166 def _matches_type(self): 

167 try: 

168 expected = self.schema["type"] 

169 except (KeyError, TypeError): 

170 return False 

171 

172 if isinstance(expected, str): 

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

174 

175 return any( 

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

177 for expected_type in expected 

178 ) 

179 

180 

181class ValidationError(_Error): 

182 """ 

183 An instance was invalid under a provided schema. 

184 """ 

185 

186 _word_for_schema_in_error_message = "schema" 

187 _word_for_instance_in_error_message = "instance" 

188 

189 

190class SchemaError(_Error): 

191 """ 

192 A schema was invalid under its corresponding metaschema. 

193 """ 

194 

195 _word_for_schema_in_error_message = "metaschema" 

196 _word_for_instance_in_error_message = "schema" 

197 

198 

199@define(slots=False) 

200class _RefResolutionError(Exception): 

201 """ 

202 A ref could not be resolved. 

203 """ 

204 

205 _DEPRECATION_MESSAGE = ( 

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

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

208 "directly catch referencing.exceptions.Unresolvable." 

209 ) 

210 

211 _cause: Exception 

212 

213 def __eq__(self, other): 

214 if self.__class__ is not other.__class__: 

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

216 return self._cause == other._cause 

217 

218 def __str__(self): 

219 return str(self._cause) 

220 

221 

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

223 def __init__(self, cause: _Unresolvable): 

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

225 

226 def __eq__(self, other): 

227 if other.__class__ is self.__class__: 

228 return self._wrapped == other._wrapped 

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

230 return self._wrapped == other 

231 return NotImplemented 

232 

233 def __getattr__(self, attr): 

234 return getattr(self._wrapped, attr) 

235 

236 def __hash__(self): 

237 return hash(self._wrapped) 

238 

239 def __repr__(self): 

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

241 

242 def __str__(self): 

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

244 

245 

246class UndefinedTypeCheck(Exception): 

247 """ 

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

249 """ 

250 

251 def __init__(self, type): 

252 self.type = type 

253 

254 def __str__(self): 

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

256 

257 

258class UnknownType(Exception): 

259 """ 

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

261 """ 

262 

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

264 self.type = type 

265 self.instance = instance 

266 self.schema = schema 

267 

268 def __str__(self): 

269 prefix = 16 * " " 

270 

271 return dedent( 

272 f"""\ 

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

274 {indent(pformat(self.schema, width=72), prefix).lstrip()} 

275 

276 While checking instance: 

277 {indent(pformat(self.instance, width=72), prefix).lstrip()} 

278 """.rstrip(), 

279 ) 

280 

281 

282class FormatError(Exception): 

283 """ 

284 Validating a format failed. 

285 """ 

286 

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

288 super().__init__(message, cause) 

289 self.message = message 

290 self.cause = self.__cause__ = cause 

291 

292 def __str__(self): 

293 return self.message 

294 

295 

296class ErrorTree: 

297 """ 

298 ErrorTrees make it easier to check which validations failed. 

299 """ 

300 

301 _instance = _unset 

302 

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

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

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

306 

307 for error in errors: 

308 container = self 

309 for element in error.path: 

310 container = container[element] 

311 container.errors[error.validator] = error 

312 

313 container._instance = error.instance 

314 

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

316 """ 

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

318 """ 

319 return index in self._contents 

320 

321 def __getitem__(self, index): 

322 """ 

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

324 

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

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

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

328 some subclass of `LookupError`. 

329 """ 

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

331 self._instance[index] 

332 return self._contents[index] 

333 

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

335 """ 

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

337 

338 .. deprecated:: v4.20.0 

339 

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

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

342 construct the tree. 

343 """ 

344 warnings.warn( 

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

346 DeprecationWarning, 

347 stacklevel=2, 

348 ) 

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

350 

351 def __iter__(self): 

352 """ 

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

354 """ 

355 return iter(self._contents) 

356 

357 def __len__(self): 

358 """ 

359 Return the `total_errors`. 

360 """ 

361 return self.total_errors 

362 

363 def __repr__(self): 

364 total = len(self) 

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

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

367 

368 @property 

369 def total_errors(self): 

370 """ 

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

372 """ 

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

374 return len(self.errors) + child_errors 

375 

376 

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

378 """ 

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

380 

381 Arguments: 

382 weak (set): 

383 a collection of validation keywords to consider to be 

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

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

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

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

388 superseded by other same-level validation errors. 

389 

390 strong (set): 

391 a collection of validation keywords to consider to be 

392 "strong" 

393 """ 

394 

395 def relevance(error): 

396 validator = error.validator 

397 return ( 

398 -len(error.path), 

399 validator not in weak, 

400 validator in strong, 

401 not error._matches_type(), 

402 ) 

403 

404 return relevance 

405 

406 

407relevance = by_relevance() 

408""" 

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

410 

411Example: 

412 

413.. code:: python 

414 

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

416""" 

417 

418 

419def best_match(errors, key=relevance): 

420 """ 

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

422 

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

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

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

426 

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

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

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

430 may not be relevant. 

431 

432 Arguments: 

433 errors (collections.abc.Iterable): 

434 

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

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

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

438 sensical output. 

439 

440 key (collections.abc.Callable): 

441 

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

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

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

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

446 that rates errors but still want the error context descent 

447 done by this function. 

448 

449 Returns: 

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

451 

452 .. note:: 

453 

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

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

456 """ 

457 errors = iter(errors) 

458 best = next(errors, None) 

459 if best is None: 

460 return 

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

462 

463 while best.context: 

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

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

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

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

468 return best 

469 best = smallest[0] 

470 return best