Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jsonschema/_format.py: 42%
232 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-01 06:54 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-01 06:54 +0000
1from __future__ import annotations
3from contextlib import suppress
4from uuid import UUID
5import datetime
6import ipaddress
7import re
8import typing
9import warnings
11from jsonschema.exceptions import FormatError
13_FormatCheckCallable = typing.Callable[[object], bool]
14_F = typing.TypeVar("_F", bound=_FormatCheckCallable)
15_RaisesType = typing.Union[
16 typing.Type[Exception], typing.Tuple[typing.Type[Exception], ...],
17]
20class FormatChecker:
21 """
22 A ``format`` property checker.
24 JSON Schema does not mandate that the ``format`` property actually do any
25 validation. If validation is desired however, instances of this class can
26 be hooked into validators to enable format validation.
28 `FormatChecker` objects always return ``True`` when asked about
29 formats that they do not know how to validate.
31 To add a check for a custom format use the `FormatChecker.checks`
32 decorator.
34 Arguments:
36 formats:
38 The known formats to validate. This argument can be used to
39 limit which formats will be used during validation.
40 """
42 checkers: dict[
43 str,
44 tuple[_FormatCheckCallable, _RaisesType],
45 ] = {}
47 def __init__(self, formats: typing.Iterable[str] | None = None):
48 if formats is None:
49 formats = self.checkers.keys()
50 self.checkers = {k: self.checkers[k] for k in formats}
52 def __repr__(self):
53 return "<FormatChecker checkers={}>".format(sorted(self.checkers))
55 def checks(
56 self, format: str, raises: _RaisesType = (),
57 ) -> typing.Callable[[_F], _F]:
58 """
59 Register a decorated function as validating a new format.
61 Arguments:
63 format:
65 The format that the decorated function will check.
67 raises:
69 The exception(s) raised by the decorated function when an
70 invalid instance is found.
72 The exception object will be accessible as the
73 `jsonschema.exceptions.ValidationError.cause` attribute of the
74 resulting validation error.
75 """
77 def _checks(func: _F) -> _F:
78 self.checkers[format] = (func, raises)
79 return func
81 return _checks
83 @classmethod
84 def cls_checks(
85 cls, format: str, raises: _RaisesType = (),
86 ) -> typing.Callable[[_F], _F]:
87 warnings.warn(
88 (
89 "FormatChecker.cls_checks is deprecated. Call "
90 "FormatChecker.checks on a specific FormatChecker instance "
91 "instead."
92 ),
93 DeprecationWarning,
94 stacklevel=2,
95 )
96 return cls._cls_checks(format=format, raises=raises)
98 @classmethod
99 def _cls_checks(
100 cls, format: str, raises: _RaisesType = (),
101 ) -> typing.Callable[[_F], _F]:
102 def _checks(func: _F) -> _F:
103 cls.checkers[format] = (func, raises)
104 return func
106 return _checks
108 def check(self, instance: object, format: str) -> None:
109 """
110 Check whether the instance conforms to the given format.
112 Arguments:
114 instance (*any primitive type*, i.e. str, number, bool):
116 The instance to check
118 format:
120 The format that instance should conform to
122 Raises:
124 FormatError:
126 if the instance does not conform to ``format``
127 """
129 if format not in self.checkers:
130 return
132 func, raises = self.checkers[format]
133 result, cause = None, None
134 try:
135 result = func(instance)
136 except raises as e:
137 cause = e
138 if not result:
139 raise FormatError(f"{instance!r} is not a {format!r}", cause=cause)
141 def conforms(self, instance: object, format: str) -> bool:
142 """
143 Check whether the instance conforms to the given format.
145 Arguments:
147 instance (*any primitive type*, i.e. str, number, bool):
149 The instance to check
151 format:
153 The format that instance should conform to
155 Returns:
157 bool: whether it conformed
158 """
160 try:
161 self.check(instance, format)
162 except FormatError:
163 return False
164 else:
165 return True
168draft3_format_checker = FormatChecker()
169draft4_format_checker = FormatChecker()
170draft6_format_checker = FormatChecker()
171draft7_format_checker = FormatChecker()
172draft201909_format_checker = FormatChecker()
173draft202012_format_checker = FormatChecker()
175_draft_checkers: dict[str, FormatChecker] = dict(
176 draft3=draft3_format_checker,
177 draft4=draft4_format_checker,
178 draft6=draft6_format_checker,
179 draft7=draft7_format_checker,
180 draft201909=draft201909_format_checker,
181 draft202012=draft202012_format_checker,
182)
185def _checks_drafts(
186 name=None,
187 draft3=None,
188 draft4=None,
189 draft6=None,
190 draft7=None,
191 draft201909=None,
192 draft202012=None,
193 raises=(),
194) -> typing.Callable[[_F], _F]:
195 draft3 = draft3 or name
196 draft4 = draft4 or name
197 draft6 = draft6 or name
198 draft7 = draft7 or name
199 draft201909 = draft201909 or name
200 draft202012 = draft202012 or name
202 def wrap(func: _F) -> _F:
203 if draft3:
204 func = _draft_checkers["draft3"].checks(draft3, raises)(func)
205 if draft4:
206 func = _draft_checkers["draft4"].checks(draft4, raises)(func)
207 if draft6:
208 func = _draft_checkers["draft6"].checks(draft6, raises)(func)
209 if draft7:
210 func = _draft_checkers["draft7"].checks(draft7, raises)(func)
211 if draft201909:
212 func = _draft_checkers["draft201909"].checks(draft201909, raises)(
213 func,
214 )
215 if draft202012:
216 func = _draft_checkers["draft202012"].checks(draft202012, raises)(
217 func,
218 )
220 # Oy. This is bad global state, but relied upon for now, until
221 # deprecation. See #519 and test_format_checkers_come_with_defaults
222 FormatChecker._cls_checks(
223 draft202012 or draft201909 or draft7 or draft6 or draft4 or draft3,
224 raises,
225 )(func)
226 return func
228 return wrap
231@_checks_drafts(name="idn-email")
232@_checks_drafts(name="email")
233def is_email(instance: object) -> bool:
234 if not isinstance(instance, str):
235 return True
236 return "@" in instance
239@_checks_drafts(
240 draft3="ip-address",
241 draft4="ipv4",
242 draft6="ipv4",
243 draft7="ipv4",
244 draft201909="ipv4",
245 draft202012="ipv4",
246 raises=ipaddress.AddressValueError,
247)
248def is_ipv4(instance: object) -> bool:
249 if not isinstance(instance, str):
250 return True
251 return bool(ipaddress.IPv4Address(instance))
254@_checks_drafts(name="ipv6", raises=ipaddress.AddressValueError)
255def is_ipv6(instance: object) -> bool:
256 if not isinstance(instance, str):
257 return True
258 address = ipaddress.IPv6Address(instance)
259 return not getattr(address, "scope_id", "")
262with suppress(ImportError):
263 from fqdn import FQDN
265 @_checks_drafts(
266 draft3="host-name",
267 draft4="hostname",
268 draft6="hostname",
269 draft7="hostname",
270 draft201909="hostname",
271 draft202012="hostname",
272 )
273 def is_host_name(instance: object) -> bool:
274 if not isinstance(instance, str):
275 return True
276 return FQDN(instance).is_valid
279with suppress(ImportError):
280 # The built-in `idna` codec only implements RFC 3890, so we go elsewhere.
281 import idna
283 @_checks_drafts(
284 draft7="idn-hostname",
285 draft201909="idn-hostname",
286 draft202012="idn-hostname",
287 raises=(idna.IDNAError, UnicodeError),
288 )
289 def is_idn_host_name(instance: object) -> bool:
290 if not isinstance(instance, str):
291 return True
292 idna.encode(instance)
293 return True
296try:
297 import rfc3987
298except ImportError:
299 with suppress(ImportError):
300 from rfc3986_validator import validate_rfc3986
302 @_checks_drafts(name="uri")
303 def is_uri(instance: object) -> bool:
304 if not isinstance(instance, str):
305 return True
306 return validate_rfc3986(instance, rule="URI")
308 @_checks_drafts(
309 draft6="uri-reference",
310 draft7="uri-reference",
311 draft201909="uri-reference",
312 draft202012="uri-reference",
313 raises=ValueError,
314 )
315 def is_uri_reference(instance: object) -> bool:
316 if not isinstance(instance, str):
317 return True
318 return validate_rfc3986(instance, rule="URI_reference")
320else:
322 @_checks_drafts(
323 draft7="iri",
324 draft201909="iri",
325 draft202012="iri",
326 raises=ValueError,
327 )
328 def is_iri(instance: object) -> bool:
329 if not isinstance(instance, str):
330 return True
331 return rfc3987.parse(instance, rule="IRI")
333 @_checks_drafts(
334 draft7="iri-reference",
335 draft201909="iri-reference",
336 draft202012="iri-reference",
337 raises=ValueError,
338 )
339 def is_iri_reference(instance: object) -> bool:
340 if not isinstance(instance, str):
341 return True
342 return rfc3987.parse(instance, rule="IRI_reference")
344 @_checks_drafts(name="uri", raises=ValueError)
345 def is_uri(instance: object) -> bool:
346 if not isinstance(instance, str):
347 return True
348 return rfc3987.parse(instance, rule="URI")
350 @_checks_drafts(
351 draft6="uri-reference",
352 draft7="uri-reference",
353 draft201909="uri-reference",
354 draft202012="uri-reference",
355 raises=ValueError,
356 )
357 def is_uri_reference(instance: object) -> bool:
358 if not isinstance(instance, str):
359 return True
360 return rfc3987.parse(instance, rule="URI_reference")
363with suppress(ImportError):
364 from rfc3339_validator import validate_rfc3339
366 @_checks_drafts(name="date-time")
367 def is_datetime(instance: object) -> bool:
368 if not isinstance(instance, str):
369 return True
370 return validate_rfc3339(instance.upper())
372 @_checks_drafts(
373 draft7="time",
374 draft201909="time",
375 draft202012="time",
376 )
377 def is_time(instance: object) -> bool:
378 if not isinstance(instance, str):
379 return True
380 return is_datetime("1970-01-01T" + instance)
383@_checks_drafts(name="regex", raises=re.error)
384def is_regex(instance: object) -> bool:
385 if not isinstance(instance, str):
386 return True
387 return bool(re.compile(instance))
390@_checks_drafts(
391 draft3="date",
392 draft7="date",
393 draft201909="date",
394 draft202012="date",
395 raises=ValueError,
396)
397def is_date(instance: object) -> bool:
398 if not isinstance(instance, str):
399 return True
400 return bool(instance.isascii() and datetime.date.fromisoformat(instance))
403@_checks_drafts(draft3="time", raises=ValueError)
404def is_draft3_time(instance: object) -> bool:
405 if not isinstance(instance, str):
406 return True
407 return bool(datetime.datetime.strptime(instance, "%H:%M:%S"))
410with suppress(ImportError):
411 from webcolors import CSS21_NAMES_TO_HEX
412 import webcolors
414 def is_css_color_code(instance: object) -> bool:
415 return webcolors.normalize_hex(instance)
417 @_checks_drafts(draft3="color", raises=(ValueError, TypeError))
418 def is_css21_color(instance: object) -> bool:
419 if (
420 not isinstance(instance, str)
421 or instance.lower() in CSS21_NAMES_TO_HEX
422 ):
423 return True
424 return is_css_color_code(instance)
427with suppress(ImportError):
428 import jsonpointer
430 @_checks_drafts(
431 draft6="json-pointer",
432 draft7="json-pointer",
433 draft201909="json-pointer",
434 draft202012="json-pointer",
435 raises=jsonpointer.JsonPointerException,
436 )
437 def is_json_pointer(instance: object) -> bool:
438 if not isinstance(instance, str):
439 return True
440 return bool(jsonpointer.JsonPointer(instance))
442 # TODO: I don't want to maintain this, so it
443 # needs to go either into jsonpointer (pending
444 # https://github.com/stefankoegl/python-json-pointer/issues/34) or
445 # into a new external library.
446 @_checks_drafts(
447 draft7="relative-json-pointer",
448 draft201909="relative-json-pointer",
449 draft202012="relative-json-pointer",
450 raises=jsonpointer.JsonPointerException,
451 )
452 def is_relative_json_pointer(instance: object) -> bool:
453 # Definition taken from:
454 # https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3
455 if not isinstance(instance, str):
456 return True
457 if not instance:
458 return False
460 non_negative_integer, rest = [], ""
461 for i, character in enumerate(instance):
462 if character.isdigit():
463 # digits with a leading "0" are not allowed
464 if i > 0 and int(instance[i - 1]) == 0:
465 return False
467 non_negative_integer.append(character)
468 continue
470 if not non_negative_integer:
471 return False
473 rest = instance[i:]
474 break
475 return (rest == "#") or bool(jsonpointer.JsonPointer(rest))
478with suppress(ImportError):
479 import uri_template
481 @_checks_drafts(
482 draft6="uri-template",
483 draft7="uri-template",
484 draft201909="uri-template",
485 draft202012="uri-template",
486 )
487 def is_uri_template(instance: object) -> bool:
488 if not isinstance(instance, str):
489 return True
490 return uri_template.validate(instance)
493with suppress(ImportError):
494 import isoduration
496 @_checks_drafts(
497 draft201909="duration",
498 draft202012="duration",
499 raises=isoduration.DurationParsingException,
500 )
501 def is_duration(instance: object) -> bool:
502 if not isinstance(instance, str):
503 return True
504 isoduration.parse_duration(instance)
505 # FIXME: See bolsote/isoduration#25 and bolsote/isoduration#21
506 return instance.endswith(tuple("DMYWHMS"))
509@_checks_drafts(
510 draft201909="uuid",
511 draft202012="uuid",
512 raises=ValueError,
513)
514def is_uuid(instance: object) -> bool:
515 if not isinstance(instance, str):
516 return True
517 UUID(instance)
518 return all(instance[position] == "-" for position in (8, 13, 18, 23))