diff --git a/.travis.yml b/.travis.yml index 317d30d..3774093 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ os: linux -dist: xenial +dist: bionic language: python @@ -8,7 +8,7 @@ jobs: # Python version is just for the look on travis. # Test the full python/django matrix with postgresql - python: 3.6 - env: TOXENV=py36-django111-postgresql + env: TOXENV=py36-django111-postgresql COLLECT_COVERAGE=true - python: 3.7 env: TOXENV=py37-django111-postgresql - python: 3.6 @@ -43,25 +43,41 @@ jobs: env: TOXENV=py38-django31-postgresql - python: 3.9 env: TOXENV=py39-django31-postgresql + - python: 3.6 + env: TOXENV=py36-django32-postgresql + - python: 3.7 + env: TOXENV=py37-django32-postgresql + - python: 3.8 + env: TOXENV=py38-django32-postgresql + - python: 3.9 + env: TOXENV=py39-django32-postgresql # Test against sqlite once for each major django version. - python: 3.6 env: TOXENV=py36-django111-sqlite - python: 3.7 env: TOXENV=py37-django22-sqlite - python: 3.8 - env: TOXENV=py38-django31-sqlite + env: TOXENV=py38-django32-sqlite + # Test on ppc64le once for each major django version. + - python: 3.6 + env: TOXENV=py36-django111-sqlite + arch: ppc64le + - python: 3.7 + env: TOXENV=py37-django22-sqlite + arch: ppc64le + - python: 3.8 + env: TOXENV=py38-django32-sqlite + arch: ppc64le # Check flake8 once. - python: 3.6 env: TOXENV=py36-flake8 -services: - - postgresql - addons: postgresql: "9.6" before_script: - - psql -c 'create database dirtyfields_test;' -U postgres + # only create postgres database if needed. + - if [[ $TOXENV =~ "postgresql" ]]; then psql -c 'create database dirtyfields_test;' -U postgres; fi script: - tox @@ -72,7 +88,7 @@ install: after_success: # only upload coverage report to coveralls from one job. - - if test "$TOXENV" = "py36-django111-postgresql"; then coveralls; fi + - if test "$COLLECT_COVERAGE" = "true"; then coveralls; fi deploy: edge: true # opt in to dpl v2 diff --git a/CLASSIFIERS.txt b/CLASSIFIERS.txt index 74f79e3..d362e05 100644 --- a/CLASSIFIERS.txt +++ b/CLASSIFIERS.txt @@ -16,4 +16,5 @@ Framework :: Django :: 2.1 Framework :: Django :: 2.2 Framework :: Django :: 3.0 Framework :: Django :: 3.1 +Framework :: Django :: 3.2 Topic :: Software Development :: Libraries :: Python Modules diff --git a/ChangeLog.rst b/ChangeLog.rst index 8b75213..f164394 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -8,6 +8,17 @@ master No changes yet +.. _v1.6.0: + +1.6.0 (07/04/2021) +------------------ + +*New:* + - Remove pytz as a dependency. + - Confirm support of Django 3.2 + +.. _v1.5.0: + 1.5.0 (15/01/2021) ------------------ diff --git a/README.rst b/README.rst index cd9b79c..81d4069 100644 --- a/README.rst +++ b/README.rst @@ -6,12 +6,16 @@ Django Dirty Fields :alt: Join the chat at https://gitter.im/romgar/django-dirtyfields :target: https://gitter.im/romgar/django-dirtyfields?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge .. image:: https://img.shields.io/pypi/v/django-dirtyfields.svg + :alt: Published PyPI version :target: https://pypi.org/project/django-dirtyfields/ .. image:: https://travis-ci.org/romgar/django-dirtyfields.svg?branch=develop - :target: https://travis-ci.org/romgar/django-dirtyfields?branch=develop -.. image:: https://coveralls.io/repos/romgar/django-dirtyfields/badge.svg?branch=develop - :target: https://coveralls.io/r/romgar/django-dirtyfields?branch=develop + :alt: Travis CI status + :target: https://travis-ci.org/romgar/django-dirtyfields +.. image:: https://coveralls.io/repos/github/romgar/django-dirtyfields/badge.svg?branch=develop + :alt: Coveralls code coverage status + :target: https://coveralls.io/github/romgar/django-dirtyfields?branch=develop .. image:: https://readthedocs.org/projects/django-dirtyfields/badge/?version=develop + :alt: Read the Docs documentation status :target: https://django-dirtyfields.readthedocs.org/en/develop/?badge=develop Tracking dirty fields on a Django model instance. @@ -20,20 +24,13 @@ Dirty means that field in-memory and database values are different. This package is compatible and tested with the following Python & Django versions: - -+---------------+------------------------------------------------------+ -| Django | Python | -+===============+======================================================+ -| 1.11 | 3.6, 3.7 (as of 1.11.17) | -+---------------+------------------------------------------------------+ -| 2.0, 2.1 | 3.6, 3.7 | -+---------------+------------------------------------------------------+ -| 2.2 | 3.6, 3.7, 3.8 (as of 2.2.8), 3.9 (as of 2.2.17) | -+---------------+------------------------------------------------------+ -| 3.0 | 3.6, 3.7, 3.8, 3.9 (as of 3.0.11) | -+---------------+------------------------------------------------------+ -| 3.1 | 3.6, 3.7, 3.8, 3.9 (as of 3.1.3) | -+---------------+------------------------------------------------------+ ++------------------------+------------------------+ +| Django | Python | ++========================+========================+ +| 1.11, 2.0, 2.1 | 3.6, 3.7 | ++------------------------+------------------------+ +| 2.2, 3.0, 3.1, 3.2 | 3.6, 3.7, 3.8 ,3.9 | ++------------------------+------------------------+ diff --git a/pyproject.toml b/pyproject.toml index 6029dd0..2abdcb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,11 @@ +[tool.pytest.ini_options] +django_find_project = false +DJANGO_SETTINGS_MODULE = 'tests.django_settings' + [tool.coverage.run] branch = true source = ['dirtyfields'] + +[tool.coverage.report] +show_missing = true +precision = 2 diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index c748d5d..0000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -django_find_project = false - diff --git a/requirements.txt b/requirements.txt index 7080741..e97c9bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ Django>=1.11 -pytz>=2015.7 diff --git a/setup.py b/setup.py index df9598b..82a9b59 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ def listify(filename): setup( name="django-dirtyfields", - version="1.5.0", + version="1.6.0", url='https://github.com/romgar/django-dirtyfields', project_urls={ "Documentation": "https://django-dirtyfields.readthedocs.org/en/develop/", diff --git a/src/dirtyfields/compare.py b/src/dirtyfields/compare.py index d239611..96dc624 100644 --- a/src/dirtyfields/compare.py +++ b/src/dirtyfields/compare.py @@ -1,8 +1,7 @@ -import datetime -import pytz import warnings +from datetime import datetime, timezone -from django.utils import timezone +from django.utils import timezone as django_timezone def compare_states(new_state, original_state, compare_function, normalise_function): @@ -33,13 +32,13 @@ def raw_compare(new_value, old_value): return new_value == old_value -def timezone_support_compare(new_value, old_value, timezone_to_set=pytz.UTC): +def timezone_support_compare(new_value, old_value, timezone_to_set=timezone.utc): - if not (isinstance(new_value, datetime.datetime) and isinstance(old_value, datetime.datetime)): + if not (isinstance(new_value, datetime) and isinstance(old_value, datetime)): return raw_compare(new_value, old_value) - db_value_is_aware = timezone.is_aware(old_value) - in_memory_value_is_aware = timezone.is_aware(new_value) + db_value_is_aware = django_timezone.is_aware(old_value) + in_memory_value_is_aware = django_timezone.is_aware(new_value) if db_value_is_aware == in_memory_value_is_aware: return raw_compare(new_value, old_value) @@ -49,14 +48,14 @@ def timezone_support_compare(new_value, old_value, timezone_to_set=pytz.UTC): warnings.warn(u"DateTimeField received a naive datetime (%s)" u" while time zone support is active." % new_value, RuntimeWarning) - new_value = timezone.make_aware(new_value, timezone_to_set).astimezone(pytz.utc) + new_value = django_timezone.make_aware(new_value, timezone_to_set).astimezone(timezone.utc) else: # The db is not timezone aware, but the value we are passing for comparison is aware. warnings.warn(u"Time zone support is not active (settings.USE_TZ=False), " u"and you pass a time zone aware value (%s)" u" Converting database value before comparison." % new_value, RuntimeWarning) - old_value = timezone.make_aware(old_value, pytz.utc).astimezone(timezone_to_set) + old_value = django_timezone.make_aware(old_value, timezone.utc).astimezone(timezone_to_set) return raw_compare(new_value, old_value) diff --git a/tests-requirements.txt b/tests-requirements.txt index 86fe95c..2b133ce 100644 --- a/tests-requirements.txt +++ b/tests-requirements.txt @@ -1,5 +1,4 @@ -pytest==6.2.1 -pytz +pytest==6.2.3 # Because we test a wide range of Django versions, leave versions of these unspecified. pytest-django jsonfield diff --git a/tests/django_settings.py b/tests/django_settings.py index 5aece06..9d214f6 100644 --- a/tests/django_settings.py +++ b/tests/django_settings.py @@ -1,5 +1,6 @@ # Minimum settings that are needed to run django test suite import os +import tempfile SECRET_KEY = 'WE DONT CARE ABOUT IT' @@ -26,3 +27,5 @@ } INSTALLED_APPS = ('tests', ) + +MEDIA_ROOT = tempfile.mkdtemp(prefix="django-dirtyfields-test-media-root-") diff --git a/tests/files/bar.txt b/tests/files/bar.txt new file mode 100644 index 0000000..50b30ad --- /dev/null +++ b/tests/files/bar.txt @@ -0,0 +1 @@ +bar-content diff --git a/tests/files/foo.txt b/tests/files/foo.txt new file mode 100644 index 0000000..d1e7a10 --- /dev/null +++ b/tests/files/foo.txt @@ -0,0 +1 @@ +foo-content diff --git a/tests/models.py b/tests/models.py index 554b13d..ae58f4c 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,6 +1,6 @@ from django.db import models from django.db.models.signals import pre_save -from django.utils import timezone +from django.utils import timezone as django_timezone from jsonfield import JSONField as JSONFieldThirdParty from dirtyfields import DirtyFieldsMixin @@ -61,12 +61,15 @@ class ExpressionModelTest(DirtyFieldsMixin, models.Model): class DatetimeModelTest(DirtyFieldsMixin, models.Model): compare_function = (timezone_support_compare, {}) - datetime_field = models.DateTimeField(default=timezone.now) + datetime_field = models.DateTimeField(default=django_timezone.now) class CurrentDatetimeModelTest(DirtyFieldsMixin, models.Model): - compare_function = (timezone_support_compare, {'timezone_to_set': timezone.get_current_timezone()}) - datetime_field = models.DateTimeField(default=timezone.now) + compare_function = ( + timezone_support_compare, + {'timezone_to_set': django_timezone.get_current_timezone()}, + ) + datetime_field = models.DateTimeField(default=django_timezone.now) class Many2ManyModelTest(DirtyFieldsMixin, models.Model): @@ -155,3 +158,7 @@ class ModelWithM2MAndSpecifiedFieldsTest(DirtyFieldsMixin, models.Model): class BinaryModelTest(DirtyFieldsMixin, models.Model): bytea = models.BinaryField() + + +class FileFieldModel(DirtyFieldsMixin, models.Model): + file1 = models.FileField(upload_to="file1/") diff --git a/tests/test_core.py b/tests/test_core.py index 0e319fc..2aed90a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,8 +1,14 @@ from decimal import Decimal +from os.path import dirname, join + import pytest +from django.core.files.base import ContentFile, File -from .models import (ModelTest, ModelWithForeignKeyTest, ModelWithOneToOneFieldTest, - SubclassModelTest, ModelWithDecimalFieldTest) +from .models import (ModelTest, ModelWithForeignKeyTest, + ModelWithOneToOneFieldTest, + SubclassModelTest, ModelWithDecimalFieldTest, + FileFieldModel) +from .utils import FakeFieldFile @pytest.mark.django_db @@ -199,3 +205,47 @@ def test_refresh_from_db_no_fields(): assert tm.boolean is False assert tm.characters == "new value" assert tm.get_dirty_fields() == {"boolean": True, "characters": "old value"} + + +@pytest.mark.django_db +def test_file_fields_content_file(): + tm = FileFieldModel() + # field is dirty because model is unsaved + assert tm.get_dirty_fields() == {"file1": FakeFieldFile("")} + tm.save() + assert tm.get_dirty_fields() == {} + + # set file makes field dirty + tm.file1.save("test-file-1.txt", ContentFile(b"Test file content 1"), save=False) + assert tm.get_dirty_fields() == {"file1": FakeFieldFile("")} + tm.save() + assert tm.get_dirty_fields() == {} + + # change file makes field dirty + tm.file1.save("test-file-2.txt", ContentFile(b"Test file content 2"), save=False) + assert tm.get_dirty_fields() == {"file1": FakeFieldFile("file1/test-file-1.txt")} + tm.save() + assert tm.get_dirty_fields() == {} + + +@pytest.mark.django_db +def test_file_fields_real_file(): + tm = FileFieldModel() + # field is dirty because model is unsaved + assert tm.get_dirty_fields() == {"file1": FakeFieldFile("")} + tm.save() + assert tm.get_dirty_fields() == {} + + # set file makes field dirty + with open(join(dirname(__file__), "files", "foo.txt"), "rb") as f: + tm.file1.save("test-file-3.txt", File(f), save=False) + assert tm.get_dirty_fields() == {"file1": FakeFieldFile("")} + tm.save() + assert tm.get_dirty_fields() == {} + + # change file makes field dirty + with open(join(dirname(__file__), "files", "bar.txt"), "rb") as f: + tm.file1.save("test-file-4.txt", File(f), save=False) + assert tm.get_dirty_fields() == {"file1": FakeFieldFile("file1/test-file-3.txt")} + tm.save() + assert tm.get_dirty_fields() == {} diff --git a/tests/test_timezone_aware_fields.py b/tests/test_timezone_aware_fields.py index 522b737..253cf96 100644 --- a/tests/test_timezone_aware_fields.py +++ b/tests/test_timezone_aware_fields.py @@ -1,17 +1,16 @@ -import pytest -import pytz -from datetime import datetime +from datetime import datetime, timedelta, timezone -from django.utils import timezone +import pytest from django.test.utils import override_settings +from django.utils import timezone as django_timezone from .models import DatetimeModelTest, CurrentDatetimeModelTest -@override_settings(USE_TZ=True, TIME_ZONE='America/Chicago') +@override_settings(USE_TZ=True) @pytest.mark.django_db def test_datetime_fields_when_aware_db_and_naive_current_value(): - tm = DatetimeModelTest.objects.create(datetime_field=datetime(2000, 1, 1, tzinfo=pytz.utc)) + tm = DatetimeModelTest.objects.create(datetime_field=datetime(2000, 1, 1, tzinfo=timezone.utc)) # Adding a naive datetime tm.datetime_field = datetime(2016, 1, 1) @@ -23,7 +22,7 @@ def test_datetime_fields_when_aware_db_and_naive_current_value(): r"while time zone support is active\." ), ): - assert tm.get_dirty_fields() == {'datetime_field': datetime(2000, 1, 1, tzinfo=pytz.utc)} + assert tm.get_dirty_fields() == {'datetime_field': datetime(2000, 1, 1, tzinfo=timezone.utc)} @override_settings(USE_TZ=False) @@ -32,7 +31,7 @@ def test_datetime_fields_when_naive_db_and_aware_current_value(): tm = DatetimeModelTest.objects.create(datetime_field=datetime(2000, 1, 1)) # Adding an aware datetime - tm.datetime_field = datetime(2016, 1, 1, tzinfo=pytz.utc) + tm.datetime_field = datetime(2016, 1, 1, tzinfo=timezone.utc) with pytest.warns( RuntimeWarning, @@ -46,12 +45,13 @@ def test_datetime_fields_when_naive_db_and_aware_current_value(): assert tm.get_dirty_fields() == {'datetime_field': datetime(2000, 1, 1)} +@override_settings(USE_TZ=True) @pytest.mark.django_db def test_datetime_fields_when_aware_db_and_aware_current_value(): - aware_dt = timezone.now() + aware_dt = django_timezone.now() tm = DatetimeModelTest.objects.create(datetime_field=aware_dt) - tm.datetime_field = timezone.now() + tm.datetime_field = django_timezone.now() assert tm.get_dirty_fields() == {'datetime_field': aware_dt} @@ -69,7 +69,7 @@ def test_datetime_fields_when_naive_db_and_naive_current_value(): @override_settings(USE_TZ=True, TIME_ZONE='America/Chicago') @pytest.mark.django_db def test_datetime_fields_with_current_timezone_conversion(): - tm = CurrentDatetimeModelTest.objects.create(datetime_field=datetime(2000, 1, 1, 12, 0, 0, tzinfo=pytz.utc)) + tm = CurrentDatetimeModelTest.objects.create(datetime_field=datetime(2000, 1, 1, 12, 0, 0, tzinfo=timezone.utc)) # Adding a naive datetime, that will be converted to local timezone. tm.datetime_field = datetime(2000, 1, 1, 6, 0, 0) @@ -91,9 +91,9 @@ def test_datetime_fields_with_current_timezone_conversion(): def test_datetime_fields_with_current_timezone_conversion_without_timezone_support(): tm = CurrentDatetimeModelTest.objects.create(datetime_field=datetime(2000, 1, 1, 12, 0, 0)) - # Adding an aware datetime - chicago_timezone = pytz.timezone('America/Chicago') - tm.datetime_field = chicago_timezone.localize(datetime(2000, 1, 1, 6, 0, 0), is_dst=None) + # Adding an aware datetime, Chicago is UTC-6h + chicago_timezone = timezone(timedelta(hours=-6)) + tm.datetime_field = datetime(2000, 1, 1, 6, 0, 0, tzinfo=chicago_timezone) # If the database is naive, then we consider that it is defined as in UTC. with pytest.warns( diff --git a/tests/utils.py b/tests/utils.py index b686fe0..1e05837 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,4 +1,5 @@ import re +from collections import namedtuple from django.conf import settings from django.db import connection @@ -62,3 +63,7 @@ def is_postgresql_env_with_jsonb_field(): PG_VERSION = 0 return PG_VERSION >= 90400 + + +# Will compare equal with a django `FieldFile` instance in tests. +FakeFieldFile = namedtuple("FakeFieldFile", ["name"]) diff --git a/tox.ini b/tox.ini index 82316c9..8db280a 100644 --- a/tox.ini +++ b/tox.ini @@ -3,9 +3,8 @@ skipsdist = True ; Define all possible test environments, travis runs just a subset of these. envlist = py{36,37}-django{111,20,21}-{postgresql,sqlite} - py{36,37,38,39}-django{22}-{postgresql,sqlite} - py{36,37,38,39}-django{30,31}-{postgresql,sqlite} - py{36,37,38}-flake8 + py{36,37,38,39}-django{22,30,31,32}-{postgresql,sqlite} + py{36,37,38,39}-flake8 [testenv] usedevelop = True @@ -21,18 +20,19 @@ deps = django22: Django>=2.2,<2.3 django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 + django32: Django>=3.2,<3.3 postgresql: psycopg2 -rtests-requirements.txt commands = python --version pip freeze -l - coverage run -m py.test --ds=tests.django_settings -v - coverage report -m + coverage run -m pytest -v + coverage report [flake8] ignore = E501 -[testenv:py{36,37,38}-flake8] +[testenv:py{36,37,38,39}-flake8] deps = flake8 commands = python --version