diff --git a/django-stubs/db/models/fields/__init__.pyi b/django-stubs/db/models/fields/__init__.pyi index 90f254d67..ece2144bf 100644 --- a/django-stubs/db/models/fields/__init__.pyi +++ b/django-stubs/db/models/fields/__init__.pyi @@ -3,7 +3,7 @@ import uuid from collections.abc import Callable, Iterable, Sequence from datetime import date, time, timedelta from datetime import datetime as real_datetime -from typing import Any, ClassVar, Generic, Protocol, TypeVar, overload +from typing import Any, ClassVar, Generic, Literal, Protocol, TypeVar, overload from django.core import validators # due to weird mypy.stubtest error from django.core.checks import CheckMessage @@ -122,7 +122,7 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]): primary_key: bool remote_field: ForeignObjectRel | None is_relation: bool - related_model: type[Model] | None + related_model: type[Model] | Literal["self"] | None one_to_many: bool | None one_to_one: bool | None many_to_many: bool | None diff --git a/django-stubs/db/models/fields/related.pyi b/django-stubs/db/models/fields/related.pyi index 1005a6dde..b36361d12 100644 --- a/django-stubs/db/models/fields/related.pyi +++ b/django-stubs/db/models/fields/related.pyi @@ -43,7 +43,7 @@ class RelatedField(FieldCacheMixin, Field[_ST, _GT]): rel_class: type[ForeignObjectRel] swappable: bool @property - def related_model(self) -> type[Model]: ... # type: ignore + def related_model(self) -> type[Model] | Literal["self"]: ... # type: ignore def get_forward_related_filter(self, obj: Model) -> dict[str, int | UUID]: ... def get_reverse_related_filter(self, obj: Model) -> Q: ... @property diff --git a/django-stubs/db/models/fields/reverse_related.pyi b/django-stubs/db/models/fields/reverse_related.pyi index 8e2a2b8f8..4b96e7257 100644 --- a/django-stubs/db/models/fields/reverse_related.pyi +++ b/django-stubs/db/models/fields/reverse_related.pyi @@ -51,7 +51,7 @@ class ForeignObjectRel(FieldCacheMixin): @property def target_field(self) -> AutoField: ... @property - def related_model(self) -> type[Model]: ... + def related_model(self) -> type[Model] | Literal["self"]: ... @property def many_to_many(self) -> bool: ... @property diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index 79b60053d..16b42c1b2 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -206,6 +206,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 # Try to retrieve set type from a model's TypeInfo object and fallback to retrieving it manually # from django-stubs own declaration. This is to align with the setter types declared for # assignment. diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index 35c033321..14ae1b588 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -217,7 +217,8 @@ def get_private_descriptor_type(type_info: TypeInfo, private_field_name: str, is def get_field_lookup_exact_type(api: TypeChecker, field: "Field[Any, Any]") -> MypyType: if isinstance(field, (RelatedField, ForeignObjectRel)): - lookup_type_class = field.related_model + # Not using field.related_model because that may have str value "self" + lookup_type_class = field.remote_field.model rel_model_info = lookup_class_typeinfo(api, lookup_type_class) if rel_model_info is None: return AnyType(TypeOfAny.from_error) diff --git a/tests/typecheck/models/test_abstract.yml b/tests/typecheck/models/test_abstract.yml index 7bc48ea97..dfaff3b42 100644 --- a/tests/typecheck/models/test_abstract.yml +++ b/tests/typecheck/models/test_abstract.yml @@ -41,3 +41,33 @@ class MyModel(BaseModel): field = models.IntegerField() + +- case: test_can_instantiate_with_recursive_relation_on_abstract_model + main: | + from myapp.models import Concrete, Recursive + first = Concrete.objects.create(parent=None) + Concrete.objects.create(parent=first) + Recursive.objects.create(parent=None) + Recursive(parent=Recursive(parent=None)) + Concrete(parent=Concrete(parent=None)) + out: | + main:4: error: Unexpected attribute "parent" for model "Recursive" + main:4: error: Cannot instantiate abstract model "Recursive" + main:5: error: Unexpected attribute "parent" for model "Recursive" + main:5: error: Cannot instantiate abstract model "Recursive" + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class Recursive(models.Model): + parent = models.ForeignKey("self", null=True, on_delete=models.CASCADE) + + class Meta: + abstract = True + + class Concrete(Recursive): + ...