Skip to content

Conversation

ljodal
Copy link
Contributor

@ljodal ljodal commented Sep 27, 2022

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.

@ljodal
Copy link
Contributor Author

ljodal commented Sep 27, 2022

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 Type argument "T" of "MyManager" must be a subtype of "MyModel" [type-var].

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 SecondManager should be interpreted as SecondManager[T: MyModel] rather than SecondManager[T: Model]. I'll take a stab and see :)

Comment on lines +391 to +393
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")
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 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

@ljodal
Copy link
Contributor Author

ljodal commented Sep 27, 2022

[...]
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 SecondManager should be interpreted as SecondManager[T: MyModel] rather than SecondManager[T: Model]. I'll take a stab and see :)

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 Anys

@ljodal ljodal force-pushed the fix/reparametrize-managers branch from 8824289 to 0ad212c Compare September 29, 2022 12:42
Copy link
Member

@sobolevn sobolevn left a 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.

Copy link
Member

@sobolevn sobolevn left a 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 :)

@sobolevn
Copy link
Member

@flaeppe any comments?

Copy link
Member

@flaeppe flaeppe left a 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
Copy link
Member

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..

Copy link
Member

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

Copy link
Contributor Author

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 :)

Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

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?

Copy link
Member

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 🤔

Copy link
Contributor Author

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)

Copy link
Contributor Author

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

Copy link
Contributor Author

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(
Copy link
Member

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"
Copy link
Member

@sobolevn sobolevn Sep 30, 2022

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)

Copy link
Contributor Author

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

Copy link
Contributor Author

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?

Copy link
Member

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)?

Copy link
Contributor Author

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?

Copy link
Member

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?

Copy link
Member

@sobolevn sobolevn Oct 1, 2022

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.

Copy link
Contributor Author

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.
@ljodal ljodal force-pushed the fix/reparametrize-managers branch from d0b95ef to fc6406a Compare September 30, 2022 19:53
@ljodal ljodal force-pushed the fix/reparametrize-managers branch from de9416b to 7079f33 Compare October 3, 2022 17:16
Copy link
Member

@sobolevn sobolevn left a 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.

@sobolevn sobolevn merged commit dde0f2f into typeddjango:master Oct 3, 2022
@ljodal ljodal deleted the fix/reparametrize-managers branch October 3, 2022 19:21
UnknownPlatypus added a commit to UnknownPlatypus/django-stubs that referenced this pull request Aug 21, 2025
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

error: Name "Sequence" is not defined
3 participants