Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/attr/validators.py: 49%

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

235 statements  

1# SPDX-License-Identifier: MIT 

2 

3""" 

4Commonly useful validators. 

5""" 

6 

7import operator 

8import re 

9 

10from contextlib import contextmanager 

11from re import Pattern 

12 

13from ._config import get_run_validators, set_run_validators 

14from ._make import _AndValidator, and_, attrib, attrs 

15from .converters import default_if_none 

16from .exceptions import NotCallableError 

17 

18 

19__all__ = [ 

20 "and_", 

21 "deep_iterable", 

22 "deep_mapping", 

23 "disabled", 

24 "ge", 

25 "get_disabled", 

26 "gt", 

27 "in_", 

28 "instance_of", 

29 "is_callable", 

30 "le", 

31 "lt", 

32 "matches_re", 

33 "max_len", 

34 "min_len", 

35 "not_", 

36 "optional", 

37 "or_", 

38 "set_disabled", 

39] 

40 

41 

42def set_disabled(disabled): 

43 """ 

44 Globally disable or enable running validators. 

45 

46 By default, they are run. 

47 

48 Args: 

49 disabled (bool): If `True`, disable running all validators. 

50 

51 .. warning:: 

52 

53 This function is not thread-safe! 

54 

55 .. versionadded:: 21.3.0 

56 """ 

57 set_run_validators(not disabled) 

58 

59 

60def get_disabled(): 

61 """ 

62 Return a bool indicating whether validators are currently disabled or not. 

63 

64 Returns: 

65 bool:`True` if validators are currently disabled. 

66 

67 .. versionadded:: 21.3.0 

68 """ 

69 return not get_run_validators() 

70 

71 

72@contextmanager 

73def disabled(): 

74 """ 

75 Context manager that disables running validators within its context. 

76 

77 .. warning:: 

78 

79 This context manager is not thread-safe! 

80 

81 .. versionadded:: 21.3.0 

82 """ 

83 set_run_validators(False) 

84 try: 

85 yield 

86 finally: 

87 set_run_validators(True) 

88 

89 

90@attrs(repr=False, slots=True, unsafe_hash=True) 

91class _InstanceOfValidator: 

92 type = attrib() 

93 

94 def __call__(self, inst, attr, value): 

95 """ 

96 We use a callable class to be able to change the ``__repr__``. 

97 """ 

98 if not isinstance(value, self.type): 

99 msg = f"'{attr.name}' must be {self.type!r} (got {value!r} that is a {value.__class__!r})." 

100 raise TypeError( 

101 msg, 

102 attr, 

103 self.type, 

104 value, 

105 ) 

106 

107 def __repr__(self): 

108 return f"<instance_of validator for type {self.type!r}>" 

109 

110 

111def instance_of(type): 

112 """ 

113 A validator that raises a `TypeError` if the initializer is called with a 

114 wrong type for this particular attribute (checks are performed using 

115 `isinstance` therefore it's also valid to pass a tuple of types). 

116 

117 Args: 

118 type (type | tuple[type]): The type to check for. 

119 

120 Raises: 

121 TypeError: 

122 With a human readable error message, the attribute (of type 

123 `attrs.Attribute`), the expected type, and the value it got. 

124 """ 

125 return _InstanceOfValidator(type) 

126 

127 

128@attrs(repr=False, frozen=True, slots=True) 

129class _MatchesReValidator: 

130 pattern = attrib() 

131 match_func = attrib() 

132 

133 def __call__(self, inst, attr, value): 

134 """ 

135 We use a callable class to be able to change the ``__repr__``. 

136 """ 

137 if not self.match_func(value): 

138 msg = f"'{attr.name}' must match regex {self.pattern.pattern!r} ({value!r} doesn't)" 

139 raise ValueError( 

140 msg, 

141 attr, 

142 self.pattern, 

143 value, 

144 ) 

145 

146 def __repr__(self): 

147 return f"<matches_re validator for pattern {self.pattern!r}>" 

148 

149 

150def matches_re(regex, flags=0, func=None): 

151 r""" 

152 A validator that raises `ValueError` if the initializer is called with a 

153 string that doesn't match *regex*. 

154 

155 Args: 

156 regex (str, re.Pattern): 

157 A regex string or precompiled pattern to match against 

158 

159 flags (int): 

160 Flags that will be passed to the underlying re function (default 0) 

161 

162 func (typing.Callable): 

163 Which underlying `re` function to call. Valid options are 

164 `re.fullmatch`, `re.search`, and `re.match`; the default `None` 

165 means `re.fullmatch`. For performance reasons, the pattern is 

166 always precompiled using `re.compile`. 

167 

168 .. versionadded:: 19.2.0 

169 .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern. 

170 """ 

171 valid_funcs = (re.fullmatch, None, re.search, re.match) 

172 if func not in valid_funcs: 

173 msg = "'func' must be one of {}.".format( 

174 ", ".join( 

175 sorted((e and e.__name__) or "None" for e in set(valid_funcs)) 

176 ) 

177 ) 

178 raise ValueError(msg) 

179 

180 if isinstance(regex, Pattern): 

181 if flags: 

182 msg = "'flags' can only be used with a string pattern; pass flags to re.compile() instead" 

183 raise TypeError(msg) 

184 pattern = regex 

185 else: 

186 pattern = re.compile(regex, flags) 

187 

188 if func is re.match: 

189 match_func = pattern.match 

190 elif func is re.search: 

191 match_func = pattern.search 

192 else: 

193 match_func = pattern.fullmatch 

194 

195 return _MatchesReValidator(pattern, match_func) 

196 

197 

198@attrs(repr=False, slots=True, unsafe_hash=True) 

199class _OptionalValidator: 

200 validator = attrib() 

201 

202 def __call__(self, inst, attr, value): 

203 if value is None: 

204 return 

205 

206 self.validator(inst, attr, value) 

207 

208 def __repr__(self): 

209 return f"<optional validator for {self.validator!r} or None>" 

210 

211 

212def optional(validator): 

213 """ 

214 A validator that makes an attribute optional. An optional attribute is one 

215 which can be set to `None` in addition to satisfying the requirements of 

216 the sub-validator. 

217 

218 Args: 

219 validator 

220 (typing.Callable | tuple[typing.Callable] | list[typing.Callable]): 

221 A validator (or validators) that is used for non-`None` values. 

222 

223 .. versionadded:: 15.1.0 

224 .. versionchanged:: 17.1.0 *validator* can be a list of validators. 

225 .. versionchanged:: 23.1.0 *validator* can also be a tuple of validators. 

226 """ 

227 if isinstance(validator, (list, tuple)): 

228 return _OptionalValidator(_AndValidator(validator)) 

229 

230 return _OptionalValidator(validator) 

231 

232 

233@attrs(repr=False, slots=True, unsafe_hash=True) 

234class _InValidator: 

235 options = attrib() 

236 _original_options = attrib(hash=False) 

237 

238 def __call__(self, inst, attr, value): 

239 try: 

240 in_options = value in self.options 

241 except TypeError: # e.g. `1 in "abc"` 

242 in_options = False 

243 

244 if not in_options: 

245 msg = f"'{attr.name}' must be in {self._original_options!r} (got {value!r})" 

246 raise ValueError( 

247 msg, 

248 attr, 

249 self._original_options, 

250 value, 

251 ) 

252 

253 def __repr__(self): 

254 return f"<in_ validator with options {self._original_options!r}>" 

255 

256 

257def in_(options): 

258 """ 

259 A validator that raises a `ValueError` if the initializer is called with a 

260 value that does not belong in the *options* provided. 

261 

262 The check is performed using ``value in options``, so *options* has to 

263 support that operation. 

264 

265 To keep the validator hashable, dicts, lists, and sets are transparently 

266 transformed into a `tuple`. 

267 

268 Args: 

269 options: Allowed options. 

270 

271 Raises: 

272 ValueError: 

273 With a human readable error message, the attribute (of type 

274 `attrs.Attribute`), the expected options, and the value it got. 

275 

276 .. versionadded:: 17.1.0 

277 .. versionchanged:: 22.1.0 

278 The ValueError was incomplete until now and only contained the human 

279 readable error message. Now it contains all the information that has 

280 been promised since 17.1.0. 

281 .. versionchanged:: 24.1.0 

282 *options* that are a list, dict, or a set are now transformed into a 

283 tuple to keep the validator hashable. 

284 """ 

285 repr_options = options 

286 if isinstance(options, (list, dict, set)): 

287 options = tuple(options) 

288 

289 return _InValidator(options, repr_options) 

290 

291 

292@attrs(repr=False, slots=False, unsafe_hash=True) 

293class _IsCallableValidator: 

294 def __call__(self, inst, attr, value): 

295 """ 

296 We use a callable class to be able to change the ``__repr__``. 

297 """ 

298 if not callable(value): 

299 message = ( 

300 "'{name}' must be callable " 

301 "(got {value!r} that is a {actual!r})." 

302 ) 

303 raise NotCallableError( 

304 msg=message.format( 

305 name=attr.name, value=value, actual=value.__class__ 

306 ), 

307 value=value, 

308 ) 

309 

310 def __repr__(self): 

311 return "<is_callable validator>" 

312 

313 

314def is_callable(): 

315 """ 

316 A validator that raises a `attrs.exceptions.NotCallableError` if the 

317 initializer is called with a value for this particular attribute that is 

318 not callable. 

319 

320 .. versionadded:: 19.1.0 

321 

322 Raises: 

323 attrs.exceptions.NotCallableError: 

324 With a human readable error message containing the attribute 

325 (`attrs.Attribute`) name, and the value it got. 

326 """ 

327 return _IsCallableValidator() 

328 

329 

330@attrs(repr=False, slots=True, unsafe_hash=True) 

331class _DeepIterable: 

332 member_validator = attrib(validator=is_callable()) 

333 iterable_validator = attrib( 

334 default=None, validator=optional(is_callable()) 

335 ) 

336 

337 def __call__(self, inst, attr, value): 

338 """ 

339 We use a callable class to be able to change the ``__repr__``. 

340 """ 

341 if self.iterable_validator is not None: 

342 self.iterable_validator(inst, attr, value) 

343 

344 for member in value: 

345 self.member_validator(inst, attr, member) 

346 

347 def __repr__(self): 

348 iterable_identifier = ( 

349 "" 

350 if self.iterable_validator is None 

351 else f" {self.iterable_validator!r}" 

352 ) 

353 return ( 

354 f"<deep_iterable validator for{iterable_identifier}" 

355 f" iterables of {self.member_validator!r}>" 

356 ) 

357 

358 

359def deep_iterable(member_validator, iterable_validator=None): 

360 """ 

361 A validator that performs deep validation of an iterable. 

362 

363 Args: 

364 member_validator: Validator(s) to apply to iterable members. 

365 

366 iterable_validator: 

367 Validator(s) to apply to iterable itself (optional). 

368 

369 Raises 

370 TypeError: if any sub-validators fail 

371 

372 .. versionadded:: 19.1.0 

373 

374 .. versionchanged:: 25.4.0 

375 *member_validator* and *iterable_validator* can now be a list or tuple 

376 of validators. 

377 """ 

378 if isinstance(member_validator, (list, tuple)): 

379 member_validator = and_(*member_validator) 

380 if isinstance(iterable_validator, (list, tuple)): 

381 iterable_validator = and_(*iterable_validator) 

382 return _DeepIterable(member_validator, iterable_validator) 

383 

384 

385@attrs(repr=False, slots=True, unsafe_hash=True) 

386class _DeepMapping: 

387 key_validator = attrib(validator=optional(is_callable())) 

388 value_validator = attrib(validator=optional(is_callable())) 

389 mapping_validator = attrib(validator=optional(is_callable())) 

390 

391 def __call__(self, inst, attr, value): 

392 """ 

393 We use a callable class to be able to change the ``__repr__``. 

394 """ 

395 if self.mapping_validator is not None: 

396 self.mapping_validator(inst, attr, value) 

397 

398 for key in value: 

399 if self.key_validator is not None: 

400 self.key_validator(inst, attr, key) 

401 if self.value_validator is not None: 

402 self.value_validator(inst, attr, value[key]) 

403 

404 def __repr__(self): 

405 return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>" 

406 

407 

408def deep_mapping( 

409 key_validator=None, value_validator=None, mapping_validator=None 

410): 

411 """ 

412 A validator that performs deep validation of a dictionary. 

413 

414 All validators are optional, but at least one of *key_validator* or 

415 *value_validator* must be provided. 

416 

417 Args: 

418 key_validator: Validator(s) to apply to dictionary keys. 

419 

420 value_validator: Validator(s) to apply to dictionary values. 

421 

422 mapping_validator: 

423 Validator(s) to apply to top-level mapping attribute. 

424 

425 .. versionadded:: 19.1.0 

426 

427 .. versionchanged:: 25.4.0 

428 *key_validator* and *value_validator* are now optional, but at least one 

429 of them must be provided. 

430 

431 .. versionchanged:: 25.4.0 

432 *key_validator*, *value_validator*, and *mapping_validator* can now be a 

433 list or tuple of validators. 

434 

435 Raises: 

436 TypeError: If any sub-validator fails on validation. 

437 

438 ValueError: 

439 If neither *key_validator* nor *value_validator* is provided on 

440 instantiation. 

441 """ 

442 if key_validator is None and value_validator is None: 

443 msg = ( 

444 "At least one of key_validator or value_validator must be provided" 

445 ) 

446 raise ValueError(msg) 

447 

448 if isinstance(key_validator, (list, tuple)): 

449 key_validator = and_(*key_validator) 

450 if isinstance(value_validator, (list, tuple)): 

451 value_validator = and_(*value_validator) 

452 if isinstance(mapping_validator, (list, tuple)): 

453 mapping_validator = and_(*mapping_validator) 

454 

455 return _DeepMapping(key_validator, value_validator, mapping_validator) 

456 

457 

458@attrs(repr=False, frozen=True, slots=True) 

459class _NumberValidator: 

460 bound = attrib() 

461 compare_op = attrib() 

462 compare_func = attrib() 

463 

464 def __call__(self, inst, attr, value): 

465 """ 

466 We use a callable class to be able to change the ``__repr__``. 

467 """ 

468 if not self.compare_func(value, self.bound): 

469 msg = f"'{attr.name}' must be {self.compare_op} {self.bound}: {value}" 

470 raise ValueError(msg) 

471 

472 def __repr__(self): 

473 return f"<Validator for x {self.compare_op} {self.bound}>" 

474 

475 

476def lt(val): 

477 """ 

478 A validator that raises `ValueError` if the initializer is called with a 

479 number larger or equal to *val*. 

480 

481 The validator uses `operator.lt` to compare the values. 

482 

483 Args: 

484 val: Exclusive upper bound for values. 

485 

486 .. versionadded:: 21.3.0 

487 """ 

488 return _NumberValidator(val, "<", operator.lt) 

489 

490 

491def le(val): 

492 """ 

493 A validator that raises `ValueError` if the initializer is called with a 

494 number greater than *val*. 

495 

496 The validator uses `operator.le` to compare the values. 

497 

498 Args: 

499 val: Inclusive upper bound for values. 

500 

501 .. versionadded:: 21.3.0 

502 """ 

503 return _NumberValidator(val, "<=", operator.le) 

504 

505 

506def ge(val): 

507 """ 

508 A validator that raises `ValueError` if the initializer is called with a 

509 number smaller than *val*. 

510 

511 The validator uses `operator.ge` to compare the values. 

512 

513 Args: 

514 val: Inclusive lower bound for values 

515 

516 .. versionadded:: 21.3.0 

517 """ 

518 return _NumberValidator(val, ">=", operator.ge) 

519 

520 

521def gt(val): 

522 """ 

523 A validator that raises `ValueError` if the initializer is called with a 

524 number smaller or equal to *val*. 

525 

526 The validator uses `operator.gt` to compare the values. 

527 

528 Args: 

529 val: Exclusive lower bound for values 

530 

531 .. versionadded:: 21.3.0 

532 """ 

533 return _NumberValidator(val, ">", operator.gt) 

534 

535 

536@attrs(repr=False, frozen=True, slots=True) 

537class _MaxLengthValidator: 

538 max_length = attrib() 

539 

540 def __call__(self, inst, attr, value): 

541 """ 

542 We use a callable class to be able to change the ``__repr__``. 

543 """ 

544 if len(value) > self.max_length: 

545 msg = f"Length of '{attr.name}' must be <= {self.max_length}: {len(value)}" 

546 raise ValueError(msg) 

547 

548 def __repr__(self): 

549 return f"<max_len validator for {self.max_length}>" 

550 

551 

552def max_len(length): 

553 """ 

554 A validator that raises `ValueError` if the initializer is called 

555 with a string or iterable that is longer than *length*. 

556 

557 Args: 

558 length (int): Maximum length of the string or iterable 

559 

560 .. versionadded:: 21.3.0 

561 """ 

562 return _MaxLengthValidator(length) 

563 

564 

565@attrs(repr=False, frozen=True, slots=True) 

566class _MinLengthValidator: 

567 min_length = attrib() 

568 

569 def __call__(self, inst, attr, value): 

570 """ 

571 We use a callable class to be able to change the ``__repr__``. 

572 """ 

573 if len(value) < self.min_length: 

574 msg = f"Length of '{attr.name}' must be >= {self.min_length}: {len(value)}" 

575 raise ValueError(msg) 

576 

577 def __repr__(self): 

578 return f"<min_len validator for {self.min_length}>" 

579 

580 

581def min_len(length): 

582 """ 

583 A validator that raises `ValueError` if the initializer is called 

584 with a string or iterable that is shorter than *length*. 

585 

586 Args: 

587 length (int): Minimum length of the string or iterable 

588 

589 .. versionadded:: 22.1.0 

590 """ 

591 return _MinLengthValidator(length) 

592 

593 

594@attrs(repr=False, slots=True, unsafe_hash=True) 

595class _SubclassOfValidator: 

596 type = attrib() 

597 

598 def __call__(self, inst, attr, value): 

599 """ 

600 We use a callable class to be able to change the ``__repr__``. 

601 """ 

602 if not issubclass(value, self.type): 

603 msg = f"'{attr.name}' must be a subclass of {self.type!r} (got {value!r})." 

604 raise TypeError( 

605 msg, 

606 attr, 

607 self.type, 

608 value, 

609 ) 

610 

611 def __repr__(self): 

612 return f"<subclass_of validator for type {self.type!r}>" 

613 

614 

615def _subclass_of(type): 

616 """ 

617 A validator that raises a `TypeError` if the initializer is called with a 

618 wrong type for this particular attribute (checks are performed using 

619 `issubclass` therefore it's also valid to pass a tuple of types). 

620 

621 Args: 

622 type (type | tuple[type, ...]): The type(s) to check for. 

623 

624 Raises: 

625 TypeError: 

626 With a human readable error message, the attribute (of type 

627 `attrs.Attribute`), the expected type, and the value it got. 

628 """ 

629 return _SubclassOfValidator(type) 

630 

631 

632@attrs(repr=False, slots=True, unsafe_hash=True) 

633class _NotValidator: 

634 validator = attrib() 

635 msg = attrib( 

636 converter=default_if_none( 

637 "not_ validator child '{validator!r}' " 

638 "did not raise a captured error" 

639 ) 

640 ) 

641 exc_types = attrib( 

642 validator=deep_iterable( 

643 member_validator=_subclass_of(Exception), 

644 iterable_validator=instance_of(tuple), 

645 ), 

646 ) 

647 

648 def __call__(self, inst, attr, value): 

649 try: 

650 self.validator(inst, attr, value) 

651 except self.exc_types: 

652 pass # suppress error to invert validity 

653 else: 

654 raise ValueError( 

655 self.msg.format( 

656 validator=self.validator, 

657 exc_types=self.exc_types, 

658 ), 

659 attr, 

660 self.validator, 

661 value, 

662 self.exc_types, 

663 ) 

664 

665 def __repr__(self): 

666 return f"<not_ validator wrapping {self.validator!r}, capturing {self.exc_types!r}>" 

667 

668 

669def not_(validator, *, msg=None, exc_types=(ValueError, TypeError)): 

670 """ 

671 A validator that wraps and logically 'inverts' the validator passed to it. 

672 It will raise a `ValueError` if the provided validator *doesn't* raise a 

673 `ValueError` or `TypeError` (by default), and will suppress the exception 

674 if the provided validator *does*. 

675 

676 Intended to be used with existing validators to compose logic without 

677 needing to create inverted variants, for example, ``not_(in_(...))``. 

678 

679 Args: 

680 validator: A validator to be logically inverted. 

681 

682 msg (str): 

683 Message to raise if validator fails. Formatted with keys 

684 ``exc_types`` and ``validator``. 

685 

686 exc_types (tuple[type, ...]): 

687 Exception type(s) to capture. Other types raised by child 

688 validators will not be intercepted and pass through. 

689 

690 Raises: 

691 ValueError: 

692 With a human readable error message, the attribute (of type 

693 `attrs.Attribute`), the validator that failed to raise an 

694 exception, the value it got, and the expected exception types. 

695 

696 .. versionadded:: 22.2.0 

697 """ 

698 try: 

699 exc_types = tuple(exc_types) 

700 except TypeError: 

701 exc_types = (exc_types,) 

702 return _NotValidator(validator, msg, exc_types) 

703 

704 

705@attrs(repr=False, slots=True, unsafe_hash=True) 

706class _OrValidator: 

707 validators = attrib() 

708 

709 def __call__(self, inst, attr, value): 

710 for v in self.validators: 

711 try: 

712 v(inst, attr, value) 

713 except Exception: # noqa: BLE001, PERF203, S112 

714 continue 

715 else: 

716 return 

717 

718 msg = f"None of {self.validators!r} satisfied for value {value!r}" 

719 raise ValueError(msg) 

720 

721 def __repr__(self): 

722 return f"<or validator wrapping {self.validators!r}>" 

723 

724 

725def or_(*validators): 

726 """ 

727 A validator that composes multiple validators into one. 

728 

729 When called on a value, it runs all wrapped validators until one of them is 

730 satisfied. 

731 

732 Args: 

733 validators (~collections.abc.Iterable[typing.Callable]): 

734 Arbitrary number of validators. 

735 

736 Raises: 

737 ValueError: 

738 If no validator is satisfied. Raised with a human-readable error 

739 message listing all the wrapped validators and the value that 

740 failed all of them. 

741 

742 .. versionadded:: 24.1.0 

743 """ 

744 vals = [] 

745 for v in validators: 

746 vals.extend(v.validators if isinstance(v, _OrValidator) else [v]) 

747 

748 return _OrValidator(tuple(vals))