Skip to content
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

🐛 Fix RootModel equality logic #5948

Merged
merged 2 commits into from
Jun 1, 2023
Merged

🐛 Fix RootModel equality logic #5948

merged 2 commits into from
Jun 1, 2023

Conversation

lig
Copy link
Contributor

@lig lig commented May 31, 2023

Change Summary

Fix crash on comparing RootModels for equality

Related issue number

None

Checklist

  • Unit tests for the changes exist
  • Tests pass on CI and coverage remains at 100%
  • Documentation reflects the changes where applicable
  • changes/<pull request or issue id>-<github username>.md file added describing change
    (see changes/README.md for details)
  • My PR is ready to review, please add a comment including the phrase "please review" to assign reviewers

Selected Reviewer: @dmontagu

@lig lig marked this pull request as ready for review May 31, 2023 10:13
@lig
Copy link
Contributor Author

lig commented May 31, 2023

please review

@lig
Copy link
Contributor Author

lig commented May 31, 2023

chaining this one after #5943

@lig lig mentioned this pull request May 31, 2023
5 tasks
@@ -1096,14 +1096,14 @@ def _calculate_keys(self, *args: Any, **kwargs: Any) -> Any:

class RootModel(BaseModel, typing.Generic[RootModelRootType]):
__pydantic_root_model__ = True
__pydantic_fields_set__ = {'root'} # It's fine having a set here as it will never change
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I think if we wanted it to be consistent with how it works for regular basemodel, this should depend on whether the default value was used. (If it needed to use the default, the value would be set())

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fair. Let me add a TODO here and get back to it in a separate PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you change the comment on the line so that it isn't in conflict with the TODO comment above it? Just thinking it will be a bit confusing to come back to

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I think we should fix this in the PR addressing the default setting for RootModel.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has been fixed already in #5949

Base automatically changed from more-root-model to main May 31, 2023 14:19
def __eq__(self, other: Any) -> bool:
if not isinstance(other, RootModel):
return NotImplemented
return self.model_fields['root'].annotation == other.model_fields['root'].annotation and super().__eq__(other)
Copy link
Contributor

@dmontagu dmontagu May 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this check on the annotations doing for us? I think we don't want to check the annotations are equal because that would mean that RootModel[Any](root=1) is not equal to RootModel[int](root=1), but that is the behavior that would be consistent with how generic BaseModel works. (You could similar concerns imagine for generic subclasses of RootModel)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought was that this should be true RootModel[int](42) != RootModel[float](42), shouldn't it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having that return True is a price I'm willing to pay to get equality with Any. I mean, if you did:

from typing import Generic, TypeVar

from pydantic import BaseModel

T = TypeVar("T")


class MyModel(BaseModel, Generic[T]):
    x: T


assert MyModel[int](x=42) == MyModel[float](x=42.0)

it currently doesn't error. And I think there's a lot of stuff that behaves worse if you try to change this, because of things like reparametrizing generics, etc. I just think it's easiest if we have RootModel behave consistently with this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with the annotation but, but I wonder if we should do and self.root == other.root?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samuelcolvin we also should compare private fields and that is why there is the super call.

Copy link
Contributor

@dmontagu dmontagu Jun 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think comparing private fields is a good reason to keep the super().__eq__.

But also, you need to have the super().__eq__ to make sure the classes match — if you subclass RootModel[T] (which I would imagine is the reason why you are using a RootModel and not TypeAdapter in most cases), instance equality should require type equality. At least if we want to be consistent with BaseModel, and I think we should. (Well, you could drop the super().__eq__, but then I think we should copy that logic as well.)

I still think we should drop the annotation comparison thing because it's surprisingly inconsistent with other generic BaseModel subclasses, but it's not a hill I want to die on.

@lig
Copy link
Contributor Author

lig commented May 31, 2023

please review

@cloudflare-pages
Copy link

Deploying with  Cloudflare Pages  Cloudflare Pages

Latest commit: c575e77
Status: ✅  Deploy successful!
Preview URL: https://83fec789.pydantic-docs2.pages.dev
Branch Preview URL: https://fix-root-model-eq.pydantic-docs2.pages.dev

View logs

Copy link
Member

@samuelcolvin samuelcolvin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please update (or we can discuss if you think this is right)

def __eq__(self, other: Any) -> bool:
if not isinstance(other, RootModel):
return NotImplemented
return self.model_fields['root'].annotation == other.model_fields['root'].annotation and super().__eq__(other)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with the annotation but, but I wonder if we should do and self.root == other.root?

@lig
Copy link
Contributor Author

lig commented May 31, 2023

please review

@samuelcolvin I'm happy to discuss this

Copy link
Contributor

@dmontagu dmontagu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted, I still would like to drop the annotation comparison in the __eq__, but if I'm the only one that feels that way it doesn't need to hold up merging. (The thing about setting __pydantic_fields_set__ seems better addressed in the other PR so I'm okay with that here.)

@samuelcolvin
Copy link
Member

No, I think your right, we should compare the generic annotations.

Does equality work correctly for generic basmodels? Surely if it did, we wouldn't need a custom method for RootModel.

@lig lig merged commit 8e1dead into main Jun 1, 2023
53 checks passed
@lig lig deleted the fix-root-model-eq branch June 1, 2023 06:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants