0
# Protocols and Base Classes
1
2
Protocol definitions for flexible attribute access and typed base classes for Django components. These provide structural typing support and typed interfaces for Django's dynamic components like Model.Meta classes and database routers.
3
4
## Capabilities
5
6
### Flexible Attribute Access Protocol
7
8
A protocol defining arbitrary attribute access patterns, used internally by the mypy Django plugin for dynamic attribute handling.
9
10
```python { .api }
11
class AnyAttrAllowed(Protocol):
12
"""
13
Protocol for classes allowing arbitrary attribute access.
14
15
Used internally by mypy_django_plugin to handle dynamic attributes
16
on Django models and other classes that support flexible attribute access.
17
"""
18
19
def __getattr__(self, item: str) -> Any:
20
"""Allow getting any attribute by name."""
21
...
22
23
def __setattr__(self, item: str, value: Any) -> None:
24
"""Allow setting any attribute by name."""
25
...
26
```
27
28
### Typed Model Meta Base Class
29
30
A typed base class for Django Model Meta inner classes, providing comprehensive type safety for model metadata configuration.
31
32
```python { .api }
33
class TypedModelMeta:
34
"""
35
Typed base class for Django Model `class Meta:` inner class.
36
At runtime this is just an alias to `object`.
37
38
Most attributes are the same as `django.db.models.options.Options`.
39
Options has some additional attributes and some values are normalized by Django.
40
"""
41
42
abstract: ClassVar[bool] # default: False
43
app_label: ClassVar[str]
44
base_manager_name: ClassVar[str]
45
db_table: ClassVar[str]
46
db_table_comment: ClassVar[str]
47
db_tablespace: ClassVar[str]
48
default_manager_name: ClassVar[str]
49
default_related_name: ClassVar[str]
50
get_latest_by: ClassVar[Union[str, Sequence[str]]]
51
managed: ClassVar[bool] # default: True
52
order_with_respect_to: ClassVar[str]
53
ordering: ClassVar[Sequence[Union[str, OrderBy]]]
54
permissions: ClassVar[List[Tuple[str, str]]]
55
default_permissions: ClassVar[Sequence[str]] # default: ("add", "change", "delete", "view")
56
proxy: ClassVar[bool] # default: False
57
required_db_features: ClassVar[List[str]]
58
required_db_vendor: ClassVar[Literal["sqlite", "postgresql", "mysql", "oracle"]]
59
select_on_save: ClassVar[bool] # default: False
60
indexes: ClassVar[List[Index]]
61
unique_together: ClassVar[Union[Sequence[Sequence[str]], Sequence[str]]]
62
index_together: ClassVar[Union[Sequence[Sequence[str]], Sequence[str]]] # Deprecated in Django 4.2
63
constraints: ClassVar[List[BaseConstraint]]
64
verbose_name: ClassVar[StrOrPromise]
65
verbose_name_plural: ClassVar[StrOrPromise]
66
```
67
68
### Typed Database Router Base Class
69
70
A typed base class for Django's DATABASE_ROUTERS setting, providing comprehensive type safety for database routing logic.
71
72
```python { .api }
73
class TypedDatabaseRouter:
74
"""
75
Typed base class for Django's DATABASE_ROUTERS setting.
76
At runtime this is just an alias to `object`.
77
78
All methods are optional.
79
80
Django documentation: https://docs.djangoproject.com/en/stable/topics/db/multi-db/#automatic-database-routing
81
"""
82
83
def db_for_read(self, model: Type[Model], **hints: Any) -> Optional[str]:
84
"""Suggest the database to read from for objects of type model."""
85
...
86
87
def db_for_write(self, model: Type[Model], **hints: Any) -> Optional[str]:
88
"""Suggest the database to write to for objects of type model."""
89
...
90
91
def allow_relation(self, obj1: Type[Model], obj2: Type[Model], **hints: Any) -> Optional[bool]:
92
"""Return True if a relation between obj1 and obj2 should be allowed."""
93
...
94
95
def allow_migrate(
96
self, db: str, app_label: str, model_name: Optional[str] = None, **hints: Any
97
) -> Optional[bool]:
98
"""Return True if the migration operation is allowed on the database with alias db."""
99
...
100
```
101
102
### Related Manager Protocols
103
104
Protocol definitions for Django's dynamically-created related managers, providing type safety for relationship management. These are re-exported from django-stubs for convenience.
105
106
```python { .api }
107
# During TYPE_CHECKING: imported from django-stubs
108
# At runtime: Protocol definitions to prevent isinstance() usage
109
110
class RelatedManager(Protocol[_T]):
111
"""
112
Protocol for Django related managers.
113
114
These are fake classes - Django defines these inside function bodies.
115
Defined as Protocol to prevent use with isinstance().
116
These actually inherit from BaseManager.
117
"""
118
pass
119
120
class ManyRelatedManager(Protocol[_T]):
121
"""
122
Protocol for Django many-to-many related managers.
123
124
These are fake classes - Django defines these inside function bodies.
125
Defined as Protocol to prevent use with isinstance().
126
These actually inherit from BaseManager.
127
"""
128
pass
129
```
130
131
### Usage Examples
132
133
Using TypedModelMeta for model configuration:
134
135
```python
136
from django_stubs_ext.db.models import TypedModelMeta
137
from django.db import models
138
from django.utils.translation import gettext_lazy as _
139
140
class Article(models.Model):
141
title = models.CharField(max_length=200)
142
content = models.TextField()
143
created_at = models.DateTimeField(auto_now_add=True)
144
145
class Meta(TypedModelMeta):
146
verbose_name = _('Article')
147
verbose_name_plural = _('Articles')
148
ordering = ['-created_at']
149
indexes = [
150
models.Index(fields=['created_at']),
151
models.Index(fields=['title'])
152
]
153
permissions = [
154
('can_publish', 'Can publish articles'),
155
('can_feature', 'Can feature articles')
156
]
157
```
158
159
Implementing a typed database router:
160
161
```python
162
from django_stubs_ext.db.router import TypedDatabaseRouter
163
from django.db.models import Model
164
from typing import Optional, Type, Any
165
166
class DatabaseRouter(TypedDatabaseRouter):
167
"""Route reads and writes based on model type."""
168
169
def db_for_read(self, model: Type[Model], **hints: Any) -> Optional[str]:
170
"""Route read operations."""
171
if model._meta.app_label == 'analytics':
172
return 'analytics_db'
173
return None
174
175
def db_for_write(self, model: Type[Model], **hints: Any) -> Optional[str]:
176
"""Route write operations."""
177
if model._meta.app_label == 'analytics':
178
return 'analytics_db'
179
return None
180
181
def allow_relation(self, obj1: Type[Model], obj2: Type[Model], **hints: Any) -> Optional[bool]:
182
"""Allow relations within the same app."""
183
db_set = {'default', 'analytics_db'}
184
if hints.get('instance1')._meta.app_label in db_set:
185
return hints.get('instance2')._meta.app_label in db_set
186
return None
187
188
def allow_migrate(
189
self, db: str, app_label: str, model_name: Optional[str] = None, **hints: Any
190
) -> Optional[bool]:
191
"""Ensure analytics models only appear in analytics_db."""
192
if app_label == 'analytics':
193
return db == 'analytics_db'
194
elif db == 'analytics_db':
195
return False
196
return None
197
```
198
199
Using related manager protocols:
200
201
```python
202
from django_stubs_ext.db.models.manager import RelatedManager, ManyRelatedManager
203
from django.db import models
204
from django.db.models import QuerySet
205
from typing import TYPE_CHECKING
206
207
class Author(models.Model):
208
name = models.CharField(max_length=100)
209
210
class Publisher(models.Model):
211
name = models.CharField(max_length=100)
212
213
class Book(models.Model):
214
title = models.CharField(max_length=200)
215
authors = models.ManyToManyField(Author, related_name='books')
216
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE, related_name='books')
217
218
if TYPE_CHECKING:
219
# Type annotations for related managers
220
Author.books: ManyRelatedManager[Book]
221
Publisher.books: RelatedManager[Book]
222
223
# Type-safe usage
224
def get_author_books(author: Author) -> QuerySet[Book]:
225
"""Get all books by an author."""
226
return author.books.all() # Type checker knows this returns QuerySet[Book]
227
```
228
229
### AnyAttrAllowed Protocol Usage
230
231
The AnyAttrAllowed protocol is primarily used internally by the mypy Django plugin:
232
233
```python
234
from django_stubs_ext import AnyAttrAllowed
235
from typing import Any
236
237
class DynamicModel(AnyAttrAllowed):
238
"""Example class with flexible attribute access."""
239
240
def __init__(self):
241
self._data = {}
242
243
def __getattr__(self, item: str) -> Any:
244
return self._data.get(item)
245
246
def __setattr__(self, item: str, value: Any) -> None:
247
if item.startswith('_'):
248
super().__setattr__(item, value)
249
else:
250
self._data[item] = value
251
252
# Type-safe dynamic attribute access
253
model = DynamicModel()
254
model.any_attribute = "value" # Type checker allows this
255
retrieved = model.any_attribute # Type checker knows this is Any
256
```
257
258
## Types
259
260
```python { .api }
261
from typing import TYPE_CHECKING, Any, ClassVar, List, Literal, Optional, Protocol, Sequence, Tuple, Type, TypeVar, Union
262
from django.db.models import BaseConstraint, Index, Model, OrderBy
263
from django_stubs_ext import StrOrPromise
264
265
_T = TypeVar("_T")
266
267
if TYPE_CHECKING:
268
class TypedModelMeta:
269
"""Typed base class for Django Model `class Meta:` inner class."""
270
# ... (all the attributes as shown above)
271
272
class TypedDatabaseRouter:
273
"""Typed base class for Django's DATABASE_ROUTERS setting."""
274
# ... (all the methods as shown above)
275
else:
276
TypedModelMeta = object
277
TypedDatabaseRouter = object
278
279
class AnyAttrAllowed(Protocol):
280
"""Protocol for classes allowing arbitrary attribute access."""
281
def __getattr__(self, item: str) -> Any: ...
282
def __setattr__(self, item: str, value: Any) -> None: ...
283
284
class RelatedManager(Protocol[_T]):
285
"""Protocol for Django related managers."""
286
pass
287
288
class ManyRelatedManager(Protocol[_T]):
289
"""Protocol for Django many-to-many related managers."""
290
pass
291
```