Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jsonschema/_format.py: 43%
233 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +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#: A format checker callable.
15_F = typing.TypeVar("_F", bound=_FormatCheckCallable)
16_RaisesType = typing.Union[
17 typing.Type[Exception], typing.Tuple[typing.Type[Exception], ...],
18]
20_RE_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$", re.ASCII)
23class FormatChecker:
24 """
25 A ``format`` property checker.
27 JSON Schema does not mandate that the ``format`` property actually do any
28 validation. If validation is desired however, instances of this class can
29 be hooked into validators to enable format validation.
31 `FormatChecker` objects always return ``True`` when asked about
32 formats that they do not know how to validate.
34 To add a check for a custom format use the `FormatChecker.checks`
35 decorator.
37 Arguments:
39 formats:
41 The known formats to validate. This argument can be used to
42 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( # noqa: D417
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.
78 """ # noqa: D214,D405 (charliermarsh/ruff#3547)
80 def _checks(func: _F) -> _F:
81 self.checkers[format] = (func, raises)
82 return func
84 return _checks
86 @classmethod
87 def cls_checks(
88 cls, format: str, raises: _RaisesType = (),
89 ) -> typing.Callable[[_F], _F]:
90 warnings.warn(
91 (
92 "FormatChecker.cls_checks is deprecated. Call "
93 "FormatChecker.checks on a specific FormatChecker instance "
94 "instead."
95 ),
96 DeprecationWarning,
97 stacklevel=2,
98 )
99 return cls._cls_checks(format=format, raises=raises)
101 @classmethod
102 def _cls_checks(
103 cls, format: str, raises: _RaisesType = (),
104 ) -> typing.Callable[[_F], _F]:
105 def _checks(func: _F) -> _F:
106 cls.checkers[format] = (func, raises)
107 return func
109 return _checks
111 def check(self, instance: object, format: str) -> None:
112 """
113 Check whether the instance conforms to the given format.
115 Arguments:
117 instance (*any primitive type*, i.e. str, number, bool):
119 The instance to check
121 format:
123 The format that instance should conform to
125 Raises:
127 FormatError:
129 if the instance does not conform to ``format``
130 """
131 if format not in self.checkers:
132 return
134 func, raises = self.checkers[format]
135 result, cause = None, None
136 try:
137 result = func(instance)
138 except raises as e:
139 cause = e
140 if not result:
141 raise FormatError(f"{instance!r} is not a {format!r}", cause=cause)
143 def conforms(self, instance: object, format: str) -> bool:
144 """
145 Check whether the instance conforms to the given format.
147 Arguments:
149 instance (*any primitive type*, i.e. str, number, bool):
151 The instance to check
153 format:
155 The format that instance should conform to
157 Returns:
159 bool: whether it conformed
160 """
161 try:
162 self.check(instance, format)
163 except FormatError:
164 return False
165 else:
166 return True
169draft3_format_checker = FormatChecker()
170draft4_format_checker = FormatChecker()
171draft6_format_checker = FormatChecker()
172draft7_format_checker = FormatChecker()
173draft201909_format_checker = FormatChecker()
174draft202012_format_checker = FormatChecker()
176_draft_checkers: dict[str, FormatChecker] = dict(
177 draft3=draft3_format_checker,
178 draft4=draft4_format_checker,
179 draft6=draft6_format_checker,
180 draft7=draft7_format_checker,
181 draft201909=draft201909_format_checker,
182 draft202012=draft202012_format_checker,
183)
186def _checks_drafts(
187 name=None,
188 draft3=None,
189 draft4=None,
190 draft6=None,
191 draft7=None,
192 draft201909=None,
193 draft202012=None,
194 raises=(),
195) -> typing.Callable[[_F], _F]:
196 draft3 = draft3 or name
197 draft4 = draft4 or name
198 draft6 = draft6 or name
199 draft7 = draft7 or name
200 draft201909 = draft201909 or name
201 draft202012 = draft202012 or name
203 def wrap(func: _F) -> _F:
204 if draft3:
205 func = _draft_checkers["draft3"].checks(draft3, raises)(func)
206 if draft4:
207 func = _draft_checkers["draft4"].checks(draft4, raises)(func)
208 if draft6:
209 func = _draft_checkers["draft6"].checks(draft6, raises)(func)
210 if draft7:
211 func = _draft_checkers["draft7"].checks(draft7, raises)(func)
212 if draft201909:
213 func = _draft_checkers["draft201909"].checks(draft201909, raises)(
214 func,
215 )
216 if draft202012:
217 func = _draft_checkers["draft202012"].checks(draft202012, raises)(
218 func,
219 )
221 # Oy. This is bad global state, but relied upon for now, until
222 # deprecation. See #519 and test_format_checkers_come_with_defaults
223 FormatChecker._cls_checks(
224 draft202012 or draft201909 or draft7 or draft6 or draft4 or draft3,
225 raises,
226 )(func)
227 return func
229 return wrap
232@_checks_drafts(name="idn-email")
233@_checks_drafts(name="email")
234def is_email(instance: object) -> bool:
235 if not isinstance(instance, str):
236 return True
237 return "@" in instance
240@_checks_drafts(
241 draft3="ip-address",
242 draft4="ipv4",
243 draft6="ipv4",
244 draft7="ipv4",
245 draft201909="ipv4",
246 draft202012="ipv4",
247 raises=ipaddress.AddressValueError,
248)
249def is_ipv4(instance: object) -> bool:
250 if not isinstance(instance, str):
251 return True
252 return bool(ipaddress.IPv4Address(instance))
255@_checks_drafts(name="ipv6", raises=ipaddress.AddressValueError)
256def is_ipv6(instance: object) -> bool:
257 if not isinstance(instance, str):
258 return True
259 address = ipaddress.IPv6Address(instance)
260 return not getattr(address, "scope_id", "")
263with suppress(ImportError):
264 from fqdn import FQDN
266 @_checks_drafts(
267 draft3="host-name",
268 draft4="hostname",
269 draft6="hostname",
270 draft7="hostname",
271 draft201909="hostname",
272 draft202012="hostname",
273 )
274 def is_host_name(instance: object) -> bool:
275 if not isinstance(instance, str):
276 return True
277 return FQDN(instance, min_labels=1).is_valid
280with suppress(ImportError):
281 # The built-in `idna` codec only implements RFC 3890, so we go elsewhere.
282 import idna
284 @_checks_drafts(
285 draft7="idn-hostname",
286 draft201909="idn-hostname",
287 draft202012="idn-hostname",
288 raises=(idna.IDNAError, UnicodeError),
289 )
290 def is_idn_host_name(instance: object) -> bool:
291 if not isinstance(instance, str):
292 return True
293 idna.encode(instance)
294 return True
297try:
298 import rfc3987
299except ImportError:
300 with suppress(ImportError):
301 from rfc3986_validator import validate_rfc3986
303 @_checks_drafts(name="uri")
304 def is_uri(instance: object) -> bool:
305 if not isinstance(instance, str):
306 return True
307 return validate_rfc3986(instance, rule="URI")
309 @_checks_drafts(
310 draft6="uri-reference",
311 draft7="uri-reference",
312 draft201909="uri-reference",
313 draft202012="uri-reference",
314 raises=ValueError,
315 )
316 def is_uri_reference(instance: object) -> bool:
317 if not isinstance(instance, str):
318 return True
319 return validate_rfc3986(instance, rule="URI_reference")
321else:
323 @_checks_drafts(
324 draft7="iri",
325 draft201909="iri",
326 draft202012="iri",
327 raises=ValueError,
328 )
329 def is_iri(instance: object) -> bool:
330 if not isinstance(instance, str):
331 return True
332 return rfc3987.parse(instance, rule="IRI")
334 @_checks_drafts(
335 draft7="iri-reference",
336 draft201909="iri-reference",
337 draft202012="iri-reference",
338 raises=ValueError,
339 )
340 def is_iri_reference(instance: object) -> bool:
341 if not isinstance(instance, str):
342 return True
343 return rfc3987.parse(instance, rule="IRI_reference")
345 @_checks_drafts(name="uri", raises=ValueError)
346 def is_uri(instance: object) -> bool:
347 if not isinstance(instance, str):
348 return True
349 return rfc3987.parse(instance, rule="URI")
351 @_checks_drafts(
352 draft6="uri-reference",
353 draft7="uri-reference",
354 draft201909="uri-reference",
355 draft202012="uri-reference",
356 raises=ValueError,
357 )
358 def is_uri_reference(instance: object) -> bool:
359 if not isinstance(instance, str):
360 return True
361 return rfc3987.parse(instance, rule="URI_reference")
364with suppress(ImportError):
365 from rfc3339_validator import validate_rfc3339
367 @_checks_drafts(name="date-time")
368 def is_datetime(instance: object) -> bool:
369 if not isinstance(instance, str):
370 return True
371 return validate_rfc3339(instance.upper())
373 @_checks_drafts(
374 draft7="time",
375 draft201909="time",
376 draft202012="time",
377 )
378 def is_time(instance: object) -> bool:
379 if not isinstance(instance, str):
380 return True
381 return is_datetime("1970-01-01T" + instance)
384@_checks_drafts(name="regex", raises=re.error)
385def is_regex(instance: object) -> bool:
386 if not isinstance(instance, str):
387 return True
388 return bool(re.compile(instance))
391@_checks_drafts(
392 draft3="date",
393 draft7="date",
394 draft201909="date",
395 draft202012="date",
396 raises=ValueError,
397)
398def is_date(instance: object) -> bool:
399 if not isinstance(instance, str):
400 return True
401 return bool(
402 _RE_DATE.fullmatch(instance)
403 and datetime.date.fromisoformat(instance)
404 )
407@_checks_drafts(draft3="time", raises=ValueError)
408def is_draft3_time(instance: object) -> bool:
409 if not isinstance(instance, str):
410 return True
411 return bool(datetime.datetime.strptime(instance, "%H:%M:%S"))
414with suppress(ImportError):
415 from webcolors import CSS21_NAMES_TO_HEX
416 import webcolors
418 def is_css_color_code(instance: object) -> bool:
419 return webcolors.normalize_hex(instance)
421 @_checks_drafts(draft3="color", raises=(ValueError, TypeError))
422 def is_css21_color(instance: object) -> bool:
423 if (
424 not isinstance(instance, str)
425 or instance.lower() in CSS21_NAMES_TO_HEX
426 ):
427 return True
428 return is_css_color_code(instance)
431with suppress(ImportError):
432 import jsonpointer
434 @_checks_drafts(
435 draft6="json-pointer",
436 draft7="json-pointer",
437 draft201909="json-pointer",
438 draft202012="json-pointer",
439 raises=jsonpointer.JsonPointerException,
440 )
441 def is_json_pointer(instance: object) -> bool:
442 if not isinstance(instance, str):
443 return True
444 return bool(jsonpointer.JsonPointer(instance))
446 # TODO: I don't want to maintain this, so it
447 # needs to go either into jsonpointer (pending
448 # https://github.com/stefankoegl/python-json-pointer/issues/34) or
449 # into a new external library.
450 @_checks_drafts(
451 draft7="relative-json-pointer",
452 draft201909="relative-json-pointer",
453 draft202012="relative-json-pointer",
454 raises=jsonpointer.JsonPointerException,
455 )
456 def is_relative_json_pointer(instance: object) -> bool:
457 # Definition taken from:
458 # https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3
459 if not isinstance(instance, str):
460 return True
461 if not instance:
462 return False
464 non_negative_integer, rest = [], ""
465 for i, character in enumerate(instance):
466 if character.isdigit():
467 # digits with a leading "0" are not allowed
468 if i > 0 and int(instance[i - 1]) == 0:
469 return False
471 non_negative_integer.append(character)
472 continue
474 if not non_negative_integer:
475 return False
477 rest = instance[i:]
478 break
479 return (rest == "#") or bool(jsonpointer.JsonPointer(rest))
482with suppress(ImportError):
483 import uri_template
485 @_checks_drafts(
486 draft6="uri-template",
487 draft7="uri-template",
488 draft201909="uri-template",
489 draft202012="uri-template",
490 )
491 def is_uri_template(instance: object) -> bool:
492 if not isinstance(instance, str):
493 return True
494 return uri_template.validate(instance)
497with suppress(ImportError):
498 import isoduration
500 @_checks_drafts(
501 draft201909="duration",
502 draft202012="duration",
503 raises=isoduration.DurationParsingException,
504 )
505 def is_duration(instance: object) -> bool:
506 if not isinstance(instance, str):
507 return True
508 isoduration.parse_duration(instance)
509 # FIXME: See bolsote/isoduration#25 and bolsote/isoduration#21
510 return instance.endswith(tuple("DMYWHMS"))
513@_checks_drafts(
514 draft201909="uuid",
515 draft202012="uuid",
516 raises=ValueError,
517)
518def is_uuid(instance: object) -> bool:
519 if not isinstance(instance, str):
520 return True
521 UUID(instance)
522 return all(instance[position] == "-" for position in (8, 13, 18, 23))