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

193 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-25 06:30 +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 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 

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

20STRONG_MATCHES: frozenset[str] = frozenset() 

21 

22_unset = _utils.Unset() 

23 

24 

25def __getattr__(name): 

26 if name == "RefResolutionError": 

27 warnings.warn( 

28 _RefResolutionError._DEPRECATION_MESSAGE, 

29 DeprecationWarning, 

30 stacklevel=2, 

31 ) 

32 return _RefResolutionError 

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

34 

35 

36class _Error(Exception): 

37 

38 _word_for_schema_in_error_message: ClassVar[str] 

39 _word_for_instance_in_error_message: ClassVar[str] 

40 

41 def __init__( 

42 self, 

43 message: str, 

44 validator=_unset, 

45 path=(), 

46 cause=None, 

47 context=(), 

48 validator_value=_unset, 

49 instance=_unset, 

50 schema=_unset, 

51 schema_path=(), 

52 parent=None, 

53 type_checker=_unset, 

54 ): 

55 super().__init__( 

56 message, 

57 validator, 

58 path, 

59 cause, 

60 context, 

61 validator_value, 

62 instance, 

63 schema, 

64 schema_path, 

65 parent, 

66 ) 

67 self.message = message 

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

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

70 self.context = list(context) 

71 self.cause = self.__cause__ = cause 

72 self.validator = validator 

73 self.validator_value = validator_value 

74 self.instance = instance 

75 self.schema = schema 

76 self.parent = parent 

77 self._type_checker = type_checker 

78 

79 for error in context: 

80 error.parent = self 

81 

82 def __repr__(self): 

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

84 

85 def __str__(self): 

86 essential_for_verbose = ( 

87 self.validator, self.validator_value, self.instance, self.schema, 

88 ) 

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

90 return self.message 

91 

92 schema_path = _utils.format_as_index( 

93 container=self._word_for_schema_in_error_message, 

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

95 ) 

96 instance_path = _utils.format_as_index( 

97 container=self._word_for_instance_in_error_message, 

98 indices=self.relative_path, 

99 ) 

100 prefix = 16 * " " 

101 

102 return dedent( 

103 f"""\ 

104 {self.message} 

105 

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

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

108 

109 On {instance_path}: 

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

111 """.rstrip(), 

112 ) 

113 

114 @classmethod 

115 def create_from(cls, other): 

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

117 

118 @property 

119 def absolute_path(self): 

120 parent = self.parent 

121 if parent is None: 

122 return self.relative_path 

123 

124 path = deque(self.relative_path) 

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

126 return path 

127 

128 @property 

129 def absolute_schema_path(self): 

130 parent = self.parent 

131 if parent is None: 

132 return self.relative_schema_path 

133 

134 path = deque(self.relative_schema_path) 

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

136 return path 

137 

138 @property 

139 def json_path(self): 

140 path = "$" 

141 for elem in self.absolute_path: 

142 if isinstance(elem, int): 

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

144 else: 

145 path += "." + elem 

146 return path 

147 

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

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

150 self._type_checker = type_checker 

151 

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

153 if getattr(self, k) is _unset: 

154 setattr(self, k, v) 

155 

156 def _contents(self): 

157 attrs = ( 

158 "message", "cause", "context", "validator", "validator_value", 

159 "path", "schema_path", "instance", "schema", "parent", 

160 ) 

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

162 

163 def _matches_type(self): 

164 try: 

165 expected = self.schema["type"] 

166 except (KeyError, TypeError): 

167 return False 

168 

169 if isinstance(expected, str): 

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

171 

172 return any( 

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

174 for expected_type in expected 

175 ) 

176 

177 

178class ValidationError(_Error): 

179 """ 

180 An instance was invalid under a provided schema. 

181 """ 

182 

183 _word_for_schema_in_error_message = "schema" 

184 _word_for_instance_in_error_message = "instance" 

185 

186 

187class SchemaError(_Error): 

188 """ 

189 A schema was invalid under its corresponding metaschema. 

190 """ 

191 

192 _word_for_schema_in_error_message = "metaschema" 

193 _word_for_instance_in_error_message = "schema" 

194 

195 

196@define(slots=False) 

197class _RefResolutionError(Exception): 

198 """ 

199 A ref could not be resolved. 

200 """ 

201 

202 _DEPRECATION_MESSAGE = ( 

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

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

205 "directly catch referencing.exceptions.Unresolvable." 

206 ) 

207 

208 _cause: Exception 

209 

210 def __eq__(self, other): 

211 if self.__class__ is not other.__class__: 

212 return NotImplemented 

213 return self._cause == other._cause 

214 

215 def __str__(self): 

216 return str(self._cause) 

217 

218 

219class _WrappedReferencingError(_RefResolutionError, _Unresolvable): 

220 def __init__(self, cause: _Unresolvable): 

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

222 

223 def __eq__(self, other): 

224 if other.__class__ is self.__class__: 

225 return self._wrapped == other._wrapped 

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

227 return self._wrapped == other 

228 return NotImplemented 

229 

230 def __getattr__(self, attr): 

231 return getattr(self._wrapped, attr) 

232 

233 def __hash__(self): 

234 return hash(self._wrapped) 

235 

236 def __repr__(self): 

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

238 

239 def __str__(self): 

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

241 

242 

243class UndefinedTypeCheck(Exception): 

244 """ 

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

246 """ 

247 

248 def __init__(self, type): 

249 self.type = type 

250 

251 def __str__(self): 

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

253 

254 

255class UnknownType(Exception): 

256 """ 

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

258 """ 

259 

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

261 self.type = type 

262 self.instance = instance 

263 self.schema = schema 

264 

265 def __str__(self): 

266 prefix = 16 * " " 

267 

268 return dedent( 

269 f"""\ 

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

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

272 

273 While checking instance: 

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

275 """.rstrip(), 

276 ) 

277 

278 

279class FormatError(Exception): 

280 """ 

281 Validating a format failed. 

282 """ 

283 

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

285 super().__init__(message, cause) 

286 self.message = message 

287 self.cause = self.__cause__ = cause 

288 

289 def __str__(self): 

290 return self.message 

291 

292 

293class ErrorTree: 

294 """ 

295 ErrorTrees make it easier to check which validations failed. 

296 """ 

297 

298 _instance = _unset 

299 

300 def __init__(self, errors=()): 

301 self.errors = {} 

302 self._contents = defaultdict(self.__class__) 

303 

304 for error in errors: 

305 container = self 

306 for element in error.path: 

307 container = container[element] 

308 container.errors[error.validator] = error 

309 

310 container._instance = error.instance 

311 

312 def __contains__(self, index): 

313 """ 

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

315 """ 

316 return index in self._contents 

317 

318 def __getitem__(self, index): 

319 """ 

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

321 

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

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

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

325 some subclass of `LookupError`. 

326 """ 

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

328 self._instance[index] 

329 return self._contents[index] 

330 

331 def __setitem__(self, index, value): 

332 """ 

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

334 """ 

335 self._contents[index] = value 

336 

337 def __iter__(self): 

338 """ 

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

340 """ 

341 return iter(self._contents) 

342 

343 def __len__(self): 

344 """ 

345 Return the `total_errors`. 

346 """ 

347 return self.total_errors 

348 

349 def __repr__(self): 

350 total = len(self) 

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

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

353 

354 @property 

355 def total_errors(self): 

356 """ 

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

358 """ 

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

360 return len(self.errors) + child_errors 

361 

362 

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

364 """ 

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

366 

367 Arguments: 

368 weak (set): 

369 a collection of validation keywords to consider to be 

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

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

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

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

374 superseded by other same-level validation errors. 

375 

376 strong (set): 

377 a collection of validation keywords to consider to be 

378 "strong" 

379 """ 

380 

381 def relevance(error): 

382 validator = error.validator 

383 return ( 

384 -len(error.path), 

385 validator not in weak, 

386 validator in strong, 

387 not error._matches_type(), 

388 ) 

389 

390 return relevance 

391 

392 

393relevance = by_relevance() 

394""" 

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

396 

397Example: 

398 

399.. code:: python 

400 

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

402""" 

403 

404 

405def best_match(errors, key=relevance): 

406 """ 

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

408 

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

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

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

412 

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

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

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

416 may not be relevant. 

417 

418 Arguments: 

419 errors (collections.abc.Iterable): 

420 

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

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

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

424 sensical output. 

425 

426 key (collections.abc.Callable): 

427 

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

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

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

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

432 that rates errors but still want the error context descent 

433 done by this function. 

434 

435 Returns: 

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

437 

438 .. note:: 

439 

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

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

442 """ 

443 errors = iter(errors) 

444 best = next(errors, None) 

445 if best is None: 

446 return 

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

448 

449 while best.context: 

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

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

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

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

454 return best 

455 best = smallest[0] 

456 return best