plain.models 0.33.1__py3-none-any.whl → 0.34.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/models/CHANGELOG.md +27 -0
- plain/models/README.md +8 -10
- plain/models/__init__.py +2 -6
- plain/models/backends/base/base.py +10 -18
- plain/models/backends/base/creation.py +3 -4
- plain/models/backends/base/introspection.py +2 -3
- plain/models/backends/base/schema.py +3 -9
- plain/models/backends/mysql/validation.py +1 -1
- plain/models/backends/postgresql/base.py +15 -23
- plain/models/backends/postgresql/schema.py +0 -2
- plain/models/backends/sqlite3/base.py +1 -1
- plain/models/backends/sqlite3/creation.py +2 -2
- plain/models/backends/sqlite3/features.py +1 -1
- plain/models/backends/sqlite3/schema.py +1 -1
- plain/models/backends/utils.py +2 -6
- plain/models/backups/core.py +15 -22
- plain/models/base.py +179 -225
- plain/models/cli.py +25 -62
- plain/models/connections.py +48 -165
- plain/models/constraints.py +10 -10
- plain/models/db.py +7 -15
- plain/models/default_settings.py +13 -20
- plain/models/deletion.py +14 -16
- plain/models/expressions.py +7 -10
- plain/models/fields/__init__.py +56 -76
- plain/models/fields/json.py +9 -12
- plain/models/fields/related.py +5 -17
- plain/models/fields/related_descriptors.py +43 -95
- plain/models/forms.py +2 -4
- plain/models/indexes.py +2 -3
- plain/models/lookups.py +0 -7
- plain/models/manager.py +1 -14
- plain/models/migrations/executor.py +0 -16
- plain/models/migrations/loader.py +1 -1
- plain/models/migrations/migration.py +1 -1
- plain/models/migrations/operations/base.py +4 -11
- plain/models/migrations/operations/fields.py +4 -4
- plain/models/migrations/operations/models.py +10 -10
- plain/models/migrations/operations/special.py +6 -14
- plain/models/migrations/recorder.py +1 -1
- plain/models/options.py +4 -7
- plain/models/preflight.py +25 -44
- plain/models/query.py +47 -102
- plain/models/query_utils.py +4 -4
- plain/models/sql/compiler.py +7 -11
- plain/models/sql/query.py +32 -42
- plain/models/sql/subqueries.py +6 -8
- plain/models/sql/where.py +1 -1
- plain/models/test/pytest.py +21 -32
- plain/models/test/utils.py +7 -143
- plain/models/transaction.py +66 -164
- {plain_models-0.33.1.dist-info → plain_models-0.34.1.dist-info}/METADATA +9 -11
- {plain_models-0.33.1.dist-info → plain_models-0.34.1.dist-info}/RECORD +56 -55
- {plain_models-0.33.1.dist-info → plain_models-0.34.1.dist-info}/WHEEL +0 -0
- {plain_models-0.33.1.dist-info → plain_models-0.34.1.dist-info}/entry_points.txt +0 -0
- {plain_models-0.33.1.dist-info → plain_models-0.34.1.dist-info}/licenses/LICENSE +0 -0
plain/models/sql/query.py
CHANGED
@@ -20,7 +20,7 @@ from string import ascii_uppercase
|
|
20
20
|
from plain.exceptions import FieldDoesNotExist, FieldError
|
21
21
|
from plain.models.aggregates import Count
|
22
22
|
from plain.models.constants import LOOKUP_SEP
|
23
|
-
from plain.models.db import
|
23
|
+
from plain.models.db import NotSupportedError, db_connection
|
24
24
|
from plain.models.expressions import (
|
25
25
|
BaseExpression,
|
26
26
|
Col,
|
@@ -83,10 +83,9 @@ JoinInfo = namedtuple(
|
|
83
83
|
class RawQuery:
|
84
84
|
"""A single raw SQL query."""
|
85
85
|
|
86
|
-
def __init__(self, sql,
|
86
|
+
def __init__(self, sql, params=()):
|
87
87
|
self.params = params
|
88
88
|
self.sql = sql
|
89
|
-
self.using = using
|
90
89
|
self.cursor = None
|
91
90
|
|
92
91
|
# Mirror some properties of a normal query so that
|
@@ -95,23 +94,23 @@ class RawQuery:
|
|
95
94
|
self.extra_select = {}
|
96
95
|
self.annotation_select = {}
|
97
96
|
|
98
|
-
def chain(self
|
99
|
-
return self.clone(
|
97
|
+
def chain(self):
|
98
|
+
return self.clone()
|
100
99
|
|
101
|
-
def clone(self
|
102
|
-
return RawQuery(self.sql,
|
100
|
+
def clone(self):
|
101
|
+
return RawQuery(self.sql, params=self.params)
|
103
102
|
|
104
103
|
def get_columns(self):
|
105
104
|
if self.cursor is None:
|
106
105
|
self._execute_query()
|
107
|
-
converter =
|
106
|
+
converter = db_connection.introspection.identifier_converter
|
108
107
|
return [converter(column_meta[0]) for column_meta in self.cursor.description]
|
109
108
|
|
110
109
|
def __iter__(self):
|
111
110
|
# Always execute a new query for a new iterator.
|
112
111
|
# This could be optimized with a cache at the expense of RAM.
|
113
112
|
self._execute_query()
|
114
|
-
if not
|
113
|
+
if not db_connection.features.can_use_chunked_reads:
|
115
114
|
# If the database can't use chunked reads we need to make sure we
|
116
115
|
# evaluate the entire query up front.
|
117
116
|
result = list(self.cursor)
|
@@ -134,12 +133,10 @@ class RawQuery:
|
|
134
133
|
return self.sql % self.params_type(self.params)
|
135
134
|
|
136
135
|
def _execute_query(self):
|
137
|
-
connection = connections[self.using]
|
138
|
-
|
139
136
|
# Adapt parameters to the database, as much as possible considering
|
140
137
|
# that the target type isn't known. See #17755.
|
141
138
|
params_type = self.params_type
|
142
|
-
adapter =
|
139
|
+
adapter = db_connection.ops.adapt_unknown_value
|
143
140
|
if params_type is tuple:
|
144
141
|
params = tuple(adapter(val) for val in self.params)
|
145
142
|
elif params_type is dict:
|
@@ -149,7 +146,7 @@ class RawQuery:
|
|
149
146
|
else:
|
150
147
|
raise RuntimeError(f"Unexpected params type: {params_type}")
|
151
148
|
|
152
|
-
self.cursor =
|
149
|
+
self.cursor = db_connection.cursor()
|
153
150
|
self.cursor.execute(self.sql, params)
|
154
151
|
|
155
152
|
|
@@ -286,7 +283,7 @@ class Query(BaseExpression):
|
|
286
283
|
Return the query as an SQL string and the parameters that will be
|
287
284
|
substituted into the query.
|
288
285
|
"""
|
289
|
-
return self.get_compiler(
|
286
|
+
return self.get_compiler().as_sql()
|
290
287
|
|
291
288
|
def __deepcopy__(self, memo):
|
292
289
|
"""Limit the amount of work when a Query is deepcopied."""
|
@@ -294,13 +291,9 @@ class Query(BaseExpression):
|
|
294
291
|
memo[id(self)] = result
|
295
292
|
return result
|
296
293
|
|
297
|
-
def get_compiler(self,
|
298
|
-
|
299
|
-
|
300
|
-
if using:
|
301
|
-
connection = connections[using]
|
302
|
-
return connection.ops.compiler(self.compiler)(
|
303
|
-
self, connection, using, elide_empty
|
294
|
+
def get_compiler(self, *, elide_empty=True):
|
295
|
+
return db_connection.ops.compiler(self.compiler)(
|
296
|
+
self, db_connection, elide_empty
|
304
297
|
)
|
305
298
|
|
306
299
|
def get_meta(self):
|
@@ -382,7 +375,7 @@ class Query(BaseExpression):
|
|
382
375
|
alias = None
|
383
376
|
return target.get_col(alias, field)
|
384
377
|
|
385
|
-
def get_aggregation(self,
|
378
|
+
def get_aggregation(self, aggregate_exprs):
|
386
379
|
"""
|
387
380
|
Return the dictionary with the values of the existing aggregations.
|
388
381
|
"""
|
@@ -519,7 +512,7 @@ class Query(BaseExpression):
|
|
519
512
|
outer_query.clear_limits()
|
520
513
|
outer_query.select_for_update = False
|
521
514
|
outer_query.select_related = False
|
522
|
-
compiler = outer_query.get_compiler(
|
515
|
+
compiler = outer_query.get_compiler(elide_empty=elide_empty)
|
523
516
|
result = compiler.execute_sql(SINGLE)
|
524
517
|
if result is None:
|
525
518
|
result = empty_set_result
|
@@ -529,12 +522,12 @@ class Query(BaseExpression):
|
|
529
522
|
|
530
523
|
return dict(zip(outer_query.annotation_select, result))
|
531
524
|
|
532
|
-
def get_count(self
|
525
|
+
def get_count(self):
|
533
526
|
"""
|
534
527
|
Perform a COUNT() query using the current filter constraints.
|
535
528
|
"""
|
536
529
|
obj = self.clone()
|
537
|
-
return obj.get_aggregation(
|
530
|
+
return obj.get_aggregation({"__count": Count("*")})["__count"]
|
538
531
|
|
539
532
|
def has_filters(self):
|
540
533
|
return self.where
|
@@ -561,12 +554,12 @@ class Query(BaseExpression):
|
|
561
554
|
q.add_annotation(Value(1), "a")
|
562
555
|
return q
|
563
556
|
|
564
|
-
def has_results(self
|
565
|
-
q = self.exists(
|
566
|
-
compiler = q.get_compiler(
|
557
|
+
def has_results(self):
|
558
|
+
q = self.exists()
|
559
|
+
compiler = q.get_compiler()
|
567
560
|
return compiler.has_results()
|
568
561
|
|
569
|
-
def explain(self,
|
562
|
+
def explain(self, format=None, **options):
|
570
563
|
q = self.clone()
|
571
564
|
for option_name in options:
|
572
565
|
if (
|
@@ -575,7 +568,7 @@ class Query(BaseExpression):
|
|
575
568
|
):
|
576
569
|
raise ValueError(f"Invalid option name: {option_name!r}.")
|
577
570
|
q.explain_info = ExplainInfo(format, options)
|
578
|
-
compiler = q.get_compiler(
|
571
|
+
compiler = q.get_compiler()
|
579
572
|
return "\n".join(compiler.explain_query())
|
580
573
|
|
581
574
|
def combine(self, rhs, connector):
|
@@ -1110,12 +1103,12 @@ class Query(BaseExpression):
|
|
1110
1103
|
# unnecessary ORDER BY clause.
|
1111
1104
|
if (
|
1112
1105
|
self.subquery
|
1113
|
-
and not
|
1106
|
+
and not db_connection.features.ignores_unnecessary_order_by_in_subqueries
|
1114
1107
|
):
|
1115
1108
|
self.clear_ordering(force=False)
|
1116
1109
|
for query in self.combined_queries:
|
1117
1110
|
query.clear_ordering(force=False)
|
1118
|
-
sql, params = self.get_compiler(
|
1111
|
+
sql, params = self.get_compiler().as_sql()
|
1119
1112
|
if self.subquery:
|
1120
1113
|
sql = f"({sql})"
|
1121
1114
|
return sql, params
|
@@ -1241,13 +1234,12 @@ class Query(BaseExpression):
|
|
1241
1234
|
return lhs.get_lookup("isnull")(lhs, True)
|
1242
1235
|
|
1243
1236
|
# For Oracle '' is equivalent to null. The check must be done at this
|
1244
|
-
# stage because join promotion can't be done in the compiler.
|
1245
|
-
#
|
1246
|
-
# A similar thing is done in is_nullable(), too.
|
1237
|
+
# stage because join promotion can't be done in the compiler. A similar
|
1238
|
+
# thing is done in is_nullable(), too.
|
1247
1239
|
if (
|
1248
1240
|
lookup_name == "exact"
|
1249
1241
|
and lookup.rhs == ""
|
1250
|
-
and
|
1242
|
+
and db_connection.features.interprets_empty_strings_as_nulls
|
1251
1243
|
):
|
1252
1244
|
return lhs.get_lookup("isnull")(lhs, True)
|
1253
1245
|
|
@@ -2481,14 +2473,12 @@ class Query(BaseExpression):
|
|
2481
2473
|
nullable for those backends. In such situations field.allow_null can be
|
2482
2474
|
False even if we should treat the field as nullable.
|
2483
2475
|
"""
|
2484
|
-
#
|
2485
|
-
#
|
2486
|
-
#
|
2487
|
-
# is_nullable() is needed to the compiler stage, but that is not easy
|
2488
|
-
# to do currently.
|
2476
|
+
# QuerySet does not have knowledge of which connection is going to be
|
2477
|
+
# used. For the single-database setup we always reference the default
|
2478
|
+
# connection here.
|
2489
2479
|
return field.allow_null or (
|
2490
2480
|
field.empty_strings_allowed
|
2491
|
-
and
|
2481
|
+
and db_connection.features.interprets_empty_strings_as_nulls
|
2492
2482
|
)
|
2493
2483
|
|
2494
2484
|
|
plain/models/sql/subqueries.py
CHANGED
@@ -14,16 +14,16 @@ class DeleteQuery(Query):
|
|
14
14
|
|
15
15
|
compiler = "SQLDeleteCompiler"
|
16
16
|
|
17
|
-
def do_query(self, table, where
|
17
|
+
def do_query(self, table, where):
|
18
18
|
self.alias_map = {table: self.alias_map[table]}
|
19
19
|
self.where = where
|
20
|
-
cursor = self.get_compiler(
|
20
|
+
cursor = self.get_compiler().execute_sql(CURSOR)
|
21
21
|
if cursor:
|
22
22
|
with cursor:
|
23
23
|
return cursor.rowcount
|
24
24
|
return 0
|
25
25
|
|
26
|
-
def delete_batch(self, pk_list
|
26
|
+
def delete_batch(self, pk_list):
|
27
27
|
"""
|
28
28
|
Set up and execute delete queries for all the objects in pk_list.
|
29
29
|
|
@@ -39,9 +39,7 @@ class DeleteQuery(Query):
|
|
39
39
|
f"{field.attname}__in",
|
40
40
|
pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE],
|
41
41
|
)
|
42
|
-
num_deleted += self.do_query(
|
43
|
-
self.get_meta().db_table, self.where, using=using
|
44
|
-
)
|
42
|
+
num_deleted += self.do_query(self.get_meta().db_table, self.where)
|
45
43
|
return num_deleted
|
46
44
|
|
47
45
|
|
@@ -68,14 +66,14 @@ class UpdateQuery(Query):
|
|
68
66
|
obj.related_updates = self.related_updates.copy()
|
69
67
|
return obj
|
70
68
|
|
71
|
-
def update_batch(self, pk_list, values
|
69
|
+
def update_batch(self, pk_list, values):
|
72
70
|
self.add_update_values(values)
|
73
71
|
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
|
74
72
|
self.clear_where()
|
75
73
|
self.add_filter(
|
76
74
|
"pk__in", pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]
|
77
75
|
)
|
78
|
-
self.get_compiler(
|
76
|
+
self.get_compiler().execute_sql(NO_RESULTS)
|
79
77
|
|
80
78
|
def add_update_values(self, values):
|
81
79
|
"""
|
plain/models/sql/where.py
CHANGED
@@ -351,5 +351,5 @@ class SubqueryConstraint:
|
|
351
351
|
def as_sql(self, compiler, connection):
|
352
352
|
query = self.query_object
|
353
353
|
query.set_values(self.targets)
|
354
|
-
query_compiler = query.get_compiler(
|
354
|
+
query_compiler = query.get_compiler()
|
355
355
|
return query_compiler.as_subquery_condition(self.alias, self.columns, compiler)
|
plain/models/test/pytest.py
CHANGED
@@ -6,10 +6,10 @@ from plain.signals import request_finished, request_started
|
|
6
6
|
|
7
7
|
from .. import transaction
|
8
8
|
from ..backends.base.base import BaseDatabaseWrapper
|
9
|
-
from ..db import close_old_connections,
|
9
|
+
from ..db import close_old_connections, db_connection
|
10
10
|
from .utils import (
|
11
|
-
|
12
|
-
|
11
|
+
setup_database,
|
12
|
+
teardown_database,
|
13
13
|
)
|
14
14
|
|
15
15
|
|
@@ -40,7 +40,7 @@ def setup_db(request):
|
|
40
40
|
verbosity = request.config.option.verbose
|
41
41
|
|
42
42
|
# Set up the test db across the entire session
|
43
|
-
|
43
|
+
_old_db_name = setup_database(verbosity=verbosity)
|
44
44
|
|
45
45
|
# Keep connections open during request client / testing
|
46
46
|
request_started.disconnect(close_old_connections)
|
@@ -53,7 +53,7 @@ def setup_db(request):
|
|
53
53
|
request_finished.connect(close_old_connections)
|
54
54
|
|
55
55
|
# When the test session is done, tear down the test db
|
56
|
-
|
56
|
+
teardown_database(_old_db_name, verbosity=verbosity)
|
57
57
|
|
58
58
|
|
59
59
|
@pytest.fixture
|
@@ -63,37 +63,26 @@ def db(setup_db, request):
|
|
63
63
|
# Set .cursor() back to the original implementation to unblock it
|
64
64
|
BaseDatabaseWrapper.cursor = BaseDatabaseWrapper._enabled_cursor
|
65
65
|
|
66
|
-
|
67
|
-
|
66
|
+
if not db_connection.features.supports_transactions:
|
67
|
+
pytest.fail("Database does not support transactions")
|
68
68
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
if not connection.features.supports_transactions:
|
73
|
-
pytest.fail("Database does not support transactions")
|
74
|
-
|
75
|
-
# Clear the queries log before each test?
|
76
|
-
# connection.queries_log.clear()
|
77
|
-
|
78
|
-
atomic = transaction.atomic(using=connection.alias)
|
79
|
-
atomic._from_testcase = True # TODO remove this somehow?
|
80
|
-
atomic.__enter__()
|
81
|
-
atomics[connection] = atomic
|
69
|
+
atomic = transaction.atomic()
|
70
|
+
atomic._from_testcase = True # TODO remove this somehow?
|
71
|
+
atomic.__enter__()
|
82
72
|
|
83
73
|
yield
|
84
74
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
)
|
91
|
-
connection.check_constraints()
|
75
|
+
if (
|
76
|
+
db_connection.features.can_defer_constraint_checks
|
77
|
+
and not db_connection.needs_rollback
|
78
|
+
and db_connection.is_usable()
|
79
|
+
):
|
80
|
+
db_connection.check_constraints()
|
92
81
|
|
93
|
-
|
94
|
-
|
82
|
+
db_connection.set_rollback(True)
|
83
|
+
atomic.__exit__(None, None, None)
|
95
84
|
|
96
|
-
|
85
|
+
db_connection.close()
|
97
86
|
|
98
87
|
|
99
88
|
@pytest.fixture
|
@@ -115,9 +104,9 @@ def isolated_db(request):
|
|
115
104
|
prefix = re.sub(r"[^0-9A-Za-z_]+", "_", raw_name)
|
116
105
|
|
117
106
|
# Set up a fresh test database for this test, using the prefix
|
118
|
-
|
107
|
+
_old_db_name = setup_database(verbosity=verbosity, prefix=prefix)
|
119
108
|
|
120
109
|
yield
|
121
110
|
|
122
111
|
# Tear down the test database created for this test
|
123
|
-
|
112
|
+
teardown_database(_old_db_name, verbosity=verbosity)
|
plain/models/test/utils.py
CHANGED
@@ -1,147 +1,11 @@
|
|
1
|
-
from plain.
|
2
|
-
from plain.models import DEFAULT_DB_ALIAS, connections
|
1
|
+
from plain.models import db_connection
|
3
2
|
|
4
3
|
|
5
|
-
def
|
6
|
-
""
|
7
|
-
|
4
|
+
def setup_database(*, verbosity, prefix=""):
|
5
|
+
old_name = db_connection.settings_dict["NAME"]
|
6
|
+
db_connection.creation.create_test_db(verbosity=verbosity, prefix=prefix)
|
7
|
+
return old_name
|
8
8
|
|
9
|
-
If prefix is provided, each test database name will be prefixed with
|
10
|
-
"<prefix>_" to isolate it from the default test database.
|
11
|
-
"""
|
12
9
|
|
13
|
-
|
14
|
-
|
15
|
-
old_names = []
|
16
|
-
|
17
|
-
for db_name, aliases in test_databases.values():
|
18
|
-
first_alias = None
|
19
|
-
for alias in aliases:
|
20
|
-
connection = connections[alias]
|
21
|
-
old_names.append((connection, db_name, first_alias is None))
|
22
|
-
|
23
|
-
# Actually create the database for the first connection
|
24
|
-
if first_alias is None:
|
25
|
-
first_alias = alias
|
26
|
-
connection.creation.create_test_db(
|
27
|
-
verbosity=verbosity,
|
28
|
-
prefix=prefix,
|
29
|
-
)
|
30
|
-
# Configure all other connections as mirrors of the first one
|
31
|
-
else:
|
32
|
-
connections[alias].creation.set_as_test_mirror(
|
33
|
-
connections[first_alias].settings_dict
|
34
|
-
)
|
35
|
-
|
36
|
-
# Configure the test mirrors.
|
37
|
-
for alias, mirror_alias in mirrored_aliases.items():
|
38
|
-
connections[alias].creation.set_as_test_mirror(
|
39
|
-
connections[mirror_alias].settings_dict
|
40
|
-
)
|
41
|
-
|
42
|
-
return old_names
|
43
|
-
|
44
|
-
|
45
|
-
def get_unique_databases_and_mirrors(prefix=""):
|
46
|
-
"""
|
47
|
-
Figure out which databases actually need to be created.
|
48
|
-
|
49
|
-
Deduplicate entries in DATABASES that correspond the same database or are
|
50
|
-
configured as test mirrors.
|
51
|
-
|
52
|
-
Return two values:
|
53
|
-
- test_databases: ordered mapping of signatures to (name, list of aliases)
|
54
|
-
where all aliases share the same underlying database.
|
55
|
-
- mirrored_aliases: mapping of mirror aliases to original aliases.
|
56
|
-
"""
|
57
|
-
|
58
|
-
aliases = connections
|
59
|
-
mirrored_aliases = {}
|
60
|
-
test_databases = {}
|
61
|
-
dependencies = {}
|
62
|
-
default_sig = connections[DEFAULT_DB_ALIAS].creation.test_db_signature(prefix)
|
63
|
-
|
64
|
-
for alias in connections:
|
65
|
-
connection = connections[alias]
|
66
|
-
test_settings = connection.settings_dict["TEST"]
|
67
|
-
|
68
|
-
if test_settings["MIRROR"]:
|
69
|
-
# If the database is marked as a test mirror, save the alias.
|
70
|
-
mirrored_aliases[alias] = test_settings["MIRROR"]
|
71
|
-
elif alias in aliases:
|
72
|
-
# Store a tuple with DB parameters that uniquely identify it.
|
73
|
-
# If we have two aliases with the same values for that tuple,
|
74
|
-
# we only need to create the test database once.
|
75
|
-
item = test_databases.setdefault(
|
76
|
-
connection.creation.test_db_signature(prefix),
|
77
|
-
(connection.settings_dict["NAME"], []),
|
78
|
-
)
|
79
|
-
# The default database must be the first because data migrations
|
80
|
-
# use the default alias by default.
|
81
|
-
if alias == DEFAULT_DB_ALIAS:
|
82
|
-
item[1].insert(0, alias)
|
83
|
-
else:
|
84
|
-
item[1].append(alias)
|
85
|
-
|
86
|
-
if "DEPENDENCIES" in test_settings:
|
87
|
-
dependencies[alias] = test_settings["DEPENDENCIES"]
|
88
|
-
else:
|
89
|
-
if (
|
90
|
-
alias != DEFAULT_DB_ALIAS
|
91
|
-
and connection.creation.test_db_signature(prefix) != default_sig
|
92
|
-
):
|
93
|
-
dependencies[alias] = test_settings.get(
|
94
|
-
"DEPENDENCIES", [DEFAULT_DB_ALIAS]
|
95
|
-
)
|
96
|
-
|
97
|
-
test_databases = dict(dependency_ordered(test_databases.items(), dependencies))
|
98
|
-
return test_databases, mirrored_aliases
|
99
|
-
|
100
|
-
|
101
|
-
def teardown_databases(old_config, verbosity):
|
102
|
-
"""Destroy all the non-mirror databases."""
|
103
|
-
for connection, old_name, destroy in old_config:
|
104
|
-
if destroy:
|
105
|
-
connection.creation.destroy_test_db(old_name, verbosity)
|
106
|
-
|
107
|
-
|
108
|
-
def dependency_ordered(test_databases, dependencies):
|
109
|
-
"""
|
110
|
-
Reorder test_databases into an order that honors the dependencies
|
111
|
-
described in TEST[DEPENDENCIES].
|
112
|
-
"""
|
113
|
-
ordered_test_databases = []
|
114
|
-
resolved_databases = set()
|
115
|
-
|
116
|
-
# Maps db signature to dependencies of all its aliases
|
117
|
-
dependencies_map = {}
|
118
|
-
|
119
|
-
# Check that no database depends on its own alias
|
120
|
-
for sig, (_, aliases) in test_databases:
|
121
|
-
all_deps = set()
|
122
|
-
for alias in aliases:
|
123
|
-
all_deps.update(dependencies.get(alias, []))
|
124
|
-
if not all_deps.isdisjoint(aliases):
|
125
|
-
raise ImproperlyConfigured(
|
126
|
-
f"Circular dependency: databases {aliases!r} depend on each other, "
|
127
|
-
"but are aliases."
|
128
|
-
)
|
129
|
-
dependencies_map[sig] = all_deps
|
130
|
-
|
131
|
-
while test_databases:
|
132
|
-
changed = False
|
133
|
-
deferred = []
|
134
|
-
|
135
|
-
# Try to find a DB that has all its dependencies met
|
136
|
-
for signature, (db_name, aliases) in test_databases:
|
137
|
-
if dependencies_map[signature].issubset(resolved_databases):
|
138
|
-
resolved_databases.update(aliases)
|
139
|
-
ordered_test_databases.append((signature, (db_name, aliases)))
|
140
|
-
changed = True
|
141
|
-
else:
|
142
|
-
deferred.append((signature, (db_name, aliases)))
|
143
|
-
|
144
|
-
if not changed:
|
145
|
-
raise ImproperlyConfigured("Circular dependency in TEST[DEPENDENCIES]")
|
146
|
-
test_databases = deferred
|
147
|
-
return ordered_test_databases
|
10
|
+
def teardown_database(old_name, verbosity):
|
11
|
+
db_connection.creation.destroy_test_db(old_name, verbosity)
|