Typing Django Model Reverse Relations
Find related items
Find models with a ForeignKey field with an argument that looks like 'related_name="billings"'.
It'll look something like this:
salesorder = models.ForeignKey(SalesOrder, related_name="line_items")There will be other arguments, but you can do a global search for "related_name=".
If you see related_name="+"You can skip that line. The plus tells Django not to setup the reverse relation. Since there's no reverse relation, there's nothing to add type hints for.
Identify receiving model
In the foreign key field, you'll see the first argument is the target model. If the target model is unquoted like above, the model is likely in the same file, or has been imported.
If the file is quoted, such as "salesorders.SalesOrder", you can find that in slate/salesorders/models.py
Adding imports
One major concern here is circular imports. So while we need to import the source model with the foreign key, it can only during the type checking process.
- Line 1 shows importing TYPE_CHECKING. This will need to be done once for any file we're going to add type hints for.
- Lines 11-12 show importing things conditionally. This is only needed if we're getting circular import issues.
from typing import TYPE_CHECKING
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError, ValidationError
from django.db import models
from slate.filebrowser.models import FileCore, FileNode, FileVer
from slate.notifications.models import Notification
from slate.projects.models import PriceTier, Project
if TYPE_CHECKING:
from slate.whatever.models import SourceModelAdding the type hint to the model
Now, in the model, after all our fields, but before any methods, let's add our type hints.
This is the hype hint we're pulling from the source model. We'll be adding this inside our target model.
Lines 9 - 10 are the relevant bits.
# class fields
created = models.DateTimeField(editable=False, auto_now_add=True, db_index=True)
updated = models.DateTimeField(editable=False, auto_now=True)
has_shipments = models.BooleanField(default=False)
html_fields = ["notes", "revision_notes"]
if TYPE_CHECKING:
line_items: models.manager.RelatedManager["SalesOrderLineItem"]
class Meta:
ordering = ["-created"]
verbose_name = "Sales Order"If you need to add more later, just add them to the TYPE_CHECKING block on the model.
Testing
The way to test this is to write a script and have your IDE show you what it things the type is.
We're going to create a fake management command.
Let's create slate/management/commands/type_hint_test.py
cd ~/Sites/Slate/backend
code slate/management/commands/type_hint_test.pyAnd now let's add a basic code we can plug things into.
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Template management command. Probably dont actually run this'
def handle(self, *args, **options):
from slate.TARGET_APP.models import TargetModel
obj = TargetModel()Now when you start typing obj.l... you should see auto completion and the correct type show up.
