[Python-modules-commits] [django-dirtyfields] 02/06: Import django-dirtyfields_1.3.orig.tar.gz

Christopher Stuart Hoskin mans0954 at moszumanska.debian.org
Mon Oct 30 23:25:10 UTC 2017


This is an automated email from the git hooks/post-receive script.

mans0954 pushed a commit to branch master
in repository django-dirtyfields.

commit c1d695ba5f9b425468b1ecb942feac275e50f690
Author: Christopher Hoskin <mans0954 at debian.org>
Date:   Mon Oct 30 23:17:16 2017 +0000

    Import django-dirtyfields_1.3.orig.tar.gz
---
 .travis.yml                    | 11 +--------
 ChangeLog.rst                  | 33 +++++++++++++++++++++++++--
 README.rst                     |  2 +-
 docs/index.rst                 | 24 ++++++++++++++++++++
 requirements.txt               |  2 +-
 setup.py                       |  2 +-
 src/dirtyfields/compat.py      | 45 -------------------------------------
 src/dirtyfields/dirtyfields.py | 51 ++++++++++++++++++++++++++++--------------
 tests/models.py                | 14 ++++++++++++
 tests/test_core.py             | 10 +++++++++
 tests/test_memory_leak.py      | 16 +++++++++++++
 tests/test_save_fields.py      | 40 +++++++++++++++++++++++++++++++--
 tests/test_specified_fields.py | 28 +++++++++++++++++++++++
 tox.ini                        | 33 ++++++++-------------------
 14 files changed, 208 insertions(+), 103 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index bc8e8ca..70c732d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,24 +2,15 @@ language: python
 
 python:
   - 2.7
-  - 3.4
+  - 3.5
 
 env:
-  - TOXENV=django14
-  - TOXENV=django15
-  - TOXENV=django16
-  - TOXENV=django17
   - TOXENV=django18
   - TOXENV=django19
   - TOXENV=django110
   - TOXENV=coverage
   - TOXENV=postgresql
 
-matrix:
-  exclude:
-    - python: 3.4
-      env: TOXENV=django14
-
 services:
   - postgresql
 
diff --git a/ChangeLog.rst b/ChangeLog.rst
index 00f2afb..3b02ffc 100644
--- a/ChangeLog.rst
+++ b/ChangeLog.rst
@@ -1,6 +1,32 @@
 ChangeLog
 =========
 
+.. _master:
+
+master (unreleased)
+-------------------
+
+Up-to-date with 1.3
+
+
+.. _v1.3:
+
+1.3 (23/08/2017)
+----------------
+
+*New:*
+
+    - Drop support for unsupported Django versions: 1.4, 1.5, 1.6 and 1.7 series.
+    - Fixes issue with verbose mode when the object has not been yet saved in the database (MR #99). Thanks vapkarian.
+    - Add test coverage for Django 1.11.
+    - A new attribute :code:`FIELDS_TO_CHECK` has been added to :code:`DirtyFieldsMixin` to specify a limited set of fields to check.
+
+*Bugfix:*
+
+    - Correctly handle :code:`ForeignKey.db_column` :code:`{}_id` in :code:`update_fields`. Thanks Hugo Smett.
+    - Fixes #111: Eliminate a memory leak.
+    - Handle deferred fields in :code:`update_fields`
+
 
 .. _v1.2.1:
 
@@ -10,6 +36,9 @@ ChangeLog
 *New:*
 
     - :code:`django-dirtyfields` is now tested with PostgreSQL, especially with specific fields
+
+*Bugfix:*
+
     - Fixes #80: Use of :code:`Field.rel` raises warnings from Django 1.9+
     - Fixes #84: Use :code:`only()` in conjunction with 2 foreign keys triggers a recursion error
     - Fixes #77: Shallow copy does not work with Django 1.9's JSONField
@@ -46,7 +75,7 @@ ChangeLog
 1.0.1 (2016-07-25)
 ------------------
 
-*bugfix:*
+*Bugfix:*
 
     - Fixing a bug preventing :code:`django-dirtyfields` to work properly on models with custom primary keys.
 
@@ -97,7 +126,7 @@ There is a backward-incompatibility on this version. Please read careful below.
 0.8.1 (2015-12-08)
 ------------------
 
-*bugfix:*
+*Bugfix:*
 
     - Not comparing fields that are deferred (:code:`only` method on :code:`QuerySet`).
     - Being more tolerant when comparing values that can be on another type than expected.
diff --git a/README.rst b/README.rst
index 35b656b..21226a5 100644
--- a/README.rst
+++ b/README.rst
@@ -15,7 +15,7 @@ Django Dirty Fields
 Tracking dirty fields on a Django model instance.
 Dirty means that field in-memory and database values are different.
 
-This package is compatible and tested with Django 1.4 to 1.10.
+This package is compatible and tested with latest versions of Django (1.8, 1.9, 1.10, 1.11 series).
 
 `Full documentation <http://django-dirtyfields.readthedocs.org/en/develop/>`_
 
diff --git a/docs/index.rst b/docs/index.rst
index 6697fe0..5982673 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -118,6 +118,30 @@ in the database needs to be updated with these form values.
 It can have serious performance issues depending on your project.
 
 
+Checking a limited set of model fields.
+-------------------------------------
+If you want to check a limited set of model fields, you should set ``FIELDS_TO_CHECK`` in your model inheriting from ``DirtyFieldsMixin``:
+
+::
+
+    class TestModelWithSpecifiedFields(DirtyFieldsMixin, models.Model):
+        boolean1 = models.BooleanField(default=True)
+        boolean2 = models.BooleanField(default=True)
+        FIELDS_TO_CHECK = ['boolean1']
+
+    >>> from tests.models import TestModelWithSpecifiedFields
+    >>> tm = TestModelWithSpecifiedFields.objects.create()
+
+    >>> tm.boolean1 = False
+    >>> tm.boolean2 = False
+
+    >>> tm.get_dirty_fields()
+    {'boolean1': True}
+
+
+This can be used in order to increase performance.
+
+
 Saving dirty fields.
 ----------------------------
 If you want to only save dirty fields from an instance in the database (only these fields will be involved in SQL query), you can use ``save_dirty_fields`` method.
diff --git a/requirements.txt b/requirements.txt
index f11637f..a42745b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
-Django>=1.4
+Django>=1.8
 pytz>=2015.7
\ No newline at end of file
diff --git a/setup.py b/setup.py
index c55eabe..0f36875 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,7 @@ def listify(filename):
 
 setup(
     name="django-dirtyfields",
-    version="1.2.1",
+    version="1.3",
     url='http://github.com/romgar/django-dirtyfields',
     license='BSD',
     description=("Tracking dirty fields on a Django model instance "
diff --git a/src/dirtyfields/compat.py b/src/dirtyfields/compat.py
index c4e6da9..4c951e5 100644
--- a/src/dirtyfields/compat.py
+++ b/src/dirtyfields/compat.py
@@ -1,7 +1,5 @@
 import sys
 import django
-from django.db.models import signals
-from django.db.models.query_utils import DeferredAttribute
 
 
 def get_m2m_with_model(given_model):
@@ -15,49 +13,6 @@ def get_m2m_with_model(given_model):
         ]
 
 
-def is_db_expression(value):
-    try:
-        # django < 1.8
-        from django.db.models.expressions import ExpressionNode
-        return isinstance(value, ExpressionNode)
-    except ImportError:
-        # django >= 1.8  (big refactoring in Lookup/Expressions/Transforms)
-        from django.db.models.expressions import BaseExpression, Combinable
-        return isinstance(value, (BaseExpression, Combinable))
-
-
-def is_deferred(instance, field):
-    if django.VERSION < (1, 8):
-        attr = instance.__class__.__dict__.get(field.attname)
-        return isinstance(attr, DeferredAttribute)
-    else:
-        return field.get_attname() in instance.get_deferred_fields()
-
-
-def save_specific_fields(instance, fields_list):
-
-    update_fields = fields_list.keys()
-    if django.VERSION >= (1, 5):
-        instance.save(update_fields=update_fields)
-    else:
-        # dirtyfields is by default returning dirty fields with their old value
-        # We should pass the new value(s) to update the database
-        new_fields_list = {field_name: getattr(instance, field_name)
-                           for field_name, field_value in fields_list.items()}
-
-        # dirtyfield is based on post_save signal to save last database value in memory.
-        # As we need to manually launch post_save signal, we also launch pre_save
-        # to be coherent with django 'classic' save signals.
-        signals.pre_save.send(sender=instance.__class__, instance=instance, update_fields=update_fields)
-
-        # django < 1.5 does not support update_fields option on save method
-        instance.__class__.objects.filter(pk=instance.pk).update(**new_fields_list)
-
-        # dirtyfield is based on post_save signal to save last database value in memory.
-        # As update() method does not trigger this signal, we launch it explicitly.
-        signals.post_save.send(sender=instance.__class__, instance=instance, update_fields=update_fields)
-
-
 def is_buffer(value):
     if sys.version_info < (3, 0, 0):
         return isinstance(value, buffer)
diff --git a/src/dirtyfields/dirtyfields.py b/src/dirtyfields/dirtyfields.py
index 985f2d5..4f7b025 100644
--- a/src/dirtyfields/dirtyfields.py
+++ b/src/dirtyfields/dirtyfields.py
@@ -2,11 +2,12 @@
 from copy import deepcopy
 
 from django.core.exceptions import ValidationError
+from django.db.models.expressions import BaseExpression
+from django.db.models.expressions import Combinable
 from django.db.models.signals import post_save, m2m_changed
 
 from .compare import raw_compare, compare_states
-from .compat import (is_db_expression, save_specific_fields,
-                     is_deferred, is_buffer, get_m2m_with_model, remote_field)
+from .compat import is_buffer, get_m2m_with_model, remote_field
 
 
 class DirtyFieldsMixin(object):
@@ -16,10 +17,12 @@ class DirtyFieldsMixin(object):
     # https://github.com/romgar/django-dirtyfields/issues/73
     ENABLE_M2M_CHECK = False
 
+    FIELDS_TO_CHECK = None
+
     def __init__(self, *args, **kwargs):
         super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
         post_save.connect(
-            reset_state, sender=self.__class__,
+            reset_state, sender=self.__class__, weak=False,
             dispatch_uid='{name}-DirtyFieldsMixin-sweeper'.format(
                 name=self.__class__.__name__))
         if self.ENABLE_M2M_CHECK:
@@ -29,7 +32,7 @@ class DirtyFieldsMixin(object):
     def _connect_m2m_relations(self):
         for m2m_field, model in get_m2m_with_model(self.__class__):
             m2m_changed.connect(
-                reset_state, sender=remote_field(m2m_field).through,
+                reset_state, sender=remote_field(m2m_field).through, weak=False,
                 dispatch_uid='{name}-DirtyFieldsMixin-sweeper-m2m'.format(
                     name=self.__class__.__name__))
 
@@ -37,6 +40,9 @@ class DirtyFieldsMixin(object):
         all_field = {}
 
         for field in self._meta.fields:
+            if self.FIELDS_TO_CHECK and (field.get_attname() not in self.FIELDS_TO_CHECK):
+                continue
+
             if field.primary_key and not include_primary_key:
                 continue
 
@@ -44,13 +50,13 @@ class DirtyFieldsMixin(object):
                 if not check_relationship:
                     continue
 
-            if is_deferred(self, field):
+            if field.get_attname() in self.get_deferred_fields():
                 continue
 
             field_value = getattr(self, field.attname)
 
             # If current field value is an expression, we are not evaluating it
-            if is_db_expression(field_value):
+            if isinstance(field_value, (BaseExpression, Combinable)):
                 continue
 
             try:
@@ -71,15 +77,17 @@ class DirtyFieldsMixin(object):
         return all_field
 
     def _as_dict_m2m(self):
+        m2m_fields = {}
+
         if self.pk:
-            m2m_fields = dict([
-                (f.attname, set([
-                    obj.pk for obj in getattr(self, f.attname).all()
-                ]))
-                for f, model in get_m2m_with_model(self.__class__)
-            ])
-            return m2m_fields
-        return {}
+            for f, model in get_m2m_with_model(self.__class__):
+                if self.FIELDS_TO_CHECK and (f.attname not in self.FIELDS_TO_CHECK):
+                    continue
+
+                m2m_fields[f.attname] = set([obj.pk for obj in getattr(self, f.attname).all()])
+
+        return m2m_fields
+
 
     def get_dirty_fields(self, check_relationship=False, check_m2m=None, verbose=False):
         if self._state.adding:
@@ -87,6 +95,9 @@ class DirtyFieldsMixin(object):
             # for consistency (see https://github.com/romgar/django-dirtyfields/issues/65 for more details)
             pk_specified = self.pk is not None
             initial_dict = self._as_dict(check_relationship, include_primary_key=pk_specified)
+            if verbose:
+                initial_dict = {key: {'saved': None, 'current': value}
+                                for key, value in initial_dict.items()}
             return initial_dict
 
         if check_m2m is not None and not self.ENABLE_M2M_CHECK:
@@ -114,7 +125,7 @@ class DirtyFieldsMixin(object):
 
     def save_dirty_fields(self):
         dirty_fields = self.get_dirty_fields(check_relationship=True)
-        save_specific_fields(self, dirty_fields)
+        self.save(update_fields=dirty_fields.keys())
 
 
 def reset_state(sender, instance, **kwargs):
@@ -123,8 +134,14 @@ def reset_state(sender, instance, **kwargs):
     update_fields = kwargs.pop('update_fields', {})
     new_state = instance._as_dict(check_relationship=True)
     if update_fields:
-        for field in update_fields:
-            instance._original_state[field] = new_state[field]
+        for field_name in update_fields:
+            field = sender._meta.get_field(field_name)
+
+            if field.get_attname() in instance.get_deferred_fields():
+                continue
+
+            instance._original_state[field.name] = new_state[field.name]
+
     else:
         instance._original_state = new_state
     if instance.ENABLE_M2M_CHECK:
diff --git a/tests/models.py b/tests/models.py
index ba6a7d2..70a8001 100644
--- a/tests/models.py
+++ b/tests/models.py
@@ -117,3 +117,17 @@ if is_postgresql_env_with_json_field():
 
     class TestModelWithJSONField(DirtyFieldsMixin, models.Model):
         json_field = JSONField()
+
+
+class TestModelWithSpecifiedFields(DirtyFieldsMixin, models.Model):
+    boolean1 = models.BooleanField(default=True)
+    boolean2 = models.BooleanField(default=True)
+    FIELDS_TO_CHECK = ['boolean1']
+
+
+class TestModelWithM2MAndSpecifiedFields(DirtyFieldsMixin, models.Model):
+    m2m1 = models.ManyToManyField(TestModel)
+    m2m2 = models.ManyToManyField(TestModel)
+    ENABLE_M2M_CHECK = True
+    FIELDS_TO_CHECK = ['m2m1']
+
diff --git a/tests/test_core.py b/tests/test_core.py
index 7a1bb3b..30ba2a8 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -153,3 +153,13 @@ def test_verbose_mode():
     assert tm.get_dirty_fields(verbose=True) == {
         'boolean': {'saved': True, 'current': False}
     }
+
+
+ at pytest.mark.django_db
+def test_verbose_mode_on_adding():
+    tm = TestModel()
+
+    assert tm.get_dirty_fields(verbose=True) == {
+        'boolean': {'saved': None, 'current': True},
+        'characters': {'saved': None, 'current': u''}
+    }
diff --git a/tests/test_memory_leak.py b/tests/test_memory_leak.py
new file mode 100644
index 0000000..56d32a7
--- /dev/null
+++ b/tests/test_memory_leak.py
@@ -0,0 +1,16 @@
+import resource
+
+import pytest
+
+from .models import TestModel as DirtyMixinModel
+
+pytestmark = pytest.mark.django_db
+
+
+def test_rss_usage():
+    DirtyMixinModel()
+    rss_1 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
+    for _ in range(1000):
+        DirtyMixinModel()
+    rss_2 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
+    assert rss_2 == rss_1, 'There is a memory leak!'
diff --git a/tests/test_save_fields.py b/tests/test_save_fields.py
index 7eaf372..fdde255 100644
--- a/tests/test_save_fields.py
+++ b/tests/test_save_fields.py
@@ -3,7 +3,7 @@ import unittest
 import django
 import pytest
 
-from .models import TestModel, TestMixedFieldsModel
+from .models import TestModel, TestMixedFieldsModel, TestModelWithForeignKey
 from .utils import assert_number_of_queries_on_regex
 
 
@@ -55,7 +55,6 @@ def test_save_dirty_related_field():
     assert TestMixedFieldsModel.objects.get(pk=tmfm.pk).fkey_id == tm1.id
 
 
- at unittest.skipIf(django.VERSION < (1, 5), "Django 1.4 doesn't support update_fields param on save()")
 @pytest.mark.django_db
 def test_save_only_specific_fields_should_let_other_fields_dirty():
     tm = TestModel.objects.create(boolean=True, characters='dummy')
@@ -67,3 +66,40 @@ def test_save_only_specific_fields_should_let_other_fields_dirty():
 
     # 'characters' field should still be dirty, update_fields was only saving the 'boolean' field in the db
     assert tm.get_dirty_fields() == {'characters': 'dummy'}
+
+
+ at pytest.mark.django_db
+def test_handle_foreignkeys_id_field_in_update_fields():
+    tm1 = TestModel.objects.create(boolean=True, characters='dummy')
+    tm2 = TestModel.objects.create(boolean=True, characters='dummy')
+    tmwfk = TestModelWithForeignKey.objects.create(fkey=tm1)
+
+    tmwfk.fkey = tm2
+    assert tmwfk.get_dirty_fields(check_relationship=True) == {'fkey': tm1.pk}
+
+    tmwfk.save(update_fields=['fkey_id'])
+    assert tmwfk.get_dirty_fields(check_relationship=True) == {}
+
+
+ at pytest.mark.django_db
+def test_correctly_handle_foreignkeys_id_field_in_update_fields():
+    tm1 = TestModel.objects.create(boolean=True, characters='dummy')
+    tm2 = TestModel.objects.create(boolean=True, characters='dummy')
+    tmwfk = TestModelWithForeignKey.objects.create(fkey=tm1)
+
+    tmwfk.fkey_id = tm2.pk
+    assert tmwfk.get_dirty_fields(check_relationship=True) == {'fkey': tm1.pk}
+
+    tmwfk.save(update_fields=['fkey'])
+    assert tmwfk.get_dirty_fields(check_relationship=True) == {}
+
+
+ at pytest.mark.django_db
+def test_save_deferred_field_with_update_fields():
+    TestModel.objects.create()
+
+    tm = TestModel.objects.defer('boolean').first()
+    tm.boolean = False
+    # Test that providing a deferred field to the update_fields
+    # save parameter doesn't raise a KeyError anymore.
+    tm.save(update_fields=['boolean'])
diff --git a/tests/test_specified_fields.py b/tests/test_specified_fields.py
new file mode 100644
index 0000000..9d68d0b
--- /dev/null
+++ b/tests/test_specified_fields.py
@@ -0,0 +1,28 @@
+import pytest
+
+from .models import TestModel, TestModelWithSpecifiedFields, TestModelWithM2MAndSpecifiedFields
+
+
+ at pytest.mark.django_db
+def test_dirty_fields_on_model_with_specified_fields():
+    tm = TestModelWithSpecifiedFields.objects.create()
+
+    tm.boolean1 = False
+    tm.boolean2 = False
+
+    # boolean1 is tracked, boolean2 isn`t tracked
+    assert tm.get_dirty_fields() == {'boolean1': True}
+
+
+ at pytest.mark.django_db
+def test_dirty_fields_on_model_with_m2m_and_specified_fields():
+    tm = TestModelWithM2MAndSpecifiedFields.objects.create()
+    tm2 = TestModel.objects.create()
+
+    tm.m2m1.add(tm2)
+    tm.m2m2.add(tm2)
+
+    # m2m1 is tracked, m2m2 isn`t tracked
+    assert tm.get_dirty_fields(check_m2m={'m2m1': set([])}) == {'m2m1': set([tm2.id])}
+    assert tm.get_dirty_fields(check_m2m={'m2m2': set([])}) == {}
+
diff --git a/tox.ini b/tox.ini
index e4d3fc9..07cb563 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = django14,django15,django16,django17,django18,django19,django110,coverage,postgresql
+envlist = django18,django19,django110,django111,coverage,postgresql
 
 [testenv]
 setenv =
@@ -7,31 +7,11 @@ setenv =
 commands =
     py.test --ds=tests.django_settings -v
 deps =
-    pytest==2.7.1
-    pytest-django==2.8.0
+    pytest==3.0.6
+    pytest-django==3.1.2
     jsonfield==1.0.3
     pytz
 
-[testenv:django14]
-deps =
-    django>=1.4,<1.4.99
-    {[testenv]deps}
-
-[testenv:django15]
-deps =
-    django>=1.5,<1.5.99
-    {[testenv]deps}
-
-[testenv:django16]
-deps =
-    django>=1.6,<1.6.99
-    {[testenv]deps}
-
-[testenv:django17]
-deps =
-    django>=1.7,<1.7.99
-    {[testenv]deps}
-
 [testenv:django18]
 deps =
     django>=1.8,<1.8.99
@@ -47,13 +27,18 @@ deps =
     django>=1.10,<1.10.99
     {[testenv]deps}
 
+[testenv:django111]
+deps =
+    django>=1.11,<1.11.99
+    {[testenv]deps}
+
 [testenv:postgresql]
 setenv =
     PYTHONPATH = {toxinidir}
 commands =
     py.test --ds=tests.postgresql_django_settings -v
 deps =
-    django>=1.10,<1.10.99
+    django>=1.11,<1.11.99
     psycopg2
     {[testenv]deps}
 

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/django-dirtyfields.git



More information about the Python-modules-commits mailing list