diff --git a/entity/sync.py b/entity/sync.py index 88bcb09..6326977 100644 --- a/entity/sync.py +++ b/entity/sync.py @@ -56,40 +56,56 @@ def wrapper(wrapped, instance, args, kwargs): 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() - # Clear the buffer - sync_entities.buffer = {} + # Sync the entities that were deferred if any + if len(sync_entities.buffer): + handler(*model_objs) + + # 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 @@ -193,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 diff --git a/entity/tests/sync_tests.py b/entity/tests/sync_tests.py index 3c36cf0..7435b7b 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,26 @@ def test_method(test): # Ensure that we did not call sync entities 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 + 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): """ 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/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 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: