]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-90104: avoid RecursionError on recursive dataclass field repr (gh-100756)
authorCarl Meyer <carl@oddbird.net>
Fri, 6 Jan 2023 00:19:40 +0000 (17:19 -0700)
committerGitHub <noreply@github.com>
Fri, 6 Jan 2023 00:19:40 +0000 (19:19 -0500)
Avoid RecursionError on recursive dataclass field repr

Lib/dataclasses.py
Lib/test/test_dataclasses.py
Misc/NEWS.d/next/Library/2023-01-04-22-10-31.gh-issue-90104.yZk5EX.rst [new file with mode: 0644]

index b54e16984cb15c1a6fa53c13f357f4ccf5abdff5..5c0257eba186d15b349775a7fb5e0e0a3a0e9dc3 100644 (file)
@@ -223,6 +223,26 @@ _POST_INIT_NAME = '__post_init__'
 # https://bugs.python.org/issue33453 for details.
 _MODULE_IDENTIFIER_RE = re.compile(r'^(?:\s*(\w+)\s*\.)?\s*(\w+)')
 
+# This function's logic is copied from "recursive_repr" function in
+# reprlib module to avoid dependency.
+def _recursive_repr(user_function):
+    # Decorator to make a repr function return "..." for a recursive
+    # call.
+    repr_running = set()
+
+    @functools.wraps(user_function)
+    def wrapper(self):
+        key = id(self), _thread.get_ident()
+        if key in repr_running:
+            return '...'
+        repr_running.add(key)
+        try:
+            result = user_function(self)
+        finally:
+            repr_running.discard(key)
+        return result
+    return wrapper
+
 class InitVar:
     __slots__ = ('type', )
 
@@ -280,6 +300,7 @@ class Field:
         self.kw_only = kw_only
         self._field_type = None
 
+    @_recursive_repr
     def __repr__(self):
         return ('Field('
                 f'name={self.name!r},'
@@ -403,27 +424,6 @@ def _tuple_str(obj_name, fields):
     return f'({",".join([f"{obj_name}.{f.name}" for f in fields])},)'
 
 
-# This function's logic is copied from "recursive_repr" function in
-# reprlib module to avoid dependency.
-def _recursive_repr(user_function):
-    # Decorator to make a repr function return "..." for a recursive
-    # call.
-    repr_running = set()
-
-    @functools.wraps(user_function)
-    def wrapper(self):
-        key = id(self), _thread.get_ident()
-        if key in repr_running:
-            return '...'
-        repr_running.add(key)
-        try:
-            result = user_function(self)
-        finally:
-            repr_running.discard(key)
-        return result
-    return wrapper
-
-
 def _create_fn(name, args, body, *, globals=None, locals=None,
                return_type=MISSING):
     # Note that we may mutate locals. Callers beware!
index a09f36c3dac1ce1caa67df548adc40ca9a8771a5..81a36aa241acf76944462e702ab9e0f52909a130 100644 (file)
@@ -68,6 +68,24 @@ class TestCase(unittest.TestCase):
 
         self.assertEqual(repr_output, expected_output)
 
+    def test_field_recursive_repr(self):
+        rec_field = field()
+        rec_field.type = rec_field
+        rec_field.name = "id"
+        repr_output = repr(rec_field)
+
+        self.assertIn(",type=...,", repr_output)
+
+    def test_recursive_annotation(self):
+        class C:
+            pass
+
+        @dataclass
+        class D:
+            C: C = field()
+
+        self.assertIn(",type=...,", repr(D.__dataclass_fields__["C"]))
+
     def test_dataclass_params_repr(self):
         # Even though this is testing an internal implementation detail,
         # it's testing a feature we want to make sure is correctly implemented
diff --git a/Misc/NEWS.d/next/Library/2023-01-04-22-10-31.gh-issue-90104.yZk5EX.rst b/Misc/NEWS.d/next/Library/2023-01-04-22-10-31.gh-issue-90104.yZk5EX.rst
new file mode 100644 (file)
index 0000000..6695c92
--- /dev/null
@@ -0,0 +1 @@
+Avoid RecursionError on ``repr`` if a dataclass field definition has a cyclic reference.