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

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

221 statements  

1# SPDX-License-Identifier: MIT 

2 

3""" 

4Commonly useful validators. 

5""" 

6 

7 

8import operator 

9import re 

10 

11from contextlib import contextmanager 

12from re import Pattern 

13 

14from ._config import get_run_validators, set_run_validators 

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

16from .converters import default_if_none 

17from .exceptions import NotCallableError 

18 

19 

20__all__ = [ 

21 "and_", 

22 "deep_iterable", 

23 "deep_mapping", 

24 "disabled", 

25 "ge", 

26 "get_disabled", 

27 "gt", 

28 "in_", 

29 "instance_of", 

30 "is_callable", 

31 "le", 

32 "lt", 

33 "matches_re", 

34 "max_len", 

35 "min_len", 

36 "not_", 

37 "optional", 

38 "or_", 

39 "set_disabled", 

40] 

41 

42 

43def set_disabled(disabled): 

44 """ 

45 Globally disable or enable running validators. 

46 

47 By default, they are run. 

48 

49 Args: 

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

51 

52 .. warning:: 

53 

54 This function is not thread-safe! 

55 

56 .. versionadded:: 21.3.0 

57 """ 

58 set_run_validators(not disabled) 

59 

60 

61def get_disabled(): 

62 """ 

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

64 

65 Returns: 

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

67 

68 .. versionadded:: 21.3.0 

69 """ 

70 return not get_run_validators() 

71 

72 

73@contextmanager 

74def disabled(): 

75 """ 

76 Context manager that disables running validators within its context. 

77 

78 .. warning:: 

79 

80 This context manager is not thread-safe! 

81 

82 .. versionadded:: 21.3.0 

83 """ 

84 set_run_validators(False) 

85 try: 

86 yield 

87 finally: 

88 set_run_validators(True) 

89 

90 

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

92class _InstanceOfValidator: 

93 type = attrib() 

94 

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

96 """ 

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

98 """ 

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

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

101 raise TypeError( 

102 msg, 

103 attr, 

104 self.type, 

105 value, 

106 ) 

107 

108 def __repr__(self): 

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

110 

111 

112def instance_of(type): 

113 """ 

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

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

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

117 

118 Args: 

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

120 

121 Raises: 

122 TypeError: 

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

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

125 """ 

126 return _InstanceOfValidator(type) 

127 

128 

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

130class _MatchesReValidator: 

131 pattern = attrib() 

132 match_func = attrib() 

133 

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

135 """ 

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

137 """ 

138 if not self.match_func(value): 

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

140 raise ValueError( 

141 msg, 

142 attr, 

143 self.pattern, 

144 value, 

145 ) 

146 

147 def __repr__(self): 

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

149 

150 

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

152 r""" 

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

154 string that doesn't match *regex*. 

155 

156 Args: 

157 regex (str, re.Pattern): 

158 A regex string or precompiled pattern to match against 

159 

160 flags (int): 

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

162 

163 func (typing.Callable): 

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

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

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

167 always precompiled using `re.compile`. 

168 

169 .. versionadded:: 19.2.0 

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

171 """ 

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

173 if func not in valid_funcs: 

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

175 ", ".join( 

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

177 ) 

178 ) 

179 raise ValueError(msg) 

180 

181 if isinstance(regex, Pattern): 

182 if flags: 

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

184 raise TypeError(msg) 

185 pattern = regex 

186 else: 

187 pattern = re.compile(regex, flags) 

188 

189 if func is re.match: 

190 match_func = pattern.match 

191 elif func is re.search: 

192 match_func = pattern.search 

193 else: 

194 match_func = pattern.fullmatch 

195 

196 return _MatchesReValidator(pattern, match_func) 

197 

198 

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

200class _OptionalValidator: 

201 validator = attrib() 

202 

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

204 if value is None: 

205 return 

206 

207 self.validator(inst, attr, value) 

208 

209 def __repr__(self): 

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

211 

212 

213def optional(validator): 

214 """ 

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

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

217 the sub-validator. 

218 

219 Args: 

220 validator 

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

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

223 

224 .. versionadded:: 15.1.0 

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

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

227 """ 

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

229 return _OptionalValidator(_AndValidator(validator)) 

230 

231 return _OptionalValidator(validator) 

232 

233 

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

235class _InValidator: 

236 options = attrib() 

237 _original_options = attrib(hash=False) 

238 

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

240 try: 

241 in_options = value in self.options 

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

243 in_options = False 

244 

245 if not in_options: 

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

247 raise ValueError( 

248 msg, 

249 attr, 

250 self._original_options, 

251 value, 

252 ) 

253 

254 def __repr__(self): 

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

256 

257 

258def in_(options): 

259 """ 

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

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

262 

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

264 support that operation. 

265 

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

267 transformed into a `tuple`. 

268 

269 Args: 

270 options: Allowed options. 

271 

272 Raises: 

273 ValueError: 

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

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

276 

277 .. versionadded:: 17.1.0 

278 .. versionchanged:: 22.1.0 

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

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

281 been promised since 17.1.0. 

282 .. versionchanged:: 24.1.0 

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

284 tuple to keep the validator hashable. 

285 """ 

286 repr_options = options 

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

288 options = tuple(options) 

289 

290 return _InValidator(options, repr_options) 

291 

292 

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

294class _IsCallableValidator: 

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

296 """ 

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

298 """ 

299 if not callable(value): 

300 message = ( 

301 "'{name}' must be callable " 

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

303 ) 

304 raise NotCallableError( 

305 msg=message.format( 

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

307 ), 

308 value=value, 

309 ) 

310 

311 def __repr__(self): 

312 return "<is_callable validator>" 

313 

314 

315def is_callable(): 

316 """ 

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

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

319 not callable. 

320 

321 .. versionadded:: 19.1.0 

322 

323 Raises: 

324 attrs.exceptions.NotCallableError: 

325 With a human readable error message containing the attribute 

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

327 """ 

328 return _IsCallableValidator() 

329 

330 

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

332class _DeepIterable: 

333 member_validator = attrib(validator=is_callable()) 

334 iterable_validator = attrib( 

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

336 ) 

337 

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

339 """ 

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

341 """ 

342 if self.iterable_validator is not None: 

343 self.iterable_validator(inst, attr, value) 

344 

345 for member in value: 

346 self.member_validator(inst, attr, member) 

347 

348 def __repr__(self): 

349 iterable_identifier = ( 

350 "" 

351 if self.iterable_validator is None 

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

353 ) 

354 return ( 

355 f"<deep_iterable validator for{iterable_identifier}" 

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

357 ) 

358 

359 

360def deep_iterable(member_validator, iterable_validator=None): 

361 """ 

362 A validator that performs deep validation of an iterable. 

363 

364 Args: 

365 member_validator: Validator to apply to iterable members. 

366 

367 iterable_validator: 

368 Validator to apply to iterable itself (optional). 

369 

370 Raises 

371 TypeError: if any sub-validators fail 

372 

373 .. versionadded:: 19.1.0 

374 """ 

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

376 member_validator = and_(*member_validator) 

377 return _DeepIterable(member_validator, iterable_validator) 

378 

379 

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

381class _DeepMapping: 

382 key_validator = attrib(validator=is_callable()) 

383 value_validator = attrib(validator=is_callable()) 

384 mapping_validator = attrib(default=None, validator=optional(is_callable())) 

385 

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

387 """ 

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

389 """ 

390 if self.mapping_validator is not None: 

391 self.mapping_validator(inst, attr, value) 

392 

393 for key in value: 

394 self.key_validator(inst, attr, key) 

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

396 

397 def __repr__(self): 

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

399 

400 

401def deep_mapping(key_validator, value_validator, mapping_validator=None): 

402 """ 

403 A validator that performs deep validation of a dictionary. 

404 

405 Args: 

406 key_validator: Validator to apply to dictionary keys. 

407 

408 value_validator: Validator to apply to dictionary values. 

409 

410 mapping_validator: 

411 Validator to apply to top-level mapping attribute (optional). 

412 

413 .. versionadded:: 19.1.0 

414 

415 Raises: 

416 TypeError: if any sub-validators fail 

417 """ 

418 return _DeepMapping(key_validator, value_validator, mapping_validator) 

419 

420 

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

422class _NumberValidator: 

423 bound = attrib() 

424 compare_op = attrib() 

425 compare_func = attrib() 

426 

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

428 """ 

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

430 """ 

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

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

433 raise ValueError(msg) 

434 

435 def __repr__(self): 

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

437 

438 

439def lt(val): 

440 """ 

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

442 number larger or equal to *val*. 

443 

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

445 

446 Args: 

447 val: Exclusive upper bound for values. 

448 

449 .. versionadded:: 21.3.0 

450 """ 

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

452 

453 

454def le(val): 

455 """ 

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

457 number greater than *val*. 

458 

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

460 

461 Args: 

462 val: Inclusive upper bound for values. 

463 

464 .. versionadded:: 21.3.0 

465 """ 

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

467 

468 

469def ge(val): 

470 """ 

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

472 number smaller than *val*. 

473 

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

475 

476 Args: 

477 val: Inclusive lower bound for values 

478 

479 .. versionadded:: 21.3.0 

480 """ 

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

482 

483 

484def gt(val): 

485 """ 

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

487 number smaller or equal to *val*. 

488 

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

490 

491 Args: 

492 val: Exclusive lower bound for values 

493 

494 .. versionadded:: 21.3.0 

495 """ 

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

497 

498 

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

500class _MaxLengthValidator: 

501 max_length = attrib() 

502 

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

504 """ 

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

506 """ 

507 if len(value) > self.max_length: 

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

509 raise ValueError(msg) 

510 

511 def __repr__(self): 

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

513 

514 

515def max_len(length): 

516 """ 

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

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

519 

520 Args: 

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

522 

523 .. versionadded:: 21.3.0 

524 """ 

525 return _MaxLengthValidator(length) 

526 

527 

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

529class _MinLengthValidator: 

530 min_length = attrib() 

531 

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

533 """ 

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

535 """ 

536 if len(value) < self.min_length: 

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

538 raise ValueError(msg) 

539 

540 def __repr__(self): 

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

542 

543 

544def min_len(length): 

545 """ 

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

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

548 

549 Args: 

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

551 

552 .. versionadded:: 22.1.0 

553 """ 

554 return _MinLengthValidator(length) 

555 

556 

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

558class _SubclassOfValidator: 

559 type = attrib() 

560 

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

562 """ 

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

564 """ 

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

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

567 raise TypeError( 

568 msg, 

569 attr, 

570 self.type, 

571 value, 

572 ) 

573 

574 def __repr__(self): 

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

576 

577 

578def _subclass_of(type): 

579 """ 

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

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

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

583 

584 Args: 

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

586 

587 Raises: 

588 TypeError: 

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

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

591 """ 

592 return _SubclassOfValidator(type) 

593 

594 

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

596class _NotValidator: 

597 validator = attrib() 

598 msg = attrib( 

599 converter=default_if_none( 

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

601 "did not raise a captured error" 

602 ) 

603 ) 

604 exc_types = attrib( 

605 validator=deep_iterable( 

606 member_validator=_subclass_of(Exception), 

607 iterable_validator=instance_of(tuple), 

608 ), 

609 ) 

610 

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

612 try: 

613 self.validator(inst, attr, value) 

614 except self.exc_types: 

615 pass # suppress error to invert validity 

616 else: 

617 raise ValueError( 

618 self.msg.format( 

619 validator=self.validator, 

620 exc_types=self.exc_types, 

621 ), 

622 attr, 

623 self.validator, 

624 value, 

625 self.exc_types, 

626 ) 

627 

628 def __repr__(self): 

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

630 

631 

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

633 """ 

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

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

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

637 if the provided validator *does*. 

638 

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

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

641 

642 Args: 

643 validator: A validator to be logically inverted. 

644 

645 msg (str): 

646 Message to raise if validator fails. Formatted with keys 

647 ``exc_types`` and ``validator``. 

648 

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

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

651 validators will not be intercepted and pass through. 

652 

653 Raises: 

654 ValueError: 

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

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

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

658 

659 .. versionadded:: 22.2.0 

660 """ 

661 try: 

662 exc_types = tuple(exc_types) 

663 except TypeError: 

664 exc_types = (exc_types,) 

665 return _NotValidator(validator, msg, exc_types) 

666 

667 

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

669class _OrValidator: 

670 validators = attrib() 

671 

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

673 for v in self.validators: 

674 try: 

675 v(inst, attr, value) 

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

677 continue 

678 else: 

679 return 

680 

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

682 raise ValueError(msg) 

683 

684 def __repr__(self): 

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

686 

687 

688def or_(*validators): 

689 """ 

690 A validator that composes multiple validators into one. 

691 

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

693 satisfied. 

694 

695 Args: 

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

697 Arbitrary number of validators. 

698 

699 Raises: 

700 ValueError: 

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

702 message listing all the wrapped validators and the value that 

703 failed all of them. 

704 

705 .. versionadded:: 24.1.0 

706 """ 

707 vals = [] 

708 for v in validators: 

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

710 

711 return _OrValidator(tuple(vals))