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:
parent
0c769095ff
commit
9709dd82e1
|
@ -0,0 +1 @@
|
||||||
|
The generated ``__repr__`` is significantly faster on Pythons with F-strings.
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue