From 934a86d76ac4d489b69fce459ce8d4808b76d987 Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Thu, 11 Apr 2024 00:04:30 -0400 Subject: [PATCH 01/19] Add only_non_default_attr_in_repr option for dynamic repr --- src/attr/_make.py | 47 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index d3bfb440f..c4ea3d8bd 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1014,9 +1014,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 @@ -1332,6 +1337,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 @@ -1562,6 +1568,12 @@ def attrs( 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* @@ -1655,7 +1667,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() @@ -1961,7 +1976,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 @@ -2008,7 +2023,20 @@ 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 a. repr is False or value == a.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))", ] @@ -2018,14 +2046,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 From c8081ecee6c55a6c8a8705d7493813bbe134a6b0 Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Thu, 11 Apr 2024 00:05:09 -0400 Subject: [PATCH 02/19] Update define to accept only_non_default_attr_in_repr param --- src/attr/_next_gen.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index 07db702d5..b27524003 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -44,6 +44,7 @@ def define( on_setattr=None, field_transformer=None, match_args=True, + only_non_default_attr_in_repr=False, ): r""" Define an *attrs* class. @@ -110,6 +111,7 @@ def do_it(cls, auto_attribs): on_setattr=on_setattr, field_transformer=field_transformer, match_args=match_args, + only_non_default_attr_in_repr=only_non_default_attr_in_repr, ) def wrap(cls): From 83782d1a9164665df78884650bc48d61dacc2d71 Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Thu, 11 Apr 2024 00:39:13 -0400 Subject: [PATCH 03/19] Add versionadded to docstring --- src/attr/_make.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/attr/_make.py b/src/attr/_make.py index c4ea3d8bd..34ac223ed 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1611,6 +1611,10 @@ def attrs( .. 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 From 2305452c9a8b8356d71b9d8c420d38cb06c33753 Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Thu, 11 Apr 2024 00:51:12 -0400 Subject: [PATCH 04/19] Update attrs typing defs --- src/attr/__init__.pyi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 14e53a8d2..f76a8706a 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -249,6 +249,7 @@ def attrs( field_transformer: _FieldTransformer | None = ..., match_args: bool = ..., unsafe_hash: bool | None = ..., + only_non_default_attr_in_repr: bool = ..., ) -> _C: ... @overload @dataclass_transform(order_default=True, field_specifiers=(attrib, field)) @@ -277,6 +278,7 @@ def attrs( field_transformer: _FieldTransformer | None = ..., match_args: bool = ..., unsafe_hash: bool | None = ..., + only_non_default_attr_in_repr: bool = ..., ) -> Callable[[_C], _C]: ... def fields(cls: type[AttrsInstance]) -> Any: ... def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ... From a8f80ca5d6f2c4acef1f7b11f1a3eaf0440bb5c5 Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Thu, 11 Apr 2024 00:59:06 -0400 Subject: [PATCH 05/19] Update define typing defs for new param --- src/attrs/__init__.pyi | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/attrs/__init__.pyi b/src/attrs/__init__.pyi index 33624666c..82b7e6004 100644 --- a/src/attrs/__init__.pyi +++ b/src/attrs/__init__.pyi @@ -1,16 +1,12 @@ import sys - -from typing import ( - Any, - Callable, - Mapping, - Sequence, - overload, - TypeVar, -) +from typing import Any, Callable, Mapping, Sequence, TypeVar, overload # Because we need to type our own stuff, we have to make everything from # attr explicitly public too. +from attr import NOTHING as NOTHING +from attr import Attribute as Attribute +from attr import AttrsInstance as AttrsInstance +from attr import Factory as Factory from attr import __author__ as __author__ from attr import __copyright__ as __copyright__ from attr import __description__ as __description__ @@ -20,25 +16,23 @@ from attr import __title__ as __title__ from attr import __url__ as __url__ from attr import __version__ as __version__ from attr import __version_info__ as __version_info__ +from attr import asdict as asdict from attr import assoc as assoc -from attr import Attribute as Attribute -from attr import AttrsInstance as AttrsInstance +from attr import astuple as astuple +from attr import attrib from attr import cmp_using as cmp_using from attr import converters as converters from attr import evolve as evolve from attr import exceptions as exceptions -from attr import Factory as Factory from attr import fields as fields from attr import fields_dict as fields_dict from attr import filters as filters from attr import has as has from attr import make_class as make_class -from attr import NOTHING as NOTHING from attr import resolve_types as resolve_types from attr import setters as setters from attr import validate as validate from attr import validators as validators -from attr import attrib, asdict as asdict, astuple as astuple if sys.version_info >= (3, 11): from typing import dataclass_transform @@ -167,6 +161,7 @@ def define( on_setattr: _OnSetAttrArgType | None = ..., field_transformer: _FieldTransformer | None = ..., match_args: bool = ..., + only_non_default_attr_in_repr: bool = ..., ) -> _C: ... @overload @dataclass_transform(field_specifiers=(attrib, field)) @@ -193,6 +188,7 @@ def define( on_setattr: _OnSetAttrArgType | None = ..., field_transformer: _FieldTransformer | None = ..., match_args: bool = ..., + only_non_default_attr_in_repr: bool = ..., ) -> Callable[[_C], _C]: ... mutable = define From ddf1257766bfe5e3916a589f043c2e36d744a90e Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Sat, 8 Jun 2024 12:23:20 -0400 Subject: [PATCH 06/19] Add tests of only_non_default_attr_in_repr --- tests/test_dunders.py | 67 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/tests/test_dunders.py b/tests/test_dunders.py index 85c9309b3..c0a766b55 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -4,7 +4,6 @@ Tests for dunder methods from `attrib._make`. """ - import copy import inspect import pickle @@ -440,6 +439,65 @@ def test_str_no_repr(self): "__str__ can only be generated if a __repr__ exists." ) == e.value.args[0] + def test_only_non_default_attr_in_repr(self): + """ + Validate repr behavior when using only_non_default_attr_in_repr parameter. + """ + + # Use the only_non_default_attr_in_repr option means params whose values + # equal their default aren't included in their repr + @attr.s(only_non_default_attr_in_repr=True) + class SomeClass: + positional: int = attr.ib() + something = attr.ib(default=None, repr=True) + something_else = attr.ib(default=False, repr=True) + another = attr.ib(default=11.0, init=False, repr=False) + + some_class = SomeClass(8, something=7) + r = "SomeClass(positional=8, something=7)" + assert r == repr(some_class) + + # If we wanted to exclude the something param from the repr in the field + # definition or override it with a callable for repr we still can + # but this is only used when the parameter has a non-default value + @attr.s(only_non_default_attr_in_repr=True) + class SomeClass: + something = attr.ib(default=None, repr=False) + something_else = attr.ib(default=False, repr=True) + another = attr.ib(default=11.0, init=False, repr=False) + another_one = attr.ib(default=17.0, repr=lambda x: "P") + + some_class = SomeClass(something=7, another_one=17.0) + r = "SomeClass()" + assert r == repr(some_class) + + # The default is equivalent of only_non_default_attr_in_repr=False, + # so existing behavior is default + @attr.s() + class SomeClass: + positional = attr.ib() + something = attr.ib(default=None) + something_else = attr.ib(default=False) + another = attr.ib(default=11.0, init=False, repr=False) + + some_class = SomeClass("P", something=7) + r = "SomeClass(positional='P', something=7, something_else=False)" + assert r == repr(some_class) + + # The use of repr in a field works just like it did before so users can still + # exclude individual attributes from the repr (or pass custom callable) + @attr.s() + class SomeClass: + positional = attr.ib(repr=lambda x: "P") + something = attr.ib(default=None, repr=False) + something_else = attr.ib(default=False) + another = attr.ib(default=11.0, init=False) + another1 = attr.ib(default="Something", repr=lambda x: 7) + + some_class = SomeClass(8, something=7) + r = "SomeClass(positional=P, something_else=False, another=11.0, another1=7)" + assert r == repr(some_class) + # these are for use in TestAddHash.test_cache_hash_serialization # they need to be out here so they can be un-pickled @@ -481,12 +539,12 @@ def test_enforces_type(self): exc_args = ("Invalid value for hash. Must be True, False, or None.",) with pytest.raises(TypeError) as e: - make_class("C", {}, hash=1), + (make_class("C", {}, hash=1),) assert exc_args == e.value.args with pytest.raises(TypeError) as e: - make_class("C", {"a": attr.ib(hash=1)}), + (make_class("C", {"a": attr.ib(hash=1)}),) assert exc_args == e.value.args @@ -517,8 +575,7 @@ def test_enforce_no_cached_hash_without_init(self): but attrs is not requested to generate `__init__`. """ exc_args = ( - "Invalid value for cache_hash. To use hash caching," - " init must be True.", + "Invalid value for cache_hash. To use hash caching, init must be True.", ) with pytest.raises(TypeError) as e: make_class("C", {}, init=False, hash=True, cache_hash=True) From 06152e909ff06df9f740c8407abdb1e9af58603e Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Sat, 8 Jun 2024 12:59:19 -0400 Subject: [PATCH 07/19] Test interaction with other repr attributes --- tests/test_dunders.py | 53 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/test_dunders.py b/tests/test_dunders.py index c0a766b55..d7f21eec8 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -408,9 +408,30 @@ def test_repr_uninitialized_member(self): repr signals unset attributes """ C = make_class("C", {"a": attr.ib(init=False)}) - assert "C(a=NOTHING)" == repr(C()) + @given(only_non_default_attr_in_repr=booleans()) + def test_repr_uninitialized_member_with_only_non_default_attr_in_repr( + self, only_non_default_attr_in_repr + ): + """ + repr signals unset attributes when using only_non_default_attr_in_repr. + """ + C = make_class( + "C", + {"a": attr.ib(init=False), "b": attr.ib(default=10)}, + only_non_default_attr_in_repr=only_non_default_attr_in_repr, + ) + # When using only_non_default_attr_in_repr we don't get signal that + # the value for param a was not initialized because it is set to implicit + # default value "NOTHING" + if only_non_default_attr_in_repr: + assert "C()" == repr(C()) + assert "C(b=7)" == repr(C(b=7)) + else: + assert "C(a=NOTHING, b=10)" == repr(C()) + assert "C(a=NOTHING, b=7)" == repr(C(b=7)) + @given(add_str=booleans(), slots=booleans()) def test_str(self, add_str, slots): """ @@ -428,6 +449,36 @@ class Error(Exception): assert (str(e) == repr(e)) is add_str + @given( + add_str=booleans(), + slots=booleans(), + only_non_default_attr_in_repr=booleans(), + ) + def test_str_with_only_non_default_attr_in_repr( + self, add_str, slots, only_non_default_attr_in_repr + ): + """ + If str is True, it returns the same as repr. + + This verifies this continues to work with use of only_non_default_attr_in_repr. + + This only makes sense when subclassing a class with an poor __str__ + (like Exceptions). + """ + + @attr.s( + str=add_str, + slots=slots, + only_non_default_attr_in_repr=only_non_default_attr_in_repr, + ) + class Error(Exception): + x = attr.ib() + y = attr.ib(default=False) + + e = Error(42) + + assert (str(e) == repr(e)) is add_str + def test_str_no_repr(self): """ Raises a ValueError if repr=False and str=True. From addc6f95c7de377bde5e641a264ac7ca7da08f45 Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Sat, 8 Jun 2024 14:15:50 -0400 Subject: [PATCH 08/19] Handle default factories and field default decorator --- src/attr/_make.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 34ac223ed..71d8b8ac0 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -2032,7 +2032,16 @@ def _make_repr(attrs, ns, cls, only_non_default_attr_in_repr=False): " attr_frags = []", " for a in getattr(self, '__attrs_attrs__', []):", " value = getattr(self, a.name, NOTHING)", - " if a. repr is False or value == a.default:", + " if hasattr(a.default, 'factory') and callable(a.default.factory):" + " has_factory = True", + " else:", + " has_factory = False", + " maybe_self = self if has_factory and a.default.takes_self else ''", + " if (", + " a.repr is False", + " or (has_factory and value == a.default.factory(maybe_self))", + " or value == a.default", + " ):", " frag = ''", " else:", " _repr = repr if a.repr is True else a.repr", From 82e7f4e789ba8fbcedabd86255587feb00f747d8 Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Sat, 8 Jun 2024 14:17:16 -0400 Subject: [PATCH 09/19] Add test for default factories --- tests/test_dunders.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_dunders.py b/tests/test_dunders.py index d7f21eec8..3115569c7 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -549,6 +549,24 @@ class SomeClass: r = "SomeClass(positional=P, something_else=False, another=11.0, another1=7)" assert r == repr(some_class) + def test_only_non_default_attr_in_repr_default_decorator(self): + """ + Validate repr behavior with default decorator and only_non_default_attr_in_repr. + """ + + @attr.s(only_non_default_attr_in_repr=True) + class SomeClass: + x: int = attr.ib(default=1) + y: int = attr.ib() + + @y.default + def _y(self): + """Default value for y.""" + return self.x + 1 + + some_class = SomeClass(x=1) + assert "SomeClass()" == repr(some_class) + # these are for use in TestAddHash.test_cache_hash_serialization # they need to be out here so they can be un-pickled From 34cfcdfb711b30f65526b7268f47e3b78804706d Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Sat, 8 Jun 2024 14:17:51 -0400 Subject: [PATCH 10/19] Add to examples.md --- docs/examples.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/docs/examples.md b/docs/examples.md index e43764205..5746022b0 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -381,6 +381,62 @@ 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. + +```{docttest} +>>> @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. + +```{docttest} +>>> @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. + +```{docttest} +>>> @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) +``` + (examples-validators)= ## Validators From 5af400b3d78998b3b0c7cc7df62b702539d2dd17 Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Sat, 8 Jun 2024 14:59:23 -0400 Subject: [PATCH 11/19] Add test for interaction with converters and default --- tests/test_dunders.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_dunders.py b/tests/test_dunders.py index 3115569c7..2964513ae 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -564,8 +564,34 @@ def _y(self): """Default value for y.""" return self.x + 1 - some_class = SomeClass(x=1) + some_class = SomeClass() assert "SomeClass()" == repr(some_class) + some_class = SomeClass(y=3) + assert "SomeClass(y=3)" == repr(some_class) + + def test_only_non_default_attr_in_repr_with_converter(self): + """ + Validate repr behavior with converter and only_non_default_attr_in_repr. + """ + + @attr.s(only_non_default_attr_in_repr=True) + class SomeClass: + x: int = attr.ib(default=1, converter=lambda value: value + 0.5) + y: int = attr.ib() + z: int = attr.ib(default=12, converter=int) + + @y.default + def _y(self): + """Default value for y.""" + return self.x + 1 + + some_class = SomeClass(x=0.5, z="12") + # B/c converter applies before setting x, x will equal default value + # Likewise, z is converted to integer 12 before setting, so it equals default + assert "SomeClass()" == repr(some_class) + + some_class = SomeClass(x=1) + assert "SomeClass(x=1.5)" == repr(some_class) # these are for use in TestAddHash.test_cache_hash_serialization From 0601224122d6b589a015596aa923a624a9696e9c Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Sat, 8 Jun 2024 15:00:15 -0400 Subject: [PATCH 12/19] Update handling of defaults and default factories --- src/attr/_make.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 71d8b8ac0..222ccc101 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -2032,16 +2032,14 @@ def _make_repr(attrs, ns, cls, only_non_default_attr_in_repr=False): " 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):" - " has_factory = True", + " 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:", - " has_factory = False", - " maybe_self = self if has_factory and a.default.takes_self else ''", - " if (", - " a.repr is False", - " or (has_factory and value == a.default.factory(maybe_self))", - " or value == a.default", - " ):", + " default_ = a.default", + " if (a.repr is False or value == default_):", " frag = ''", " else:", " _repr = repr if a.repr is True else a.repr", From c8d7ebc4606619176b63c093e849de9500ab571d Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Sat, 8 Jun 2024 15:07:07 -0400 Subject: [PATCH 13/19] Update example to cover interaction with converters --- docs/examples.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index 5746022b0..9b9d00148 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -386,7 +386,7 @@ in your attrs repr output, you can use the `only_non_default_attr_in_repr` argum When the argument isn't specified the repr works as expected. -```{docttest} +```{doctest} >>> @define ... class C: ... x: int = 1 @@ -398,7 +398,7 @@ 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. -```{docttest} +```{doctest} >>> @define(only_non_default_attr_in_repr=True) ... class C: ... x: int = 1 @@ -421,7 +421,7 @@ But the field is still excluded from the repr when it is set to the default valu 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. -```{docttest} +```{doctest} >>> @define(only_non_default_attr_in_repr=True) ... class C: ... x: int = field(default=1, repr=False) @@ -437,6 +437,24 @@ C(y='foo: 3') 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} +>>> @attr.s(only_non_default_attr_in_repr=True) +>>> class SomeClass: +... x: int = attr.ib(default=1, converter=lambda value: value + 0.5) +... z: int = attr.ib(default=12, converter=int) +>>> SomeClass(x=0.5, z="12") +SomeClass() +>>> SomeClass(x=1) +SomeClass(1.5) +``` + (examples-validators)= ## Validators From 1217c7f2724fca98b831d35d46dabf02666f0860 Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Sun, 9 Jun 2024 09:41:52 -0400 Subject: [PATCH 14/19] Fix incorrect str quotes in repr example --- docs/examples.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index 9b9d00148..ac49f47a3 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -430,9 +430,9 @@ custom callable when the field is set to its default value. >>> C() C() >>> C(y=3) -C(y='foo: 3') +C(y=foo: 3) >>> C(x=2, y=3) -C(y='foo: 3') +C(y=foo: 3) >>> C(z=4) C(z=4) ``` From 1a686bc4f62915a335392c1d0f2f9c56f13c7042 Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Sun, 9 Jun 2024 10:57:18 -0400 Subject: [PATCH 15/19] Fix examples to use define decorator not attr.s --- docs/examples.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples.md b/docs/examples.md index ac49f47a3..fc309d66d 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -445,7 +445,7 @@ converted value will be checked against the default, meaning this functionality expects the defaults in the converted format. ```{doctest} ->>> @attr.s(only_non_default_attr_in_repr=True) +>>> @define(only_non_default_attr_in_repr=True) >>> class SomeClass: ... x: int = attr.ib(default=1, converter=lambda value: value + 0.5) ... z: int = attr.ib(default=12, converter=int) From 50ed7bbd6d351aa1a271332c6a9e94337dd07930 Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Sun, 9 Jun 2024 11:01:57 -0400 Subject: [PATCH 16/19] Fix incorrect formatting in examples.md code example --- docs/examples.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index fc309d66d..3b083a035 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -446,9 +446,9 @@ expects the defaults in the converted format. ```{doctest} >>> @define(only_non_default_attr_in_repr=True) ->>> class SomeClass: -... x: int = attr.ib(default=1, converter=lambda value: value + 0.5) -... z: int = attr.ib(default=12, converter=int) +>>> class C: +... x: int = field(default=1, converter=lambda value: value + 0.5) +... z: int = field(default=12, converter=int) >>> SomeClass(x=0.5, z="12") SomeClass() >>> SomeClass(x=1) From 98a75237ff452248963ebb9a976ced7ad69e59e0 Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Sun, 9 Jun 2024 11:22:35 -0400 Subject: [PATCH 17/19] Another examples fix --- docs/examples.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples.md b/docs/examples.md index 3b083a035..29b362a4b 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -446,7 +446,7 @@ expects the defaults in the converted format. ```{doctest} >>> @define(only_non_default_attr_in_repr=True) ->>> class C: +... class C: ... x: int = field(default=1, converter=lambda value: value + 0.5) ... z: int = field(default=12, converter=int) >>> SomeClass(x=0.5, z="12") From 17cdc4447bad163e0ce2060f4b86b0f36c38658d Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Sun, 9 Jun 2024 11:24:59 -0400 Subject: [PATCH 18/19] Update name of class in example --- docs/examples.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index 29b362a4b..55efa737c 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -449,10 +449,10 @@ expects the defaults in the converted format. ... class C: ... x: int = field(default=1, converter=lambda value: value + 0.5) ... z: int = field(default=12, converter=int) ->>> SomeClass(x=0.5, z="12") -SomeClass() ->>> SomeClass(x=1) -SomeClass(1.5) +>>> C(x=0.5, z="12") +C() +>>> C(x=1) +C(1.5) ``` (examples-validators)= From bcbbe72938e1b1e520b6a013bcd23369c0d33f09 Mon Sep 17 00:00:00 2001 From: rnkuhns Date: Sun, 9 Jun 2024 11:27:28 -0400 Subject: [PATCH 19/19] One last example update --- docs/examples.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples.md b/docs/examples.md index 55efa737c..8cbbbd313 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -452,7 +452,7 @@ expects the defaults in the converted format. >>> C(x=0.5, z="12") C() >>> C(x=1) -C(1.5) +C(x=1.5) ``` (examples-validators)=