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

Fix crash when constructing an abstract model with recursive relation #1393

Merged
merged 19 commits into from
Aug 9, 2023

Conversation

zuzelvp
Copy link
Contributor

@zuzelvp zuzelvp commented Mar 13, 2023

I have made things!

We have abstract django model with a recursive relation. When we added code using one of its child django db models mypy crashed with the following trace

  /Users/jimmylai/workspace/carta-web/mypy/checkexpr.py(4821)accept()
  /Users/jimmylai/workspace/carta-web/mypy/nodes.py(1891)accept()
  /Users/jimmylai/workspace/carta-web/mypy/checkexpr.py(425)visit_call_expr()
  /Users/jimmylai/workspace/carta-web/mypy/checkexpr.py(545)visit_call_expr_inner()
  /Users/jimmylai/workspace/carta-web/mypy/checkexpr.py(1197)check_call_expr_with_callee_type()
  /Users/jimmylai/workspace/carta-web/mypy/checkexpr.py(1280)check_call()
  /Users/jimmylai/workspace/carta-web/mypy/checkexpr.py(1490)check_callable_call()
  /Users/jimmylai/workspace/carta-web/mypy/checkexpr.py(1020)apply_function_plugin()
  /Users/jimmylai/miniconda3/lib/python3.8/site-packages/mypy_django_plugin/transformers/init_create.py(63)redefine_and_typecheck_model_init()
-> return typecheck_model_method(ctx, django_context, model_cls, "__init__")
  /Users/jimmylai/miniconda3/lib/python3.8/site-packages/mypy_django_plugin/transformers/init_create.py(38)typecheck_model_method()
-> expected_types = django_context.get_expected_types(typechecker_api, model_cls, method=method)
  /Users/jimmylai/miniconda3/lib/python3.8/site-packages/mypy_django_plugin/django/context.py(201)get_expected_types()
-> ) or self.get_field_set_type(api, field, method=method)
  /Users/jimmylai/miniconda3/lib/python3.8/site-packages/mypy_django_plugin/django/context.py(290)get_field_set_type()
-> target_field = field.target_field
  /Users/jimmylai/miniconda3/lib/python3.8/site-packages/django/db/models/fields/related.py(878)target_field()
-> return self.foreign_related_fields[0]
  /Users/jimmylai/miniconda3/lib/python3.8/site-packages/django/db/models/fields/related.py(632)foreign_related_fields()
-> return tuple(rhs_field for lhs_field, rhs_field in self.related_fields if rhs_field)
  /Users/jimmylai/miniconda3/lib/python3.8/site-packages/django/db/models/fields/related.py(619)related_fields()
-> self._related_fields = self.resolve_related_fields()
> /Users/jimmylai/miniconda3/lib/python3.8/site-packages/django/db/models/fields/related.py(604)resolve_related_fields()
-> raise ValueError('Related model %r cannot be resolved' % self.remote_field.model)

After some investigation we tracked the issue to a field.targer_field call with a reflective field on an abstract django model for which django raises a ValueError https://github.com/django/django/blob/main/django/db/models/fields/related.py#L712

Noticed you are also skipping getting the type for the primary key when the django model is abstract so doing something similar for these fields.

Related issues

Did not find any already created

Copy link
Member

@sobolevn sobolevn left a comment

Choose a reason for hiding this comment

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

Can you please add a simple test case for that in https://github.com/typeddjango/django-stubs/blob/master/tests/typecheck/models/test_related_fields.yml

Other than that I am fine with the change. Thank you!

@zuzelvp
Copy link
Contributor Author

zuzelvp commented Mar 14, 2023

@sobolevn can you run test suite on #1394 to see if it fails before the fix?

@zuzelvp
Copy link
Contributor Author

zuzelvp commented Mar 14, 2023

Noticed some tests are failing. Investigating

@sobolevn
Copy link
Member

Yes, sure!

@intgr
Copy link
Collaborator

intgr commented Apr 7, 2023

Hi! Are you planning to continue this work?

@zuzelvp zuzelvp closed this Apr 7, 2023
@intgr
Copy link
Collaborator

intgr commented Apr 8, 2023

Thanks for the effort so far. If I understand correctly, this PR intended to add support for ManyToManyField("self"), ForeginKey("self") for abstract models, which would currently crash our plugin?

If you don't have time to finish this, hopefully someone else can pick it up and finalize it. Maybe @flaeppe is interested, no pressure though. :)

It would be appreciated if you briefly describe the current state of the PR.

@intgr intgr reopened this Apr 8, 2023
@intgr intgr added the help wanted Extra attention is needed label Apr 8, 2023
@zuzelvp
Copy link
Contributor Author

zuzelvp commented Apr 13, 2023

TODO: add a test that fails without the fix and passes with it and does not break the whole test suite. Got busy at work so had to pause this work.

@@ -195,6 +195,9 @@ def get_field_set_type_from_model_type_info(info: Optional[TypeInfo], field_name
for field in model_cls._meta.get_fields():
if isinstance(field, Field):
field_name = field.attname
# Can not determine target_field for recursive relationship when model is abstract
if field.related_model == "self" and model_cls._meta.abstract:
continue
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the main portion of the fix. The rest was trying to get typing to work now that model_cls can be a string.

@@ -7,6 +7,7 @@
reveal_type(Model2().test) # N: Revealed type is "app3.models.Model3_RelatedManager"
reveal_type(Model1().test2) # N: Revealed type is "app3.models.Model4_RelatedManager"
reveal_type(Model2().test2) # N: Revealed type is "app3.models.Model4_RelatedManager"
Model1()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This does not seem to fail without the fix so need a better test #1394

@flaeppe
Copy link
Member

flaeppe commented Jul 11, 2023

I rebased, figured out and added a breaking test in fb92aec.

The rebase was a bit messy, gonna try to resolve it. I also realised that we the plugin could yield an error when trying to instantiate an abstract model. As Django raises a TypeError attempting to do that.

@@ -362,7 +365,7 @@ def get_field_related_model_cls(self, field: Union["RelatedField[Any, Any]", For
def _resolve_field_from_parts(
self, field_parts: Iterable[str], model_cls: Type[Model]
) -> Union["Field[Any, Any]", ForeignObjectRel]:
currently_observed_model = model_cls
currently_observed_model: type[Model] | Literal["self"] = model_cls
Copy link
Member

Choose a reason for hiding this comment

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

I'm not certain the added annotation here is correct, it only introduces a theoretical Literal["self"] while call sites of _resolve_field_from_parts still passes model_cls: Type[Model].

So I'm not sure how currently_observed_model would then end up being a string in this context..

If anything it should be checked whether any of the existing call sites could end up with Type[Model] | Literal["self"]. Unless I'm missing something, none of the existing call sites for _resolve_field_from_parts is part of the provided traceback, which makes me slightly more interested in not changing anything here until we've encountered something being wrong.

@flaeppe
Copy link
Member

flaeppe commented Jul 11, 2023

@intgr In my eyes, this looks ready now as long as CI doesn't say otherwise. Sorry for being slow to look into this

first = Concrete.objects.create(parent=None)
Concrete.objects.create(parent=first)
Recursive.objects.create(parent=None)
Recursive(parent=Recursive(parent=None)) # E: Unexpected attribute "parent" for model "Recursive"
Copy link
Member

Choose a reason for hiding this comment

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

For the curious one; it was initialisation of an abstract model(i.e. this line) that raised the error seen in the provided traceback

@flaeppe flaeppe requested a review from sobolevn July 12, 2023 12:23
@flaeppe
Copy link
Member

flaeppe commented Aug 9, 2023

Friendly ping @sobolevn and @intgr, would any of you mind having a second look?

@flaeppe flaeppe removed the help wanted Extra attention is needed label Aug 9, 2023
@intgr intgr self-assigned this Aug 9, 2023
@intgr intgr changed the title 500 error when abstract model has a recursive relation Fix crash when constructing an abstract model with recursive relation Aug 9, 2023
Copy link
Collaborator

@intgr intgr left a 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, just one minor tweak request.

Thanks for picking up this fix. And don't hesitate to ping me again if another PR is sitting without reviews.

mypy_django_plugin/lib/helpers.py Show resolved Hide resolved
@flaeppe flaeppe merged commit 325006c into typeddjango:master Aug 9, 2023
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants