django-stubs icon indicating copy to clipboard operation
django-stubs copied to clipboard

Abstract models cannot be used in type annotations

Open orsinium opened this issue 1 year ago • 5 comments

Bug report

What's wrong

If BaseMessage is an abstract model and EmailMessage is a concrete model based on BaseMessage:

class _BaseViewSet(GenericViewSet):
    model: type[BaseMessage]
    def create(self):
        query = self.model.objects.filter(brand_id=brand.pk)

class EmailViewSet(_BaseViewSet):
    model = EmailMessage

The code produces an error:

error: "Type[BaseMessage] has no attribute "objects"

How is that should be

The code should pass type checking without errors, like in django-stubs 4.2.3. It's a common practice in Python to use a base class for type annotations, even if only its subclasses may be used. The idea is that the base class defines all the attributes and methods that every subclass must define, and then abc.ABC is used if you need to ensure that only subclasses can be instantiated. There is no way in mypy to tell on the user side "This attribute is any subclass of the class but not the class itself".

For example, this code won't report any errors despite the type attribute being actually defined only on subclasses (and so if you try to use BaseMessage.type it will fail in runtime):


class BaseMessage:
    type: str

class EmailMessage(BaseMessage):
    type = "email"

def f(m: BaseMessage):
    return m.type

m = EmailMessage()
f(m)

System information

  • OS: linux
  • python version: 3.8
  • django version: 4.2.4
  • mypy version: 1.5.1
  • django-stubs version: 4.2.4
  • django-stubs-ext version: 4.2.2

orsinium avatar Sep 15 '23 15:09 orsinium

The issue was introduced in #1663 as a fix for #1653. The comment https://github.com/typeddjango/django-stubs/issues/1653#issuecomment-1674496381 suggests a workaround solution which is listing all possible subtypes for the type explicitly and using their TypeVar or Union. But that's a bad solution that doesn't scale well. In my case, the base abstract model has 6 submodels and more will be added in the future, and I don't want it to require changing a bunch of unrelated annotations.

orsinium avatar Sep 15 '23 15:09 orsinium

There's 2 options here, I think

  1. Declare an objects manager on your abstract class
  2. Use ._default_manager instead of objects. See #1684

flaeppe avatar Sep 15 '23 15:09 flaeppe

Tried the first option, and it works 👍🏿 That's a bit confusing, though. I like @sobolevn 's suggestion to put it in the docs (https://github.com/typeddjango/django-stubs/issues/1684#issuecomment-1706756028).

orsinium avatar Sep 15 '23 16:09 orsinium

Would it make sense to keep the issue open to update the documentation? I think the suggestion https://github.com/typeddjango/django-stubs/issues/1684#issuecomment-1706756028 hasn't been addressed, and it should prevent questions like I had in the future.

orsinium avatar Sep 15 '23 16:09 orsinium

Yes, we should totally work on docs :)

sobolevn avatar Sep 15 '23 17:09 sobolevn