Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/glom/matching.py: 39%
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
1"""
2.. versionadded:: 20.7.0
4Sometimes you want to confirm that your target data matches your
5code's assumptions. With glom, you don't need a separate validation
6step, you can do these checks inline with your glom spec, using
7:class:`~glom.Match` and friends.
8"""
10import re
11import sys
12from pprint import pprint
14from boltons.iterutils import is_iterable
15from boltons.typeutils import make_sentinel
17from .core import GlomError, glom, T, MODE, bbrepr, bbformat, format_invocation, Path, chain_child, Val, arg_val
20_MISSING = make_sentinel('_MISSING')
23# NOTE: it is important that MatchErrors be cheap to construct,
24# because negative matches are part of normal control flow
25# (e.g. often it is idiomatic to cascade from one possible match
26# to the next and take the first one that works)
27class MatchError(GlomError):
28 """
29 Raised when a :class:`Match` or :data:`M` check fails.
31 >>> glom({123: 'a'}, Match({'id': int}))
32 Traceback (most recent call last):
33 ...
34 MatchError: key 123 didn't match any of ['id']
36 """
37 def __init__(self, fmt, *args):
38 super().__init__(fmt, *args)
40 def get_message(self):
41 fmt, args = self.args[0], self.args[1:]
42 return bbformat(fmt, *args)
45class TypeMatchError(MatchError, TypeError):
46 """:exc:`MatchError` subtype raised when a
47 :class:`Match` fails a type check.
49 >>> glom({'id': 'a'}, Match({'id': int}))
50 Traceback (most recent call last):
51 ...
52 TypeMatchError: error raised while processing.
53 Target-spec trace, with error detail (most recent last):
54 - Target: {'id': 'a'}
55 - Spec: Match({'id': <type 'int'>})
56 - Spec: {'id': <type 'int'>}
57 - Target: 'a'
58 - Spec: int
59 TypeMatchError: expected type int, not str
60 """
62 def __init__(self, actual, expected):
63 super().__init__(
64 "expected type {0.__name__}, not {1.__name__}", expected, actual)
66 def __copy__(self):
67 # __init__ args = (actual, expected)
68 # self.args = (fmt_str, expected, actual)
69 return TypeMatchError(self.args[2], self.args[1])
72class Match:
73 """glom's ``Match`` specifier type enables a new mode of glom usage:
74 pattern matching. In particular, this mode has been designed for
75 nested data validation.
77 Pattern specs are evaluated as follows:
79 1. Spec instances are always evaluated first
80 2. Types match instances of that type
81 3. Instances of :class:`dict`, :class:`list`, :class:`tuple`,
82 :class:`set`, and :class:`frozenset` are matched recursively
83 4. Any other values are compared for equality to the target with
84 ``==``
86 By itself, this allows to assert that structures match certain
87 patterns, and may be especially familiar to users of the `schema`_
88 library.
90 For example, let's load some data::
92 >>> target = [
93 ... {'id': 1, 'email': 'alice@example.com'},
94 ... {'id': 2, 'email': 'bob@example.com'}]
96 A :class:`Match` pattern can be used to ensure this data is in its expected form:
98 >>> spec = Match([{'id': int, 'email': str}])
100 This ``spec`` succinctly describes our data structure's pattern
101 Specifically, a :class:`list` of :class:`dict` objects, each of
102 which has exactly two keys, ``'id'`` and ``'email'``, whose values are
103 an :class:`int` and :class:`str`, respectively. Now,
104 :func:`~glom.glom` will ensure our ``target`` matches our pattern
105 ``spec``:
107 >>> result = glom(target, spec)
108 >>> assert result == \\
109 ... [{'id': 1, 'email': 'alice@example.com'}, {'id': 2, 'email': 'bob@example.com'}]
111 With a more complex :class:`Match` spec, we can be more precise:
113 >>> spec = Match([{'id': And(M > 0, int), 'email': Regex('[^@]+@[^@]+')}])
115 :class:`~glom.And` allows multiple conditions to be applied.
116 :class:`~glom.Regex` evaluates the regular expression against the
117 target value under the ``'email'`` key. In this case, we take a
118 simple approach: an email has exactly one ``@``, with at least one
119 character before and after.
121 Finally, :attr:`~glom.M` is our stand-in for the current target
122 we're matching against, allowing us to perform in-line comparisons
123 using Python's native greater-than operator (as well as
124 others). We apply our :class:`Match` pattern as before::
126 >>> assert glom(target, spec) == \\
127 ... [{'id': 1, 'email': 'alice@example.com'}, {'id': 2, 'email': 'bob@example.com'}]
129 And as usual, upon a successful match, we get the matched result.
131 .. note::
133 For Python 3.6+ where dictionaries are ordered, keys in the target
134 are matched against keys in the spec in their insertion order.
136 .. _schema: https://github.com/keleshev/schema
138 Args:
139 spec: The glomspec representing the pattern to match data against.
140 default: The default value to be returned if a match fails. If not
141 set, a match failure will raise a :class:`MatchError`.
143 """
144 def __init__(self, spec, default=_MISSING):
145 self.spec = spec
146 self.default = default
148 def glomit(self, target, scope):
149 scope[MODE] = _glom_match
150 try:
151 ret = scope[glom](target, self.spec, scope)
152 except GlomError:
153 if self.default is _MISSING:
154 raise
155 ret = arg_val(target, self.default, scope)
156 return ret
158 def verify(self, target):
159 """A convenience function a :class:`Match` instance which returns the
160 matched value when *target* matches, or raises a
161 :exc:`MatchError` when it does not.
163 Args:
164 target: Target value or data structure to match against.
166 Raises:
167 glom.MatchError
169 """
170 return glom(target, self)
172 def matches(self, target):
173 """A convenience method on a :class:`Match` instance, returns
174 ``True`` if the *target* matches, ``False`` if not.
176 >>> Match(int).matches(-1.0)
177 False
179 Args:
180 target: Target value or data structure to match against.
181 """
182 try:
183 glom(target, self)
184 except GlomError:
185 return False
186 return True
188 def __repr__(self):
189 return f'{self.__class__.__name__}({bbrepr(self.spec)})'
192_RE_FULLMATCH = getattr(re, "fullmatch", None)
193_RE_VALID_FUNCS = {_RE_FULLMATCH, None, re.search, re.match}
194_RE_FUNC_ERROR = ValueError("'func' must be one of %s" % (", ".join(
195 sorted(e and e.__name__ or "None" for e in _RE_VALID_FUNCS))))
197_RE_TYPES = ()
198try: re.match("", "")
199except Exception: pass # pragma: no cover
200else: _RE_TYPES += (str,)
201try: re.match(b"", b"")
202except Exception: pass # pragma: no cover
203else: _RE_TYPES += (bytes,)
206class Regex:
207 """
208 checks that target is a string which matches the passed regex pattern
210 raises MatchError if there isn't a match; returns Target if match
212 variables captures in regex are added to the scope so they can
213 be used by downstream processes
214 """
215 __slots__ = ('flags', 'func', 'match_func', 'pattern')
217 def __init__(self, pattern, flags=0, func=None):
218 if func not in _RE_VALID_FUNCS:
219 raise _RE_FUNC_ERROR
220 regex = re.compile(pattern, flags)
221 if func is re.match:
222 match_func = regex.match
223 elif func is re.search:
224 match_func = regex.search
225 else:
226 if _RE_FULLMATCH:
227 match_func = regex.fullmatch
228 else:
229 regex = re.compile(fr"(?:{pattern})\Z", flags)
230 match_func = regex.match
231 self.flags, self.func = flags, func
232 self.match_func, self.pattern = match_func, pattern
234 def glomit(self, target, scope):
235 if type(target) not in _RE_TYPES:
236 raise MatchError(
237 "{0!r} not valid as a Regex target -- expected {1!r}", type(target), _RE_TYPES)
238 match = self.match_func(target)
239 if not match:
240 raise MatchError("target did not match pattern {0!r}", self.pattern)
241 scope.update(match.groupdict())
242 return target
244 def __repr__(self):
245 args = '(' + bbrepr(self.pattern)
246 if self.flags:
247 args += ', flags=' + bbrepr(self.flags)
248 if self.func is not None:
249 args += ', func=' + self.func.__name__
250 args += ')'
251 return self.__class__.__name__ + args
254#TODO: combine this with other functionality elsewhere?
255def _bool_child_repr(child):
256 if child is M:
257 return repr(child)
258 elif isinstance(child, _MExpr):
259 return "(" + bbrepr(child) + ")"
260 return bbrepr(child)
263class _Bool:
264 def __init__(self, *children, **kw):
265 self.children = children
266 if not children:
267 raise ValueError("need at least one operand for {}".format(
268 self.__class__.__name__))
269 self.default = kw.pop('default', _MISSING)
270 if kw:
271 raise TypeError('got unexpected kwargs: %r' % list(kw.keys()))
273 def __and__(self, other):
274 return And(self, other)
276 def __or__(self, other):
277 return Or(self, other)
279 def __invert__(self):
280 return Not(self)
282 def glomit(self, target, scope):
283 try:
284 return self._glomit(target, scope)
285 except GlomError:
286 if self.default is not _MISSING:
287 return arg_val(target, self.default, scope)
288 raise
290 def _m_repr(self):
291 """should this Or() repr as M |?"""
292 # only used by And() and Or(), not Not(), so len(children) >= 1
293 if isinstance(self.children[0], (_MType, _MExpr)):
294 return True
295 if type(self.children[0]) in (And, Or, Not):
296 return self.children[0]._m_repr()
297 return False
299 def __repr__(self):
300 child_reprs = [_bool_child_repr(c) for c in self.children]
301 if self._m_repr() and self.default is _MISSING:
302 return f" {self.OP} ".join(child_reprs)
303 if self.default is not _MISSING:
304 child_reprs.append("default=" + repr(self.default))
305 return self.__class__.__name__ + "(" + ", ".join(child_reprs) + ")"
308class And(_Bool):
309 """
310 Applies child specs one after the other to the target; if none of the
311 specs raises `GlomError`, returns the last result.
312 """
313 OP = "&"
314 __slots__ = ('children',)
316 def _glomit(self, target, scope):
317 # all children must match without exception
318 result = target # so that And() == True, similar to all([]) == True
319 for child in self.children:
320 result = scope[glom](target, child, scope)
321 return result
323 def __and__(self, other):
324 # reduce number of layers of spec
325 return And(*(self.children + (other,)))
328class Or(_Bool):
329 """
330 Tries to apply the first child spec to the target, and return the result.
331 If `GlomError` is raised, try the next child spec until there are no
332 all child specs have been tried, then raise `MatchError`.
333 """
334 OP = "|"
335 __slots__ = ('children',)
337 def _glomit(self, target, scope):
338 for child in self.children[:-1]:
339 try: # one child must match without exception
340 return scope[glom](target, child, scope)
341 except GlomError:
342 pass
343 return scope[glom](target, self.children[-1], scope)
345 def __or__(self, other):
346 # reduce number of layers of spec
347 return Or(*(self.children + (other,)))
350class Not(_Bool):
351 """
352 Inverts the *child*. Child spec will be expected to raise
353 :exc:`GlomError` (or subtype), in which case the target will be returned.
355 If the child spec does not raise :exc:`GlomError`, :exc:`MatchError`
356 will be raised.
357 """
358 __slots__ = ('child',)
360 def __init__(self, child):
361 self.child = child
363 def glomit(self, target, scope):
364 try: # one child must match without exception
365 scope[glom](target, self.child, scope)
366 except GlomError:
367 return target
368 else:
369 raise GlomError("child shouldn't have passed", self.child)
371 def _m_repr(self):
372 if isinstance(self.child, (_MType, _MExpr)):
373 return True
374 if type(self.child) not in (And, Or, Not):
375 return False
376 return self.child._m_repr()
378 def __repr__(self):
379 if self.child is M:
380 return '~M'
381 if self._m_repr(): # is in M repr
382 return "~(" + bbrepr(self.child) + ")"
383 return "Not(" + bbrepr(self.child) + ")"
386_M_OP_MAP = {'=': '==', '!': '!=', 'g': '>=', 'l': '<='}
389class _MSubspec:
390 """used by MType.__call__ to wrap a sub-spec for comparison"""
391 __slots__ = ('spec')
393 def __init__(self, spec):
394 self.spec = spec
396 def __eq__(self, other):
397 return _MExpr(self, '=', other)
399 def __ne__(self, other):
400 return _MExpr(self, '!', other)
402 def __gt__(self, other):
403 return _MExpr(self, '>', other)
405 def __lt__(self, other):
406 return _MExpr(self, '<', other)
408 def __ge__(self, other):
409 return _MExpr(self, 'g', other)
411 def __le__(self, other):
412 return _MExpr(self, 'l', other)
414 def __repr__(self):
415 return f'M({bbrepr(self.spec)})'
417 def glomit(self, target, scope):
418 match = scope[glom](target, self.spec, scope)
419 if match:
420 return target
421 raise MatchError('expected truthy value from {0!r}, got {1!r}', self.spec, match)
424class _MExpr:
425 __slots__ = ('lhs', 'op', 'rhs')
427 def __init__(self, lhs, op, rhs):
428 self.lhs, self.op, self.rhs = lhs, op, rhs
430 def __and__(self, other):
431 return And(self, other)
433 __rand__ = __and__
435 def __or__(self, other):
436 return Or(self, other)
438 def __invert__(self):
439 return Not(self)
441 def glomit(self, target, scope):
442 lhs, op, rhs = self.lhs, self.op, self.rhs
443 if lhs is M:
444 lhs = target
445 if rhs is M:
446 rhs = target
447 if type(lhs) is _MSubspec:
448 lhs = scope[glom](target, lhs.spec, scope)
449 if type(rhs) is _MSubspec:
450 rhs = scope[glom](target, rhs.spec, scope)
451 matched = (
452 (op == '=' and lhs == rhs) or
453 (op == '!' and lhs != rhs) or
454 (op == '>' and lhs > rhs) or
455 (op == '<' and lhs < rhs) or
456 (op == 'g' and lhs >= rhs) or
457 (op == 'l' and lhs <= rhs)
458 )
459 if matched:
460 return target
461 raise MatchError("{0!r} {1} {2!r}", lhs, _M_OP_MAP.get(op, op), rhs)
463 def __repr__(self):
464 op = _M_OP_MAP.get(self.op, self.op)
465 return f"{self.lhs!r} {op} {self.rhs!r}"
468class _MType:
469 """:attr:`~glom.M` is similar to :attr:`~glom.T`, a stand-in for the
470 current target, but where :attr:`~glom.T` allows for attribute and
471 key access and method calls, :attr:`~glom.M` allows for comparison
472 operators.
474 If a comparison succeeds, the target is returned unchanged.
475 If a comparison fails, :class:`~glom.MatchError` is thrown.
477 Some examples:
479 >>> glom(1, M > 0)
480 1
481 >>> glom(0, M == 0)
482 0
483 >>> glom('a', M != 'b') == 'a'
484 True
486 :attr:`~glom.M` by itself evaluates the current target for truthiness.
487 For example, `M | Val(None)` is a simple idiom for normalizing all falsey values to None:
489 >>> from glom import Val
490 >>> glom([0, False, "", None], [M | Val(None)])
491 [None, None, None, None]
493 For convenience, ``&`` and ``|`` operators are overloaded to
494 construct :attr:`~glom.And` and :attr:`~glom.Or` instances.
496 >>> glom(1.0, (M > 0) & float)
497 1.0
499 .. note::
501 Python's operator overloading may make for concise code,
502 but it has its limits.
504 Because bitwise operators (``&`` and ``|``) have higher precedence
505 than comparison operators (``>``, ``<``, etc.), expressions must
506 be parenthesized.
508 >>> M > 0 & float
509 Traceback (most recent call last):
510 ...
511 TypeError: unsupported operand type(s) for &: 'int' and 'type'
513 Similarly, because of special handling around ternary
514 comparisons (``1 < M < 5``) are implemented via
515 short-circuiting evaluation, they also cannot be captured by
516 :data:`M`.
518 """
519 __slots__ = ()
521 def __call__(self, spec):
522 """wrap a sub-spec in order to apply comparison operators to the result"""
523 if not isinstance(spec, type(T)):
524 # TODO: open this up for other specs so we can do other
525 # checks, like function calls
526 raise TypeError("M() only accepts T-style specs, not %s" % type(spec).__name__)
527 return _MSubspec(spec)
529 def __eq__(self, other):
530 return _MExpr(self, '=', other)
532 def __ne__(self, other):
533 return _MExpr(self, '!', other)
535 def __gt__(self, other):
536 return _MExpr(self, '>', other)
538 def __lt__(self, other):
539 return _MExpr(self, '<', other)
541 def __ge__(self, other):
542 return _MExpr(self, 'g', other)
544 def __le__(self, other):
545 return _MExpr(self, 'l', other)
547 def __and__(self, other):
548 return And(self, other)
550 __rand__ = __and__
552 def __or__(self, other):
553 return Or(self, other)
555 def __invert__(self):
556 return Not(self)
558 def __repr__(self):
559 return "M"
561 def glomit(self, target, spec):
562 if target:
563 return target
564 raise MatchError("{0!r} not truthy", target)
567M = _MType()
571class Optional:
572 """Used as a :class:`dict` key in a :class:`~glom.Match()` spec,
573 marks that a value match key which would otherwise be required is
574 optional and should not raise :exc:`~glom.MatchError` even if no
575 keys match.
577 For example::
579 >>> spec = Match({Optional("name"): str})
580 >>> glom({"name": "alice"}, spec)
581 {'name': 'alice'}
582 >>> glom({}, spec)
583 {}
584 >>> spec = Match({Optional("name", default=""): str})
585 >>> glom({}, spec)
586 {'name': ''}
588 """
589 __slots__ = ('key', 'default')
591 def __init__(self, key, default=_MISSING):
592 if type(key) in (Required, Optional):
593 raise TypeError("double wrapping of Optional")
594 hash(key) # ensure is a valid key
595 if _precedence(key) != 0:
596 raise ValueError(f"Optional() keys must be == match constants, not {key!r}")
597 self.key, self.default = key, default
599 def glomit(self, target, scope):
600 if target != self.key:
601 raise MatchError("target {0} != spec {1}", target, self.key)
602 return target
604 def __repr__(self):
605 return f'{self.__class__.__name__}({bbrepr(self.key)})'
608class Required:
609 """Used as a :class:`dict` key in :class:`~glom.Match()` mode, marks
610 that a key which might otherwise not be required should raise
611 :exc:`~glom.MatchError` if the key in the target does not match.
613 For example::
615 >>> spec = Match({object: object})
617 This spec will match any dict, because :class:`object` is the base
618 type of every object::
620 >>> glom({}, spec)
621 {}
623 ``{}`` will also match because match mode does not require at
624 least one match by default. If we want to require that a key
625 matches, we can use :class:`~glom.Required`::
627 >>> spec = Match({Required(object): object})
628 >>> glom({}, spec)
629 Traceback (most recent call last):
630 ...
631 MatchError: error raised while processing.
632 Target-spec trace, with error detail (most recent last):
633 - Target: {}
634 - Spec: Match({Required(object): <type 'object'>})
635 - Spec: {Required(object): <type 'object'>}
636 MatchError: target missing expected keys Required(object)
638 Now our spec requires at least one key of any type. You can refine
639 the spec by putting more specific subpatterns inside of
640 :class:`~glom.Required`.
642 """
643 __slots__ = ('key',)
645 def __init__(self, key):
646 if type(key) in (Required, Optional):
647 raise TypeError("double wrapping of Required")
648 hash(key) # ensure is a valid key
649 if _precedence(key) == 0:
650 raise ValueError("== match constants are already required: " + bbrepr(key))
651 self.key = key
653 def __repr__(self):
654 return f'{self.__class__.__name__}({bbrepr(self.key)})'
657def _precedence(match):
658 """
659 in a dict spec, target-keys may match many
660 spec-keys (e.g. 1 will match int, M > 0, and 1);
661 therefore we need a precedence for which order to try
662 keys in; higher = later
663 """
664 if type(match) in (Required, Optional):
665 match = match.key
666 if type(match) in (tuple, frozenset):
667 if not match:
668 return 0
669 return max([_precedence(item) for item in match])
670 if isinstance(match, type):
671 return 2
672 if hasattr(match, "glomit"):
673 return 1
674 return 0 # == match
677def _handle_dict(target, spec, scope):
678 if not isinstance(target, dict):
679 raise TypeMatchError(type(target), dict)
680 spec_keys = spec # cheating a little bit here, list-vs-dict, but saves an object copy sometimes
682 required = {
683 key for key in spec_keys
684 if _precedence(key) == 0 and type(key) is not Optional
685 or type(key) is Required}
686 defaults = { # pre-load result with defaults
687 key.key: key.default for key in spec_keys
688 if type(key) is Optional and key.default is not _MISSING}
689 result = {}
690 for key, val in target.items():
691 for maybe_spec_key in spec_keys:
692 # handle Required as a special case here rather than letting it be a stand-alone spec
693 if type(maybe_spec_key) is Required:
694 spec_key = maybe_spec_key.key
695 else:
696 spec_key = maybe_spec_key
697 try:
698 key = scope[glom](key, spec_key, scope)
699 except GlomError:
700 pass
701 else:
702 result[key] = scope[glom](val, spec[maybe_spec_key], chain_child(scope))
703 required.discard(maybe_spec_key)
704 break
705 else:
706 raise MatchError("key {0!r} didn't match any of {1!r}", key, spec_keys)
707 for key in set(defaults) - set(result):
708 result[key] = arg_val(target, defaults[key], scope)
709 if required:
710 raise MatchError("target missing expected keys: {0}", ', '.join([bbrepr(r) for r in required]))
711 return result
714def _glom_match(target, spec, scope):
715 if isinstance(spec, type):
716 if not isinstance(target, spec):
717 raise TypeMatchError(type(target), spec)
718 elif isinstance(spec, dict):
719 return _handle_dict(target, spec, scope)
720 elif isinstance(spec, (list, set, frozenset)):
721 if not isinstance(target, type(spec)):
722 raise TypeMatchError(type(target), type(spec))
723 result = []
724 for item in target:
725 for child in spec:
726 try:
727 result.append(scope[glom](item, child, scope))
728 break
729 except GlomError as e:
730 last_error = e
731 else: # did not break, something went wrong
732 if target and not spec:
733 raise MatchError(
734 "{0!r} does not match empty {1}", target, type(spec).__name__)
735 # NOTE: unless error happens above, break will skip else branch
736 # so last_error will have been assigned
737 raise last_error
738 if type(spec) is not list:
739 return type(spec)(result)
740 return result
741 elif isinstance(spec, tuple):
742 if not isinstance(target, tuple):
743 raise TypeMatchError(type(target), tuple)
744 if len(target) != len(spec):
745 raise MatchError("{0!r} does not match {1!r}", target, spec)
746 result = []
747 for sub_target, sub_spec in zip(target, spec):
748 result.append(scope[glom](sub_target, sub_spec, scope))
749 return tuple(result)
750 elif callable(spec):
751 try:
752 if spec(target):
753 return target
754 except Exception as e:
755 raise MatchError(
756 "{0}({1!r}) did not validate (got exception {2!r})", spec.__name__, target, e)
757 raise MatchError(
758 "{0}({1!r}) did not validate (non truthy return)", spec.__name__, target)
759 elif target != spec:
760 raise MatchError("{0!r} does not match {1!r}", target, spec)
761 return target
764class Switch:
765 r"""The :class:`Switch` specifier type routes data processing based on
766 matching keys, much like the classic switch statement.
768 Here is a spec which differentiates between lowercase English
769 vowel and consonant characters:
771 >>> switch_spec = Match(Switch([(Or('a', 'e', 'i', 'o', 'u'), Val('vowel')),
772 ... (And(str, M, M(T[2:]) == ''), Val('consonant'))]))
774 The constructor accepts a :class:`dict` of ``{keyspec: valspec}``
775 or a list of items, ``[(keyspec, valspec)]``. Keys are tried
776 against the current target in order. If a keyspec raises
777 :class:`GlomError`, the next keyspec is tried. Once a keyspec
778 succeeds, the corresponding valspec is evaluated and returned.
779 Let's try it out:
781 >>> glom('a', switch_spec)
782 'vowel'
783 >>> glom('z', switch_spec)
784 'consonant'
786 If no keyspec succeeds, a :class:`MatchError` is raised. Our spec
787 only works on characters (strings of length 1). Let's try a
788 non-character, the integer ``3``:
790 >>> glom(3, switch_spec)
791 Traceback (most recent call last):
792 ...
793 glom.matching.MatchError: error raised while processing, details below.
794 Target-spec trace (most recent last):
795 - Target: 3
796 - Spec: Match(Switch([(Or('a', 'e', 'i', 'o', 'u'), Val('vowel')), (And(str, M, (M(T[2:]) == '')), Val('...
797 + Spec: Switch([(Or('a', 'e', 'i', 'o', 'u'), Val('vowel')), (And(str, M, (M(T[2:]) == '')), Val('conson...
798 |\ Spec: Or('a', 'e', 'i', 'o', 'u')
799 ||\ Spec: 'a'
800 ||X glom.matching.MatchError: 3 does not match 'a'
801 ||\ Spec: 'e'
802 ||X glom.matching.MatchError: 3 does not match 'e'
803 ||\ Spec: 'i'
804 ||X glom.matching.MatchError: 3 does not match 'i'
805 ||\ Spec: 'o'
806 ||X glom.matching.MatchError: 3 does not match 'o'
807 ||\ Spec: 'u'
808 ||X glom.matching.MatchError: 3 does not match 'u'
809 |X glom.matching.MatchError: 3 does not match 'u'
810 |\ Spec: And(str, M, (M(T[2:]) == ''))
811 || Spec: str
812 |X glom.matching.TypeMatchError: expected type str, not int
813 glom.matching.MatchError: no matches for target in Switch
816 .. note::
818 :class:`~glom.Switch` is one of several *branching* specifier
819 types in glom. See ":ref:`branched-exceptions`" for details on
820 interpreting its exception messages.
822 A *default* value can be passed to the spec to be returned instead
823 of raising a :class:`MatchError`.
825 .. note::
827 Switch implements control flow similar to the switch statement
828 proposed in `PEP622 <https://www.python.org/dev/peps/pep-0622/>`_.
830 """
831 def __init__(self, cases, default=_MISSING):
832 if type(cases) is dict:
833 cases = list(cases.items())
834 if type(cases) is not list:
835 raise TypeError(
836 "expected cases argument to be of format {{keyspec: valspec}}"
837 " or [(keyspec, valspec)] not: {}".format(type(cases)))
838 self.cases = cases
839 # glom.match(cases, Or([(object, object)], dict))
840 # start dogfooding ^
841 self.default = default
842 if not cases:
843 raise ValueError('expected at least one case in %s, got: %r'
844 % (self.__class__.__name__, self.cases))
845 return
848 def glomit(self, target, scope):
849 for keyspec, valspec in self.cases:
850 try:
851 scope[glom](target, keyspec, scope)
852 except GlomError as ge:
853 continue
854 return scope[glom](target, valspec, chain_child(scope))
855 if self.default is not _MISSING:
856 return arg_val(target, self.default, scope)
857 raise MatchError("no matches for target in %s" % self.__class__.__name__)
859 def __repr__(self):
860 return f'{self.__class__.__name__}({bbrepr(self.cases)})'
863RAISE = make_sentinel('RAISE') # flag object for "raise on check failure"
866class Check:
867 """Check objects are used to make assertions about the target data,
868 and either pass through the data or raise exceptions if there is a
869 problem.
871 If any check condition fails, a :class:`~glom.CheckError` is raised.
873 Args:
875 spec: a sub-spec to extract the data to which other assertions will
876 be checked (defaults to applying checks to the target itself)
877 type: a type or sequence of types to be checked for exact match
878 equal_to: a value to be checked for equality match ("==")
879 validate: a callable or list of callables, each representing a
880 check condition. If one or more return False or raise an
881 exception, the Check will fail.
882 instance_of: a type or sequence of types to be checked with isinstance()
883 one_of: an iterable of values, any of which can match the target ("in")
884 default: an optional default value to replace the value when the check fails
885 (if default is not specified, GlomCheckError will be raised)
887 Aside from *spec*, all arguments are keyword arguments. Each
888 argument, except for *default*, represent a check
889 condition. Multiple checks can be passed, and if all check
890 conditions are left unset, Check defaults to performing a basic
891 truthy check on the value.
893 """
894 # TODO: the next level of Check would be to play with the Scope to
895 # allow checking to continue across the same level of
896 # dictionary. Basically, collect as many errors as possible before
897 # raising the unified CheckError.
898 def __init__(self, spec=T, **kwargs):
899 self.spec = spec
900 self._orig_kwargs = dict(kwargs)
901 self.default = kwargs.pop('default', RAISE)
903 def _get_arg_val(name, cond, func, val, can_be_empty=True):
904 if val is _MISSING:
905 return ()
906 if not is_iterable(val):
907 val = (val,)
908 elif not val and not can_be_empty:
909 raise ValueError('expected %r argument to contain at least one value,'
910 ' not: %r' % (name, val))
911 for v in val:
912 if not func(v):
913 raise ValueError('expected %r argument to be %s, not: %r'
914 % (name, cond, v))
915 return val
917 # if there are other common validation functions, maybe a
918 # small set of special strings would work as valid arguments
919 # to validate, too.
920 def truthy(val):
921 return bool(val)
923 validate = kwargs.pop('validate', _MISSING if kwargs else truthy)
924 type_arg = kwargs.pop('type', _MISSING)
925 instance_of = kwargs.pop('instance_of', _MISSING)
926 equal_to = kwargs.pop('equal_to', _MISSING)
927 one_of = kwargs.pop('one_of', _MISSING)
928 if kwargs:
929 raise TypeError('unexpected keyword arguments: %r' % kwargs.keys())
931 self.validators = _get_arg_val('validate', 'callable', callable, validate)
932 self.instance_of = _get_arg_val('instance_of', 'a type',
933 lambda x: isinstance(x, type), instance_of, False)
934 self.types = _get_arg_val('type', 'a type',
935 lambda x: isinstance(x, type), type_arg, False)
937 if equal_to is not _MISSING:
938 self.vals = (equal_to,)
939 if one_of is not _MISSING:
940 raise TypeError('expected "one_of" argument to be unset when'
941 ' "equal_to" argument is passed')
942 elif one_of is not _MISSING:
943 if not is_iterable(one_of):
944 raise ValueError('expected "one_of" argument to be iterable'
945 ' , not: %r' % one_of)
946 if not one_of:
947 raise ValueError('expected "one_of" to contain at least'
948 ' one value, not: %r' % (one_of,))
949 self.vals = one_of
950 else:
951 self.vals = ()
952 return
954 class _ValidationError(Exception):
955 "for internal use inside of Check only"
956 pass
958 def glomit(self, target, scope):
959 ret = target
960 errs = []
961 if self.spec is not T:
962 target = scope[glom](target, self.spec, scope)
963 if self.types and type(target) not in self.types:
964 if self.default is not RAISE:
965 return arg_val(target, self.default, scope)
966 errs.append('expected type to be %r, found type %r' %
967 (self.types[0].__name__ if len(self.types) == 1
968 else tuple([t.__name__ for t in self.types]),
969 type(target).__name__))
971 if self.vals and target not in self.vals:
972 if self.default is not RAISE:
973 return arg_val(target, self.default, scope)
974 if len(self.vals) == 1:
975 errs.append(f"expected {self.vals[0]}, found {target}")
976 else:
977 errs.append(f'expected one of {self.vals}, found {target}')
979 if self.validators:
980 for i, validator in enumerate(self.validators):
981 try:
982 res = validator(target)
983 if res is False:
984 raise self._ValidationError
985 except Exception as e:
986 msg = ('expected %r check to validate target'
987 % getattr(validator, '__name__', None) or ('#%s' % i))
988 if type(e) is self._ValidationError:
989 if self.default is not RAISE:
990 return self.default
991 else:
992 msg += ' (got exception: %r)' % e
993 errs.append(msg)
995 if self.instance_of and not isinstance(target, self.instance_of):
996 # TODO: can these early returns be done without so much copy-paste?
997 # (early return to avoid potentially expensive or even error-causeing
998 # string formats)
999 if self.default is not RAISE:
1000 return arg_val(target, self.default, scope)
1001 errs.append('expected instance of %r, found instance of %r' %
1002 (self.instance_of[0].__name__ if len(self.instance_of) == 1
1003 else tuple([t.__name__ for t in self.instance_of]),
1004 type(target).__name__))
1006 if errs:
1007 raise CheckError(errs, self, scope[Path])
1008 return ret
1010 def __repr__(self):
1011 cn = self.__class__.__name__
1012 posargs = (self.spec,) if self.spec is not T else ()
1013 return format_invocation(cn, posargs, self._orig_kwargs, repr=bbrepr)
1016class CheckError(GlomError):
1017 """This :exc:`GlomError` subtype is raised when target data fails to
1018 pass a :class:`Check`'s specified validation.
1020 An uncaught ``CheckError`` looks like this::
1022 >>> target = {'a': {'b': 'c'}}
1023 >>> glom(target, {'b': ('a.b', Check(type=int))})
1024 Traceback (most recent call last):
1025 ...
1026 CheckError: target at path ['a.b'] failed check, got error: "expected type to be 'int', found type 'str'"
1028 If the ``Check`` contains more than one condition, there may be
1029 more than one error message. The string rendition of the
1030 ``CheckError`` will include all messages.
1032 You can also catch the ``CheckError`` and programmatically access
1033 messages through the ``msgs`` attribute on the ``CheckError``
1034 instance.
1036 """
1037 def __init__(self, msgs, check, path):
1038 self.msgs = msgs
1039 self.check_obj = check
1040 self.path = path
1042 def get_message(self):
1043 msg = 'target at path %s failed check,' % self.path
1044 if self.check_obj.spec is not T:
1045 msg += f' subtarget at {self.check_obj.spec!r}'
1046 if len(self.msgs) == 1:
1047 msg += f' got error: {self.msgs[0]!r}'
1048 else:
1049 msg += f' got {len(self.msgs)} errors: {self.msgs!r}'
1050 return msg
1052 def __repr__(self):
1053 cn = self.__class__.__name__
1054 return f'{cn}({self.msgs!r}, {self.check_obj!r}, {self.path!r})'