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

177 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:51 +0000

1""" 

2Validation errors, and some surrounding helpers. 

3""" 

4from __future__ import annotations 

5 

6from collections import defaultdict, deque 

7from collections.abc import Iterable, Mapping, MutableMapping 

8from pprint import pformat 

9from textwrap import dedent, indent 

10from typing import ClassVar 

11import heapq 

12import itertools 

13import warnings 

14 

15from attrs import define 

16from referencing.exceptions import Unresolvable as _Unresolvable 

17 

18from jsonschema import _utils 

19 

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

21STRONG_MATCHES: frozenset[str] = frozenset() 

22 

23_unset = _utils.Unset() 

24 

25 

26def __getattr__(name): 

27 if name == "RefResolutionError": 

28 warnings.warn( 

29 _RefResolutionError._DEPRECATION_MESSAGE, 

30 DeprecationWarning, 

31 stacklevel=2, 

32 ) 

33 return _RefResolutionError 

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

35 

36 

37class _Error(Exception): 

38 

39 _word_for_schema_in_error_message: ClassVar[str] 

40 _word_for_instance_in_error_message: ClassVar[str] 

41 

42 def __init__( 

43 self, 

44 message: str, 

45 validator=_unset, 

46 path=(), 

47 cause=None, 

48 context=(), 

49 validator_value=_unset, 

50 instance=_unset, 

51 schema=_unset, 

52 schema_path=(), 

53 parent=None, 

54 type_checker=_unset, 

55 ): 

56 super().__init__( 

57 message, 

58 validator, 

59 path, 

60 cause, 

61 context, 

62 validator_value, 

63 instance, 

64 schema, 

65 schema_path, 

66 parent, 

67 ) 

68 self.message = message 

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

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

71 self.context = list(context) 

72 self.cause = self.__cause__ = cause 

73 self.validator = validator 

74 self.validator_value = validator_value 

75 self.instance = instance 

76 self.schema = schema 

77 self.parent = parent 

78 self._type_checker = type_checker 

79 

80 for error in context: 

81 error.parent = self 

82 

83 def __repr__(self): 

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

85 

86 def __str__(self): 

87 essential_for_verbose = ( 

88 self.validator, self.validator_value, self.instance, self.schema, 

89 ) 

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

91 return self.message 

92 

93 schema_path = _utils.format_as_index( 

94 container=self._word_for_schema_in_error_message, 

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

96 ) 

97 instance_path = _utils.format_as_index( 

98 container=self._word_for_instance_in_error_message, 

99 indices=self.relative_path, 

100 ) 

101 prefix = 16 * " " 

102 

103 return dedent( 

104 f"""\ 

105 {self.message} 

106 

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

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

109 

110 On {instance_path}: 

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

112 """.rstrip(), 

113 ) 

114 

115 @classmethod 

116 def create_from(cls, other): 

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

118 

119 @property 

120 def absolute_path(self): 

121 parent = self.parent 

122 if parent is None: 

123 return self.relative_path 

124 

125 path = deque(self.relative_path) 

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

127 return path 

128 

129 @property 

130 def absolute_schema_path(self): 

131 parent = self.parent 

132 if parent is None: 

133 return self.relative_schema_path 

134 

135 path = deque(self.relative_schema_path) 

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

137 return path 

138 

139 @property 

140 def json_path(self): 

141 path = "$" 

142 for elem in self.absolute_path: 

143 if isinstance(elem, int): 

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

145 else: 

146 path += "." + elem 

147 return path 

148 

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

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

151 self._type_checker = type_checker 

152 

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

154 if getattr(self, k) is _unset: 

155 setattr(self, k, v) 

156 

157 def _contents(self): 

158 attrs = ( 

159 "message", "cause", "context", "validator", "validator_value", 

160 "path", "schema_path", "instance", "schema", "parent", 

161 ) 

162 return dict((attr, getattr(self, attr)) for attr in attrs) 

163 

164 def _matches_type(self): 

165 try: 

166 expected = self.schema["type"] 

167 except (KeyError, TypeError): 

168 return False 

169 

170 if isinstance(expected, str): 

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

172 

173 return any( 

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

175 for expected_type in expected 

176 ) 

177 

178 

179class ValidationError(_Error): 

180 """ 

181 An instance was invalid under a provided schema. 

182 """ 

183 

184 _word_for_schema_in_error_message = "schema" 

185 _word_for_instance_in_error_message = "instance" 

186 

187 

188class SchemaError(_Error): 

189 """ 

190 A schema was invalid under its corresponding metaschema. 

191 """ 

192 

193 _word_for_schema_in_error_message = "metaschema" 

194 _word_for_instance_in_error_message = "schema" 

195 

196 

197@define(slots=False) 

198class _RefResolutionError(Exception): 

199 """ 

200 A ref could not be resolved. 

201 """ 

202 

203 _DEPRECATION_MESSAGE = ( 

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

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

206 "directly catch referencing.exceptions.Unresolvable." 

207 ) 

208 

209 _cause: Exception 

210 

211 def __eq__(self, other): 

212 if self.__class__ is not other.__class__: 

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

214 return self._cause == other._cause 

215 

216 def __str__(self): 

217 return str(self._cause) 

218 

219 

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

221 def __init__(self, cause: _Unresolvable): 

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

223 

224 def __eq__(self, other): 

225 if other.__class__ is self.__class__: 

226 return self._wrapped == other._wrapped 

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

228 return self._wrapped == other 

229 return NotImplemented 

230 

231 def __getattr__(self, attr): 

232 return getattr(self._wrapped, attr) 

233 

234 def __hash__(self): 

235 return hash(self._wrapped) 

236 

237 def __repr__(self): 

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

239 

240 def __str__(self): 

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

242 

243 

244class UndefinedTypeCheck(Exception): 

245 """ 

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

247 """ 

248 

249 def __init__(self, type): 

250 self.type = type 

251 

252 def __str__(self): 

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

254 

255 

256class UnknownType(Exception): 

257 """ 

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

259 """ 

260 

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

262 self.type = type 

263 self.instance = instance 

264 self.schema = schema 

265 

266 def __str__(self): 

267 prefix = 16 * " " 

268 

269 return dedent( 

270 f"""\ 

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

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

273 

274 While checking instance: 

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

276 """.rstrip(), 

277 ) 

278 

279 

280class FormatError(Exception): 

281 """ 

282 Validating a format failed. 

283 """ 

284 

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

286 super().__init__(message, cause) 

287 self.message = message 

288 self.cause = self.__cause__ = cause 

289 

290 def __str__(self): 

291 return self.message 

292 

293 

294class ErrorTree: 

295 """ 

296 ErrorTrees make it easier to check which validations failed. 

297 """ 

298 

299 _instance = _unset 

300 

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

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

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

304 

305 for error in errors: 

306 container = self 

307 for element in error.path: 

308 container = container[element] 

309 container.errors[error.validator] = error 

310 

311 container._instance = error.instance 

312 

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

314 """ 

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

316 """ 

317 return index in self._contents 

318 

319 def __getitem__(self, index): 

320 """ 

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

322 

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

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

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

326 some subclass of `LookupError`. 

327 """ 

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

329 self._instance[index] 

330 return self._contents[index] 

331 

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

333 """ 

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

335 

336 .. deprecated:: v4.20.0 

337 

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

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

340 construct the tree. 

341 """ 

342 warnings.warn( 

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

344 DeprecationWarning, 

345 stacklevel=2, 

346 ) 

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

348 

349 def __iter__(self): 

350 """ 

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

352 """ 

353 return iter(self._contents) 

354 

355 def __len__(self): 

356 """ 

357 Return the `total_errors`. 

358 """ 

359 return self.total_errors 

360 

361 def __repr__(self): 

362 total = len(self) 

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

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

365 

366 @property 

367 def total_errors(self): 

368 """ 

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

370 """ 

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

372 return len(self.errors) + child_errors 

373 

374 

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

376 """ 

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

378 

379 Arguments: 

380 weak (set): 

381 a collection of validation keywords to consider to be 

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

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

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

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

386 superseded by other same-level validation errors. 

387 

388 strong (set): 

389 a collection of validation keywords to consider to be 

390 "strong" 

391 """ 

392 

393 def relevance(error): 

394 validator = error.validator 

395 return ( 

396 -len(error.path), 

397 validator not in weak, 

398 validator in strong, 

399 not error._matches_type(), 

400 ) 

401 

402 return relevance 

403 

404 

405relevance = by_relevance() 

406""" 

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

408 

409Example: 

410 

411.. code:: python 

412 

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

414""" 

415 

416 

417def best_match(errors, key=relevance): 

418 """ 

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

420 

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

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

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

424 

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

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

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

428 may not be relevant. 

429 

430 Arguments: 

431 errors (collections.abc.Iterable): 

432 

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

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

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

436 sensical output. 

437 

438 key (collections.abc.Callable): 

439 

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

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

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

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

444 that rates errors but still want the error context descent 

445 done by this function. 

446 

447 Returns: 

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

449 

450 .. note:: 

451 

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

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

454 """ 

455 errors = iter(errors) 

456 best = next(errors, None) 

457 if best is None: 

458 return 

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

460 

461 while best.context: 

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

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

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

465 if len(smallest) == 2 and key(smallest[0]) == key(smallest[1]): 

466 return best 

467 best = smallest[0] 

468 return best