This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

Title: Solution for recursion error when comparing dataclass objects
Type: behavior Stage:
Components: Library (Lib) Versions: Python 3.11
Status: open Resolution:
Dependencies: Superseder:
Assigned To: eric.smith Nosy List: dankner, eric.smith
Priority: normal Keywords:

Created on 2022-03-20 22:56 by dankner, last changed 2022-04-11 14:59 by admin.

Messages (1)
msg415641 - (view) Author: Yonatan Dankner (dankner) Date: 2022-03-20 22:56
TL;DR: A possible method to avoid infinite recursion when comparing dataclasses with circular references, using a decorator.

>>> @dataclass
... class A:
...     x: int = 1
...     self_reference: Any = field(init=False)
...     def __post_init__(self):
...         self.self_reference = self
>>> A(x=1) == A(x=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 3, in __eq__
  File "<string>", line 3, in __eq__
  File "<string>", line 3, in __eq__
  [Previous line repeated 330 more times]
RecursionError: maximum recursion depth exceeded in comparison

It should in my opinion return True.

To avoid a recursion error, I would like to suggest a solution using a wrapper, similar to the one implemented for repr (see dataclasses._recursive_repr):
If within the process of comparing two objects, we have encountered an attempt to compare these objects (meaning we have reached a loop), we should return true, since no inequalities will be discovered if we continue recursing.

I suggest an addition to which would look something like this (drew inspiration from _recursive_repr):

def _recursive_eq(user_function):
    # Decorator to make a eq function return True when a loop was detected.
    eq_running = set()

    def wrapper(self, other):
        key = id(self), id(other), _thread.get_ident()
        if key in eq_running:
            return True
            result = user_function(self, other)
        return result
    return wrapper

# And then replacing the _cmp_fn with the following.

def _cmp_fn(name, op, self_tuple, other_tuple):
    return _recursive_eq(_create_fn(name,
                      ('self', 'other'),
                      [ 'if other.__class__ is self.__class__:',
                       f' return {self_tuple}{op}{other_tuple}',
                        'return NotImplemented']))

I would like to hear your thoughts!
Date User Action Args
2022-04-11 14:59:57adminsetgithub: 91229
2022-03-21 01:08:56eric.smithsetassignee: eric.smith
2022-03-20 23:42:57AlexWaygoodsetnosy: + eric.smith
2022-03-20 22:56:45danknercreate