Kracekumar
Geek
from django.contrib.auth.models import User
type(User.objects.filter(
email='foo@example.com'))
# output
django.db.models.query.QuerySet
type(("127.0.0.1", 8000))
# output
tuple
addr = "127.0.0.1"
port = 8000
reveal_type((addr, port))
# mypy filename.py
note: Revealed type is
'Tuple[builtins.str, builtins.int]'
from django.contrib.auth.models import User
reveal_type(User.objects.filter(
email='foo@example.com'))
# mypy filename.py
note: Revealed type is
'django.contrib.auth.models.UserManager
[django.contrib.auth.models.User]'
from django.http import (HttpRequest, HttpResponse,
HttpResponseNotFound)
def index(request: HttpRequest) -> HttpResponse:
return HttpResponse("hello world!")
def view_404_0(request: HttpRequest) -> HttpResponse:
return HttpResponseNotFound(
'<h1>Page not found</h1>')
def view_404_1(request:
HttpRequest) -> HttpResponseNotFound:
return HttpResponseNotFound(
'<h1>Page not found</h1>')
# bad - not precise and not useful
def view_404_2(request: HttpRequest) -> object:
return HttpResponseNotFound(
'<h1>Page not found</h1>')
HttpResponse.mro()
[django.http.response.HttpResponse,
django.http.response.HttpResponseBase,
object]
HttpResponseNotFound.mro()
[django.http.response.HttpResponseNotFound,
django.http.response.HttpResponse,
django.http.response.HttpResponseBase,
object]
The LSP states
that in an object-oriented program,
substituting a superclass
object reference with an object
of any of its subclasses,
the program should not break.
While designing objects and annotating return values
try adhering to LSP.
Mypy will complain when the return
value deviates from LSP
from django.db import models
from django.utils import timezone
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published")
def create_question(question_text: str) -> Question:
qs = Question(question_text=question_text,
pub_date=timezone.now())
qs.save()
return qs
def get_question(question_text: str) -> Question:
return Question.objects.filter(
question_text=question_text).first()
error: Incompatible return value type
(got "Optional[Any]", expected "Question")
from typing as Optional
def get_question(question_text: str) ->
Optional[Question]:
return Question.objects.filter(
question_text=question_text).first()
# mypy.ini
strict_optional = False
def get_question(question_text: str) ->
Question:
return Question.objects.filter(
question_text=question_text).first()
In [8]: Question.objects.all()
Out[8]: <QuerySet [<Question: Question object (1)>,
<Question: Question object (2)>]>
In [9]: Question.objects.filter()
Out[9]: <QuerySet [<Question: Question object (1)>,
<Question: Question object (2)>]>
def filter_question(text: str) ->
QuerySet[Question]:
return Question.objects.filter(
text__startswith=text)
def exclude_question(text: str) ->
QuerySet[Question]:
return Question.objects.exclude(
text__startswith=text)
all, reverse, none, complex_filter,
union, order_by, distinct,
defer, only, using, extra,
select_related, select_for_update,
prefetch_related
disallow_any_generics = False
class Author(models.Model):
name = models.CharField(max_length=100)
age = models.IntegerField()
class Publisher(models.Model):
name = models.CharField(max_length=300)
class Book(models.Model):
name = models.CharField(max_length=300)
pages = models.IntegerField()
# use integer field in production
price = models.DecimalField(max_digits=10,
decimal_places=2)
rating = models.FloatField()
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher)
pubdate = models.DateField()
def get_avg_price():
return Book.objects.all().aggregate(
avg_price=Avg("price"))
print(get_avg_price())
{'avg_price': Decimal('276.666666666667')}
from decimal import Decimal
def get_avg_price() -> dict[str, Decimal]:
return Book.objects.all().aggregate(
avg_price=Avg("price"))
def count_by_publisher():
return Publisher.objects.annotate(
num_books=Count("book").values(
'name', 'num_books'))
In [17]: [i for i in count_by_publisher()]
Out[17]: [{'name': 'Penguin', 'num_books': 2},
{'name': 'vintage', 'num_books': 1}]
from collections.abc import Iterable
class PublishedBookCount(t.TypedDict):
name: str
num_books: int
def count_by_publisher() ->
Iterable[PublishedBookCount]:
...
def count_by_publisher():
return Publisher.objects.annotate(
num_books=Count("book"))
def print_pub(num_books=0):
if num_books > 0:
res = count_by_publisher().filter(
num_books__gt=num_books)
else:
res = count_by_publisher()
for item in res:
print(item.name, item.num_books)
In [42]: print_pub()
Penguin 2
vintage 1
In [43]: connection.queries[-1]['sql']
Out[43]: 'SELECT "id", "name", COUNT("id")
AS "num_books" FROM "polls_publisher"
LEFT OUTER JOIN "polls_book"
ON ("polls_publisher"."id" =
"polls_book"."publisher_id")
GROUP BY
"polls_publisher"."id", "polls_publisher"."name"'
def count_by_publisher() -> PublishedBookCount:
...
# mypy output
scratch.py:46: error: Incompatible return value
type (got "QuerySet[Any]", expected
"PublishedBookCount")
return Publisher.objects.annotate(
num_books=Count("book"))
^
scratch.py:51: error:
"PublishedBookCount" has no attribute "filter"
res = count_by_publisher().filter(
num_books__gt=num_books)
def count_by_publisher() -> QuerySet[Publisher]:
...
def print_pub(num_books: int=0) -> None:
...
for item in res:
print(item.name, item.num_books)
# mypy output
scratch.py:55: error: "Publisher" has
no attribute "num_books"
print(item.name, item.num_books)
from typing import TYPE_CHECKING
if TYPE_CHECKING:
class TypedPublisher(Publisher):
num_books = models.IntegerField()
class meta:
abstract = True
def count_by_publisher() -> QuerySet[TypedPublisher]:
return Publisher.objects.annotate(
num_books=Count("book"))
def print_pub(num_books: int=0) -> None:
if num_books > 0:
res = count_by_publisher().filter(
num_books__gt=num_books)
else:
res = count_by_publisher()
for item in res:
print(item.name, item.num_books)
from django.http import (HttpResponse,
HttpResponseNotFound)
# Create your views here.
# annotate the return value
def index(request):
return HttpResponse("hello world!")
def view_404_0(request):
return HttpResponseNotFound(
'<h1>Page not found</h1>')
from polls.views import *
from django.test import RequestFactory
def test_index():
request_factory = RequestFactory()
request = request_factory.post('/index')
index(request)
def test_view_404_0():
request_factory = RequestFactory()
request = request_factory.post('/404')
view_404_0(request)
$DJANGO_SETTINGS_MODULE="mysite.settings"
PYTHONPATH='.' poetry run pytest
-sv polls/tests.py
--annotate-output=./annotations.json
$cat annotations.json
[...
{
"path": "polls/views.py",
"line": 7,
"func_name": "index",
"type_comments": [
"(django.core.handlers.wsgi.WSGIRequest) ->
django.http.response.HttpResponse"
],
"samples": 1
},
{
"path": "polls/views.py",
"line": 10,
"func_name": "view_404_0",
"type_comments": [
"(django.core.handlers.wsgi.WSGIRequest) ->
django.http.response.HttpResponseNotFound"
],
"samples": 1
}
]
$poetry run pyannotate --type-info
./annotations.json
-w polls/views.py --py3
from django.http import HttpResponse, HttpResponseNotFound
from django.core.handlers.wsgi import WSGIRequest
from django.http.response import HttpResponse
from django.http.response import HttpResponseNotFound
def index(request: WSGIRequest) -> HttpResponse:
return HttpResponse("hello world!")
def view_404_0(request: WSGIRequest) ->
HttpResponseNotFound:
return HttpResponseNotFound(
'<h1>Page not found</h1>')
By Kracekumar
How to annotate the optional python static type hints in the Django project.