Skip to content

Commit ebe8d23

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

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
@@ -222,6 +222,26 @@ def __repr__(self):
222222
# https://bugs.python.org/issue33453 for details.
223223
_MODULE_IDENTIFIER_RE = re.compile(r'^(?:\s*(\w+)\s*\.)?\s*(\w+)')
224224

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

@@ -279,6 +299,7 @@ def __init__(self, default, default_factory, init, repr, hash, compare,
279299
self.kw_only = kw_only
280300
self._field_type = None
281301

302+
@_recursive_repr
282303
def __repr__(self):
283304
return ('Field('
284305
f'name={self.name!r},'
@@ -388,27 +409,6 @@ def _tuple_str(obj_name, fields):
388409
return f'({",".join([f"{obj_name}.{f.name}" for f in fields])},)'
389410

390411

391-
# This function's logic is copied from "recursive_repr" function in
392-
# reprlib module to avoid dependency.
393-
def _recursive_repr(user_function):
394-
# Decorator to make a repr function return "..." for a recursive
395-
# call.
396-
repr_running = set()
397-
398-
@functools.wraps(user_function)
399-
def wrapper(self):
400-
key = id(self), _thread.get_ident()
401-
if key in repr_running:
402-
return '...'
403-
repr_running.add(key)
404-
try:
405-
result = user_function(self)
406-
finally:
407-
repr_running.discard(key)
408-
return result
409-
return wrapper
410-
411-
412412
def _create_fn(name, args, body, *, globals=None, locals=None,
413413
return_type=MISSING):
414414
# 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
@@ -67,6 +67,24 @@ def test_field_repr(self):
6767

6868
self.assertEqual(repr_output, expected_output)
6969

70+
def test_field_recursive_repr(self):
71+
rec_field = field()
72+
rec_field.type = rec_field
73+
rec_field.name = "id"
74+
repr_output = repr(rec_field)
75+
76+
self.assertIn(",type=...,", repr_output)
77+
78+
def test_recursive_annotation(self):
79+
class C:
80+
pass
81+
82+
@dataclass
83+
class D:
84+
C: C = field()
85+
86+
self.assertIn(",type=...,", repr(D.__dataclass_fields__["C"]))
87+
7088
def test_named_init_params(self):
7189
@dataclass
7290
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)