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

support custom repr() callable for attributes #568

Merged
merged 15 commits into from
Sep 6, 2019
Merged
1 change: 1 addition & 0 deletions changelog.d/568.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The value passed to ``@attr.ib(repr=…)`` can now be either a boolean (as before) or a custom callable, which makes it possible to influence how the attribute value is formatted by the generated ``__repr__()_`` method.
wbolster marked this conversation as resolved.
Show resolved Hide resolved
15 changes: 13 additions & 2 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ It will get called at the end of the generated ``__init__`` method.
>>> obj
C(x=1, y=2, z=3)

Finally, you can exclude single attributes from certain methods:
You can exclude single attributes from certain methods:

.. doctest::

Expand All @@ -665,4 +665,15 @@ Finally, you can exclude single attributes from certain methods:
... user = attr.ib()
... password = attr.ib(repr=False)
>>> C("me", "s3kr3t")
C(user='me')
C(user='me', password=***)
wbolster marked this conversation as resolved.
Show resolved Hide resolved

Alternatively, to influence how the generated ``__repr__()`` method formats a specific attribute, specify custom callable to be used instead of the `repr()` built-in function:
wbolster marked this conversation as resolved.
Show resolved Hide resolved

.. doctest::

>>> @attr.s
... class C(object):
... user = attr.ib()
... password = attr.ib(repr=lambda value: '***')
>>> C("me", "s3kr3t")
C(user='me', password=***)
12 changes: 7 additions & 5 deletions src/attr/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ _C = TypeVar("_C", bound=type)
_ValidatorType = Callable[[Any, Attribute[_T], _T], Any]
_ConverterType = Callable[[Any], _T]
_FilterType = Callable[[Attribute[_T], _T], bool]
_ReprType = Callable[[Any], str]
_ReprArgType = Union[bool, _ReprType]
# FIXME: in reality, if multiple validators are passed they must be in a list or tuple,
# but those are invariant and so would prevent subtypes of _ValidatorType from working
# when passed in a list or tuple.
Expand All @@ -49,7 +51,7 @@ class Attribute(Generic[_T]):
name: str
default: Optional[_T]
validator: Optional[_ValidatorType[_T]]
repr: bool
repr: _ReprArgType
cmp: bool
hash: Optional[bool]
init: bool
Expand Down Expand Up @@ -89,7 +91,7 @@ class Attribute(Generic[_T]):
def attrib(
default: None = ...,
validator: None = ...,
repr: bool = ...,
repr: _ReprArgType = ...,
cmp: bool = ...,
hash: Optional[bool] = ...,
init: bool = ...,
Expand All @@ -105,7 +107,7 @@ def attrib(
def attrib(
default: None = ...,
validator: Optional[_ValidatorArgType[_T]] = ...,
repr: bool = ...,
repr: _ReprArgType = ...,
cmp: bool = ...,
hash: Optional[bool] = ...,
init: bool = ...,
Expand All @@ -121,7 +123,7 @@ def attrib(
def attrib(
default: _T,
validator: Optional[_ValidatorArgType[_T]] = ...,
repr: bool = ...,
repr: _ReprArgType = ...,
cmp: bool = ...,
hash: Optional[bool] = ...,
init: bool = ...,
Expand All @@ -137,7 +139,7 @@ def attrib(
def attrib(
default: Optional[_T] = ...,
validator: Optional[_ValidatorArgType[_T]] = ...,
repr: bool = ...,
repr: _ReprArgType = ...,
cmp: bool = ...,
hash: Optional[bool] = ...,
init: bool = ...,
Expand Down
22 changes: 16 additions & 6 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,12 @@ def attrib(

:type validator: ``callable`` or a ``list`` of ``callable``\\ s.

:param bool repr: Include this attribute in the generated ``__repr__``
method.
:param repr: Include this attribute in the generated ``__repr__``
wbolster marked this conversation as resolved.
Show resolved Hide resolved
method. If ``True``, include the attribute; if ``False``, omit it. By
default, the built-in ``repr()`` function is used. To override how the
attribute value is formatted in the, pass a custom ``callable``, which
should take a single value, and return a string.
wbolster marked this conversation as resolved.
Show resolved Hide resolved
:type repr: a ``bool`` or a ``callable`` to use a custom function.
wbolster marked this conversation as resolved.
Show resolved Hide resolved
:param bool cmp: Include this attribute in the generated comparison methods
(``__eq__`` et al).
:param hash: Include this attribute in the generated ``__hash__``
Expand Down Expand Up @@ -1210,9 +1214,9 @@ def _add_cmp(cls, attrs=None):

def _make_repr(attrs, ns):
"""
Make a repr method for *attr_names* adding *ns* to the full name.
Make a repr method that includes relevant *attrs*, adding *ns* to the full
wbolster marked this conversation as resolved.
Show resolved Hide resolved
name.
"""
attr_names = tuple(a.name for a in attrs if a.repr)

def __repr__(self):
"""
Expand Down Expand Up @@ -1244,12 +1248,18 @@ def __repr__(self):
try:
result = [class_name, "("]
first = True
for name in attr_names:
for attr in attrs:
if not attr.repr:
wbolster marked this conversation as resolved.
Show resolved Hide resolved
continue
if first:
first = False
else:
result.append(", ")
result.extend((name, "=", repr(getattr(self, name, NOTHING))))
name = attr.name
attr_repr = repr if attr.repr is True else attr.repr
result.extend(
(name, "=", attr_repr(getattr(self, name, NOTHING)))
)
return "".join(result) + ")"
finally:
working_set.remove(id(self))
Expand Down
15 changes: 15 additions & 0 deletions tests/test_dunders.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,21 @@ def test_repr_works(self, cls):
"""
assert "C(a=1, b=2)" == repr(cls(1, 2))

def test_custom_repr_works(self):
"""
repr returns a sensible value for attributes with a custom repr
callable.
"""

def custom_repr(value):
return "foo:" + str(value)

@attr.s
class C(object):
a = attr.ib(repr=custom_repr)

assert "C(a=foo:1)" == repr(C(1))

def test_infinite_recursion(self):
"""
In the presence of a cyclic graph, repr will emit an ellipsis and not
Expand Down