validators.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. """
  2. Commonly useful validators.
  3. """
  4. from __future__ import absolute_import, division, print_function
  5. import re
  6. from ._make import _AndValidator, and_, attrib, attrs
  7. from .exceptions import NotCallableError
  8. __all__ = [
  9. "and_",
  10. "deep_iterable",
  11. "deep_mapping",
  12. "in_",
  13. "instance_of",
  14. "is_callable",
  15. "matches_re",
  16. "optional",
  17. "provides",
  18. ]
  19. @attrs(repr=False, slots=True, hash=True)
  20. class _InstanceOfValidator(object):
  21. type = attrib()
  22. def __call__(self, inst, attr, value):
  23. """
  24. We use a callable class to be able to change the ``__repr__``.
  25. """
  26. if not isinstance(value, self.type):
  27. raise TypeError(
  28. "'{name}' must be {type!r} (got {value!r} that is a "
  29. "{actual!r}).".format(
  30. name=attr.name,
  31. type=self.type,
  32. actual=value.__class__,
  33. value=value,
  34. ),
  35. attr,
  36. self.type,
  37. value,
  38. )
  39. def __repr__(self):
  40. return "<instance_of validator for type {type!r}>".format(
  41. type=self.type
  42. )
  43. def instance_of(type):
  44. """
  45. A validator that raises a `TypeError` if the initializer is called
  46. with a wrong type for this particular attribute (checks are performed using
  47. `isinstance` therefore it's also valid to pass a tuple of types).
  48. :param type: The type to check for.
  49. :type type: type or tuple of types
  50. :raises TypeError: With a human readable error message, the attribute
  51. (of type `attr.Attribute`), the expected type, and the value it
  52. got.
  53. """
  54. return _InstanceOfValidator(type)
  55. @attrs(repr=False, frozen=True, slots=True)
  56. class _MatchesReValidator(object):
  57. regex = attrib()
  58. flags = attrib()
  59. match_func = attrib()
  60. def __call__(self, inst, attr, value):
  61. """
  62. We use a callable class to be able to change the ``__repr__``.
  63. """
  64. if not self.match_func(value):
  65. raise ValueError(
  66. "'{name}' must match regex {regex!r}"
  67. " ({value!r} doesn't)".format(
  68. name=attr.name, regex=self.regex.pattern, value=value
  69. ),
  70. attr,
  71. self.regex,
  72. value,
  73. )
  74. def __repr__(self):
  75. return "<matches_re validator for pattern {regex!r}>".format(
  76. regex=self.regex
  77. )
  78. def matches_re(regex, flags=0, func=None):
  79. r"""
  80. A validator that raises `ValueError` if the initializer is called
  81. with a string that doesn't match *regex*.
  82. :param str regex: a regex string to match against
  83. :param int flags: flags that will be passed to the underlying re function
  84. (default 0)
  85. :param callable func: which underlying `re` function to call (options
  86. are `re.fullmatch`, `re.search`, `re.match`, default
  87. is ``None`` which means either `re.fullmatch` or an emulation of
  88. it on Python 2). For performance reasons, they won't be used directly
  89. but on a pre-`re.compile`\ ed pattern.
  90. .. versionadded:: 19.2.0
  91. """
  92. fullmatch = getattr(re, "fullmatch", None)
  93. valid_funcs = (fullmatch, None, re.search, re.match)
  94. if func not in valid_funcs:
  95. raise ValueError(
  96. "'func' must be one of %s."
  97. % (
  98. ", ".join(
  99. sorted(
  100. e and e.__name__ or "None" for e in set(valid_funcs)
  101. )
  102. ),
  103. )
  104. )
  105. pattern = re.compile(regex, flags)
  106. if func is re.match:
  107. match_func = pattern.match
  108. elif func is re.search:
  109. match_func = pattern.search
  110. else:
  111. if fullmatch:
  112. match_func = pattern.fullmatch
  113. else:
  114. pattern = re.compile(r"(?:{})\Z".format(regex), flags)
  115. match_func = pattern.match
  116. return _MatchesReValidator(pattern, flags, match_func)
  117. @attrs(repr=False, slots=True, hash=True)
  118. class _ProvidesValidator(object):
  119. interface = attrib()
  120. def __call__(self, inst, attr, value):
  121. """
  122. We use a callable class to be able to change the ``__repr__``.
  123. """
  124. if not self.interface.providedBy(value):
  125. raise TypeError(
  126. "'{name}' must provide {interface!r} which {value!r} "
  127. "doesn't.".format(
  128. name=attr.name, interface=self.interface, value=value
  129. ),
  130. attr,
  131. self.interface,
  132. value,
  133. )
  134. def __repr__(self):
  135. return "<provides validator for interface {interface!r}>".format(
  136. interface=self.interface
  137. )
  138. def provides(interface):
  139. """
  140. A validator that raises a `TypeError` if the initializer is called
  141. with an object that does not provide the requested *interface* (checks are
  142. performed using ``interface.providedBy(value)`` (see `zope.interface
  143. <https://zopeinterface.readthedocs.io/en/latest/>`_).
  144. :param interface: The interface to check for.
  145. :type interface: ``zope.interface.Interface``
  146. :raises TypeError: With a human readable error message, the attribute
  147. (of type `attr.Attribute`), the expected interface, and the
  148. value it got.
  149. """
  150. return _ProvidesValidator(interface)
  151. @attrs(repr=False, slots=True, hash=True)
  152. class _OptionalValidator(object):
  153. validator = attrib()
  154. def __call__(self, inst, attr, value):
  155. if value is None:
  156. return
  157. self.validator(inst, attr, value)
  158. def __repr__(self):
  159. return "<optional validator for {what} or None>".format(
  160. what=repr(self.validator)
  161. )
  162. def optional(validator):
  163. """
  164. A validator that makes an attribute optional. An optional attribute is one
  165. which can be set to ``None`` in addition to satisfying the requirements of
  166. the sub-validator.
  167. :param validator: A validator (or a list of validators) that is used for
  168. non-``None`` values.
  169. :type validator: callable or `list` of callables.
  170. .. versionadded:: 15.1.0
  171. .. versionchanged:: 17.1.0 *validator* can be a list of validators.
  172. """
  173. if isinstance(validator, list):
  174. return _OptionalValidator(_AndValidator(validator))
  175. return _OptionalValidator(validator)
  176. @attrs(repr=False, slots=True, hash=True)
  177. class _InValidator(object):
  178. options = attrib()
  179. def __call__(self, inst, attr, value):
  180. try:
  181. in_options = value in self.options
  182. except TypeError: # e.g. `1 in "abc"`
  183. in_options = False
  184. if not in_options:
  185. raise ValueError(
  186. "'{name}' must be in {options!r} (got {value!r})".format(
  187. name=attr.name, options=self.options, value=value
  188. )
  189. )
  190. def __repr__(self):
  191. return "<in_ validator with options {options!r}>".format(
  192. options=self.options
  193. )
  194. def in_(options):
  195. """
  196. A validator that raises a `ValueError` if the initializer is called
  197. with a value that does not belong in the options provided. The check is
  198. performed using ``value in options``.
  199. :param options: Allowed options.
  200. :type options: list, tuple, `enum.Enum`, ...
  201. :raises ValueError: With a human readable error message, the attribute (of
  202. type `attr.Attribute`), the expected options, and the value it
  203. got.
  204. .. versionadded:: 17.1.0
  205. """
  206. return _InValidator(options)
  207. @attrs(repr=False, slots=False, hash=True)
  208. class _IsCallableValidator(object):
  209. def __call__(self, inst, attr, value):
  210. """
  211. We use a callable class to be able to change the ``__repr__``.
  212. """
  213. if not callable(value):
  214. message = (
  215. "'{name}' must be callable "
  216. "(got {value!r} that is a {actual!r})."
  217. )
  218. raise NotCallableError(
  219. msg=message.format(
  220. name=attr.name, value=value, actual=value.__class__
  221. ),
  222. value=value,
  223. )
  224. def __repr__(self):
  225. return "<is_callable validator>"
  226. def is_callable():
  227. """
  228. A validator that raises a `attr.exceptions.NotCallableError` if the
  229. initializer is called with a value for this particular attribute
  230. that is not callable.
  231. .. versionadded:: 19.1.0
  232. :raises `attr.exceptions.NotCallableError`: With a human readable error
  233. message containing the attribute (`attr.Attribute`) name,
  234. and the value it got.
  235. """
  236. return _IsCallableValidator()
  237. @attrs(repr=False, slots=True, hash=True)
  238. class _DeepIterable(object):
  239. member_validator = attrib(validator=is_callable())
  240. iterable_validator = attrib(
  241. default=None, validator=optional(is_callable())
  242. )
  243. def __call__(self, inst, attr, value):
  244. """
  245. We use a callable class to be able to change the ``__repr__``.
  246. """
  247. if self.iterable_validator is not None:
  248. self.iterable_validator(inst, attr, value)
  249. for member in value:
  250. self.member_validator(inst, attr, member)
  251. def __repr__(self):
  252. iterable_identifier = (
  253. ""
  254. if self.iterable_validator is None
  255. else " {iterable!r}".format(iterable=self.iterable_validator)
  256. )
  257. return (
  258. "<deep_iterable validator for{iterable_identifier}"
  259. " iterables of {member!r}>"
  260. ).format(
  261. iterable_identifier=iterable_identifier,
  262. member=self.member_validator,
  263. )
  264. def deep_iterable(member_validator, iterable_validator=None):
  265. """
  266. A validator that performs deep validation of an iterable.
  267. :param member_validator: Validator to apply to iterable members
  268. :param iterable_validator: Validator to apply to iterable itself
  269. (optional)
  270. .. versionadded:: 19.1.0
  271. :raises TypeError: if any sub-validators fail
  272. """
  273. return _DeepIterable(member_validator, iterable_validator)
  274. @attrs(repr=False, slots=True, hash=True)
  275. class _DeepMapping(object):
  276. key_validator = attrib(validator=is_callable())
  277. value_validator = attrib(validator=is_callable())
  278. mapping_validator = attrib(default=None, validator=optional(is_callable()))
  279. def __call__(self, inst, attr, value):
  280. """
  281. We use a callable class to be able to change the ``__repr__``.
  282. """
  283. if self.mapping_validator is not None:
  284. self.mapping_validator(inst, attr, value)
  285. for key in value:
  286. self.key_validator(inst, attr, key)
  287. self.value_validator(inst, attr, value[key])
  288. def __repr__(self):
  289. return (
  290. "<deep_mapping validator for objects mapping {key!r} to {value!r}>"
  291. ).format(key=self.key_validator, value=self.value_validator)
  292. def deep_mapping(key_validator, value_validator, mapping_validator=None):
  293. """
  294. A validator that performs deep validation of a dictionary.
  295. :param key_validator: Validator to apply to dictionary keys
  296. :param value_validator: Validator to apply to dictionary values
  297. :param mapping_validator: Validator to apply to top-level mapping
  298. attribute (optional)
  299. .. versionadded:: 19.1.0
  300. :raises TypeError: if any sub-validators fail
  301. """
  302. return _DeepMapping(key_validator, value_validator, mapping_validator)