Skip to content

Commit b9182aa

Browse files
miss-islingtonsrinivasreddy
authored andcommitted
bpo-33947: dataclasses no longer can raise RecursionError in repr (GF9916) (#9970)
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. (cherry picked from commit dd13c88) Co-authored-by: Srinivas Thatiparthy (శ్రీనివాస్ తాటిపర్తి) <[email protected]>
1 parent bd9c2ce commit b9182aa

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)