docs: Add a section on django-stubs.

This adds examples for QuerySet, ValuesQuerySet, TestHttpResponse, which
are some common examples that need a bit of extra care when typing.

Signed-off-by: Zixuan James Li <p359101898@gmail.com>
This commit is contained in:
Zixuan James Li 2022-10-06 00:12:56 -04:00 committed by Tim Abbott
parent 8e472402fe
commit dd1f8c6bfb
1 changed files with 82 additions and 0 deletions

View File

@ -107,6 +107,88 @@ everything in the third-party module as an `Any`, which is the right
model (one certainly wouldn't want to need stubs for everything just
to use `mypy`!), but means the code can't be fully type-checked.
## Working with types from django-stubs
For features that are difficult to be expressed with static type
annotations, type analysis is supplemented with mypy plugins. Zulip's
Python codebases uses the Django web framework, and such a plugin is
required in order for `mypy` to correctly infer the types of most code
interacting with Django model classes (i.e. code that accesses the
database).
We use the `mypy_django_plugin` plugin from the
[django-stubs](https://github.com/typeddjango/django-stubs) project,
which supports accurate type inference for classes like
`QuerySet`. For example, `Stream.objects.filter(realm=realm)` is
simple Django code to fetch all the streams in a realm. With this
plugin, mypy will correctly determine its type is `QuerySet[Stream]`,
aka a standard, lazily evaluated Django query object that can be
iterated through to access `Stream` objects, without the developer
needing to do an explicit annotation.
When declaring the types for functions that accept a `QuerySet`
object, you should always supply the model type that it accepts as the
type parameter.
```python
def foo(user: QuerySet[UserProfile]) -> None:
...
```
In cases where you need to type the return value from `.values_list`
or `.values` on a `QuerySet`, you can use the special
`django_stubs_ext.ValuesQuerySet` type.
For `.values_list`, the second type parameter will be the type of the
column.
```python
from django_stubs_ext import ValuesQuerySet
def get_book_page_counts() -> ValuesQuerySet[Book, int]:
return Book.objects.filter().values_list("page_count", flat=True)
```
For `.values`, we prefer to define a `TypedDict` containing the
key-value pairs for the columns.
```python
from django_stubs_ext import ValuesQuerySet
class BookMetadata(TypedDict):
id: int
name: str
def get_book_meta_data(
book_ids: List[int],
) -> ValuesQuerySet[Book, BookMetadata]:
return Book.objects.filter(id__in=book_ids).values("name", "id")
```
When writing a helper function ehat returns the response from a test
client, it should be typed as `TestHttpResponse` instead of
`HttpResponse`. This type is only defined in the Django stubs, so it
has to be conditionally imported only when type
checking. Conventionally, we alias it as `TestHttpResponse`, which is
internally named `_MonkeyPatchedWSGIResponse` within django-stubs.
```python
from typing import TYPE_CHECKING
from zerver.lib.test_classes import ZulipTestCase
if TYPE_CHECKING:
from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
class FooTestCase(ZulipTestCase):
def helper(self) -> "TestHttpResponse":
return self.client_get("/bar")
```
We sometimes encounter innaccurate type annotations in the Django
stubs project. We prefer to address these by [submiting a pull
request](https://github.com/typeddjango/django-stubs/pulls) to fix the
issue in the upstream project, just like we do with `typeshed` bugs.
## Using @overload to accurately describe variations
Sometimes, a function's type is most precisely expressed as a few