diff --git a/changelog.d/819.changes.rst b/changelog.d/819.changes.rst new file mode 100644 index 000000000..eb45d6168 --- /dev/null +++ b/changelog.d/819.changes.rst @@ -0,0 +1 @@ +The generated ``__repr__`` is significantly faster on Pythons with F-strings. diff --git a/src/attr/_compat.py b/src/attr/_compat.py index c218e2453..69329e996 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -8,6 +8,11 @@ PY2 = sys.version_info[0] == 2 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) diff --git a/src/attr/_make.py b/src/attr/_make.py index ad17109db..658eb047b 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -12,6 +12,7 @@ from . import _config, setters from ._compat import ( + HAS_F_STRINGS, PY2, PY310, PYPY, @@ -888,7 +889,7 @@ def _create_slots_class(self): def add_repr(self, ns): self._cls_dict["__repr__"] = self._add_method_dunders( - _make_repr(self._attrs, ns=ns) + _make_repr(self._attrs, ns, self._cls) ) return self @@ -1873,64 +1874,136 @@ def _add_eq(cls, attrs=None): _already_repring = threading.local() +if HAS_F_STRINGS: + + def _make_repr(attrs, ns, cls): + unique_filename = "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 + # 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) -def _make_repr(attrs, ns): - """ - Make a repr method that includes relevant *attrs*, adding *ns* to the full - name. - """ + if ns is None: + cls_name_fragment = ( + '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' + ) + else: + cls_name_fragment = ns + ".{self.__class__.__name__}" + + lines = [] + lines.append("def __repr__(self):") + lines.append(" try:") + lines.append(" working_set = _already_repring.working_set") + lines.append(" except AttributeError:") + lines.append(" working_set = {id(self),}") + lines.append(" _already_repring.working_set = working_set") + lines.append(" else:") + lines.append(" if id(self) in working_set:") + lines.append(" return '...'") + 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))") - # 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 - ) + return _make_method( + "__repr__", "\n".join(lines), unique_filename, globs=globs + ) - def __repr__(self): + +else: + + def _make_repr(attrs, ns, _): """ - Automatically created by attrs. + Make a repr method that includes relevant *attrs*, adding *ns* to the + full name. """ - 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: - class_name = qualname.rsplit(">.", 1)[-1] - else: - class_name = real_cls.__name__ - else: - class_name = ns + "." + real_cls.__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 + ) - # 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 + 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: - result.append(", ") - result.extend( - (name, "=", attr_repr(getattr(self, name, NOTHING))) - ) - return "".join(result) + ")" - finally: - working_set.remove(id(self)) + class_name = real_cls.__name__ + else: + class_name = ns + "." + real_cls.__name__ + + # 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__ + return __repr__ def _add_repr(cls, ns=None, attrs=None): @@ -1940,7 +2013,7 @@ def _add_repr(cls, ns=None, attrs=None): if attrs is None: attrs = cls.__attrs_attrs__ - cls.__repr__ = _make_repr(attrs, ns) + cls.__repr__ = _make_repr(attrs, ns, cls) return cls