Skip to content

Commit 5726a2b

Browse files
committed
Backport dbdiff.assertNoDiff
1 parent e45d984 commit 5726a2b

File tree

12 files changed

+164
-25
lines changed

12 files changed

+164
-25
lines changed

CHANGELOG

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
Unreleased
2+
Remove db-diff dependency
3+
14
2023-10-30
25
Add support for Python 3.12
36
Add support for Django 5.0

src/cities_light/apps.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
from django.apps import AppConfig
2+
from django.core.serializers import register_serializer
23

34

45
class CitiesLightConfig(AppConfig):
56
default_auto_field = 'django.db.models.AutoField'
67
name = 'cities_light'
8+
9+
def ready(self):
10+
register_serializer('sorted_json', 'cities_light.serializers.json')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Serializers with predictible (ordered) output."""

src/cities_light/serializers/base.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

src/cities_light/serializers/json.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

src/cities_light/tests/base.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""."""
2+
import json
23
import os
34
from unittest import mock
45

56
from django import test
67
from django.core import management
78
from django.conf import settings
89

10+
from io import StringIO
911

1012
class FixtureDir:
1113
"""Helper class to construct fixture paths."""
@@ -80,3 +82,23 @@ def _patch(setting, *values):
8082
management.call_command('cities_light', progress=True,
8183
force_import_all=True,
8284
**options)
85+
86+
def export_data(self) -> bytes:
87+
out = StringIO()
88+
management.call_command(
89+
"dumpdata",
90+
"cities_light",
91+
format="sorted_json",
92+
natural_foreign=True,
93+
indent=4,
94+
stdout=out
95+
)
96+
return out.getvalue()
97+
98+
def assertNoDiff(self, fixture_path):
99+
"""Assert that dumped data matches fixture."""
100+
101+
with open(fixture_path) as f:
102+
self.assertListEqual(
103+
json.loads(f.read()), json.loads(self.export_data())
104+
)

src/cities_light/tests/fixtures/update/noinsert.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@
2929
"model": "cities_light.region",
3030
"pk": 1
3131
},
32+
{
33+
"fields": {
34+
"alternate_names": "Юргинский район",
35+
"country": [2017370],
36+
"display_name": "Yurginskiy Rayon, Russia",
37+
"geoname_code": "1485714",
38+
"geoname_id": 1485714,
39+
"name": "Yurginskiy Rayon",
40+
"name_ascii": "Yurginskiy Rayon",
41+
"region": [1503900],
42+
"slug": "yurginskiy-rayon"
43+
},
44+
"model": "cities_light.subregion",
45+
"pk": 1
46+
},
3247
{
3348
"fields": {
3449
"alternate_names": "\u041a\u0435\u043c\u0435\u0440\u043e\u0432\u043e",

src/cities_light/tests/test_fixtures.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from django.core.management import call_command
88
from django.core.management.base import CommandError
99

10-
from dbdiff.fixture import Fixture
10+
# from dbdiff.fixture import Fixture
1111
from cities_light.settings import DATA_DIR, FIXTURES_BASE_URL
1212
from cities_light.management.commands.cities_light_fixtures import Command
1313
from cities_light.downloader import Downloader

src/cities_light/tests/test_import.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import glob
22
import os
33

4-
from dbdiff.fixture import Fixture
4+
from django.core import management
5+
from django.core.management.commands import dumpdata
6+
57
from .base import TestImportBase, FixtureDir
68
from ..settings import DATA_DIR
79

@@ -20,7 +22,8 @@ def test_single_city(self):
2022
'angouleme_city',
2123
'angouleme_translations'
2224
)
23-
Fixture(fixture_dir.get_file_path('angouleme.json')).assertNoDiff()
25+
26+
self.assertNoDiff(fixture_dir.get_file_path("angouleme.json"))
2427

2528
def test_single_city_zip(self):
2629
"""Load single city."""
@@ -38,7 +41,7 @@ def test_single_city_zip(self):
3841
'angouleme_translations',
3942
file_type="zip"
4043
)
41-
Fixture(FixtureDir('import').get_file_path('angouleme.json')).assertNoDiff()
44+
self.assertNoDiff(FixtureDir('import').get_file_path("angouleme.json"))
4245

4346
def test_city_wrong_timezone(self):
4447
"""Load single city with wrong timezone."""
@@ -51,7 +54,8 @@ def test_city_wrong_timezone(self):
5154
'angouleme_city_wtz',
5255
'angouleme_translations'
5356
)
54-
Fixture(fixture_dir.get_file_path('angouleme_wtz.json')).assertNoDiff()
57+
58+
self.assertNoDiff(FixtureDir('import').get_file_path("angouleme_wtz.json"))
5559

5660
from ..loading import get_cities_model
5761
city_model = get_cities_model('City')

src/cities_light/tests/test_update.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Tests for update records."""
22
import unittest
33

4-
from dbdiff.fixture import Fixture
54
from .base import TestImportBase, FixtureDir
65

76

@@ -30,9 +29,9 @@ def test_update_fields(self):
3029
'update_translations',
3130
)
3231

33-
Fixture(
32+
self.assertNoDiff(
3433
fixture_dir.get_file_path('update_fields.json')
35-
).assertNoDiff()
34+
)
3635

3736
def test_update_fields_wrong_timezone(self):
3837
"""Test all fields are updated, but timezone field is wrong."""
@@ -56,9 +55,9 @@ def test_update_fields_wrong_timezone(self):
5655
'update_translations',
5756
)
5857

59-
Fixture(
58+
self.assertNoDiff(
6059
fixture_dir.get_file_path('update_fields_wtz.json')
61-
).assertNoDiff()
60+
)
6261

6362
def test_change_country(self):
6463
"""Test change country for region/city."""
@@ -82,9 +81,9 @@ def test_change_country(self):
8281
'update_translations',
8382
)
8483

85-
Fixture(
84+
self.assertNoDiff(
8685
fixture_dir.get_file_path('change_country.json')
87-
).assertNoDiff()
86+
)
8887

8988
def test_change_region_and_country(self):
9089
"""Test change region and country."""
@@ -108,9 +107,9 @@ def test_change_region_and_country(self):
108107
'update_translations',
109108
)
110109

111-
Fixture(
110+
self.assertNoDiff(
112111
fixture_dir.get_file_path('change_region_and_country.json')
113-
).assertNoDiff()
112+
)
114113

115114
def test_keep_slugs(self):
116115
"""Test --keep-slugs option."""
@@ -135,9 +134,9 @@ def test_keep_slugs(self):
135134
keep_slugs=True
136135
)
137136

138-
Fixture(
137+
self.assertNoDiff(
139138
fixture_dir.get_file_path('keep_slugs.json'),
140-
).assertNoDiff()
139+
)
141140

142141
def test_add_records(self):
143142
"""Test that new records are added."""
@@ -161,9 +160,9 @@ def test_add_records(self):
161160
'add_translations'
162161
)
163162

164-
Fixture(
163+
self.assertNoDiff(
165164
fixture_dir.get_file_path('add_records.json')
166-
).assertNoDiff()
165+
)
167166

168167
def test_noinsert(self):
169168
"""Test --noinsert option."""
@@ -188,9 +187,9 @@ def test_noinsert(self):
188187
noinsert=True
189188
)
190189

191-
Fixture(
190+
self.assertNoDiff(
192191
fixture_dir.get_file_path('noinsert.json'),
193-
).assertNoDiff()
192+
)
194193

195194
# TODO: make the test pass
196195
@unittest.skip("Obsolete records are not removed yet.")

0 commit comments

Comments
 (0)