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.
Files changed (105) hide show
  1. plain/models/CHANGELOG.md +23 -0
  2. plain/models/aggregates.py +42 -19
  3. plain/models/backends/base/base.py +125 -105
  4. plain/models/backends/base/client.py +11 -3
  5. plain/models/backends/base/creation.py +22 -12
  6. plain/models/backends/base/features.py +10 -4
  7. plain/models/backends/base/introspection.py +29 -16
  8. plain/models/backends/base/operations.py +187 -91
  9. plain/models/backends/base/schema.py +267 -165
  10. plain/models/backends/base/validation.py +12 -3
  11. plain/models/backends/ddl_references.py +85 -43
  12. plain/models/backends/mysql/base.py +29 -26
  13. plain/models/backends/mysql/client.py +7 -2
  14. plain/models/backends/mysql/compiler.py +12 -3
  15. plain/models/backends/mysql/creation.py +5 -2
  16. plain/models/backends/mysql/features.py +24 -22
  17. plain/models/backends/mysql/introspection.py +22 -13
  18. plain/models/backends/mysql/operations.py +106 -39
  19. plain/models/backends/mysql/schema.py +48 -24
  20. plain/models/backends/mysql/validation.py +13 -6
  21. plain/models/backends/postgresql/base.py +41 -34
  22. plain/models/backends/postgresql/client.py +7 -2
  23. plain/models/backends/postgresql/creation.py +10 -5
  24. plain/models/backends/postgresql/introspection.py +15 -8
  25. plain/models/backends/postgresql/operations.py +109 -42
  26. plain/models/backends/postgresql/schema.py +85 -46
  27. plain/models/backends/sqlite3/_functions.py +151 -115
  28. plain/models/backends/sqlite3/base.py +37 -23
  29. plain/models/backends/sqlite3/client.py +7 -1
  30. plain/models/backends/sqlite3/creation.py +9 -5
  31. plain/models/backends/sqlite3/features.py +5 -3
  32. plain/models/backends/sqlite3/introspection.py +32 -16
  33. plain/models/backends/sqlite3/operations.py +125 -42
  34. plain/models/backends/sqlite3/schema.py +82 -58
  35. plain/models/backends/utils.py +52 -29
  36. plain/models/backups/cli.py +8 -6
  37. plain/models/backups/clients.py +16 -7
  38. plain/models/backups/core.py +24 -13
  39. plain/models/base.py +113 -74
  40. plain/models/cli.py +94 -63
  41. plain/models/config.py +1 -1
  42. plain/models/connections.py +23 -7
  43. plain/models/constraints.py +65 -47
  44. plain/models/database_url.py +1 -1
  45. plain/models/db.py +6 -2
  46. plain/models/deletion.py +66 -43
  47. plain/models/entrypoints.py +1 -1
  48. plain/models/enums.py +22 -11
  49. plain/models/exceptions.py +23 -8
  50. plain/models/expressions.py +440 -257
  51. plain/models/fields/__init__.py +253 -202
  52. plain/models/fields/json.py +120 -54
  53. plain/models/fields/mixins.py +12 -8
  54. plain/models/fields/related.py +284 -252
  55. plain/models/fields/related_descriptors.py +34 -25
  56. plain/models/fields/related_lookups.py +23 -11
  57. plain/models/fields/related_managers.py +81 -47
  58. plain/models/fields/reverse_related.py +58 -55
  59. plain/models/forms.py +89 -63
  60. plain/models/functions/comparison.py +71 -18
  61. plain/models/functions/datetime.py +79 -29
  62. plain/models/functions/math.py +43 -10
  63. plain/models/functions/mixins.py +24 -7
  64. plain/models/functions/text.py +104 -25
  65. plain/models/functions/window.py +12 -6
  66. plain/models/indexes.py +52 -28
  67. plain/models/lookups.py +228 -153
  68. plain/models/migrations/autodetector.py +86 -43
  69. plain/models/migrations/exceptions.py +7 -3
  70. plain/models/migrations/executor.py +33 -7
  71. plain/models/migrations/graph.py +79 -50
  72. plain/models/migrations/loader.py +45 -22
  73. plain/models/migrations/migration.py +23 -18
  74. plain/models/migrations/operations/base.py +37 -19
  75. plain/models/migrations/operations/fields.py +89 -42
  76. plain/models/migrations/operations/models.py +245 -143
  77. plain/models/migrations/operations/special.py +82 -25
  78. plain/models/migrations/optimizer.py +7 -2
  79. plain/models/migrations/questioner.py +58 -31
  80. plain/models/migrations/recorder.py +18 -11
  81. plain/models/migrations/serializer.py +50 -39
  82. plain/models/migrations/state.py +220 -133
  83. plain/models/migrations/utils.py +29 -13
  84. plain/models/migrations/writer.py +17 -14
  85. plain/models/options.py +63 -56
  86. plain/models/otel.py +16 -6
  87. plain/models/preflight.py +35 -12
  88. plain/models/query.py +323 -228
  89. plain/models/query_utils.py +93 -58
  90. plain/models/registry.py +34 -16
  91. plain/models/sql/compiler.py +146 -97
  92. plain/models/sql/datastructures.py +38 -25
  93. plain/models/sql/query.py +255 -169
  94. plain/models/sql/subqueries.py +32 -21
  95. plain/models/sql/where.py +54 -29
  96. plain/models/test/pytest.py +15 -11
  97. plain/models/test/utils.py +4 -2
  98. plain/models/transaction.py +20 -7
  99. plain/models/utils.py +13 -5
  100. {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/METADATA +1 -1
  101. plain_models-0.50.0.dist-info/RECORD +122 -0
  102. plain_models-0.49.1.dist-info/RECORD +0 -122
  103. {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/WHEEL +0 -0
  104. {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/entry_points.txt +0 -0
  105. {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/licenses/LICENSE +0 -0
@@ -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__(self, key, origin, error_message):
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(self, key, origin, error_message):
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(self, migration, child, parent, skip_validation=False):
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(self, replacement, replaced):
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
- replaced = set(replaced)
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 replaced:
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 replaced:
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 replaced:
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(self, replacement, replaced):
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(self, start, forwards=True):
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(self, nodes, at_end):
296
- plan = []
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(self, nodes=None, at_end=True, real_packages=None):
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 and MIGRATIONS_MODULE_NAME in e.name.split(".")
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(self, package_label, name_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(self, key, current_package):
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(self, key, migration):
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(self, key, migration):
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(self, nodes=None, at_end=True):
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(self, project_state, schema_editor, collect_sql=False):
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)