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.

Author dankner
Recipients dankner
Date 2022-03-20.22:56:45
SpamBayes Score -1.0
Marked as misclassified Yes
Message-id <1647817005.88.0.843741930968.issue47073@roundup.psfhosted.org>
In-reply-to
Content
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 dataclasses.py 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()

    @functools.wraps(user_function)
    def wrapper(self, other):
        key = id(self), id(other), _thread.get_ident()
        if key in eq_running:
            return True
        eq_running.add(key)
       
        try:
            result = user_function(self, other)
        finally:
            eq_running.discard(key)
        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!
History
Date User Action Args
2022-03-20 22:56:45danknersetrecipients: + dankner
2022-03-20 22:56:45danknersetmessageid: <1647817005.88.0.843741930968.issue47073@roundup.psfhosted.org>
2022-03-20 22:56:45danknerlinkissue47073 messages
2022-03-20 22:56:45danknercreate