plain.models 0.49.1__py3-none-any.whl → 0.50.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/models/CHANGELOG.md +23 -0
- plain/models/aggregates.py +42 -19
- plain/models/backends/base/base.py +125 -105
- plain/models/backends/base/client.py +11 -3
- plain/models/backends/base/creation.py +22 -12
- plain/models/backends/base/features.py +10 -4
- plain/models/backends/base/introspection.py +29 -16
- plain/models/backends/base/operations.py +187 -91
- plain/models/backends/base/schema.py +267 -165
- plain/models/backends/base/validation.py +12 -3
- plain/models/backends/ddl_references.py +85 -43
- plain/models/backends/mysql/base.py +29 -26
- plain/models/backends/mysql/client.py +7 -2
- plain/models/backends/mysql/compiler.py +12 -3
- plain/models/backends/mysql/creation.py +5 -2
- plain/models/backends/mysql/features.py +24 -22
- plain/models/backends/mysql/introspection.py +22 -13
- plain/models/backends/mysql/operations.py +106 -39
- plain/models/backends/mysql/schema.py +48 -24
- plain/models/backends/mysql/validation.py +13 -6
- plain/models/backends/postgresql/base.py +41 -34
- plain/models/backends/postgresql/client.py +7 -2
- plain/models/backends/postgresql/creation.py +10 -5
- plain/models/backends/postgresql/introspection.py +15 -8
- plain/models/backends/postgresql/operations.py +109 -42
- plain/models/backends/postgresql/schema.py +85 -46
- plain/models/backends/sqlite3/_functions.py +151 -115
- plain/models/backends/sqlite3/base.py +37 -23
- plain/models/backends/sqlite3/client.py +7 -1
- plain/models/backends/sqlite3/creation.py +9 -5
- plain/models/backends/sqlite3/features.py +5 -3
- plain/models/backends/sqlite3/introspection.py +32 -16
- plain/models/backends/sqlite3/operations.py +125 -42
- plain/models/backends/sqlite3/schema.py +82 -58
- plain/models/backends/utils.py +52 -29
- plain/models/backups/cli.py +8 -6
- plain/models/backups/clients.py +16 -7
- plain/models/backups/core.py +24 -13
- plain/models/base.py +113 -74
- plain/models/cli.py +94 -63
- plain/models/config.py +1 -1
- plain/models/connections.py +23 -7
- plain/models/constraints.py +65 -47
- plain/models/database_url.py +1 -1
- plain/models/db.py +6 -2
- plain/models/deletion.py +66 -43
- plain/models/entrypoints.py +1 -1
- plain/models/enums.py +22 -11
- plain/models/exceptions.py +23 -8
- plain/models/expressions.py +440 -257
- plain/models/fields/__init__.py +253 -202
- plain/models/fields/json.py +120 -54
- plain/models/fields/mixins.py +12 -8
- plain/models/fields/related.py +284 -252
- plain/models/fields/related_descriptors.py +34 -25
- plain/models/fields/related_lookups.py +23 -11
- plain/models/fields/related_managers.py +81 -47
- plain/models/fields/reverse_related.py +58 -55
- plain/models/forms.py +89 -63
- plain/models/functions/comparison.py +71 -18
- plain/models/functions/datetime.py +79 -29
- plain/models/functions/math.py +43 -10
- plain/models/functions/mixins.py +24 -7
- plain/models/functions/text.py +104 -25
- plain/models/functions/window.py +12 -6
- plain/models/indexes.py +52 -28
- plain/models/lookups.py +228 -153
- plain/models/migrations/autodetector.py +86 -43
- plain/models/migrations/exceptions.py +7 -3
- plain/models/migrations/executor.py +33 -7
- plain/models/migrations/graph.py +79 -50
- plain/models/migrations/loader.py +45 -22
- plain/models/migrations/migration.py +23 -18
- plain/models/migrations/operations/base.py +37 -19
- plain/models/migrations/operations/fields.py +89 -42
- plain/models/migrations/operations/models.py +245 -143
- plain/models/migrations/operations/special.py +82 -25
- plain/models/migrations/optimizer.py +7 -2
- plain/models/migrations/questioner.py +58 -31
- plain/models/migrations/recorder.py +18 -11
- plain/models/migrations/serializer.py +50 -39
- plain/models/migrations/state.py +220 -133
- plain/models/migrations/utils.py +29 -13
- plain/models/migrations/writer.py +17 -14
- plain/models/options.py +63 -56
- plain/models/otel.py +16 -6
- plain/models/preflight.py +35 -12
- plain/models/query.py +323 -228
- plain/models/query_utils.py +93 -58
- plain/models/registry.py +34 -16
- plain/models/sql/compiler.py +146 -97
- plain/models/sql/datastructures.py +38 -25
- plain/models/sql/query.py +255 -169
- plain/models/sql/subqueries.py +32 -21
- plain/models/sql/where.py +54 -29
- plain/models/test/pytest.py +15 -11
- plain/models/test/utils.py +4 -2
- plain/models/transaction.py +20 -7
- plain/models/utils.py +13 -5
- {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/METADATA +1 -1
- plain_models-0.50.0.dist-info/RECORD +122 -0
- plain_models-0.49.1.dist-info/RECORD +0 -122
- {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/WHEEL +0 -0
- {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/licenses/LICENSE +0 -0
plain/models/migrations/graph.py
CHANGED
@@ -1,9 +1,15 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
from functools import total_ordering
|
4
|
+
from typing import TYPE_CHECKING, Any
|
2
5
|
|
3
6
|
from plain.models.migrations.state import ProjectState
|
4
7
|
|
5
8
|
from .exceptions import CircularDependencyError, NodeNotFoundError
|
6
9
|
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from plain.models.migrations.migration import Migration
|
12
|
+
|
7
13
|
|
8
14
|
@total_ordering
|
9
15
|
class Node:
|
@@ -12,33 +18,33 @@ class Node:
|
|
12
18
|
nodes in either direction.
|
13
19
|
"""
|
14
20
|
|
15
|
-
def __init__(self, key):
|
21
|
+
def __init__(self, key: tuple[str, str]):
|
16
22
|
self.key = key
|
17
|
-
self.children = set()
|
18
|
-
self.parents = set()
|
23
|
+
self.children: set[Node] = set()
|
24
|
+
self.parents: set[Node] = set()
|
19
25
|
|
20
|
-
def __eq__(self, other):
|
26
|
+
def __eq__(self, other: object) -> bool:
|
21
27
|
return self.key == other
|
22
28
|
|
23
|
-
def __lt__(self, other):
|
29
|
+
def __lt__(self, other: object) -> bool:
|
24
30
|
return self.key < other
|
25
31
|
|
26
|
-
def __hash__(self):
|
32
|
+
def __hash__(self) -> int:
|
27
33
|
return hash(self.key)
|
28
34
|
|
29
|
-
def __getitem__(self, item):
|
35
|
+
def __getitem__(self, item: int) -> str:
|
30
36
|
return self.key[item]
|
31
37
|
|
32
|
-
def __str__(self):
|
38
|
+
def __str__(self) -> str:
|
33
39
|
return str(self.key)
|
34
40
|
|
35
|
-
def __repr__(self):
|
41
|
+
def __repr__(self) -> str:
|
36
42
|
return f"<{self.__class__.__name__}: ({self.key[0]!r}, {self.key[1]!r})>"
|
37
43
|
|
38
|
-
def add_child(self, child):
|
44
|
+
def add_child(self, child: Node) -> None:
|
39
45
|
self.children.add(child)
|
40
46
|
|
41
|
-
def add_parent(self, parent):
|
47
|
+
def add_parent(self, parent: Node) -> None:
|
42
48
|
self.parents.add(parent)
|
43
49
|
|
44
50
|
|
@@ -51,12 +57,14 @@ class DummyNode(Node):
|
|
51
57
|
If there are any left, a nonexistent dependency error is raised.
|
52
58
|
"""
|
53
59
|
|
54
|
-
def __init__(
|
60
|
+
def __init__(
|
61
|
+
self, key: tuple[str, str], origin: tuple[str, str], error_message: str
|
62
|
+
):
|
55
63
|
super().__init__(key)
|
56
64
|
self.origin = origin
|
57
65
|
self.error_message = error_message
|
58
66
|
|
59
|
-
def raise_error(self):
|
67
|
+
def raise_error(self) -> None:
|
60
68
|
raise NodeNotFoundError(self.error_message, self.key, origin=self.origin)
|
61
69
|
|
62
70
|
|
@@ -84,21 +92,29 @@ class MigrationGraph:
|
|
84
92
|
"""
|
85
93
|
|
86
94
|
def __init__(self):
|
87
|
-
self.node_map = {}
|
88
|
-
self.nodes = {}
|
95
|
+
self.node_map: dict[tuple[str, str], Node] = {}
|
96
|
+
self.nodes: dict[tuple[str, str], Migration | None] = {}
|
89
97
|
|
90
|
-
def add_node(self, key, migration):
|
98
|
+
def add_node(self, key: tuple[str, str], migration: Migration) -> None:
|
91
99
|
assert key not in self.node_map
|
92
100
|
node = Node(key)
|
93
101
|
self.node_map[key] = node
|
94
102
|
self.nodes[key] = migration
|
95
103
|
|
96
|
-
def add_dummy_node(
|
104
|
+
def add_dummy_node(
|
105
|
+
self, key: tuple[str, str], origin: tuple[str, str], error_message: str
|
106
|
+
) -> None:
|
97
107
|
node = DummyNode(key, origin, error_message)
|
98
108
|
self.node_map[key] = node
|
99
109
|
self.nodes[key] = None
|
100
110
|
|
101
|
-
def add_dependency(
|
111
|
+
def add_dependency(
|
112
|
+
self,
|
113
|
+
migration: tuple[str, str] | None,
|
114
|
+
child: tuple[str, str],
|
115
|
+
parent: tuple[str, str],
|
116
|
+
skip_validation: bool = False,
|
117
|
+
) -> None:
|
102
118
|
"""
|
103
119
|
This may create dummy nodes if they don't yet exist. If
|
104
120
|
`skip_validation=True`, validate_consistency() should be called
|
@@ -109,26 +125,28 @@ class MigrationGraph:
|
|
109
125
|
f"Migration {migration} dependencies reference nonexistent"
|
110
126
|
f" child node {child!r}"
|
111
127
|
)
|
112
|
-
self.add_dummy_node(child, migration, error_message)
|
128
|
+
self.add_dummy_node(child, migration, error_message) # type: ignore[arg-type]
|
113
129
|
if parent not in self.nodes:
|
114
130
|
error_message = (
|
115
131
|
f"Migration {migration} dependencies reference nonexistent"
|
116
132
|
f" parent node {parent!r}"
|
117
133
|
)
|
118
|
-
self.add_dummy_node(parent, migration, error_message)
|
134
|
+
self.add_dummy_node(parent, migration, error_message) # type: ignore[arg-type]
|
119
135
|
self.node_map[child].add_parent(self.node_map[parent])
|
120
136
|
self.node_map[parent].add_child(self.node_map[child])
|
121
137
|
if not skip_validation:
|
122
138
|
self.validate_consistency()
|
123
139
|
|
124
|
-
def remove_replaced_nodes(
|
140
|
+
def remove_replaced_nodes(
|
141
|
+
self, replacement: tuple[str, str], replaced: list[tuple[str, str]]
|
142
|
+
) -> None:
|
125
143
|
"""
|
126
144
|
Remove each of the `replaced` nodes (when they exist). Any
|
127
145
|
dependencies that were referencing them are changed to reference the
|
128
146
|
`replacement` node instead.
|
129
147
|
"""
|
130
148
|
# Cast list of replaced keys to set to speed up lookup later.
|
131
|
-
|
149
|
+
replaced_set: set[tuple[str, str]] = set(replaced)
|
132
150
|
try:
|
133
151
|
replacement_node = self.node_map[replacement]
|
134
152
|
except KeyError as err:
|
@@ -137,7 +155,7 @@ class MigrationGraph:
|
|
137
155
|
" to the migration graph, or has been removed.",
|
138
156
|
replacement,
|
139
157
|
) from err
|
140
|
-
for replaced_key in
|
158
|
+
for replaced_key in replaced_set:
|
141
159
|
self.nodes.pop(replaced_key, None)
|
142
160
|
replaced_node = self.node_map.pop(replaced_key, None)
|
143
161
|
if replaced_node:
|
@@ -146,17 +164,19 @@ class MigrationGraph:
|
|
146
164
|
# We don't want to create dependencies between the replaced
|
147
165
|
# node and the replacement node as this would lead to
|
148
166
|
# self-referencing on the replacement node at a later iteration.
|
149
|
-
if child.key not in
|
167
|
+
if child.key not in replaced_set:
|
150
168
|
replacement_node.add_child(child)
|
151
169
|
child.add_parent(replacement_node)
|
152
170
|
for parent in replaced_node.parents:
|
153
171
|
parent.children.remove(replaced_node)
|
154
172
|
# Again, to avoid self-referencing.
|
155
|
-
if parent.key not in
|
173
|
+
if parent.key not in replaced_set:
|
156
174
|
replacement_node.add_parent(parent)
|
157
175
|
parent.add_child(replacement_node)
|
158
176
|
|
159
|
-
def remove_replacement_node(
|
177
|
+
def remove_replacement_node(
|
178
|
+
self, replacement: tuple[str, str], replaced: list[tuple[str, str]]
|
179
|
+
) -> None:
|
160
180
|
"""
|
161
181
|
The inverse operation to `remove_replaced_nodes`. Almost. Remove the
|
162
182
|
replacement node `replacement` and remap its child nodes to `replaced`
|
@@ -172,8 +192,8 @@ class MigrationGraph:
|
|
172
192
|
" to the migration graph, or has been removed already.",
|
173
193
|
replacement,
|
174
194
|
) from err
|
175
|
-
replaced_nodes = set()
|
176
|
-
replaced_nodes_parents = set()
|
195
|
+
replaced_nodes: set[Node] = set()
|
196
|
+
replaced_nodes_parents: set[Node] = set()
|
177
197
|
for key in replaced:
|
178
198
|
replaced_node = self.node_map.get(key)
|
179
199
|
if replaced_node:
|
@@ -192,11 +212,11 @@ class MigrationGraph:
|
|
192
212
|
# NOTE: There is no need to remap parent dependencies as we can
|
193
213
|
# assume the replaced nodes already have the correct ancestry.
|
194
214
|
|
195
|
-
def validate_consistency(self):
|
215
|
+
def validate_consistency(self) -> None:
|
196
216
|
"""Ensure there are no dummy nodes remaining in the graph."""
|
197
217
|
[n.raise_error() for n in self.node_map.values() if isinstance(n, DummyNode)]
|
198
218
|
|
199
|
-
def forwards_plan(self, target):
|
219
|
+
def forwards_plan(self, target: tuple[str, str]) -> list[tuple[str, str]]:
|
200
220
|
"""
|
201
221
|
Given a node, return a list of which previous nodes (dependencies) must
|
202
222
|
be applied, ending with the node itself. This is the list you would
|
@@ -206,11 +226,13 @@ class MigrationGraph:
|
|
206
226
|
raise NodeNotFoundError(f"Node {target!r} not a valid node", target)
|
207
227
|
return self.iterative_dfs(self.node_map[target])
|
208
228
|
|
209
|
-
def iterative_dfs(
|
229
|
+
def iterative_dfs(
|
230
|
+
self, start: Node, forwards: bool = True
|
231
|
+
) -> list[tuple[str, str]]:
|
210
232
|
"""Iterative depth-first search for finding dependencies."""
|
211
|
-
visited = []
|
212
|
-
visited_set = set()
|
213
|
-
stack = [(start, False)]
|
233
|
+
visited: list[tuple[str, str]] = []
|
234
|
+
visited_set: set[Node] = set()
|
235
|
+
stack: list[tuple[Node, bool]] = [(start, False)]
|
214
236
|
while stack:
|
215
237
|
node, processed = stack.pop()
|
216
238
|
if node in visited_set:
|
@@ -226,12 +248,12 @@ class MigrationGraph:
|
|
226
248
|
]
|
227
249
|
return visited
|
228
250
|
|
229
|
-
def root_nodes(self, app=None):
|
251
|
+
def root_nodes(self, app: str | None = None) -> list[tuple[str, str]]:
|
230
252
|
"""
|
231
253
|
Return all root nodes - that is, nodes with no dependencies inside
|
232
254
|
their app. These are the starting point for an app.
|
233
255
|
"""
|
234
|
-
roots = set()
|
256
|
+
roots: set[tuple[str, str]] = set()
|
235
257
|
for node in self.nodes:
|
236
258
|
if all(key[0] != node[0] for key in self.node_map[node].parents) and (
|
237
259
|
not app or app == node[0]
|
@@ -239,7 +261,7 @@ class MigrationGraph:
|
|
239
261
|
roots.add(node)
|
240
262
|
return sorted(roots)
|
241
263
|
|
242
|
-
def leaf_nodes(self, app=None):
|
264
|
+
def leaf_nodes(self, app: str | None = None) -> list[tuple[str, str]]:
|
243
265
|
"""
|
244
266
|
Return all leaf nodes - that is, nodes with no dependents in their app.
|
245
267
|
These are the "most current" version of an app's schema.
|
@@ -247,7 +269,7 @@ class MigrationGraph:
|
|
247
269
|
gets handled further up, in the interactive command - it's usually the
|
248
270
|
result of a VCS merge and needs some user input.
|
249
271
|
"""
|
250
|
-
leaves = set()
|
272
|
+
leaves: set[tuple[str, str]] = set()
|
251
273
|
for node in self.nodes:
|
252
274
|
if all(key[0] != node[0] for key in self.node_map[node].children) and (
|
253
275
|
not app or app == node[0]
|
@@ -255,13 +277,13 @@ class MigrationGraph:
|
|
255
277
|
leaves.add(node)
|
256
278
|
return sorted(leaves)
|
257
279
|
|
258
|
-
def ensure_not_cyclic(self):
|
280
|
+
def ensure_not_cyclic(self) -> None:
|
259
281
|
# Algo from GvR:
|
260
282
|
# https://neopythonic.blogspot.com/2009/01/detecting-cycles-in-directed-graph.html
|
261
|
-
todo = set(self.nodes)
|
283
|
+
todo: set[tuple[str, str]] = set(self.nodes)
|
262
284
|
while todo:
|
263
285
|
node = todo.pop()
|
264
|
-
stack = [node]
|
286
|
+
stack: list[tuple[str, str]] = [node]
|
265
287
|
while stack:
|
266
288
|
top = stack[-1]
|
267
289
|
for child in self.node_map[top].children:
|
@@ -280,27 +302,34 @@ class MigrationGraph:
|
|
280
302
|
else:
|
281
303
|
node = stack.pop()
|
282
304
|
|
283
|
-
def __str__(self):
|
305
|
+
def __str__(self) -> str:
|
284
306
|
return "Graph: {} nodes, {} edges".format(*self._nodes_and_edges())
|
285
307
|
|
286
|
-
def __repr__(self):
|
308
|
+
def __repr__(self) -> str:
|
287
309
|
nodes, edges = self._nodes_and_edges()
|
288
310
|
return f"<{self.__class__.__name__}: nodes={nodes}, edges={edges}>"
|
289
311
|
|
290
|
-
def _nodes_and_edges(self):
|
312
|
+
def _nodes_and_edges(self) -> tuple[int, int]:
|
291
313
|
return len(self.nodes), sum(
|
292
314
|
len(node.parents) for node in self.node_map.values()
|
293
315
|
)
|
294
316
|
|
295
|
-
def _generate_plan(
|
296
|
-
|
317
|
+
def _generate_plan(
|
318
|
+
self, nodes: list[tuple[str, str]], at_end: bool
|
319
|
+
) -> list[tuple[str, str]]:
|
320
|
+
plan: list[tuple[str, str]] = []
|
297
321
|
for node in nodes:
|
298
322
|
for migration in self.forwards_plan(node):
|
299
323
|
if migration not in plan and (at_end or migration not in nodes):
|
300
324
|
plan.append(migration)
|
301
325
|
return plan
|
302
326
|
|
303
|
-
def make_state(
|
327
|
+
def make_state(
|
328
|
+
self,
|
329
|
+
nodes: tuple[str, str] | list[tuple[str, str]] | None = None,
|
330
|
+
at_end: bool = True,
|
331
|
+
real_packages: Any = None,
|
332
|
+
) -> ProjectState:
|
304
333
|
"""
|
305
334
|
Given a migration node or nodes, return a complete ProjectState for it.
|
306
335
|
If at_end is False, return the state before the migration has run.
|
@@ -311,12 +340,12 @@ class MigrationGraph:
|
|
311
340
|
if not nodes:
|
312
341
|
return ProjectState()
|
313
342
|
if not isinstance(nodes[0], tuple):
|
314
|
-
nodes = [nodes]
|
343
|
+
nodes = [nodes] # type: ignore[list-item]
|
315
344
|
plan = self._generate_plan(nodes, at_end)
|
316
345
|
project_state = ProjectState(real_packages=real_packages)
|
317
346
|
for node in plan:
|
318
|
-
project_state = self.nodes[node].mutate_state(project_state, preserve=False)
|
347
|
+
project_state = self.nodes[node].mutate_state(project_state, preserve=False) # type: ignore[union-attr]
|
319
348
|
return project_state
|
320
349
|
|
321
|
-
def __contains__(self, node):
|
350
|
+
def __contains__(self, node: tuple[str, str]) -> bool:
|
322
351
|
return node in self.nodes
|
@@ -1,6 +1,9 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import pkgutil
|
2
4
|
import sys
|
3
5
|
from importlib import import_module, reload
|
6
|
+
from typing import TYPE_CHECKING, Any
|
4
7
|
|
5
8
|
from plain.models.migrations.graph import MigrationGraph
|
6
9
|
from plain.models.migrations.recorder import MigrationRecorder
|
@@ -13,6 +16,10 @@ from .exceptions import (
|
|
13
16
|
NodeNotFoundError,
|
14
17
|
)
|
15
18
|
|
19
|
+
if TYPE_CHECKING:
|
20
|
+
from plain.models.backends.base.base import BaseDatabaseWrapper
|
21
|
+
from plain.models.migrations.migration import Migration
|
22
|
+
|
16
23
|
MIGRATIONS_MODULE_NAME = "migrations"
|
17
24
|
|
18
25
|
|
@@ -43,21 +50,25 @@ class MigrationLoader:
|
|
43
50
|
|
44
51
|
def __init__(
|
45
52
|
self,
|
46
|
-
connection,
|
47
|
-
load=True,
|
48
|
-
ignore_no_migrations=False,
|
49
|
-
replace_migrations=True,
|
53
|
+
connection: BaseDatabaseWrapper | None,
|
54
|
+
load: bool = True,
|
55
|
+
ignore_no_migrations: bool = False,
|
56
|
+
replace_migrations: bool = True,
|
50
57
|
):
|
51
58
|
self.connection = connection
|
52
|
-
self.disk_migrations = None
|
53
|
-
self.applied_migrations = None
|
59
|
+
self.disk_migrations: dict[tuple[str, str], Migration] | None = None
|
60
|
+
self.applied_migrations: dict[tuple[str, str], Any] | None = None
|
54
61
|
self.ignore_no_migrations = ignore_no_migrations
|
55
62
|
self.replace_migrations = replace_migrations
|
63
|
+
self.unmigrated_packages: set[str]
|
64
|
+
self.migrated_packages: set[str]
|
65
|
+
self.graph: MigrationGraph
|
66
|
+
self.replacements: dict[tuple[str, str], Migration]
|
56
67
|
if load:
|
57
68
|
self.build_graph()
|
58
69
|
|
59
70
|
@classmethod
|
60
|
-
def migrations_module(cls, package_label):
|
71
|
+
def migrations_module(cls, package_label: str) -> tuple[str | None, bool]:
|
61
72
|
"""
|
62
73
|
Return the path to the migrations module for the specified package_label
|
63
74
|
and a boolean indicating if the module is specified in
|
@@ -71,7 +82,7 @@ class MigrationLoader:
|
|
71
82
|
app = packages_registry.get_package_config(package_label)
|
72
83
|
return f"{app.name}.{MIGRATIONS_MODULE_NAME}", False
|
73
84
|
|
74
|
-
def load_disk(self):
|
85
|
+
def load_disk(self) -> None:
|
75
86
|
"""Load the migrations from all INSTALLED_PACKAGES from disk."""
|
76
87
|
self.disk_migrations = {}
|
77
88
|
self.unmigrated_packages = set()
|
@@ -87,7 +98,9 @@ class MigrationLoader:
|
|
87
98
|
module = import_module(module_name)
|
88
99
|
except ModuleNotFoundError as e:
|
89
100
|
if (explicit and self.ignore_no_migrations) or (
|
90
|
-
not explicit
|
101
|
+
not explicit
|
102
|
+
and e.name is not None
|
103
|
+
and MIGRATIONS_MODULE_NAME in e.name.split(".")
|
91
104
|
):
|
92
105
|
self.unmigrated_packages.add(package_config.package_label)
|
93
106
|
continue
|
@@ -132,17 +145,19 @@ class MigrationLoader:
|
|
132
145
|
f"Migration {migration_name} in app {package_config.package_label} has no Migration class"
|
133
146
|
)
|
134
147
|
self.disk_migrations[package_config.package_label, migration_name] = (
|
135
|
-
migration_module.Migration(
|
148
|
+
migration_module.Migration( # type: ignore[call-non-callable]
|
136
149
|
migration_name,
|
137
150
|
package_config.package_label,
|
138
151
|
)
|
139
152
|
)
|
140
153
|
|
141
|
-
def get_migration(self, package_label, name_prefix):
|
154
|
+
def get_migration(self, package_label: str, name_prefix: str) -> Migration | None:
|
142
155
|
"""Return the named migration or raise NodeNotFoundError."""
|
143
156
|
return self.graph.nodes[package_label, name_prefix]
|
144
157
|
|
145
|
-
def get_migration_by_prefix(
|
158
|
+
def get_migration_by_prefix(
|
159
|
+
self, package_label: str, name_prefix: str
|
160
|
+
) -> Migration:
|
146
161
|
"""
|
147
162
|
Return the migration(s) which match the given app label and name_prefix.
|
148
163
|
"""
|
@@ -165,7 +180,9 @@ class MigrationLoader:
|
|
165
180
|
else:
|
166
181
|
return self.disk_migrations[results[0]]
|
167
182
|
|
168
|
-
def check_key(
|
183
|
+
def check_key(
|
184
|
+
self, key: tuple[str, str], current_package: str
|
185
|
+
) -> tuple[str, str] | None:
|
169
186
|
if (key[1] != "__first__" and key[1] != "__latest__") or key in self.graph:
|
170
187
|
return key
|
171
188
|
# Special-case __first__, which means "the first migration" for
|
@@ -174,12 +191,12 @@ class MigrationLoader:
|
|
174
191
|
# migrations.
|
175
192
|
if key[0] == current_package:
|
176
193
|
# Ignore __first__ references to the same app (#22325)
|
177
|
-
return
|
194
|
+
return None
|
178
195
|
if key[0] in self.unmigrated_packages:
|
179
196
|
# This app isn't migrated, but something depends on it.
|
180
197
|
# The models will get auto-added into the state, though
|
181
198
|
# so we're fine.
|
182
|
-
return
|
199
|
+
return None
|
183
200
|
if key[0] in self.migrated_packages:
|
184
201
|
try:
|
185
202
|
if key[1] == "__first__":
|
@@ -193,7 +210,9 @@ class MigrationLoader:
|
|
193
210
|
raise ValueError(f"Dependency on app with no migrations: {key[0]}")
|
194
211
|
raise ValueError(f"Dependency on unknown app: {key[0]}")
|
195
212
|
|
196
|
-
def add_internal_dependencies(
|
213
|
+
def add_internal_dependencies(
|
214
|
+
self, key: tuple[str, str], migration: Migration
|
215
|
+
) -> None:
|
197
216
|
"""
|
198
217
|
Internal dependencies need to be added first to ensure `__first__`
|
199
218
|
dependencies find the correct root node.
|
@@ -203,7 +222,9 @@ class MigrationLoader:
|
|
203
222
|
if parent[0] == key[0] and parent[1] != "__first__":
|
204
223
|
self.graph.add_dependency(migration, key, parent, skip_validation=True)
|
205
224
|
|
206
|
-
def add_external_dependencies(
|
225
|
+
def add_external_dependencies(
|
226
|
+
self, key: tuple[str, str], migration: Migration
|
227
|
+
) -> None:
|
207
228
|
for parent in migration.dependencies:
|
208
229
|
# Skip internal dependencies
|
209
230
|
if key[0] == parent[0]:
|
@@ -212,7 +233,7 @@ class MigrationLoader:
|
|
212
233
|
if parent is not None:
|
213
234
|
self.graph.add_dependency(migration, key, parent, skip_validation=True)
|
214
235
|
|
215
|
-
def build_graph(self):
|
236
|
+
def build_graph(self) -> None:
|
216
237
|
"""
|
217
238
|
Build a migration dependency graph using both the disk and database.
|
218
239
|
You'll need to rebuild the graph if you apply migrations. This isn't
|
@@ -295,7 +316,7 @@ class MigrationLoader:
|
|
295
316
|
raise
|
296
317
|
self.graph.ensure_not_cyclic()
|
297
318
|
|
298
|
-
def check_consistent_history(self, connection):
|
319
|
+
def check_consistent_history(self, connection: BaseDatabaseWrapper) -> None:
|
299
320
|
"""
|
300
321
|
Raise InconsistentMigrationHistory if any applied migrations have
|
301
322
|
unapplied dependencies.
|
@@ -320,7 +341,7 @@ class MigrationLoader:
|
|
320
341
|
f"{parent[0]}.{parent[1]} on the database."
|
321
342
|
)
|
322
343
|
|
323
|
-
def detect_conflicts(self):
|
344
|
+
def detect_conflicts(self) -> dict[str, list[str]]:
|
324
345
|
"""
|
325
346
|
Look through the loaded graph and detect any conflicts - packages
|
326
347
|
with more than one leaf migration. Return a dict of the app labels
|
@@ -337,7 +358,9 @@ class MigrationLoader:
|
|
337
358
|
for package_label in conflicting_packages
|
338
359
|
}
|
339
360
|
|
340
|
-
def project_state(
|
361
|
+
def project_state(
|
362
|
+
self, nodes: tuple[str, str] | None = None, at_end: bool = True
|
363
|
+
) -> Any:
|
341
364
|
"""
|
342
365
|
Return a ProjectState object representing the most recent state
|
343
366
|
that the loaded migrations represent.
|
@@ -348,7 +371,7 @@ class MigrationLoader:
|
|
348
371
|
nodes=nodes, at_end=at_end, real_packages=self.unmigrated_packages
|
349
372
|
)
|
350
373
|
|
351
|
-
def collect_sql(self, plan):
|
374
|
+
def collect_sql(self, plan: list[Migration]) -> list[str]:
|
352
375
|
"""
|
353
376
|
Take a migration plan and return a list of collected SQL statements
|
354
377
|
that represent the best-efforts version of that plan.
|
@@ -1,4 +1,7 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import re
|
4
|
+
from typing import Any
|
2
5
|
|
3
6
|
from plain.models.migrations.utils import get_migration_name_timestamp
|
4
7
|
from plain.models.transaction import atomic
|
@@ -22,29 +25,29 @@ class Migration:
|
|
22
25
|
"""
|
23
26
|
|
24
27
|
# Operations to apply during this migration, in order.
|
25
|
-
operations = []
|
28
|
+
operations: list[Any] = []
|
26
29
|
|
27
30
|
# Other migrations that should be run before this migration.
|
28
31
|
# Should be a list of (app, migration_name).
|
29
|
-
dependencies = []
|
32
|
+
dependencies: list[tuple[str, str]] = []
|
30
33
|
|
31
34
|
# Migration names in this app that this migration replaces. If this is
|
32
35
|
# non-empty, this migration will only be applied if all these migrations
|
33
36
|
# are not applied.
|
34
|
-
replaces = []
|
37
|
+
replaces: list[str] = []
|
35
38
|
|
36
39
|
# Is this an initial migration? Initial migrations are skipped on
|
37
40
|
# --fake-initial if the table or fields already exist. If None, check if
|
38
41
|
# the migration has any dependencies to determine if there are dependencies
|
39
42
|
# to tell if db introspection needs to be done. If True, always perform
|
40
43
|
# introspection. If False, never perform introspection.
|
41
|
-
initial = None
|
44
|
+
initial: bool | None = None
|
42
45
|
|
43
46
|
# Whether to wrap the whole migration in a transaction. Only has an effect
|
44
47
|
# on database backends which support transactional DDL.
|
45
|
-
atomic = True
|
48
|
+
atomic: bool = True
|
46
49
|
|
47
|
-
def __init__(self, name, package_label):
|
50
|
+
def __init__(self, name: str, package_label: str) -> None:
|
48
51
|
self.name = name
|
49
52
|
self.package_label = package_label
|
50
53
|
# Copy dependencies & other attrs as we might mutate them at runtime
|
@@ -52,23 +55,23 @@ class Migration:
|
|
52
55
|
self.dependencies = list(self.__class__.dependencies)
|
53
56
|
self.replaces = list(self.__class__.replaces)
|
54
57
|
|
55
|
-
def __eq__(self, other):
|
58
|
+
def __eq__(self, other: object) -> bool:
|
56
59
|
return (
|
57
60
|
isinstance(other, Migration)
|
58
61
|
and self.name == other.name
|
59
62
|
and self.package_label == other.package_label
|
60
63
|
)
|
61
64
|
|
62
|
-
def __repr__(self):
|
65
|
+
def __repr__(self) -> str:
|
63
66
|
return f"<Migration {self.package_label}.{self.name}>"
|
64
67
|
|
65
|
-
def __str__(self):
|
68
|
+
def __str__(self) -> str:
|
66
69
|
return f"{self.package_label}.{self.name}"
|
67
70
|
|
68
|
-
def __hash__(self):
|
71
|
+
def __hash__(self) -> int:
|
69
72
|
return hash(f"{self.package_label}.{self.name}")
|
70
73
|
|
71
|
-
def mutate_state(self, project_state, preserve=True):
|
74
|
+
def mutate_state(self, project_state: Any, preserve: bool = True) -> Any:
|
72
75
|
"""
|
73
76
|
Take a ProjectState and return a new one with the migration's
|
74
77
|
operations applied to it. Preserve the original object state by
|
@@ -82,7 +85,9 @@ class Migration:
|
|
82
85
|
operation.state_forwards(self.package_label, new_state)
|
83
86
|
return new_state
|
84
87
|
|
85
|
-
def apply(
|
88
|
+
def apply(
|
89
|
+
self, project_state: Any, schema_editor: Any, collect_sql: bool = False
|
90
|
+
) -> Any:
|
86
91
|
"""
|
87
92
|
Take a project_state representing all migrations prior to this one
|
88
93
|
and a schema_editor for a live database and apply the migration
|
@@ -127,7 +132,7 @@ class Migration:
|
|
127
132
|
schema_editor.collected_sql.append("-- (no-op)")
|
128
133
|
return project_state
|
129
134
|
|
130
|
-
def suggest_name(self):
|
135
|
+
def suggest_name(self) -> str:
|
131
136
|
"""
|
132
137
|
Suggest a name for the operations this migration might represent. Names
|
133
138
|
are not guaranteed to be unique, but put some effort into the fallback
|
@@ -152,18 +157,18 @@ class Migration:
|
|
152
157
|
return name
|
153
158
|
|
154
159
|
|
155
|
-
class SettingsTuple(tuple):
|
160
|
+
class SettingsTuple(tuple): # type: ignore[type-arg]
|
156
161
|
"""
|
157
162
|
Subclass of tuple so Plain can tell this was originally a settings
|
158
163
|
dependency when it reads the migration file.
|
159
164
|
"""
|
160
165
|
|
161
|
-
def __new__(cls, value, setting):
|
166
|
+
def __new__(cls, value: tuple[str, str], setting: str) -> SettingsTuple:
|
162
167
|
self = tuple.__new__(cls, value)
|
163
|
-
self.setting = setting
|
164
|
-
return self
|
168
|
+
self.setting = setting # type: ignore[attr-defined]
|
169
|
+
return self # type: ignore[return-value]
|
165
170
|
|
166
171
|
|
167
|
-
def settings_dependency(value):
|
172
|
+
def settings_dependency(value: str) -> SettingsTuple:
|
168
173
|
"""Turn a setting value into a dependency."""
|
169
174
|
return SettingsTuple((value.split(".", 1)[0], "__first__"), value)
|