Skip to content

Commit

Permalink
Rewrited chamber handlers to the classes
Browse files Browse the repository at this point in the history
  • Loading branch information
matllubos committed Sep 25, 2019
1 parent b9b96ba commit 15b007e
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 106 deletions.
4 changes: 0 additions & 4 deletions chamber/models/dispatchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@

from django.core.exceptions import ImproperlyConfigured

from chamber.utils.transaction import (
on_success, OnSuccessHandler, OneTimeOnSuccessHandler, InstanceOneTimeOnSuccessHandler
)

from .signals import dispatcher_post_save


Expand Down
86 changes: 86 additions & 0 deletions chamber/models/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from django.core.exceptions import ImproperlyConfigured

from chamber.utils.transaction import on_success, UniqueOnSuccessCallable
from chamber.models.signals import dispatcher_post_save


class BaseHandler:
"""
Base dispatcher class that can be subclassed to call a handler based on a change in some a SmartModel.
If you subclass, be sure the __call__ method does not change signature.
"""

signal = None

def __init__(self, signal=None):
self._signal = signal if signal is not None else self.signal

def connect(self, sender):
if not self._signal:
raise ImproperlyConfigured('Handler cannot be connected as dispatcher, because signal is not set')
self._signal.connect(self, sender=sender)

def __call__(self, instance, **kwargs):
"""
`instance` ... instance of the SmartModel where the handler is being called
Some dispatchers require additional params to evaluate the handler can be dispatched,
these are hidden in args and kwargs.
"""
if self.can_handle(instance, **kwargs):
self._handle(instance, **kwargs)

def _handle(self, instance, **kwargs):
self.handle(instance, **kwargs)

def can_handle(self, instance, **kwargs):
return True

def handle(self, instance, **kwargs):
raise NotImplementedError


class OnSuccessHandler(BaseHandler):
"""
Handler class that is used for performing on success operations.
"""

def __init__(self, using=None, *args, **kwargs):
self.using = using
super().__init__(*args, **kwargs)

def _handle(self, instance, **kwargs):

on_success(lambda: self.handle(instance, **kwargs), using=self.using)


class InstanceOneTimeOnSuccessHandlerCallable(UniqueOnSuccessCallable):
"""
Use this class to create on success caller that will be unique per instance and will be called only once per
instance.
"""

def __init__(self, handler, instance):
super().__init__(instance=instance)
self.handler = handler

def _get_instance(self):
instance = self.kwargs_list[0]['instance']
return instance.__class__.objects.get(pk=instance.pk)

def _get_unique_id(self):
instance = self.kwargs_list[0]['instance']
return hash((self.handler.__class__, instance.__class__, instance.pk))

def __call__(self):
self.handler.handle(instance=self._get_instance())


class InstanceOneTimeOnSuccessHandler(OnSuccessHandler):
"""
Use this class to create handler that will be unique per instance and will be called only once per instance.
"""

signal = dispatcher_post_save

def _handle(self, instance, **kwargs):
on_success(InstanceOneTimeOnSuccessHandlerCallable(self, instance), using=self.using)
108 changes: 36 additions & 72 deletions chamber/utils/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,39 +25,39 @@ def atomic(func):

class TransactionSignalsContext:
"""
Context object that stores handlers and call it after successful pass trough surrounded code block
with "transaction_signals decorator. Handlers can be unique or standard. Unique handlers are registered
Context object that stores callable and call it after successful pass trough surrounded code block
with "transaction_signals decorator. Handlers can be unique or standard. Unique callable are registered
and executed only once.
"""

def __init__(self):
self._unique_handlers = OrderedDict()
self._handlers = []
self._unique_callable = OrderedDict()
self._callable_list = []

def register(self, handler):
if getattr(handler, 'is_unique', False):
if hash(handler) in self._unique_handlers:
self._unique_handlers.get(hash(handler)).join(handler)
def register(self, callable):
if isinstance(callable, UniqueOnSuccessCallable):
if hash(callable) in self._unique_callable:
self._unique_callable.get(hash(callable)).join(callable)
else:
self._unique_handlers[hash(handler)] = handler
self._handlers.append(handler)
self._unique_callable[hash(callable)] = callable
self._callable_list.append(callable)
else:
self._handlers.append(handler)
self._callable_list.append(callable)

def handle_all(self):
for handler in self._handlers:
handler()
for callable in self._callable_list:
callable()

def join(self, transaction_signals_context):
for handler in transaction_signals_context._handlers:
self.register(handler)
for callable in transaction_signals_context._callable_list:
self.register(callable)


class TransactionSignals(ContextDecorator):
"""
Context decorator that supports usage python keyword "with".
Decorator that adds transaction context to the connection on input.
Finally handlers are called on the output.
Finally callables are called on the output.
"""

def __init__(self, using):
Expand All @@ -81,30 +81,30 @@ def __exit__(self, exc_type, exc_value, traceback):
connection.transaction_signals_context_list[-1].join(transaction_signals_context)


def on_success(handler, using=None):
def on_success(callable, using=None):
"""
Register a handler or a function to be called after successful code pass.
If transaction signals are not active the handler/function is called immediately.
:param handler: handler or function that will be called.
Register a callable or a function to be called after successful code pass.
If transaction signals are not active the callable/function is called immediately.
:param callable: callable or function that will be called.
:param using: name of the database
"""

connection = get_connection(using)
if getattr(connection, 'transaction_signals_context_list', False):
connection.transaction_signals_context_list[-1].register(handler)
connection.transaction_signals_context_list[-1].register(callable)
else:
if settings.DEBUG:
logger.warning(
'For on success signal should be activated transaction signals via transaction_signals decorator.'
'Function is called immediately now.'
)
handler()
callable()


def transaction_signals(using=None):
"""
Decorator that adds transaction context to the connection on input.
Finally handlers are called on the output.
Finally callable are called on the output.
:param using: name of the database
"""
if callable(using):
Expand All @@ -125,72 +125,36 @@ def atomic_with_signals(func):
return transaction.atomic(transaction_signals(func))


class OnSuccessHandler:
class UniqueOnSuccessCallable:
"""
Handler class that is used for performing on success operations.
One time callable class that is used for performing on success operations.
Handler is callable only once, but data of all calls are stored inside list (kwargs_list).
"""

is_unique = False

def __init__(self, using=None, **kwargs):
self.kwargs = kwargs
on_success(self, using=using)

def __call__(self):
self.handle(**self.kwargs)

def handle(self, **kwargs):
"""
There should be implemented handler operations.
:param kwargs: input data that was send during hanlder creation.
"""
raise NotImplementedError


class OneTimeOnSuccessHandler(OnSuccessHandler):
"""
One time handler class that is used for performing on success operations.
Handler is called only once, but data of all calls are stored inside list (kwargs_list).
"""

is_unique = True

def __init__(self, using=None, **kwargs):
def __init__(self, **kwargs):
self.kwargs_list = (kwargs,)
on_success(self, using=using)

def join(self, handler):
def join(self, callable):
"""
Joins two unique handlers.
Joins two unique callable.
"""
self.kwargs_list += handler.kwargs_list
self.kwargs_list += callable.kwargs_list

def _get_unique_id(self):
"""
Unique handler must be identified with some was
Unique callable must be identified with some was
:return:
"""
return None

def __hash__(self):
return hash((self.__class__, self._get_unique_id()))

def _get_kwargs(self):
return self.kwargs_list[-1]

def __call__(self):
self.handle(self.kwargs_list)
self.handle()

def handle(self, kwargs_list):
def handle(self):
raise NotImplementedError


class InstanceOneTimeOnSuccessHandler(OneTimeOnSuccessHandler):
"""
Use this class to create handler that will be unique per instance and will be called only once per instance.
"""

def _get_instance(self):
instance = self.kwargs_list[0]['instance']
return instance.__class__.objects.get(pk=instance.pk)

def _get_unique_id(self):
instance = self.kwargs_list[0]['instance']
return hash((instance.__class__, instance.pk))
2 changes: 1 addition & 1 deletion example/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ SETTINGS = settings
DJANGO_SETTINGS_MODULE = $(DJANGO_DIR).settings.$(SETTINGS)
DJANGO_POSTFIX = --settings=$(DJANGO_SETTINGS_MODULE) --pythonpath=$(PYTHONPATH)
PYTHON_BIN = $(VIRTUAL_ENV)/bin
PYTHON = python3.6
PYTHON = python3.7
PYTHON_VERSION_FULL := $(wordlist 2,4,$(subst ., ,$(shell python --version 2>&1)))
PYTHON_VERSION_MAJOR := $(word 1,${PYTHON_VERSION_FULL}).$(word 2,${PYTHON_VERSION_FULL})
TYPE = dev
Expand Down
14 changes: 14 additions & 0 deletions example/dj/apps/test_chamber/handlers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from chamber.models.handlers import InstanceOneTimeOnSuccessHandler


def create_test_smart_model_handler(instance, **kwargs):
from .models import TestSmartModel # NOQA

Expand All @@ -20,3 +23,14 @@ def create_csv_record_handler(instance, **kwargs):
from .models import CSVRecord # NOQA

CSVRecord.objects.create()


class OneTimeStateChangedHandler(InstanceOneTimeOnSuccessHandler):

def can_handle(self, instance, **kwargs):
return 'state' in instance.changed_fields

def handle(self, instance, **kwargs):
from .models import TestOnDispatchModel

TestOnDispatchModel.objects.create()
11 changes: 9 additions & 2 deletions example/dj/apps/test_chamber/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
from chamber.models.signals import dispatcher_pre_save, dispatcher_post_save
from chamber.utils.datastructures import ChoicesNumEnum, SequenceChoicesNumEnum, SubstatesChoicesNumEnum

from .handlers import (create_test_dispatchers_model_handler, create_test_fields_model_handler,
create_test_smart_model_handler, create_csv_record_handler)
from .handlers import (
create_test_dispatchers_model_handler, create_test_fields_model_handler, create_test_smart_model_handler,
create_csv_record_handler, OneTimeStateChangedHandler
)


class ShortcutsModel(models.Model):
Expand Down Expand Up @@ -103,6 +105,7 @@ class TestDispatchersModel(chamber_models.SmartModel):
StateDispatcher(create_test_smart_model_handler, STATE, state, STATE.SECOND, signal=dispatcher_pre_save),
PropertyDispatcher(create_test_fields_model_handler, 'always_dispatch', signal=dispatcher_post_save),
PropertyDispatcher(create_test_dispatchers_model_handler, 'never_dispatch', signal=dispatcher_post_save),
OneTimeStateChangedHandler(),
)

@property
Expand All @@ -112,3 +115,7 @@ def always_dispatch(self):
@property
def never_dispatch(self):
return False


class TestOnDispatchModel(chamber_models.SmartModel):
pass
24 changes: 22 additions & 2 deletions example/dj/apps/test_chamber/tests/models/dispatchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@

from chamber.models.dispatchers import BaseDispatcher, StateDispatcher
from chamber.shortcuts import change_and_save
from chamber.utils.transaction import transaction_signals

from germanium.tools import assert_equal # pylint: disable=E0401

from test_chamber.models import (CSVRecord, TestDispatchersModel, TestFieldsModel, # pylint: disable=E0401
TestSmartModel) # pylint: disable=E0401
from test_chamber.models import (
CSVRecord, TestDispatchersModel, TestFieldsModel, TestSmartModel, TestOnDispatchModel
) # pylint: disable=E0401


class DispatchersTestCase(TransactionTestCase):
Expand Down Expand Up @@ -71,3 +73,21 @@ def _validate_init_params(self):

InvalidDispatcher(lambda: False)(object())
BaseDispatcher(lambda: False)

def test_more_test_on_dispatch_instances_should_be_created_if_transaction_signals_is_not_activated(self):
model = TestDispatchersModel.objects.create()
assert_equal(TestOnDispatchModel.objects.count(), 1)
model.change_and_save(state=2)
assert_equal(TestOnDispatchModel.objects.count(), 2)
model.change_and_save(state=1)
assert_equal(TestOnDispatchModel.objects.count(), 3)

def test_only_one_test_on_dispatch_instances_should_be_created_if_transaction_signals_is_activated(self):
with transaction_signals():
model = TestDispatchersModel.objects.create()
assert_equal(TestOnDispatchModel.objects.count(), 0)
model.change_and_save(state=2)
assert_equal(TestOnDispatchModel.objects.count(), 0)
model.change_and_save(state=1)
assert_equal(TestOnDispatchModel.objects.count(), 0)
assert_equal(TestOnDispatchModel.objects.count(), 1)

0 comments on commit 15b007e

Please sign in to comment.