Skip to content

Update Apps.get_model() return type #1318

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

false-vacuum
Copy link

@@ -24,7 +26,7 @@ class Apps:
def get_app_config(self, app_label: str) -> AppConfig: ...
# it's not possible to support it in plugin properly now
def get_models(self, include_auto_created: bool = ..., include_swapped: bool = ...) -> list[type[Model]]: ...
def get_model(self, app_label: str, model_name: str | None = ..., require_ready: bool = ...) -> type[Any]: ...
def get_model(self, app_label: str, model_name: str | None = ..., require_ready: bool = ...) -> ModelType: ...
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hi. I don't think it can work like this. The problem is that the model class name is passed in as string to model_name argument.

With this construction I suspect it will end up being type[Any] as well.

Copy link
Author

Choose a reason for hiding this comment

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

This works with the static type checker built into pycharm.

Copy link
Author

@false-vacuum false-vacuum Jan 12, 2023

Choose a reason for hiding this comment

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

After making this change

Screenshot_20230112_160729

Screenshot_20230112_160720


Before

Screenshot_20230112_161017

@intgr
Copy link
Collaborator

intgr commented Jan 13, 2023

If all you want is the Model type, not the correct subclass of Model, then we could use type[Model] as return, no need for this complexity with TypeVar.

But you usually need the correct Model subclass, for example User, with its managers an fields etc. Right now you can use model_class: type[User] = apps.get_model(...), but if you make this return type[Model] then you would always need an explicit cast() or assert isinstance().

It used to be type[Model], but was explicitly reverted with a comment in #109:

d53121b#diff-90641780bd3e00fc85922341c39871903999c6e562992cfc2b065550de1debafL26-R28

I'm personally undecided whether type[Any] or type[Model] is better. Maybe @mkurnikov as author of the PR can chime in on why the former was preferred.

@mkurnikov
Copy link
Member

This specific annotation could be automatically generated, or I just put something in as I didn't focus much of this specific method.

Proper implementation should have custom support from the codebase, it's not typeable just from annotations. See get_user_model() support for example, it has similar properties.

@intgr intgr changed the title Update registry.pyi Update Apps.get_model() return type Jan 13, 2023
@mkurnikov
Copy link
Member

For the PyCharm: interop with PyCharm is extremely hard without close collaboration, they have all kind of special-casing for the models inside the plugin. So do we. If we want to support it properly, we need to design it together, let's maybe ask
@vlasovskikh ?

@false-vacuum
Copy link
Author

If all you want is the Model type, not the correct subclass of Model, then we could use type[Model] as return, no need for this complexity with TypeVar.

But you usually need the correct Model subclass, for example User, with its managers an fields etc. Right now you can use model_class: type[User] = apps.get_model(...), but if you make this return type[Model] then you would always need an explicit cast() or assert isinstance().

It used to be type[Model], but was explicitly reverted with a comment in #109:

d53121b#diff-90641780bd3e00fc85922341c39871903999c6e562992cfc2b065550de1debafL26-R28

I'm personally undecided whether type[Any] or type[Model] is better. Maybe @mkurnikov as author of the PR can chime in on why the former was preferred.

If I'm understanding pep484 correctly, the problem with type[Model] in this case is that by default type declarations are considered invariant.
So if you wanted to do something like this:

def some_function(model: SomeModel):
   pass

cls = apps.get_model('aws_manager', 'SomeModel')

some_function(cls)   # type check error here

The type checker would report: "Expected type 'SomeModel', got 'Type[Model | Model]' instead" when calling some_function(cls).

Using TypeVar with the bound parameter tells the type checker that the return type can be a subclass of Model and not just Model. I'm not sure if this is the only way to do this, but it does seem correct.

That being said, type[Model] would definitely be preferred over type[Any], because it at least it would allow you to access the manager without reporting an error.

@intgr
Copy link
Collaborator

intgr commented Jan 19, 2023

I did some tests with this on mypy and PyCharm.

type[TypeVar(...)] is an invalid construction. Mypy doesn't report this as error, but PyCharm does:

image

You can tell that mypy doesn't handle it correctly because it doesn't report errors here, it's being treated as Any:

from django.apps.registry import Apps

def takes_int(value: int) -> None:
    pass

def bla(apps: Apps) -> None:
    model_type = apps.get_model("auth", "User")
    reveal_type(model_type)  # Mypy: note: Revealed type is "ModelType?"
    takes_int(model_type)  # <-- This should be error, but no error is reported
    model_type.no_such_attribute = 123  # <-- No error reported

If I change it like this:

-ModelType = type[TypeVar("ModelType", bound=Model)]
+ModelType = TypeVar("ModelType", bound=type[Model])

Then mypy actually does something slightly better, it asks for an explicit type hint for model_type variable:

def bla(apps: Apps) -> None:
    model_type = apps.get_model("auth", "User")  # Mypy: error: Need type annotation for "model_type"
    reveal_type(model_type)  # Mypy: note: Revealed type is "Any"
    takes_int(model_type)  # <-- Still no error reported

But I can hint it as model_type: int and it will just accept it without any errors 🤷

def bla(apps: Apps) -> None:
    model_type: int = apps.get_model("auth", "User")  # <-- No error
    reveal_type(model_type)  # Mypy: note: Revealed type is "builtins.int"
    takes_int(model_type)  # <-- Also no error

Arguably it should infer the type as type[Model] from TypeVar's bound and report it as incompatbile with int. But that's not what happens right now. For TypeVars to be useful, there needs to be a function parameter using it also: https://mypy.readthedocs.io/en/stable/generics.html#generic-functions

For now, all of these seem to boil down to being equivalent to type[Any].


Is there anything I'm missing?

@intgr
Copy link
Collaborator

intgr commented Jan 19, 2023

As for why this change causes PyCharm to offer better autocompletion suggestions, I don't know, but it doesn't seem to affect the type checking.

@intgr intgr added the stale Pull requests that have been out of date or unmergeable for a long time. label Feb 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stale Pull requests that have been out of date or unmergeable for a long time.
Development

Successfully merging this pull request may close these issues.

Too broad return value for apps.get_model()
3 participants