-
-
Notifications
You must be signed in to change notification settings - Fork 501
Reparametrize managers without explicit type parameters #1169
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
Reparametrize managers without explicit type parameters #1169
Conversation
In addition to new type errors being discovered in our codebase, I've seen one new side-effect of this, which I'm unsure if we should consider a problem or not. Say you have a manager defined like this: from typing import TypeVar
from django.db import models
T = TypeVar("T", bound="Model")
class MyManager(models.Manager[T]):
pass
class MyModel(models.Model):
pass If you subclass this without specifying generics, like this: class SecondManager(MyManager):
pass you'll get an error saying But as I'm writing this I realize I might be able to make the logic smart enough to pick the "last" parameter in the MRO stack, if that sounds reasonable? Then |
out: | | ||
myapp/models:4: error: Return type "MyModel" of "create" incompatible with return type "_T" in supertype "BaseManager" | ||
myapp/models:5: error: Incompatible return value type (got "_T", expected "MyModel") |
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.
This is the failing test I mentioned in the PR description. I decided to fix it like this, basically saying this is an error. I think it should be fine to raise an error if you've restricted the type of an overridden method but the type vars of to the manager class
I've implemented this fix now, so I think this should be good to go. It does not cause any new issues that I've seen in our codebase other than surfacing more type errors that were previously hidden behind implicit |
8824289
to
0ad212c
Compare
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.
Can you please rebase your branch?
I've just merged a similar PR, I want to be sure that it is fine.
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.
Oh, you just did :)
@flaeppe any comments? |
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.
I only have one thought, I'm a bit unsure about it though
is_missing_params = ( | ||
len(parent_manager.args) == 1 | ||
and isinstance(parent_manager.args[0], AnyType) | ||
and parent_manager.args[0].type_of_any is not TypeOfAny.explicit |
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.
There's also a TypeOfAny.from_omitted_generics
. Not sure what we should use here, but we might perhaps be a bit more narrow if we do TypeOfAny.from_omitted_generics
?
If it even works, might be cases I'm forgetting..
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.
Otherwise we could overwrite something like TypeOfAny.from_error
etc etc
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.
Aight, I'll take a look and see what type of any we actually get in these cases and make it specific to that if possible :)
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.
This will trigger an error if disallow_any_generics = True
is set.
I am not sure we want 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.
I'm not following @sobolevn, you mean as the code is written now or if using TypeOfAny.from_omitted_generics
?
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.
I don't think that hits the new hook here, as that's (as far as I know) only called for class MyClass:
statements in code. Related managers are handled elsewhere I think?
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.
Sorry for causing so much confusion :)
I was thinking about cases where our users have disallow_any_generics = True
set, current implementation looks like it can still cause this error. Let's add a test case with disallow_any_generics = True
set. I am pretty sure that we won't be able to fix / suppress this error, because it happens too early: https://github.com/python/mypy/blob/ddd917767aa8135f3b1aeef47b0bb0616a4b63fb/mypy/typeanal.py#L1462
But, we can at least be sure: what happens and what to expect.
Maybe even mention this in the docs 🤔
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.
Ah, okay :) I'll add a test case!
I don't think we should try to suppress the error message if possible though, if the user has enabled that flag I'd expect them to want a warning. At least in our codebase the implicit generics we add are less specific than what you'd manually add, so I consider this a sort of in-between solution until all of the project is fully typed (we're working in a fairly large codebase, gradually adding types)
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.
Hmm, it seems like this interacts a bit weirdly with disallow_any_generics = True
. You'll get the expected warning from mypy and it seems like whatever we do in the hook is overwritten and the return types are changed back to Any
🤔 I added a test that demonstrates this behaviour
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.
Ah, I dug a bit deeper and when that option is enabled the Any
type is changed to TypeOfAny.from_error
, so the hook doesn't do anything. I think that's fair enough if you've enabled that option?
return api.anal_type(t) | ||
|
||
|
||
def copy_method_to_another_class( |
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.
How great, we finally get rid of this one!
installed_apps: | ||
- myapp | ||
out: | | ||
myapp/models:4: error: Return type "MyModel" of "create" incompatible with return type "_T" in supertype "BaseManager" |
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.
This looks like a problem: BaseManager
defines create() -> [T: Model]
, and our ModelManger(BaseManager)
defines it as create() -> MyModel
. Why is that an error?
Refs #1169 (comment)
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.
Hmm, yeah, I can dig a bit deeper into 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.
The error here is exactly the same as you'd get from mypy if you did this:
from typing import TypeVar
from django.db import models
T = TypeVar("T", bound=models.Model)
class MyManager(models.Manager[T]):
def create(self, **kwargs) -> "MyModel":
...
class MyModel(models.Model):
...
So it's expected based on what the plugin is doing, but I guess it might be surprising?
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.
Yes, I now understand. The error is correct, but is very confusing.
Last question: can we somehow make it more clear? Or complete remove it (eventhough it iw correct)?
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.
Hmm, I can see what I can do, but I'm not aware of any methods we have available to change mypy's warnings?
At the very least I can add a note about this in the readme. I might also be able to detect when this has happened and omit an additional error maybe, but I'm not sure if that helps much?
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.
We can add a note
saying that it probably should be class MyManager(models.Manager[MyModel]):
, not class MyManager(models.Manager[T]):
How does that sound?
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.
Or we can just ignore it for now :)
And wait for some complains first.
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.
I don't think it's easy to detect these cases, given how many methods on Manager
might be affected by this. I added a new section to the FAQ about this issue, so hopefully that should be enough.
This extracts the reparametrization logic from typeddjango#1030 in addition to removing the codepath that copied methods from querysets to managers. That code path seems to not be needed with this change.
This removes warnings when subclassing from something other than the base manager class, where the typevar has been restricted.
d0b95ef
to
fc6406a
Compare
de9416b
to
7079f33
Compare
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.
Thank you so much! Awesome feature.
I've done this in the same pr because now that we infer more correctly the queryset type, we were infering incomplete types more often
This extracts the reparametrization logic from #1030 in addition to removing the codepath that copied methods from querysets to managers. That code path seems to not be needed with this change.
This should properly fix #1022 as that keeps popping up from time to time when managers without type parameters are used.