diff --git a/.github/workflows/django.yml b/.github/workflows/ci.yml similarity index 51% rename from .github/workflows/django.yml rename to .github/workflows/ci.yml index f23d22b..6129e67 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 diff --git a/CHANGELOG b/CHANGELOG index b668745..1e53313 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 diff --git a/README.rst b/README.rst index dd9c11b..fcd885c 100644 --- a/README.rst +++ b/README.rst @@ -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 @@ -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): @@ -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 @@ -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 ======= diff --git a/dbdiff/apps.py b/dbdiff/apps.py index 8ab9458..376f75a 100644 --- a/dbdiff/apps.py +++ b/dbdiff/apps.py @@ -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 @@ -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. diff --git a/dbdiff/exceptions.py b/dbdiff/exceptions.py index f9ebc96..2276849 100644 --- a/dbdiff/exceptions.py +++ b/dbdiff/exceptions.py @@ -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 diff --git a/dbdiff/fixture.py b/dbdiff/fixture.py index 6340d0f..2bb58f1 100644 --- a/dbdiff/fixture.py +++ b/dbdiff/fixture.py @@ -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, @@ -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 @@ -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`. @@ -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')] @@ -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. @@ -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: + 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 @@ -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) diff --git a/dbdiff/sequence.py b/dbdiff/sequence.py index 7abb425..1a70403 100644 --- a/dbdiff/sequence.py +++ b/dbdiff/sequence.py @@ -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, @@ -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( diff --git a/dbdiff/serializers/base.py b/dbdiff/serializers/base.py index ed4f8ab..7468323 100644 --- a/dbdiff/serializers/base.py +++ b/dbdiff/serializers/base.py @@ -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 @@ -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 @@ -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. @@ -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. diff --git a/dbdiff/serializers/json.py b/dbdiff/serializers/json.py index 69de31c..3939f1e 100644 --- a/dbdiff/serializers/json.py +++ b/dbdiff/serializers/json.py @@ -4,7 +4,6 @@ from .base import BaseSerializerMixin - __all__ = ('Serializer', 'Deserializer') diff --git a/dbdiff/test.py b/dbdiff/test.py index 32ef657..5771bb1 100644 --- a/dbdiff/test.py +++ b/dbdiff/test.py @@ -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, diff --git a/dbdiff/tests/decimal_test/migrations/0001_initial.py b/dbdiff/tests/decimal_test/migrations/0001_initial.py index 0bb0c69..59aae50 100644 --- a/dbdiff/tests/decimal_test/migrations/0001_initial.py +++ b/dbdiff/tests/decimal_test/migrations/0001_initial.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/dbdiff/tests/decimal_test/migrations/0002_auto_20160102_0914.py b/dbdiff/tests/decimal_test/migrations/0002_auto_20160102_0914.py index d2b4449..96be174 100644 --- a/dbdiff/tests/decimal_test/migrations/0002_auto_20160102_0914.py +++ b/dbdiff/tests/decimal_test/migrations/0002_auto_20160102_0914.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/dbdiff/tests/project/settings.py b/dbdiff/tests/project/settings.py index f6af394..1340e79 100644 --- a/dbdiff/tests/project/settings.py +++ b/dbdiff/tests/project/settings.py @@ -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. diff --git a/dbdiff/tests/project/settings_postgresql.py b/dbdiff/tests/project/settings_postgresql.py index af94004..cb12e69 100644 --- a/dbdiff/tests/project/settings_postgresql.py +++ b/dbdiff/tests/project/settings_postgresql.py @@ -4,13 +4,13 @@ DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'ENGINE': 'django.db.backends.postgresql', 'HOST': os.environ.get('DB_HOST', ''), 'NAME': os.environ.get('DB_NAME', 'dbdiff_test'), 'USER': os.environ.get('DB_USER', 'postgres'), 'PASSWORD': os.environ.get('DB_PASSWORD', ''), 'PORT': os.environ.get('DB_PORT', '5432'), 'OPTIONS': {}, - + } } diff --git a/dbdiff/tests/test_decimal.py b/dbdiff/tests/test_decimal.py index 283986a..2b58f95 100644 --- a/dbdiff/tests/test_decimal.py +++ b/dbdiff/tests/test_decimal.py @@ -2,8 +2,8 @@ from django import test -from .decimal_test.models import TestModel as DecimalTestModel from ..fixture import Fixture +from .decimal_test.models import TestModel as DecimalTestModel class DecimalDiffTest(test.TransactionTestCase): diff --git a/dbdiff/tests/test_fixture.py b/dbdiff/tests/test_fixture.py index b75366f..0f80f81 100644 --- a/dbdiff/tests/test_fixture.py +++ b/dbdiff/tests/test_fixture.py @@ -23,3 +23,12 @@ def test_indent(self): def test_models(self): assert self.fixture.models == [Group] + + def test_diff_exact_match_returns_none(self): + # Regression: the always-False `not diff` (imported function) bug + # caused diff() to never return None, leaking temp files and breaking + # assertNoDiff() with a TypeError on exact matches. + # Fixture: # [{"model": "auth.group", "pk": 1, + # "fields": {"name": "initial_name"}}] + Group.objects.create(id=1, name='initial_name') + assert self.fixture.diff() is None diff --git a/dbdiff/tests/test_mixin.py b/dbdiff/tests/test_mixin.py index 5ec3cdd..c191ad1 100644 --- a/dbdiff/tests/test_mixin.py +++ b/dbdiff/tests/test_mixin.py @@ -1,14 +1,15 @@ -from dbdiff.test import DbdiffTestMixin - from django import test from django.contrib.contenttypes.models import ContentType from django.db import connection +from dbdiff.test import DbdiffTestMixin + class ContentTypeTestCase(DbdiffTestMixin, test.TestCase): dbdiff_models = [ContentType] dbdiff_exclude = {'*': ['created']} dbdiff_reset_sequences = True + dbdiff_ignore_pk = True dbdiff_expected = 'dbdiff/tests/test_mixin.json' def test_db_import(self): diff --git a/dbdiff/tests/test_plugin.py b/dbdiff/tests/test_plugin.py index b0bcce0..4b1e694 100644 --- a/dbdiff/tests/test_plugin.py +++ b/dbdiff/tests/test_plugin.py @@ -1,9 +1,9 @@ +import pytest + from dbdiff.tests.decimal_test.models import TestModel as DecimalModel from dbdiff.tests.inheritance.models import Child, Parent from dbdiff.tests.nonintpk.models import Nonintpk -import pytest - @pytest.mark.dbdiff(models=[DecimalModel]) def test_insert_first(): diff --git a/dbdiff/tests/test_utils.py b/dbdiff/tests/test_utils.py index b7ce69d..82ac548 100644 --- a/dbdiff/tests/test_utils.py +++ b/dbdiff/tests/test_utils.py @@ -1,9 +1,9 @@ import os -from dbdiff.utils import diff, get_absolute_path, get_model_names - from django.contrib.auth.models import Group +from dbdiff.utils import diff, get_absolute_path, get_model_names + def test_diff_ignore_pk(): """With ignore_pk=True, records are matched by content, not pk.""" @@ -14,7 +14,7 @@ def test_diff_ignore_pk(): } result = { 'auth.group': { - 99: {'name': 'testgroup', 'permissions': []}, # same content, diff pk + 99: {'name': 'testgroup', 'permissions': []}, # diff pk }, } unexpected, missing, different = diff(expected, result, ignore_pk=True) @@ -22,7 +22,7 @@ def test_diff_ignore_pk(): def test_diff_with_pk_by_default(): - """With ignore_pk=False, same content but different pk yields missing/unexpected.""" + """Records with different pk are missing/unexpected without ignore_pk.""" expected = { 'auth.group': { 1: {'name': 'testgroup', 'permissions': []}, @@ -34,8 +34,12 @@ def test_diff_with_pk_by_default(): }, } unexpected, missing, different = diff(expected, result, ignore_pk=False) - assert missing == {'auth.group': {1: {'name': 'testgroup', 'permissions': []}}} - assert unexpected == {'auth.group': {99: {'name': 'testgroup', 'permissions': []}}} + assert missing == { + 'auth.group': {1: {'name': 'testgroup', 'permissions': []}} + } + assert unexpected == { + 'auth.group': {99: {'name': 'testgroup', 'permissions': []}} + } def test_get_model_names(): diff --git a/dbdiff/utils.py b/dbdiff/utils.py index 5c9936a..63c80ff 100644 --- a/dbdiff/utils.py +++ b/dbdiff/utils.py @@ -1,11 +1,11 @@ """Utils for dbdiff.""" import os +from importlib.util import find_spec from django.apps import apps from django.db import connections -from importlib.util import find_spec def get_tree(dump, exclude=None): """Return a tree of model -> pk -> fields.""" @@ -43,7 +43,7 @@ def _get_unexpected(expected, result): return unexpected -def diff(expected, result, ignore_pk=False): +def diff(expected, result, ignore_pk=False): # noqa: C901 """Return unexpected, missing and diff between expected and result. When ignore_pk is True, records are matched by their field values (content) @@ -69,8 +69,7 @@ def diff(expected, result, ignore_pk=False): if expected_fields == result_fields: continue - different.setdefault(model, {}) - different[model].setdefault(pk, {}) + different.setdefault(model, {}).setdefault(pk, {}) for expected_field, expected_value in expected_fields.items(): result_value = result_fields[expected_field] @@ -85,7 +84,7 @@ def diff(expected, result, ignore_pk=False): return unexpected, missing, different -def _diff_by_content(expected, result): +def _diff_by_content(expected, result): # noqa: C901 """Diff by matching records on field content instead of primary key.""" unexpected, missing, different = {}, {}, {} @@ -123,7 +122,8 @@ def get_absolute_path(path): if path.startswith('.'): module_path = '.' else: - module_path = find_spec(path.split('/')[0]).submodule_search_locations[0] + spec = find_spec(path.split('/')[0]) + module_path = spec.submodule_search_locations[0] return os.path.abspath(os.path.join( module_path, @@ -153,27 +153,49 @@ def get_models_tables(models): def patch_transaction_test_case(): - """Monkeypatch TransactionTestCase._reset_sequences to support SQLite.""" + """Monkeypatch TransactionTestCase._reset_sequences to support SQLite. + + Safe to call once, from AppConfig.ready(). Calling it a second time would + wrap the already-patched classmethod again; the inner _needs_explicit_cls + check would then see a classmethod and call _original(db_name), passing + db_name correctly, so double-patching doesn't break anything — but callers + should avoid it anyway. + """ + import inspect + from django.test.testcases import TransactionTestCase - TransactionTestCase.old_reset_sequences = \ - TransactionTestCase._reset_sequences - def new_reset_sequences(self, db_name): - self.old_reset_sequences(db_name) - connection = connections[db_name] + _raw = inspect.getattr_static(TransactionTestCase, '_reset_sequences') + # Capture the already-resolved callable before we replace it. + # For classmethod/staticmethod, the descriptor protocol strips the + # wrapper so TransactionTestCase._reset_sequences(db_name) works. + # For a plain function (Django 4.x), it needs an explicit first arg. + _needs_explicit_cls = not isinstance(_raw, (classmethod, staticmethod)) + _original = TransactionTestCase._reset_sequences + def _sqlite_reset(db_name): + connection = connections[db_name] if connection.vendor != 'sqlite': return - tables = get_models_tables(apps.get_models()) statements = [ "UPDATE SQLITE_SEQUENCE SET SEQ=0 WHERE NAME='%s';" % t for t in tables ] - cursor = connection.cursor() - for statement in statements: cursor.execute(statement) + # Always install as @classmethod so Django 5.x (which calls + # cls._reset_sequences(db_name)) passes db_name in the right position. + # Django 4.x calls self._reset_sequences(db_name) on instances, which + # also works because classmethods accept both call styles. + @classmethod + def new_reset_sequences(cls, db_name): # noqa: N805 + if _needs_explicit_cls: + _original(cls, db_name) + else: + _original(db_name) + _sqlite_reset(db_name) + TransactionTestCase._reset_sequences = new_reset_sequences diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..f786aaf --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,22 @@ +import os +import sys +import django + +sys.path.insert(0, os.path.abspath('..')) +os.environ.setdefault( + 'DJANGO_SETTINGS_MODULE', + 'dbdiff.tests.project.settings_sqlite', +) +django.setup() + +project = 'django-dbdiff' +copyright = '2026, James Pic' +author = 'James Pic' +release = '0.9.7' + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode', +] + +html_theme = 'alabaster' diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..536f6c1 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +.. include:: ../README.rst + +API Reference +============= + +.. automodule:: dbdiff.fixture + :members: + +.. automodule:: dbdiff.utils + :members: + +.. automodule:: dbdiff.exceptions + :members: + +.. automodule:: dbdiff.test + :members: + +.. automodule:: dbdiff.serializers.json + :members: + +.. automodule:: dbdiff.apps + :members: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9b29562 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,62 @@ +[build-system] +requires = ["setuptools>=61", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "django-dbdiff" +version = "0.9.7" +description = "Database data diffing against fixtures for testing" +readme = "README.rst" +license = {text = "MIT"} +authors = [{name = "James Pic", email = "jamespic@gmail.com"}] +keywords = ["django", "test", "database", "fixture", "diff"] +requires-python = ">=3.10" +dependencies = ["ijson", "json_delta"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Framework :: Django", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.2", + "Framework :: Django :: 6.0", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +[project.urls] +Homepage = "https://github.com/yourlabs/django-dbdiff" + +[project.entry-points."pytest11"] +dbdiff = "dbdiff.plugin" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +where = ["."] +exclude = ["dbdiff.tests*", "tests*", "tests.*"] + +[tool.ruff] +line-length = 79 + +[tool.ruff.lint] +select = ["E", "W", "F", "C90", "T10", "I", "D", "N"] +# D203 (blank-line-before-docstring) conflicts with D211; D213 conflicts with D212 +ignore = ["D203", "D213", "N818"] + +[tool.ruff.lint.mccabe] +max-complexity = 7 + +[tool.ruff.lint.per-file-ignores] +"dbdiff/tests/**/*.py" = ["D100", "D101", "D102", "D103"] +"dbdiff/tests/**/migrations/**" = ["D", "E501", "I"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 1a818b3..0000000 --- a/setup.py +++ /dev/null @@ -1,49 +0,0 @@ -from setuptools import setup, find_packages -import os - - -# Utility function to read the README file. -# Used for the long_description. It's nice, because now 1) we have a top level -# README file and 2) it's easier to type in the README file than to put a raw -# string in below ... -def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() - - -setup( - name='django-dbdiff', - version='0.9.6', - description='Database data diffing against fixtures for testing', - author='James Pic', - author_email='jamespic@gmail.com', - url='https://github.com/yourlabs/django-dbdiff', - packages=find_packages(), - include_package_data=True, - long_description=read('README.rst'), - license='MIT', - keywords='django test database fixture diff', - install_requires=['ijson', 'json_delta'], - entry_points={'pytest11': ['dbdiff = dbdiff.plugin']}, - classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Framework :: Django', - 'Framework :: Django :: 3.2', - 'Framework :: Django :: 4.0', - 'Framework :: Django :: 4.1', - 'Framework :: Django :: 4.2', - 'Framework :: Django :: 5.0', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Software Development :: Libraries :: Python Modules', - ], -) diff --git a/tox.ini b/tox.ini index 3d02a39..33d08c9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,21 +1,20 @@ [tox] envlist = - py{310,311,312}-django50-{sqlite,mysql,postgresql} - py{38,39,310,311}-django42-{sqlite,mysql,postgresql} - py{38,39,310,311}-django41-{sqlite,mysql,postgresql} - py{38,39,310}-django40-{sqlite,mysql,postgresql} - py{38,39,310}-django32-{sqlite,mysql,postgresql} + py{310,311,312,313,314}-django52-{sqlite,mysql,postgresql} + py{312,313,314}-django60-{sqlite,mysql,postgresql} + py{310,311,312}-django42-{sqlite,mysql,postgresql} qa + docs skip_missing_interpreters = True sitepackages = False [gh-actions] python = - 3.8: py38, docs, checkqa, pylint, mypy - 3.9: py39 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 + 3.14: py314 [testenv] usedevelop = true @@ -32,12 +31,12 @@ deps = pytest-cov mock coverage - django50: Django>=5.0rc1,<5.1 + django52: Django>=5.2,<6.0 + django60: Django>=6.0,<6.1 django42: Django>=4.2,<5.0 - django41: Django>=4.1,<4.2 - django40: Django>=4.0,<4.1 - django32: Django>=3.2,<4.0 - postgresql: psycopg2-binary==2.9.9 + postgresql-django42: psycopg2-binary + postgresql-django52: psycopg2-binary + postgresql-django60: psycopg[binary] mysql: mysqlclient setenv = PIP_ALLOW_EXTERNAL=true @@ -51,22 +50,26 @@ setenv = mysql: DB_NAME=dbdiff_test mysql: DB_ENGINE=mysql mysql: DB_USER=root -passenv = +passenv = TEST_* DBDIFF_* DB_* PGPASSWORD [testenv:qa] -basepython = python3.8 +basepython = python3.12 commands = - flake8 --show-source --exclude tests --max-complexity=7 --ignore=D203 dbdiff - flake8 --show-source --exclude migrations --max-complexity=3 --ignore=D100,D101,D102,D103 dbdiff/tests + ruff check dbdiff +deps = + ruff +[testenv:docs] +basepython = python3.12 +usedevelop = true +commands = + sphinx-build -W -b html docs docs/_build/html deps = - flake8 - mccabe - flake8-debugger - flake8-import-order - flake8-docstrings - pep8-naming + sphinx + Django>=5.2,<6.0 +setenv = + DJANGO_SETTINGS_MODULE=dbdiff.tests.project.settings_sqlite