Skip to content

Commit 1f9d541

Browse files
committed
pythongh-90104: avoid RecursionError on recursive dataclass field repr
1 parent 52017db commit 1f9d541

File tree

2 files changed

+35
-21
lines changed

2 files changed

+35
-21
lines changed

Lib/dataclasses.py

+21-21
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},'
@@ -403,27 +424,6 @@ def _tuple_str(obj_name, fields):
403424
return f'({",".join([f"{obj_name}.{f.name}" for f in fields])},)'
404425

405426

406-
# This function's logic is copied from "recursive_repr" function in
407-
# reprlib module to avoid dependency.
408-
def _recursive_repr(user_function):
409-
# Decorator to make a repr function return "..." for a recursive
410-
# call.
411-
repr_running = set()
412-
413-
@functools.wraps(user_function)
414-
def wrapper(self):
415-
key = id(self), _thread.get_ident()
416-
if key in repr_running:
417-
return '...'
418-
repr_running.add(key)
419-
try:
420-
result = user_function(self)
421-
finally:
422-
repr_running.discard(key)
423-
return result
424-
return wrapper
425-
426-
427427
def _create_fn(name, args, body, *, globals=None, locals=None,
428428
return_type=MISSING):
429429
# Note that we may mutate locals. Callers beware!

Lib/test/test_dataclasses.py

+14
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,20 @@ 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+
expected_output = "Field(name='id',type=...," \
77+
f"default={MISSING!r},default_factory={MISSING!r}," \
78+
"init=True,repr=True,hash=None," \
79+
"compare=True,metadata=mappingproxy({})," \
80+
f"kw_only={MISSING!r}," \
81+
"_field_type=None)"
82+
83+
self.assertEqual(repr_output, expected_output)
84+
7185
def test_dataclass_params_repr(self):
7286
# Even though this is testing an internal implementation detail,
7387
# it's testing a feature we want to make sure is correctly implemented

0 commit comments

Comments
 (0)