Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/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

222 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 to apply to iterable members. 

365 

366 iterable_validator: 

367 Validator to apply to iterable itself (optional). 

368 

369 Raises 

370 TypeError: if any sub-validators fail 

371 

372 .. versionadded:: 19.1.0 

373 """ 

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

375 member_validator = and_(*member_validator) 

376 return _DeepIterable(member_validator, iterable_validator) 

377 

378 

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

380class _DeepMapping: 

381 key_validator = attrib(validator=is_callable()) 

382 value_validator = attrib(validator=is_callable()) 

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

384 

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

386 """ 

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

388 """ 

389 if self.mapping_validator is not None: 

390 self.mapping_validator(inst, attr, value) 

391 

392 for key in value: 

393 self.key_validator(inst, attr, key) 

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

395 

396 def __repr__(self): 

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

398 

399 

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

401 """ 

402 A validator that performs deep validation of a dictionary. 

403 

404 Args: 

405 key_validator: Validator to apply to dictionary keys. 

406 

407 value_validator: Validator to apply to dictionary values. 

408 

409 mapping_validator: 

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

411 

412 .. versionadded:: 19.1.0 

413 

414 Raises: 

415 TypeError: if any sub-validators fail 

416 """ 

417 return _DeepMapping(key_validator, value_validator, mapping_validator) 

418 

419 

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

421class _NumberValidator: 

422 bound = attrib() 

423 compare_op = attrib() 

424 compare_func = attrib() 

425 

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

427 """ 

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

429 """ 

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

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

432 raise ValueError(msg) 

433 

434 def __repr__(self): 

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

436 

437 

438def lt(val): 

439 """ 

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

441 number larger or equal to *val*. 

442 

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

444 

445 Args: 

446 val: Exclusive upper bound for values. 

447 

448 .. versionadded:: 21.3.0 

449 """ 

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

451 

452 

453def le(val): 

454 """ 

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

456 number greater than *val*. 

457 

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

459 

460 Args: 

461 val: Inclusive upper bound for values. 

462 

463 .. versionadded:: 21.3.0 

464 """ 

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

466 

467 

468def ge(val): 

469 """ 

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

471 number smaller than *val*. 

472 

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

474 

475 Args: 

476 val: Inclusive lower bound for values 

477 

478 .. versionadded:: 21.3.0 

479 """ 

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

481 

482 

483def gt(val): 

484 """ 

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

486 number smaller or equal to *val*. 

487 

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

489 

490 Args: 

491 val: Exclusive lower bound for values 

492 

493 .. versionadded:: 21.3.0 

494 """ 

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

496 

497 

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

499class _MaxLengthValidator: 

500 max_length = attrib() 

501 

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

503 """ 

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

505 """ 

506 if len(value) > self.max_length: 

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

508 raise ValueError(msg) 

509 

510 def __repr__(self): 

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

512 

513 

514def max_len(length): 

515 """ 

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

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

518 

519 Args: 

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

521 

522 .. versionadded:: 21.3.0 

523 """ 

524 return _MaxLengthValidator(length) 

525 

526 

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

528class _MinLengthValidator: 

529 min_length = attrib() 

530 

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

532 """ 

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

534 """ 

535 if len(value) < self.min_length: 

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

537 raise ValueError(msg) 

538 

539 def __repr__(self): 

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

541 

542 

543def min_len(length): 

544 """ 

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

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

547 

548 Args: 

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

550 

551 .. versionadded:: 22.1.0 

552 """ 

553 return _MinLengthValidator(length) 

554 

555 

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

557class _SubclassOfValidator: 

558 type = attrib() 

559 

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

561 """ 

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

563 """ 

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

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

566 raise TypeError( 

567 msg, 

568 attr, 

569 self.type, 

570 value, 

571 ) 

572 

573 def __repr__(self): 

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

575 

576 

577def _subclass_of(type): 

578 """ 

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

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

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

582 

583 Args: 

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

585 

586 Raises: 

587 TypeError: 

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

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

590 """ 

591 return _SubclassOfValidator(type) 

592 

593 

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

595class _NotValidator: 

596 validator = attrib() 

597 msg = attrib( 

598 converter=default_if_none( 

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

600 "did not raise a captured error" 

601 ) 

602 ) 

603 exc_types = attrib( 

604 validator=deep_iterable( 

605 member_validator=_subclass_of(Exception), 

606 iterable_validator=instance_of(tuple), 

607 ), 

608 ) 

609 

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

611 try: 

612 self.validator(inst, attr, value) 

613 except self.exc_types: 

614 pass # suppress error to invert validity 

615 else: 

616 raise ValueError( 

617 self.msg.format( 

618 validator=self.validator, 

619 exc_types=self.exc_types, 

620 ), 

621 attr, 

622 self.validator, 

623 value, 

624 self.exc_types, 

625 ) 

626 

627 def __repr__(self): 

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

629 

630 

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

632 """ 

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

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

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

636 if the provided validator *does*. 

637 

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

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

640 

641 Args: 

642 validator: A validator to be logically inverted. 

643 

644 msg (str): 

645 Message to raise if validator fails. Formatted with keys 

646 ``exc_types`` and ``validator``. 

647 

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

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

650 validators will not be intercepted and pass through. 

651 

652 Raises: 

653 ValueError: 

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

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

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

657 

658 .. versionadded:: 22.2.0 

659 """ 

660 try: 

661 exc_types = tuple(exc_types) 

662 except TypeError: 

663 exc_types = (exc_types,) 

664 return _NotValidator(validator, msg, exc_types) 

665 

666 

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

668class _OrValidator: 

669 validators = attrib() 

670 

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

672 for v in self.validators: 

673 try: 

674 v(inst, attr, value) 

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

676 continue 

677 else: 

678 return 

679 

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

681 raise ValueError(msg) 

682 

683 def __repr__(self): 

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

685 

686 

687def or_(*validators): 

688 """ 

689 A validator that composes multiple validators into one. 

690 

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

692 satisfied. 

693 

694 Args: 

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

696 Arbitrary number of validators. 

697 

698 Raises: 

699 ValueError: 

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

701 message listing all the wrapped validators and the value that 

702 failed all of them. 

703 

704 .. versionadded:: 24.1.0 

705 """ 

706 vals = [] 

707 for v in validators: 

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

709 

710 return _OrValidator(tuple(vals))