plain.models 0.49.2__py3-none-any.whl → 0.51.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 (108) hide show
  1. plain/models/CHANGELOG.md +27 -0
  2. plain/models/README.md +26 -42
  3. plain/models/__init__.py +2 -0
  4. plain/models/aggregates.py +42 -19
  5. plain/models/backends/base/base.py +125 -105
  6. plain/models/backends/base/client.py +11 -3
  7. plain/models/backends/base/creation.py +24 -14
  8. plain/models/backends/base/features.py +10 -4
  9. plain/models/backends/base/introspection.py +37 -20
  10. plain/models/backends/base/operations.py +187 -91
  11. plain/models/backends/base/schema.py +338 -218
  12. plain/models/backends/base/validation.py +13 -4
  13. plain/models/backends/ddl_references.py +85 -43
  14. plain/models/backends/mysql/base.py +29 -26
  15. plain/models/backends/mysql/client.py +7 -2
  16. plain/models/backends/mysql/compiler.py +13 -4
  17. plain/models/backends/mysql/creation.py +5 -2
  18. plain/models/backends/mysql/features.py +24 -22
  19. plain/models/backends/mysql/introspection.py +22 -13
  20. plain/models/backends/mysql/operations.py +107 -40
  21. plain/models/backends/mysql/schema.py +52 -28
  22. plain/models/backends/mysql/validation.py +13 -6
  23. plain/models/backends/postgresql/base.py +41 -34
  24. plain/models/backends/postgresql/client.py +7 -2
  25. plain/models/backends/postgresql/creation.py +10 -5
  26. plain/models/backends/postgresql/introspection.py +15 -8
  27. plain/models/backends/postgresql/operations.py +110 -43
  28. plain/models/backends/postgresql/schema.py +88 -49
  29. plain/models/backends/sqlite3/_functions.py +151 -115
  30. plain/models/backends/sqlite3/base.py +37 -23
  31. plain/models/backends/sqlite3/client.py +7 -1
  32. plain/models/backends/sqlite3/creation.py +9 -5
  33. plain/models/backends/sqlite3/features.py +5 -3
  34. plain/models/backends/sqlite3/introspection.py +32 -16
  35. plain/models/backends/sqlite3/operations.py +126 -43
  36. plain/models/backends/sqlite3/schema.py +127 -92
  37. plain/models/backends/utils.py +52 -29
  38. plain/models/backups/cli.py +8 -6
  39. plain/models/backups/clients.py +16 -7
  40. plain/models/backups/core.py +24 -13
  41. plain/models/base.py +221 -229
  42. plain/models/cli.py +98 -67
  43. plain/models/config.py +1 -1
  44. plain/models/connections.py +23 -7
  45. plain/models/constraints.py +79 -56
  46. plain/models/database_url.py +1 -1
  47. plain/models/db.py +6 -2
  48. plain/models/deletion.py +80 -56
  49. plain/models/entrypoints.py +1 -1
  50. plain/models/enums.py +22 -11
  51. plain/models/exceptions.py +23 -8
  52. plain/models/expressions.py +441 -258
  53. plain/models/fields/__init__.py +272 -217
  54. plain/models/fields/json.py +123 -57
  55. plain/models/fields/mixins.py +12 -8
  56. plain/models/fields/related.py +324 -290
  57. plain/models/fields/related_descriptors.py +33 -24
  58. plain/models/fields/related_lookups.py +24 -12
  59. plain/models/fields/related_managers.py +102 -79
  60. plain/models/fields/reverse_related.py +66 -63
  61. plain/models/forms.py +101 -75
  62. plain/models/functions/comparison.py +71 -18
  63. plain/models/functions/datetime.py +79 -29
  64. plain/models/functions/math.py +43 -10
  65. plain/models/functions/mixins.py +24 -7
  66. plain/models/functions/text.py +104 -25
  67. plain/models/functions/window.py +12 -6
  68. plain/models/indexes.py +57 -32
  69. plain/models/lookups.py +228 -153
  70. plain/models/meta.py +505 -0
  71. plain/models/migrations/autodetector.py +86 -43
  72. plain/models/migrations/exceptions.py +7 -3
  73. plain/models/migrations/executor.py +33 -7
  74. plain/models/migrations/graph.py +79 -50
  75. plain/models/migrations/loader.py +45 -22
  76. plain/models/migrations/migration.py +23 -18
  77. plain/models/migrations/operations/base.py +38 -20
  78. plain/models/migrations/operations/fields.py +95 -48
  79. plain/models/migrations/operations/models.py +246 -142
  80. plain/models/migrations/operations/special.py +82 -25
  81. plain/models/migrations/optimizer.py +7 -2
  82. plain/models/migrations/questioner.py +58 -31
  83. plain/models/migrations/recorder.py +27 -16
  84. plain/models/migrations/serializer.py +50 -39
  85. plain/models/migrations/state.py +232 -156
  86. plain/models/migrations/utils.py +30 -14
  87. plain/models/migrations/writer.py +17 -14
  88. plain/models/options.py +189 -518
  89. plain/models/otel.py +16 -6
  90. plain/models/preflight.py +42 -17
  91. plain/models/query.py +400 -251
  92. plain/models/query_utils.py +109 -69
  93. plain/models/registry.py +40 -21
  94. plain/models/sql/compiler.py +190 -127
  95. plain/models/sql/datastructures.py +38 -25
  96. plain/models/sql/query.py +320 -225
  97. plain/models/sql/subqueries.py +36 -25
  98. plain/models/sql/where.py +54 -29
  99. plain/models/test/pytest.py +15 -11
  100. plain/models/test/utils.py +4 -2
  101. plain/models/transaction.py +20 -7
  102. plain/models/utils.py +17 -6
  103. {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/METADATA +27 -43
  104. plain_models-0.51.0.dist-info/RECORD +123 -0
  105. plain_models-0.49.2.dist-info/RECORD +0 -122
  106. {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/WHEEL +0 -0
  107. {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/entry_points.txt +0 -0
  108. {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING, Any, cast
5
+
1
6
  from .base import Operation
2
7
 
8
+ if TYPE_CHECKING:
9
+ from plain.models.backends.base.schema import BaseDatabaseSchemaEditor
10
+ from plain.models.migrations.state import ProjectState
11
+
3
12
 
4
13
  class SeparateDatabaseAndState(Operation):
5
14
  """
@@ -11,23 +20,33 @@ class SeparateDatabaseAndState(Operation):
11
20
 
12
21
  serialization_expand_args = ["database_operations", "state_operations"]
13
22
 
14
- def __init__(self, database_operations=None, state_operations=None):
23
+ def __init__(
24
+ self,
25
+ database_operations: list[Operation] | None = None,
26
+ state_operations: list[Operation] | None = None,
27
+ ) -> None:
15
28
  self.database_operations = database_operations or []
16
29
  self.state_operations = state_operations or []
17
30
 
18
- def deconstruct(self):
19
- kwargs = {}
31
+ def deconstruct(self) -> tuple[str, list[Any], dict[str, list[Operation]]]:
32
+ kwargs: dict[str, list[Operation]] = {}
20
33
  if self.database_operations:
21
34
  kwargs["database_operations"] = self.database_operations
22
35
  if self.state_operations:
23
36
  kwargs["state_operations"] = self.state_operations
24
37
  return (self.__class__.__qualname__, [], kwargs)
25
38
 
26
- def state_forwards(self, package_label, state):
39
+ def state_forwards(self, package_label: str, state: ProjectState) -> None:
27
40
  for state_operation in self.state_operations:
28
41
  state_operation.state_forwards(package_label, state)
29
42
 
30
- def database_forwards(self, package_label, schema_editor, from_state, to_state):
43
+ def database_forwards(
44
+ self,
45
+ package_label: str,
46
+ schema_editor: BaseDatabaseSchemaEditor,
47
+ from_state: ProjectState,
48
+ to_state: ProjectState,
49
+ ) -> None:
31
50
  # We calculate state separately in here since our state functions aren't useful
32
51
  for database_operation in self.database_operations:
33
52
  to_state = from_state.clone()
@@ -37,7 +56,7 @@ class SeparateDatabaseAndState(Operation):
37
56
  )
38
57
  from_state = to_state
39
58
 
40
- def describe(self):
59
+ def describe(self) -> str:
41
60
  return "Custom state/database change combination"
42
61
 
43
62
 
@@ -49,42 +68,68 @@ class RunSQL(Operation):
49
68
  by this SQL change, in case it's custom column/table creation/deletion.
50
69
  """
51
70
 
52
- def __init__(self, sql, *, state_operations=None, elidable=False):
71
+ def __init__(
72
+ self,
73
+ sql: str
74
+ | list[str | tuple[str, list[Any]]]
75
+ | tuple[str | tuple[str, list[Any]], ...],
76
+ *,
77
+ state_operations: list[Operation] | None = None,
78
+ elidable: bool = False,
79
+ ) -> None:
53
80
  self.sql = sql
54
81
  self.state_operations = state_operations or []
55
82
  self.elidable = elidable
56
83
 
57
- def deconstruct(self):
58
- kwargs = {
84
+ def deconstruct(self) -> tuple[str, list[Any], dict[str, Any]]:
85
+ kwargs: dict[str, Any] = {
59
86
  "sql": self.sql,
60
87
  }
61
88
  if self.state_operations:
62
89
  kwargs["state_operations"] = self.state_operations
63
90
  return (self.__class__.__qualname__, [], kwargs)
64
91
 
65
- def state_forwards(self, package_label, state):
92
+ def state_forwards(self, package_label: str, state: ProjectState) -> None:
66
93
  for state_operation in self.state_operations:
67
94
  state_operation.state_forwards(package_label, state)
68
95
 
69
- def database_forwards(self, package_label, schema_editor, from_state, to_state):
96
+ def database_forwards(
97
+ self,
98
+ package_label: str,
99
+ schema_editor: BaseDatabaseSchemaEditor,
100
+ from_state: ProjectState,
101
+ to_state: ProjectState,
102
+ ) -> None:
70
103
  self._run_sql(schema_editor, self.sql)
71
104
 
72
- def describe(self):
105
+ def describe(self) -> str:
73
106
  return "Raw SQL operation"
74
107
 
75
- def _run_sql(self, schema_editor, sqls):
108
+ def _run_sql(
109
+ self,
110
+ schema_editor: BaseDatabaseSchemaEditor,
111
+ sqls: str
112
+ | list[str | tuple[str, list[Any]]]
113
+ | tuple[str | tuple[str, list[Any]], ...],
114
+ ) -> None:
76
115
  if isinstance(sqls, list | tuple):
77
- for sql in sqls:
78
- params = None
79
- if isinstance(sql, list | tuple):
80
- elements = len(sql)
116
+ for sql_item in sqls:
117
+ params: list[Any] | None = None
118
+ sql: str
119
+ if isinstance(sql_item, list | tuple):
120
+ elements = len(sql_item)
81
121
  if elements == 2:
82
- sql, params = sql
122
+ sql, params = sql_item # type: ignore[misc]
83
123
  else:
84
124
  raise ValueError("Expected a 2-tuple but got %d" % elements) # noqa: UP031
125
+ else:
126
+ sql = sql_item
85
127
  schema_editor.execute(sql, params=params)
86
128
  else:
87
- statements = schema_editor.connection.ops.prepare_sql_script(sqls)
129
+ # sqls is a str in this branch
130
+ statements = schema_editor.connection.ops.prepare_sql_script(
131
+ cast(str, sqls)
132
+ )
88
133
  for statement in statements:
89
134
  schema_editor.execute(statement, params=None)
90
135
 
@@ -96,7 +141,13 @@ class RunPython(Operation):
96
141
 
97
142
  reduces_to_sql = False
98
143
 
99
- def __init__(self, code, *, atomic=None, elidable=False):
144
+ def __init__(
145
+ self,
146
+ code: Callable[..., Any],
147
+ *,
148
+ atomic: bool | None = None,
149
+ elidable: bool = False,
150
+ ) -> None:
100
151
  self.atomic = atomic
101
152
  # Forwards code
102
153
  if not callable(code):
@@ -104,20 +155,26 @@ class RunPython(Operation):
104
155
  self.code = code
105
156
  self.elidable = elidable
106
157
 
107
- def deconstruct(self):
108
- kwargs = {
158
+ def deconstruct(self) -> tuple[str, list[Any], dict[str, Any]]:
159
+ kwargs: dict[str, Any] = {
109
160
  "code": self.code,
110
161
  }
111
162
  if self.atomic is not None:
112
163
  kwargs["atomic"] = self.atomic
113
164
  return (self.__class__.__qualname__, [], kwargs)
114
165
 
115
- def state_forwards(self, package_label, state):
166
+ def state_forwards(self, package_label: str, state: Any) -> None:
116
167
  # RunPython objects have no state effect. To add some, combine this
117
168
  # with SeparateDatabaseAndState.
118
169
  pass
119
170
 
120
- def database_forwards(self, package_label, schema_editor, from_state, to_state):
171
+ def database_forwards(
172
+ self,
173
+ package_label: str,
174
+ schema_editor: BaseDatabaseSchemaEditor,
175
+ from_state: ProjectState,
176
+ to_state: ProjectState,
177
+ ) -> None:
121
178
  # RunPython has access to all models. Ensure that all models are
122
179
  # reloaded in case any are delayed.
123
180
  from_state.clear_delayed_models_cache()
@@ -127,5 +184,5 @@ class RunPython(Operation):
127
184
  # use direct imports, so we go with a documentation approach instead.
128
185
  self.code(from_state.models_registry, schema_editor)
129
186
 
130
- def describe(self):
187
+ def describe(self) -> str:
131
188
  return "Raw Python operation"
@@ -1,3 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
1
6
  class MigrationOptimizer:
2
7
  """
3
8
  Power the optimization process, where you provide a list of Operations
@@ -9,7 +14,7 @@ class MigrationOptimizer:
9
14
  nothing.
10
15
  """
11
16
 
12
- def optimize(self, operations, package_label):
17
+ def optimize(self, operations: list[Any], package_label: str) -> list[Any]:
13
18
  """
14
19
  Main optimization entry point. Pass in a list of Operation instances,
15
20
  get out a new list of Operation instances.
@@ -35,7 +40,7 @@ class MigrationOptimizer:
35
40
  return result
36
41
  operations = result
37
42
 
38
- def optimize_inner(self, operations, package_label):
43
+ def optimize_inner(self, operations: list[Any], package_label: str) -> list[Any]:
39
44
  """Inner optimization loop."""
40
45
  new_operations = []
41
46
  for i, operation in enumerate(operations):
@@ -1,7 +1,11 @@
1
+ from __future__ import annotations
2
+
1
3
  import datetime
2
4
  import importlib
3
5
  import os
4
6
  import sys
7
+ from collections.abc import Callable
8
+ from typing import TYPE_CHECKING, Any
5
9
 
6
10
  import click
7
11
 
@@ -11,6 +15,9 @@ from plain.utils import timezone
11
15
 
12
16
  from .loader import MigrationLoader
13
17
 
18
+ if TYPE_CHECKING:
19
+ from plain.models.fields import Field
20
+
14
21
 
15
22
  class MigrationQuestioner:
16
23
  """
@@ -19,12 +26,17 @@ class MigrationQuestioner:
19
26
  interactive subclass is what the command-line arguments will use.
20
27
  """
21
28
 
22
- def __init__(self, defaults=None, specified_packages=None, dry_run=None):
29
+ def __init__(
30
+ self,
31
+ defaults: dict[str, Any] | None = None,
32
+ specified_packages: set[str] | None = None,
33
+ dry_run: bool | None = None,
34
+ ) -> None:
23
35
  self.defaults = defaults or {}
24
36
  self.specified_packages = specified_packages or set()
25
37
  self.dry_run = dry_run
26
38
 
27
- def ask_initial(self, package_label):
39
+ def ask_initial(self, package_label: str) -> bool:
28
40
  """Should we create an initial migration for the app?"""
29
41
  # If it was specified on the command line, definitely true
30
42
  if package_label in self.specified_packages:
@@ -49,52 +61,61 @@ class MigrationQuestioner:
49
61
  return self.defaults.get("ask_initial", False)
50
62
  else:
51
63
  if getattr(migrations_module, "__file__", None):
52
- filenames = os.listdir(os.path.dirname(migrations_module.__file__))
64
+ filenames = os.listdir(os.path.dirname(migrations_module.__file__)) # type: ignore[arg-type]
53
65
  elif hasattr(migrations_module, "__path__"):
54
66
  if len(migrations_module.__path__) > 1:
55
67
  return False
56
68
  filenames = os.listdir(list(migrations_module.__path__)[0])
57
69
  return not any(x.endswith(".py") for x in filenames if x != "__init__.py")
58
70
 
59
- def ask_not_null_addition(self, field_name, model_name):
71
+ def ask_not_null_addition(self, field_name: str, model_name: str) -> Any:
60
72
  """Adding a NOT NULL field to a model."""
61
73
  # None means quit
62
74
  return None
63
75
 
64
- def ask_not_null_alteration(self, field_name, model_name):
76
+ def ask_not_null_alteration(self, field_name: str, model_name: str) -> Any:
65
77
  """Changing a NULL field to NOT NULL."""
66
78
  # None means quit
67
79
  return None
68
80
 
69
- def ask_rename(self, model_name, old_name, new_name, field_instance):
81
+ def ask_rename(
82
+ self, model_name: str, old_name: str, new_name: str, field_instance: Field
83
+ ) -> bool:
70
84
  """Was this field really renamed?"""
71
85
  return self.defaults.get("ask_rename", False)
72
86
 
73
- def ask_rename_model(self, old_model_state, new_model_state):
87
+ def ask_rename_model(self, old_model_state: Any, new_model_state: Any) -> bool:
74
88
  """Was this model really renamed?"""
75
89
  return self.defaults.get("ask_rename_model", False)
76
90
 
77
- def ask_auto_now_add_addition(self, field_name, model_name):
91
+ def ask_auto_now_add_addition(self, field_name: str, model_name: str) -> Any:
78
92
  """Adding an auto_now_add field to a model."""
79
93
  # None means quit
80
94
  return None
81
95
 
82
- def ask_unique_callable_default_addition(self, field_name, model_name):
96
+ def ask_unique_callable_default_addition(
97
+ self, field_name: str, model_name: str
98
+ ) -> Any:
83
99
  """Adding a unique field with a callable default."""
84
100
  # None means continue.
85
101
  return None
86
102
 
87
103
 
88
104
  class InteractiveMigrationQuestioner(MigrationQuestioner):
89
- def __init__(self, defaults=None, specified_packages=None, dry_run=None):
105
+ def __init__(
106
+ self,
107
+ defaults: dict[str, Any] | None = None,
108
+ specified_packages: set[str] | None = None,
109
+ dry_run: bool | None = None,
110
+ ) -> None:
90
111
  super().__init__(
91
112
  defaults=defaults, specified_packages=specified_packages, dry_run=dry_run
92
113
  )
93
114
 
94
- def _boolean_input(self, question, default=None):
115
+ def _boolean_input(self, question: str, default: bool | None = None) -> bool:
95
116
  return click.confirm(question, default=default)
96
117
 
97
- def _choice_input(self, question, choices):
118
+ def _choice_input(self, question: str, choices: list[str]) -> int:
98
119
  choice_map = {str(i + 1): choice for i, choice in enumerate(choices)}
99
120
  choice_map_str = "\n".join(
100
121
  [f"{i}) {choice}" for i, choice in choice_map.items()]
@@ -105,7 +126,7 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
105
126
  )
106
127
  return int(choice)
107
128
 
108
- def _ask_default(self, default=""):
129
+ def _ask_default(self, default: str = "") -> Any:
109
130
  """
110
131
  Prompt for a default value.
111
132
 
@@ -144,7 +165,7 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
144
165
  except (SyntaxError, NameError) as e:
145
166
  click.echo(f"Invalid input: {e}")
146
167
 
147
- def ask_not_null_addition(self, field_name, model_name):
168
+ def ask_not_null_addition(self, field_name: str, model_name: str) -> Any:
148
169
  """Adding a NOT NULL field to a model."""
149
170
  if not self.dry_run:
150
171
  choice = self._choice_input(
@@ -167,7 +188,7 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
167
188
  return self._ask_default()
168
189
  return None
169
190
 
170
- def ask_not_null_alteration(self, field_name, model_name):
191
+ def ask_not_null_alteration(self, field_name: str, model_name: str) -> Any:
171
192
  """Changing a NULL field to NOT NULL."""
172
193
  if not self.dry_run:
173
194
  choice = self._choice_input(
@@ -195,7 +216,9 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
195
216
  return self._ask_default()
196
217
  return None
197
218
 
198
- def ask_rename(self, model_name, old_name, new_name, field_instance):
219
+ def ask_rename(
220
+ self, model_name: str, old_name: str, new_name: str, field_instance: Field
221
+ ) -> bool:
199
222
  """Was this field really renamed?"""
200
223
  msg = "Was %s.%s renamed to %s.%s (a %s)?"
201
224
  return self._boolean_input(
@@ -210,7 +233,7 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
210
233
  default=False,
211
234
  )
212
235
 
213
- def ask_rename_model(self, old_model_state, new_model_state):
236
+ def ask_rename_model(self, old_model_state: Any, new_model_state: Any) -> bool:
214
237
  """Was this model really renamed?"""
215
238
  msg = "Was the model %s.%s renamed to %s?"
216
239
  return self._boolean_input(
@@ -223,7 +246,7 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
223
246
  default=False,
224
247
  )
225
248
 
226
- def ask_auto_now_add_addition(self, field_name, model_name):
249
+ def ask_auto_now_add_addition(self, field_name: str, model_name: str) -> Any:
227
250
  """Adding an auto_now_add field to a model."""
228
251
  if not self.dry_run:
229
252
  choice = self._choice_input(
@@ -243,7 +266,9 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
243
266
  return self._ask_default(default="timezone.now")
244
267
  return None
245
268
 
246
- def ask_unique_callable_default_addition(self, field_name, model_name):
269
+ def ask_unique_callable_default_addition(
270
+ self, field_name: str, model_name: str
271
+ ) -> Any:
247
272
  """Adding a unique field with a callable default."""
248
273
  if not self.dry_run:
249
274
  choice = self._choice_input(
@@ -264,12 +289,12 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
264
289
  class NonInteractiveMigrationQuestioner(MigrationQuestioner):
265
290
  def __init__(
266
291
  self,
267
- defaults=None,
268
- specified_packages=None,
269
- dry_run=None,
270
- verbosity=1,
271
- log=None,
272
- ):
292
+ defaults: dict[str, Any] | None = None,
293
+ specified_packages: set[str] | None = None,
294
+ dry_run: bool | None = None,
295
+ verbosity: int = 1,
296
+ log: Callable[[str], Any] | None = None,
297
+ ) -> None:
273
298
  self.verbosity = verbosity
274
299
  self.log = log
275
300
  super().__init__(
@@ -278,13 +303,15 @@ class NonInteractiveMigrationQuestioner(MigrationQuestioner):
278
303
  dry_run=dry_run,
279
304
  )
280
305
 
281
- def log_lack_of_migration(self, field_name, model_name, reason):
306
+ def log_lack_of_migration(
307
+ self, field_name: str, model_name: str, reason: str
308
+ ) -> None:
282
309
  if self.verbosity > 0:
283
- self.log(
310
+ self.log( # type: ignore[misc]
284
311
  f"Field '{field_name}' on model '{model_name}' not migrated: {reason}."
285
312
  )
286
313
 
287
- def ask_not_null_addition(self, field_name, model_name):
314
+ def ask_not_null_addition(self, field_name: str, model_name: str) -> Any:
288
315
  # We can't ask the user, so act like the user aborted.
289
316
  self.log_lack_of_migration(
290
317
  field_name,
@@ -293,15 +320,15 @@ class NonInteractiveMigrationQuestioner(MigrationQuestioner):
293
320
  )
294
321
  sys.exit(3)
295
322
 
296
- def ask_not_null_alteration(self, field_name, model_name):
323
+ def ask_not_null_alteration(self, field_name: str, model_name: str) -> Any:
297
324
  # We can't ask the user, so set as not provided.
298
- self.log(
325
+ self.log( # type: ignore[misc]
299
326
  f"Field '{field_name}' on model '{model_name}' given a default of "
300
327
  f"NOT PROVIDED and must be corrected."
301
328
  )
302
329
  return NOT_PROVIDED
303
330
 
304
- def ask_auto_now_add_addition(self, field_name, model_name):
331
+ def ask_auto_now_add_addition(self, field_name: str, model_name: str) -> Any:
305
332
  # We can't ask the user, so act like the user aborted.
306
333
  self.log_lack_of_migration(
307
334
  field_name,
@@ -1,11 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
1
5
  from plain import models
2
6
  from plain.models.db import DatabaseError
7
+ from plain.models.meta import Meta
3
8
  from plain.models.registry import ModelsRegistry
4
9
  from plain.utils.functional import classproperty
5
10
  from plain.utils.timezone import now
6
11
 
7
12
  from .exceptions import MigrationSchemaMissing
8
13
 
14
+ if TYPE_CHECKING:
15
+ from plain.models.backends.base.base import BaseDatabaseWrapper
16
+
9
17
 
10
18
  class MigrationRecorder:
11
19
  """
@@ -20,10 +28,10 @@ class MigrationRecorder:
20
28
  a row in the table always means a migration is applied.
21
29
  """
22
30
 
23
- _migration_class = None
31
+ _migration_class: type[models.Model] | None = None
24
32
 
25
33
  @classproperty
26
- def Migration(cls):
34
+ def Migration(cls) -> type[models.Model]: # type: ignore[misc]
27
35
  """
28
36
  Lazy load to avoid PackageRegistryNotReady if installed packages import
29
37
  MigrationRecorder.
@@ -37,31 +45,34 @@ class MigrationRecorder:
37
45
  name = models.CharField(max_length=255)
38
46
  applied = models.DateTimeField(default=now)
39
47
 
40
- class Meta:
41
- models_registry = _models_registry
42
- package_label = "migrations"
43
- db_table = "plainmigrations"
48
+ # Use isolated models registry for migrations
49
+ _model_meta = Meta(models_registry=_models_registry)
50
+
51
+ model_options = models.Options(
52
+ package_label="migrations",
53
+ db_table="plainmigrations",
54
+ )
44
55
 
45
- def __str__(self):
56
+ def __str__(self) -> str:
46
57
  return f"Migration {self.name} for {self.app}"
47
58
 
48
59
  cls._migration_class = Migration
49
60
  return cls._migration_class
50
61
 
51
- def __init__(self, connection):
62
+ def __init__(self, connection: BaseDatabaseWrapper) -> None:
52
63
  self.connection = connection
53
64
 
54
65
  @property
55
- def migration_qs(self):
66
+ def migration_qs(self) -> Any:
56
67
  return self.Migration.query.all()
57
68
 
58
- def has_table(self):
69
+ def has_table(self) -> bool:
59
70
  """Return True if the plainmigrations table exists."""
60
71
  with self.connection.cursor() as cursor:
61
72
  tables = self.connection.introspection.table_names(cursor)
62
- return self.Migration._meta.db_table in tables
73
+ return self.Migration.model_options.db_table in tables
63
74
 
64
- def ensure_schema(self):
75
+ def ensure_schema(self) -> None:
65
76
  """Ensure the table exists and has the correct schema."""
66
77
  # If the table's there, that's fine - we've never changed its schema
67
78
  # in the codebase.
@@ -76,7 +87,7 @@ class MigrationRecorder:
76
87
  f"Unable to create the plainmigrations table ({exc})"
77
88
  )
78
89
 
79
- def applied_migrations(self):
90
+ def applied_migrations(self) -> dict[tuple[str, str], Any]:
80
91
  """
81
92
  Return a dict mapping (package_name, migration_name) to Migration instances
82
93
  for all applied migrations.
@@ -91,16 +102,16 @@ class MigrationRecorder:
91
102
  # are applied.
92
103
  return {}
93
104
 
94
- def record_applied(self, app, name):
105
+ def record_applied(self, app: str, name: str) -> None:
95
106
  """Record that a migration was applied."""
96
107
  self.ensure_schema()
97
108
  self.migration_qs.create(app=app, name=name)
98
109
 
99
- def record_unapplied(self, app, name):
110
+ def record_unapplied(self, app: str, name: str) -> None:
100
111
  """Record that a migration was unapplied."""
101
112
  self.ensure_schema()
102
113
  self.migration_qs.filter(app=app, name=name).delete()
103
114
 
104
- def flush(self):
115
+ def flush(self) -> None:
105
116
  """Delete all migration records. Useful for testing migrations."""
106
117
  self.migration_qs.all().delete()