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

161 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-01 06:54 +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 

9import heapq 

10import itertools 

11 

12import attr 

13 

14from jsonschema import _utils 

15 

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

17STRONG_MATCHES: frozenset[str] = frozenset() 

18 

19_unset = _utils.Unset() 

20 

21 

22class _Error(Exception): 

23 def __init__( 

24 self, 

25 message, 

26 validator=_unset, 

27 path=(), 

28 cause=None, 

29 context=(), 

30 validator_value=_unset, 

31 instance=_unset, 

32 schema=_unset, 

33 schema_path=(), 

34 parent=None, 

35 type_checker=_unset, 

36 ): 

37 super(_Error, self).__init__( 

38 message, 

39 validator, 

40 path, 

41 cause, 

42 context, 

43 validator_value, 

44 instance, 

45 schema, 

46 schema_path, 

47 parent, 

48 ) 

49 self.message = message 

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

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

52 self.context = list(context) 

53 self.cause = self.__cause__ = cause 

54 self.validator = validator 

55 self.validator_value = validator_value 

56 self.instance = instance 

57 self.schema = schema 

58 self.parent = parent 

59 self._type_checker = type_checker 

60 

61 for error in context: 

62 error.parent = self 

63 

64 def __repr__(self): 

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

66 

67 def __str__(self): 

68 essential_for_verbose = ( 

69 self.validator, self.validator_value, self.instance, self.schema, 

70 ) 

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

72 return self.message 

73 

74 schema_path = _utils.format_as_index( 

75 container=self._word_for_schema_in_error_message, 

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

77 ) 

78 instance_path = _utils.format_as_index( 

79 container=self._word_for_instance_in_error_message, 

80 indices=self.relative_path, 

81 ) 

82 prefix = 16 * " " 

83 

84 return dedent( 

85 f"""\ 

86 {self.message} 

87 

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

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

90 

91 On {instance_path}: 

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

93 """.rstrip(), 

94 ) 

95 

96 @classmethod 

97 def create_from(cls, other): 

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

99 

100 @property 

101 def absolute_path(self): 

102 parent = self.parent 

103 if parent is None: 

104 return self.relative_path 

105 

106 path = deque(self.relative_path) 

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

108 return path 

109 

110 @property 

111 def absolute_schema_path(self): 

112 parent = self.parent 

113 if parent is None: 

114 return self.relative_schema_path 

115 

116 path = deque(self.relative_schema_path) 

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

118 return path 

119 

120 @property 

121 def json_path(self): 

122 path = "$" 

123 for elem in self.absolute_path: 

124 if isinstance(elem, int): 

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

126 else: 

127 path += "." + elem 

128 return path 

129 

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

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

132 self._type_checker = type_checker 

133 

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

135 if getattr(self, k) is _unset: 

136 setattr(self, k, v) 

137 

138 def _contents(self): 

139 attrs = ( 

140 "message", "cause", "context", "validator", "validator_value", 

141 "path", "schema_path", "instance", "schema", "parent", 

142 ) 

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

144 

145 def _matches_type(self): 

146 try: 

147 expected = self.schema["type"] 

148 except (KeyError, TypeError): 

149 return False 

150 

151 if isinstance(expected, str): 

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

153 

154 return any( 

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

156 for expected_type in expected 

157 ) 

158 

159 

160class ValidationError(_Error): 

161 """ 

162 An instance was invalid under a provided schema. 

163 """ 

164 

165 _word_for_schema_in_error_message = "schema" 

166 _word_for_instance_in_error_message = "instance" 

167 

168 

169class SchemaError(_Error): 

170 """ 

171 A schema was invalid under its corresponding metaschema. 

172 """ 

173 

174 _word_for_schema_in_error_message = "metaschema" 

175 _word_for_instance_in_error_message = "schema" 

176 

177 

178@attr.s(hash=True) 

179class RefResolutionError(Exception): 

180 """ 

181 A ref could not be resolved. 

182 """ 

183 

184 _cause = attr.ib() 

185 

186 def __str__(self): 

187 return str(self._cause) 

188 

189 

190class UndefinedTypeCheck(Exception): 

191 """ 

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

193 """ 

194 

195 def __init__(self, type): 

196 self.type = type 

197 

198 def __str__(self): 

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

200 

201 

202class UnknownType(Exception): 

203 """ 

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

205 """ 

206 

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

208 self.type = type 

209 self.instance = instance 

210 self.schema = schema 

211 

212 def __str__(self): 

213 prefix = 16 * " " 

214 

215 return dedent( 

216 f"""\ 

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

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

219 

220 While checking instance: 

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

222 """.rstrip(), 

223 ) 

224 

225 

226class FormatError(Exception): 

227 """ 

228 Validating a format failed. 

229 """ 

230 

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

232 super(FormatError, self).__init__(message, cause) 

233 self.message = message 

234 self.cause = self.__cause__ = cause 

235 

236 def __str__(self): 

237 return self.message 

238 

239 

240class ErrorTree: 

241 """ 

242 ErrorTrees make it easier to check which validations failed. 

243 """ 

244 

245 _instance = _unset 

246 

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

248 self.errors = {} 

249 self._contents = defaultdict(self.__class__) 

250 

251 for error in errors: 

252 container = self 

253 for element in error.path: 

254 container = container[element] 

255 container.errors[error.validator] = error 

256 

257 container._instance = error.instance 

258 

259 def __contains__(self, index): 

260 """ 

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

262 """ 

263 

264 return index in self._contents 

265 

266 def __getitem__(self, index): 

267 """ 

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

269 

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

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

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

273 some subclass of `LookupError`. 

274 """ 

275 

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

277 self._instance[index] 

278 return self._contents[index] 

279 

280 def __setitem__(self, index, value): 

281 """ 

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

283 """ 

284 self._contents[index] = value 

285 

286 def __iter__(self): 

287 """ 

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

289 """ 

290 

291 return iter(self._contents) 

292 

293 def __len__(self): 

294 """ 

295 Return the `total_errors`. 

296 """ 

297 return self.total_errors 

298 

299 def __repr__(self): 

300 total = len(self) 

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

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

303 

304 @property 

305 def total_errors(self): 

306 """ 

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

308 """ 

309 

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

311 return len(self.errors) + child_errors 

312 

313 

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

315 """ 

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

317 

318 Arguments: 

319 weak (set): 

320 a collection of validation keywords to consider to be 

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

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

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

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

325 superseded by other same-level validation errors. 

326 

327 strong (set): 

328 a collection of validation keywords to consider to be 

329 "strong" 

330 """ 

331 def relevance(error): 

332 validator = error.validator 

333 return ( 

334 -len(error.path), 

335 validator not in weak, 

336 validator in strong, 

337 not error._matches_type(), 

338 ) 

339 return relevance 

340 

341 

342relevance = by_relevance() 

343 

344 

345def best_match(errors, key=relevance): 

346 """ 

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

348 

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

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

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

352 

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

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

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

356 may not be relevant. 

357 

358 Arguments: 

359 errors (collections.abc.Iterable): 

360 

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

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

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

364 sensical output. 

365 

366 key (collections.abc.Callable): 

367 

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

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

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

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

372 that rates errors but still want the error context descent 

373 done by this function. 

374 

375 Returns: 

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

377 

378 .. note:: 

379 

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

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

382 """ 

383 errors = iter(errors) 

384 best = next(errors, None) 

385 if best is None: 

386 return 

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

388 

389 while best.context: 

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

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

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

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

394 return best 

395 best = smallest[0] 

396 return best