-
-
Notifications
You must be signed in to change notification settings - Fork 480
Feat/builtin models types #2590
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
base: master
Are you sure you want to change the base?
Feat/builtin models types #2590
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.
Thanks, this is not a full review.
Resolved most of the comments so far Additionally wanted to ask do I go ahead and also add types for the Edit: Added types and tests |
@sobolevn I think we should be all done, should we also update the
|
|
Thats because of the extension too, here is how the extension handles nulls, first it checks if the expression is nullable then based on that it fetches set_type, get_type = get_field_descriptor_types(
default_return_type.type,
is_set_nullable=is_set_nullable or is_nullable,
is_get_nullable=is_get_nullable or is_nullable,
) after that it fetches mapped types which is the types if generic params are provided and replace the default types gotten above with the mapped types # bail if either mapped_set_type or mapped_get_type have type Never
if not (isinstance(mapped_set_type, UninhabitedType) or isinstance(mapped_get_type, UninhabitedType)):
# always replace set_type and get_type with (non-Any) mapped types
set_type = helpers.convert_any_to_type(mapped_set_type, set_type)
get_type = get_proper_type(helpers.convert_any_to_type(mapped_get_type, get_type)) the reason it works currently is because mapped types are from django.db import models
from datetime import date as Date
from typing_extensions import reveal_type
class MyUser(models.Model):
date = models.DateField(null=True)
my_date = models.DateField[Date, Date](null=True)
u = MyUser()
reveal_type(u.date) # unknown for pyright/pylance, date | None for mypy
reveal_type(u.my_date) # type date for both
As you can see if types are mapped the default expression types are overwritten which is whats happening since all our fields are now inherently mapped with generic type params So to fix this I think we can just make the mapped types optional too if the field is nullable |
@sobolevn any thoughts on this? should we have the plugin enforce null for us if |
We 100% should, but I am pretty sure that it already works. If it does not, please open a new issue. |
It works with the current fields types because the generic parameters are unknown as soon as you add those generic type parameters it overwrites the nullable expression with the generic parameters provided instead, which what's happening here since each field now have specified generic params such as I'll open a new issue regarding this and reference it here |
Sorry, I missed this message #2590 (comment) Reading! |
Oh, now I see: |
But currently our internal fields are all typed with default types meaning our stubs already defines
The only cornercase in soln 2 (preferred to reduce redundancy) is that if user defines explicit typehints our plugin may override that and make it nullable if null=True is defined (imo thats expected behaviour) since currently I dont think there is a way to tell if the generic type params are coming from django-stubs types or user codebase, due to this raising an error might not work unless we can determine whether these explicit type hints are from stubs or user maybe we can use inspect.getFile() or |
One thing I noticed is that if the generic types are user defined the following mypyc attrs are filled or populated in the mypyc plugin, information such a Not sure how foolproof or reliable this method is any thoughts @sobolevn ? https://pastebin.com/7MMsJTMs Here is an example where the types were user defined, you see that line no. and column is populated, whereas in stubs it would just show -1 for both
from django.db import models
from typing_extensions import reveal_type
class TestModel(models.Model):
my_field = models.CharField[str, str](null=True)
t = TestModel()
reveal_type(t.my_field)
from django.db import models
from typing_extensions import reveal_type
class TestModel(models.Model):
my_field = models.CharField(null=True)
t = TestModel()
reveal_type(t.my_field) Difference between the 2 different types of type definition can be seen here https://www.diffchecker.com/0ujD9s30/ |
@sobolevn any ideas? What are your thoughts on the different options we can approach with in regard to this, I feel like using some meta properties from mypyc might be a bit too hacky of a soln? We could alternatively just make them nullable regardless of where the generic params are coming from and users can force a typing.cast if they are sure it's not nullable via virtue of their code, since django internals wise the model field typing returned is accurate if |
Couple of thoughts:
class A[T]:
def __init__(self, x: T) -> None:
self.x = x
A[int]('a') https://mypy-play.net/?mypy=latest&python=3.12&gist=ccb14fd0cc0bf9711ba4d513c16adf5a
|
@sobolevn I think there is a misunderstanding here check this mypy example for context: What I am trying to say is as of now the mypy plugin uses the fields
This works well and good since currently the mapped types for generics are always Unknown or Never, since we didn't provide the generic params before. In which case currently the types are remapped or changed to the mapped get/set types, if the user ever provides them in their code since they take priority. django-stubs/mypy_django_plugin/transformers/fields.py Lines 166 to 169 in c37850b
Now I completely agree with you that above cases as you mentioned should raise errors, but right now the dilemma is that we are not using the default get/set types (i.e ones inferred from
default= parameter, so the issue rises in the following points:
class A(models.Model):
text_nullable = models.CharField(null=True)
a = A()
# So now this is just str instead of str | None
reveal_type(a.text_nullable) The above is basically the main issue that we are encountering, since we need to make our own inbuilt mapped types nullable somehow via the plugin. Which imo is a valid use case for the plugin, but as a side effect if we make this decision this can give rise to the following specific edge case
class A(models.Model):
# No reliable way to tell if the mapped types are from user code here, or from the stubs .pyi file
text_nullable = models.CharField[str, str](null=True) # wrong typehint we should raise an error
a = A()
reveal_type(a.text_nullable) I hope this clears it up a bit this is kind of a chicken and egg situation 😅 |
It feels like we should not use |
This isn't really a @intgr @flaeppe you folks have had some prior experience with this in #2214 any ideas on how to approach this? |
One solution to this can be to use from __future__ import annotations
import typing as t
_ST = t.TypeVar("_ST")
_GT = t.TypeVar("_GT")
class Field(t.Generic[_ST, _GT]):
@t.overload
def __new__(cls, *, null: t.Literal[True]) -> Field[_ST | None, _GT | None]:
...
@t.overload
def __new__(cls, *, null: t.Literal[False] = False) -> Field[_ST, _GT]:
...
def __new__(cls, *, null: bool = False) -> Field[_ST, _GT] | Field[_ST | None, _GT | None]:
return super().__new__(cls)
def __init__(self, *, null: bool = False) -> None:
super().__init__()
_ST_CharField = t.TypeVar("_ST_CharField", default=str | int)
_GT_CharField = t.TypeVar("_GT_CharField", default=str)
class CharField(Field[_ST_CharField, _GT_CharField]):
...
x = CharField(null=False)
reveal_type(x)
y = CharField(null=True)
reveal_type(y)
z = CharField()
reveal_type(z) edit: This seems to work with pyright but not mypy python/mypy#15220 😭 , also this one python/mypy#17251, If we change the order of methods and redefine the overloads locally for each field individually mypy seems to work (this is due to mypy using mro to pick between init and new for return types 😭 ) Overall after a bunch of edits this works on both mypy and pyright, https://mypy-play.net/?mypy=latest&python=3.12&gist=1f9176a8c2bef850b4e9a5d9af2e8211 only downside is |
I have made things!
This PR aims to add type hints to builtin model fields, i.e for example models in
contrib
,admin
,auth
etc.Base generic fields are modified to use the
default=
from PEP-696 to allow the following behaviour for modelsIt eliminates the need to add generic arguments explicitly at each step whenever defining models, and ensures all the models being used internally have type hints.
This PR is not complete yet as I have a few questions to ask as I am not entirely clear about the whole process here are a few points I had doubts on, and wanted some feedback before proceeding
Is there any particular reason models forcontrib/gis/db/backends
are typed Any and not included inallowlist_todo.txt
, since they seem to have types here, should I type out the model fields as suchedit: models typed in line with source
I noticed the presence of most of the inbuilt model fields and methods inallowlist_todo.txt
so how would that be handled do I write tests under theassert_type
andtypecheck/contrib
folder for these models and move them toallowlist.txt
manually if they are type hinted and tested?edit: Resolved created a category
Final question, there is one case where the current model field typing system might need some explicit type hinting from the user in case of where fields become optional or withnull=True
, because with current system I don't think there is a way to infer types from the field parameters passed, Here is an exampleedit: Opening a new issue referencing this
This is fixable if we modify the
django-mypy-plugin
code but not sure if thats the best option, here is how one might go about itThis was the only way I could think of going about it, and user needs to have the plugin installed.
Related issues
TODO
contrib/gis/backends
models (most probably)