django-compose 0.1.0.dev1__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.
- django_compose/__init__.py +0 -0
- django_compose/__main__.py +5 -0
- django_compose/addons.py +205 -0
- django_compose/apps.py +103 -0
- django_compose/artifacts.py +224 -0
- django_compose/asgi.py +110 -0
- django_compose/autoreload.py +110 -0
- django_compose/build.py +282 -0
- django_compose/checks.py +11 -0
- django_compose/cli.py +12 -0
- django_compose/conf.py +322 -0
- django_compose/declarations.py +168 -0
- django_compose/history.py +36 -0
- django_compose/management/__init__.py +221 -0
- django_compose/management/commands/__init__.py +1 -0
- django_compose/management/commands/check_migrations.py +57 -0
- django_compose/management/commands/compose.py +42 -0
- django_compose/management/commands/makemigrations.py +136 -0
- django_compose/management/commands/migrate.py +18 -0
- django_compose/management/commands/reset_migration_history.py +34 -0
- django_compose/management/commands/retire_history.py +24 -0
- django_compose/migration_imports.py +474 -0
- django_compose/migrations.py +843 -0
- django_compose/model_composition.py +292 -0
- django_compose/models.py +65 -0
- django_compose/py.typed +0 -0
- django_compose/rendering.py +92 -0
- django_compose/serving.py +405 -0
- django_compose/snapshot.py +419 -0
- django_compose/sources.py +147 -0
- django_compose/typing.py +633 -0
- django_compose/urls.py +58 -0
- django_compose-0.1.0.dev1.dist-info/METADATA +101 -0
- django_compose-0.1.0.dev1.dist-info/RECORD +39 -0
- django_compose-0.1.0.dev1.dist-info/WHEEL +4 -0
- django_compose-0.1.0.dev1.dist-info/entry_points.txt +2 -0
- django_compose-0.1.0.dev1.dist-info/licenses/COPYING +674 -0
- django_compose-0.1.0.dev1.dist-info/licenses/LICENSE +165 -0
- django_compose-0.1.0.dev1.dist-info/licenses/NOTICE +10 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Discover native abstract declarations and validate additive composition edges."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import keyword
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Iterable, Mapping
|
|
9
|
+
from graphlib import CycleError, TopologicalSorter
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from django.apps import AppConfig
|
|
13
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
14
|
+
from django.db import models
|
|
15
|
+
|
|
16
|
+
from django_compose.models import Composable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _ordered(graph: Mapping[str, Iterable[str]], description: str) -> tuple[str, ...]:
|
|
20
|
+
try:
|
|
21
|
+
return tuple(TopologicalSorter(graph).static_order())
|
|
22
|
+
except CycleError as error:
|
|
23
|
+
raise ImproperlyConfigured(f"Cyclic {description}: {' -> '.join(error.args[1])}") from error
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _fields(model: type[models.Model]) -> tuple[models.Field[Any, Any], ...]:
|
|
27
|
+
return (*model._meta.local_fields, *model._meta.local_many_to_many, *model._meta.private_fields)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ModelComposition:
|
|
31
|
+
"""Validated native sources, donors, parent chains, and contributor provenance."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
sources_by_label: dict[str, tuple[type[models.Model], ...]],
|
|
36
|
+
extensions: dict[str, tuple[type[models.Model], ...]],
|
|
37
|
+
model_owners: dict[type[models.Model], str] | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.sources_by_label = {
|
|
40
|
+
label: tuple(sorted(sources, key=lambda item: item.__name__))
|
|
41
|
+
for label, sources in sorted(sources_by_label.items())
|
|
42
|
+
}
|
|
43
|
+
self.extensions = extensions
|
|
44
|
+
self._owners = model_owners or {}
|
|
45
|
+
self.models_by_label: dict[str, type[models.Model]] = {}
|
|
46
|
+
for label, sources in self.sources_by_label.items():
|
|
47
|
+
if not label.isidentifier() or keyword.iskeyword(label):
|
|
48
|
+
raise ImproperlyConfigured(f"Invalid generated app label: {label!r}")
|
|
49
|
+
for source in sources:
|
|
50
|
+
if not source.__name__.isidentifier() or keyword.iskeyword(source.__name__):
|
|
51
|
+
raise ImproperlyConfigured(f"Invalid generated model name: {source.__name__!r}")
|
|
52
|
+
key = source._meta.label_lower
|
|
53
|
+
if key in self.models_by_label:
|
|
54
|
+
raise ImproperlyConfigured(f"Duplicate source model label {key!r}")
|
|
55
|
+
self.models_by_label[key] = source
|
|
56
|
+
for target, donors in extensions.items():
|
|
57
|
+
if target not in self.models_by_label:
|
|
58
|
+
raise ImproperlyConfigured(
|
|
59
|
+
f"{', '.join(donor._meta.label for donor in donors)} extends unknown model {target!r}"
|
|
60
|
+
)
|
|
61
|
+
if any(donor not in self._owners for donor in donors):
|
|
62
|
+
raise ImproperlyConfigured("Addon ownership is required for extension donors")
|
|
63
|
+
graph = {}
|
|
64
|
+
for key, source in sorted(self.models_by_label.items()):
|
|
65
|
+
parent_ref = vars(source).get("compose_inherits")
|
|
66
|
+
if parent_ref is not None and parent_ref not in self.models_by_label:
|
|
67
|
+
raise ImproperlyConfigured(f"{source._meta.label} inherits unknown model {parent_ref!r}")
|
|
68
|
+
graph[key] = () if parent_ref is None else (parent_ref,)
|
|
69
|
+
order = _ordered(graph, "concrete model parents")
|
|
70
|
+
modules: dict[str, set[str]] = {label: set() for label in self.sources_by_label}
|
|
71
|
+
for key, dependencies in graph.items():
|
|
72
|
+
own_label = self.models_by_label[key]._meta.app_label
|
|
73
|
+
for target in dependencies:
|
|
74
|
+
parent_label = self.models_by_label[target]._meta.app_label
|
|
75
|
+
if parent_label != own_label:
|
|
76
|
+
modules[own_label].add(parent_label)
|
|
77
|
+
_ordered({label: tuple(sorted(edges)) for label, edges in modules.items()}, "generated model modules")
|
|
78
|
+
self._parents = {
|
|
79
|
+
self.models_by_label[key]: self.models_by_label[edges[0]] if edges else None
|
|
80
|
+
for key, edges in graph.items()
|
|
81
|
+
}
|
|
82
|
+
self.ordered_models = tuple(self.models_by_label[key] for key in order)
|
|
83
|
+
self.labels = tuple(self.sources_by_label)
|
|
84
|
+
for model in (*self.ordered_models, *(donor for donors in extensions.values() for donor in donors)):
|
|
85
|
+
module = sys.modules.get(model.__module__)
|
|
86
|
+
if module is None or getattr(module, model.__name__, None) is not model:
|
|
87
|
+
raise ImproperlyConfigured(
|
|
88
|
+
f"Source model {model.__module__}.{model.__name__} is not importable"
|
|
89
|
+
)
|
|
90
|
+
self.required_ancestors = self._requirements()
|
|
91
|
+
self._contributions: dict[
|
|
92
|
+
type[models.Model], tuple[tuple[models.Field[Any, Any], type[models.Model] | None], ...]
|
|
93
|
+
] = {}
|
|
94
|
+
self._validate_additions()
|
|
95
|
+
self._constraints = {source: self._resolve_constraints(source) for source in self.ordered_models}
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def discover(cls, configs: Iterable[AppConfig]) -> ModelComposition:
|
|
99
|
+
"""Use native models_module exports, including custom import_models hooks."""
|
|
100
|
+
configs = tuple(configs)
|
|
101
|
+
sources: dict[str, list[type[models.Model]]] = {}
|
|
102
|
+
donors: dict[str, list[type[models.Model]]] = {}
|
|
103
|
+
owners: dict[type[models.Model], str] = {}
|
|
104
|
+
for config in configs:
|
|
105
|
+
if config.models_module is None:
|
|
106
|
+
continue
|
|
107
|
+
seen = set()
|
|
108
|
+
for model in vars(config.models_module).values():
|
|
109
|
+
if (
|
|
110
|
+
not inspect.isclass(model)
|
|
111
|
+
or not issubclass(model, models.Model)
|
|
112
|
+
or model is models.Model
|
|
113
|
+
or model in seen
|
|
114
|
+
or not (model.__module__ == config.name or model.__module__.startswith(config.name + "."))
|
|
115
|
+
):
|
|
116
|
+
continue
|
|
117
|
+
seen.add(model)
|
|
118
|
+
if not issubclass(model, Composable):
|
|
119
|
+
continue
|
|
120
|
+
own = vars(model)
|
|
121
|
+
marked = any(name.startswith("compose_") for name in own)
|
|
122
|
+
if not model._meta.abstract:
|
|
123
|
+
if marked:
|
|
124
|
+
raise ImproperlyConfigured(
|
|
125
|
+
f"{model._meta.label}: composition markers require abstract models"
|
|
126
|
+
)
|
|
127
|
+
continue
|
|
128
|
+
concrete = own.get("compose_concrete", False)
|
|
129
|
+
if type(concrete) is not bool:
|
|
130
|
+
raise ImproperlyConfigured(f"{model._meta.label}.compose_concrete must be a boolean")
|
|
131
|
+
target = own.get("compose_extends")
|
|
132
|
+
if not concrete and target is None:
|
|
133
|
+
continue
|
|
134
|
+
if model._meta.app_label != config.label:
|
|
135
|
+
raise ImproperlyConfigured(f"{model._meta.label}: expected app_label {config.label!r}")
|
|
136
|
+
if concrete:
|
|
137
|
+
sources.setdefault(config.label, []).append(model)
|
|
138
|
+
else:
|
|
139
|
+
assert target is not None
|
|
140
|
+
donors.setdefault(target, []).append(model)
|
|
141
|
+
owners[model] = config.name
|
|
142
|
+
return cls(
|
|
143
|
+
{key: tuple(value) for key, value in sources.items()},
|
|
144
|
+
{key: tuple(value) for key, value in donors.items()},
|
|
145
|
+
owners,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def parent(self, source: type[models.Model]) -> type[models.Model] | None:
|
|
149
|
+
"""Return the immediate concrete parent, or None for a root identity."""
|
|
150
|
+
return self._parents[source]
|
|
151
|
+
|
|
152
|
+
def donors(self, source: type[models.Model]) -> tuple[type[models.Model], ...]:
|
|
153
|
+
"""Return additive donors in native enabled-app and declaration order."""
|
|
154
|
+
return self.extensions.get(source._meta.label_lower, ())
|
|
155
|
+
|
|
156
|
+
def contributed_fields(self, source: type[models.Model]) -> tuple[models.Field[Any, Any], ...]:
|
|
157
|
+
"""Return the native fields added by donors, excluding source-owned ancestry."""
|
|
158
|
+
return tuple(native for native, _ in self._contributions.get(source, ()))
|
|
159
|
+
|
|
160
|
+
def contributed_field_origins(self, source: type[models.Model]) -> tuple[tuple[str, str], ...]:
|
|
161
|
+
"""Return sorted (field name, declaring app name) pairs for owned additions."""
|
|
162
|
+
return tuple(
|
|
163
|
+
(native.name, self._owners[owner])
|
|
164
|
+
for native, owner in self._contributions.get(source, ())
|
|
165
|
+
if owner is not None
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def constraint_sources(self, source: type[models.Model]) -> tuple[tuple[type[models.Model], int], ...]:
|
|
169
|
+
"""Return one native declaration per constraint, preserving shared ancestry."""
|
|
170
|
+
return self._constraints[source]
|
|
171
|
+
|
|
172
|
+
def _resolve_constraints(self, source: type[models.Model]) -> tuple[tuple[type[models.Model], int], ...]:
|
|
173
|
+
selected = []
|
|
174
|
+
seen: set[int] = set()
|
|
175
|
+
names: set[str] = set()
|
|
176
|
+
for contributor in (source, *self.donors(source)):
|
|
177
|
+
for index, constraint in enumerate(contributor._meta.constraints):
|
|
178
|
+
if id(constraint) in seen:
|
|
179
|
+
continue
|
|
180
|
+
# Django clones constraints when interpolating native concrete
|
|
181
|
+
# names. Its private helper is not exposed by django-stubs.
|
|
182
|
+
name = source._meta._format_names([constraint])[0].name # type: ignore[attr-defined]
|
|
183
|
+
if name in names:
|
|
184
|
+
raise ImproperlyConfigured(f"{source._meta.label} composes constraint {name!r} twice")
|
|
185
|
+
seen.add(id(constraint))
|
|
186
|
+
names.add(name)
|
|
187
|
+
selected.append((contributor, index))
|
|
188
|
+
return tuple(selected)
|
|
189
|
+
|
|
190
|
+
def _requirements(self) -> dict[type[models.Model], type[models.Model]]:
|
|
191
|
+
required = {}
|
|
192
|
+
for source in self.ordered_models:
|
|
193
|
+
ancestors = set()
|
|
194
|
+
parent = self.parent(source)
|
|
195
|
+
while parent is not None:
|
|
196
|
+
ancestors.add(parent)
|
|
197
|
+
parent = self.parent(parent)
|
|
198
|
+
for contributor in (*self.donors(source), source):
|
|
199
|
+
for declaration in contributor.__mro__:
|
|
200
|
+
if not issubclass(declaration, models.Model):
|
|
201
|
+
continue
|
|
202
|
+
reference = vars(declaration).get("compose_demands")
|
|
203
|
+
if reference is None:
|
|
204
|
+
continue
|
|
205
|
+
target = self.models_by_label.get(reference)
|
|
206
|
+
if target is None or target not in ancestors:
|
|
207
|
+
raise ImproperlyConfigured(
|
|
208
|
+
f"{source._meta.label} demands ancestor {reference!r} "
|
|
209
|
+
f"through {declaration._meta.label}"
|
|
210
|
+
)
|
|
211
|
+
required[declaration] = target
|
|
212
|
+
return required
|
|
213
|
+
|
|
214
|
+
def _validate_additions(self) -> None:
|
|
215
|
+
inherited_fields: dict[type[models.Model], dict[str, models.Field[Any, Any]]] = {}
|
|
216
|
+
for source in self.ordered_models:
|
|
217
|
+
parent = self.parent(source)
|
|
218
|
+
inherited = inherited_fields[parent] if parent else {}
|
|
219
|
+
fields: dict[str, tuple[models.Field[Any, Any], type[models.Model] | None]] = {}
|
|
220
|
+
managers: dict[str, models.Manager[models.Model]] = {}
|
|
221
|
+
inherited_names = {
|
|
222
|
+
name for native in inherited.values() for name in (native.name, native.attname)
|
|
223
|
+
}
|
|
224
|
+
inherited_columns = {
|
|
225
|
+
native.column
|
|
226
|
+
for native in inherited.values()
|
|
227
|
+
if native.concrete and native.column is not None
|
|
228
|
+
}
|
|
229
|
+
local_names: dict[str, models.Field[Any, Any]] = {}
|
|
230
|
+
local_columns: dict[str, models.Field[Any, Any]] = {}
|
|
231
|
+
for contributor in (*self.donors(source), source):
|
|
232
|
+
if contributor is not source:
|
|
233
|
+
authored = vars(contributor.Meta)
|
|
234
|
+
forbidden = (set(authored) & set(models.options.DEFAULT_NAMES)) - {
|
|
235
|
+
"abstract",
|
|
236
|
+
"app_label",
|
|
237
|
+
"constraints",
|
|
238
|
+
}
|
|
239
|
+
if forbidden:
|
|
240
|
+
raise ImproperlyConfigured(
|
|
241
|
+
f"{contributor._meta.label}: donor Meta authors unsupported options "
|
|
242
|
+
f"{', '.join(sorted(forbidden))}"
|
|
243
|
+
)
|
|
244
|
+
for native in _fields(contributor):
|
|
245
|
+
if (
|
|
246
|
+
native.name in inherited_names
|
|
247
|
+
or native.attname in inherited_names
|
|
248
|
+
or native.concrete
|
|
249
|
+
and native.column is not None
|
|
250
|
+
and native.column in inherited_columns
|
|
251
|
+
):
|
|
252
|
+
raise ImproperlyConfigured(
|
|
253
|
+
f"{source._meta.label} redeclares parent field {native.name!r}"
|
|
254
|
+
)
|
|
255
|
+
for local_name in (native.name, native.attname):
|
|
256
|
+
existing = local_names.setdefault(local_name, native)
|
|
257
|
+
if existing.creation_counter != native.creation_counter:
|
|
258
|
+
raise ImproperlyConfigured(
|
|
259
|
+
f"{source._meta.label} composes field attribute {local_name!r} twice"
|
|
260
|
+
)
|
|
261
|
+
if native.concrete and native.column is not None:
|
|
262
|
+
existing = local_columns.setdefault(native.column, native)
|
|
263
|
+
if existing.creation_counter != native.creation_counter:
|
|
264
|
+
raise ImproperlyConfigured(
|
|
265
|
+
f"{source._meta.label} composes database column {native.column!r} twice"
|
|
266
|
+
)
|
|
267
|
+
previous = fields.get(native.name)
|
|
268
|
+
if previous is None:
|
|
269
|
+
fields[native.name] = (native, contributor)
|
|
270
|
+
elif previous[0] is not native:
|
|
271
|
+
fields[native.name] = (previous[0], source if contributor is source else None)
|
|
272
|
+
for manager in contributor._meta.managers:
|
|
273
|
+
if manager.auto_created:
|
|
274
|
+
continue
|
|
275
|
+
previous_manager = managers.setdefault(manager.name, manager)
|
|
276
|
+
if (
|
|
277
|
+
contributor is not source
|
|
278
|
+
and previous_manager.creation_counter != manager.creation_counter
|
|
279
|
+
):
|
|
280
|
+
raise ImproperlyConfigured(
|
|
281
|
+
f"{source._meta.label} composes manager {manager.name!r} twice"
|
|
282
|
+
)
|
|
283
|
+
for manager_name in managers.keys() & local_names.keys():
|
|
284
|
+
raise ImproperlyConfigured(
|
|
285
|
+
f"{source._meta.label} composes field and manager {manager_name!r} twice"
|
|
286
|
+
)
|
|
287
|
+
inherited_fields[source] = {**inherited, **{name: item[0] for name, item in fields.items()}}
|
|
288
|
+
self._contributions[source] = tuple(
|
|
289
|
+
(native, owner)
|
|
290
|
+
for _, (native, owner) in sorted(fields.items())
|
|
291
|
+
if owner is not source and (native.concrete or native.many_to_many)
|
|
292
|
+
)
|
django_compose/models.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Native abstract model declarations for generated concrete composition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import keyword
|
|
6
|
+
from typing import Any, ClassVar
|
|
7
|
+
|
|
8
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
9
|
+
from django.db import models
|
|
10
|
+
from django.db.models.utils import make_model_tuple
|
|
11
|
+
|
|
12
|
+
_UNSET: Any = object()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Composable(models.Model):
|
|
16
|
+
"""Declare composition through class keywords, retaining Django's Meta rules.
|
|
17
|
+
|
|
18
|
+
``concrete``, ``extends`` and ``inherits`` are mutually exclusive and
|
|
19
|
+
apply only to the class declaring them. ``inherits`` implies concrete.
|
|
20
|
+
``demands`` applies through the complete MRO and combines with any
|
|
21
|
+
declaration. Every source and mixin must explicitly remain abstract using
|
|
22
|
+
native Meta. Factories may pass the same keywords to ``type()``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
compose_concrete: ClassVar[bool] = False
|
|
26
|
+
compose_extends: ClassVar[str | None] = None
|
|
27
|
+
compose_inherits: ClassVar[str | None] = None
|
|
28
|
+
compose_demands: ClassVar[str | None] = None
|
|
29
|
+
|
|
30
|
+
class Meta:
|
|
31
|
+
abstract = True
|
|
32
|
+
|
|
33
|
+
def __init_subclass__(
|
|
34
|
+
cls,
|
|
35
|
+
*,
|
|
36
|
+
concrete: bool = _UNSET,
|
|
37
|
+
extends: str | None = None,
|
|
38
|
+
inherits: str | None = None,
|
|
39
|
+
demands: str | None = None,
|
|
40
|
+
**kwargs: Any,
|
|
41
|
+
) -> None:
|
|
42
|
+
super().__init_subclass__(**kwargs)
|
|
43
|
+
identity = f"{cls.__module__}.{cls.__name__}"
|
|
44
|
+
if concrete is not _UNSET and type(concrete) is not bool:
|
|
45
|
+
raise ImproperlyConfigured(f"{identity}: concrete must be a boolean")
|
|
46
|
+
if sum((concrete is not _UNSET, extends is not None, inherits is not None)) > 1:
|
|
47
|
+
raise ImproperlyConfigured(f"{identity}: concrete, extends and inherits are mutually exclusive")
|
|
48
|
+
if concrete is True or inherits is not None:
|
|
49
|
+
cls.compose_concrete = True
|
|
50
|
+
for name, value in (
|
|
51
|
+
("extends", extends),
|
|
52
|
+
("inherits", inherits),
|
|
53
|
+
("demands", demands),
|
|
54
|
+
):
|
|
55
|
+
if value is None:
|
|
56
|
+
continue
|
|
57
|
+
if isinstance(value, str):
|
|
58
|
+
try:
|
|
59
|
+
label, model = make_model_tuple(value)
|
|
60
|
+
if label.isidentifier() and not keyword.iskeyword(label) and model.isidentifier():
|
|
61
|
+
setattr(cls, f"compose_{name}", f"{label}.{model}")
|
|
62
|
+
continue
|
|
63
|
+
except ValueError:
|
|
64
|
+
pass
|
|
65
|
+
raise ImproperlyConfigured(f"{identity}: {name} requires an 'app_label.Model' reference")
|
django_compose/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Emit ordinary subclasses while Django retains field and inheritance ownership."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
7
|
+
from django.db import models
|
|
8
|
+
|
|
9
|
+
from django_compose.declarations import ModelDeclaration, render_literal
|
|
10
|
+
from django_compose.model_composition import ModelComposition
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def render_models(
|
|
14
|
+
composition: ModelComposition,
|
|
15
|
+
label: str,
|
|
16
|
+
*,
|
|
17
|
+
runtime_module: str = "runtime",
|
|
18
|
+
declarations: Mapping[type[models.Model], ModelDeclaration] | None = None,
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Render donor, source, concrete-parent bases and additive literal declarations."""
|
|
21
|
+
used_names = {source.__name__ for source in composition.sources_by_label.get(label, ())}
|
|
22
|
+
|
|
23
|
+
def alias(preferred: str) -> str:
|
|
24
|
+
selected = preferred
|
|
25
|
+
while selected in used_names:
|
|
26
|
+
selected += "_"
|
|
27
|
+
used_names.add(selected)
|
|
28
|
+
return selected
|
|
29
|
+
|
|
30
|
+
classvar = alias("_ComposeClassVar")
|
|
31
|
+
imports = {f"from typing import ClassVar as {classvar}"}
|
|
32
|
+
classes = []
|
|
33
|
+
for source in composition.ordered_models:
|
|
34
|
+
if source._meta.app_label != label:
|
|
35
|
+
continue
|
|
36
|
+
name = source.__name__
|
|
37
|
+
abstract = alias(f"_Source{name}")
|
|
38
|
+
imports.add(f"from {source.__module__} import {name} as {abstract}")
|
|
39
|
+
bases = []
|
|
40
|
+
contributor_aliases = {source: abstract}
|
|
41
|
+
for index, donor in enumerate(composition.donors(source)):
|
|
42
|
+
donor_alias = alias(f"_Donor{name}{index}")
|
|
43
|
+
imports.add(f"from {donor.__module__} import {donor.__name__} as {donor_alias}")
|
|
44
|
+
bases.append(donor_alias)
|
|
45
|
+
contributor_aliases[donor] = donor_alias
|
|
46
|
+
bases.append(abstract)
|
|
47
|
+
parent = composition.parent(source)
|
|
48
|
+
if parent is not None:
|
|
49
|
+
parent_name = parent.__name__
|
|
50
|
+
if parent._meta.app_label != label:
|
|
51
|
+
parent_name = alias(f"_Parent{name}")
|
|
52
|
+
imports.add(
|
|
53
|
+
f"from {runtime_module}.{parent._meta.app_label}.models "
|
|
54
|
+
f"import {parent.__name__} as {parent_name}"
|
|
55
|
+
)
|
|
56
|
+
bases.append(parent_name)
|
|
57
|
+
declaration = (declarations or {}).get(source, ModelDeclaration())
|
|
58
|
+
body = [
|
|
59
|
+
f"class {name}({', '.join(bases)}):",
|
|
60
|
+
f' """Concrete {name} composed from native abstract models."""',
|
|
61
|
+
]
|
|
62
|
+
body.extend(
|
|
63
|
+
f" {key} = {render_literal(value)}" for key, value in sorted(declaration.attributes.items())
|
|
64
|
+
)
|
|
65
|
+
body.extend(
|
|
66
|
+
[
|
|
67
|
+
f" compose_contributed_field_origins: {classvar}[tuple[tuple[str, str], ...]] = "
|
|
68
|
+
+ repr(composition.contributed_field_origins(source)),
|
|
69
|
+
"",
|
|
70
|
+
f" class Meta({abstract}.Meta):",
|
|
71
|
+
" abstract = False",
|
|
72
|
+
f" app_label = {label!r}",
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
if constraint_sources := composition.constraint_sources(source):
|
|
76
|
+
constraints = ", ".join(
|
|
77
|
+
f"{contributor_aliases[contributor]}._meta.constraints[{index}]"
|
|
78
|
+
for contributor, index in constraint_sources
|
|
79
|
+
)
|
|
80
|
+
body.append(f" constraints = [{constraints}]")
|
|
81
|
+
body.extend(
|
|
82
|
+
f" {key} = {render_literal(value)}"
|
|
83
|
+
for key, value in sorted(declaration.meta_options.items())
|
|
84
|
+
)
|
|
85
|
+
classes.append("\n".join(body))
|
|
86
|
+
return (
|
|
87
|
+
'"""Prebuilt concrete Django models."""\n\nfrom __future__ import annotations\n\n'
|
|
88
|
+
+ "\n".join(sorted(imports))
|
|
89
|
+
+ "\n\n"
|
|
90
|
+
+ "\n\n\n".join(classes)
|
|
91
|
+
+ "\n"
|
|
92
|
+
)
|