plain.postgres 0.84.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/postgres/CHANGELOG.md +1028 -0
- plain/postgres/README.md +925 -0
- plain/postgres/__init__.py +120 -0
- plain/postgres/agents/.claude/rules/plain-postgres.md +78 -0
- plain/postgres/aggregates.py +236 -0
- plain/postgres/backups/__init__.py +0 -0
- plain/postgres/backups/cli.py +148 -0
- plain/postgres/backups/clients.py +94 -0
- plain/postgres/backups/core.py +172 -0
- plain/postgres/base.py +1415 -0
- plain/postgres/cli/__init__.py +3 -0
- plain/postgres/cli/db.py +142 -0
- plain/postgres/cli/migrations.py +1085 -0
- plain/postgres/config.py +18 -0
- plain/postgres/connection.py +1331 -0
- plain/postgres/connections.py +77 -0
- plain/postgres/constants.py +13 -0
- plain/postgres/constraints.py +495 -0
- plain/postgres/database_url.py +94 -0
- plain/postgres/db.py +59 -0
- plain/postgres/default_settings.py +38 -0
- plain/postgres/deletion.py +475 -0
- plain/postgres/dialect.py +640 -0
- plain/postgres/entrypoints.py +4 -0
- plain/postgres/enums.py +103 -0
- plain/postgres/exceptions.py +217 -0
- plain/postgres/expressions.py +1912 -0
- plain/postgres/fields/__init__.py +2118 -0
- plain/postgres/fields/encrypted.py +354 -0
- plain/postgres/fields/json.py +413 -0
- plain/postgres/fields/mixins.py +30 -0
- plain/postgres/fields/related.py +1192 -0
- plain/postgres/fields/related_descriptors.py +290 -0
- plain/postgres/fields/related_lookups.py +223 -0
- plain/postgres/fields/related_managers.py +661 -0
- plain/postgres/fields/reverse_descriptors.py +229 -0
- plain/postgres/fields/reverse_related.py +328 -0
- plain/postgres/fields/timezones.py +143 -0
- plain/postgres/forms.py +773 -0
- plain/postgres/functions/__init__.py +189 -0
- plain/postgres/functions/comparison.py +127 -0
- plain/postgres/functions/datetime.py +454 -0
- plain/postgres/functions/math.py +140 -0
- plain/postgres/functions/mixins.py +59 -0
- plain/postgres/functions/text.py +282 -0
- plain/postgres/functions/window.py +125 -0
- plain/postgres/indexes.py +286 -0
- plain/postgres/lookups.py +758 -0
- plain/postgres/meta.py +584 -0
- plain/postgres/migrations/__init__.py +53 -0
- plain/postgres/migrations/autodetector.py +1379 -0
- plain/postgres/migrations/exceptions.py +54 -0
- plain/postgres/migrations/executor.py +188 -0
- plain/postgres/migrations/graph.py +364 -0
- plain/postgres/migrations/loader.py +377 -0
- plain/postgres/migrations/migration.py +180 -0
- plain/postgres/migrations/operations/__init__.py +34 -0
- plain/postgres/migrations/operations/base.py +139 -0
- plain/postgres/migrations/operations/fields.py +373 -0
- plain/postgres/migrations/operations/models.py +798 -0
- plain/postgres/migrations/operations/special.py +184 -0
- plain/postgres/migrations/optimizer.py +74 -0
- plain/postgres/migrations/questioner.py +340 -0
- plain/postgres/migrations/recorder.py +119 -0
- plain/postgres/migrations/serializer.py +378 -0
- plain/postgres/migrations/state.py +882 -0
- plain/postgres/migrations/utils.py +147 -0
- plain/postgres/migrations/writer.py +302 -0
- plain/postgres/options.py +207 -0
- plain/postgres/otel.py +231 -0
- plain/postgres/preflight.py +336 -0
- plain/postgres/query.py +2242 -0
- plain/postgres/query_utils.py +456 -0
- plain/postgres/registry.py +217 -0
- plain/postgres/schema.py +1885 -0
- plain/postgres/sql/__init__.py +40 -0
- plain/postgres/sql/compiler.py +1869 -0
- plain/postgres/sql/constants.py +22 -0
- plain/postgres/sql/datastructures.py +222 -0
- plain/postgres/sql/query.py +2947 -0
- plain/postgres/sql/where.py +374 -0
- plain/postgres/test/__init__.py +0 -0
- plain/postgres/test/pytest.py +117 -0
- plain/postgres/test/utils.py +18 -0
- plain/postgres/transaction.py +222 -0
- plain/postgres/types.py +92 -0
- plain/postgres/types.pyi +751 -0
- plain/postgres/utils.py +345 -0
- plain_postgres-0.84.0.dist-info/METADATA +937 -0
- plain_postgres-0.84.0.dist-info/RECORD +93 -0
- plain_postgres-0.84.0.dist-info/WHEEL +4 -0
- plain_postgres-0.84.0.dist-info/entry_points.txt +5 -0
- plain_postgres-0.84.0.dist-info/licenses/LICENSE +61 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from plain import postgres
|
|
6
|
+
from plain.postgres.db import DatabaseError
|
|
7
|
+
from plain.postgres.meta import Meta
|
|
8
|
+
from plain.postgres.registry import ModelsRegistry
|
|
9
|
+
from plain.utils.functional import classproperty
|
|
10
|
+
from plain.utils.timezone import now
|
|
11
|
+
|
|
12
|
+
from .exceptions import MigrationSchemaMissing
|
|
13
|
+
|
|
14
|
+
MIGRATION_TABLE_NAME = "plainmigrations"
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from plain.postgres.connection import DatabaseConnection
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MigrationRecorder:
|
|
21
|
+
"""
|
|
22
|
+
Deal with storing migration records in the database.
|
|
23
|
+
|
|
24
|
+
Because this table is actually itself used for dealing with model
|
|
25
|
+
creation, it's the one thing we can't do normally via migrations.
|
|
26
|
+
We manually handle table creation/schema updating (using schema backend)
|
|
27
|
+
and then have a floating model to do queries with.
|
|
28
|
+
|
|
29
|
+
If a migration is unapplied its row is removed from the table. Having
|
|
30
|
+
a row in the table always means a migration is applied.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
_migration_class: type[postgres.Model] | None = None
|
|
34
|
+
|
|
35
|
+
@classproperty # type: ignore[invalid-argument-type]
|
|
36
|
+
def Migration(cls) -> type[postgres.Model]:
|
|
37
|
+
"""
|
|
38
|
+
Lazy load to avoid PackageRegistryNotReady if installed packages import
|
|
39
|
+
MigrationRecorder.
|
|
40
|
+
"""
|
|
41
|
+
if cls._migration_class is None:
|
|
42
|
+
_models_registry = ModelsRegistry()
|
|
43
|
+
_models_registry.ready = True
|
|
44
|
+
|
|
45
|
+
class Migration(postgres.Model):
|
|
46
|
+
app = postgres.CharField(max_length=255)
|
|
47
|
+
name = postgres.CharField(max_length=255)
|
|
48
|
+
applied = postgres.DateTimeField(default=now)
|
|
49
|
+
|
|
50
|
+
# Use isolated models registry for migrations
|
|
51
|
+
_model_meta = Meta(models_registry=_models_registry)
|
|
52
|
+
|
|
53
|
+
model_options = postgres.Options(
|
|
54
|
+
package_label="migrations",
|
|
55
|
+
db_table=MIGRATION_TABLE_NAME,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def __str__(self) -> str:
|
|
59
|
+
return f"Migration {self.name} for {self.app}"
|
|
60
|
+
|
|
61
|
+
cls._migration_class = Migration
|
|
62
|
+
return cls._migration_class
|
|
63
|
+
|
|
64
|
+
def __init__(self, connection: DatabaseConnection) -> None:
|
|
65
|
+
self.connection = connection
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def migration_qs(self) -> Any:
|
|
69
|
+
return self.Migration.query.all()
|
|
70
|
+
|
|
71
|
+
def has_table(self) -> bool:
|
|
72
|
+
"""Return True if the plainmigrations table exists."""
|
|
73
|
+
with self.connection.cursor() as cursor:
|
|
74
|
+
tables = self.connection.table_names(cursor)
|
|
75
|
+
return self.Migration.model_options.db_table in tables
|
|
76
|
+
|
|
77
|
+
def ensure_schema(self) -> None:
|
|
78
|
+
"""Ensure the table exists and has the correct schema."""
|
|
79
|
+
# If the table's there, that's fine - we've never changed its schema
|
|
80
|
+
# in the codebase.
|
|
81
|
+
if self.has_table():
|
|
82
|
+
return
|
|
83
|
+
# Make the table
|
|
84
|
+
try:
|
|
85
|
+
with self.connection.schema_editor() as editor:
|
|
86
|
+
editor.create_model(self.Migration)
|
|
87
|
+
except DatabaseError as exc:
|
|
88
|
+
raise MigrationSchemaMissing(
|
|
89
|
+
f"Unable to create the plainmigrations table ({exc})"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def applied_migrations(self) -> dict[tuple[str, str], Any]:
|
|
93
|
+
"""
|
|
94
|
+
Return a dict mapping (package_name, migration_name) to Migration instances
|
|
95
|
+
for all applied migrations.
|
|
96
|
+
"""
|
|
97
|
+
if self.has_table():
|
|
98
|
+
return {
|
|
99
|
+
(migration.app, migration.name): migration
|
|
100
|
+
for migration in self.migration_qs
|
|
101
|
+
}
|
|
102
|
+
else:
|
|
103
|
+
# If the plainmigrations table doesn't exist, then no migrations
|
|
104
|
+
# are applied.
|
|
105
|
+
return {}
|
|
106
|
+
|
|
107
|
+
def record_applied(self, app: str, name: str) -> None:
|
|
108
|
+
"""Record that a migration was applied."""
|
|
109
|
+
self.ensure_schema()
|
|
110
|
+
self.migration_qs.create(app=app, name=name)
|
|
111
|
+
|
|
112
|
+
def record_unapplied(self, app: str, name: str) -> None:
|
|
113
|
+
"""Record that a migration was unapplied."""
|
|
114
|
+
self.ensure_schema()
|
|
115
|
+
self.migration_qs.filter(app=app, name=name).delete()
|
|
116
|
+
|
|
117
|
+
def flush(self) -> None:
|
|
118
|
+
"""Delete all migration records. Useful for testing migrations."""
|
|
119
|
+
self.migration_qs.all().delete()
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import builtins
|
|
4
|
+
import collections.abc
|
|
5
|
+
import datetime
|
|
6
|
+
import decimal
|
|
7
|
+
import enum
|
|
8
|
+
import functools
|
|
9
|
+
import math
|
|
10
|
+
import os
|
|
11
|
+
import pathlib
|
|
12
|
+
import re
|
|
13
|
+
import types
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from plain.postgres.base import Model
|
|
18
|
+
from plain.postgres.enums import Choices
|
|
19
|
+
from plain.postgres.fields import Field
|
|
20
|
+
from plain.postgres.migrations.operations.base import Operation
|
|
21
|
+
from plain.postgres.migrations.utils import COMPILED_REGEX_TYPE, RegexObject
|
|
22
|
+
from plain.runtime import SettingsReference
|
|
23
|
+
from plain.utils.functional import LazyObject, Promise
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseSerializer:
|
|
27
|
+
def __init__(self, value: Any) -> None:
|
|
28
|
+
self.value = value
|
|
29
|
+
|
|
30
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
31
|
+
raise NotImplementedError(
|
|
32
|
+
"subclasses of BaseSerializer must provide a serialize() method"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BaseSequenceSerializer(BaseSerializer):
|
|
37
|
+
def _format(self) -> str:
|
|
38
|
+
raise NotImplementedError(
|
|
39
|
+
"subclasses of BaseSequenceSerializer must provide a _format() method"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
43
|
+
imports: set[str] = set()
|
|
44
|
+
strings = []
|
|
45
|
+
for item in self.value:
|
|
46
|
+
item_string, item_imports = serializer_factory(item).serialize()
|
|
47
|
+
imports.update(item_imports)
|
|
48
|
+
strings.append(item_string)
|
|
49
|
+
value = self._format()
|
|
50
|
+
return value % (", ".join(strings)), imports
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BaseSimpleSerializer(BaseSerializer):
|
|
54
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
55
|
+
return repr(self.value), set()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ChoicesSerializer(BaseSerializer):
|
|
59
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
60
|
+
return serializer_factory(self.value.value).serialize()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DateTimeSerializer(BaseSerializer):
|
|
64
|
+
"""For datetime.*, except datetime.datetime."""
|
|
65
|
+
|
|
66
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
67
|
+
return repr(self.value), {"import datetime"}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DatetimeDatetimeSerializer(BaseSerializer):
|
|
71
|
+
"""For datetime.datetime."""
|
|
72
|
+
|
|
73
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
74
|
+
if self.value.tzinfo is not None and self.value.tzinfo != datetime.UTC:
|
|
75
|
+
self.value = self.value.astimezone(datetime.UTC)
|
|
76
|
+
imports = ["import datetime"]
|
|
77
|
+
return repr(self.value), set(imports)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DecimalSerializer(BaseSerializer):
|
|
81
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
82
|
+
return repr(self.value), {"from decimal import Decimal"}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class DeconstructableSerializer(BaseSerializer):
|
|
86
|
+
@staticmethod
|
|
87
|
+
def serialize_deconstructed(
|
|
88
|
+
path: str, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
89
|
+
) -> tuple[str, set[str]]:
|
|
90
|
+
name, imports = DeconstructableSerializer._serialize_path(path)
|
|
91
|
+
strings = []
|
|
92
|
+
for arg in args:
|
|
93
|
+
arg_string, arg_imports = serializer_factory(arg).serialize()
|
|
94
|
+
strings.append(arg_string)
|
|
95
|
+
imports.update(arg_imports)
|
|
96
|
+
for kw, arg in sorted(kwargs.items()):
|
|
97
|
+
arg_string, arg_imports = serializer_factory(arg).serialize()
|
|
98
|
+
imports.update(arg_imports)
|
|
99
|
+
strings.append(f"{kw}={arg_string}")
|
|
100
|
+
return "{}({})".format(name, ", ".join(strings)), imports
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def _serialize_path(path: str) -> tuple[str, set[str]]:
|
|
104
|
+
module, name = path.rsplit(".", 1)
|
|
105
|
+
if module == "plain.postgres":
|
|
106
|
+
imports: set[str] = {"from plain import postgres"}
|
|
107
|
+
name = f"postgres.{name}"
|
|
108
|
+
else:
|
|
109
|
+
imports = {f"import {module}"}
|
|
110
|
+
name = path
|
|
111
|
+
return name, imports
|
|
112
|
+
|
|
113
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
114
|
+
return self.serialize_deconstructed(*self.value.deconstruct())
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class DictionarySerializer(BaseSerializer):
|
|
118
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
119
|
+
imports: set[str] = set()
|
|
120
|
+
strings = []
|
|
121
|
+
for k, v in sorted(self.value.items()):
|
|
122
|
+
k_string, k_imports = serializer_factory(k).serialize()
|
|
123
|
+
v_string, v_imports = serializer_factory(v).serialize()
|
|
124
|
+
imports.update(k_imports)
|
|
125
|
+
imports.update(v_imports)
|
|
126
|
+
strings.append((k_string, v_string))
|
|
127
|
+
return "{{{}}}".format(", ".join(f"{k}: {v}" for k, v in strings)), imports
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class EnumSerializer(BaseSerializer):
|
|
131
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
132
|
+
enum_class = self.value.__class__
|
|
133
|
+
module = enum_class.__module__
|
|
134
|
+
if issubclass(enum_class, enum.Flag):
|
|
135
|
+
members = list(self.value)
|
|
136
|
+
else:
|
|
137
|
+
members = (self.value,)
|
|
138
|
+
return (
|
|
139
|
+
" | ".join(
|
|
140
|
+
[
|
|
141
|
+
f"{module}.{enum_class.__qualname__}[{item.name!r}]"
|
|
142
|
+
for item in members
|
|
143
|
+
]
|
|
144
|
+
),
|
|
145
|
+
{f"import {module}"},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class FloatSerializer(BaseSimpleSerializer):
|
|
150
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
151
|
+
if math.isnan(self.value) or math.isinf(self.value):
|
|
152
|
+
return f'float("{self.value}")', set()
|
|
153
|
+
return super().serialize()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class FrozensetSerializer(BaseSequenceSerializer):
|
|
157
|
+
def _format(self) -> str:
|
|
158
|
+
return "frozenset([%s])"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class FunctionTypeSerializer(BaseSerializer):
|
|
162
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
163
|
+
if getattr(self.value, "__self__", None) and isinstance(
|
|
164
|
+
self.value.__self__, type
|
|
165
|
+
):
|
|
166
|
+
klass = self.value.__self__
|
|
167
|
+
module = klass.__module__
|
|
168
|
+
return f"{module}.{klass.__name__}.{self.value.__name__}", {
|
|
169
|
+
f"import {module}"
|
|
170
|
+
}
|
|
171
|
+
# Further error checking
|
|
172
|
+
if self.value.__name__ == "<lambda>":
|
|
173
|
+
raise ValueError("Cannot serialize function: lambda")
|
|
174
|
+
if self.value.__module__ is None:
|
|
175
|
+
raise ValueError(f"Cannot serialize function {self.value!r}: No module")
|
|
176
|
+
|
|
177
|
+
module_name = self.value.__module__
|
|
178
|
+
|
|
179
|
+
if "<" not in self.value.__qualname__: # Qualname can include <locals>
|
|
180
|
+
return f"{module_name}.{self.value.__qualname__}", {
|
|
181
|
+
f"import {self.value.__module__}"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"Could not find function {self.value.__name__} in {module_name}.\n"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class FunctoolsPartialSerializer(BaseSerializer):
|
|
190
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
191
|
+
# Serialize functools.partial() arguments
|
|
192
|
+
func_string, func_imports = serializer_factory(self.value.func).serialize()
|
|
193
|
+
args_string, args_imports = serializer_factory(self.value.args).serialize()
|
|
194
|
+
keywords_string, keywords_imports = serializer_factory(
|
|
195
|
+
self.value.keywords
|
|
196
|
+
).serialize()
|
|
197
|
+
# Add any imports needed by arguments
|
|
198
|
+
imports: set[str] = {
|
|
199
|
+
"import functools",
|
|
200
|
+
*func_imports,
|
|
201
|
+
*args_imports,
|
|
202
|
+
*keywords_imports,
|
|
203
|
+
}
|
|
204
|
+
return (
|
|
205
|
+
f"functools.{self.value.__class__.__name__}({func_string}, *{args_string}, **{keywords_string})",
|
|
206
|
+
imports,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class IterableSerializer(BaseSerializer):
|
|
211
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
212
|
+
imports: set[str] = set()
|
|
213
|
+
strings = []
|
|
214
|
+
for item in self.value:
|
|
215
|
+
item_string, item_imports = serializer_factory(item).serialize()
|
|
216
|
+
imports.update(item_imports)
|
|
217
|
+
strings.append(item_string)
|
|
218
|
+
# When len(strings)==0, the empty iterable should be serialized as
|
|
219
|
+
# "()", not "(,)" because (,) is invalid Python syntax.
|
|
220
|
+
value = "(%s)" if len(strings) != 1 else "(%s,)"
|
|
221
|
+
return value % (", ".join(strings)), imports
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class ModelFieldSerializer(DeconstructableSerializer):
|
|
225
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
226
|
+
attr_name, path, args, kwargs = self.value.deconstruct()
|
|
227
|
+
return self.serialize_deconstructed(path, args, kwargs)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class OperationSerializer(BaseSerializer):
|
|
231
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
232
|
+
from plain.postgres.migrations.writer import OperationWriter
|
|
233
|
+
|
|
234
|
+
string, imports = OperationWriter(self.value, indentation=0).serialize()
|
|
235
|
+
# Nested operation, trailing comma is handled in upper OperationWriter._write()
|
|
236
|
+
return string.rstrip(","), imports
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class PathLikeSerializer(BaseSerializer):
|
|
240
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
241
|
+
return repr(os.fspath(self.value)), set()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class PathSerializer(BaseSerializer):
|
|
245
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
246
|
+
# Convert concrete paths to pure paths to avoid issues with migrations
|
|
247
|
+
# generated on one platform being used on a different platform.
|
|
248
|
+
prefix = "Pure" if isinstance(self.value, pathlib.Path) else ""
|
|
249
|
+
return f"pathlib.{prefix}{self.value!r}", {"import pathlib"}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class RegexSerializer(BaseSerializer):
|
|
253
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
254
|
+
regex_pattern, pattern_imports = serializer_factory(
|
|
255
|
+
self.value.pattern
|
|
256
|
+
).serialize()
|
|
257
|
+
# Turn off default implicit flags (e.g. re.U) because regexes with the
|
|
258
|
+
# same implicit and explicit flags aren't equal.
|
|
259
|
+
flags = self.value.flags ^ re.compile("").flags
|
|
260
|
+
regex_flags, flag_imports = serializer_factory(flags).serialize()
|
|
261
|
+
imports: set[str] = {"import re", *pattern_imports, *flag_imports}
|
|
262
|
+
args = [regex_pattern]
|
|
263
|
+
if flags:
|
|
264
|
+
args.append(regex_flags)
|
|
265
|
+
return "re.compile({})".format(", ".join(args)), imports
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class SequenceSerializer(BaseSequenceSerializer):
|
|
269
|
+
def _format(self) -> str:
|
|
270
|
+
return "[%s]"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class SetSerializer(BaseSequenceSerializer):
|
|
274
|
+
def _format(self) -> str:
|
|
275
|
+
# Serialize as a set literal except when value is empty because {}
|
|
276
|
+
# is an empty dict.
|
|
277
|
+
return "{%s}" if self.value else "set(%s)"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class SettingsReferenceSerializer(BaseSerializer):
|
|
281
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
282
|
+
return f"settings.{self.value.setting_name}", {
|
|
283
|
+
"from plain.runtime import settings"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class TupleSerializer(BaseSequenceSerializer):
|
|
288
|
+
def _format(self) -> str:
|
|
289
|
+
# When len(value)==0, the empty tuple should be serialized as "()",
|
|
290
|
+
# not "(,)" because (,) is invalid Python syntax.
|
|
291
|
+
return "(%s)" if len(self.value) != 1 else "(%s,)"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class TypeSerializer(BaseSerializer):
|
|
295
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
296
|
+
special_cases = [
|
|
297
|
+
(Model, "postgres.Model", ["from plain import postgres"]),
|
|
298
|
+
(types.NoneType, "types.NoneType", ["import types"]),
|
|
299
|
+
]
|
|
300
|
+
for case, string, imports in special_cases:
|
|
301
|
+
if case is self.value:
|
|
302
|
+
return string, set(imports)
|
|
303
|
+
if hasattr(self.value, "__module__"):
|
|
304
|
+
module = self.value.__module__
|
|
305
|
+
if module == builtins.__name__:
|
|
306
|
+
return self.value.__name__, set()
|
|
307
|
+
else:
|
|
308
|
+
return f"{module}.{self.value.__qualname__}", {f"import {module}"}
|
|
309
|
+
return "", set()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class UUIDSerializer(BaseSerializer):
|
|
313
|
+
def serialize(self) -> tuple[str, set[str]]:
|
|
314
|
+
return f"uuid.{repr(self.value)}", {"import uuid"}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class Serializer:
|
|
318
|
+
_registry = {
|
|
319
|
+
# Some of these are order-dependent.
|
|
320
|
+
frozenset: FrozensetSerializer,
|
|
321
|
+
list: SequenceSerializer,
|
|
322
|
+
set: SetSerializer,
|
|
323
|
+
tuple: TupleSerializer,
|
|
324
|
+
dict: DictionarySerializer,
|
|
325
|
+
Choices: ChoicesSerializer,
|
|
326
|
+
enum.Enum: EnumSerializer,
|
|
327
|
+
datetime.datetime: DatetimeDatetimeSerializer,
|
|
328
|
+
(datetime.date, datetime.timedelta, datetime.time): DateTimeSerializer,
|
|
329
|
+
SettingsReference: SettingsReferenceSerializer,
|
|
330
|
+
float: FloatSerializer,
|
|
331
|
+
(bool, int, types.NoneType, bytes, str, range): BaseSimpleSerializer,
|
|
332
|
+
decimal.Decimal: DecimalSerializer,
|
|
333
|
+
(functools.partial, functools.partialmethod): FunctoolsPartialSerializer,
|
|
334
|
+
(
|
|
335
|
+
types.FunctionType,
|
|
336
|
+
types.BuiltinFunctionType,
|
|
337
|
+
types.MethodType,
|
|
338
|
+
): FunctionTypeSerializer,
|
|
339
|
+
collections.abc.Iterable: IterableSerializer,
|
|
340
|
+
(COMPILED_REGEX_TYPE, RegexObject): RegexSerializer,
|
|
341
|
+
uuid.UUID: UUIDSerializer,
|
|
342
|
+
pathlib.PurePath: PathSerializer,
|
|
343
|
+
os.PathLike: PathLikeSerializer,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
@classmethod
|
|
347
|
+
def register(cls, type_: type[Any], serializer: type[BaseSerializer]) -> None:
|
|
348
|
+
if not issubclass(serializer, BaseSerializer):
|
|
349
|
+
raise ValueError(
|
|
350
|
+
f"'{serializer.__name__}' must inherit from 'BaseSerializer'."
|
|
351
|
+
)
|
|
352
|
+
cls._registry[type_] = serializer # type: ignore[assignment]
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def serializer_factory(value: Any) -> BaseSerializer:
|
|
356
|
+
if isinstance(value, Promise):
|
|
357
|
+
value = str(value)
|
|
358
|
+
elif isinstance(value, LazyObject):
|
|
359
|
+
# The unwrapped value is returned as the first item of the arguments
|
|
360
|
+
# tuple.
|
|
361
|
+
value = value.__reduce__()[1][0]
|
|
362
|
+
|
|
363
|
+
if isinstance(value, Field):
|
|
364
|
+
return ModelFieldSerializer(value)
|
|
365
|
+
if isinstance(value, Operation):
|
|
366
|
+
return OperationSerializer(value)
|
|
367
|
+
if isinstance(value, type):
|
|
368
|
+
return TypeSerializer(value)
|
|
369
|
+
# Anything that knows how to deconstruct itself.
|
|
370
|
+
if hasattr(value, "deconstruct"):
|
|
371
|
+
return DeconstructableSerializer(value)
|
|
372
|
+
for type_, serializer_cls in Serializer._registry.items():
|
|
373
|
+
if isinstance(value, type_):
|
|
374
|
+
return serializer_cls(value)
|
|
375
|
+
raise ValueError(
|
|
376
|
+
f"Cannot serialize: {value!r}\nThere are some values Plain cannot serialize into "
|
|
377
|
+
"migration files."
|
|
378
|
+
)
|