-
-
Notifications
You must be signed in to change notification settings - Fork 453
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
Implement support for <QuerySet>.as_manager()
#1025
Implement support for <QuerySet>.as_manager()
#1025
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.
Looks good to me. Awesome work.
But, I want to have another look from someone :)
Did you have anyone in mind for a second pair of eyes? Or did you mean for me to ask someone? |
@mkurnikov or @intgr or @syastrov |
Sorry, mypy plugin code is entirely foreign to me and I don't have time right now to delve into it. |
Seems like you managed to implement support for creating these managers inline, does that mean that it's technically possible to support Oh and I'd be happy to review this, but I'm kinda swamped for the next few days so I might not get to it until Friday. |
Yes, it should be technically possible (I'm assuming class level/definition, when you say inline) as long as we're not talking about the "full" call class MyModel(models.Model):
class ManagerFromMyQuerySet(...):
...
objects = ManagerFromMyQuerySet() Felt more natural to keep force it outside of the model class definition. |
This is what I wanted. All our models currently do I'd forgotten about that extra call to initialise the model. I assume that causes mypy to not call |
Yes, exactly, for clarity, the reason it wont work with inlining the full call, seen below, is that it wont trigger the class MyModel(models.Model):
objects = Manager.from_queryset(MyQuerySet)()
Ref: https://mypy.readthedocs.io/en/stable/extending_mypy.html#current-list-of-plugin-hooks (docs for The more fortunate case for |
There are conflicts right now 😉 |
ad71834
to
086301f
Compare
Hehe, yeah. Think it's resolved now |
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 changes look very nice, I've just added some minor nits here and there. I am however seeing this when running this on our largest Django project at work:
project/users/forms/normal.py: error: INTERNAL ERROR: maximum semantic analysis iteration count reached
I'm not sure what's causing this (we're subclassing some Django builtin forms etc), but I assume it's a defer()
call somewhere. I can dig into that tomorrow if you need help reproducing it.
# type won't be defined on variable level. | ||
var = Var( | ||
name=ctx.name, | ||
type=TypeType(Instance(new_manager_info, [AnyType(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.
Why is this Any
? Shouldn't we take the model from either the queryset or the manager (and potentially validate that they match)?
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 sure, I'm kinda split on if we should display a model here. Since this is when we're outside of a model class definition, e.g.
MyManager = Manager.from_queryset(MyQuerySet)
What I'm thinking about here is what the manager type is during runtime, model won't be set until the model metaclass is called. Which is basically inside model class definition. And, while I have never used it like that, same manager could be used on multiple models..
But perhaps you're right, maybe we should require model arguments for both queryset and manager to match. Since we're basically merging those two arguments into one and the model argument exists for manager and queryset types.
Also, I think AddManagers
will change the model argument if called on a different model? But perhaps that should also 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.
I was thinking it would make sense to inspect the generics of the manager and querysets (if possible). If they are different an error message should probably be emitted and in other case use the class from either?
And I'd think that if AddManagers
sees a model argument that's not compatible with the one on the manager then an error should be emitted?
I'm okay with not adding this in this PR, but that's at least how I would expect this to behave. I see that it's really an edge case and probably not a big problem.
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.
Yeah, I've been looking in to exactly that. And it seems possible if we for .from_queryset
traverse .bases
(as those are types.Instance
, we'll get access to Instance.args
) and compare model arguments, then on mismatch emit an error.
This should probably happen for both .from_queryset
and AddManagers
, as you say.
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 a case left to decide on, when model arguments aren't populated for Queryset
or Manager
. Either:
- We emit an error and enforce population of the model arg or,
- We then allow it to be any model, thus just populate it with whatever we got
I'm leaning towards the latter, since one can switch on enforcing via mypy config: disallow_any_generics = true
.
For the first case, we could then establish some sort of "auto-fill". Depending on if 1 of the model arguments are populated, if both are missing .from_queryset
results in ManagerFromQueryset[Any]
while AddManagers
just populates it with the model it's processing.
This is basically the table:
- Manager and queryset model argument populated -> error if mismatch for both
.from_queryset
andAddManagers
- Only manager model argument populated -> Populate queryset model argument with manager model argument
- Only queryset model argument populated -> Populate manager model argument with queryset model argument
- No argument populated ->
.from_queryset
setsAny
andAddManagers
populates with the model it's processing.
I think this aligns with when using Django builtins e.g. BaseManager.from_queryset(CustomQuerySet)
, since otherwise we'll enforce people to write custom managers to populate the argument, even though they only have/need a queryset defined. Like this:
class SuperFluousManager(BaseManager[MyModel]):
...
What do you 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.
Yeah, that sounds exactly like what I'd expect 👍
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've been looking in to this for a bit and think we should push for it to a future PR, as it's quite an isolated addition but generates quite a bit of lines. And the changes we have here are already large enough.
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've pushed progress to a PR here: flaeppe#29
Thanks for the review. It'd be very helpful if you could find a repro, I have no idea where to start looking for it really. |
I dug a bit into the mypy code yesterday and from what I could see the problem isn't guaranteed to be in the file it reports, so I think this is related to something else. The curious thing is that we don't have any I'll try to extract just the "refactoring" bit of this PR and see if it reproduces with just that at least, but I'm struggling to understand what causes it. It does happen during processing of the majority of our models though. Running |
I commented out the new |
Interesting, could it be related to the new bailing out when django-stubs/mypy_django_plugin/transformers/managers.py Lines 274 to 277 in c232226
|
I'm pretty sure it's not related to that. I've had that line there unrelated to this and that worked fine. I'll try to pick the PR apart and test it bit by bit next week to see if I can figure it out. |
I think it might be related to the name changes for generated managers. I tried to do something similar while working on #1045 and quickly got the same problem in a test repo. Should probably be solvable, but maybe we could try without those changes and do it in a separate PR? |
Sure, we could drop the name change. I ran in to it too when fiddling with flaeppe#29 Got stuck in AddManagers with a metadata key that didn't match the runtime manager name for a dynamic manager. I think you should get a trace to the model definition it gets stuck on? Anything special happening with it? |
Yeah, I get a trace, but it ends in a random form class (for our user model, which I guess subclasses a few |
Yeah, it's related to the manager name. Changing that logic back fixes the issue. I wonder if it might be because the name already exists in the module? diff --git a/mypy_django_plugin/transformers/managers.py b/mypy_django_plugin/transformers/managers.py
index 6aa00c8..3e9b8fe 100644
--- a/mypy_django_plugin/transformers/managers.py
+++ b/mypy_django_plugin/transformers/managers.py
@@ -318,20 +318,20 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
symbol_kind = semanal_api.current_symbol_kind()
# Annotate the module variable as `<Variable>: Type[<NewManager[Any]>]` as the model
# type won't be defined on variable level.
- var = Var(
- name=ctx.name,
- type=TypeType(Instance(new_manager_info, [AnyType(TypeOfAny.from_omitted_generics)])),
- )
- var.info = new_manager_info
- var._fullname = f"{semanal_api.cur_mod_id}.{ctx.name}"
- var.is_inferred = True
+ # var = Var(
+ # name=ctx.name,
+ # type=TypeType(Instance(new_manager_info, [AnyType(TypeOfAny.from_omitted_generics)])),
+ # )
+ # var.info = new_manager_info
+ # var._fullname = f"{semanal_api.cur_mod_id}.{ctx.name}"
+ # var.is_inferred = True
# Note: Order of `add_symbol_table_node` calls matter. Case being if
# `ctx.name == new_manager_info.name`, then we'd like the type to override the
# `Var`, so the `Var` won't exist in the end.
- assert semanal_api.add_symbol_table_node(ctx.name, SymbolTableNode(symbol_kind, var, plugin_generated=True))
+ # assert semanal_api.add_symbol_table_node(ctx.name, SymbolTableNode(symbol_kind, var, plugin_generated=True))
# Insert the new manager dynamic class
assert semanal_api.add_symbol_table_node(
- new_manager_info.name, SymbolTableNode(symbol_kind, new_manager_info, plugin_generated=True)
+ ctx.name, SymbolTableNode(symbol_kind, new_manager_info, plugin_generated=True)
) |
Yep, that might be it. I added a test which didn't resolve that properly. |
I added a test for that for |
@ljodal I pushed an update resolving name collisions with |
Still crashing :( |
Can you please rebase this? I am open to get it merged |
I think there's some rework to get done here before merging is possible. Gonna have to get to that first, not sure when I'll be able to though. (I think it's #1045 we have to align changes here with) |
Is there anything I can help with to have it merged? About 30% of mypy violations we have in our code base are caused by this issue 🙃 We use custom QuerySet manager a lot. |
@flaeppe the PR seems quite clean and seemingly solving the issue. I can try the patch on our code base if you want to be double sure, though. What kind of rework do you see as blocking? Maybe, I can help with that. And if it's just a matter of rebasing, I can do that for you too. |
@orsinium the changes here depends the version previous to #1045. Basically Although #1045 had to adjust the same logic. So the pieces I extracted conflicts with #1045, basically they look different now. So one kind of have to go find them again and see if/how it can be extracted/shared. I'll have a look now to see if I'll figure it out |
Thank you! Lemmino if you need any help. |
d927b96
to
29b5c5b
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.
Looks good 👍 I've tested it on our codebase and it introduces no new errors. Also converted a few Manager.from_queryset(MyQuerySet)
calls and saw no new errors
- case: handles_call_outside_of_model_class_definition | ||
main: | | ||
from myapp.models import MyModel, MyModelManager | ||
reveal_type(MyModelManager) # N: Revealed type is "myapp.models.ManagerFromModelQuerySet[Any]" |
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 guess this really should produce a Manager[MyModel]
, but if I remember correctly you were thinking of fixing that in a separate PR?
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.
Yeah, exactly. I opened a branch for that in my fork, it's quite a bit of code to get it up and running I think. Probably more suitable for a subsequent PR I'd say
manager_base.metadata.setdefault("from_queryset_managers", {}) | ||
# The `__module__` value of the manager type created by Django's | ||
# `.from_queryset` is `django.db.models.manager`. But we put new type(s) in the | ||
# module currently being processed, so we'll map those together through metadata. | ||
runtime_fullname = ".".join(["django.db.models.manager", manager_name]) | ||
manager_base.metadata["from_queryset_managers"][runtime_fullname] = fullname |
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.
Shouldn't these be under the django
key?
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.
Yeah, probably. They weren't previously so I kept them the same. Will probably enforce cache clear to work properly. But that might be completely fine too.
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!
I propose to merge this PR and adjust any parts later on.
Tested the new version, and it works. For posterity, I faced a few issues along the way, but I'm not sure if they are related to this PR:
But again, this can be unrelated, I'll investigate it later. |
I've bumped it recently, please check if that's now correct. |
You're right, the constraint is correct. I think I've messed up with environments somehow. It failed with mypy 0.961 but the constraint is already |
This one is due to this: #1163. Basically you most likely import something in the settings file that imports |
I've reorganised and extended dynamic manager class generation to support
<QuerySet>.as_manager()
.This also renames the dynamically generated manager types to align with what those class names will be during runtime, set by Django. See here: https://github.com/django/django/blob/b2eff16806057095c7dd3daa9402ad615e51627f/django/db/models/manager.py#L109-L110
That means that
reveal_type(<Model>.<manager>)
should output the same type name astype(<Model>.<manager>)
(unless there's collisions etc etc.)Related issues
Closes: #324