Skip to content
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

Demonstrate bug where related_name is not working properly when using… #73

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

mcosti
Copy link

@mcosti mcosti commented Jun 19, 2023

 File "/Users/mcosti/dev/django-typed-models/env/bin/activate/lib/python3.9/site-packages/django/apps/registry.py", line 433, in do_pending_operations
    function(model)
  File "/Users/mcosti/dev/django-typed-models/env/bin/activate/lib/python3.9/site-packages/django/apps/registry.py", line 411, in apply_next_model
    self.lazy_model_operation(next_function, *more_models)
  File "/Users/mcosti/dev/django-typed-models/env/bin/activate/lib/python3.9/site-packages/django/apps/registry.py", line 397, in lazy_model_operation
    function()
  File "/Users/mcosti/dev/django-typed-models/env/bin/activate/lib/python3.9/site-packages/django/db/models/fields/related.py", line 373, in resolve_related_class
    field.do_related_class(related, model)
  File "/Users/mcosti/dev/django-typed-models/typedmodels/models.py", line 108, in do_related_class
    old_do_related_class(other, cls)
  File "/Users/mcosti/dev/django-typed-models/env/bin/activate/lib/python3.9/site-packages/django/db/models/fields/related.py", line 448, in do_related_class
    self.set_attributes_from_rel()
  File "/Users/mcosti/dev/django-typed-models/env/bin/activate/lib/python3.9/site-packages/django/db/models/fields/related.py", line 445, in set_attributes_from_rel
    self.remote_field.set_field_name()
  File "/Users/mcosti/dev/django-typed-models/env/bin/activate/lib/python3.9/site-packages/django/db/models/fields/reverse_related.py", line 306, in set_field_name
    self.field_name = self.field_name or self.model._meta.pk.name
AttributeError: 'str' object has no attribute '_meta'

@mcosti
Copy link
Author

mcosti commented Jun 19, 2023

The culprit lives somewhere here

                if isinstance(field, models.fields.related.RelatedField):
                    # Monkey patching field instance to make do_related_class use created class instead of base_class.
                    # Actually that class doesn't exist yet, so we just monkey patch base_class for a while,
                    # changing _meta.model_name, so accessor names are generated properly.
                    # We'll do more stuff when the class is created.
                    old_do_related_class = field.do_related_class

                    def do_related_class(self, other, cls):
                        base_class_name = base_class.__name__
                        cls._meta.model_name = classname.lower()
                        old_do_related_class(other, cls)
                        cls._meta.model_name = base_class_name.lower()

                    field.do_related_class = types.MethodType(do_related_class, field)
                if isinstance(field, models.fields.related.RelatedField):
                    remote_field = field.remote_field
                    if (
                        isinstance(remote_field.model, TypedModel)
                        and remote_field.model.base_class
                    ):
                        remote_field.limit_choices_to[
                            "type__in"
                        ] = remote_field.model._typedmodels_subtypes

@mcosti
Copy link
Author

mcosti commented Jun 20, 2023

This is fixed now.
I added a print in RelatedFIeld.resolve_related_class

Without my changes:

<class 'testapp.models.UniqueIdentifier'> <class 'django.contrib.contenttypes.models.ContentType'> testapp.UniqueIdentifier.content_type
<class 'testapp.models.Animal'> <class 'testapp.models.UniqueIdentifier'> testapp.Animal.unique_identifiers
<class 'testapp.models.Canine'> <class 'testapp.models.UniqueIdentifier'> testapp.Canine.unique_identifiers
<class 'testapp.models.Feline'> <class 'testapp.models.UniqueIdentifier'> testapp.Feline.unique_identifiers
<class 'testapp.models.BigCat'> <class 'testapp.models.UniqueIdentifier'> testapp.BigCat.unique_identifiers
<class 'testapp.models.Animal'> <class 'testapp.models.Canine'> testapp.Animal.canines_eaten
<class 'testapp.models.Animal_canines_eaten'> <class 'testapp.models.Animal'> testapp.Animal_canines_eaten.animal
<class 'testapp.models.Animal_canines_eaten'> <class 'testapp.models.Canine'> testapp.Animal_canines_eaten.canine
<class 'testapp.models.AngryBigCat'> <class 'testapp.models.UniqueIdentifier'> testapp.AngryBigCat.unique_identifiers
<class 'testapp.models.Parrot'> <class 'testapp.models.UniqueIdentifier'> testapp.Parrot.unique_identifiers
<class 'testapp.models.Parent'> <class 'testapp.models.Parent'> testapp.Parent.b
<class 'second_testapp.models.BaseReview'> <class 'testapp.models.Product'> second_testapp.BaseReview.product

With the changes (respecting the same way Django deals with these things):

<class '__fake__.Animal_canines_eaten'> <class '__fake__.Animal'> testapp.Animal_canines_eaten.animal
<class '__fake__.Animal_canines_eaten'> <class '__fake__.Canine'> testapp.Animal_canines_eaten.canine
<class '__fake__.Animal'> <class '__fake__.Canine'> testapp.Animal.canines_eaten
<class '__fake__.Parent'> <class '__fake__.Parent'> testapp.Parent.b
<class '__fake__.BaseReview'> <class '__fake__.Product'> second_testapp.BaseReview.product
<class '__fake__.BaseReview'> <class '__fake__.Order'> second_testapp.BaseReview.order
<class '__fake__.Order'> <class '__fake__.Product'> testapp.Order.product

@craigds if you could please have a look that would be amazing. Thank you

@@ -0,0 +1,22 @@
#!/usr/bin/env python
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file doesn't seem to be required

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought it would be easier for future people to create migrations :D But I can remove it

Comment on lines +454 to +457
"""Regression test for the following scenario:
A subclass of a typed model has foreign key to two models in a different app, while they are also related
to each other.
"""
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Regression test for the following scenario:
A subclass of a typed model has foreign key to two models in a different app, while they are also related
to each other.
"""
"""
Regression test for the following scenario:
* A typed model has a ForeignKey or OneToOneField to a model in another app (lazily referenced via a string model name)
* The ForeignKey or OneToOneField has a `related_name` specified.
Previously, the related_name argument was not initialized correctly during startup, and threw an AttributeError.
In this test, the fixed behaviour is tested via the `Order.order_review` and `Product.reviews` attributes.
"""

I'm still trying to get my head around what the issue is - is this an accurate description of what's going on?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it important that the ForeignKey/OneToOneField is in another app, or would this happen with related models even in the same app if they used lazy/string references to each other?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description is accurate and indeed, the models being in a different app is what triggers the error. I encountered it in my project and this was the minimum demonstration possible.

@@ -453,6 +443,24 @@ def _get_unique_checks(self, exclude=None, **kwargs):
return unique_checks, date_checks


def _get_related_class_function(base_class, classname, field):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't seem to figure out what's changed – other than a bit of rearrangement this looks like the same code as before. Can you explain how this fixes the bug?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be honest with you:
I am not entirely sure why this works. Before the new monkey patched method was bound to the metaclass itself. Somehow this was affecting the model discovery, but to be fair, I came to this solution via a bit of trial and error.

It definitely makes a difference. If you want, you can undo my change and have a look at the error

@mcosti
Copy link
Author

mcosti commented Aug 31, 2023

Hey @craigds . Any updates? This no longer impacts me as I've removed the depdency, but I think it's still a good QoL improvement

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants