Usage
To make use of this plugin in code means using the annotation classes that are provided.
The following examples assume there is an abstract model AbstractModel
with the concrete models Concrete1, Concrete2, and Concrete3.
Additionally, Concrete2 has a custom queryset class called Concrete2QS.
Concrete
To resolve a union of the concrete models, use the
Concrete annotation:
from extended_mypy_django_plugin import Concrete
instance: Concrete[AbstractModel]
# --------------
# Equivalent to
# --------------
instance: Concrete1 | Concrete2 | Concrete3
This also works for types:
from extended_mypy_django_plugin import Concrete
cls: Concrete[type[AbstractModel]]
# --------------
# Equivalent to
# --------------
cls: type[Concrete1 | Concrete2 | Concrete3]
Concrete TypeVar
To create a type var representing any one of the concrete models of an abstract
model, create a TypeVar object like normal and bind it to the concrete of
the desired model:
from extended_mypy_django_plugin import Concrete
from typing import TypeVar
T_Concrete = TypeVar("T_Concrete", bound=Concrete[AbstractModel])
def create_row(cls: type[T_Concrete]) -> T_Concrete:
return cls.objects.create()
# --------------
# Equivalent to
# --------------
from typing import TypeVar
T_Concrete = TypeVar("T_Concrete", bound=Concrete1 | Concrete2 | Concrete3)
def create_row(cls: type[T_Concrete]) -> T_Concrete:
return cls.objects.create()
Concrete.cast_as_concrete
To type narrow an object as a concrete descendent of that object, the
Concrete.cast_as_concrete
may be used:
from extended_mypy_django_plugin import Concrete
def takes_model(model: AbstractModel) -> None:
narrowed = Concrete.cast_as_concrete(model)
reveal_type(narrowed) # Concrete1 | Concrete2 | Concrete3
def takes_model_cls(model_cls: type[AbstractModel]) -> None:
narrowed = Concrete.cast_as_concrete(model_cls)
reveal_type(narrowed) # type[Concrete1] | type[Concrete2] | type[Concrete3]
Note that at runtime this will raise an exception if the passed in object is either not a Django model class/instance or is an abstract one.
Using Concrete annotations on classmethods would look like:
from extended_mypy_django_plugin import DefaultQuerySet
from django.db import models
from typing import Self
class AbstractModel(models.Model):
class Meta:
abstract = True
@classmethod
def new(cls) -> Self:
concrete = Concrete.cast_as_concrete(cls)
reveal_type(concrete) # type[Concrete1] | type[Concrete2] | type[Concrete3]
created = cls.objects.create()
# Note that convincing mypy that created matches self, requires this
assert isinstance(created, cls)
# Otherwise the return will make mypy complain that it doesn't match self
return created
# # Note: the following isn't possible
# # : because the annotations cannot be used with TypeVars
# def qs(self) -> DefaultQuerySet[Self]:
# concrete = Concrete.cast_as_concrete(self)
# reveal_type(concrete) # Concrete1 | Concrete2 | Concrete3
# return concrete.__class__.objects.filter(pk=self.pk)
class Concrete1(AbstractModel):
pass
class Concrete2(AbstractModel):
pass
class Concrete3(AbstractModel):
pass
model: type[AbstractModel] = Concrete1
instance = model.new()
reveal_type(instance) # Concrete1 | Concrete2 | Concrete3
# # NOTE: the qs method specific to which instance isn't possible
# qs = instance.qs()
# reveal_type(qs) # QuerySet[Concrete1] | Concrete2QS | QuerySet[Concrete3]
specific = Concrete1.new()
reveal_type(specific) # Concrete1
# # NOTE: the qs method specific to which instance isn't possible
# specific_qs = instance.qs()
# reveal_type(specific_qs) # QuerySet[Concrete1]
DefaultQuerySet
To resolve a union of the default querysets for the concrete models of an
abstract class, use the
DefaultQuerySet
annotation:
from extended_mypy_django_plugin import DefaultQuerySet
from django.db import models
qs: DefaultQuerySet[AbstractModel]
# --------------
# Equivalent to
# --------------
qs: models.QuerySet[Concrete1] | Concrete2QuerySet | models.QuerySet[Concrete3]
This also works on the concrete models themselves:
from extended_mypy_django_plugin import DefaultQuerySet
qs1: DefaultQuerySet[Concrete1]
qs2: DefaultQuerySet[Concrete2]
# --------------
# Equivalent to
# --------------
from django.db import models
qs1: models.QuerySet[Concrete1]
qs2: Concrete2QuerySet