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

476 statements  

1""" 

2.. versionadded:: 20.7.0 

3 

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""" 

9 

10import re 

11import sys 

12from pprint import pprint 

13 

14from boltons.iterutils import is_iterable 

15from boltons.typeutils import make_sentinel 

16 

17from .core import GlomError, glom, T, MODE, bbrepr, bbformat, format_invocation, Path, chain_child, Val, arg_val 

18 

19 

20_MISSING = make_sentinel('_MISSING') 

21 

22 

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. 

30 

31 >>> glom({123: 'a'}, Match({'id': int})) 

32 Traceback (most recent call last): 

33 ... 

34 MatchError: key 123 didn't match any of ['id'] 

35 

36 """ 

37 def __init__(self, fmt, *args): 

38 super().__init__(fmt, *args) 

39 

40 def get_message(self): 

41 fmt, args = self.args[0], self.args[1:] 

42 return bbformat(fmt, *args) 

43 

44 

45class TypeMatchError(MatchError, TypeError): 

46 """:exc:`MatchError` subtype raised when a 

47 :class:`Match` fails a type check. 

48 

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 """ 

61 

62 def __init__(self, actual, expected): 

63 super().__init__( 

64 "expected type {0.__name__}, not {1.__name__}", expected, actual) 

65 

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]) 

70 

71 

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. 

76 

77 Pattern specs are evaluated as follows: 

78 

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 ``==`` 

85 

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. 

89 

90 For example, let's load some data:: 

91 

92 >>> target = [ 

93 ... {'id': 1, 'email': 'alice@example.com'}, 

94 ... {'id': 2, 'email': 'bob@example.com'}] 

95 

96 A :class:`Match` pattern can be used to ensure this data is in its expected form: 

97 

98 >>> spec = Match([{'id': int, 'email': str}]) 

99 

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``: 

106 

107 >>> result = glom(target, spec) 

108 >>> assert result == \\ 

109 ... [{'id': 1, 'email': 'alice@example.com'}, {'id': 2, 'email': 'bob@example.com'}] 

110 

111 With a more complex :class:`Match` spec, we can be more precise: 

112 

113 >>> spec = Match([{'id': And(M > 0, int), 'email': Regex('[^@]+@[^@]+')}]) 

114 

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. 

120 

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:: 

125 

126 >>> assert glom(target, spec) == \\ 

127 ... [{'id': 1, 'email': 'alice@example.com'}, {'id': 2, 'email': 'bob@example.com'}] 

128 

129 And as usual, upon a successful match, we get the matched result. 

130 

131 .. note:: 

132 

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. 

135 

136 .. _schema: https://github.com/keleshev/schema 

137 

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`. 

142 

143 """ 

144 def __init__(self, spec, default=_MISSING): 

145 self.spec = spec 

146 self.default = default 

147 

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 

157 

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. 

162 

163 Args: 

164 target: Target value or data structure to match against. 

165 

166 Raises: 

167 glom.MatchError 

168 

169 """ 

170 return glom(target, self) 

171 

172 def matches(self, target): 

173 """A convenience method on a :class:`Match` instance, returns 

174 ``True`` if the *target* matches, ``False`` if not. 

175 

176 >>> Match(int).matches(-1.0) 

177 False 

178 

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 

187 

188 def __repr__(self): 

189 return f'{self.__class__.__name__}({bbrepr(self.spec)})' 

190 

191 

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)))) 

196 

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,) 

204 

205 

206class Regex: 

207 """ 

208 checks that target is a string which matches the passed regex pattern 

209 

210 raises MatchError if there isn't a match; returns Target if match 

211 

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') 

216 

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 

233 

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 

243 

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 

252 

253 

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) 

261 

262 

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())) 

272 

273 def __and__(self, other): 

274 return And(self, other) 

275 

276 def __or__(self, other): 

277 return Or(self, other) 

278 

279 def __invert__(self): 

280 return Not(self) 

281 

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 

289 

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 

298 

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) + ")" 

306 

307 

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',) 

315 

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 

322 

323 def __and__(self, other): 

324 # reduce number of layers of spec 

325 return And(*(self.children + (other,))) 

326 

327 

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',) 

336 

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) 

344 

345 def __or__(self, other): 

346 # reduce number of layers of spec 

347 return Or(*(self.children + (other,))) 

348 

349 

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. 

354 

355 If the child spec does not raise :exc:`GlomError`, :exc:`MatchError` 

356 will be raised. 

357 """ 

358 __slots__ = ('child',) 

359 

360 def __init__(self, child): 

361 self.child = child 

362 

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) 

370 

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() 

377 

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) + ")" 

384 

385 

386_M_OP_MAP = {'=': '==', '!': '!=', 'g': '>=', 'l': '<='} 

387 

388 

389class _MSubspec: 

390 """used by MType.__call__ to wrap a sub-spec for comparison""" 

391 __slots__ = ('spec') 

392 

393 def __init__(self, spec): 

394 self.spec = spec 

395 

396 def __eq__(self, other): 

397 return _MExpr(self, '=', other) 

398 

399 def __ne__(self, other): 

400 return _MExpr(self, '!', other) 

401 

402 def __gt__(self, other): 

403 return _MExpr(self, '>', other) 

404 

405 def __lt__(self, other): 

406 return _MExpr(self, '<', other) 

407 

408 def __ge__(self, other): 

409 return _MExpr(self, 'g', other) 

410 

411 def __le__(self, other): 

412 return _MExpr(self, 'l', other) 

413 

414 def __repr__(self): 

415 return f'M({bbrepr(self.spec)})' 

416 

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) 

422 

423 

424class _MExpr: 

425 __slots__ = ('lhs', 'op', 'rhs') 

426 

427 def __init__(self, lhs, op, rhs): 

428 self.lhs, self.op, self.rhs = lhs, op, rhs 

429 

430 def __and__(self, other): 

431 return And(self, other) 

432 

433 __rand__ = __and__ 

434 

435 def __or__(self, other): 

436 return Or(self, other) 

437 

438 def __invert__(self): 

439 return Not(self) 

440 

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) 

462 

463 def __repr__(self): 

464 op = _M_OP_MAP.get(self.op, self.op) 

465 return f"{self.lhs!r} {op} {self.rhs!r}" 

466 

467 

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. 

473 

474 If a comparison succeeds, the target is returned unchanged. 

475 If a comparison fails, :class:`~glom.MatchError` is thrown. 

476 

477 Some examples: 

478 

479 >>> glom(1, M > 0) 

480 1 

481 >>> glom(0, M == 0) 

482 0 

483 >>> glom('a', M != 'b') == 'a' 

484 True 

485 

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: 

488 

489 >>> from glom import Val 

490 >>> glom([0, False, "", None], [M | Val(None)]) 

491 [None, None, None, None] 

492 

493 For convenience, ``&`` and ``|`` operators are overloaded to 

494 construct :attr:`~glom.And` and :attr:`~glom.Or` instances. 

495 

496 >>> glom(1.0, (M > 0) & float) 

497 1.0 

498 

499 .. note:: 

500 

501 Python's operator overloading may make for concise code, 

502 but it has its limits. 

503 

504 Because bitwise operators (``&`` and ``|``) have higher precedence 

505 than comparison operators (``>``, ``<``, etc.), expressions must 

506 be parenthesized. 

507 

508 >>> M > 0 & float 

509 Traceback (most recent call last): 

510 ... 

511 TypeError: unsupported operand type(s) for &: 'int' and 'type' 

512 

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`. 

517 

518 """ 

519 __slots__ = () 

520 

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) 

528 

529 def __eq__(self, other): 

530 return _MExpr(self, '=', other) 

531 

532 def __ne__(self, other): 

533 return _MExpr(self, '!', other) 

534 

535 def __gt__(self, other): 

536 return _MExpr(self, '>', other) 

537 

538 def __lt__(self, other): 

539 return _MExpr(self, '<', other) 

540 

541 def __ge__(self, other): 

542 return _MExpr(self, 'g', other) 

543 

544 def __le__(self, other): 

545 return _MExpr(self, 'l', other) 

546 

547 def __and__(self, other): 

548 return And(self, other) 

549 

550 __rand__ = __and__ 

551 

552 def __or__(self, other): 

553 return Or(self, other) 

554 

555 def __invert__(self): 

556 return Not(self) 

557 

558 def __repr__(self): 

559 return "M" 

560 

561 def glomit(self, target, spec): 

562 if target: 

563 return target 

564 raise MatchError("{0!r} not truthy", target) 

565 

566 

567M = _MType() 

568 

569 

570 

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. 

576 

577 For example:: 

578 

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': ''} 

587 

588 """ 

589 __slots__ = ('key', 'default') 

590 

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 

598 

599 def glomit(self, target, scope): 

600 if target != self.key: 

601 raise MatchError("target {0} != spec {1}", target, self.key) 

602 return target 

603 

604 def __repr__(self): 

605 return f'{self.__class__.__name__}({bbrepr(self.key)})' 

606 

607 

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. 

612 

613 For example:: 

614 

615 >>> spec = Match({object: object}) 

616 

617 This spec will match any dict, because :class:`object` is the base 

618 type of every object:: 

619 

620 >>> glom({}, spec) 

621 {} 

622 

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`:: 

626 

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) 

637 

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`. 

641 

642 """ 

643 __slots__ = ('key',) 

644 

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 

652 

653 def __repr__(self): 

654 return f'{self.__class__.__name__}({bbrepr(self.key)})' 

655 

656 

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 

675 

676 

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 

681 

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 

712 

713 

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 

762 

763 

764class Switch: 

765 r"""The :class:`Switch` specifier type routes data processing based on 

766 matching keys, much like the classic switch statement. 

767 

768 Here is a spec which differentiates between lowercase English 

769 vowel and consonant characters: 

770 

771 >>> switch_spec = Match(Switch([(Or('a', 'e', 'i', 'o', 'u'), Val('vowel')), 

772 ... (And(str, M, M(T[2:]) == ''), Val('consonant'))])) 

773 

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: 

780 

781 >>> glom('a', switch_spec) 

782 'vowel' 

783 >>> glom('z', switch_spec) 

784 'consonant' 

785 

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``: 

789 

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 

814 

815 

816 .. note:: 

817 

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. 

821 

822 A *default* value can be passed to the spec to be returned instead 

823 of raising a :class:`MatchError`. 

824 

825 .. note:: 

826 

827 Switch implements control flow similar to the switch statement 

828 proposed in `PEP622 <https://www.python.org/dev/peps/pep-0622/>`_. 

829 

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 

846 

847 

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__) 

858 

859 def __repr__(self): 

860 return f'{self.__class__.__name__}({bbrepr(self.cases)})' 

861 

862 

863RAISE = make_sentinel('RAISE') # flag object for "raise on check failure" 

864 

865 

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. 

870 

871 If any check condition fails, a :class:`~glom.CheckError` is raised. 

872 

873 Args: 

874 

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) 

886 

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. 

892 

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) 

902 

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 

916 

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) 

922 

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()) 

930 

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) 

936 

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 

953 

954 class _ValidationError(Exception): 

955 "for internal use inside of Check only" 

956 pass 

957 

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__)) 

970 

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}') 

978 

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) 

994 

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__)) 

1005 

1006 if errs: 

1007 raise CheckError(errs, self, scope[Path]) 

1008 return ret 

1009 

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) 

1014 

1015 

1016class CheckError(GlomError): 

1017 """This :exc:`GlomError` subtype is raised when target data fails to 

1018 pass a :class:`Check`'s specified validation. 

1019 

1020 An uncaught ``CheckError`` looks like this:: 

1021 

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'" 

1027 

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. 

1031 

1032 You can also catch the ``CheckError`` and programmatically access 

1033 messages through the ``msgs`` attribute on the ``CheckError`` 

1034 instance. 

1035 

1036 """ 

1037 def __init__(self, msgs, check, path): 

1038 self.msgs = msgs 

1039 self.check_obj = check 

1040 self.path = path 

1041 

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 

1051 

1052 def __repr__(self): 

1053 cn = self.__class__.__name__ 

1054 return f'{cn}({self.msgs!r}, {self.check_obj!r}, {self.path!r})'