Skip to content

Commit

Permalink
Improve the error message of iscallable validator
Browse files Browse the repository at this point in the history
  • Loading branch information
williamjamir committed Jun 17, 2019
1 parent 45adbb9 commit 3bfb793
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 10 deletions.
2 changes: 2 additions & 0 deletions changelog.d/536.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
``is_callable()`` validator now raises a ``NotCallableError`` exception informing the value and type received.
`#536 <https://github.com/python-attrs/attrs/pull/536>`_.
2 changes: 1 addition & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ Validators
>>> C("not a callable")
Traceback (most recent call last):
...
TypeError: 'x' must be callable
attr.exceptions.NotCallableError: 'x' must be callable (got 'not a callable' that is a <class 'str'>).


.. autofunction:: attr.validators.deep_iterable
Expand Down
17 changes: 17 additions & 0 deletions src/attr/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,20 @@ class PythonTooOldError(RuntimeError):
.. versionadded:: 18.2.0
"""


class NotCallableError(TypeError):
"""
A ``attr.ib()`` requiring a callable has been set with a value
that is not callable.
.. versionadded:: 19.2.0
"""

def __init__(self, message, value):
super(TypeError, self).__init__(message, value)
self.message = message
self.value = value

def __str__(self):
return str(self.message)
22 changes: 17 additions & 5 deletions src/attr/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from __future__ import absolute_import, division, print_function

from ._make import _AndValidator, and_, attrib, attrs
from .exceptions import NotCallableError


__all__ = [
Expand Down Expand Up @@ -186,21 +187,32 @@ def __call__(self, inst, attr, value):
We use a callable class to be able to change the ``__repr__``.
"""
if not callable(value):
raise TypeError("'{name}' must be callable".format(name=attr.name))
message = (
"'{name}' must be callable "
"(got {value!r} that is a {actual!r})."
)
raise NotCallableError(
message=message.format(
name=attr.name, value=value, actual=value.__class__
),
value=value,
)

def __repr__(self):
return "<is_callable validator>"


def is_callable():
"""
A validator that raises a :class:`TypeError` if the initializer is called
with a value for this particular attribute that is not callable.
A validator that raises a :class:`attr.exceptions.NotCallableError`
if the initializer is called with a value for this particular attribute
that is not callable.
.. versionadded:: 19.1.0
:raises TypeError: With a human readable error message containing the
attribute (of type :class:`attr.Attribute`) name.
:raises `attr.exceptions.NotCallableError`: With a human readable error
message containing the attribute (:class:`attr.Attribute`) name,
and the value it got.
"""
return _IsCallableValidator()

Expand Down
27 changes: 23 additions & 4 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,14 @@ def test_noncallable_validators(
"""
with pytest.raises(TypeError) as e:
deep_iterable(member_validator, iterable_validator)

e.match(r"\w* must be callable")
value = 42
message = "must be callable (got {value} that is a {type_}).".format(
value=value, type_=value.__class__
)
assert message in e.value.args[0]
assert value == e.value.args[1]
assert message in e.value.message
assert value == e.value.value

def test_fail_invalid_member(self):
"""
Expand Down Expand Up @@ -466,7 +472,14 @@ def test_noncallable_validators(
with pytest.raises(TypeError) as e:
deep_mapping(key_validator, value_validator, mapping_validator)

e.match(r"\w* must be callable")
value = 42
message = "must be callable (got {value} that is a {type_}).".format(
value=value, type_=value.__class__
)
assert message in e.value.args[0]
assert value == e.value.args[1]
assert message in e.value.message
assert value == e.value.value

def test_fail_invalid_mapping(self):
"""
Expand Down Expand Up @@ -550,7 +563,13 @@ def test_fail(self):
with pytest.raises(TypeError) as e:
v(None, a, None)

e.match("'test' must be callable")
value = None
message = "'test' must be callable (got {value} that is a {type_})."
expected_message = message.format(value=value, type_=value.__class__)
assert expected_message == e.value.args[0]
assert value == e.value.args[1]
assert expected_message == e.value.message
assert value == e.value.value

def test_repr(self):
"""
Expand Down

0 comments on commit 3bfb793

Please sign in to comment.