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

Provide option in attrs.define to allow users to exclude parameters set to default value from repr #1276

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/attr/__init__.pyi
Expand Up @@ -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))
Expand Down Expand Up @@ -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]]: ...
Expand Down
51 changes: 44 additions & 7 deletions src/attr/_make.py
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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*
Expand Down Expand Up @@ -1599,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
Expand Down Expand Up @@ -1655,7 +1671,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()

Expand Down Expand Up @@ -1961,7 +1980,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
Expand Down Expand Up @@ -2008,7 +2027,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))",
]
Expand All @@ -2018,14 +2050,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


Expand Down
2 changes: 2 additions & 0 deletions src/attr/_next_gen.py
Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down
24 changes: 10 additions & 14 deletions 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__
Expand All @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down