Skip to content

Commit dd13c88

Browse files
srinivasreddyericvsmith
authored andcommitted
bpo-33947: dataclasses no longer can raise RecursionError in repr (GF9916)
The reprlib code was copied here instead of importing reprlib. I'm not sure if we really need to avoid the import, but since I expect dataclasses to be more common that reprlib, it seems wise. Plus, the code is small.
1 parent 55f8249 commit dd13c88

File tree

3 files changed

+118
-6
lines changed

3 files changed

+118
-6
lines changed

Lib/dataclasses.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import inspect
66
import keyword
77
import builtins
8+
import functools
9+
import _thread
10+
811

912
__all__ = ['dataclass',
1013
'field',
@@ -337,6 +340,27 @@ def _tuple_str(obj_name, fields):
337340
return f'({",".join([f"{obj_name}.{f.name}" for f in fields])},)'
338341

339342

343+
# This function's logic is copied from "recursive_repr" function in
344+
# reprlib module to avoid dependency.
345+
def _recursive_repr(user_function):
346+
# Decorator to make a repr function return "..." for a recursive
347+
# call.
348+
repr_running = set()
349+
350+
@functools.wraps(user_function)
351+
def wrapper(self):
352+
key = id(self), _thread.get_ident()
353+
if key in repr_running:
354+
return '...'
355+
repr_running.add(key)
356+
try:
357+
result = user_function(self)
358+
finally:
359+
repr_running.discard(key)
360+
return result
361+
return wrapper
362+
363+
340364
def _create_fn(name, args, body, *, globals=None, locals=None,
341365
return_type=MISSING):
342366
# Note that we mutate locals when exec() is called. Caller
@@ -497,12 +521,13 @@ def _init_fn(fields, frozen, has_post_init, self_name):
497521

498522

499523
def _repr_fn(fields):
500-
return _create_fn('__repr__',
501-
('self',),
502-
['return self.__class__.__qualname__ + f"(' +
503-
', '.join([f"{f.name}={{self.{f.name}!r}}"
504-
for f in fields]) +
505-
')"'])
524+
fn = _create_fn('__repr__',
525+
('self',),
526+
['return self.__class__.__qualname__ + f"(' +
527+
', '.join([f"{f.name}={{self.{f.name}!r}}"
528+
for f in fields]) +
529+
')"'])
530+
return _recursive_repr(fn)
506531

507532

508533
def _frozen_get_del_attr(cls, fields):

Lib/test/test_dataclasses.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3169,6 +3169,92 @@ def __post_init__(self, y):
31693169
replace(c, x=3)
31703170
c = replace(c, x=3, y=5)
31713171
self.assertEqual(c.x, 15)
3172+
3173+
def test_recursive_repr(self):
3174+
@dataclass
3175+
class C:
3176+
f: "C"
3177+
3178+
c = C(None)
3179+
c.f = c
3180+
self.assertEqual(repr(c), "TestReplace.test_recursive_repr.<locals>.C(f=...)")
3181+
3182+
def test_recursive_repr_two_attrs(self):
3183+
@dataclass
3184+
class C:
3185+
f: "C"
3186+
g: "C"
3187+
3188+
c = C(None, None)
3189+
c.f = c
3190+
c.g = c
3191+
self.assertEqual(repr(c), "TestReplace.test_recursive_repr_two_attrs"
3192+
".<locals>.C(f=..., g=...)")
3193+
3194+
def test_recursive_repr_indirection(self):
3195+
@dataclass
3196+
class C:
3197+
f: "D"
3198+
3199+
@dataclass
3200+
class D:
3201+
f: "C"
3202+
3203+
c = C(None)
3204+
d = D(None)
3205+
c.f = d
3206+
d.f = c
3207+
self.assertEqual(repr(c), "TestReplace.test_recursive_repr_indirection"
3208+
".<locals>.C(f=TestReplace.test_recursive_repr_indirection"
3209+
".<locals>.D(f=...))")
3210+
3211+
def test_recursive_repr_indirection_two(self):
3212+
@dataclass
3213+
class C:
3214+
f: "D"
3215+
3216+
@dataclass
3217+
class D:
3218+
f: "E"
3219+
3220+
@dataclass
3221+
class E:
3222+
f: "C"
3223+
3224+
c = C(None)
3225+
d = D(None)
3226+
e = E(None)
3227+
c.f = d
3228+
d.f = e
3229+
e.f = c
3230+
self.assertEqual(repr(c), "TestReplace.test_recursive_repr_indirection_two"
3231+
".<locals>.C(f=TestReplace.test_recursive_repr_indirection_two"
3232+
".<locals>.D(f=TestReplace.test_recursive_repr_indirection_two"
3233+
".<locals>.E(f=...)))")
3234+
3235+
def test_recursive_repr_two_attrs(self):
3236+
@dataclass
3237+
class C:
3238+
f: "C"
3239+
g: "C"
3240+
3241+
c = C(None, None)
3242+
c.f = c
3243+
c.g = c
3244+
self.assertEqual(repr(c), "TestReplace.test_recursive_repr_two_attrs"
3245+
".<locals>.C(f=..., g=...)")
3246+
3247+
def test_recursive_repr_misc_attrs(self):
3248+
@dataclass
3249+
class C:
3250+
f: "C"
3251+
g: int
3252+
3253+
c = C(None, 1)
3254+
c.f = c
3255+
self.assertEqual(repr(c), "TestReplace.test_recursive_repr_misc_attrs"
3256+
".<locals>.C(f=..., g=1)")
3257+
31723258
## def test_initvar(self):
31733259
## @dataclass
31743260
## class C:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dataclasses now handle recursive reprs without raising RecursionError.

0 commit comments

Comments
 (0)