-
-
Notifications
You must be signed in to change notification settings - Fork 374
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
Provide option in attrs.define to allow users to exclude parameters set to default value from repr
#1276
base: main
Are you sure you want to change the base?
Provide option in attrs.define to allow users to exclude parameters set to default value from repr
#1276
Changes from all commits
934a86d
c8081ec
83782d1
2305452
a8f80ca
ddf1257
06152e9
addc6f9
82e7f4e
34cfcdf
5af400b
0601224
c8d7ebc
1217c7f
2042b0a
1a686bc
48b9f5f
50ed7bb
98a7523
17cdc44
bcbbe72
149d7a3
c8fdfd0
5df2c90
01dd4f5
9f1963b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -381,6 +381,80 @@ C(x=1, y=2, z=[]) | |
Please keep in mind that the decorator approach *only* works if the attribute in question has a {func}`~attrs.field` assigned to it. | ||
As a result, annotating an attribute with a type is *not* enough if you use `@default`. | ||
|
||
In the event you only want to include fields that are set to a non-default value | ||
in your attrs repr output, you can use the `only_non_default_attr_in_repr` argument to {func}`~attrs.define`. | ||
|
||
When the argument isn't specified the repr works as expected. | ||
|
||
```{doctest} | ||
>>> @define | ||
... class C: | ||
... x: int = 1 | ||
... y: int = field(default=2) | ||
>>> C() | ||
C(x=1, y=2) | ||
``` | ||
|
||
Instead, if `only_non_default_attr_in_repr=True` the parameters set to their | ||
defaults won't be included in the repr output. | ||
|
||
```{doctest} | ||
>>> @define(only_non_default_attr_in_repr=True) | ||
... class C: | ||
... x: int = 1 | ||
... y: int = field(default=2) | ||
>>> C() | ||
C() | ||
>>> C(x=2) | ||
C(x=2) | ||
>>> C(y=3) | ||
C(y=3) | ||
>>> C(x=2, y=3) | ||
C(x=2, y=3) | ||
``` | ||
|
||
Other attrs repr features, including turning the repr off for a field or | ||
providing a custom callable for the repr of a field, will work as usual, when the | ||
field's value is not the default. | ||
|
||
But the field is still excluded from the repr when it is set to the default value | ||
because `only_non_default_attr_in_repr` overrides `repr=True` or the repr being a | ||
custom callable when the field is set to its default value. | ||
|
||
```{doctest} | ||
>>> @define(only_non_default_attr_in_repr=True) | ||
... class C: | ||
... x: int = field(default=1, repr=False) | ||
... y: int = field(default=2, repr=lambda value: "foo: " + str(value)) | ||
... z: int = field(default=3, repr=True) | ||
>>> C() | ||
C() | ||
>>> C(y=3) | ||
C(y=foo: 3) | ||
>>> C(x=2, y=3) | ||
C(y=foo: 3) | ||
>>> C(z=4) | ||
C(z=4) | ||
``` | ||
|
||
The usual attrs order of execution applies. For each variable (in the order they | ||
are specified), the default factory (or default value) is considered. Then it is | ||
compared to set value of the field. When converters are applied, this occurs | ||
after the default factory. So when `only_non_default_attr_in_repr=True` the | ||
converted value will be checked against the default, meaning this functionality | ||
expects the defaults in the converted format. | ||
|
||
```{doctest} | ||
>>> @define(only_non_default_attr_in_repr=True) | ||
... class C: | ||
... x: int = field(default=1, converter=lambda value: value + 0.5) | ||
... z: int = field(default=12, converter=int) | ||
>>> C(x=0.5, z="12") | ||
C() | ||
>>> C(x=1) | ||
C(x=1.5) | ||
``` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't think this is correct (see the other comment). The real default for field >>> import attrs
>>>
>>> @attrs.define()
... class Test:
... x = attrs.field(default=1, converter=lambda v: v + 0.5)
...
>>> test = Test()
>>> test.x
1.5 (note: expand the file to see context) |
||
|
||
(examples-validators)= | ||
|
||
## Validators | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1036,9 +1036,14 @@ def _create_slots_class(self): | |
cell.cell_contents = cls | ||
return cls | ||
|
||
def add_repr(self, ns): | ||
def add_repr(self, ns, only_non_default_attr_in_repr=False): | ||
self._cls_dict["__repr__"] = self._add_method_dunders( | ||
_make_repr(self._attrs, ns, self._cls) | ||
_make_repr( | ||
self._attrs, | ||
ns, | ||
self._cls, | ||
only_non_default_attr_in_repr=only_non_default_attr_in_repr, | ||
) | ||
) | ||
return self | ||
|
||
|
@@ -1354,6 +1359,7 @@ def attrs( | |
field_transformer=None, | ||
match_args=True, | ||
unsafe_hash=None, | ||
only_non_default_attr_in_repr=False, | ||
): | ||
r""" | ||
A class decorator that adds :term:`dunder methods` according to the | ||
|
@@ -1623,6 +1629,12 @@ class is ``object``, this means it will fall back to id-based | |
non-keyword-only ``__init__`` parameter names on Python 3.10 and | ||
later. Ignored on older Python versions. | ||
|
||
:param bool only_non_default_attr_in_repr: | ||
If `False` (default), then the usual ``attrs`` repr is created. If `True` | ||
then only parameters set to their non-default values will be printed. | ||
This means when this is set to `True` the repr output is dynamic based | ||
on the state of the class. | ||
|
||
.. versionadded:: 16.0.0 *slots* | ||
.. versionadded:: 16.1.0 *frozen* | ||
.. versionadded:: 16.3.0 *str* | ||
|
@@ -1660,6 +1672,10 @@ class is ``object``, this means it will fall back to id-based | |
.. versionadded:: 22.2.0 | ||
*unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). | ||
.. deprecated:: 24.1.0 *repr_ns* | ||
.. versionadded:: 24.1 | ||
*only_non_default_attr_in_repr* added to allow users to choose to have their | ||
classes dynamically include only those parameters whose values are set to | ||
non-default values in the repr. | ||
""" | ||
if repr_ns is not None: | ||
import warnings | ||
|
@@ -1716,7 +1732,10 @@ def wrap(cls): | |
if _determine_whether_to_implement( | ||
cls, repr, auto_detect, ("__repr__",) | ||
): | ||
builder.add_repr(repr_ns) | ||
builder.add_repr( | ||
repr_ns, | ||
only_non_default_attr_in_repr=only_non_default_attr_in_repr, | ||
) | ||
if str is True: | ||
builder.add_str() | ||
|
||
|
@@ -2022,7 +2041,7 @@ def _add_eq(cls, attrs=None): | |
return cls | ||
|
||
|
||
def _make_repr(attrs, ns, cls): | ||
def _make_repr(attrs, ns, cls, only_non_default_attr_in_repr=False): | ||
unique_filename = _generate_unique_filename(cls, "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 | ||
|
@@ -2069,7 +2088,27 @@ def _make_repr(attrs, ns, cls): | |
" else:", | ||
" already_repring.add(id(self))", | ||
" try:", | ||
f" return f'{cls_name_fragment}({repr_fragment})'", | ||
f" if not {only_non_default_attr_in_repr}:", | ||
f" return f'{cls_name_fragment}({repr_fragment})'", | ||
" attr_frags = []", | ||
" for a in getattr(self, '__attrs_attrs__', []):", | ||
" value = getattr(self, a.name, NOTHING)", | ||
" if hasattr(a.default, 'factory') and callable(a.default.factory):", | ||
" if a.default.takes_self:", | ||
" default_ = a.default.factory(self)", | ||
" else:", | ||
" default_ = a.default.factory()", | ||
" else:", | ||
" default_ = a.default", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
FWIW, I had written a caller-side wrapper to hide defaults. (The way it works around the factory problem is by letting you mark a factory as dynamic if you don't want it called in repr.) Disclaimer: I'm not an attrs maintainer, so my views may not matter here. |
||
" if (a.repr is False or value == default_):", | ||
" frag = ''", | ||
" else:", | ||
" _repr = repr if a.repr is True else a.repr", | ||
" frag = f'{a.name}={_repr(value)}'", | ||
" attr_frags.append(frag)", | ||
" repr_fragment = ', '.join(f for f in attr_frags if f != '')", | ||
f" dynamic_repr = f'{cls_name_fragment}(' + repr_fragment + ')'", | ||
" return dynamic_repr", | ||
" finally:", | ||
" already_repring.remove(id(self))", | ||
] | ||
|
@@ -2079,14 +2118,19 @@ def _make_repr(attrs, ns, cls): | |
) | ||
|
||
|
||
def _add_repr(cls, ns=None, attrs=None): | ||
def _add_repr(cls, ns=None, attrs=None, only_non_default_attr_in_repr=False): | ||
""" | ||
Add a repr method to *cls*. | ||
""" | ||
if attrs is None: | ||
attrs = cls.__attrs_attrs__ | ||
|
||
cls.__repr__ = _make_repr(attrs, ns, cls) | ||
cls.__repr__ = _make_repr( | ||
attrs, | ||
ns, | ||
cls, | ||
only_non_default_attr_in_repr=only_non_default_attr_in_repr, | ||
) | ||
return cls | ||
|
||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't it be nice for an explicit
repr=True
to override the defaults hiding?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, no, because then this wouldn't be available if a custom repr is given. so perhaps add a new argument
always_repr
or something?