comparison env/lib/python3.9/site-packages/schema_salad/validate.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 import logging
2 import pprint
3 from typing import Any, List, MutableMapping, MutableSequence, Optional, Set
4 from urllib.parse import urlsplit
5
6 from . import avro
7 from .avro.schema import Schema # pylint: disable=no-name-in-module, import-error
8 from .exceptions import (
9 ClassValidationException,
10 SchemaSaladException,
11 ValidationException,
12 )
13 from .sourceline import SourceLine
14
15 _logger = logging.getLogger("salad")
16
17
18 def validate(
19 expected_schema: Schema,
20 datum: Any,
21 identifiers: Optional[List[str]] = None,
22 strict: bool = False,
23 foreign_properties: Optional[Set[str]] = None,
24 ) -> bool:
25 if not identifiers:
26 identifiers = []
27 if not foreign_properties:
28 foreign_properties = set()
29 return validate_ex(
30 expected_schema,
31 datum,
32 identifiers,
33 strict=strict,
34 foreign_properties=foreign_properties,
35 raise_ex=False,
36 )
37
38
39 INT_MIN_VALUE = -(1 << 31)
40 INT_MAX_VALUE = (1 << 31) - 1
41 LONG_MIN_VALUE = -(1 << 63)
42 LONG_MAX_VALUE = (1 << 63) - 1
43
44
45 def friendly(v): # type: (Any) -> Any
46 if isinstance(v, avro.schema.NamedSchema):
47 return v.name
48 if isinstance(v, avro.schema.ArraySchema):
49 return "array of <{}>".format(friendly(v.items))
50 elif isinstance(v, avro.schema.PrimitiveSchema):
51 return v.type
52 elif isinstance(v, avro.schema.UnionSchema):
53 return " or ".join([friendly(s) for s in v.schemas])
54 else:
55 return v
56
57
58 def vpformat(datum): # type: (Any) -> str
59 a = pprint.pformat(datum)
60 if len(a) > 160:
61 a = a[0:160] + "[...]"
62 return a
63
64
65 def validate_ex(
66 expected_schema: Schema,
67 datum, # type: Any
68 identifiers=None, # type: Optional[List[str]]
69 strict=False, # type: bool
70 foreign_properties=None, # type: Optional[Set[str]]
71 raise_ex=True, # type: bool
72 strict_foreign_properties=False, # type: bool
73 logger=_logger, # type: logging.Logger
74 skip_foreign_properties=False, # type: bool
75 ):
76 # type: (...) -> bool
77 """Determine if a python datum is an instance of a schema."""
78
79 if not identifiers:
80 identifiers = []
81
82 if not foreign_properties:
83 foreign_properties = set()
84
85 schema_type = expected_schema.type
86
87 if schema_type == "null":
88 if datum is None:
89 return True
90 if raise_ex:
91 raise ValidationException("the value is not null")
92 return False
93 elif schema_type == "boolean":
94 if isinstance(datum, bool):
95 return True
96 if raise_ex:
97 raise ValidationException("the value is not boolean")
98 return False
99 elif schema_type == "string":
100 if isinstance(datum, str):
101 return True
102 if isinstance(datum, bytes):
103 return True
104 if raise_ex:
105 raise ValidationException("the value is not string")
106 return False
107 elif schema_type == "int":
108 if isinstance(datum, int) and INT_MIN_VALUE <= datum <= INT_MAX_VALUE:
109 return True
110 if raise_ex:
111 raise ValidationException("`{}` is not int".format(vpformat(datum)))
112 return False
113 elif schema_type == "long":
114 if (isinstance(datum, int)) and LONG_MIN_VALUE <= datum <= LONG_MAX_VALUE:
115 return True
116 if raise_ex:
117 raise ValidationException(
118 "the value `{}` is not long".format(vpformat(datum))
119 )
120 return False
121 elif schema_type in ["float", "double"]:
122 if isinstance(datum, int) or isinstance(datum, float):
123 return True
124 if raise_ex:
125 raise ValidationException(
126 "the value `{}` is not float or double".format(vpformat(datum))
127 )
128 return False
129 elif isinstance(expected_schema, avro.schema.EnumSchema):
130 if expected_schema.name == "Any":
131 if datum is not None:
132 return True
133 if raise_ex:
134 raise ValidationException("'Any' type must be non-null")
135 return False
136 if not isinstance(datum, str):
137 if raise_ex:
138 raise ValidationException(
139 "value is a {} but expected a string".format(type(datum).__name__)
140 )
141 return False
142 if expected_schema.name == "Expression":
143 if "$(" in datum or "${" in datum:
144 return True
145 if raise_ex:
146 raise ValidationException(
147 "value `{}` does not contain an expression in the form $() or ${{}}".format(
148 datum
149 )
150 )
151 return False
152 if datum in expected_schema.symbols:
153 return True
154 else:
155 if raise_ex:
156 raise ValidationException(
157 "the value {} is not a valid {}, expected {}{}".format(
158 vpformat(datum),
159 expected_schema.name,
160 "one of " if len(expected_schema.symbols) > 1 else "",
161 "'" + "', '".join(expected_schema.symbols) + "'",
162 )
163 )
164 return False
165 elif isinstance(expected_schema, avro.schema.ArraySchema):
166 if isinstance(datum, MutableSequence):
167 for i, d in enumerate(datum):
168 try:
169 sl = SourceLine(datum, i, ValidationException)
170 if not validate_ex(
171 expected_schema.items,
172 d,
173 identifiers,
174 strict=strict,
175 foreign_properties=foreign_properties,
176 raise_ex=raise_ex,
177 strict_foreign_properties=strict_foreign_properties,
178 logger=logger,
179 skip_foreign_properties=skip_foreign_properties,
180 ):
181 return False
182 except ValidationException as v:
183 if raise_ex:
184 raise ValidationException("item is invalid because", sl, [v])
185 return False
186 return True
187 else:
188 if raise_ex:
189 raise ValidationException(
190 "the value {} is not a list, expected list of {}".format(
191 vpformat(datum), friendly(expected_schema.items)
192 )
193 )
194 return False
195 elif isinstance(expected_schema, avro.schema.UnionSchema):
196 for s in expected_schema.schemas:
197 if validate_ex(
198 s,
199 datum,
200 identifiers,
201 strict=strict,
202 raise_ex=False,
203 strict_foreign_properties=strict_foreign_properties,
204 logger=logger,
205 skip_foreign_properties=skip_foreign_properties,
206 ):
207 return True
208
209 if not raise_ex:
210 return False
211
212 errors = [] # type: List[SchemaSaladException]
213 checked = []
214 for s in expected_schema.schemas:
215 if isinstance(datum, MutableSequence) and not isinstance(
216 s, avro.schema.ArraySchema
217 ):
218 continue
219 elif isinstance(datum, MutableMapping) and not isinstance(
220 s, avro.schema.RecordSchema
221 ):
222 continue
223 elif isinstance(datum, (bool, int, float, str)) and isinstance(
224 s, (avro.schema.ArraySchema, avro.schema.RecordSchema)
225 ):
226 continue
227 elif datum is not None and s.type == "null":
228 continue
229
230 checked.append(s)
231 try:
232 validate_ex(
233 s,
234 datum,
235 identifiers,
236 strict=strict,
237 foreign_properties=foreign_properties,
238 raise_ex=True,
239 strict_foreign_properties=strict_foreign_properties,
240 logger=logger,
241 skip_foreign_properties=skip_foreign_properties,
242 )
243 except ClassValidationException:
244 raise
245 except ValidationException as e:
246 errors.append(e)
247 if bool(errors):
248 raise ValidationException(
249 "",
250 None,
251 [
252 ValidationException(
253 "tried {} but".format(friendly(check)), None, [err]
254 )
255 for (check, err) in zip(checked, errors)
256 ],
257 "-",
258 )
259 else:
260 raise ValidationException(
261 "value is a {}, expected {}".format(
262 type(datum).__name__, friendly(expected_schema)
263 )
264 )
265
266 elif isinstance(expected_schema, avro.schema.RecordSchema):
267 if not isinstance(datum, MutableMapping):
268 if raise_ex:
269 raise ValidationException("is not a dict")
270 else:
271 return False
272
273 classmatch = None
274 for f in expected_schema.fields:
275 if f.name in ("class",):
276 d = datum.get(f.name)
277 if not d:
278 if raise_ex:
279 raise ValidationException(f"Missing '{f.name}' field")
280 else:
281 return False
282 if expected_schema.name != d:
283 if raise_ex:
284 raise ValidationException(
285 "Expected class '{}' but this is '{}'".format(
286 expected_schema.name, d
287 )
288 )
289 else:
290 return False
291 classmatch = d
292 break
293
294 errors = []
295 for f in expected_schema.fields:
296 if f.name in ("class",):
297 continue
298
299 if f.name in datum:
300 fieldval = datum[f.name]
301 else:
302 try:
303 fieldval = f.default
304 except KeyError:
305 fieldval = None
306
307 try:
308 sl = SourceLine(datum, f.name, str)
309 if not validate_ex(
310 f.type,
311 fieldval,
312 identifiers,
313 strict=strict,
314 foreign_properties=foreign_properties,
315 raise_ex=raise_ex,
316 strict_foreign_properties=strict_foreign_properties,
317 logger=logger,
318 skip_foreign_properties=skip_foreign_properties,
319 ):
320 return False
321 except ValidationException as v:
322 if f.name not in datum:
323 errors.append(
324 ValidationException(f"missing required field `{f.name}`")
325 )
326 else:
327 errors.append(
328 ValidationException(
329 f"the `{f.name}` field is not valid because",
330 sl,
331 [v],
332 )
333 )
334
335 for d in datum:
336 found = False
337 for f in expected_schema.fields:
338 if d == f.name:
339 found = True
340 if not found:
341 sl = SourceLine(datum, d, str)
342 if d is None:
343 err = ValidationException("mapping with implicit null key", sl)
344 if strict:
345 errors.append(err)
346 else:
347 logger.warning(err.as_warning())
348 continue
349 if (
350 d not in identifiers
351 and d not in foreign_properties
352 and d[0] not in ("@", "$")
353 ):
354 if (
355 (d not in identifiers and strict)
356 and (
357 d not in foreign_properties
358 and strict_foreign_properties
359 and not skip_foreign_properties
360 )
361 and not raise_ex
362 ):
363 return False
364 split = urlsplit(d)
365 if split.scheme:
366 if not skip_foreign_properties:
367 err = ValidationException(
368 "unrecognized extension field `{}`{}.{}".format(
369 d,
370 " and strict_foreign_properties checking is enabled"
371 if strict_foreign_properties
372 else "",
373 "\nForeign properties from $schemas:\n {}".format(
374 "\n ".join(sorted(foreign_properties))
375 )
376 if len(foreign_properties) > 0
377 else "",
378 ),
379 sl,
380 )
381 if strict_foreign_properties:
382 errors.append(err)
383 elif len(foreign_properties) > 0:
384 logger.warning(err.as_warning())
385 else:
386 err = ValidationException(
387 "invalid field `{}`, expected one of: {}".format(
388 d,
389 ", ".join(
390 f"'{fn.name}'" for fn in expected_schema.fields
391 ),
392 ),
393 sl,
394 )
395 if strict:
396 errors.append(err)
397 else:
398 logger.warning(err.as_warning())
399
400 if bool(errors):
401 if raise_ex:
402 if classmatch:
403 raise ClassValidationException("", None, errors, "*")
404 else:
405 raise ValidationException("", None, errors, "*")
406 else:
407 return False
408 else:
409 return True
410 if raise_ex:
411 raise ValidationException(f"Unrecognized schema_type {schema_type}")
412 else:
413 return False