Coverage for /pythoncovmergedfiles/medio/medio/src/jsonschema/jsonschema/_format.py: 42%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3from contextlib import suppress
4from datetime import date, datetime
5from uuid import UUID
6import ipaddress
7import re
8import string
9import typing
10import warnings
12from jsonschema.exceptions import FormatError
14_FormatCheckCallable = typing.Callable[[object], bool]
15#: A format checker callable.
16_F = typing.TypeVar("_F", bound=_FormatCheckCallable)
17_RaisesType = type[Exception] | tuple[type[Exception], ...]
19_RE_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$", re.ASCII)
22class FormatChecker:
23 """
24 A ``format`` property checker.
26 JSON Schema does not mandate that the ``format`` property actually do any
27 validation. If validation is desired however, instances of this class can
28 be hooked into validators to enable format validation.
30 `FormatChecker` objects always return ``True`` when asked about
31 formats that they do not know how to validate.
33 To add a check for a custom format use the `FormatChecker.checks`
34 decorator.
36 Arguments:
38 formats:
40 The known formats to validate. This argument can be used to
41 limit which formats will be used during validation.
43 """
45 checkers: dict[
46 str,
47 tuple[_FormatCheckCallable, _RaisesType],
48 ] = {} # noqa: RUF012
50 def __init__(self, formats: typing.Iterable[str] | None = None):
51 if formats is None:
52 formats = self.checkers.keys()
53 self.checkers = {k: self.checkers[k] for k in formats}
55 def __repr__(self):
56 return f"<FormatChecker checkers={sorted(self.checkers)}>"
58 def checks(
59 self, format: str, raises: _RaisesType = (),
60 ) -> typing.Callable[[_F], _F]:
61 """
62 Register a decorated function as validating a new format.
64 Arguments:
66 format:
68 The format that the decorated function will check.
70 raises:
72 The exception(s) raised by the decorated function when an
73 invalid instance is found.
75 The exception object will be accessible as the
76 `jsonschema.exceptions.ValidationError.cause` attribute of the
77 resulting validation error.
79 """
81 def _checks(func: _F) -> _F:
82 self.checkers[format] = (func, raises)
83 return func
85 return _checks
87 @classmethod
88 def cls_checks(
89 cls, format: str, raises: _RaisesType = (),
90 ) -> typing.Callable[[_F], _F]:
91 warnings.warn(
92 (
93 "FormatChecker.cls_checks is deprecated. Call "
94 "FormatChecker.checks on a specific FormatChecker instance "
95 "instead."
96 ),
97 DeprecationWarning,
98 stacklevel=2,
99 )
100 return cls._cls_checks(format=format, raises=raises)
102 @classmethod
103 def _cls_checks(
104 cls, format: str, raises: _RaisesType = (),
105 ) -> typing.Callable[[_F], _F]:
106 def _checks(func: _F) -> _F:
107 cls.checkers[format] = (func, raises)
108 return func
110 return _checks
112 def check(self, instance: object, format: str) -> None:
113 """
114 Check whether the instance conforms to the given format.
116 Arguments:
118 instance (*any primitive type*, i.e. str, number, bool):
120 The instance to check
122 format:
124 The format that instance should conform to
126 Raises:
128 FormatError:
130 if the instance does not conform to ``format``
132 """
133 if format not in self.checkers:
134 return
136 func, raises = self.checkers[format]
137 result, cause = None, None
138 try:
139 result = func(instance)
140 except raises as e:
141 cause = e
142 if not result:
143 raise FormatError(f"{instance!r} is not a {format!r}", cause=cause)
145 def conforms(self, instance: object, format: str) -> bool:
146 """
147 Check whether the instance conforms to the given format.
149 Arguments:
151 instance (*any primitive type*, i.e. str, number, bool):
153 The instance to check
155 format:
157 The format that instance should conform to
159 Returns:
161 bool: whether it conformed
163 """
164 try:
165 self.check(instance, format)
166 except FormatError:
167 return False
168 else:
169 return True
172draft3_format_checker = FormatChecker()
173draft4_format_checker = FormatChecker()
174draft6_format_checker = FormatChecker()
175draft7_format_checker = FormatChecker()
176draft201909_format_checker = FormatChecker()
177draft202012_format_checker = FormatChecker()
179_draft_checkers: dict[str, FormatChecker] = dict(
180 draft3=draft3_format_checker,
181 draft4=draft4_format_checker,
182 draft6=draft6_format_checker,
183 draft7=draft7_format_checker,
184 draft201909=draft201909_format_checker,
185 draft202012=draft202012_format_checker,
186)
189def _checks_drafts(
190 name=None,
191 draft3=None,
192 draft4=None,
193 draft6=None,
194 draft7=None,
195 draft201909=None,
196 draft202012=None,
197 raises=(),
198) -> typing.Callable[[_F], _F]:
199 draft3 = draft3 or name
200 draft4 = draft4 or name
201 draft6 = draft6 or name
202 draft7 = draft7 or name
203 draft201909 = draft201909 or name
204 draft202012 = draft202012 or name
206 def wrap(func: _F) -> _F:
207 if draft3:
208 func = _draft_checkers["draft3"].checks(draft3, raises)(func)
209 if draft4:
210 func = _draft_checkers["draft4"].checks(draft4, raises)(func)
211 if draft6:
212 func = _draft_checkers["draft6"].checks(draft6, raises)(func)
213 if draft7:
214 func = _draft_checkers["draft7"].checks(draft7, raises)(func)
215 if draft201909:
216 func = _draft_checkers["draft201909"].checks(draft201909, raises)(
217 func,
218 )
219 if draft202012:
220 func = _draft_checkers["draft202012"].checks(draft202012, raises)(
221 func,
222 )
224 # Oy. This is bad global state, but relied upon for now, until
225 # deprecation. See #519 and test_format_checkers_come_with_defaults
226 FormatChecker._cls_checks(
227 draft202012 or draft201909 or draft7 or draft6 or draft4 or draft3,
228 raises,
229 )(func)
230 return func
232 return wrap
235@_checks_drafts(name="idn-email")
236@_checks_drafts(name="email")
237def is_email(instance: object) -> bool:
238 if not isinstance(instance, str):
239 return True
240 return "@" in instance
243@_checks_drafts(
244 draft3="ip-address",
245 draft4="ipv4",
246 draft6="ipv4",
247 draft7="ipv4",
248 draft201909="ipv4",
249 draft202012="ipv4",
250 raises=ipaddress.AddressValueError,
251)
252def is_ipv4(instance: object) -> bool:
253 if not isinstance(instance, str):
254 return True
255 return bool(ipaddress.IPv4Address(instance))
258@_checks_drafts(name="ipv6", raises=ipaddress.AddressValueError)
259def is_ipv6(instance: object) -> bool:
260 if not isinstance(instance, str):
261 return True
262 address = ipaddress.IPv6Address(instance)
263 return not getattr(address, "scope_id", "")
266with suppress(ImportError):
267 from fqdn import FQDN
269 @_checks_drafts(
270 draft3="host-name",
271 draft4="hostname",
272 draft6="hostname",
273 draft7="hostname",
274 draft201909="hostname",
275 draft202012="hostname",
276 # fqdn.FQDN("") raises a ValueError due to a bug
277 # however, it's not clear when or if that will be fixed, so catch it
278 # here for now
279 raises=ValueError,
280 )
281 def is_host_name(instance: object) -> bool:
282 if not isinstance(instance, str):
283 return True
284 return FQDN(instance, min_labels=1).is_valid
287with suppress(ImportError):
288 # The built-in `idna` codec only implements RFC 3890, so we go elsewhere.
289 import idna
291 @_checks_drafts(
292 draft7="idn-hostname",
293 draft201909="idn-hostname",
294 draft202012="idn-hostname",
295 raises=(idna.IDNAError, UnicodeError),
296 )
297 def is_idn_host_name(instance: object) -> bool:
298 if not isinstance(instance, str):
299 return True
300 idna.encode(instance)
301 return True
304try:
305 import rfc3987
306except ImportError:
307 with suppress(ImportError):
308 from rfc3986_validator import validate_rfc3986
310 @_checks_drafts(name="uri")
311 def is_uri(instance: object) -> bool:
312 if not isinstance(instance, str):
313 return True
314 return validate_rfc3986(instance, rule="URI")
316 @_checks_drafts(
317 draft6="uri-reference",
318 draft7="uri-reference",
319 draft201909="uri-reference",
320 draft202012="uri-reference",
321 raises=ValueError,
322 )
323 def is_uri_reference(instance: object) -> bool:
324 if not isinstance(instance, str):
325 return True
326 return validate_rfc3986(instance, rule="URI_reference")
328 with suppress(ImportError):
329 from rfc3987_syntax import is_valid_syntax as _rfc3987_is_valid_syntax
331 @_checks_drafts(
332 draft7="iri",
333 draft201909="iri",
334 draft202012="iri",
335 raises=ValueError,
336 )
337 def is_iri(instance: object) -> bool:
338 if not isinstance(instance, str):
339 return True
340 return _rfc3987_is_valid_syntax("iri", instance)
342 @_checks_drafts(
343 draft7="iri-reference",
344 draft201909="iri-reference",
345 draft202012="iri-reference",
346 raises=ValueError,
347 )
348 def is_iri_reference(instance: object) -> bool:
349 if not isinstance(instance, str):
350 return True
351 return _rfc3987_is_valid_syntax("iri_reference", instance)
353else:
355 @_checks_drafts(
356 draft7="iri",
357 draft201909="iri",
358 draft202012="iri",
359 raises=ValueError,
360 )
361 def is_iri(instance: object) -> bool:
362 if not isinstance(instance, str):
363 return True
364 return rfc3987.parse(instance, rule="IRI")
366 @_checks_drafts(
367 draft7="iri-reference",
368 draft201909="iri-reference",
369 draft202012="iri-reference",
370 raises=ValueError,
371 )
372 def is_iri_reference(instance: object) -> bool:
373 if not isinstance(instance, str):
374 return True
375 return rfc3987.parse(instance, rule="IRI_reference")
377 @_checks_drafts(name="uri", raises=ValueError)
378 def is_uri(instance: object) -> bool:
379 if not isinstance(instance, str):
380 return True
381 return rfc3987.parse(instance, rule="URI")
383 @_checks_drafts(
384 draft6="uri-reference",
385 draft7="uri-reference",
386 draft201909="uri-reference",
387 draft202012="uri-reference",
388 raises=ValueError,
389 )
390 def is_uri_reference(instance: object) -> bool:
391 if not isinstance(instance, str):
392 return True
393 return rfc3987.parse(instance, rule="URI_reference")
396with suppress(ImportError):
397 from rfc3339_validator import validate_rfc3339
399 @_checks_drafts(name="date-time")
400 def is_datetime(instance: object) -> bool:
401 if not isinstance(instance, str):
402 return True
403 return validate_rfc3339(instance.upper())
405 @_checks_drafts(
406 draft7="time",
407 draft201909="time",
408 draft202012="time",
409 )
410 def is_time(instance: object) -> bool:
411 if not isinstance(instance, str):
412 return True
413 return is_datetime("1970-01-01T" + instance)
416@_checks_drafts(name="regex", raises=re.error)
417def is_regex(instance: object) -> bool:
418 if not isinstance(instance, str):
419 return True
420 return bool(re.compile(instance))
423@_checks_drafts(
424 draft3="date",
425 draft7="date",
426 draft201909="date",
427 draft202012="date",
428 raises=ValueError,
429)
430def is_date(instance: object) -> bool:
431 if not isinstance(instance, str):
432 return True
433 return bool(_RE_DATE.fullmatch(instance) and date.fromisoformat(instance))
436@_checks_drafts(draft3="time", raises=ValueError)
437def is_draft3_time(instance: object) -> bool:
438 if not isinstance(instance, str):
439 return True
440 return bool(datetime.strptime(instance, "%H:%M:%S")) # noqa: DTZ007
443with suppress(ImportError):
444 import webcolors
446 @_checks_drafts(draft3="color", raises=(ValueError, TypeError))
447 def is_css21_color(instance: object) -> bool:
448 if isinstance(instance, str):
449 try:
450 webcolors.name_to_hex(instance)
451 except ValueError:
452 webcolors.normalize_hex(instance.lower())
453 return True
456with suppress(ImportError):
457 import jsonpointer
459 @_checks_drafts(
460 draft6="json-pointer",
461 draft7="json-pointer",
462 draft201909="json-pointer",
463 draft202012="json-pointer",
464 raises=jsonpointer.JsonPointerException,
465 )
466 def is_json_pointer(instance: object) -> bool:
467 if not isinstance(instance, str):
468 return True
469 return bool(jsonpointer.JsonPointer(instance))
471 # TODO: I don't want to maintain this, so it
472 # needs to go either into jsonpointer (pending
473 # https://github.com/stefankoegl/python-json-pointer/issues/34) or
474 # into a new external library.
475 @_checks_drafts(
476 draft7="relative-json-pointer",
477 draft201909="relative-json-pointer",
478 draft202012="relative-json-pointer",
479 raises=jsonpointer.JsonPointerException,
480 )
481 def is_relative_json_pointer(instance: object) -> bool:
482 # Definition taken from:
483 # https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3
484 if not isinstance(instance, str):
485 return True
486 if not instance:
487 return False
489 # A non-negative-integer prefix of ASCII digits (%x30-39), which is
490 # either "0" or has no leading "0".
491 rest = instance.lstrip(string.digits)
492 prefix = instance[:len(instance) - len(rest)]
493 if not prefix:
494 return False
495 if len(prefix) > 1 and prefix[0] == "0":
496 return False
497 return (rest == "#") or bool(jsonpointer.JsonPointer(rest))
500with suppress(ImportError):
501 import uri_template
503 @_checks_drafts(
504 draft6="uri-template",
505 draft7="uri-template",
506 draft201909="uri-template",
507 draft202012="uri-template",
508 )
509 def is_uri_template(instance: object) -> bool:
510 if not isinstance(instance, str):
511 return True
512 return uri_template.validate(instance)
515with suppress(ImportError):
516 import isoduration
518 @_checks_drafts(
519 draft201909="duration",
520 draft202012="duration",
521 raises=isoduration.DurationParsingException,
522 )
523 def is_duration(instance: object) -> bool:
524 if not isinstance(instance, str):
525 return True
526 isoduration.parse_duration(instance)
527 # FIXME: See bolsote/isoduration#25 and bolsote/isoduration#21
528 return instance.endswith(tuple("DMYWHMS"))
531@_checks_drafts(
532 draft201909="uuid",
533 draft202012="uuid",
534 raises=ValueError,
535)
536def is_uuid(instance: object) -> bool:
537 if not isinstance(instance, str):
538 return True
539 return str(UUID(instance)) == instance.lower()