From 7ed70fbd5f09589c7e93f2904924659ad360b581 Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Wed, 14 Aug 2024 16:07:12 -0400 Subject: [PATCH 1/5] ability to set a custom handler for defer entity syncing --- entity/sync.py | 67 +++++++++++++++++++++++--------------- entity/tests/sync_tests.py | 19 ++++++++++- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/entity/sync.py b/entity/sync.py index 88bcb09..0de6f6a 100644 --- a/entity/sync.py +++ b/entity/sync.py @@ -55,41 +55,56 @@ def wrapper(wrapped, instance, args, kwargs): # Return the decorator return wrapper - -@wrapt.decorator -def defer_entity_syncing(wrapped, instance, args, kwargs): +def defer_entity_syncing(*args, handler=None): """ - A decorator that can be used to defer the syncing of entities until after the method has been run - This is being introduced to help avoid deadlocks in the meantime as we attempt to better understand - why they are happening + A decorator for deferring entity syncing until after the function is complete + An optional handler can be specified to handle the entity syncing. + If no handler is passed the default sync_entities method will be called """ - # Defer entity syncing while we run our method - sync_entities.defer = True + # Set a default handler + handler = handler or sync_entities - # Run the method - try: - return wrapped(*args, **kwargs) + @wrapt.decorator + def wrapper(wrapped, instance, args, kwargs): + """ + A decorator that can be used to defer the syncing of entities until after the method has been run + This is being introduced to help avoid deadlocks in the meantime as we attempt to better understand + why they are happening + """ - # After we run the method disable the deferred syncing - # and sync all the entities that have been buffered to be synced - finally: - # Enable entity syncing again - sync_entities.defer = False + # Defer entity syncing while we run our method + sync_entities.defer = True - # Get the models that need to be synced - model_objs = list(sync_entities.buffer.values()) + # Run the method + try: + return wrapped(*args, **kwargs) - # If none is in the model objects we need to sync all - if None in sync_entities.buffer: - model_objs = list() + # After we run the method disable the deferred syncing + # and sync all the entities that have been buffered to be synced + finally: + # Enable entity syncing again + sync_entities.defer = False - # Sync the entities that were deferred if any - if len(sync_entities.buffer): - sync_entities(*model_objs) + # Get the models that need to be synced + model_objs = list(sync_entities.buffer.values()) + + # If none is in the model objects we need to sync all + if None in sync_entities.buffer: + model_objs = list() + + # Sync the entities that were deferred if any + if len(sync_entities.buffer): + handler(*model_objs) + + # Clear the buffer + sync_entities.buffer = {} - # Clear the buffer - sync_entities.buffer = {} + # If the decorator is called without arguments + if len(args) == 1 and callable(args[0]): + return wrapper(args[0]) + else: + return wrapper @wrapt.decorator diff --git a/entity/tests/sync_tests.py b/entity/tests/sync_tests.py index 3c36cf0..807d5c7 100644 --- a/entity/tests/sync_tests.py +++ b/entity/tests/sync_tests.py @@ -12,7 +12,7 @@ suppress_entity_syncing, ) from entity.signal_handlers import turn_on_syncing, turn_off_syncing -from unittest.mock import patch, MagicMock, call +from unittest.mock import patch, MagicMock, call, Mock from entity.tests.models import ( Account, Team, EntityPointer, DummyModel, MultiInheritEntity, AccountConfig, TeamConfig, TeamGroup, @@ -1064,6 +1064,23 @@ def test_method(test): # Ensure that we did not call sync entities self.assertFalse(mock_sync_entities.called) + def test_defer_custom_handler(self): + mock_handler = Mock() + @defer_entity_syncing(handler=mock_handler) + def test_method(count=5): + # Create some entities + for i in range(count): + Account.objects.create() + + # Call the test method + test_method(count=5) + + # Assert that we called our custom handler + self.assertEqual(Entity.objects.all().count(), 0) + mock_handler.assert_called_once_with( + *Account.objects.all() + ) + class SuppressEntitySyncingTests(EntityTestCase): """ From 620553caab60c23083abe223bafd7f7d66fc2f5a Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Thu, 15 Aug 2024 12:23:47 -0400 Subject: [PATCH 2/5] spacing --- entity/sync.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/entity/sync.py b/entity/sync.py index 0de6f6a..6326977 100644 --- a/entity/sync.py +++ b/entity/sync.py @@ -55,6 +55,7 @@ def wrapper(wrapped, instance, args, kwargs): # Return the decorator return wrapper + def defer_entity_syncing(*args, handler=None): """ A decorator for deferring entity syncing until after the function is complete @@ -208,6 +209,7 @@ def sync_entities(*model_objs): Args: model_objs (List[Model]): The model objects to sync. If empty, all entities will be synced """ + if sync_entities.suppress: # Return false that we did not do anything return False From 864d7e4666ae5eafba1598cefbc9d8d8ac7255db Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Thu, 15 Aug 2024 12:24:08 -0400 Subject: [PATCH 3/5] nose patch --- manage.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manage.py b/manage.py index d6c2b44..7c98b34 100644 --- a/manage.py +++ b/manage.py @@ -1,6 +1,10 @@ #!/usr/bin/env python import sys +# These lines allow nose tests to work in Python 3.10 +import collections.abc +collections.Callable = collections.abc.Callable + # Show warnings about django deprecations - uncomment for version upgrade testing import warnings from django.utils.deprecation import RemovedInNextVersionWarning From cf93c3f8e68eac1561a6f7ff3f69694c6f710fc5 Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Thu, 15 Aug 2024 23:34:49 -0400 Subject: [PATCH 4/5] bump version, add release notes --- entity/version.py | 2 +- release_notes.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/entity/version.py b/entity/version.py index ac1df1f..3fec2bf 100644 --- a/entity/version.py +++ b/entity/version.py @@ -1 +1 @@ -__version__ = '6.2.2' +__version__ = '6.2.3' diff --git a/release_notes.md b/release_notes.md index cd92e35..400493c 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,5 +1,7 @@ ## Release Notes +- 6.2.3: + - Update the `defer_entity_syncing` decorator to support an optional handler. - 6.2.2: - Fixed entity group logic string for logic sets containing more than 2 items being operated on - 6.2.1: From d994caf83686e0e5ff4d958d113bc41db4360532 Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Thu, 15 Aug 2024 23:38:28 -0400 Subject: [PATCH 5/5] flake8 --- entity/tests/sync_tests.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/entity/tests/sync_tests.py b/entity/tests/sync_tests.py index 807d5c7..7435b7b 100644 --- a/entity/tests/sync_tests.py +++ b/entity/tests/sync_tests.py @@ -1065,7 +1065,10 @@ def test_method(test): self.assertFalse(mock_sync_entities.called) def test_defer_custom_handler(self): + # Create a mock handler mock_handler = Mock() + + # Create a test sync method to be decorated @defer_entity_syncing(handler=mock_handler) def test_method(count=5): # Create some entities