diff --git a/changelog.d/568.change.rst b/changelog.d/568.change.rst new file mode 100644 index 000000000..320491019 --- /dev/null +++ b/changelog.d/568.change.rst @@ -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. diff --git a/docs/examples.rst b/docs/examples.rst index fad5c2637..5ee63a1bd 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -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:: @@ -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=***) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 5ffa6dfe5..e331ea62e 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -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. @@ -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 @@ -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 = ..., @@ -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 = ..., @@ -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 = ..., @@ -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 = ..., diff --git a/src/attr/_make.py b/src/attr/_make.py index bca6f9a90..687b0f447 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -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__`` + 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. :param bool cmp: Include this attribute in the generated comparison methods (``__eq__`` et al). :param hash: Include this attribute in the generated ``__hash__`` @@ -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( @@ -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 + 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): """ @@ -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)) diff --git a/tests/test_dunders.py b/tests/test_dunders.py index 69f91a84e..84e092b4f 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -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 diff --git a/tests/typing_example.py b/tests/typing_example.py index 18e837e52..fb3cddf11 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -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)