Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 35 additions & 18 deletions .github/workflows/django.yml → .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
name: Python package
name: CI

on:
push:
branches:
- master
branches: [master]
pull_request:
branches:
- master
branches: [master]

jobs:
build:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install tox
- run: tox -e qa

docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install tox
- run: tox -e docs

tests:
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- '3.8'
- '3.9'
- '3.10'
- '3.11'
- '3.12'
- '3.13'
- '3.14'
services:
postgres:
image: postgres:14-alpine
Expand All @@ -40,20 +58,19 @@ jobs:
env:
MYSQL_ROOT_PASSWORD: dbdiff
steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions codecov
- name: Test with tox
- name: Install tox
run: pip install tox tox-gh-actions
- name: Run tests
run: tox -v
env:
DB_HOST: 127.0.0.1
DB_PASSWORD: dbdiff
PGPASSWORD: dbdiff
- name: Codecov
run: codecov
- name: Upload coverage
uses: codecov/codecov-action@v6
with:
fail_ci_if_error: false
5 changes: 4 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
Unreleased Refresh supported dependencies and support
0.9.7 Fix always-False diff condition (temp file leak + assertNoDiff TypeError on exact match)
Use ignore_pk in ContentTypeTestCase for Django 6.0 content-type ordering stability
Add Python 3.10-3.14 and Django 4.2/5.2/6.0; drop EOL Python 3.8/3.9 and Django 4.0/4.1
Migrate to pyproject.toml; modernize CI (checkout@v4, setup-python@v5, codecov-action@v6)

0.9.6 Add support for Python 3.12

Expand Down
30 changes: 20 additions & 10 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.. image:: https://travis-ci.org/yourlabs/django-dbdiff.svg
:target: https://travis-ci.org/yourlabs/django-dbdiff
.. image:: https://codecov.io/github/yourlabs/django-dbdiff/coverage.svg?branch=master
:target: https://codecov.io/github/yourlabs/django-dbdiff?branch=master
.. image:: https://github.com/yourlabs/django-dbdiff/actions/workflows/ci.yml/badge.svg
:target: https://github.com/yourlabs/django-dbdiff/actions/workflows/ci.yml
.. image:: https://codecov.io/gh/yourlabs/django-dbdiff/branch/master/graph/badge.svg
:target: https://codecov.io/gh/yourlabs/django-dbdiff
.. image:: https://badge.fury.io/py/django-dbdiff.png
:target: http://badge.fury.io/py/django-dbdiff

Expand Down Expand Up @@ -57,11 +57,11 @@ Example:

.. code-block:: python

from django import TransactionTestCase
from django.test import TransactionTestCase
from dbdiff.fixture import Fixture


class YourImportTest(test.TransactionTestCase):
class YourImportTest(TransactionTestCase):
reset_sequences = True

def test_your_import(self):
Expand All @@ -83,6 +83,16 @@ If you need to ignore fields globally, set the class-level variable exclude as s

Fixture.exclude = {'mrsrequest.mrsrequest': ['token']}

If your import produces records with non-deterministic primary keys (e.g.
UUIDs or sequence gaps), pass ``ignore_pk=True`` to match records by their
field content instead of by pk:

.. code-block:: python

Fixture('yourapp/tests/yourtest.json',
models=[YourModel],
ignore_pk=True).assertNoDiff()

Instead of deleting the fixtures manually before running the tests to
regenerate them, just run your tests with FIXTURE_REWRITE=1 environment
variable. This will overwrite the fixtures and make the tests look like it
Expand All @@ -93,10 +103,10 @@ See tests and docstrings for crunchy details.
Requirements
============

MySQL, SQLite and PostgreSQL, Python 3.8 to 3.12 are supported along with
Django 3.2 to 5.0 - it's always better to support django's master so that we
can **upgrade easily when it is released**, which is one of the selling points
for having 100% coverage.
MySQL, SQLite and PostgreSQL, Python 3.10 to 3.14 are supported along with
Django 4.2, 5.2, and 6.0 - it's always better to support django's master so
that we can **upgrade easily when it is released**, which is one of the selling
points for having 100% coverage.

Install
=======
Expand Down
6 changes: 2 additions & 4 deletions dbdiff/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@


class DefaultConfig(AppConfig):
"""
Register patched serializers and patch TransactionTestCase for sqlite.
"""Register patched serializers and patch TransactionTestCase for sqlite.

.. py:attribute:: debug

Expand All @@ -23,8 +22,7 @@ class DefaultConfig(AppConfig):
default_indent = 4

def ready(self):
"""
Register dbdiff.serializers.json and set debug.
"""Register dbdiff.serializers.json and set debug.

Enables debug if a DBDIFF_DEBUG environment variable is found.

Expand Down
3 changes: 1 addition & 2 deletions dbdiff/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ def __init__(self, fixture, unexpected, missing, diff):


class FixtureCreated(DbDiffException):
"""
Raised when a fixture was created.
"""Raised when a fixture was created.

This purposely fails a test, to avoid misleading the user into thinking
that the test was properly executed against a versioned fixture. Imagine
Expand Down
52 changes: 27 additions & 25 deletions dbdiff/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
import os
import tempfile

import ijson
from django.apps import apps
from django.core.management import call_command

import ijson

from .exceptions import DiffFound, FixtureCreated
from .utils import (
diff,
Expand All @@ -18,13 +17,11 @@
get_tree,
)


REWRITE = os.getenv('FIXTURE_REWRITE')


class Fixture(object):
"""
Is able to print out diffs between database and a fixture.
"""Is able to print out diffs between database and a fixture.

.. py:attribute:: path

Expand All @@ -46,9 +43,10 @@ class Fixture(object):

exclude = dict()

def __init__(self, relative_path, models=None, database=None, ignore_pk=False):
"""
Instanciate a FixtureDiff on a database.
def __init__(
self, relative_path, models=None, database=None, ignore_pk=False
):
"""Instanciate a FixtureDiff on a database.

relative_path is used to calculate :py:attr:`path`, with
:py:func:`~utils.get_absolute_path`.
Expand All @@ -70,7 +68,7 @@ def __init__(self, relative_path, models=None, database=None, ignore_pk=False):

def parse_models(self):
"""Return the list of models inside the fixture file."""
with open(self.path, 'r') as f:
with open(self.path, 'rb') as f:
return [apps.get_model(i.lower())
for i in ijson.items(f, 'item.model')]

Expand All @@ -97,8 +95,7 @@ def indent(self):
return len(line) - len(line.lstrip(' '))

def diff(self, exclude=None, ignore_pk=None):
"""
Diff the fixture against a datadump of fixture models.
"""Diff the fixture against a datadump of fixture models.

If passed, exclude should be a list of field names to exclude from
being diff'ed.
Expand All @@ -111,23 +108,25 @@ def diff(self, exclude=None, ignore_pk=None):
exclude_final = copy.copy(self.exclude)
exclude_final.update(exclude or {})

with os.fdopen(fh, 'w') as f:
self.dump(f)

with open(self.path, 'r') as e, open(dump_path, 'r') as r:
expected, result = json.load(e), json.load(r)
try:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the new try/finally for here exactly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To ensure tmp file (dump_path) is always deleted

with os.fdopen(fh, 'w') as f:
self.dump(f)

if ignore_pk is None:
ignore_pk = self.ignore_pk
with open(self.path, 'r') as e, open(dump_path, 'r') as r:
expected, result = json.load(e), json.load(r)

unexpected, missing, different = diff(
get_tree(expected, exclude_final),
get_tree(result, exclude_final),
ignore_pk=ignore_pk,
)
if ignore_pk is None:
ignore_pk = self.ignore_pk

if not unexpected and not missing and not diff:
unexpected, missing, different = diff(
get_tree(expected, exclude_final),
get_tree(result, exclude_final),
ignore_pk=ignore_pk,
)
finally:
os.unlink(dump_path)

if not unexpected and not missing and not different:
return None

return unexpected, missing, different
Expand Down Expand Up @@ -162,8 +161,11 @@ def assertNoDiff(self, exclude=None): # noqa
if not REWRITE:
raise FixtureCreated(self)

unexpected, missing, different = self.diff(exclude=exclude)
result = self.diff(exclude=exclude)
if result is None:
return

unexpected, missing, different = result
if unexpected or missing or different:
raise DiffFound(self, unexpected, missing, different)

Expand Down
5 changes: 2 additions & 3 deletions dbdiff/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ def pk_sequence_get(model):


def sequence_reset(model):
"""
Better sequence reset than TransactionTestCase.
"""Better sequence reset than TransactionTestCase.

The difference with using TransactionTestCase with reset_sequences=True is
that this will reset sequences for the given models to their higher value,
Expand Down Expand Up @@ -49,7 +48,7 @@ def sequence_reset(model):
column=pk_field, table=table
)
)
result = cursor.fetchone()[0] or 0
result = cursor.fetchone()[0] or 1
reset = 'ALTER TABLE {table} AUTO_INCREMENT = %s' % result

connection.cursor().execute(
Expand Down
12 changes: 4 additions & 8 deletions dbdiff/serializers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ class BaseSerializerMixin(object):

@classmethod
def recursive_dict_sort(cls, data):
"""
Return a recursive OrderedDict for a dict.
"""Return a recursive OrderedDict for a dict.

Django's default model-to-dict logic - implemented in
django.core.serializers.python.Serializer.get_dump_object() - returns a
Expand All @@ -28,8 +27,7 @@ def recursive_dict_sort(cls, data):

@classmethod
def remove_microseconds(cls, data):
"""
Strip microseconds from datetimes for mysql.
"""Strip microseconds from datetimes for mysql.

MySQL doesn't have microseconds in datetimes, so dbdiff's serializer
removes microseconds from datetimes so that fixtures are cross-database
Expand All @@ -51,8 +49,7 @@ def remove_microseconds(cls, data):

@classmethod
def normalize_decimals(cls, data):
"""
Strip trailing zeros for constitency.
"""Strip trailing zeros for consistency.

In addition, dbdiff serialization forces Decimal normalization, because
trailing zeros could happen in inconsistent ways.
Expand All @@ -67,8 +64,7 @@ def normalize_decimals(cls, data):
data['fields'][key] = value.normalize()

def get_dump_object(self, obj):
"""
Actual method used by Django serializers to dump dicts.
"""Actual method used by Django serializers to dump dicts.

By overridding this method, we're able to run our various
data dump predictability methods.
Expand Down
1 change: 0 additions & 1 deletion dbdiff/serializers/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from .base import BaseSerializerMixin


__all__ = ('Serializer', 'Deserializer')


Expand Down
3 changes: 1 addition & 2 deletions dbdiff/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@


class DbdiffTestMixin(object):
"""
Convenience mixin with better sequence resetting than TransactionTestCase.
"""Mixin with better sequence resetting than TransactionTestCase.

The difference with using TransactionTestCase with reset_sequences=True is
that this will reset sequences for the given models to their higher value,
Expand Down
2 changes: 1 addition & 1 deletion dbdiff/tests/decimal_test/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.db import models, migrations
from django.db import migrations, models


class Migration(migrations.Migration):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.db import models, migrations
from django.db import migrations, models


class Migration(migrations.Migration):
Expand Down
3 changes: 1 addition & 2 deletions dbdiff/tests/project/settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""
Django settings for project project.
"""Django settings for project project.
Generated by 'django-admin startproject' using Django 1.8.3.dev20150604012123.
Expand Down
Loading
Loading