Skip to content

Commit f488831

Browse files
authored
[3.11] gh-90104: avoid RecursionError on recursive dataclass field repr (gh-100756) (GH-100784)
Avoid RecursionError on recursive dataclass field repr (cherry picked from commit 0a7936a) Automerge-Triggered-By: GH:ericvsmith
1 parent d6b8413 commit f488831

File tree

3 files changed

+40
-21
lines changed

3 files changed

+40
-21
lines changed

Lib/dataclasses.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,26 @@ def __repr__(self):
223223
# https://bugs.python.org/issue33453 for details.
224224
_MODULE_IDENTIFIER_RE = re.compile(r'^(?:\s*(\w+)\s*\.)?\s*(\w+)')
225225

226+
# This function's logic is copied from "recursive_repr" function in
227+
# reprlib module to avoid dependency.
228+
def _recursive_repr(user_function):
229+
# Decorator to make a repr function return "..." for a recursive
230+
# call.
231+
repr_running = set()
232+
233+
@functools.wraps(user_function)
234+
def wrapper(self):
235+
key = id(self), _thread.get_ident()
236+
if key in repr_running:
237+
return '...'
238+
repr_running.add(key)
239+
try:
240+
result = user_function(self)
241+
finally:
242+
repr_running.discard(key)
243+
return result
244+
return wrapper
245+
226246
class InitVar:
227247
__slots__ = ('type', )
228248

@@ -280,6 +300,7 @@ def __init__(self, default, default_factory, init, repr, hash, compare,
280300
self.kw_only = kw_only
281301
self._field_type = None
282302

303+
@_recursive_repr
283304
def __repr__(self):
284305
return ('Field('
285306
f'name={self.name!r},'
@@ -389,27 +410,6 @@ def _tuple_str(obj_name, fields):
389410
return f'({",".join([f"{obj_name}.{f.name}" for f in fields])},)'
390411

391412

392-
# This function's logic is copied from "recursive_repr" function in
393-
# reprlib module to avoid dependency.
394-
def _recursive_repr(user_function):
395-
# Decorator to make a repr function return "..." for a recursive
396-
# call.
397-
repr_running = set()
398-
399-
@functools.wraps(user_function)
400-
def wrapper(self):
401-
key = id(self), _thread.get_ident()
402-
if key in repr_running:
403-
return '...'
404-
repr_running.add(key)
405-
try:
406-
result = user_function(self)
407-
finally:
408-
repr_running.discard(key)
409-
return result
410-
return wrapper
411-
412-
413413
def _create_fn(name, args, body, *, globals=None, locals=None,
414414
return_type=MISSING):
415415
# Note that we may mutate locals. Callers beware!

Lib/test/test_dataclasses.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,24 @@ def test_field_repr(self):
6868

6969
self.assertEqual(repr_output, expected_output)
7070

71+
def test_field_recursive_repr(self):
72+
rec_field = field()
73+
rec_field.type = rec_field
74+
rec_field.name = "id"
75+
repr_output = repr(rec_field)
76+
77+
self.assertIn(",type=...,", repr_output)
78+
79+
def test_recursive_annotation(self):
80+
class C:
81+
pass
82+
83+
@dataclass
84+
class D:
85+
C: C = field()
86+
87+
self.assertIn(",type=...,", repr(D.__dataclass_fields__["C"]))
88+
7189
def test_named_init_params(self):
7290
@dataclass
7391
class C:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Avoid RecursionError on ``repr`` if a dataclass field definition has a cyclic reference.

0 commit comments

Comments
 (0)