django-dbdiff 0.9.6__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.
- dbdiff/__init__.py +3 -0
- dbdiff/apps.py +44 -0
- dbdiff/exceptions.py +72 -0
- dbdiff/fixture.py +172 -0
- dbdiff/plugin.py +35 -0
- dbdiff/sequence.py +57 -0
- dbdiff/serializers/__init__.py +1 -0
- dbdiff/serializers/base.py +80 -0
- dbdiff/serializers/json.py +15 -0
- dbdiff/test.py +58 -0
- dbdiff/tests/__init__.py +1 -0
- dbdiff/tests/decimal_test/__init__.py +1 -0
- dbdiff/tests/decimal_test/migrations/0001_initial.py +17 -0
- dbdiff/tests/decimal_test/migrations/0002_auto_20160102_0914.py +16 -0
- dbdiff/tests/decimal_test/migrations/__init__.py +0 -0
- dbdiff/tests/decimal_test/models.py +5 -0
- dbdiff/tests/inheritance/__init__.py +1 -0
- dbdiff/tests/inheritance/models.py +9 -0
- dbdiff/tests/nonintpk/__init__.py +1 -0
- dbdiff/tests/nonintpk/models.py +8 -0
- dbdiff/tests/project/__init__.py +1 -0
- dbdiff/tests/project/settings.py +107 -0
- dbdiff/tests/project/settings_mysql.py +21 -0
- dbdiff/tests/project/settings_postgresql.py +16 -0
- dbdiff/tests/project/settings_sqlite.py +8 -0
- dbdiff/tests/project/urls.py +1 -0
- dbdiff/tests/test_compare.py +102 -0
- dbdiff/tests/test_decimal.py +54 -0
- dbdiff/tests/test_fixture.py +25 -0
- dbdiff/tests/test_mixin.py +22 -0
- dbdiff/tests/test_plugin.py +34 -0
- dbdiff/tests/test_utils.py +49 -0
- dbdiff/utils.py +179 -0
- django_dbdiff-0.9.6.dist-info/METADATA +151 -0
- django_dbdiff-0.9.6.dist-info/RECORD +38 -0
- django_dbdiff-0.9.6.dist-info/WHEEL +5 -0
- django_dbdiff-0.9.6.dist-info/entry_points.txt +2 -0
- django_dbdiff-0.9.6.dist-info/top_level.txt +1 -0
dbdiff/__init__.py
ADDED
dbdiff/apps.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""AppConfig for dbdiff."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from django.apps import AppConfig
|
|
6
|
+
from django.core.serializers import register_serializer
|
|
7
|
+
|
|
8
|
+
from .utils import patch_transaction_test_case
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DefaultConfig(AppConfig):
|
|
12
|
+
"""
|
|
13
|
+
Register patched serializers and patch TransactionTestCase for sqlite.
|
|
14
|
+
|
|
15
|
+
.. py:attribute:: debug
|
|
16
|
+
|
|
17
|
+
If True, then diff commands will be printed to stdout and temporary
|
|
18
|
+
files will not be deleted.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name = 'dbdiff'
|
|
22
|
+
debug = False
|
|
23
|
+
default_indent = 4
|
|
24
|
+
|
|
25
|
+
def ready(self):
|
|
26
|
+
"""
|
|
27
|
+
Register dbdiff.serializers.json and set debug.
|
|
28
|
+
|
|
29
|
+
Enables debug if a DBDIFF_DEBUG environment variable is found.
|
|
30
|
+
|
|
31
|
+
It is important to use serializers which dump data in a predictible way
|
|
32
|
+
because this app relies on diff between an expected - user-generated
|
|
33
|
+
and versioned - fixture and dumped database data. This method also
|
|
34
|
+
overrides the default json serializer with dbdiff's.
|
|
35
|
+
|
|
36
|
+
When dbdiff is installed, ``dumpdata`` will use its serializers which
|
|
37
|
+
have predictible output and cross-database support, so fixtures dumped
|
|
38
|
+
without dbdiff installed will have to be regenerated after dbdiff is
|
|
39
|
+
installed to be usable with dbdiff.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
self.debug = os.environ.get('DBDIFF_DEBUG', False)
|
|
43
|
+
register_serializer('json', 'dbdiff.serializers.json')
|
|
44
|
+
patch_transaction_test_case()
|
dbdiff/exceptions.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Exceptions for dbdiff module."""
|
|
2
|
+
import pprint
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DbDiffException(Exception):
|
|
6
|
+
"""Base exception for this app."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DiffFound(DbDiffException):
|
|
10
|
+
"""Raised when a diff is found by the context manager."""
|
|
11
|
+
|
|
12
|
+
def _add_messages(self, msg, title, tree):
|
|
13
|
+
if tree:
|
|
14
|
+
for model, instances in tree.items():
|
|
15
|
+
msg.append(
|
|
16
|
+
title % (
|
|
17
|
+
len(instances),
|
|
18
|
+
model
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
for pk, fields in instances.items():
|
|
23
|
+
msg.append('#%s:\n%s' % (pk, pprint.pformat(fields)))
|
|
24
|
+
|
|
25
|
+
def __init__(self, fixture, unexpected, missing, diff):
|
|
26
|
+
"""Exception for when a diff was found."""
|
|
27
|
+
msg = ['Diff found with dump at %s' % fixture.path]
|
|
28
|
+
|
|
29
|
+
self._add_messages(
|
|
30
|
+
msg,
|
|
31
|
+
'%s unexpected instance(s) of %s found in the dump:',
|
|
32
|
+
unexpected
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
self._add_messages(
|
|
36
|
+
msg,
|
|
37
|
+
'%s expected instance(s) of %s missing from dump:',
|
|
38
|
+
missing
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if diff:
|
|
42
|
+
for model, instances in diff.items():
|
|
43
|
+
msg.append(
|
|
44
|
+
'%s instance(s) of %s have not expected fields' % (
|
|
45
|
+
len(instances),
|
|
46
|
+
model
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
for pk, fields in instances.items():
|
|
51
|
+
msg.append('#%s:' % pk)
|
|
52
|
+
|
|
53
|
+
for field, values in fields.items():
|
|
54
|
+
msg.append(' %s:' % field)
|
|
55
|
+
msg.append('- %s' % pprint.pformat(values[0]))
|
|
56
|
+
msg.append('+ %s' % pprint.pformat(values[1]))
|
|
57
|
+
|
|
58
|
+
super(DiffFound, self).__init__('\n'.join(msg))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class FixtureCreated(DbDiffException):
|
|
62
|
+
"""
|
|
63
|
+
Raised when a fixture was created.
|
|
64
|
+
|
|
65
|
+
This purposely fails a test, to avoid misleading the user into thinking
|
|
66
|
+
that the test was properly executed against a versioned fixture. Imagine
|
|
67
|
+
one pushes a test without the fixture, it will break because of this
|
|
68
|
+
exception in CI.
|
|
69
|
+
|
|
70
|
+
However, this should only happen once per fixture - unless the user in
|
|
71
|
+
question forgets to commit the generated fixture !
|
|
72
|
+
"""
|
dbdiff/fixture.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Public fixture API."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
|
|
8
|
+
from django.apps import apps
|
|
9
|
+
from django.core.management import call_command
|
|
10
|
+
|
|
11
|
+
import ijson
|
|
12
|
+
|
|
13
|
+
from .exceptions import DiffFound, FixtureCreated
|
|
14
|
+
from .utils import (
|
|
15
|
+
diff,
|
|
16
|
+
get_absolute_path,
|
|
17
|
+
get_model_names,
|
|
18
|
+
get_tree,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
REWRITE = os.getenv('FIXTURE_REWRITE')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Fixture(object):
|
|
26
|
+
"""
|
|
27
|
+
Is able to print out diffs between database and a fixture.
|
|
28
|
+
|
|
29
|
+
.. py:attribute:: path
|
|
30
|
+
|
|
31
|
+
Absolute path to the fixture.
|
|
32
|
+
|
|
33
|
+
.. py:attribute:: models
|
|
34
|
+
|
|
35
|
+
List of models concerned by the fixture.
|
|
36
|
+
|
|
37
|
+
.. py:attribute:: database
|
|
38
|
+
|
|
39
|
+
Database name to use, 'default' by default.
|
|
40
|
+
|
|
41
|
+
.. py:attribute:: exclude
|
|
42
|
+
|
|
43
|
+
Class attribute, dict of model fields to ignore in the form of:
|
|
44
|
+
{'app.model': ['fieldN']}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
exclude = dict()
|
|
48
|
+
|
|
49
|
+
def __init__(self, relative_path, models=None, database=None, ignore_pk=False):
|
|
50
|
+
"""
|
|
51
|
+
Instanciate a FixtureDiff on a database.
|
|
52
|
+
|
|
53
|
+
relative_path is used to calculate :py:attr:`path`, with
|
|
54
|
+
:py:func:`~utils.get_absolute_path`.
|
|
55
|
+
|
|
56
|
+
If models is None, then it will be generated from reading the
|
|
57
|
+
fixture file, but generated fixtures will include all models.
|
|
58
|
+
|
|
59
|
+
database should be the name of the database to use, `default` by
|
|
60
|
+
default.
|
|
61
|
+
|
|
62
|
+
ignore_pk when True, matches records by field content instead of
|
|
63
|
+
primary key. Records with the same content but different pks
|
|
64
|
+
are considered equal.
|
|
65
|
+
"""
|
|
66
|
+
self.path = get_absolute_path(relative_path)
|
|
67
|
+
self.models = models if models else self.parse_models()
|
|
68
|
+
self.database = database or 'default'
|
|
69
|
+
self.ignore_pk = ignore_pk
|
|
70
|
+
|
|
71
|
+
def parse_models(self):
|
|
72
|
+
"""Return the list of models inside the fixture file."""
|
|
73
|
+
with open(self.path, 'r') as f:
|
|
74
|
+
return [apps.get_model(i.lower())
|
|
75
|
+
for i in ijson.items(f, 'item.model')]
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def exists(self):
|
|
79
|
+
"""Return True if :py:attr:`path` exists."""
|
|
80
|
+
return os.path.exists(self.path)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def indent(self):
|
|
84
|
+
"""Return the indentation of the fixture file or the default indent."""
|
|
85
|
+
if not os.path.exists(self.path):
|
|
86
|
+
return apps.get_app_config('dbdiff').default_indent
|
|
87
|
+
|
|
88
|
+
with open(self.path, 'r') as f:
|
|
89
|
+
line = f.readline()
|
|
90
|
+
|
|
91
|
+
while line and ':' not in line:
|
|
92
|
+
line = f.readline()
|
|
93
|
+
|
|
94
|
+
if not line:
|
|
95
|
+
return apps.get_app_config('dbdiff').default_indent
|
|
96
|
+
|
|
97
|
+
return len(line) - len(line.lstrip(' '))
|
|
98
|
+
|
|
99
|
+
def diff(self, exclude=None, ignore_pk=None):
|
|
100
|
+
"""
|
|
101
|
+
Diff the fixture against a datadump of fixture models.
|
|
102
|
+
|
|
103
|
+
If passed, exclude should be a list of field names to exclude from
|
|
104
|
+
being diff'ed.
|
|
105
|
+
|
|
106
|
+
ignore_pk when True, matches records by field content instead of
|
|
107
|
+
primary key. Defaults to the instance's ignore_pk attribute.
|
|
108
|
+
"""
|
|
109
|
+
fh, dump_path = tempfile.mkstemp('_dbdiff')
|
|
110
|
+
|
|
111
|
+
exclude_final = copy.copy(self.exclude)
|
|
112
|
+
exclude_final.update(exclude or {})
|
|
113
|
+
|
|
114
|
+
with os.fdopen(fh, 'w') as f:
|
|
115
|
+
self.dump(f)
|
|
116
|
+
|
|
117
|
+
with open(self.path, 'r') as e, open(dump_path, 'r') as r:
|
|
118
|
+
expected, result = json.load(e), json.load(r)
|
|
119
|
+
|
|
120
|
+
if ignore_pk is None:
|
|
121
|
+
ignore_pk = self.ignore_pk
|
|
122
|
+
|
|
123
|
+
unexpected, missing, different = diff(
|
|
124
|
+
get_tree(expected, exclude_final),
|
|
125
|
+
get_tree(result, exclude_final),
|
|
126
|
+
ignore_pk=ignore_pk,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if not unexpected and not missing and not diff:
|
|
130
|
+
os.unlink(dump_path)
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
return unexpected, missing, different
|
|
134
|
+
|
|
135
|
+
def load(self):
|
|
136
|
+
"""Load fixture into the database."""
|
|
137
|
+
call_command('loaddata', self.path)
|
|
138
|
+
|
|
139
|
+
def dump(self, out):
|
|
140
|
+
"""Write fixture with dumpdata for fixture models."""
|
|
141
|
+
call_command(
|
|
142
|
+
'dumpdata',
|
|
143
|
+
*get_model_names(self.models),
|
|
144
|
+
format='json',
|
|
145
|
+
traceback=True,
|
|
146
|
+
indent=self.indent,
|
|
147
|
+
stdout=out,
|
|
148
|
+
use_natural_foreign_keys=True
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def assertNoDiff(self, exclude=None): # noqa
|
|
152
|
+
"""Assert that the fixture doesn't have any diff with the database.
|
|
153
|
+
|
|
154
|
+
If the fixture doesn't exist then it's written but
|
|
155
|
+
:py:class:`~exceptions.FixtureCreated` is raised.
|
|
156
|
+
|
|
157
|
+
If a diff was found it will raise :py:class:`~exceptions.DiffFound`.
|
|
158
|
+
"""
|
|
159
|
+
if REWRITE or not self.exists:
|
|
160
|
+
with open(self.path, 'w+') as f:
|
|
161
|
+
self.dump(f)
|
|
162
|
+
if not REWRITE:
|
|
163
|
+
raise FixtureCreated(self)
|
|
164
|
+
|
|
165
|
+
unexpected, missing, different = self.diff(exclude=exclude)
|
|
166
|
+
|
|
167
|
+
if unexpected or missing or different:
|
|
168
|
+
raise DiffFound(self, unexpected, missing, different)
|
|
169
|
+
|
|
170
|
+
def __str__(self):
|
|
171
|
+
"""Return :py:attr:`path` for string representation."""
|
|
172
|
+
return self.path
|
dbdiff/plugin.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Pytest plugin for django-dbdiff.
|
|
2
|
+
|
|
3
|
+
The marker enables the smarter sequence reset feature previously available in
|
|
4
|
+
the DbdiffTestMixin in pytest, example usage::
|
|
5
|
+
|
|
6
|
+
@dbdiff(models=[YourModel])
|
|
7
|
+
def your_test():
|
|
8
|
+
assert YourModel.objects.create().pk == 1
|
|
9
|
+
"""
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from .sequence import sequence_reset
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(autouse=True)
|
|
16
|
+
def _dbdiff_marker(request):
|
|
17
|
+
marker = request.node.get_closest_marker('dbdiff')
|
|
18
|
+
if not marker:
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
# Enable transactional db
|
|
22
|
+
request.getfixturevalue('transactional_db')
|
|
23
|
+
|
|
24
|
+
for model in marker.kwargs['models']:
|
|
25
|
+
sequence_reset(model)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def pytest_load_initial_conftests(early_config, parser, args):
|
|
29
|
+
"""Register the dbdiff mark."""
|
|
30
|
+
early_config.addinivalue_line(
|
|
31
|
+
'markers',
|
|
32
|
+
'dbdiff(models, reset_sequences=True): Mark the test as using '
|
|
33
|
+
'the django test database. The *transaction* argument marks will '
|
|
34
|
+
"allow you to use real transactions in the test like Django's "
|
|
35
|
+
'TransactionTestCase.')
|
dbdiff/sequence.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Smarter model pk sequence reset."""
|
|
2
|
+
from django.db import connection, models
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def pk_sequence_get(model):
|
|
6
|
+
"""Return a list of table, column tuples which should have sequences."""
|
|
7
|
+
for field in model._meta.get_fields():
|
|
8
|
+
if not getattr(field, 'primary_key', False):
|
|
9
|
+
continue
|
|
10
|
+
if not isinstance(field, models.AutoField):
|
|
11
|
+
continue
|
|
12
|
+
return (field.db_column or field.column, field.model._meta.db_table)
|
|
13
|
+
return (None, None)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def sequence_reset(model):
|
|
17
|
+
"""
|
|
18
|
+
Better sequence reset than TransactionTestCase.
|
|
19
|
+
|
|
20
|
+
The difference with using TransactionTestCase with reset_sequences=True is
|
|
21
|
+
that this will reset sequences for the given models to their higher value,
|
|
22
|
+
supporting pre-existing models which could have been created by a
|
|
23
|
+
migration.
|
|
24
|
+
"""
|
|
25
|
+
pk_field, table = pk_sequence_get(model)
|
|
26
|
+
if not pk_field:
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
if connection.vendor == 'postgresql':
|
|
30
|
+
reset = """
|
|
31
|
+
SELECT
|
|
32
|
+
setval(
|
|
33
|
+
pg_get_serial_sequence('{table}', '{column}'),
|
|
34
|
+
coalesce(max({column}),0) + 1,
|
|
35
|
+
false
|
|
36
|
+
)
|
|
37
|
+
FROM {table}
|
|
38
|
+
"""
|
|
39
|
+
elif connection.vendor == 'sqlite':
|
|
40
|
+
reset = """
|
|
41
|
+
UPDATE sqlite_sequence
|
|
42
|
+
SET seq=(SELECT max({column}) from {table})
|
|
43
|
+
WHERE name='{table}'
|
|
44
|
+
"""
|
|
45
|
+
elif connection.vendor == 'mysql':
|
|
46
|
+
cursor = connection.cursor()
|
|
47
|
+
cursor.execute(
|
|
48
|
+
'SELECT MAX({column}) + 1 FROM {table}'.format(
|
|
49
|
+
column=pk_field, table=table
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
result = cursor.fetchone()[0] or 0
|
|
53
|
+
reset = 'ALTER TABLE {table} AUTO_INCREMENT = %s' % result
|
|
54
|
+
|
|
55
|
+
connection.cursor().execute(
|
|
56
|
+
reset.format(column=pk_field, table=table)
|
|
57
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Serializers with predictible (ordered) output."""
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Shared code for serializers."""
|
|
2
|
+
|
|
3
|
+
import collections
|
|
4
|
+
import datetime
|
|
5
|
+
import decimal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseSerializerMixin(object):
|
|
9
|
+
"""Serializer mixin for predictible and cross-db dumps."""
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def recursive_dict_sort(cls, data):
|
|
13
|
+
"""
|
|
14
|
+
Return a recursive OrderedDict for a dict.
|
|
15
|
+
|
|
16
|
+
Django's default model-to-dict logic - implemented in
|
|
17
|
+
django.core.serializers.python.Serializer.get_dump_object() - returns a
|
|
18
|
+
dict, this app registers a slightly modified version of the default
|
|
19
|
+
json serializer which returns OrderedDicts instead.
|
|
20
|
+
"""
|
|
21
|
+
ordered_data = collections.OrderedDict(sorted(data.items()))
|
|
22
|
+
|
|
23
|
+
for key, value in ordered_data.items():
|
|
24
|
+
if isinstance(value, dict):
|
|
25
|
+
ordered_data[key] = cls.recursive_dict_sort(value)
|
|
26
|
+
|
|
27
|
+
return ordered_data
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def remove_microseconds(cls, data):
|
|
31
|
+
"""
|
|
32
|
+
Strip microseconds from datetimes for mysql.
|
|
33
|
+
|
|
34
|
+
MySQL doesn't have microseconds in datetimes, so dbdiff's serializer
|
|
35
|
+
removes microseconds from datetimes so that fixtures are cross-database
|
|
36
|
+
compatible which make them usable for cross-database testing.
|
|
37
|
+
"""
|
|
38
|
+
for key, value in data['fields'].items():
|
|
39
|
+
if not isinstance(value, datetime.datetime):
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
data['fields'][key] = datetime.datetime(
|
|
43
|
+
year=value.year,
|
|
44
|
+
month=value.month,
|
|
45
|
+
day=value.day,
|
|
46
|
+
hour=value.hour,
|
|
47
|
+
minute=value.minute,
|
|
48
|
+
second=value.second,
|
|
49
|
+
tzinfo=value.tzinfo
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def normalize_decimals(cls, data):
|
|
54
|
+
"""
|
|
55
|
+
Strip trailing zeros for constitency.
|
|
56
|
+
|
|
57
|
+
In addition, dbdiff serialization forces Decimal normalization, because
|
|
58
|
+
trailing zeros could happen in inconsistent ways.
|
|
59
|
+
"""
|
|
60
|
+
for key, value in data['fields'].items():
|
|
61
|
+
if not isinstance(value, decimal.Decimal):
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
if value % 1 == 0:
|
|
65
|
+
data['fields'][key] = int(value)
|
|
66
|
+
else:
|
|
67
|
+
data['fields'][key] = value.normalize()
|
|
68
|
+
|
|
69
|
+
def get_dump_object(self, obj):
|
|
70
|
+
"""
|
|
71
|
+
Actual method used by Django serializers to dump dicts.
|
|
72
|
+
|
|
73
|
+
By overridding this method, we're able to run our various
|
|
74
|
+
data dump predictability methods.
|
|
75
|
+
"""
|
|
76
|
+
data = super(BaseSerializerMixin, self).get_dump_object(obj)
|
|
77
|
+
self.remove_microseconds(data)
|
|
78
|
+
self.normalize_decimals(data)
|
|
79
|
+
data = self.recursive_dict_sort(data)
|
|
80
|
+
return data
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Django JSON Serializer override."""
|
|
2
|
+
|
|
3
|
+
from django.core.serializers import json as upstream
|
|
4
|
+
|
|
5
|
+
from .base import BaseSerializerMixin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__all__ = ('Serializer', 'Deserializer')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Serializer(BaseSerializerMixin, upstream.Serializer):
|
|
12
|
+
"""Sorted dict JSON serializer."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Deserializer = upstream.Deserializer
|
dbdiff/test.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Convenience test mixin."""
|
|
2
|
+
from django.core.management import call_command
|
|
3
|
+
|
|
4
|
+
from .fixture import Fixture
|
|
5
|
+
from .sequence import sequence_reset
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DbdiffTestMixin(object):
|
|
9
|
+
"""
|
|
10
|
+
Convenience mixin with better sequence resetting than TransactionTestCase.
|
|
11
|
+
|
|
12
|
+
The difference with using TransactionTestCase with reset_sequences=True is
|
|
13
|
+
that this will reset sequences for the given models to their higher value,
|
|
14
|
+
supporting pre-existing models which could have been created by a
|
|
15
|
+
migration.
|
|
16
|
+
|
|
17
|
+
The test case subclass requires some attributes and an implementation of a
|
|
18
|
+
``dbdiff_test()`` method that does the actual import call that this
|
|
19
|
+
should test. Example usage::
|
|
20
|
+
|
|
21
|
+
class FrancedataImportTest(DbdiffTestMixin, test.TestCase):
|
|
22
|
+
dbdiff_models = [YourModel]
|
|
23
|
+
dbdiff_exclude = {'*': ['created']}
|
|
24
|
+
dbdiff_reset_sequences = True
|
|
25
|
+
dbdiff_expected = 'yourapp/tests/yourexpectedfixture.json'
|
|
26
|
+
dbdiff_fixtures = ['your-fixtures.json']
|
|
27
|
+
|
|
28
|
+
def dbdiff_test(self):
|
|
29
|
+
fixture = os.path.join(
|
|
30
|
+
os.path.dirname(__file__),
|
|
31
|
+
'representatives_fixture.json'
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
with open(fixture, 'r') as f:
|
|
35
|
+
do_your_import.main(f)
|
|
36
|
+
|
|
37
|
+
Supports postgresql.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def test_db_import(self):
|
|
41
|
+
"""Actual test method, ran by the test suite."""
|
|
42
|
+
call_command('flush', interactive=False)
|
|
43
|
+
|
|
44
|
+
for fixture in getattr(self, 'dbdiff_fixtures', []):
|
|
45
|
+
call_command('loaddata', fixture)
|
|
46
|
+
|
|
47
|
+
for model in self.dbdiff_models:
|
|
48
|
+
sequence_reset(model)
|
|
49
|
+
|
|
50
|
+
self.dbdiff_test()
|
|
51
|
+
|
|
52
|
+
Fixture(
|
|
53
|
+
self.dbdiff_expected,
|
|
54
|
+
models=self.dbdiff_models,
|
|
55
|
+
ignore_pk=getattr(self, 'dbdiff_ignore_pk', False),
|
|
56
|
+
).assertNoDiff(
|
|
57
|
+
exclude=self.dbdiff_exclude,
|
|
58
|
+
)
|
dbdiff/tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for the dbdiff module."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""App to test DecimalField dump predictibility."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from django.db import models, migrations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Migration(migrations.Migration):
|
|
5
|
+
|
|
6
|
+
dependencies = [
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
operations = [
|
|
10
|
+
migrations.CreateModel(
|
|
11
|
+
name='TestModel',
|
|
12
|
+
fields=[
|
|
13
|
+
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
|
14
|
+
('test_field', models.DecimalField(max_digits=3, decimal_places=3)),
|
|
15
|
+
],
|
|
16
|
+
),
|
|
17
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from django.db import models, migrations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Migration(migrations.Migration):
|
|
5
|
+
|
|
6
|
+
dependencies = [
|
|
7
|
+
('decimal_test', '0001_initial'),
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
operations = [
|
|
11
|
+
migrations.AlterField(
|
|
12
|
+
model_name='testmodel',
|
|
13
|
+
name='test_field',
|
|
14
|
+
field=models.DecimalField(max_digits=5, decimal_places=2),
|
|
15
|
+
),
|
|
16
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Test app for model inheritance."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Test that we don't crash with non sequence pks."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Test project settings."""
|