1# SPDX-License-Identifier: MIT

2

3"""

4 Commonly useful validators.

5"""

6

7

8 import operator

9 import re

10

11 from contextlib import contextmanager

12

13 from ._config import get_run_validators, set_run_validators

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

15 from .converters import default_if_none

16 from .exceptions import NotCallableError

17

18

19 try:

20 Pattern = re.Pattern

21 except AttributeError: # Python <3.7 lacks a Pattern type.

22 Pattern = type(re.compile(""))

23

24

25__all__ = [

26 "and_",

27 "deep_iterable",

28 "deep_mapping",

29 "disabled",

30 "ge",

31 "get_disabled",

32 "gt",

33 "in_",

34 "instance_of",

35 "is_callable",

36 "le",

37 "lt",

38 "matches_re",

39 "max_len",

40 "min_len",

41 "not_",

42 "optional",

43 "provides",

44 "set_disabled",

45]

46

47

48 def set_disabled(disabled):

49 """

50 Globally disable or enable running validators.

51

52 By default, they are run.

53

54 :param disabled: If ``True``, disable running all validators.

55 :type disabled: bool

56

57 .. warning::

58

59 This function is not thread-safe!

60

61 .. versionadded:: 21.3.0

62 """

63 set_run_validators(not disabled)

64

65

66 def get_disabled():

67 """

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

69

70 :return: ``True`` if validators are currently disabled.

71 :rtype: bool

72

73 .. versionadded:: 21.3.0

74 """

75 return not get_run_validators()

76

77

78@contextmanager

79 def disabled():

80 """

81 Context manager that disables running validators within its context.

82

83 .. warning::

84

85 This context manager is not thread-safe!

86

87 .. versionadded:: 21.3.0

88 """

89 set_run_validators(False)

90 try:

91 yield

92 finally:

93 set_run_validators(True)

94

95

96@attrs(repr=False, slots=True, hash=True)

97 class _InstanceOfValidator:

98 type = attrib()

99

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

101 """

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

103 """

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

105 raise TypeError(

106 "'{name}' must be {type!r} (got {value!r} that is a "

107 "{actual!r}).".format(

108 name=attr.name,

109 type=self.type,

110 actual=value.__class__,

111 value=value,

112 ),

113 attr,

114 self.type,

115 value,

116 )

117

118 def __repr__(self):

119 return "<instance_of validator for type {type!r}>".format(

120 type=self.type

121 )

122

123

124 def instance_of(type):

125 """

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

127 with a wrong type for this particular attribute (checks are performed using

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

129

130 :param type: The type to check for.

131 :type type: type or tuple of type

132

133 :raises TypeError: With a human readable error message, the attribute

134 (of type `attrs.Attribute`), the expected type, and the value it

135 got.

136 """

137 return _InstanceOfValidator(type)

138

139

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

141 class _MatchesReValidator:

142 pattern = attrib()

143 match_func = attrib()

144

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

146 """

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

148 """

149 if not self.match_func(value):

150 raise ValueError(

151 "'{name}' must match regex {pattern!r}"

152 " ({value!r} doesn't)".format(

153 name=attr.name, pattern=self.pattern.pattern, value=value

154 ),

155 attr,

156 self.pattern,

157 value,

158 )

159

160 def __repr__(self):

161 return "<matches_re validator for pattern {pattern!r}>".format(

162 pattern=self.pattern

163 )

164

165

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

167 r"""

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

169 with a string that doesn't match *regex*.

170

171 :param regex: a regex string or precompiled pattern to match against

172 :param int flags: flags that will be passed to the underlying re function

173 (default 0)

174 :param callable func: which underlying `re` function to call. Valid options

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

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

177 precompiled using `re.compile`.

178

179 .. versionadded:: 19.2.0

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

181 """

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

183 if func not in valid_funcs:

184 raise ValueError(

185 "'func' must be one of {}.".format(

186 ", ".join(

187 sorted(

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

189 )

190 )

191 )

192 )

193

194 if isinstance(regex, Pattern):

195 if flags:

196 raise TypeError(

197 "'flags' can only be used with a string pattern; "

198 "pass flags to re.compile() instead"

199 )

200 pattern = regex

201 else:

202 pattern = re.compile(regex, flags)

203

204 if func is re.match:

205 match_func = pattern.match

206 elif func is re.search:

207 match_func = pattern.search

208 else:

209 match_func = pattern.fullmatch

210

211 return _MatchesReValidator(pattern, match_func)

212

213

214@attrs(repr=False, slots=True, hash=True)

215 class _ProvidesValidator:

216 interface = attrib()

217

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

219 """

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

221 """

222 if not self.interface.providedBy(value):

223 raise TypeError(

224 "'{name}' must provide {interface!r} which {value!r} "

225 "doesn't.".format(

226 name=attr.name, interface=self.interface, value=value

227 ),

228 attr,

229 self.interface,

230 value,

231 )

232

233 def __repr__(self):

234 return "<provides validator for interface {interface!r}>".format(

235 interface=self.interface

236 )

237

238

239 def provides(interface):

240 """

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

242 with an object that does not provide the requested *interface* (checks are

243 performed using ``interface.providedBy(value)`` (see `zope.interface

244 <https://zopeinterface.readthedocs.io/en/latest/>`_).

245

246 :param interface: The interface to check for.

247 :type interface: ``zope.interface.Interface``

248

249 :raises TypeError: With a human readable error message, the attribute

250 (of type `attrs.Attribute`), the expected interface, and the

251 value it got.

252 """

253 return _ProvidesValidator(interface)

254

255

256@attrs(repr=False, slots=True, hash=True)

257 class _OptionalValidator:

258 validator = attrib()

259

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

261 if value is None:

262 return

263

264 self.validator(inst, attr, value)

265

266 def __repr__(self):

267 return "<optional validator for {what} or None>".format(

268 what=repr(self.validator)

269 )

270

271

272 def optional(validator):

273 """

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

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

276 the sub-validator.

277

278 :param validator: A validator (or a list of validators) that is used for

279 non-``None`` values.

280 :type validator: callable or `list` of callables.

281

282 .. versionadded:: 15.1.0

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

284 """

285 if isinstance(validator, list):

286 return _OptionalValidator(_AndValidator(validator))

287 return _OptionalValidator(validator)

288

289

290@attrs(repr=False, slots=True, hash=True)

291 class _InValidator:

292 options = attrib()

293

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

295 try:

296 in_options = value in self.options

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

298 in_options = False

299

300 if not in_options:

301 raise ValueError(

302 "'{name}' must be in {options!r} (got {value!r})".format(

303 name=attr.name, options=self.options, value=value

304 ),

305 attr,

306 self.options,

307 value,

308 )

309

310 def __repr__(self):

311 return "<in_ validator with options {options!r}>".format(

312 options=self.options

313 )

314

315

316 def in_(options):

317 """

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

319 with a value that does not belong in the options provided. The check is

320 performed using ``value in options``.

321

322 :param options: Allowed options.

323 :type options: list, tuple, `enum.Enum`, ...

324

325 :raises ValueError: With a human readable error message, the attribute (of

326 type `attrs.Attribute`), the expected options, and the value it

327 got.

328

329 .. versionadded:: 17.1.0

330 .. versionchanged:: 22.1.0

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

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

333 been promised since 17.1.0.

334 """

335 return _InValidator(options)

336

337

338@attrs(repr=False, slots=False, hash=True)

339 class _IsCallableValidator:

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

341 """

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

343 """

344 if not callable(value):

345 message = (

346 "'{name}' must be callable "

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

348 )

349 raise NotCallableError(

350 msg=message.format(

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

352 ),

353 value=value,

354 )

355

356 def __repr__(self):

357 return "<is_callable validator>"

358

359

360 def is_callable():

361 """

362 A validator that raises a `attr.exceptions.NotCallableError` if the

363 initializer is called with a value for this particular attribute

364 that is not callable.

365

366 .. versionadded:: 19.1.0

367

368 :raises `attr.exceptions.NotCallableError`: With a human readable error

369 message containing the attribute (`attrs.Attribute`) name,

370 and the value it got.

371 """

372 return _IsCallableValidator()

373

374

375@attrs(repr=False, slots=True, hash=True)

376 class _DeepIterable:

377 member_validator = attrib(validator=is_callable())

378 iterable_validator = attrib(

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

380 )

381

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

383 """

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

385 """

386 if self.iterable_validator is not None:

387 self.iterable_validator(inst, attr, value)

388

389 for member in value:

390 self.member_validator(inst, attr, member)

391

392 def __repr__(self):

393 iterable_identifier = (

394 ""

395 if self.iterable_validator is None

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

397 )

398 return (

399 "<deep_iterable validator for{iterable_identifier}"

400 " iterables of {member!r}>"

401 ).format(

402 iterable_identifier=iterable_identifier,

403 member=self.member_validator,

404 )

405

406

407 def deep_iterable(member_validator, iterable_validator=None):

408 """

409 A validator that performs deep validation of an iterable.

410

411 :param member_validator: Validator(s) to apply to iterable members

412 :param iterable_validator: Validator to apply to iterable itself

413 (optional)

414

415 .. versionadded:: 19.1.0

416

417 :raises TypeError: if any sub-validators fail

418 """

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

420 member_validator = and_(*member_validator)

421 return _DeepIterable(member_validator, iterable_validator)

422

423

424@attrs(repr=False, slots=True, hash=True)

425 class _DeepMapping:

426 key_validator = attrib(validator=is_callable())

427 value_validator = attrib(validator=is_callable())

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

429

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

431 """

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

433 """

434 if self.mapping_validator is not None:

435 self.mapping_validator(inst, attr, value)

436

437 for key in value:

438 self.key_validator(inst, attr, key)

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

440

441 def __repr__(self):

442 return (

443 "<deep_mapping validator for objects mapping {key!r} to {value!r}>"

444 ).format(key=self.key_validator, value=self.value_validator)

445

446

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

448 """

449 A validator that performs deep validation of a dictionary.

450

451 :param key_validator: Validator to apply to dictionary keys

452 :param value_validator: Validator to apply to dictionary values

453 :param mapping_validator: Validator to apply to top-level mapping

454 attribute (optional)

455

456 .. versionadded:: 19.1.0

457

458 :raises TypeError: if any sub-validators fail

459 """

460 return _DeepMapping(key_validator, value_validator, mapping_validator)

461

462

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

464 class _NumberValidator:

465 bound = attrib()

466 compare_op = attrib()

467 compare_func = attrib()

468

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

470 """

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

472 """

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

474 raise ValueError(

475 "'{name}' must be {op} {bound}: {value}".format(

476 name=attr.name,

477 op=self.compare_op,

478 bound=self.bound,

479 value=value,

480 )

481 )

482

483 def __repr__(self):

484 return "<Validator for x {op} {bound}>".format(

485 op=self.compare_op, bound=self.bound

486 )

487

488

489 def lt(val):

490 """

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

492 with a number larger or equal to *val*.

493

494 :param val: Exclusive upper bound for values

495

496 .. versionadded:: 21.3.0

497 """

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

499

500

501 def le(val):

502 """

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

504 with a number greater than *val*.

505

506 :param val: Inclusive upper bound for values

507

508 .. versionadded:: 21.3.0

509 """

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

511

512

513 def ge(val):

514 """

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

516 with a number smaller than *val*.

517

518 :param val: Inclusive lower bound for values

519

520 .. versionadded:: 21.3.0

521 """

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

523

524

525 def gt(val):

526 """

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

528 with a number smaller or equal to *val*.

529

530 :param val: Exclusive lower bound for values

531

532 .. versionadded:: 21.3.0

533 """

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

535

536

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

538 class _MaxLengthValidator:

539 max_length = attrib()

540

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

542 """

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

544 """

545 if len(value) > self.max_length:

546 raise ValueError(

547 "Length of '{name}' must be <= {max}: {len}".format(

548 name=attr.name, max=self.max_length, len=len(value)

549 )

550 )

551

552 def __repr__(self):

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

554

555

556 def max_len(length):

557 """

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

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

560

561 :param int length: Maximum length of the string or iterable

562

563 .. versionadded:: 21.3.0

564 """

565 return _MaxLengthValidator(length)

566

567

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

569 class _MinLengthValidator:

570 min_length = attrib()

571

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

573 """

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

575 """

576 if len(value) < self.min_length:

577 raise ValueError(

578 "Length of '{name}' must be => {min}: {len}".format(

579 name=attr.name, min=self.min_length, len=len(value)

580 )

581 )

582

583 def __repr__(self):

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

585

586

587 def min_len(length):

588 """

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

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

591

592 :param int length: Minimum length of the string or iterable

593

594 .. versionadded:: 22.1.0

595 """

596 return _MinLengthValidator(length)

597

598

599@attrs(repr=False, slots=True, hash=True)

600 class _SubclassOfValidator:

601 type = attrib()

602

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

604 """

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

606 """

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

608 raise TypeError(

609 "'{name}' must be a subclass of {type!r} "

610 "(got {value!r}).".format(

611 name=attr.name,

612 type=self.type,

613 value=value,

614 ),

615 attr,

616 self.type,

617 value,

618 )

619

620 def __repr__(self):

621 return "<subclass_of validator for type {type!r}>".format(

622 type=self.type

623 )

624

625

626 def _subclass_of(type):

627 """

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

629 with a wrong type for this particular attribute (checks are performed using

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

631

632 :param type: The type to check for.

633 :type type: type or tuple of types

634

635 :raises TypeError: With a human readable error message, the attribute

636 (of type `attrs.Attribute`), the expected type, and the value it

637 got.

638 """

639 return _SubclassOfValidator(type)

640

641

642@attrs(repr=False, slots=True, hash=True)

643 class _NotValidator:

644 validator = attrib()

645 msg = attrib(

646 converter=default_if_none(

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

648 "did not raise a captured error"

649 )

650 )

651 exc_types = attrib(

652 validator=deep_iterable(

653 member_validator=_subclass_of(Exception),

654 iterable_validator=instance_of(tuple),

655 ),

656 )

657

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

659 try:

660 self.validator(inst, attr, value)

661 except self.exc_types:

662 pass # suppress error to invert validity

663 else:

664 raise ValueError(

665 self.msg.format(

666 validator=self.validator,

667 exc_types=self.exc_types,

668 ),

669 attr,

670 self.validator,

671 value,

672 self.exc_types,

673 )

674

675 def __repr__(self):

676 return (

677 "<not_ validator wrapping {what!r}, " "capturing {exc_types!r}>"

678 ).format(

679 what=self.validator,

680 exc_types=self.exc_types,

681 )

682

683

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

685 """

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

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

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

689 if the provided validator *does*.

690

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

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

693

694 :param validator: A validator to be logically inverted.

695 :param msg: Message to raise if validator fails.

696 Formatted with keys ``exc_types`` and ``validator``.

697 :type msg: str

698 :param exc_types: Exception type(s) to capture.

699 Other types raised by child validators will not be intercepted and

700 pass through.

701

702 :raises ValueError: With a human readable error message,

703 the attribute (of type `attrs.Attribute`),

704 the validator that failed to raise an exception,

705 the value it got,

706 and the expected exception types.

707

708 .. versionadded:: 22.2.0

709 """

710 try:

711 exc_types = tuple(exc_types)

712 except TypeError:

713 exc_types = (exc_types,)

714 return _NotValidator(validator, msg, exc_types)