sbdchd / django-types

:doughnut: Type stubs for Django
MIT License
187 stars 62 forks source link

Using django-types with django-multitenant #171

Closed Mapiarz closed 1 year ago

Mapiarz commented 1 year ago

Hi!

I'm able to integrate with django-types no problem. It does work as intended overall. In my project though, I'm using django-multitenant which adds a layer of complexity. Almost ALL of my relations are modeled using TenantForeignKey. Similarly, almost all of my types are using TenantManager instead of the built-in Django Manager. This effectively breaks django-types and I'm not getting the correct type hints anywhere in my project.

See example below:

class MyBaseClass(Model):
    objects = TenantManager()

class MyFooClass(MyBaseClass):
    ...

class MyBarClass(MyBaseClass):
    my_foo = TenantForeignKey[MyFooClass](MyFooClass, on_delete=CASCADE)

Almost all types in my project are derived from MyBaseClass. All Queryset operations such as MyFooClass.objects.get() will return Any. Foreign relations are not annotated correctly either.

Please advise. What's the path of least resistance to get this working? I appreciate all the help I can get.

Mapiarz commented 1 year ago

I also tried using a TypeAlias like so:

from typing import TYPE_CHECKING, TypeAlias
from django.db import models

if TYPE_CHECKING:
    TenantForeignKey: TypeAlias = models.ForeignKey
else:
    from django_multitenant.fields import TenantForeignKey

But that doesn't work either.

Mapiarz commented 1 year ago

I was able to integrate django-multitenant by creating stubs for their models like this:

from typing import (
    TypeVar,
)

from django.db import models
from django_multitenant import mixins

_M = TypeVar("_M", bound=models.Model)

class TenantManager(models.Manager[_M]): ...
class TenantModel(mixins.TenantModelMixin, models.Model): ...

and

from typing import (
    Optional,
    TypeVar,
)

from django.db import models

_M = TypeVar("_M", bound=Optional[models.Model])

class TenantForeignKey(models.ForeignKey[_M]): ...
class TenantOneToOneField(models.OneToOneField[_M]): ...

The boundary is not exactly right as it should be intersection between django's Model and TenantModelMixin. This will allow somebody to put a tenant manager on a model without tenant mixin or do a tenant relation to a model without tenant mixin. This is a limitation I'm okay with.

Perhaps one could try to declare a type that combines the Model and TenantModelMixin and use that as a boundary but I couldn't get it to work with my non-trivial inheritance structure. Either I was doing something wrong or type checkers don't handle that case too well.