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
Add fallback related manager in final iteration of AddRelatedManagers #993
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!
except helpers.IncompleteDefnException as exc: | ||
if not self.api.final_iteration: | ||
raise exc | ||
else: | ||
continue | ||
|
||
default_manager = related_model_info.names.get("_default_manager") |
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.
Any chance we can test 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.
I don't know if this change is correct though, i see there's some test diff as well.
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.
And i've been unable to create a test-case that fails on main but passes here.
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.
What should happen if there's no default manager? Do we need to run any of the code further down as well?
@@ -345,15 +345,19 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None: | |||
related_manager_info = self.lookup_typeinfo_or_incomplete_defn_error( | |||
fullnames.RELATED_MANAGER_CLASS | |||
) # noqa: E501 | |||
default_manager = related_model_info.names.get("_default_manager") |
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 the reason we raise an exception here if it's not found is that the code is supposed to run multiple iterations over a project to actually finish?
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.
But it never runs again.
https://gitter.im/mypy-django/Lobby?at=62a86e2ef8daa71e07cbe011. Perhaps @flaeppe can chime in, I see you've written some of this code. |
Be aware of that the traceback you've linked points to a manager in a package that isn't exporting types (i.e. there's no type hints in djmoney) I would suspect that's why there's some misbehaviour. |
yes this seems to be the case! |
The weird thing is that moneyed, the package, doesn't actually export any models that subclass django's models.Model. |
django-money is a Django extension of pymoneyed. pymoneyed has no stuff related to Django. While pymoneyed is typed and django-money isn't. I could have a quick peek on the code later on, if you want |
That would be nice. I cannot share much of the code i'm working on, but i'm really not sure as to why this interaction with Money changes what the default_manager is. |
There we have it!
i get
And the project sets up relatedmanagers correctly. |
Interesting, could it be some compatibility problem in I looked quickly through Also, are you relying on having |
Nevermind, seems that In any case, I don't think this is an issue for Might be reasonable if it we changed this here to emit an error if the manager couldn't be resolved and then attach a default manager. That way it's possible to explicitly ignore any error but still get all the django defaults of a related manager. |
if not default_manager: | ||
self.add_new_node_to_model_class( | ||
attname, Instance(related_manager_info, [Instance(related_model_info, [])]) | ||
) |
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.
What if we move this piece inside the else
block on line 351. Together with emitting an error? I think that feels most correct, as the model's manager can't be resolved, an error is displayed, and the default manager type attached still includes defaults from Django.
Emitting an error there feels quite helpful IMO, for debugging etc.
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 seems like a fair approach.
Do we have any prior art on emitting errors/warnings for certain nodes?
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.
ctx.api.fail it seems.
This seems to be it exactly (django-money creating unreferenceable default managers).
Largest downside is that one cannot use django-money toghether with django-stubs then, even though it is django-stubs that is behaving unexpectedly by patching all default managers. |
Seems reasonable. But, this will require a complete test suite! 👍 |
I think, in theory, that's quite simple to trigger? Just put a random name or whatever that can't be resolved as a model's manager? Problematic part might be that since django's setup is run, it might validate that all managers are correct.. |
I can try making a failing test where the manager is the return value of some function perhaps, so the symbol doesn't exist in scope? |
It still kind of makes sense though. Since |
That sounds like an approach that could work 👍 Might also work to monkey-patch the model class via that function (e.g. after model declaration), then we're very close to imitate what |
00443f6
to
eb3fae5
Compare
sample error. |
eb3fae5
to
ce397ef
Compare
All right! Updated with testcase (that catches the error!) and comments and descriptions, and separate error code so that i can surpress django-manager-missing on my side. |
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! Great work!
content: | | ||
from django.db import models | ||
class TimestampedModel(models.Model): | ||
id = models.UUIDField(primary_key=True, editable=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.
Let's simplify this case. Do we need id
to be explicit? Or can we remove it?
class TimestampedModel(models.Model): | ||
id = models.UUIDField(primary_key=True, editable=False) | ||
created_at = models.DateTimeField(auto_now_add=True, db_index=True) | ||
updated_at = models.DateTimeField(auto_now=True, db_index=True) |
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.
Do we need this field?
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.
Also, do we need boolean args here?
class User(TimestampedModel): | ||
name = models.TextField() | ||
|
||
def DynamicManager(): |
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 now DynamicManager
is not typed checked at all. Do we need -> None
?
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.
Nice work 👍
I added a few thoughts.
attname, Instance(related_manager_info, [Instance(related_model_info, [])]) | ||
) | ||
self.ctx.api.fail( | ||
f"Couldn't resolve related manager for relation {relation.name}, constructing a default one instead.", |
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.
Perhaps it's a bit over the top to mention that a default is used?
f"Couldn't resolve related manager for relation {relation.name}, constructing a default one instead.", | |
f"Couldn't resolve related manager for relation {relation.name}.", |
# If a django model has a Manager class that cannot | ||
# be resolved on runtime (like django-money | ||
# monkeypatches all default managers after models | ||
# already have been declared, and generates this | ||
# manager as the return value of a function, so it | ||
# cannot be imported when we try to set up the | ||
# default related managers in | ||
# AddDefaultManagerAttribute), we fallback to a | ||
# default related manager, so you at least get a | ||
# base level of working type checking. |
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.
IMO, involving django-money
in this comment is a bit too much.
# If a django model has a Manager class that cannot | |
# be resolved on runtime (like django-money | |
# monkeypatches all default managers after models | |
# already have been declared, and generates this | |
# manager as the return value of a function, so it | |
# cannot be imported when we try to set up the | |
# default related managers in | |
# AddDefaultManagerAttribute), we fallback to a | |
# default related manager, so you at least get a | |
# base level of working type checking. | |
# If a django model has a Manager class that cannot | |
# be resolved, we fallback to the default related manager, | |
# so you at least get a base level of working type checking. |
I mean, if django-money
changes behaviour this comment becomes obsolete.
myapp/models:9: error: Couldn't resolve related manager for relation booking, constructing a default one instead. | ||
myapp/models:9: error: Couldn't resolve related manager for relation bookingowner_set, constructing a default one instead. |
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.
Thought: I'm wondering if it could be valuable to try to include the path/fullname to the relation declaration as well?
It's good that the name of the reverse relation is there. But maybe it's also nice if e.g. myapp.models.Booking.x
was in the message too?
e.g.
myapp/models:9: error: Couldn't resolve related manager for relation booking, constructing a default one instead. | |
myapp/models:9: error: Couldn't resolve related manager for relation bookingowner_set, constructing a default one instead. | |
myapp/models:9: error: Couldn't resolve related manager for relation booking, constructing a default one instead. (myapp.models.Booking.renter) | |
myapp/models:9: error: Couldn't resolve related manager for relation bookingowner_set, constructing a default one instead. (myapp.models.Booking.owner) |
I think we have that context/data available when emitting, right? Otherwise just skip this.
attname, Instance(related_manager_info, [Instance(related_model_info, [])]) | ||
) | ||
self.ctx.api.fail( | ||
f"Couldn't resolve related manager for relation {relation.name}, constructing a default one instead.", |
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.
f"Couldn't resolve related manager for relation {relation.name}, constructing a default one instead.", | |
f"Couldn't resolve related manager for relation {relation.name!r}, constructing a default one instead.", |
Should wrap the name inside of: ''
. e.g. 'bookingowner_set'
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.
Not '
, but "
I also think merging this PR would close #981 as that similarly uses a package ( |
ce397ef
to
d742c31
Compare
Updated with better comments, warnings, simpler test case etc. @sobolevn. |
d742c31
to
cb4ff0c
Compare
If a django model has a Manager class that cannot be resolved statically (if it is generated in a way where we cannot import it, like `objects = my_manager_factory()`), we fallback to the default related manager, so you at least get a base level of working type checking.
cb4ff0c
to
dab26d0
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.
LGTM, great work! 👍
@flaeppe do you have any feedback? :) |
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.
LGTM too. Nice work 👍
Wohoo! What's the release schedule nowadays? |
Hm, it seems like you cannot surpress mypy errors from extensions through
|
Following this to see how this gets resolved. Having this |
If a django model has a Manager class that cannot be resolved statically
(if it is generated in a way where we cannot import it, like
objects = my_manager_factory()
), we fallback to the default related manager, soyou at least get a base level of working type checking.
Closes #981, #969