Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the error message informing the value that it got it #536

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@
``attr.validators.is_callable()`` validator now raises an exception ``attr.exceptions.NotCallableError``, a subclass of ``TypeError``, informing the received value.
`#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
hynek marked this conversation as resolved.
Show resolved Hide resolved
"""

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

def __str__(self):
return str(self.msg)
7 changes: 7 additions & 0 deletions src/attr/exceptions.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from typing import Any

class FrozenInstanceError(AttributeError):
msg: str = ...

class AttrsAttributeNotFoundError(ValueError): ...
class NotAnAttrsClassError(ValueError): ...
class DefaultAlreadySetError(RuntimeError): ...
class UnannotatedAttributeError(RuntimeError): ...

class NotCallableError(TypeError):
msg: str = ...
value: Any = ...
def __init__(self, msg: str, value: Any) -> None: ...
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(
msg=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
37 changes: 34 additions & 3 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,15 @@ def test_noncallable_validators(
"""
with pytest.raises(TypeError) as e:
deep_iterable(member_validator, iterable_validator)
value = 42
message = "must be callable (got {value} that is a {type_}).".format(
value=value, type_=value.__class__
)
williamjamir marked this conversation as resolved.
Show resolved Hide resolved

e.match(r"\w* must be callable")
assert message in e.value.args[0]
assert value == e.value.args[1]
assert message in e.value.msg
assert value == e.value.value

def test_fail_invalid_member(self):
"""
Expand Down Expand Up @@ -466,7 +473,15 @@ 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__
)
williamjamir marked this conversation as resolved.
Show resolved Hide resolved

assert message in e.value.args[0]
assert value == e.value.args[1]
assert message in e.value.msg
assert value == e.value.value

def test_fail_invalid_mapping(self):
"""
Expand Down Expand Up @@ -550,7 +565,14 @@ 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__)
williamjamir marked this conversation as resolved.
Show resolved Hide resolved

assert expected_message == e.value.args[0]
assert value == e.value.args[1]
assert expected_message == e.value.msg
assert value == e.value.value

def test_repr(self):
"""
Expand All @@ -559,6 +581,15 @@ def test_repr(self):
v = is_callable()
assert "<is_callable validator>" == repr(v)

def test_exception_repr(self):
"""
Verify that NotCallableError exception has a useful `__str__`.
"""
from attr.exceptions import NotCallableError

instance = NotCallableError(msg="Some Message", value=42)
assert "Some Message" == str(instance)


def test_hashability():
"""
Expand Down