Initial implementation of a faster repr (#819)

* Initial implementation of a faster repr

* Switch to positional args for _make_repr

* Fix tests and changelog

* Remove trailing comma for Py2

* Fix lint

* __qualname__ is always present if f-strings work

* Fix Py2 qualname

* Revert "Fix Py2 qualname"

This reverts commit eb091a31d2.

* Update src/attr/_make.py

Co-authored-by: Hynek Schlawack <hs@ox.cx>

* Update changelog.d/819.changes.rst

Co-authored-by: Hynek Schlawack <hs@ox.cx>

* Update src/attr/_make.py

Co-authored-by: Hynek Schlawack <hs@ox.cx>

* Update src/attr/_make.py

Co-authored-by: Hynek Schlawack <hs@ox.cx>

* Update src/attr/_make.py

Co-authored-by: Hynek Schlawack <hs@ox.cx>

* Update src/attr/_make.py

Co-authored-by: Hynek Schlawack <hs@ox.cx>

* Update src/attr/_make.py

Co-authored-by: Hynek Schlawack <hs@ox.cx>

* Fix syntax

Co-authored-by: Hynek Schlawack <hs@ox.cx>
This commit is contained in:
Tin Tvrtković 2021-05-26 21:57:52 +02:00 committed by GitHub
parent 0c769095ff
commit 9709dd82e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 132 additions and 53 deletions

View File

@ -0,0 +1 @@
The generated ``__repr__`` is significantly faster on Pythons with F-strings.

View File

@ -8,6 +8,11 @@ import warnings
PY2 = sys.version_info[0] == 2 PY2 = sys.version_info[0] == 2
PYPY = platform.python_implementation() == "PyPy" PYPY = platform.python_implementation() == "PyPy"
HAS_F_STRINGS = (
sys.version_info[:2] >= (3, 7)
if not PYPY
else sys.version_info[:2] >= (3, 6)
)
PY310 = sys.version_info[:2] >= (3, 10) PY310 = sys.version_info[:2] >= (3, 10)

View File

@ -12,6 +12,7 @@ from operator import itemgetter
from . import _config, setters from . import _config, setters
from ._compat import ( from ._compat import (
HAS_F_STRINGS,
PY2, PY2,
PY310, PY310,
PYPY, PYPY,
@ -888,7 +889,7 @@ class _ClassBuilder(object):
def add_repr(self, ns): def add_repr(self, ns):
self._cls_dict["__repr__"] = self._add_method_dunders( self._cls_dict["__repr__"] = self._add_method_dunders(
_make_repr(self._attrs, ns=ns) _make_repr(self._attrs, ns, self._cls)
) )
return self return self
@ -1873,64 +1874,136 @@ def _add_eq(cls, attrs=None):
_already_repring = threading.local() _already_repring = threading.local()
if HAS_F_STRINGS:
def _make_repr(attrs, ns): def _make_repr(attrs, ns, cls):
""" unique_filename = "repr"
Make a repr method that includes relevant *attrs*, adding *ns* to the full # Figure out which attributes to include, and which function to use to
name. # 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), a.init)
for a in attrs
if a.repr is not False
)
globs = {
name + "_repr": r
for name, r, _ in attr_names_with_reprs
if r != repr
}
globs["_already_repring"] = _already_repring
globs["AttributeError"] = AttributeError
globs["NOTHING"] = NOTHING
attribute_fragments = []
for name, r, i in attr_names_with_reprs:
accessor = (
"self." + name
if i
else 'getattr(self, "' + name + '", NOTHING)'
)
fragment = (
"%s={%s!r}" % (name, accessor)
if r == repr
else "%s={%s_repr(%s)}" % (name, name, accessor)
)
attribute_fragments.append(fragment)
repr_fragment = ", ".join(attribute_fragments)
# 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):
"""
Automatically created by attrs.
"""
try:
working_set = _already_repring.working_set
except AttributeError:
working_set = set()
_already_repring.working_set = working_set
if id(self) in working_set:
return "..."
real_cls = self.__class__
if ns is None: if ns is None:
qualname = getattr(real_cls, "__qualname__", None) cls_name_fragment = (
if qualname is not None: '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}'
class_name = qualname.rsplit(">.", 1)[-1] )
else:
class_name = real_cls.__name__
else: else:
class_name = ns + "." + real_cls.__name__ cls_name_fragment = ns + ".{self.__class__.__name__}"
# Since 'self' remains on the stack (i.e.: strongly referenced) for the lines = []
# duration of this call, it's safe to depend on id(...) stability, and lines.append("def __repr__(self):")
# not need to track the instance and therefore worry about properties lines.append(" try:")
# like weakref- or hash-ability. lines.append(" working_set = _already_repring.working_set")
working_set.add(id(self)) lines.append(" except AttributeError:")
try: lines.append(" working_set = {id(self),}")
result = [class_name, "("] lines.append(" _already_repring.working_set = working_set")
first = True lines.append(" else:")
for name, attr_repr in attr_names_with_reprs: lines.append(" if id(self) in working_set:")
if first: lines.append(" return '...'")
first = False lines.append(" else:")
lines.append(" working_set.add(id(self))")
lines.append(" try:")
lines.append(
" return f'%s(%s)'" % (cls_name_fragment, repr_fragment)
)
lines.append(" finally:")
lines.append(" working_set.remove(id(self))")
return _make_method(
"__repr__", "\n".join(lines), unique_filename, globs=globs
)
else:
def _make_repr(attrs, ns, _):
"""
Make a repr method that includes relevant *attrs*, adding *ns* to the
full name.
"""
# 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):
"""
Automatically created by attrs.
"""
try:
working_set = _already_repring.working_set
except AttributeError:
working_set = set()
_already_repring.working_set = working_set
if id(self) in working_set:
return "..."
real_cls = self.__class__
if ns is None:
qualname = getattr(real_cls, "__qualname__", None)
if qualname is not None: # pragma: no cover
# This case only happens on Python 3.5 and 3.6. We exclude
# it from coverage, because we don't want to slow down our
# test suite by running them under coverage too for this
# one line.
class_name = qualname.rsplit(">.", 1)[-1]
else: else:
result.append(", ") class_name = real_cls.__name__
result.extend( else:
(name, "=", attr_repr(getattr(self, name, NOTHING))) class_name = ns + "." + real_cls.__name__
)
return "".join(result) + ")"
finally:
working_set.remove(id(self))
return __repr__ # Since 'self' remains on the stack (i.e.: strongly referenced)
# for the duration of this call, it's safe to depend on id(...)
# stability, and not need to track the instance and therefore
# worry about properties like weakref- or hash-ability.
working_set.add(id(self))
try:
result = [class_name, "("]
first = True
for name, attr_repr in attr_names_with_reprs:
if first:
first = False
else:
result.append(", ")
result.extend(
(name, "=", attr_repr(getattr(self, name, NOTHING)))
)
return "".join(result) + ")"
finally:
working_set.remove(id(self))
return __repr__
def _add_repr(cls, ns=None, attrs=None): def _add_repr(cls, ns=None, attrs=None):
@ -1940,7 +2013,7 @@ def _add_repr(cls, ns=None, attrs=None):
if attrs is None: if attrs is None:
attrs = cls.__attrs_attrs__ attrs = cls.__attrs_attrs__
cls.__repr__ = _make_repr(attrs, ns) cls.__repr__ = _make_repr(attrs, ns, cls)
return cls return cls