-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make dataclasses/attrs comparison recursive, fixes #4675 #6835
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! I really missed this in some previous projects where I had large elaborate attrs types.
It will be easier to evaluate this change if the test case is made a bit more complex. Specifically I'd suggest:
- Keep the 2nd level diff you added.
- Add another dataclass attribute (so 3 levels of nesting) with a differing attribute in that.
This will test more than one diff and more than one level of nesting.
I also realized that the object could somehow be self-referencing, should I add a recursion limit or keep a set of already-seen object? I'll add a test for that as well. |
It fails with RecursionError, is that acceptable? I suppose if someone creates a self-referencing dataclass with the compare setting set on that field, we can assume it's not correct. |
I wonder if this is the first case of possible cycles in pytets's diff reporting (sorry -- I am too lazy to check 😴)? If this is already handled somewhere, I'd do the same. If it's the first case, then I do think it should be handled. |
They are legit but e.g. the I don't see other cases of recursion, but I could add a depth limit for example (it just makes the code less clean). |
Usually |
I think this should pass through some state maybe (which I've thought to be useful before already - instead of just "verbose"), so that it could stop on recursions itself / handle self-referencing objects. |
I feel like to implement this properly, the hook itself should be able to return recursive comparisons to be done, and the self-reference issue could be handled at the call site. That way custom assertion rewrite would automatically be handled for recursive structure, and the implementation wouldn't have to worry about self-reference. Maybe one backward compatible way to implement this would be to add a hook wrapper around That's a bit more work of course, otherwise I'll just add a list of already seen objects in the internal comparison functions (well only the dataclass one for now). |
Any preferred solution for this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @iwanb!
While I agree we should handle recursion, I'm also happy with working to get the current work merged and iterate over the recursion problem separately. 👍
@iwanb, @nicoddemus - how about we add @nicoddemus' last suggestion and then merge? It sounds like that will at least be an improvement over the status quo 🙂 |
I missed the earlier reply, I've added the suggestions. |
Thanks @iwanb, when this is needed, it is very needed. I once created an anonymized version of a big attrs type I have in a project, and I tried this PR on it. Showfrom __future__ import annotations
from typing import Optional, List
import attr
import enum
@attr.s(slots=True, frozen=True)
class Lorem:
ipsum = attr.ib()
dolor = attr.ib()
sit = attr.ib()
amet = attr.ib()
consectetur = attr.ib()
adipiscing = attr.ib()
@attr.s(slots=True, frozen=True)
class Fugiat:
eiusmod = attr.ib()
tempor = attr.ib()
incididunt = attr.ib()
labore = attr.ib()
dolore = attr.ib()
magna = attr.ib()
aliqua = attr.ib()
veniam = attr.ib()
nostrud = attr.ib()
exercitation = attr.ib()
ullamco = attr.ib()
laboris = attr.ib()
commodo = attr.ib()
consequat = attr.ib()
aute = attr.ib()
@attr.s(slots=True, frozen=True)
class Invenire:
irure = attr.ib()
reprehenderit = attr.ib()
voluptate = attr.ib()
velit = attr.ib()
esse = attr.ib()
cillum = attr.ib()
eepcillum = attr.ib()
@attr.s(slots=True, frozen=True)
class Tritani:
nulla = attr.ib()
name = attr.ib()
value = attr.ib()
pariatur = attr.ib()
exceptuer = attr.ib()
@attr.s(slots=True, frozen=True)
class Laborum:
type = attr.ib()
urangulal = attr.ib()
ipsumal = attr.ib()
occaecat = attr.ib()
cupidatat = attr.ib()
proident = attr.ib()
class Aliquip(enum.IntEnum):
Aliquip1 = 1
Aliquip2 = 2
Aliquip3 = 3
Aliquip4 = 4
Aliquip5 = 5
@attr.s(slots=True, frozen=True)
class Assentior:
aliquip = attr.ib(type=Aliquip)
culpa = attr.ib()
fugiat = attr.ib(type=Optional[Fugiat])
invenire = attr.ib(type=Optional[Invenire])
deserunt = attr.ib()
lorem = attr.ib(type=Optional[Lorem])
mollit = attr.ib()
laborums = attr.ib(type=Optional[List[Laborum]])
tantas = attr.ib()
nominati = attr.ib()
fabulas = attr.ib()
tritani = attr.ib(type=Optional[Tritani])
replace = attr.evolve
@attr.s(slots=True, frozen=True)
class Dignissim:
assentior = attr.ib(type=Assentior)
new_cupidatat = attr.ib()
laoreet = attr.ib()
rationibus = attr.ib()
replace = attr.evolve
obj = Dignissim(
assentior=Assentior(
aliquip=Aliquip.Aliquip1,
culpa=6,
fugiat=Fugiat(
eiusmod='aaaaaaaaaaaaaaaa',
tempor='bbbbbb',
incididunt='CCCCCCCCCCCCCCCCCCC',
labore='dddddddddd',
dolore='eeeeeeeeee',
magna='fffffffffff',
aliqua='gggggggggggggg',
veniam=None,
nostrud=None,
exercitation=None,
ullamco=None,
laboris=None,
commodo=None,
consequat=None,
aute=None,
),
invenire=Invenire(
irure=53,
reprehenderit=153,
voluptate=242,
velit=100,
esse=5035,
cillum=53,
eepcillum=422,
),
deserunt=True,
lorem=Lorem(
ipsum=b'',
dolor=[
'xx',
],
sit=[
'yy'
],
amet=[
'zz',
],
consectetur=b'sdfsd',
adipiscing=b'adfsdf',
),
mollit=4294967294,
laborums=[
Laborum(
type=23,
urangulal=b"abc",
ipsumal=None,
occaecat=b'sdf',
cupidatat=13,
proident=None,
),
],
tantas='sdfsdlxcv49249sdfs90sdf==',
nominati=-1,
fabulas=False,
tritani=None,
),
new_cupidatat=13,
laoreet=1,
rationibus=False,
)
def test_cmp():
assert obj == obj.replace(assentior=obj.assentior.replace(culpa=7, aliquip=Aliquip.Aliquip3)) This is the output:
I only have one quip about it: I created two differing attributes. One level of the tree just starts immediately after the previous text (e.g. the |
It would for sure be nice to have a visual tree structure for the error, but it's a bigger change as the functions need to be made aware that they are recursive (e.g. with a counter). |
No problem, I'll open an issue about after this is merged, to keep track. |
@nicoddemus I think this is ready to merge, pending your review. I'll open a follow up issue after merge. |
Thanks @iwanb and sorry for the delay! |
No problem, thanks for fixing it up! |
Follow up issue in #7348. |
I extracted the equality check in its own function and I call it from the dataclass comparison function to get recursive comparisons.
It could also be used for dict comparison, but the issue mentioned there was already some mechanism for dicts (I didn't see it in the code though).
Fixes #4675.