-
-
Notifications
You must be signed in to change notification settings - Fork 501
Fix plugin #1030
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?
Fix plugin #1030
Conversation
…ethods. When we inherit from `models.Manager` without explicit Any, it is assumed to be missing tvar instead
Sounds like this includes quite a bit of changes discussed very recently in:
To get a better view could, at least, the linting changes be broken out to 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.
Python code is reformatted to 88 chars line length. I don't really insist on this style, but it is really inconvenient to read 120 chars with 2-3 columns layout (and it is useful to see two files or code + test). 88 is black's default and looks better, I think. Stub files remain formatted to 120 chars.
I agree, but, please, let's keep our history consistent and our diffs as minimal as possible.
I don't like this style, but I've learned that things above ^ are more important (I did it the hard way).
So, let's keep things as they are.
Removed black from requirements.txt, because we don't use it outside of pre-c
Sounds good, but let's do it in a separate PR.
Okay, I'll split this into separate PR's now, one problem per each. Any ideas where does the difference with 3.8 and 3.10 originate? One failing test that wants more annotations, I don't quite get the reason. 3.10 (local) runs without failures. |
I tried these changes out locally and got into trouble when chaining queryset/manager methods from Django's manager with custom queryset methods. What I tried on one of our projects was: MyModel.objects.select_for_update().custom_queryset_method() Resulted in these sort of errors:
But I feel like this should already be tested in our suite here? Worth noting is that the manager is created with this approach (not sure if this will be full a repro though): from django.db import models
from django.db.models.manager import BaseManager
class MyQuerySet(models.QuerySet["MyModel"]):
def custom_queryset_method(self) -> MyQuerySet:
...
MyManager = BaseManager.from_queryset(MyQuerySet)
class MyModel(models.Model):
objects = MyManager() |
@flaeppe This is not a repro, unfortunately. |
Yeah, it's not cached and not incremental. Happens each run. But currently it seems that it's only reveal_type(MyModel.objects.select_for_update) # Revealed type is "def (nowait: builtins.bool =, skip_locked: builtins.bool =, of: typing.Sequence[builtins.str] =, no_key: builtins.bool =) -> django.db.models.query._QuerySet[MyModel, MyModel]" |
One question: how?
|
Could you run |
I didn't know just yet. But now I found a repro. Problematic part, similar to #1017 (comment), is
This is a similar setup to what failed in the project I ran it on. |
Well, I figured it out! def bind_or_analyze_type(
t: MypyType, api: SemanticAnalyzer, module_name: Optional[str] = None
) -> Optional[MypyType]:
"""Analyze a type. If an unbound type, try to look it up in the given module name.
That should hopefully give a bound type."""
if isinstance(t, UnboundType) and module_name is not None:
node = api.lookup_fully_qualified_or_none(module_name + "." + t.name)
if node is not None:
if isinstance(node.node, TypeInfo):
return Instance(
node.node,
[bind_or_analyze_type(a, api, module_name) for a in t.args],
)
elif isinstance(node.node, Var):
return node.node.type
# If lookup failed or type was bound, analyze type. May be `None` too.
return api.anal_type(t) (and this is not related to |
We don't need to create custom managers when the manager isn't dynamically created. This changes the logic to just set the type of the manager on the class. There's a small regression/limitations with this approach, as demonstrated by the custom_manager_without_typevar_returns_any_model_types test case. If the manager is _not_ typed with generics (ie. `MyManager(Manager)` vs `MyManager(Manager[T])` the manager will return querysets with `Any` as the model type instead because of implicit generics. That can be solved in one of two ways: either through "reparametrization" as implemented in typeddjango#1030 possibly or through setting the methods as attributes with type `Any` as we do for dynamic managers
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 ready to me. Do others agree? :)
I think some comments marked as resolved hasn't been changed yet? e.g. #1030 (comment) |
Not ready, sorry: thanks to this comment a critical bug was found.
This produces |
Other than what's said above I'm not aware of any issues caused by this, but as I've noted previously it would be great if this was split up into multiple PRs (one for each fix). That makes it much easier to review and also if there are any issues it's much easier to identify what caused them |
That problem was related to metadata, I think it may be nice to get rid of manager metadata and rely on subclassing solely (instead of |
if not ctx.api.final_iteration: | ||
ctx.api.defer() |
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.
Is this hook called on every iteration? If so, then we're deferring on every iteration, right? A first reaction is that it feels unnecessary
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.
Should be better now
There are merge conflicts now 😞 |
If we allow
Because we create model-parametrized manager for This conflict looks quite difficult. I'll try to resolve this, but now I don't see any solution - maybe this PR will be abandoned. Should I submit separate PR with just fix for "Sequence is not defined" to fix this popular issue and release? |
Hmm, I didn't really consider that case, but technically it shouldn't really a problem. The class we create has to be generic (just like it is at runtime) and the type of I think @flaeppe has an implementation of the latter (or something very closely related here): flaeppe#29
That error is already fixed (or should be, I've not seen it since) in 84eff75, but there's at least a few other things from this PR that would be good to get merged. I don't have a full overview of exactly what's in here at the moment, but the reparametrization of manager classes is a really interesting approach and we can use it to get rid of the copying of methods (instead of trying to fix it) On a general level my experience is that it's much easier to both review and keep alive smaller PRs, so if you're able to I'd recommend trying to split this up into separate PRs. |
Unfortunately I'm 100% unable to split this into multiple chunks, because there was black change (reverted later) and trying to play with history will surely result in something broken/inconsistent.
Could you elaborate on that? After merging this is the only problem arising (I don't talk about inconsistent QuerySet parametrization now, it is a subject of separate PR and needs later discussion anyway). I'll try to recover this information from generic types now, maybe it is easier than seemed initially for me. |
Without having looked deeper. I'm fairly certain that AddManagers should be able to resolve the missing parametrization when subclassing. Or have I missed something? It currently traverses parents mapping managers at least |
There's at least #960, #1049, and #1046. Not entirely sure where all of them stem from, but at least #960 is because of the way we resolve types of methods copied to managers. We also incorrectly parametrize managers/querysets that are already parametrized. For example in this case: class MyQuerySet(QuerySet["MyModel"]):
my_custom_method(self) -> "MyQuerySet": pass When this is combined with a manager the return type is changed to Basically the |
I'm dumb, sorry. This failure is not related to declaration inside body. This is specific to my original implementation:
This is failing as well.
My implementation never reparametrizes managers that already have model parameters. Idk how this was missing in the test suite. So I'm going to fix this, thanks for the new test! It should be not this hard, I hope. |
After some investigation I think that the behavior I reported in previous comment is a correct one (and consistent with regular typing in python). Consider the following scenario:
Now this is 100% valid on Parent, but very suspicious on Child: mypy thinks that This contradicts with the fact that on runtime managers are really model-parametrized, though. IMO the most cheap solution here is to require using TypeVar's as Manager parameter unless other behavior is required. Plus, if we decide to reparametrize, we need to verify that manager is used really on model subtype only, and this is really tricky (mind TypeVar's, aliases, NewType's, recursive things which are supported now, variadic generics, ...). And this will require copying all methods to new plugin-generated Manager subclass parametrized with TypeVar or Child model, because we should not change real objects created by user (I mean we cannot just take So I ask for maintainer's opinion here: I can either resolve conflicts and prepare this to be merged again (if my arguments convinced you) or try to implement reparametrization here. There are some conflicts now, but the idea of this PR does not really interfere with these changes. |
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.
* Reparametrize managers without explicit type parameters 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. * Use typevars from parent instead of base * Use typevars from parent manager instead of base manager This removes warnings when subclassing from something other than the base manager class, where the typevar has been restricted. * Remove unused imports * Fix failed test * Only reparametrize if generics are omitted * Fix docstring * Add test with disallow_any_generics=True * Add an FAQ section and document disallow_any_generics behaviour
Bug fixes:
bind_or_analyze_type
should not checknode.type
, returning it anyway (None
too). It make sense, because if lookup succeeded, we have nothing to do to resolve the type and should defer instead..from_queryset
in class body, is used in lookup. It resulted in internal error before. The solution is to always replace this manager withAny
in such case. Rejected alternative was using default manager (models.Manager
) instead. It looks worse, because manager could override existing methods. IMO, we'd rather say "this was declared in unexpected place, so we give up and do not check calls to it" (at least because such behavior is documented in README). Surprisingly, this was not reported before.Other changes:
is interpreted as
Using explicit
Any
(class MyManager(models.Manager[typing.Any])
) preservesAny
and prevents reparametrization (including subclasses).Python code is reformatted to 88 chars line length. I don't really insist on this style, but it is really inconvenient to read 120 chars with 2-3 columns layout (and it is useful to see two files or code + test). 88 is black's default and looks better, I think. Stub files remain formatted to 120 chars.UserManager
class is made covariant by type variable (so that model subclass (ChildUser
) can safely override manager withUserManager[ChildUser]
)Couldn't resolve related manager for relation 'appointment' (from appointments.models.Appointment.appointments.Appointment.owner)
(message from my project).Removedblack
fromrequirements.txt
, because we don't use it outside of pre-commit, so it is not a real dependency.