Repository: incubator-ariatosca Updated Branches: refs/heads/runtime_props_to_attr b47ad4295 -> 18d218542
removed instumentation and fixed linting Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/18d21854 Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/18d21854 Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/18d21854 Branch: refs/heads/runtime_props_to_attr Commit: 18d2185422f38e395659fdb5b566ed215c756d8c Parents: b47ad42 Author: max-orlov <[email protected]> Authored: Tue May 16 21:38:19 2017 +0300 Committer: max-orlov <[email protected]> Committed: Tue May 16 21:38:19 2017 +0300 ---------------------------------------------------------------------- aria/modeling/types.py | 20 - aria/orchestrator/context/common.py | 13 +- aria/orchestrator/workflows/executor/process.py | 1 - aria/storage/instrumentation.py | 321 --------------- tests/orchestrator/context/test_operation.py | 28 +- tests/storage/test_instrumentation.py | 396 ------------------- 6 files changed, 26 insertions(+), 753 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/18d21854/aria/modeling/types.py ---------------------------------------------------------------------- diff --git a/aria/modeling/types.py b/aria/modeling/types.py index 7460f47..920a0c2 100644 --- a/aria/modeling/types.py +++ b/aria/modeling/types.py @@ -286,24 +286,4 @@ _LISTENER_ARGS = (mutable.mapper, 'mapper_configured', _mutable_association_list def _register_mutable_association_listener(): event.listen(*_LISTENER_ARGS) - -def remove_mutable_association_listener(): - """ - Remove the event listener that associates ``Dict`` and ``List`` column types with - ``MutableDict`` and ``MutableList``, respectively. - - This call must happen before any model instance is instantiated. - This is because once it does, that would trigger the listener we are trying to remove. - Once it is triggered, many other listeners will then be registered. - At that point, it is too late. - - The reason this function exists is that the association listener, interferes with ARIA change - tracking instrumentation, so a way to disable it is required. - - Note that the event listener this call removes is registered by default. - """ - if event.contains(*_LISTENER_ARGS): - event.remove(*_LISTENER_ARGS) - - _register_mutable_association_listener() http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/18d21854/aria/orchestrator/context/common.py ---------------------------------------------------------------------- diff --git a/aria/orchestrator/context/common.py b/aria/orchestrator/context/common.py index 260ccea..9758bb5 100644 --- a/aria/orchestrator/context/common.py +++ b/aria/orchestrator/context/common.py @@ -333,6 +333,16 @@ class DecorateAttributes(dict): def __init__(self, func): super(DecorateAttributes, self).__init__() self._func = func + self._attributes = None + self._actor = None + + @property + def attributes(self): + return self._attributes + + @property + def actor(self): + return self._actor def __getattr__(self, item): try: @@ -343,6 +353,5 @@ class DecorateAttributes(dict): def __call__(self, *args, **kwargs): func_self = args[0] self._actor = self._func(*args, **kwargs) - self._model = func_self.model - self.attributes = _Dict(self._actor, self._model) + self._attributes = _Dict(self._actor, func_self.model) return self http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/18d21854/aria/orchestrator/workflows/executor/process.py ---------------------------------------------------------------------- diff --git a/aria/orchestrator/workflows/executor/process.py b/aria/orchestrator/workflows/executor/process.py index f3a08ec..d15d878 100644 --- a/aria/orchestrator/workflows/executor/process.py +++ b/aria/orchestrator/workflows/executor/process.py @@ -49,7 +49,6 @@ from aria.utils import ( exceptions, process as process_utils ) -from aria.modeling import types as modeling_types _INT_FMT = 'I' http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/18d21854/aria/storage/instrumentation.py ---------------------------------------------------------------------- diff --git a/aria/storage/instrumentation.py b/aria/storage/instrumentation.py deleted file mode 100644 index eb5ff6c..0000000 --- a/aria/storage/instrumentation.py +++ /dev/null @@ -1,321 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import copy -import json -import os - -import sqlalchemy.event - -from ..modeling import models as _models -from ..storage.exceptions import StorageError - - -_VERSION_ID_COL = 'version' -_STUB = object() -_Collection = type('_Collection', (object, ), {}) - -collection = _Collection() -_INSTRUMENTED = { - 'modified': { - _models.Node.state: str, - _models.Task.status: str, - _models.Node.attributes: collection, - # TODO: add support for pickled type - _models.Parameter._value: lambda x: x - }, - 'new': (_models.Log, ), - -} - -_NEW_INSTANCE = 'NEW_INSTANCE' - - -def track_changes(model=None, instrumented=None): - """Track changes in the specified model columns - - This call will register event listeners using sqlalchemy's event mechanism. The listeners - instrument all returned objects such that the attributes specified in ``instrumented``, will - be replaced with a value that is stored in the returned instrumentation context - ``tracked_changes`` property. - - Why should this be implemented when sqlalchemy already does a fantastic job at tracking changes - you ask? Well, when sqlalchemy is used with sqlite, due to how sqlite works, only one process - can hold a write lock to the database. This does not work well when ARIA runs tasks in - subprocesses (by the process executor) and these tasks wish to change some state as well. These - tasks certainly deserve a chance to do so! - - To enable this, the subprocess calls ``track_changes()`` before any state changes are made. - At the end of the subprocess execution, it should return the ``tracked_changes`` attribute of - the instrumentation context returned from this call, to the parent process. The parent process - will then call ``apply_tracked_changes()`` that resides in this module as well. - At that point, the changes will actually be written back to the database. - - :param model: the model storage. it should hold a mapi for each model. the session of each mapi - is needed to setup events - :param instrumented: A dict from model columns to their python native type - :return: The instrumentation context - """ - return _Instrumentation(model, instrumented or _INSTRUMENTED) - - -class _Instrumentation(object): - - def __init__(self, model, instrumented): - self.tracked_changes = {} - self.new_instances_as_dict = {} - self.listeners = [] - self._instances_to_expunge = [] - self._model = model - self._track_changes(instrumented) - - @property - def _new_instance_id(self): - return '{prefix}_{index}'.format(prefix=_NEW_INSTANCE, - index=len(self._instances_to_expunge)) - - def expunge_session(self): - for new_instance in self._instances_to_expunge: - self._get_session_from_model(new_instance.__tablename__).expunge(new_instance) - - def _get_session_from_model(self, tablename): - mapi = getattr(self._model, tablename, None) - if mapi: - return mapi._session - raise StorageError("Could not retrieve session for {0}".format(tablename)) - - def _track_changes(self, instrumented): - instrumented_attribute_classes = {} - # Track any newly created instances. - for instrumented_class in instrumented.get('new', []): - self._register_new_instance_listener(instrumented_class) - - # Track any newly-set attributes. - for instrumented_attribute, attribute_type in instrumented.get('modified', {}).items(): - self._register_attribute_listener(instrumented_attribute=instrumented_attribute, - attribute_type=attribute_type) - if not isinstance(attribute_type, _Collection): - instrumented_class = instrumented_attribute.parent.entity - instrumented_class_attributes = instrumented_attribute_classes.setdefault( - instrumented_class, {}) - instrumented_class_attributes[instrumented_attribute.key] = attribute_type - - # Track any global instance update such as 'refresh' or 'load' - for instrumented_class, instrumented_attributes in instrumented_attribute_classes.items(): - self._register_instance_listeners(instrumented_class=instrumented_class, - instrumented_attributes=instrumented_attributes) - - def _register_new_instance_listener(self, instrumented_class): - if self._model is None: - raise StorageError("In order to keep track of new instances, a ctx is needed") - - def listener(_, instance): - if not isinstance(instance, instrumented_class): - return - self._instances_to_expunge.append(instance) - tracked_instances = self.new_instances_as_dict.setdefault(instance.__modelname__, {}) - tracked_attributes = tracked_instances.setdefault(self._new_instance_id, {}) - instance_as_dict = instance.to_dict() - instance_as_dict.update((k, getattr(instance, k)) - for k in getattr(instance, '__private_fields__', [])) - tracked_attributes.update(instance_as_dict) - session = self._get_session_from_model(instrumented_class.__tablename__) - listener_args = (session, 'after_attach', listener) - sqlalchemy.event.listen(*listener_args) - self.listeners.append(listener_args) - - def _register_attribute_listener(self, instrumented_attribute, attribute_type): - # Track and newly created instances that are a part of a collection. - if isinstance(attribute_type, _Collection): - return self._register_append_to_attribute_listener(instrumented_attribute) - else: - return self._register_set_attribute_listener(instrumented_attribute, attribute_type) - - def _register_append_to_attribute_listener(self, collection_attr): - def listener(target, value, initiator): - import pydevd; pydevd.settrace('localhost', suspend=False) - tracked_instances = self.tracked_changes.setdefault(target.__modelname__, {}) - tracked_attributes = tracked_instances.setdefault(target.id, {}) - collection_attr = tracked_attributes.setdefault(initiator.key, []) - instance_as_dict = value.to_dict() - instance_as_dict.update((k, getattr(value, k)) - for k in getattr(value, '__private_fields__', [])) - instance_as_dict['_MODEL_CLS'] = value.__modelname__ - collection_attr.append(instance_as_dict) - - listener_args = (collection_attr, 'append', listener) - sqlalchemy.event.listen(*listener_args) - self.listeners.append(listener_args) - - def _register_set_attribute_listener(self, instrumented_attribute, attribute_type): - def listener(target, value, *_): - import pydevd; pydevd.settrace('localhost', suspend=False) - mapi_name = target.__modelname__ - tracked_instances = self.tracked_changes.setdefault(mapi_name, {}) - tracked_attributes = tracked_instances.setdefault(target.id, {}) - current = copy.deepcopy(attribute_type(value)) if value else None - tracked_attributes[instrumented_attribute.key] = _Value(_STUB, current) - return current - listener_args = (instrumented_attribute, 'set', listener) - sqlalchemy.event.listen(*listener_args, retval=True) - self.listeners.append(listener_args) - - def _register_instance_listeners(self, instrumented_class, instrumented_attributes): - def listener(target, *_): - mapi_name = instrumented_class.__modelname__ - tracked_instances = self.tracked_changes.setdefault(mapi_name, {}) - tracked_attributes = tracked_instances.setdefault(target.id, {}) - if hasattr(target, _VERSION_ID_COL): - # We want to keep track of the initial version id so it can be compared - # with the committed version id when the tracked changes are applied - tracked_attributes.setdefault(_VERSION_ID_COL, - _Value(_STUB, getattr(target, _VERSION_ID_COL))) - for attribute_name, attribute_type in instrumented_attributes.items(): - if attribute_name not in tracked_attributes: - initial = getattr(target, attribute_name) - if initial is None: - current = None - else: - current = copy.deepcopy(attribute_type(initial)) - tracked_attributes[attribute_name] = _Value(initial, current) - target.__dict__[attribute_name] = tracked_attributes[attribute_name].current - for listener_args in ((instrumented_class, 'load', listener), - (instrumented_class, 'refresh', listener), - (instrumented_class, 'refresh_flush', listener)): - sqlalchemy.event.listen(*listener_args) - self.listeners.append(listener_args) - - def clear(self, target=None): - if target: - mapi_name = target.__modelname__ - tracked_instances = self.tracked_changes.setdefault(mapi_name, {}) - tracked_instances.pop(target.id, None) - else: - self.tracked_changes.clear() - - self.new_instances_as_dict.clear() - self._instances_to_expunge = [] - - def restore(self): - """Remove all listeners registered by this instrumentation""" - for listener_args in self.listeners: - if sqlalchemy.event.contains(*listener_args): - sqlalchemy.event.remove(*listener_args) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.restore() - - -class _Value(object): - # You may wonder why is this a full blown class and not a named tuple. The reason is that - # jsonpickle that is used to serialize the tracked_changes, does not handle named tuples very - # well. At the very least, I could not get it to behave. - - def __init__(self, initial, current): - self.initial = initial - self.current = current - - def __eq__(self, other): - if not isinstance(other, _Value): - return False - return self.initial == other.initial and self.current == other.current - - def __hash__(self): - return hash((self.initial, self.current)) - - @property - def dict(self): - return {'initial': self.initial, 'current': self.current}.copy() - - -def apply_tracked_changes(tracked_changes, new_instances, model): - """Write tracked changes back to the database using provided model storage - - :param tracked_changes: The ``tracked_changes`` attribute of the instrumentation context - returned by calling ``track_changes()`` - :param model: The model storage used to actually apply the changes - """ - successfully_updated_changes = dict() - try: - - # Handle new instances - for mapi_name, new_instance in new_instances.items(): - successfully_updated_changes[mapi_name] = dict() - mapi = getattr(model, mapi_name) - for tmp_id, new_instance_kwargs in new_instance.items(): - instance = mapi.model_cls(**new_instance_kwargs) - mapi.put(instance) - successfully_updated_changes[mapi_name][instance.id] = new_instance_kwargs - new_instance[tmp_id] = instance - - # handle instance updates - for mapi_name, tracked_instances in tracked_changes.items(): - successfully_updated_changes[mapi_name] = dict() - mapi = getattr(model, mapi_name) - - for instance_id, tracked_attributes in tracked_instances.items(): - successfully_updated_changes[mapi_name][instance_id] = dict() - instance = None - for attribute_name, value in tracked_attributes.items(): - instance = instance or mapi.get(instance_id) - if isinstance(value, list): - # The changes are new item to a collection - for item in value: - model_name = item.pop('_MODEL_CLS') - attr_model = getattr(model, model_name).model_cls - new_attr = attr_model(**item) - getattr(instance, attribute_name)[new_attr] = new_attr - elif value.initial != value.current: - # scalar attribute - setattr(instance, attribute_name, value.current) - if instance: - _validate_version_id(instance, mapi) - mapi.update(instance) - # TODO: reinstate this - # successfully_updated_changes[mapi_name][instance_id] = [ - # v.dict for v in tracked_attributes.values()] - - except BaseException: - for key, value in successfully_updated_changes.items(): - if not value: - del successfully_updated_changes[key] - # TODO: if the successful has _STUB, the logging fails because it can't serialize the object - model.logger.error( - 'Registering all the changes to the storage has failed. {0}' - 'The successful updates were: {0} ' - '{1}'.format(os.linesep, json.dumps(successfully_updated_changes, indent=4))) - - raise - - -def _validate_version_id(instance, mapi): - version_id = sqlalchemy.inspect(instance).committed_state.get(_VERSION_ID_COL) - # There are two version conflict code paths: - # 1. The instance committed state loaded already holds a newer version, - # in this case, we manually raise the error - # 2. The UPDATE statement is executed with version validation and sqlalchemy - # will raise a StateDataError if there is a version mismatch. - if version_id and getattr(instance, _VERSION_ID_COL) != version_id: - object_version_id = getattr(instance, _VERSION_ID_COL) - mapi._session.rollback() - raise StorageError( - 'Version conflict: committed and object {0} differ ' - '[committed {0}={1}, object {0}={2}]' - .format(_VERSION_ID_COL, - version_id, - object_version_id)) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/18d21854/tests/orchestrator/context/test_operation.py ---------------------------------------------------------------------- diff --git a/tests/orchestrator/context/test_operation.py b/tests/orchestrator/context/test_operation.py index ca5154c..5ce0b22 100644 --- a/tests/orchestrator/context/test_operation.py +++ b/tests/orchestrator/context/test_operation.py @@ -513,7 +513,7 @@ class MockModel(object): 'update': lambda *args, **kwargs: None})() -class TestDict(): +class TestDict(object): @pytest.fixture def actor(self): @@ -525,9 +525,11 @@ class TestDict(): def test_keys(self, model, actor): dict_ = common._Dict(actor, model) - actor.attributes.update({ - 'key1': Parameter.wrap('key1', 'value1'), - 'key2': Parameter.wrap('key1', 'value2')} + actor.attributes.update( + { + 'key1': Parameter.wrap('key1', 'value1'), + 'key2': Parameter.wrap('key1', 'value2') + } ) assert sorted(dict_.keys()) == sorted(['key1', 'key2']) @@ -535,24 +537,24 @@ class TestDict(): dict_ = common._Dict(actor, model) actor.attributes.update({ 'key1': Parameter.wrap('key1', 'value1'), - 'key2': Parameter.wrap('key1', 'value2')} - ) + 'key2': Parameter.wrap('key1', 'value2') + }) assert sorted(dict_.values()) == sorted(['value1', 'value2']) def test_items(self, actor, model): dict_ = common._Dict(actor, model) actor.attributes.update({ 'key1': Parameter.wrap('key1', 'value1'), - 'key2': Parameter.wrap('key1', 'value2')} - ) + 'key2': Parameter.wrap('key1', 'value2') + }) assert sorted(dict_.items()) == sorted([('key1', 'value1'), ('key2', 'value2')]) def test_iter(self, actor, model): dict_ = common._Dict(actor, model) actor.attributes.update({ 'key1': Parameter.wrap('key1', 'value1'), - 'key2': Parameter.wrap('key1', 'value2')} - ) + 'key2': Parameter.wrap('key1', 'value2') + }) assert sorted(list(dict_)) == sorted(['key1', 'key2']) def test_bool(self, actor, model): @@ -560,8 +562,8 @@ class TestDict(): assert not dict_ actor.attributes.update({ 'key1': Parameter.wrap('key1', 'value1'), - 'key2': Parameter.wrap('key1', 'value2')} - ) + 'key2': Parameter.wrap('key1', 'value2') + }) assert dict_ def test_set_item(self, actor, model): @@ -617,4 +619,4 @@ class TestDict(): dict_['key1'] = 'value1' dict_.clear() - assert len(dict_) == 0 \ No newline at end of file + assert len(dict_) == 0 http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/18d21854/tests/storage/test_instrumentation.py ---------------------------------------------------------------------- diff --git a/tests/storage/test_instrumentation.py b/tests/storage/test_instrumentation.py deleted file mode 100644 index bdbb17e..0000000 --- a/tests/storage/test_instrumentation.py +++ /dev/null @@ -1,396 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest -from sqlalchemy import Column, Text, Integer, event - -from aria.modeling import ( - mixins, - types as modeling_types, - models -) -from aria.modeling.exceptions import ValueFormatException -from aria.storage import ( - ModelStorage, - sql_mapi, - instrumentation -) - -from . import release_sqlite_storage, init_inmemory_model_storage - -STUB = instrumentation._STUB -Value = instrumentation._Value -instruments_holder = [] - - -class TestInstrumentation(object): - - def test_track_changes(self, storage): - model_kwargs = dict( - name='name', - dict1={'initial': 'value'}, - dict2={'initial': 'value'}, - list1=['initial'], - list2=['initial'], - int1=0, - int2=0, - string2='string') - model1_instance = MockModel1(**model_kwargs) - model2_instance = MockModel2(**model_kwargs) - storage.mock_model_1.put(model1_instance) - storage.mock_model_2.put(model2_instance) - - instrument = self._track_changes({ - MockModel1.dict1: dict, - MockModel1.list1: list, - MockModel1.int1: int, - MockModel1.string2: str, - MockModel2.dict2: dict, - MockModel2.list2: list, - MockModel2.int2: int, - MockModel2.name: str - }) - - assert not instrument.tracked_changes - - storage_model1_instance = storage.mock_model_1.get(model1_instance.id) - storage_model2_instance = storage.mock_model_2.get(model2_instance.id) - - storage_model1_instance.dict1 = {'hello': 'world'} - storage_model1_instance.dict2 = {'should': 'not track'} - storage_model1_instance.list1 = ['hello'] - storage_model1_instance.list2 = ['should not track'] - storage_model1_instance.int1 = 100 - storage_model1_instance.int2 = 20000 - storage_model1_instance.name = 'should not track' - storage_model1_instance.string2 = 'new_string' - - storage_model2_instance.dict1.update({'should': 'not track'}) - storage_model2_instance.dict2.update({'hello': 'world'}) - storage_model2_instance.list1.append('should not track') - storage_model2_instance.list2.append('hello') - storage_model2_instance.int1 = 100 - storage_model2_instance.int2 = 20000 - storage_model2_instance.name = 'new_name' - storage_model2_instance.string2 = 'should not track' - - assert instrument.tracked_changes == { - 'mock_model_1': { - model1_instance.id: { - 'dict1': Value(STUB, {'hello': 'world'}), - 'list1': Value(STUB, ['hello']), - 'int1': Value(STUB, 100), - 'string2': Value(STUB, 'new_string') - } - }, - 'mock_model_2': { - model2_instance.id: { - 'dict2': Value({'initial': 'value'}, {'hello': 'world', 'initial': 'value'}), - 'list2': Value(['initial'], ['initial', 'hello']), - 'int2': Value(STUB, 20000), - 'name': Value(STUB, 'new_name'), - } - } - } - - def test_attribute_initial_none_value(self, storage): - instance1 = MockModel1(name='name1', dict1=None) - instance2 = MockModel1(name='name2', dict1=None) - storage.mock_model_1.put(instance1) - storage.mock_model_1.put(instance2) - instrument = self._track_changes({MockModel1.dict1: dict}) - instance1 = storage.mock_model_1.get(instance1.id) - instance2 = storage.mock_model_1.get(instance2.id) - instance1.dict1 = {'new': 'value'} - assert instrument.tracked_changes == { - 'mock_model_1': { - instance1.id: {'dict1': Value(STUB, {'new': 'value'})}, - instance2.id: {'dict1': Value(None, None)}, - } - } - - def test_attribute_set_none_value(self, storage): - instance = MockModel1(name='name') - storage.mock_model_1.put(instance) - instrument = self._track_changes({ - MockModel1.dict1: dict, - MockModel1.list1: list, - MockModel1.string2: str, - MockModel1.int1: int - }) - instance = storage.mock_model_1.get(instance.id) - instance.dict1 = None - instance.list1 = None - instance.string2 = None - instance.int1 = None - assert instrument.tracked_changes == { - 'mock_model_1': { - instance.id: { - 'dict1': Value(STUB, None), - 'list1': Value(STUB, None), - 'string2': Value(STUB, None), - 'int1': Value(STUB, None) - } - } - } - - def test_restore(self): - instrument = self._track_changes({MockModel1.dict1: dict}) - # set instance attribute, load instance, refresh instance and flush_refresh listeners - assert len(instrument.listeners) == 4 - for listener_args in instrument.listeners: - assert event.contains(*listener_args) - instrument.restore() - assert len(instrument.listeners) == 4 - for listener_args in instrument.listeners: - assert not event.contains(*listener_args) - return instrument - - def test_restore_twice(self): - instrument = self.test_restore() - instrument.restore() - - def test_instrumentation_context_manager(self, storage): - instance = MockModel1(name='name') - storage.mock_model_1.put(instance) - with self._track_changes({MockModel1.dict1: dict}) as instrument: - instance = storage.mock_model_1.get(instance.id) - instance.dict1 = {'new': 'value'} - assert instrument.tracked_changes == { - 'mock_model_1': {instance.id: {'dict1': Value(STUB, {'new': 'value'})}} - } - assert len(instrument.listeners) == 4 - for listener_args in instrument.listeners: - assert event.contains(*listener_args) - for listener_args in instrument.listeners: - assert not event.contains(*listener_args) - - def test_apply_tracked_changes(self, storage): - initial_values = {'dict1': {'initial': 'value'}, 'list1': ['initial']} - instance1_1 = MockModel1(name='instance1_1', **initial_values) - instance1_2 = MockModel1(name='instance1_2', **initial_values) - instance2_1 = MockModel2(name='instance2_1', **initial_values) - instance2_2 = MockModel2(name='instance2_2', **initial_values) - storage.mock_model_1.put(instance1_1) - storage.mock_model_1.put(instance1_2) - storage.mock_model_2.put(instance2_1) - storage.mock_model_2.put(instance2_2) - - instrument = self._track_changes({ - MockModel1.dict1: dict, - MockModel1.list1: list, - MockModel2.dict1: dict, - MockModel2.list1: list - }) - - def get_instances(): - return (storage.mock_model_1.get(instance1_1.id), - storage.mock_model_1.get(instance1_2.id), - storage.mock_model_2.get(instance2_1.id), - storage.mock_model_2.get(instance2_2.id)) - - instance1_1, instance1_2, instance2_1, instance2_2 = get_instances() - instance1_1.dict1 = {'new': 'value'} - instance1_2.list1 = ['new_value'] - instance2_1.dict1.update({'new': 'value'}) - instance2_2.list1.append('new_value') - - instrument.restore() - storage.mock_model_1._session.expire_all() - - instance1_1, instance1_2, instance2_1, instance2_2 = get_instances() - instance1_1.dict1 = {'overriding': 'value'} - instance1_2.list1 = ['overriding_value'] - instance2_1.dict1 = {'overriding': 'value'} - instance2_2.list1 = ['overriding_value'] - storage.mock_model_1.put(instance1_1) - storage.mock_model_1.put(instance1_2) - storage.mock_model_2.put(instance2_1) - storage.mock_model_2.put(instance2_2) - instance1_1, instance1_2, instance2_1, instance2_2 = get_instances() - assert instance1_1.dict1 == {'overriding': 'value'} - assert instance1_2.list1 == ['overriding_value'] - assert instance2_1.dict1 == {'overriding': 'value'} - assert instance2_2.list1 == ['overriding_value'] - - instrumentation.apply_tracked_changes( - tracked_changes=instrument.tracked_changes, - new_instances={}, - model=storage) - - instance1_1, instance1_2, instance2_1, instance2_2 = get_instances() - assert instance1_1.dict1 == {'new': 'value'} - assert instance1_2.list1 == ['new_value'] - assert instance2_1.dict1 == {'initial': 'value', 'new': 'value'} - assert instance2_2.list1 == ['initial', 'new_value'] - - def test_clear_instance(self, storage): - instance1 = MockModel1(name='name1') - instance2 = MockModel1(name='name2') - for instance in [instance1, instance2]: - storage.mock_model_1.put(instance) - instrument = self._track_changes({MockModel1.dict1: dict}) - instance1.dict1 = {'new': 'value'} - instance2.dict1 = {'new2': 'value2'} - assert instrument.tracked_changes == { - 'mock_model_1': { - instance1.id: {'dict1': Value(STUB, {'new': 'value'})}, - instance2.id: {'dict1': Value(STUB, {'new2': 'value2'})} - } - } - instrument.clear(instance1) - assert instrument.tracked_changes == { - 'mock_model_1': { - instance2.id: {'dict1': Value(STUB, {'new2': 'value2'})} - } - } - - def test_clear_all(self, storage): - instance1 = MockModel1(name='name1') - instance2 = MockModel1(name='name2') - for instance in [instance1, instance2]: - storage.mock_model_1.put(instance) - instrument = self._track_changes({MockModel1.dict1: dict}) - instance1.dict1 = {'new': 'value'} - instance2.dict1 = {'new2': 'value2'} - assert instrument.tracked_changes == { - 'mock_model_1': { - instance1.id: {'dict1': Value(STUB, {'new': 'value'})}, - instance2.id: {'dict1': Value(STUB, {'new2': 'value2'})} - } - } - instrument.clear() - assert instrument.tracked_changes == {} - - def test_new_instances(self, storage): - model_kwargs = dict( - name='name', - dict1={'initial': 'value'}, - dict2={'initial': 'value'}, - list1=['initial'], - list2=['initial'], - int1=0, - int2=0, - string2='string') - model_instance_1 = MockModel1(**model_kwargs) - model_instance_2 = MockModel2(**model_kwargs) - - instrument = self._track_changes(model=storage, instrumented_new=(MockModel1,)) - assert not instrument.tracked_changes - - storage.mock_model_1.put(model_instance_1) - storage.mock_model_2.put(model_instance_2) - # Assert all models made it to storage - assert len(storage.mock_model_1.list()) == len(storage.mock_model_2.list()) == 1 - - # Assert only one model was tracked - assert len(instrument.new_instances) == 1 - - mock_model_1 = instrument.new_instances[MockModel1.__tablename__].values()[0] - storage_model1_instance = storage.mock_model_1.get(model_instance_1.id) - - for key in model_kwargs: - assert mock_model_1[key] == model_kwargs[key] == getattr(storage_model1_instance, key) - - def _track_changes(self, instrumented_modified=None, model=None, instrumented_new=None): - instrument = instrumentation.track_changes( - model=model, - instrumented={'modified': instrumented_modified or {}, 'new': instrumented_new or {}}) - instruments_holder.append(instrument) - return instrument - - def test_track_changes_to_strict_dict(self, storage): - model_kwargs = dict(strict_dict={'key': 'value'}, - strict_list=['item']) - model_instance = StrictMockModel(**model_kwargs) - storage.strict_mock_model.put(model_instance) - - instrument = self._track_changes({ - StrictMockModel.strict_dict: dict, - StrictMockModel.strict_list: list, - }) - - assert not instrument.tracked_changes - - storage_model_instance = storage.strict_mock_model.get(model_instance.id) - - with pytest.raises(ValueFormatException): - storage_model_instance.strict_dict = {1: 1} - - with pytest.raises(ValueFormatException): - storage_model_instance.strict_dict = {'hello': 1} - - with pytest.raises(ValueFormatException): - storage_model_instance.strict_dict = {1: 'hello'} - - storage_model_instance.strict_dict = {'hello': 'world'} - assert storage_model_instance.strict_dict == {'hello': 'world'} - - with pytest.raises(ValueFormatException): - storage_model_instance.strict_list = [1] - storage_model_instance.strict_list = ['hello'] - assert storage_model_instance.strict_list == ['hello'] - - assert instrument.tracked_changes == { - 'strict_mock_model': { - model_instance.id: { - 'strict_dict': Value(STUB, {'hello': 'world'}), - 'strict_list': Value(STUB, ['hello']), - } - }, - } - - [email protected](autouse=True) -def restore_instrumentation(): - yield - for instrument in instruments_holder: - instrument.restore() - del instruments_holder[:] - - [email protected] -def storage(): - result = ModelStorage(api_cls=sql_mapi.SQLAlchemyModelAPI, - items=(MockModel1, MockModel2, StrictMockModel), - initiator=init_inmemory_model_storage) - yield result - release_sqlite_storage(result) - - -class _MockModel(mixins.ModelMixin): - name = Column(Text) - dict1 = Column(modeling_types.Dict) - dict2 = Column(modeling_types.Dict) - list1 = Column(modeling_types.List) - list2 = Column(modeling_types.List) - int1 = Column(Integer) - int2 = Column(Integer) - string2 = Column(Text) - - -class MockModel1(_MockModel, models.aria_declarative_base): - __tablename__ = 'mock_model_1' - - -class MockModel2(_MockModel, models.aria_declarative_base): - __tablename__ = 'mock_model_2' - - -class StrictMockModel(mixins.ModelMixin, models.aria_declarative_base): - __tablename__ = 'strict_mock_model' - - strict_dict = Column(modeling_types.StrictDict(basestring, basestring)) - strict_list = Column(modeling_types.StrictList(basestring))
