Skip to main content

Type stubs for Django

Project description

django-types PyPI

Type stubs for Django.

Note: this project was forked from https://github.com/typeddjango/django-stubs with the goal of removing the mypy plugin dependency so that mypy can't crash due to Django config, and that non-mypy type checkers like pyright will work better with Django.

install

pip install django-types

You'll need to monkey patch Django's QuerySet, Manager (not needed for Django 3.1+) and ForeignKey (not needed for Django 4.1+) classes so we can index into them with a generic argument. Add this to your settings.py:

# in settings.py
from django.db.models import ForeignKey
from django.db.models.manager import BaseManager
from django.db.models.query import QuerySet

# NOTE: there are probably other items you'll need to monkey patch depending on
# your version.
for cls in [QuerySet, BaseManager, ForeignKey]:
    cls.__class_getitem__ = classmethod(lambda cls, *args, **kwargs: cls)  # type: ignore [attr-defined]

usage

ForeignKey ids and related names as properties in ORM models

When defining a Django ORM model with a foreign key, like so:

class User(models.Model):
    team = models.ForeignKey(
        "Team",
        null=True,
        on_delete=models.SET_NULL,
    )
    role = models.ForeignKey(
        "Role",
        null=True,
        on_delete=models.SET_NULL,
        related_name="users",
    )

two properties are created, team as expected, and team_id. Also, a related manager called user_set is created on Team for the reverse access.

In order to properly add typing to the foreign key itself and also for the created ids you can do something like this:

from typing import TYPE_CHECKING

from someapp.models import Team
if TYPE_CHECKING:
    # In this example Role cannot be imported due to circular import issues,
    # but doing so inside TYPE_CHECKING will make sure that the typing below
    # knows what "Role" means
    from anotherapp.models import Role


class User(models.Model):
    team_id: Optional[int]
    team = models.ForeignKey(
        Team,
        null=True,
        on_delete=models.SET_NULL,
    )
    role_id: int
    role = models.ForeignKey["Role"](
        "Role",
        null=False,
        on_delete=models.SET_NULL,
        related_name="users",
    )


reveal_type(User().team)
# note: Revealed type is 'Optional[Team]'
reveal_type(User().role)
# note: Revealed type is 'Role'

This will make sure that team_id and role_id can be accessed. Also, team and role will be typed to their right objects.

To be able to access the related manager Team and Role you could do:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # This doesn't really exists on django so it always need to be imported this way
    from django.db.models.manager import RelatedManager
    from user.models import User


class Team(models.Model):
    if TYPE_CHECKING:
        user_set = RelatedManager["User"]()


class Role(models.Model):
    if TYPE_CHECKING:
        users = RelatedManager["User"]()

reveal_type(Team().user_set)
# note: Revealed type is 'RelatedManager[User]'
reveal_type(Role().users)
# note: Revealed type is 'RelatedManager[User]'

An alternative is using annotations:

from __future__ import annotations  # or just be in python 3.11

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from django.db.models import Manager
    from user.models import User


class Team(models.Model):
    user_set: Manager[User]


class Role(models.Model):
    users: Manager[User]

reveal_type(Team().user_set)
# note: Revealed type is 'Manager[User]'
reveal_type(Role().users)
# note: Revealed type is 'Manager[User]'

id Field

By default Django will create an AutoField for you if one doesn't exist.

For type checkers to know about the id field you'll need to declare the field explicitly.

# before
class Post(models.Model):
    ...

# after
class Post(models.Model):
    id = models.AutoField(primary_key=True)
    # OR
    id: int

HttpRequest's user property

The HttpRequest's user property has a type of Union[AbstractBaseUser, AnonymousUser], but for most of your views you'll probably want either an authed user or an AnonymousUser.

So we can define a subclass for each case:

class AuthedHttpRequest(HttpRequest):
    user: User  # type: ignore [assignment]

And then you can use it in your views:

@auth.login_required
def activity(request: AuthedHttpRequest, team_id: str) -> HttpResponse:
    ...

You can also get more strict with your login_required decorator so that the first argument of the function it is decorating is AuthedHttpRequest:

from typing import Any, Union, TypeVar, cast
from django.http import HttpRequest, HttpResponse
from typing_extensions import Protocol
from functools import wraps

class RequestHandler1(Protocol):
    def __call__(self, request: AuthedHttpRequest) -> HttpResponse:
        ...


class RequestHandler2(Protocol):
    def __call__(self, request: AuthedHttpRequest, __arg1: Any) -> HttpResponse:
        ...


RequestHandler = Union[RequestHandler1, RequestHandler2]


# Verbose bound arg due to limitations of Python typing.
# see: https://github.com/python/mypy/issues/5876
_F = TypeVar("_F", bound=RequestHandler)


def login_required(view_func: _F) -> _F:
    @wraps(view_func)
    def wrapped_view(
        request: AuthedHttpRequest, *args: object, **kwargs: object
    ) -> HttpResponse:
        if request.user.is_authenticated:
            return view_func(request, *args, **kwargs)  # type: ignore [call-arg]
        raise AuthenticationRequired

    return cast(_F, wrapped_view)

Then the following will type error:

@auth.login_required
def activity(request: HttpRequest, team_id: str) -> HttpResponse:
    ...

related

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django-types-0.17.0.tar.gz (189.5 kB view details)

Uploaded Source

Built Distribution

django_types-0.17.0-py3-none-any.whl (381.9 kB view details)

Uploaded Python 3

File details

Details for the file django-types-0.17.0.tar.gz.

File metadata

  • Download URL: django-types-0.17.0.tar.gz
  • Upload date:
  • Size: 189.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.13 CPython/3.10.8 Darwin/21.5.0

File hashes

Hashes for django-types-0.17.0.tar.gz
Algorithm Hash digest
SHA256 c1c42ab78876c57c728342d5ab060725ab371fc8dc83bb85bae0be06846aad70
MD5 ebaa190e66edde8c04d8fc9da20d0886
BLAKE2b-256 8587f0f4f8387aee5cc3ca738f75a0f4c648dd45a19607e8d2b8aa7b47d2dd7e

See more details on using hashes here.

File details

Details for the file django_types-0.17.0-py3-none-any.whl.

File metadata

  • Download URL: django_types-0.17.0-py3-none-any.whl
  • Upload date:
  • Size: 381.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.13 CPython/3.10.8 Darwin/21.5.0

File hashes

Hashes for django_types-0.17.0-py3-none-any.whl
Algorithm Hash digest
SHA256 43d4931159524c1a08b25c1b9a4db2512b7c1234b64b1d4db4aff78881d6afd0
MD5 71c3f6405e6b1067ada90ca2f367be30
BLAKE2b-256 1ab09dd6e8840967753dd9f1156a41e4069ae05e4d5a14ac4bdd32c54d5e3987

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page