django-dbdiff 0.9.6__tar.gz → 0.9.7__tar.gz

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.
Files changed (47) hide show
  1. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/PKG-INFO +31 -22
  2. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/README.rst +20 -10
  3. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/apps.py +2 -4
  4. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/exceptions.py +1 -2
  5. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/fixture.py +36 -22
  6. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/sequence.py +2 -3
  7. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/serializers/base.py +4 -8
  8. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/serializers/json.py +0 -1
  9. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/test.py +2 -2
  10. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/decimal_test/migrations/0001_initial.py +1 -1
  11. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/decimal_test/migrations/0002_auto_20160102_0914.py +1 -1
  12. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/project/settings.py +1 -2
  13. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/project/settings_postgresql.py +2 -2
  14. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/test_fixture.py +9 -0
  15. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/test_mixin.py +3 -2
  16. django_dbdiff-0.9.7/dbdiff/tests/test_utils.py +53 -0
  17. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/utils.py +79 -18
  18. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/django_dbdiff.egg-info/PKG-INFO +31 -22
  19. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/django_dbdiff.egg-info/SOURCES.txt +3 -2
  20. django_dbdiff-0.9.7/django_dbdiff.egg-info/top_level.txt +3 -0
  21. django_dbdiff-0.9.7/docs/conf.py +22 -0
  22. django_dbdiff-0.9.7/pyproject.toml +62 -0
  23. django-dbdiff-0.9.6/dbdiff/tests/test_utils.py +0 -16
  24. django-dbdiff-0.9.6/django_dbdiff.egg-info/top_level.txt +0 -1
  25. django-dbdiff-0.9.6/setup.py +0 -49
  26. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/__init__.py +0 -0
  27. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/plugin.py +0 -0
  28. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/serializers/__init__.py +0 -0
  29. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/__init__.py +0 -0
  30. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/decimal_test/__init__.py +0 -0
  31. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/decimal_test/migrations/__init__.py +0 -0
  32. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/decimal_test/models.py +0 -0
  33. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/inheritance/__init__.py +0 -0
  34. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/inheritance/models.py +0 -0
  35. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/nonintpk/__init__.py +0 -0
  36. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/nonintpk/models.py +0 -0
  37. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/project/__init__.py +0 -0
  38. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/project/settings_mysql.py +0 -0
  39. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/project/settings_sqlite.py +0 -0
  40. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/project/urls.py +0 -0
  41. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/test_compare.py +0 -0
  42. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/test_decimal.py +1 -1
  43. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/dbdiff/tests/test_plugin.py +2 -2
  44. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/django_dbdiff.egg-info/dependency_links.txt +0 -0
  45. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/django_dbdiff.egg-info/entry_points.txt +0 -0
  46. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/django_dbdiff.egg-info/requires.txt +0 -0
  47. {django-dbdiff-0.9.6 → django_dbdiff-0.9.7}/setup.cfg +0 -0
@@ -1,39 +1,38 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: django-dbdiff
3
- Version: 0.9.6
3
+ Version: 0.9.7
4
4
  Summary: Database data diffing against fixtures for testing
5
- Home-page: https://github.com/yourlabs/django-dbdiff
6
- Author: James Pic
7
- Author-email: jamespic@gmail.com
5
+ Author-email: James Pic <jamespic@gmail.com>
8
6
  License: MIT
9
- Keywords: django test database fixture diff
7
+ Project-URL: Homepage, https://github.com/yourlabs/django-dbdiff
8
+ Keywords: django,test,database,fixture,diff
10
9
  Classifier: Development Status :: 4 - Beta
11
10
  Classifier: Environment :: Web Environment
12
11
  Classifier: Intended Audience :: Developers
13
12
  Classifier: License :: OSI Approved :: MIT License
14
13
  Classifier: Operating System :: OS Independent
15
14
  Classifier: Framework :: Django
16
- Classifier: Framework :: Django :: 3.2
17
- Classifier: Framework :: Django :: 4.0
18
- Classifier: Framework :: Django :: 4.1
19
15
  Classifier: Framework :: Django :: 4.2
20
- Classifier: Framework :: Django :: 5.0
16
+ Classifier: Framework :: Django :: 5.2
17
+ Classifier: Framework :: Django :: 6.0
21
18
  Classifier: Programming Language :: Python
22
19
  Classifier: Programming Language :: Python :: 3
23
- Classifier: Programming Language :: Python :: 3.8
24
- Classifier: Programming Language :: Python :: 3.9
25
20
  Classifier: Programming Language :: Python :: 3.10
26
21
  Classifier: Programming Language :: Python :: 3.11
27
22
  Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
28
25
  Classifier: Topic :: Internet :: WWW/HTTP
29
26
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/x-rst
30
29
  Requires-Dist: ijson
31
30
  Requires-Dist: json_delta
32
31
 
33
- .. image:: https://travis-ci.org/yourlabs/django-dbdiff.svg
34
- :target: https://travis-ci.org/yourlabs/django-dbdiff
35
- .. image:: https://codecov.io/github/yourlabs/django-dbdiff/coverage.svg?branch=master
36
- :target: https://codecov.io/github/yourlabs/django-dbdiff?branch=master
32
+ .. image:: https://github.com/yourlabs/django-dbdiff/actions/workflows/ci.yml/badge.svg
33
+ :target: https://github.com/yourlabs/django-dbdiff/actions/workflows/ci.yml
34
+ .. image:: https://codecov.io/gh/yourlabs/django-dbdiff/branch/master/graph/badge.svg
35
+ :target: https://codecov.io/gh/yourlabs/django-dbdiff
37
36
  .. image:: https://badge.fury.io/py/django-dbdiff.png
38
37
  :target: http://badge.fury.io/py/django-dbdiff
39
38
 
@@ -89,11 +88,11 @@ Example:
89
88
 
90
89
  .. code-block:: python
91
90
 
92
- from django import TransactionTestCase
91
+ from django.test import TransactionTestCase
93
92
  from dbdiff.fixture import Fixture
94
93
 
95
94
 
96
- class YourImportTest(test.TransactionTestCase):
95
+ class YourImportTest(TransactionTestCase):
97
96
  reset_sequences = True
98
97
 
99
98
  def test_your_import(self):
@@ -115,6 +114,16 @@ If you need to ignore fields globally, set the class-level variable exclude as s
115
114
 
116
115
  Fixture.exclude = {'mrsrequest.mrsrequest': ['token']}
117
116
 
117
+ If your import produces records with non-deterministic primary keys (e.g.
118
+ UUIDs or sequence gaps), pass ``ignore_pk=True`` to match records by their
119
+ field content instead of by pk:
120
+
121
+ .. code-block:: python
122
+
123
+ Fixture('yourapp/tests/yourtest.json',
124
+ models=[YourModel],
125
+ ignore_pk=True).assertNoDiff()
126
+
118
127
  Instead of deleting the fixtures manually before running the tests to
119
128
  regenerate them, just run your tests with FIXTURE_REWRITE=1 environment
120
129
  variable. This will overwrite the fixtures and make the tests look like it
@@ -125,10 +134,10 @@ See tests and docstrings for crunchy details.
125
134
  Requirements
126
135
  ============
127
136
 
128
- MySQL, SQLite and PostgreSQL, Python 3.8 to 3.12 are supported along with
129
- Django 3.2 to 5.0 - it's always better to support django's master so that we
130
- can **upgrade easily when it is released**, which is one of the selling points
131
- for having 100% coverage.
137
+ MySQL, SQLite and PostgreSQL, Python 3.10 to 3.14 are supported along with
138
+ Django 4.2, 5.2, and 6.0 - it's always better to support django's master so
139
+ that we can **upgrade easily when it is released**, which is one of the selling
140
+ points for having 100% coverage.
132
141
 
133
142
  Install
134
143
  =======
@@ -1,7 +1,7 @@
1
- .. image:: https://travis-ci.org/yourlabs/django-dbdiff.svg
2
- :target: https://travis-ci.org/yourlabs/django-dbdiff
3
- .. image:: https://codecov.io/github/yourlabs/django-dbdiff/coverage.svg?branch=master
4
- :target: https://codecov.io/github/yourlabs/django-dbdiff?branch=master
1
+ .. image:: https://github.com/yourlabs/django-dbdiff/actions/workflows/ci.yml/badge.svg
2
+ :target: https://github.com/yourlabs/django-dbdiff/actions/workflows/ci.yml
3
+ .. image:: https://codecov.io/gh/yourlabs/django-dbdiff/branch/master/graph/badge.svg
4
+ :target: https://codecov.io/gh/yourlabs/django-dbdiff
5
5
  .. image:: https://badge.fury.io/py/django-dbdiff.png
6
6
  :target: http://badge.fury.io/py/django-dbdiff
7
7
 
@@ -57,11 +57,11 @@ Example:
57
57
 
58
58
  .. code-block:: python
59
59
 
60
- from django import TransactionTestCase
60
+ from django.test import TransactionTestCase
61
61
  from dbdiff.fixture import Fixture
62
62
 
63
63
 
64
- class YourImportTest(test.TransactionTestCase):
64
+ class YourImportTest(TransactionTestCase):
65
65
  reset_sequences = True
66
66
 
67
67
  def test_your_import(self):
@@ -83,6 +83,16 @@ If you need to ignore fields globally, set the class-level variable exclude as s
83
83
 
84
84
  Fixture.exclude = {'mrsrequest.mrsrequest': ['token']}
85
85
 
86
+ If your import produces records with non-deterministic primary keys (e.g.
87
+ UUIDs or sequence gaps), pass ``ignore_pk=True`` to match records by their
88
+ field content instead of by pk:
89
+
90
+ .. code-block:: python
91
+
92
+ Fixture('yourapp/tests/yourtest.json',
93
+ models=[YourModel],
94
+ ignore_pk=True).assertNoDiff()
95
+
86
96
  Instead of deleting the fixtures manually before running the tests to
87
97
  regenerate them, just run your tests with FIXTURE_REWRITE=1 environment
88
98
  variable. This will overwrite the fixtures and make the tests look like it
@@ -93,10 +103,10 @@ See tests and docstrings for crunchy details.
93
103
  Requirements
94
104
  ============
95
105
 
96
- MySQL, SQLite and PostgreSQL, Python 3.8 to 3.12 are supported along with
97
- Django 3.2 to 5.0 - it's always better to support django's master so that we
98
- can **upgrade easily when it is released**, which is one of the selling points
99
- for having 100% coverage.
106
+ MySQL, SQLite and PostgreSQL, Python 3.10 to 3.14 are supported along with
107
+ Django 4.2, 5.2, and 6.0 - it's always better to support django's master so
108
+ that we can **upgrade easily when it is released**, which is one of the selling
109
+ points for having 100% coverage.
100
110
 
101
111
  Install
102
112
  =======
@@ -9,8 +9,7 @@ from .utils import patch_transaction_test_case
9
9
 
10
10
 
11
11
  class DefaultConfig(AppConfig):
12
- """
13
- Register patched serializers and patch TransactionTestCase for sqlite.
12
+ """Register patched serializers and patch TransactionTestCase for sqlite.
14
13
 
15
14
  .. py:attribute:: debug
16
15
 
@@ -23,8 +22,7 @@ class DefaultConfig(AppConfig):
23
22
  default_indent = 4
24
23
 
25
24
  def ready(self):
26
- """
27
- Register dbdiff.serializers.json and set debug.
25
+ """Register dbdiff.serializers.json and set debug.
28
26
 
29
27
  Enables debug if a DBDIFF_DEBUG environment variable is found.
30
28
 
@@ -59,8 +59,7 @@ class DiffFound(DbDiffException):
59
59
 
60
60
 
61
61
  class FixtureCreated(DbDiffException):
62
- """
63
- Raised when a fixture was created.
62
+ """Raised when a fixture was created.
64
63
 
65
64
  This purposely fails a test, to avoid misleading the user into thinking
66
65
  that the test was properly executed against a versioned fixture. Imagine
@@ -5,11 +5,10 @@ import json
5
5
  import os
6
6
  import tempfile
7
7
 
8
+ import ijson
8
9
  from django.apps import apps
9
10
  from django.core.management import call_command
10
11
 
11
- import ijson
12
-
13
12
  from .exceptions import DiffFound, FixtureCreated
14
13
  from .utils import (
15
14
  diff,
@@ -18,13 +17,11 @@ from .utils import (
18
17
  get_tree,
19
18
  )
20
19
 
21
-
22
20
  REWRITE = os.getenv('FIXTURE_REWRITE')
23
21
 
24
22
 
25
23
  class Fixture(object):
26
- """
27
- Is able to print out diffs between database and a fixture.
24
+ """Is able to print out diffs between database and a fixture.
28
25
 
29
26
  .. py:attribute:: path
30
27
 
@@ -46,9 +43,10 @@ class Fixture(object):
46
43
 
47
44
  exclude = dict()
48
45
 
49
- def __init__(self, relative_path, models=None, database=None):
50
- """
51
- Instanciate a FixtureDiff on a database.
46
+ def __init__(
47
+ self, relative_path, models=None, database=None, ignore_pk=False
48
+ ):
49
+ """Instanciate a FixtureDiff on a database.
52
50
 
53
51
  relative_path is used to calculate :py:attr:`path`, with
54
52
  :py:func:`~utils.get_absolute_path`.
@@ -58,14 +56,19 @@ class Fixture(object):
58
56
 
59
57
  database should be the name of the database to use, `default` by
60
58
  default.
59
+
60
+ ignore_pk when True, matches records by field content instead of
61
+ primary key. Records with the same content but different pks
62
+ are considered equal.
61
63
  """
62
64
  self.path = get_absolute_path(relative_path)
63
65
  self.models = models if models else self.parse_models()
64
66
  self.database = database or 'default'
67
+ self.ignore_pk = ignore_pk
65
68
 
66
69
  def parse_models(self):
67
70
  """Return the list of models inside the fixture file."""
68
- with open(self.path, 'r') as f:
71
+ with open(self.path, 'rb') as f:
69
72
  return [apps.get_model(i.lower())
70
73
  for i in ijson.items(f, 'item.model')]
71
74
 
@@ -91,31 +94,39 @@ class Fixture(object):
91
94
 
92
95
  return len(line) - len(line.lstrip(' '))
93
96
 
94
- def diff(self, exclude=None):
95
- """
96
- Diff the fixture against a datadump of fixture models.
97
+ def diff(self, exclude=None, ignore_pk=None):
98
+ """Diff the fixture against a datadump of fixture models.
97
99
 
98
100
  If passed, exclude should be a list of field names to exclude from
99
101
  being diff'ed.
102
+
103
+ ignore_pk when True, matches records by field content instead of
104
+ primary key. Defaults to the instance's ignore_pk attribute.
100
105
  """
101
106
  fh, dump_path = tempfile.mkstemp('_dbdiff')
102
107
 
103
108
  exclude_final = copy.copy(self.exclude)
104
109
  exclude_final.update(exclude or {})
105
110
 
106
- with os.fdopen(fh, 'w') as f:
107
- self.dump(f)
111
+ try:
112
+ with os.fdopen(fh, 'w') as f:
113
+ self.dump(f)
108
114
 
109
- with open(self.path, 'r') as e, open(dump_path, 'r') as r:
110
- expected, result = json.load(e), json.load(r)
115
+ with open(self.path, 'r') as e, open(dump_path, 'r') as r:
116
+ expected, result = json.load(e), json.load(r)
111
117
 
112
- unexpected, missing, different = diff(
113
- get_tree(expected, exclude_final),
114
- get_tree(result, exclude_final),
115
- )
118
+ if ignore_pk is None:
119
+ ignore_pk = self.ignore_pk
116
120
 
117
- if not unexpected and not missing and not diff:
121
+ unexpected, missing, different = diff(
122
+ get_tree(expected, exclude_final),
123
+ get_tree(result, exclude_final),
124
+ ignore_pk=ignore_pk,
125
+ )
126
+ finally:
118
127
  os.unlink(dump_path)
128
+
129
+ if not unexpected and not missing and not different:
119
130
  return None
120
131
 
121
132
  return unexpected, missing, different
@@ -150,8 +161,11 @@ class Fixture(object):
150
161
  if not REWRITE:
151
162
  raise FixtureCreated(self)
152
163
 
153
- unexpected, missing, different = self.diff(exclude=exclude)
164
+ result = self.diff(exclude=exclude)
165
+ if result is None:
166
+ return
154
167
 
168
+ unexpected, missing, different = result
155
169
  if unexpected or missing or different:
156
170
  raise DiffFound(self, unexpected, missing, different)
157
171
 
@@ -14,8 +14,7 @@ def pk_sequence_get(model):
14
14
 
15
15
 
16
16
  def sequence_reset(model):
17
- """
18
- Better sequence reset than TransactionTestCase.
17
+ """Better sequence reset than TransactionTestCase.
19
18
 
20
19
  The difference with using TransactionTestCase with reset_sequences=True is
21
20
  that this will reset sequences for the given models to their higher value,
@@ -49,7 +48,7 @@ def sequence_reset(model):
49
48
  column=pk_field, table=table
50
49
  )
51
50
  )
52
- result = cursor.fetchone()[0] or 0
51
+ result = cursor.fetchone()[0] or 1
53
52
  reset = 'ALTER TABLE {table} AUTO_INCREMENT = %s' % result
54
53
 
55
54
  connection.cursor().execute(
@@ -10,8 +10,7 @@ class BaseSerializerMixin(object):
10
10
 
11
11
  @classmethod
12
12
  def recursive_dict_sort(cls, data):
13
- """
14
- Return a recursive OrderedDict for a dict.
13
+ """Return a recursive OrderedDict for a dict.
15
14
 
16
15
  Django's default model-to-dict logic - implemented in
17
16
  django.core.serializers.python.Serializer.get_dump_object() - returns a
@@ -28,8 +27,7 @@ class BaseSerializerMixin(object):
28
27
 
29
28
  @classmethod
30
29
  def remove_microseconds(cls, data):
31
- """
32
- Strip microseconds from datetimes for mysql.
30
+ """Strip microseconds from datetimes for mysql.
33
31
 
34
32
  MySQL doesn't have microseconds in datetimes, so dbdiff's serializer
35
33
  removes microseconds from datetimes so that fixtures are cross-database
@@ -51,8 +49,7 @@ class BaseSerializerMixin(object):
51
49
 
52
50
  @classmethod
53
51
  def normalize_decimals(cls, data):
54
- """
55
- Strip trailing zeros for constitency.
52
+ """Strip trailing zeros for consistency.
56
53
 
57
54
  In addition, dbdiff serialization forces Decimal normalization, because
58
55
  trailing zeros could happen in inconsistent ways.
@@ -67,8 +64,7 @@ class BaseSerializerMixin(object):
67
64
  data['fields'][key] = value.normalize()
68
65
 
69
66
  def get_dump_object(self, obj):
70
- """
71
- Actual method used by Django serializers to dump dicts.
67
+ """Actual method used by Django serializers to dump dicts.
72
68
 
73
69
  By overridding this method, we're able to run our various
74
70
  data dump predictability methods.
@@ -4,7 +4,6 @@ from django.core.serializers import json as upstream
4
4
 
5
5
  from .base import BaseSerializerMixin
6
6
 
7
-
8
7
  __all__ = ('Serializer', 'Deserializer')
9
8
 
10
9
 
@@ -6,8 +6,7 @@ from .sequence import sequence_reset
6
6
 
7
7
 
8
8
  class DbdiffTestMixin(object):
9
- """
10
- Convenience mixin with better sequence resetting than TransactionTestCase.
9
+ """Mixin with better sequence resetting than TransactionTestCase.
11
10
 
12
11
  The difference with using TransactionTestCase with reset_sequences=True is
13
12
  that this will reset sequences for the given models to their higher value,
@@ -52,6 +51,7 @@ class DbdiffTestMixin(object):
52
51
  Fixture(
53
52
  self.dbdiff_expected,
54
53
  models=self.dbdiff_models,
54
+ ignore_pk=getattr(self, 'dbdiff_ignore_pk', False),
55
55
  ).assertNoDiff(
56
56
  exclude=self.dbdiff_exclude,
57
57
  )
@@ -1,4 +1,4 @@
1
- from django.db import models, migrations
1
+ from django.db import migrations, models
2
2
 
3
3
 
4
4
  class Migration(migrations.Migration):
@@ -1,4 +1,4 @@
1
- from django.db import models, migrations
1
+ from django.db import migrations, models
2
2
 
3
3
 
4
4
  class Migration(migrations.Migration):
@@ -1,5 +1,4 @@
1
- """
2
- Django settings for project project.
1
+ """Django settings for project project.
3
2
 
4
3
  Generated by 'django-admin startproject' using Django 1.8.3.dev20150604012123.
5
4
 
@@ -4,13 +4,13 @@ import os
4
4
 
5
5
  DATABASES = {
6
6
  'default': {
7
- 'ENGINE': 'django.db.backends.postgresql_psycopg2',
7
+ 'ENGINE': 'django.db.backends.postgresql',
8
8
  'HOST': os.environ.get('DB_HOST', ''),
9
9
  'NAME': os.environ.get('DB_NAME', 'dbdiff_test'),
10
10
  'USER': os.environ.get('DB_USER', 'postgres'),
11
11
  'PASSWORD': os.environ.get('DB_PASSWORD', ''),
12
12
  'PORT': os.environ.get('DB_PORT', '5432'),
13
13
  'OPTIONS': {},
14
-
14
+
15
15
  }
16
16
  }
@@ -23,3 +23,12 @@ class FixtureTest(test.TransactionTestCase):
23
23
 
24
24
  def test_models(self):
25
25
  assert self.fixture.models == [Group]
26
+
27
+ def test_diff_exact_match_returns_none(self):
28
+ # Regression: the always-False `not diff` (imported function) bug
29
+ # caused diff() to never return None, leaking temp files and breaking
30
+ # assertNoDiff() with a TypeError on exact matches.
31
+ # Fixture: # [{"model": "auth.group", "pk": 1,
32
+ # "fields": {"name": "initial_name"}}]
33
+ Group.objects.create(id=1, name='initial_name')
34
+ assert self.fixture.diff() is None
@@ -1,14 +1,15 @@
1
- from dbdiff.test import DbdiffTestMixin
2
-
3
1
  from django import test
4
2
  from django.contrib.contenttypes.models import ContentType
5
3
  from django.db import connection
6
4
 
5
+ from dbdiff.test import DbdiffTestMixin
6
+
7
7
 
8
8
  class ContentTypeTestCase(DbdiffTestMixin, test.TestCase):
9
9
  dbdiff_models = [ContentType]
10
10
  dbdiff_exclude = {'*': ['created']}
11
11
  dbdiff_reset_sequences = True
12
+ dbdiff_ignore_pk = True
12
13
  dbdiff_expected = 'dbdiff/tests/test_mixin.json'
13
14
 
14
15
  def test_db_import(self):
@@ -0,0 +1,53 @@
1
+ import os
2
+
3
+ from django.contrib.auth.models import Group
4
+
5
+ from dbdiff.utils import diff, get_absolute_path, get_model_names
6
+
7
+
8
+ def test_diff_ignore_pk():
9
+ """With ignore_pk=True, records are matched by content, not pk."""
10
+ expected = {
11
+ 'auth.group': {
12
+ 1: {'name': 'testgroup', 'permissions': []},
13
+ },
14
+ }
15
+ result = {
16
+ 'auth.group': {
17
+ 99: {'name': 'testgroup', 'permissions': []}, # diff pk
18
+ },
19
+ }
20
+ unexpected, missing, different = diff(expected, result, ignore_pk=True)
21
+ assert not unexpected and not missing and not different
22
+
23
+
24
+ def test_diff_with_pk_by_default():
25
+ """Records with different pk are missing/unexpected without ignore_pk."""
26
+ expected = {
27
+ 'auth.group': {
28
+ 1: {'name': 'testgroup', 'permissions': []},
29
+ },
30
+ }
31
+ result = {
32
+ 'auth.group': {
33
+ 99: {'name': 'testgroup', 'permissions': []},
34
+ },
35
+ }
36
+ unexpected, missing, different = diff(expected, result, ignore_pk=False)
37
+ assert missing == {
38
+ 'auth.group': {1: {'name': 'testgroup', 'permissions': []}}
39
+ }
40
+ assert unexpected == {
41
+ 'auth.group': {99: {'name': 'testgroup', 'permissions': []}}
42
+ }
43
+
44
+
45
+ def test_get_model_names():
46
+ assert get_model_names([Group, 'auth.user']) == ['auth.group', 'auth.user']
47
+
48
+
49
+ def test_get_absolute_path_starting_with_dot():
50
+ assert get_absolute_path('./foo') == os.path.join(
51
+ os.path.abspath(os.path.dirname('__file__')),
52
+ 'foo',
53
+ )
@@ -1,11 +1,11 @@
1
1
  """Utils for dbdiff."""
2
2
 
3
3
  import os
4
+ from importlib.util import find_spec
4
5
 
5
6
  from django.apps import apps
6
7
  from django.db import connections
7
8
 
8
- from importlib.util import find_spec
9
9
 
10
10
  def get_tree(dump, exclude=None):
11
11
  """Return a tree of model -> pk -> fields."""
@@ -43,9 +43,18 @@ def _get_unexpected(expected, result):
43
43
  return unexpected
44
44
 
45
45
 
46
- def diff(expected, result):
47
- """Return unexpected, missing and diff between expected and result."""
48
- missing, diff = {}, {}
46
+ def diff(expected, result, ignore_pk=False): # noqa: C901
47
+ """Return unexpected, missing and diff between expected and result.
48
+
49
+ When ignore_pk is True, records are matched by their field values (content)
50
+ instead of primary key. Records with the same content but different pks
51
+ are considered matching.
52
+ """
53
+ missing, different = {}, {}
54
+
55
+ if ignore_pk:
56
+ unexpected, missing, different = _diff_by_content(expected, result)
57
+ return unexpected, missing, different
49
58
 
50
59
  unexpected = _get_unexpected(expected, result)
51
60
 
@@ -60,8 +69,7 @@ def diff(expected, result):
60
69
  if expected_fields == result_fields:
61
70
  continue
62
71
 
63
- diff.setdefault(model, {})
64
- diff[model].setdefault(pk, {})
72
+ different.setdefault(model, {}).setdefault(pk, {})
65
73
 
66
74
  for expected_field, expected_value in expected_fields.items():
67
75
  result_value = result_fields[expected_field]
@@ -69,11 +77,41 @@ def diff(expected, result):
69
77
  if expected_value == result_value:
70
78
  continue
71
79
 
72
- diff[model][pk][expected_field] = (
80
+ different[model][pk][expected_field] = (
73
81
  expected_value,
74
82
  result_value
75
83
  )
76
- return unexpected, missing, diff
84
+ return unexpected, missing, different
85
+
86
+
87
+ def _diff_by_content(expected, result): # noqa: C901
88
+ """Diff by matching records on field content instead of primary key."""
89
+ unexpected, missing, different = {}, {}, {}
90
+
91
+ for model in set(expected.keys()) | set(result.keys()):
92
+ expected_list = list(expected.get(model, {}).items())
93
+ result_list = list(result.get(model, {}).items())
94
+ matched_result_indices = set()
95
+
96
+ for exp_pk, exp_fields in expected_list:
97
+ found = False
98
+ for i, (res_pk, res_fields) in enumerate(result_list):
99
+ if i in matched_result_indices:
100
+ continue
101
+ if exp_fields == res_fields:
102
+ matched_result_indices.add(i)
103
+ found = True
104
+ break
105
+ if not found:
106
+ missing.setdefault(model, {})
107
+ missing[model][exp_pk] = exp_fields
108
+
109
+ for i, (res_pk, res_fields) in enumerate(result_list):
110
+ if i not in matched_result_indices:
111
+ unexpected.setdefault(model, {})
112
+ unexpected[model][res_pk] = res_fields
113
+
114
+ return unexpected, missing, different
77
115
 
78
116
 
79
117
  def get_absolute_path(path):
@@ -84,7 +122,8 @@ def get_absolute_path(path):
84
122
  if path.startswith('.'):
85
123
  module_path = '.'
86
124
  else:
87
- module_path = find_spec(path.split('/')[0]).submodule_search_locations[0]
125
+ spec = find_spec(path.split('/')[0])
126
+ module_path = spec.submodule_search_locations[0]
88
127
 
89
128
  return os.path.abspath(os.path.join(
90
129
  module_path,
@@ -114,27 +153,49 @@ def get_models_tables(models):
114
153
 
115
154
 
116
155
  def patch_transaction_test_case():
117
- """Monkeypatch TransactionTestCase._reset_sequences to support SQLite."""
156
+ """Monkeypatch TransactionTestCase._reset_sequences to support SQLite.
157
+
158
+ Safe to call once, from AppConfig.ready(). Calling it a second time would
159
+ wrap the already-patched classmethod again; the inner _needs_explicit_cls
160
+ check would then see a classmethod and call _original(db_name), passing
161
+ db_name correctly, so double-patching doesn't break anything — but callers
162
+ should avoid it anyway.
163
+ """
164
+ import inspect
165
+
118
166
  from django.test.testcases import TransactionTestCase
119
- TransactionTestCase.old_reset_sequences = \
120
- TransactionTestCase._reset_sequences
121
167
 
122
- def new_reset_sequences(self, db_name):
123
- self.old_reset_sequences(db_name)
124
- connection = connections[db_name]
168
+ _raw = inspect.getattr_static(TransactionTestCase, '_reset_sequences')
169
+ # Capture the already-resolved callable before we replace it.
170
+ # For classmethod/staticmethod, the descriptor protocol strips the
171
+ # wrapper so TransactionTestCase._reset_sequences(db_name) works.
172
+ # For a plain function (Django 4.x), it needs an explicit first arg.
173
+ _needs_explicit_cls = not isinstance(_raw, (classmethod, staticmethod))
174
+ _original = TransactionTestCase._reset_sequences
125
175
 
176
+ def _sqlite_reset(db_name):
177
+ connection = connections[db_name]
126
178
  if connection.vendor != 'sqlite':
127
179
  return
128
-
129
180
  tables = get_models_tables(apps.get_models())
130
181
  statements = [
131
182
  "UPDATE SQLITE_SEQUENCE SET SEQ=0 WHERE NAME='%s';" % t
132
183
  for t in tables
133
184
  ]
134
-
135
185
  cursor = connection.cursor()
136
-
137
186
  for statement in statements:
138
187
  cursor.execute(statement)
139
188
 
189
+ # Always install as @classmethod so Django 5.x (which calls
190
+ # cls._reset_sequences(db_name)) passes db_name in the right position.
191
+ # Django 4.x calls self._reset_sequences(db_name) on instances, which
192
+ # also works because classmethods accept both call styles.
193
+ @classmethod
194
+ def new_reset_sequences(cls, db_name): # noqa: N805
195
+ if _needs_explicit_cls:
196
+ _original(cls, db_name)
197
+ else:
198
+ _original(db_name)
199
+ _sqlite_reset(db_name)
200
+
140
201
  TransactionTestCase._reset_sequences = new_reset_sequences
@@ -1,39 +1,38 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: django-dbdiff
3
- Version: 0.9.6
3
+ Version: 0.9.7
4
4
  Summary: Database data diffing against fixtures for testing
5
- Home-page: https://github.com/yourlabs/django-dbdiff
6
- Author: James Pic
7
- Author-email: jamespic@gmail.com
5
+ Author-email: James Pic <jamespic@gmail.com>
8
6
  License: MIT
9
- Keywords: django test database fixture diff
7
+ Project-URL: Homepage, https://github.com/yourlabs/django-dbdiff
8
+ Keywords: django,test,database,fixture,diff
10
9
  Classifier: Development Status :: 4 - Beta
11
10
  Classifier: Environment :: Web Environment
12
11
  Classifier: Intended Audience :: Developers
13
12
  Classifier: License :: OSI Approved :: MIT License
14
13
  Classifier: Operating System :: OS Independent
15
14
  Classifier: Framework :: Django
16
- Classifier: Framework :: Django :: 3.2
17
- Classifier: Framework :: Django :: 4.0
18
- Classifier: Framework :: Django :: 4.1
19
15
  Classifier: Framework :: Django :: 4.2
20
- Classifier: Framework :: Django :: 5.0
16
+ Classifier: Framework :: Django :: 5.2
17
+ Classifier: Framework :: Django :: 6.0
21
18
  Classifier: Programming Language :: Python
22
19
  Classifier: Programming Language :: Python :: 3
23
- Classifier: Programming Language :: Python :: 3.8
24
- Classifier: Programming Language :: Python :: 3.9
25
20
  Classifier: Programming Language :: Python :: 3.10
26
21
  Classifier: Programming Language :: Python :: 3.11
27
22
  Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
28
25
  Classifier: Topic :: Internet :: WWW/HTTP
29
26
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/x-rst
30
29
  Requires-Dist: ijson
31
30
  Requires-Dist: json_delta
32
31
 
33
- .. image:: https://travis-ci.org/yourlabs/django-dbdiff.svg
34
- :target: https://travis-ci.org/yourlabs/django-dbdiff
35
- .. image:: https://codecov.io/github/yourlabs/django-dbdiff/coverage.svg?branch=master
36
- :target: https://codecov.io/github/yourlabs/django-dbdiff?branch=master
32
+ .. image:: https://github.com/yourlabs/django-dbdiff/actions/workflows/ci.yml/badge.svg
33
+ :target: https://github.com/yourlabs/django-dbdiff/actions/workflows/ci.yml
34
+ .. image:: https://codecov.io/gh/yourlabs/django-dbdiff/branch/master/graph/badge.svg
35
+ :target: https://codecov.io/gh/yourlabs/django-dbdiff
37
36
  .. image:: https://badge.fury.io/py/django-dbdiff.png
38
37
  :target: http://badge.fury.io/py/django-dbdiff
39
38
 
@@ -89,11 +88,11 @@ Example:
89
88
 
90
89
  .. code-block:: python
91
90
 
92
- from django import TransactionTestCase
91
+ from django.test import TransactionTestCase
93
92
  from dbdiff.fixture import Fixture
94
93
 
95
94
 
96
- class YourImportTest(test.TransactionTestCase):
95
+ class YourImportTest(TransactionTestCase):
97
96
  reset_sequences = True
98
97
 
99
98
  def test_your_import(self):
@@ -115,6 +114,16 @@ If you need to ignore fields globally, set the class-level variable exclude as s
115
114
 
116
115
  Fixture.exclude = {'mrsrequest.mrsrequest': ['token']}
117
116
 
117
+ If your import produces records with non-deterministic primary keys (e.g.
118
+ UUIDs or sequence gaps), pass ``ignore_pk=True`` to match records by their
119
+ field content instead of by pk:
120
+
121
+ .. code-block:: python
122
+
123
+ Fixture('yourapp/tests/yourtest.json',
124
+ models=[YourModel],
125
+ ignore_pk=True).assertNoDiff()
126
+
118
127
  Instead of deleting the fixtures manually before running the tests to
119
128
  regenerate them, just run your tests with FIXTURE_REWRITE=1 environment
120
129
  variable. This will overwrite the fixtures and make the tests look like it
@@ -125,10 +134,10 @@ See tests and docstrings for crunchy details.
125
134
  Requirements
126
135
  ============
127
136
 
128
- MySQL, SQLite and PostgreSQL, Python 3.8 to 3.12 are supported along with
129
- Django 3.2 to 5.0 - it's always better to support django's master so that we
130
- can **upgrade easily when it is released**, which is one of the selling points
131
- for having 100% coverage.
137
+ MySQL, SQLite and PostgreSQL, Python 3.10 to 3.14 are supported along with
138
+ Django 4.2, 5.2, and 6.0 - it's always better to support django's master so
139
+ that we can **upgrade easily when it is released**, which is one of the selling
140
+ points for having 100% coverage.
132
141
 
133
142
  Install
134
143
  =======
@@ -1,5 +1,5 @@
1
1
  README.rst
2
- setup.py
2
+ pyproject.toml
3
3
  dbdiff/__init__.py
4
4
  dbdiff/apps.py
5
5
  dbdiff/exceptions.py
@@ -38,4 +38,5 @@ django_dbdiff.egg-info/SOURCES.txt
38
38
  django_dbdiff.egg-info/dependency_links.txt
39
39
  django_dbdiff.egg-info/entry_points.txt
40
40
  django_dbdiff.egg-info/requires.txt
41
- django_dbdiff.egg-info/top_level.txt
41
+ django_dbdiff.egg-info/top_level.txt
42
+ docs/conf.py
@@ -0,0 +1,3 @@
1
+ dbdiff
2
+ dist
3
+ docs
@@ -0,0 +1,22 @@
1
+ import os
2
+ import sys
3
+ import django
4
+
5
+ sys.path.insert(0, os.path.abspath('..'))
6
+ os.environ.setdefault(
7
+ 'DJANGO_SETTINGS_MODULE',
8
+ 'dbdiff.tests.project.settings_sqlite',
9
+ )
10
+ django.setup()
11
+
12
+ project = 'django-dbdiff'
13
+ copyright = '2026, James Pic'
14
+ author = 'James Pic'
15
+ release = '0.9.7'
16
+
17
+ extensions = [
18
+ 'sphinx.ext.autodoc',
19
+ 'sphinx.ext.viewcode',
20
+ ]
21
+
22
+ html_theme = 'alabaster'
@@ -0,0 +1,62 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "django-dbdiff"
7
+ version = "0.9.7"
8
+ description = "Database data diffing against fixtures for testing"
9
+ readme = "README.rst"
10
+ license = {text = "MIT"}
11
+ authors = [{name = "James Pic", email = "jamespic@gmail.com"}]
12
+ keywords = ["django", "test", "database", "fixture", "diff"]
13
+ requires-python = ">=3.10"
14
+ dependencies = ["ijson", "json_delta"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Web Environment",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Framework :: Django",
22
+ "Framework :: Django :: 4.2",
23
+ "Framework :: Django :: 5.2",
24
+ "Framework :: Django :: 6.0",
25
+ "Programming Language :: Python",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Programming Language :: Python :: 3.14",
32
+ "Topic :: Internet :: WWW/HTTP",
33
+ "Topic :: Software Development :: Libraries :: Python Modules",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/yourlabs/django-dbdiff"
38
+
39
+ [project.entry-points."pytest11"]
40
+ dbdiff = "dbdiff.plugin"
41
+
42
+ [tool.setuptools]
43
+ include-package-data = true
44
+
45
+ [tool.setuptools.packages.find]
46
+ where = ["."]
47
+ exclude = ["dbdiff.tests*", "tests*", "tests.*"]
48
+
49
+ [tool.ruff]
50
+ line-length = 79
51
+
52
+ [tool.ruff.lint]
53
+ select = ["E", "W", "F", "C90", "T10", "I", "D", "N"]
54
+ # D203 (blank-line-before-docstring) conflicts with D211; D213 conflicts with D212
55
+ ignore = ["D203", "D213", "N818"]
56
+
57
+ [tool.ruff.lint.mccabe]
58
+ max-complexity = 7
59
+
60
+ [tool.ruff.lint.per-file-ignores]
61
+ "dbdiff/tests/**/*.py" = ["D100", "D101", "D102", "D103"]
62
+ "dbdiff/tests/**/migrations/**" = ["D", "E501", "I"]
@@ -1,16 +0,0 @@
1
- import os
2
-
3
- from dbdiff.utils import get_absolute_path, get_model_names
4
-
5
- from django.contrib.auth.models import Group
6
-
7
-
8
- def test_get_model_names():
9
- assert get_model_names([Group, 'auth.user']) == ['auth.group', 'auth.user']
10
-
11
-
12
- def test_get_absolute_path_starting_with_dot():
13
- assert get_absolute_path('./foo') == os.path.join(
14
- os.path.abspath(os.path.dirname('__file__')),
15
- 'foo',
16
- )
@@ -1,49 +0,0 @@
1
- from setuptools import setup, find_packages
2
- import os
3
-
4
-
5
- # Utility function to read the README file.
6
- # Used for the long_description. It's nice, because now 1) we have a top level
7
- # README file and 2) it's easier to type in the README file than to put a raw
8
- # string in below ...
9
- def read(fname):
10
- return open(os.path.join(os.path.dirname(__file__), fname)).read()
11
-
12
-
13
- setup(
14
- name='django-dbdiff',
15
- version='0.9.6',
16
- description='Database data diffing against fixtures for testing',
17
- author='James Pic',
18
- author_email='jamespic@gmail.com',
19
- url='https://github.com/yourlabs/django-dbdiff',
20
- packages=find_packages(),
21
- include_package_data=True,
22
- long_description=read('README.rst'),
23
- license='MIT',
24
- keywords='django test database fixture diff',
25
- install_requires=['ijson', 'json_delta'],
26
- entry_points={'pytest11': ['dbdiff = dbdiff.plugin']},
27
- classifiers=[
28
- 'Development Status :: 4 - Beta',
29
- 'Environment :: Web Environment',
30
- 'Intended Audience :: Developers',
31
- 'License :: OSI Approved :: MIT License',
32
- 'Operating System :: OS Independent',
33
- 'Framework :: Django',
34
- 'Framework :: Django :: 3.2',
35
- 'Framework :: Django :: 4.0',
36
- 'Framework :: Django :: 4.1',
37
- 'Framework :: Django :: 4.2',
38
- 'Framework :: Django :: 5.0',
39
- 'Programming Language :: Python',
40
- 'Programming Language :: Python :: 3',
41
- 'Programming Language :: Python :: 3.8',
42
- 'Programming Language :: Python :: 3.9',
43
- 'Programming Language :: Python :: 3.10',
44
- 'Programming Language :: Python :: 3.11',
45
- 'Programming Language :: Python :: 3.12',
46
- 'Topic :: Internet :: WWW/HTTP',
47
- 'Topic :: Software Development :: Libraries :: Python Modules',
48
- ],
49
- )
@@ -2,8 +2,8 @@ import os
2
2
 
3
3
  from django import test
4
4
 
5
- from .decimal_test.models import TestModel as DecimalTestModel
6
5
  from ..fixture import Fixture
6
+ from .decimal_test.models import TestModel as DecimalTestModel
7
7
 
8
8
 
9
9
  class DecimalDiffTest(test.TransactionTestCase):
@@ -1,9 +1,9 @@
1
+ import pytest
2
+
1
3
  from dbdiff.tests.decimal_test.models import TestModel as DecimalModel
2
4
  from dbdiff.tests.inheritance.models import Child, Parent
3
5
  from dbdiff.tests.nonintpk.models import Nonintpk
4
6
 
5
- import pytest
6
-
7
7
 
8
8
  @pytest.mark.dbdiff(models=[DecimalModel])
9
9
  def test_insert_first():
File without changes