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
2 changes: 2 additions & 0 deletions changelog.d/568.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The value passed to ``@attr.ib(repr=…)`` can now be either a boolean (as before) or a callable.
That callable must return a string and is then used for formatting the attribute by the generated ``__repr__()`` method.
13 changes: 12 additions & 1 deletion 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 @@ -666,3 +666,14 @@ Finally, you can exclude single attributes from certain methods:
... password = attr.ib(repr=False)
>>> C("me", "s3kr3t")
C(user='me')

Alternatively, to influence how the generated ``__repr__()`` method formats a specific attribute, specify a custom callable to be used instead of the ``repr()`` built-in function:

.. 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
29 changes: 23 additions & 6 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,14 @@ 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, pass a ``callable`` that takes a single
value and returns a string. Note that the resulting string is used
as-is, i.e. it will be used directly *instead* of calling ``repr()``
(the default).
: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 @@ -175,6 +181,7 @@ def attrib(
``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``.
.. versionadded:: 18.2.0 *kw_only*
.. versionchanged:: 19.2.0 *convert* keyword argument removed
.. versionchanged:: 19.2.0 *repr* also accepts a custom callable.
"""
if hash is not None and hash is not True and hash is not False:
raise TypeError(
Expand Down Expand Up @@ -1210,9 +1217,17 @@ 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)

# Figure out which attributes to include, and which function to use to
# format them. The a.repr value can be either bool or a custom callable.
attr_names_with_reprs = tuple(
(a.name, repr if a.repr is True else a.repr)
for a in attrs
if a.repr is not False
)

def __repr__(self):
"""
Expand Down Expand Up @@ -1244,12 +1259,14 @@ def __repr__(self):
try:
result = [class_name, "("]
first = True
for name in attr_names:
for name, attr_repr in attr_names_with_reprs:
if first:
first = False
else:
result.append(", ")
result.extend((name, "=", repr(getattr(self, name, NOTHING))))
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
9 changes: 9 additions & 0 deletions tests/typing_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,12 @@ class Validated:
attr.validators.instance_of(C), attr.validators.instance_of(D)
),
)


# Custom repr()
@attr.s
class WithCustomRepr:
a = attr.ib(repr=True)
b = attr.ib(repr=False)
c = attr.ib(repr=lambda value: "c is for cookie")
d = attr.ib(repr=str)