Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

validators.py 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  1. # SPDX-License-Identifier: MIT
  2. """
  3. Commonly useful validators.
  4. """
  5. import operator
  6. import re
  7. from contextlib import contextmanager
  8. from re import Pattern
  9. from ._config import get_run_validators, set_run_validators
  10. from ._make import _AndValidator, and_, attrib, attrs
  11. from .converters import default_if_none
  12. from .exceptions import NotCallableError
  13. __all__ = [
  14. "and_",
  15. "deep_iterable",
  16. "deep_mapping",
  17. "disabled",
  18. "ge",
  19. "get_disabled",
  20. "gt",
  21. "in_",
  22. "instance_of",
  23. "is_callable",
  24. "le",
  25. "lt",
  26. "matches_re",
  27. "max_len",
  28. "min_len",
  29. "not_",
  30. "optional",
  31. "provides",
  32. "set_disabled",
  33. ]
  34. def set_disabled(disabled):
  35. """
  36. Globally disable or enable running validators.
  37. By default, they are run.
  38. :param disabled: If ``True``, disable running all validators.
  39. :type disabled: bool
  40. .. warning::
  41. This function is not thread-safe!
  42. .. versionadded:: 21.3.0
  43. """
  44. set_run_validators(not disabled)
  45. def get_disabled():
  46. """
  47. Return a bool indicating whether validators are currently disabled or not.
  48. :return: ``True`` if validators are currently disabled.
  49. :rtype: bool
  50. .. versionadded:: 21.3.0
  51. """
  52. return not get_run_validators()
  53. @contextmanager
  54. def disabled():
  55. """
  56. Context manager that disables running validators within its context.
  57. .. warning::
  58. This context manager is not thread-safe!
  59. .. versionadded:: 21.3.0
  60. """
  61. set_run_validators(False)
  62. try:
  63. yield
  64. finally:
  65. set_run_validators(True)
  66. @attrs(repr=False, slots=True, hash=True)
  67. class _InstanceOfValidator:
  68. type = attrib()
  69. def __call__(self, inst, attr, value):
  70. """
  71. We use a callable class to be able to change the ``__repr__``.
  72. """
  73. if not isinstance(value, self.type):
  74. raise TypeError(
  75. "'{name}' must be {type!r} (got {value!r} that is a "
  76. "{actual!r}).".format(
  77. name=attr.name,
  78. type=self.type,
  79. actual=value.__class__,
  80. value=value,
  81. ),
  82. attr,
  83. self.type,
  84. value,
  85. )
  86. def __repr__(self):
  87. return "<instance_of validator for type {type!r}>".format(
  88. type=self.type
  89. )
  90. def instance_of(type):
  91. """
  92. A validator that raises a `TypeError` if the initializer is called
  93. with a wrong type for this particular attribute (checks are performed using
  94. `isinstance` therefore it's also valid to pass a tuple of types).
  95. :param type: The type to check for.
  96. :type type: type or tuple of type
  97. :raises TypeError: With a human readable error message, the attribute
  98. (of type `attrs.Attribute`), the expected type, and the value it
  99. got.
  100. """
  101. return _InstanceOfValidator(type)
  102. @attrs(repr=False, frozen=True, slots=True)
  103. class _MatchesReValidator:
  104. pattern = attrib()
  105. match_func = attrib()
  106. def __call__(self, inst, attr, value):
  107. """
  108. We use a callable class to be able to change the ``__repr__``.
  109. """
  110. if not self.match_func(value):
  111. raise ValueError(
  112. "'{name}' must match regex {pattern!r}"
  113. " ({value!r} doesn't)".format(
  114. name=attr.name, pattern=self.pattern.pattern, value=value
  115. ),
  116. attr,
  117. self.pattern,
  118. value,
  119. )
  120. def __repr__(self):
  121. return "<matches_re validator for pattern {pattern!r}>".format(
  122. pattern=self.pattern
  123. )
  124. def matches_re(regex, flags=0, func=None):
  125. r"""
  126. A validator that raises `ValueError` if the initializer is called
  127. with a string that doesn't match *regex*.
  128. :param regex: a regex string or precompiled pattern to match against
  129. :param int flags: flags that will be passed to the underlying re function
  130. (default 0)
  131. :param callable func: which underlying `re` function to call. Valid options
  132. are `re.fullmatch`, `re.search`, and `re.match`; the default ``None``
  133. means `re.fullmatch`. For performance reasons, the pattern is always
  134. precompiled using `re.compile`.
  135. .. versionadded:: 19.2.0
  136. .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern.
  137. """
  138. valid_funcs = (re.fullmatch, None, re.search, re.match)
  139. if func not in valid_funcs:
  140. raise ValueError(
  141. "'func' must be one of {}.".format(
  142. ", ".join(
  143. sorted(
  144. e and e.__name__ or "None" for e in set(valid_funcs)
  145. )
  146. )
  147. )
  148. )
  149. if isinstance(regex, Pattern):
  150. if flags:
  151. raise TypeError(
  152. "'flags' can only be used with a string pattern; "
  153. "pass flags to re.compile() instead"
  154. )
  155. pattern = regex
  156. else:
  157. pattern = re.compile(regex, flags)
  158. if func is re.match:
  159. match_func = pattern.match
  160. elif func is re.search:
  161. match_func = pattern.search
  162. else:
  163. match_func = pattern.fullmatch
  164. return _MatchesReValidator(pattern, match_func)
  165. @attrs(repr=False, slots=True, hash=True)
  166. class _ProvidesValidator:
  167. interface = attrib()
  168. def __call__(self, inst, attr, value):
  169. """
  170. We use a callable class to be able to change the ``__repr__``.
  171. """
  172. if not self.interface.providedBy(value):
  173. raise TypeError(
  174. "'{name}' must provide {interface!r} which {value!r} "
  175. "doesn't.".format(
  176. name=attr.name, interface=self.interface, value=value
  177. ),
  178. attr,
  179. self.interface,
  180. value,
  181. )
  182. def __repr__(self):
  183. return "<provides validator for interface {interface!r}>".format(
  184. interface=self.interface
  185. )
  186. def provides(interface):
  187. """
  188. A validator that raises a `TypeError` if the initializer is called
  189. with an object that does not provide the requested *interface* (checks are
  190. performed using ``interface.providedBy(value)`` (see `zope.interface
  191. <https://zopeinterface.readthedocs.io/en/latest/>`_).
  192. :param interface: The interface to check for.
  193. :type interface: ``zope.interface.Interface``
  194. :raises TypeError: With a human readable error message, the attribute
  195. (of type `attrs.Attribute`), the expected interface, and the
  196. value it got.
  197. .. deprecated:: 23.1.0
  198. """
  199. import warnings
  200. warnings.warn(
  201. "attrs's zope-interface support is deprecated and will be removed in, "
  202. "or after, April 2024.",
  203. DeprecationWarning,
  204. stacklevel=2,
  205. )
  206. return _ProvidesValidator(interface)
  207. @attrs(repr=False, slots=True, hash=True)
  208. class _OptionalValidator:
  209. validator = attrib()
  210. def __call__(self, inst, attr, value):
  211. if value is None:
  212. return
  213. self.validator(inst, attr, value)
  214. def __repr__(self):
  215. return "<optional validator for {what} or None>".format(
  216. what=repr(self.validator)
  217. )
  218. def optional(validator):
  219. """
  220. A validator that makes an attribute optional. An optional attribute is one
  221. which can be set to ``None`` in addition to satisfying the requirements of
  222. the sub-validator.
  223. :param Callable | tuple[Callable] | list[Callable] validator: A validator
  224. (or validators) that is used for non-``None`` values.
  225. .. versionadded:: 15.1.0
  226. .. versionchanged:: 17.1.0 *validator* can be a list of validators.
  227. .. versionchanged:: 23.1.0 *validator* can also be a tuple of validators.
  228. """
  229. if isinstance(validator, (list, tuple)):
  230. return _OptionalValidator(_AndValidator(validator))
  231. return _OptionalValidator(validator)
  232. @attrs(repr=False, slots=True, hash=True)
  233. class _InValidator:
  234. options = attrib()
  235. def __call__(self, inst, attr, value):
  236. try:
  237. in_options = value in self.options
  238. except TypeError: # e.g. `1 in "abc"`
  239. in_options = False
  240. if not in_options:
  241. raise ValueError(
  242. "'{name}' must be in {options!r} (got {value!r})".format(
  243. name=attr.name, options=self.options, value=value
  244. ),
  245. attr,
  246. self.options,
  247. value,
  248. )
  249. def __repr__(self):
  250. return "<in_ validator with options {options!r}>".format(
  251. options=self.options
  252. )
  253. def in_(options):
  254. """
  255. A validator that raises a `ValueError` if the initializer is called
  256. with a value that does not belong in the options provided. The check is
  257. performed using ``value in options``.
  258. :param options: Allowed options.
  259. :type options: list, tuple, `enum.Enum`, ...
  260. :raises ValueError: With a human readable error message, the attribute (of
  261. type `attrs.Attribute`), the expected options, and the value it
  262. got.
  263. .. versionadded:: 17.1.0
  264. .. versionchanged:: 22.1.0
  265. The ValueError was incomplete until now and only contained the human
  266. readable error message. Now it contains all the information that has
  267. been promised since 17.1.0.
  268. """
  269. return _InValidator(options)
  270. @attrs(repr=False, slots=False, hash=True)
  271. class _IsCallableValidator:
  272. def __call__(self, inst, attr, value):
  273. """
  274. We use a callable class to be able to change the ``__repr__``.
  275. """
  276. if not callable(value):
  277. message = (
  278. "'{name}' must be callable "
  279. "(got {value!r} that is a {actual!r})."
  280. )
  281. raise NotCallableError(
  282. msg=message.format(
  283. name=attr.name, value=value, actual=value.__class__
  284. ),
  285. value=value,
  286. )
  287. def __repr__(self):
  288. return "<is_callable validator>"
  289. def is_callable():
  290. """
  291. A validator that raises a `attrs.exceptions.NotCallableError` if the
  292. initializer is called with a value for this particular attribute
  293. that is not callable.
  294. .. versionadded:: 19.1.0
  295. :raises attrs.exceptions.NotCallableError: With a human readable error
  296. message containing the attribute (`attrs.Attribute`) name,
  297. and the value it got.
  298. """
  299. return _IsCallableValidator()
  300. @attrs(repr=False, slots=True, hash=True)
  301. class _DeepIterable:
  302. member_validator = attrib(validator=is_callable())
  303. iterable_validator = attrib(
  304. default=None, validator=optional(is_callable())
  305. )
  306. def __call__(self, inst, attr, value):
  307. """
  308. We use a callable class to be able to change the ``__repr__``.
  309. """
  310. if self.iterable_validator is not None:
  311. self.iterable_validator(inst, attr, value)
  312. for member in value:
  313. self.member_validator(inst, attr, member)
  314. def __repr__(self):
  315. iterable_identifier = (
  316. ""
  317. if self.iterable_validator is None
  318. else f" {self.iterable_validator!r}"
  319. )
  320. return (
  321. "<deep_iterable validator for{iterable_identifier}"
  322. " iterables of {member!r}>"
  323. ).format(
  324. iterable_identifier=iterable_identifier,
  325. member=self.member_validator,
  326. )
  327. def deep_iterable(member_validator, iterable_validator=None):
  328. """
  329. A validator that performs deep validation of an iterable.
  330. :param member_validator: Validator(s) to apply to iterable members
  331. :param iterable_validator: Validator to apply to iterable itself
  332. (optional)
  333. .. versionadded:: 19.1.0
  334. :raises TypeError: if any sub-validators fail
  335. """
  336. if isinstance(member_validator, (list, tuple)):
  337. member_validator = and_(*member_validator)
  338. return _DeepIterable(member_validator, iterable_validator)
  339. @attrs(repr=False, slots=True, hash=True)
  340. class _DeepMapping:
  341. key_validator = attrib(validator=is_callable())
  342. value_validator = attrib(validator=is_callable())
  343. mapping_validator = attrib(default=None, validator=optional(is_callable()))
  344. def __call__(self, inst, attr, value):
  345. """
  346. We use a callable class to be able to change the ``__repr__``.
  347. """
  348. if self.mapping_validator is not None:
  349. self.mapping_validator(inst, attr, value)
  350. for key in value:
  351. self.key_validator(inst, attr, key)
  352. self.value_validator(inst, attr, value[key])
  353. def __repr__(self):
  354. return (
  355. "<deep_mapping validator for objects mapping {key!r} to {value!r}>"
  356. ).format(key=self.key_validator, value=self.value_validator)
  357. def deep_mapping(key_validator, value_validator, mapping_validator=None):
  358. """
  359. A validator that performs deep validation of a dictionary.
  360. :param key_validator: Validator to apply to dictionary keys
  361. :param value_validator: Validator to apply to dictionary values
  362. :param mapping_validator: Validator to apply to top-level mapping
  363. attribute (optional)
  364. .. versionadded:: 19.1.0
  365. :raises TypeError: if any sub-validators fail
  366. """
  367. return _DeepMapping(key_validator, value_validator, mapping_validator)
  368. @attrs(repr=False, frozen=True, slots=True)
  369. class _NumberValidator:
  370. bound = attrib()
  371. compare_op = attrib()
  372. compare_func = attrib()
  373. def __call__(self, inst, attr, value):
  374. """
  375. We use a callable class to be able to change the ``__repr__``.
  376. """
  377. if not self.compare_func(value, self.bound):
  378. raise ValueError(
  379. "'{name}' must be {op} {bound}: {value}".format(
  380. name=attr.name,
  381. op=self.compare_op,
  382. bound=self.bound,
  383. value=value,
  384. )
  385. )
  386. def __repr__(self):
  387. return "<Validator for x {op} {bound}>".format(
  388. op=self.compare_op, bound=self.bound
  389. )
  390. def lt(val):
  391. """
  392. A validator that raises `ValueError` if the initializer is called
  393. with a number larger or equal to *val*.
  394. :param val: Exclusive upper bound for values
  395. .. versionadded:: 21.3.0
  396. """
  397. return _NumberValidator(val, "<", operator.lt)
  398. def le(val):
  399. """
  400. A validator that raises `ValueError` if the initializer is called
  401. with a number greater than *val*.
  402. :param val: Inclusive upper bound for values
  403. .. versionadded:: 21.3.0
  404. """
  405. return _NumberValidator(val, "<=", operator.le)
  406. def ge(val):
  407. """
  408. A validator that raises `ValueError` if the initializer is called
  409. with a number smaller than *val*.
  410. :param val: Inclusive lower bound for values
  411. .. versionadded:: 21.3.0
  412. """
  413. return _NumberValidator(val, ">=", operator.ge)
  414. def gt(val):
  415. """
  416. A validator that raises `ValueError` if the initializer is called
  417. with a number smaller or equal to *val*.
  418. :param val: Exclusive lower bound for values
  419. .. versionadded:: 21.3.0
  420. """
  421. return _NumberValidator(val, ">", operator.gt)
  422. @attrs(repr=False, frozen=True, slots=True)
  423. class _MaxLengthValidator:
  424. max_length = attrib()
  425. def __call__(self, inst, attr, value):
  426. """
  427. We use a callable class to be able to change the ``__repr__``.
  428. """
  429. if len(value) > self.max_length:
  430. raise ValueError(
  431. "Length of '{name}' must be <= {max}: {len}".format(
  432. name=attr.name, max=self.max_length, len=len(value)
  433. )
  434. )
  435. def __repr__(self):
  436. return f"<max_len validator for {self.max_length}>"
  437. def max_len(length):
  438. """
  439. A validator that raises `ValueError` if the initializer is called
  440. with a string or iterable that is longer than *length*.
  441. :param int length: Maximum length of the string or iterable
  442. .. versionadded:: 21.3.0
  443. """
  444. return _MaxLengthValidator(length)
  445. @attrs(repr=False, frozen=True, slots=True)
  446. class _MinLengthValidator:
  447. min_length = attrib()
  448. def __call__(self, inst, attr, value):
  449. """
  450. We use a callable class to be able to change the ``__repr__``.
  451. """
  452. if len(value) < self.min_length:
  453. raise ValueError(
  454. "Length of '{name}' must be => {min}: {len}".format(
  455. name=attr.name, min=self.min_length, len=len(value)
  456. )
  457. )
  458. def __repr__(self):
  459. return f"<min_len validator for {self.min_length}>"
  460. def min_len(length):
  461. """
  462. A validator that raises `ValueError` if the initializer is called
  463. with a string or iterable that is shorter than *length*.
  464. :param int length: Minimum length of the string or iterable
  465. .. versionadded:: 22.1.0
  466. """
  467. return _MinLengthValidator(length)
  468. @attrs(repr=False, slots=True, hash=True)
  469. class _SubclassOfValidator:
  470. type = attrib()
  471. def __call__(self, inst, attr, value):
  472. """
  473. We use a callable class to be able to change the ``__repr__``.
  474. """
  475. if not issubclass(value, self.type):
  476. raise TypeError(
  477. "'{name}' must be a subclass of {type!r} "
  478. "(got {value!r}).".format(
  479. name=attr.name,
  480. type=self.type,
  481. value=value,
  482. ),
  483. attr,
  484. self.type,
  485. value,
  486. )
  487. def __repr__(self):
  488. return "<subclass_of validator for type {type!r}>".format(
  489. type=self.type
  490. )
  491. def _subclass_of(type):
  492. """
  493. A validator that raises a `TypeError` if the initializer is called
  494. with a wrong type for this particular attribute (checks are performed using
  495. `issubclass` therefore it's also valid to pass a tuple of types).
  496. :param type: The type to check for.
  497. :type type: type or tuple of types
  498. :raises TypeError: With a human readable error message, the attribute
  499. (of type `attrs.Attribute`), the expected type, and the value it
  500. got.
  501. """
  502. return _SubclassOfValidator(type)
  503. @attrs(repr=False, slots=True, hash=True)
  504. class _NotValidator:
  505. validator = attrib()
  506. msg = attrib(
  507. converter=default_if_none(
  508. "not_ validator child '{validator!r}' "
  509. "did not raise a captured error"
  510. )
  511. )
  512. exc_types = attrib(
  513. validator=deep_iterable(
  514. member_validator=_subclass_of(Exception),
  515. iterable_validator=instance_of(tuple),
  516. ),
  517. )
  518. def __call__(self, inst, attr, value):
  519. try:
  520. self.validator(inst, attr, value)
  521. except self.exc_types:
  522. pass # suppress error to invert validity
  523. else:
  524. raise ValueError(
  525. self.msg.format(
  526. validator=self.validator,
  527. exc_types=self.exc_types,
  528. ),
  529. attr,
  530. self.validator,
  531. value,
  532. self.exc_types,
  533. )
  534. def __repr__(self):
  535. return (
  536. "<not_ validator wrapping {what!r}, " "capturing {exc_types!r}>"
  537. ).format(
  538. what=self.validator,
  539. exc_types=self.exc_types,
  540. )
  541. def not_(validator, *, msg=None, exc_types=(ValueError, TypeError)):
  542. """
  543. A validator that wraps and logically 'inverts' the validator passed to it.
  544. It will raise a `ValueError` if the provided validator *doesn't* raise a
  545. `ValueError` or `TypeError` (by default), and will suppress the exception
  546. if the provided validator *does*.
  547. Intended to be used with existing validators to compose logic without
  548. needing to create inverted variants, for example, ``not_(in_(...))``.
  549. :param validator: A validator to be logically inverted.
  550. :param msg: Message to raise if validator fails.
  551. Formatted with keys ``exc_types`` and ``validator``.
  552. :type msg: str
  553. :param exc_types: Exception type(s) to capture.
  554. Other types raised by child validators will not be intercepted and
  555. pass through.
  556. :raises ValueError: With a human readable error message,
  557. the attribute (of type `attrs.Attribute`),
  558. the validator that failed to raise an exception,
  559. the value it got,
  560. and the expected exception types.
  561. .. versionadded:: 22.2.0
  562. """
  563. try:
  564. exc_types = tuple(exc_types)
  565. except TypeError:
  566. exc_types = (exc_types,)
  567. return _NotValidator(validator, msg, exc_types)