-
-
Notifications
You must be signed in to change notification settings - Fork 450
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
Don't remove objects
attribute from Model
in plugin
#2280
base: master
Are you sure you want to change the base?
Conversation
Well, with these changes we end up with having to declare No surprise, I suppose, but probably a bunch of changes required for many. |
It actually is surprising, we shouldn't get complaints on overriding class with instance variable in all the failing tests. I think that we're missing something in the plugin, gonna dig around and see |
Yeah sure |
@sobolevn I've tried out the test, locally, but it doesn't break on I can still add it in if you like but it's unfortunately not reproducing anything |
a60eaf1
to
3366841
Compare
Test can be found in 3366841 |
# It seems that the type checker fetches a Var from expressions, but | ||
# looks through the symbol table for the type(at some later stage?). | ||
# Currently we don't overwrite the reference mypy holds from an | ||
# expression to a Var instance when adding a new node, we only overwrite | ||
# the reference to the Var in the symbol table. That means there's a | ||
# lingering Var instance attached to expressions and if we don't flip | ||
# that to a ClassVar, the checker will emit an error for overriding a | ||
# class variable with an instance variable. As mypy seems to check that | ||
# via the expression and not the symbol table. Optimally we want to just | ||
# set a type on the existing Var like | ||
# manager_node.node.type = manager_type | ||
# but for some reason that doesn't work. It only works replacing the | ||
# existing Var with a new one in the symbol table. |
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 was a strange discovery I had to battle with for some time to find any sort of workaround.
I realised that the error "overriding a class variable with an instance variable" from mypy uses the body/expressions instances e.g. AssignmentStmt.lvalues[0]
. But we don't overwrite the expressions when inserting a new Var
in any symbol table. So the checker uses the lingering reference to a Var
instance that is not the same as in the symbol table and that one doesn't set/have is_classvar
the way we want.
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 am a bit confused about new ClassVar
annotations in tests.
from django.db import models | ||
|
||
class AbstractPerson(models.Model): | ||
abstract_persons = models.Manager['AbstractPerson']() | ||
abstract_persons: ClassVar[models.Manager["AbstractPerson"]] = models.Manager['AbstractPerson']() |
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.
Why do we need this change?
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.
Looked around a bit and It comes from the plugin no longer forcing is_classvar = True
on an all manager Var
s
I don't think we should need to set ClassVar
explicitly either, but mypy otherwise gives us an error on attribute access
main:2: error: Access to generic instance variables via class is ambiguous [misc]
Not a big thing that the plugin forces is_classvar = True
, but optimally I don't think it should be necessary. Not sure what to make of the error though, I don't see why it's a generic instance variable..
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.
Right, I think I've found the problem. Found that one exception that avoids the error accessing generic instance variable via class is "Self type wrapped in ClassVar"
But our plugin blindly adds Self
as a generic argument to a manager, regardless of is_classvar=True
or not.
django-stubs/mypy_django_plugin/transformers/querysets.py
Lines 42 to 54 in 972a4e5
def determine_proper_manager_type(ctx: FunctionContext) -> MypyType: | |
default_return_type = ctx.default_return_type | |
assert isinstance(default_return_type, Instance) | |
outer_model_info = helpers.get_typechecker_api(ctx).scope.active_class() | |
if ( | |
outer_model_info is None | |
or not outer_model_info.has_base(fullnames.MODEL_CLASS_FULLNAME) | |
or outer_model_info.self_type is None | |
): | |
return default_return_type | |
return helpers.reparametrize_instance(default_return_type, [outer_model_info.self_type]) |
Which means that the error will appear for e.g. Manager[Self]
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 suppose there's 2 ways out to keep working with Self
:
- Force
is_classvar = True
- Don't add a type argument
Self
whenis_classvar = False
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 went with alternative 1. and we're forcing ClassVar, as that's what we did previously. That should leave those parts unchanged I'm thinking.
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.
Well, now I just found that I went down this exact rabbit hole before: #1649 (comment)
Had completely forgotten about that..
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.
Let's also wait for @intgr's review. This part is very complex.
Thanks a lot for your work!
c7a4ae4
to
030a217
Compare
Without passing on an opinion about this PR, and not at all any kind of blocker, I just wanna take a moment and quietly point out the way I've been resolving this problem at my workplace (6 million line django app with over 2000 models, still on mypy 1.4, django 4.1) is I wrote this https://github.com/delfick/extended-mypy-django-plugin so I can say |
It sounds interesting, feel free to open any discussions/issues as you feel relevant |
I like the idea of @delfick please, feel free to discuss / backport any features / ideas. I would be happy to help! |
@sobolevn it's more about saying "I want the concrete models that extend this abstract model". Especially useful when statically you know about more models than are available at runtime. I'll expand when I get to making a github issue. I wanted to get us onto newer versions of mypy/django/django-stubs before I did that because of how long this process is taking. Hopefully not too much longer now (also figuring out how to make the mypy plugin took an astounding amount of churn to make it so it works without causing problems) though, rereading your comment, I get what you mean. I could be wrong but my instinct would be mypy wouldn't make that possible without enhancements to what hooks are available. |
Partially reverts #1672
#2276 started looking through the mro for manager attributes, but that also conflicts with
mypy_django_plugin.transformers.models.MetaclassAdjustments
, which removesobjects
fromdjango.db.models.Model
. Because if a lookup through the mro is done beforeMetaclassAdjustments
has run, the plugin fools itself that theobjects
attribute exists.I figured that reverting the removal of
objects
had most value, since most people use the default behaviour and it seems that not having anobjects
is quite rare.It's currently only our plugin users that gets to follow along Django implementing the default manager attribute as an abstract attribute. But instead of trying to provide our home built one I think we should wait for any built in support of abstract attributes and then see if we can get any last pieces to work via the plugin.
Related issues
ClassVar
s #1672objects
attribute toToken
djangorestframework-stubs#644 (comment)typing.Abstract
to allow hinting abstract attributes python/cpython#106248type[Model]
no longer allows access to.objects
indjango-stubs
4.2.4 #1684