Extend MutableList and MutableDict functionality

Added next methods to MutableList:
 - append
 - extend
 - insert
 - pop
 - __setslice__
 - __delslice__

Added next methods to MutableDict:
 - pop
 - popitem

Fixed doc strings.
Fixed copy and deepcopy.
Covered by unittests.

Change-Id: Ia35628f09f25f7008afa6fcf48c15f03bcbca51a
Partial-Bug: 1482658
This commit is contained in:
Valentin Kaplov 2015-12-03 13:47:24 +03:00
parent d26b966e67
commit 984f3bfc6a
3 changed files with 383 additions and 20 deletions

View File

@ -17,6 +17,34 @@
import copy
from sqlalchemy.ext.mutable import Mutable
from sqlalchemy.ext.mutable import MutableDict as MutableDictBase
class MutableDict(MutableDictBase):
# TODO(vkaplov): delete this class after methods pop and popitem
# will be implemented in sqlalchemy lib.
# https://bitbucket.org/zzzeek/sqlalchemy/issues/3605
def pop(self, *args):
"""Pop element and emit change events if item removed.
:param args: (key, default) 'default' is optional
:return: element
:raises KeyError: if dict is empty or element not found.
"""
result = dict.pop(self, *args)
self.changed()
return result
def popitem(self):
"""Pop arbitrary element and emit change events if item removed.
:return: (key, value)
:raises KeyError: if dict is empty.
"""
result = dict.popitem(self)
self.changed()
return result
class MutableList(Mutable, list):
@ -26,7 +54,13 @@ class MutableList(Mutable, list):
@classmethod
def coerce(cls, key, value):
"""Convert plain lists to MutableList."""
"""Convert plain lists to MutableList.
:param key: string name of the ORM-mapped attribute being set.
:param value: the incoming value.
:return: the method should return the coerced value
:raises ValueError: if the coercion cannot be completed.
"""
if not isinstance(value, MutableList):
if isinstance(value, list):
@ -37,31 +71,126 @@ class MutableList(Mutable, list):
else:
return value
def append(self, value):
"""Append value to end and emit change events.
:param value: element value
"""
list.append(self, value)
self.changed()
def extend(self, iterable):
"""Extend list and emit change events.
:param iterable: iterable on elements
"""
list.extend(self, iterable)
self.changed()
def insert(self, index, value):
"""Insert value before index and emit change events.
:param index: index of next element
:param value: value of inserting element
"""
list.insert(self, index, value)
self.changed()
def pop(self, index=-1):
"""Pop element and emit change events if item removed.
:param index: index of element (default last)
:return: element
:raises IndexError: if list is empty or index is out of range.
"""
result = list.pop(self, index)
self.changed()
return result
def remove(self, value):
"""Remove first occurrence of value.
If occurrence removed, then emit change events.
:param value: value of element
:raises ValueError: if the value is not present.
"""
list.remove(self, value)
self.changed()
def __setitem__(self, key, value):
"""Detect list set events and emit change events."""
"""Detect list set events and emit change events.
:param key: index of element
:param value: new value of element
"""
list.__setitem__(self, key, value)
self.changed()
def __delitem__(self, key):
"""Detect list del events and emit change events."""
"""Detect list del events and emit change events.
:param key: index of element
"""
list.__delitem__(self, key)
self.changed()
def __getstate__(self):
"""Get state as builtin list
:return: current state
"""
return list(self)
def __setstate__(self, state):
"""Detect setstate event and emit change events.
:param state: new object state
"""
self[:] = state
def __setslice__(self, first, last, sequence):
"""Envoke setslice and emit change events.
:param first: first element index
:param last: last (not included) element index
:param sequence: elements that to be inserted
"""
list.__setslice__(self, first, last, sequence)
self.changed()
def __delslice__(self, first, last):
"""Envoke delslice and emit change events.
:param first: first element index
:param last: last (not included) element index
"""
list.__delslice__(self, first, last)
self.changed()
@classmethod
def __copy__(cls, value):
"""use copy via constructor."""
"""Create and return copy of value.
return MutableList(value)
:param value: MutableList object
"""
clone = MutableList()
clone.__setstate__(value)
return clone
def __deepcopy__(self, memo, _deepcopy=copy.deepcopy):
"""recursive copy each element."""
return MutableList(_deepcopy(x, memo) for x in self)
"""Recursive copy each element."""
clone = MutableList()
clone.__setstate__((_deepcopy(x, memo) for x in self))
return clone

View File

@ -21,8 +21,6 @@ from nailgun.db.sqlalchemy.models import NodeBondInterface
from nailgun.db.sqlalchemy.models import NodeNICInterface
from nailgun.db.sqlalchemy.models import Release
from nailgun.db.sqlalchemy.models.mutable import MutableList
from nailgun.test.base import BaseTestCase
from oslo_serialization import jsonutils
@ -247,13 +245,3 @@ class TestNodeInterfacesDbModels(BaseTestCase):
{'test_property': 'test_value'}) # str type cause ValueError
}
self.assertRaises(ValueError, NodeBondInterface, **sample_bond_data)
def test_copy_mutable_list(self):
mlist = MutableList([{"a": 1, "b": 2}])
shallow_copy = copy.copy(mlist)
self.assertEqual(mlist, shallow_copy)
self.assertIs(mlist[0], shallow_copy[0])
deep_copy = copy.deepcopy(mlist)
self.assertEqual(mlist, deep_copy)
self.assertIsNot(mlist[0], deep_copy[0])

View File

@ -0,0 +1,246 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# Licensed 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.
from copy import copy
from copy import deepcopy
from mock import patch
from nailgun.db.sqlalchemy.models.mutable import MutableDict
from nailgun.db.sqlalchemy.models.mutable import MutableList
from nailgun.test.base import BaseUnitTest
@patch('nailgun.db.sqlalchemy.models.mutable.MutableDict.changed')
class TestMutableDict(BaseUnitTest):
def setUp(self):
self.standard = {'1': 1}
self.mutable_dict = MutableDict()
self.mutable_dict.update(self.standard)
def test_pop_existing(self, m_changed):
self.assertEqual(self.mutable_dict.pop('1'), self.standard.pop('1'))
m_changed.assert_called_once_with()
def test_pop_existing_with_default(self, m_changed):
self.assertEqual(self.mutable_dict.pop('1', None),
self.standard.pop('1', None))
m_changed.assert_called_once_with()
def test_pop_not_existing(self, m_changed):
self.assertRaises(KeyError, self.mutable_dict.pop, '2')
self.assertFalse(m_changed.called)
def test_pop_not_existing_with_default(self, m_changed):
self.assertEqual(self.mutable_dict.pop('2', {}),
self.standard.pop('2', {}))
m_changed.assert_called_once_with()
def test_popitem(self, m_changed):
self.assertItemsEqual(self.mutable_dict.popitem(),
self.standard.popitem())
m_changed.assert_called_once_with()
m_changed.reset_mock()
self.assertRaises(KeyError, self.mutable_dict.popitem)
self.assertFalse(m_changed.called)
class TestMutableListBase(BaseUnitTest):
def setUp(self):
self.mutable_list = MutableList()
@patch('sqlalchemy.ext.mutable.Mutable.coerce')
class TestMutableListCoerce(TestMutableListBase):
def setUp(self):
super(TestMutableListCoerce, self).setUp()
def test_coerce_mutable_list(self, m_coerce):
lst = MutableList()
self.assertIsInstance(
self.mutable_list.coerce('key', lst), MutableList)
self.assertFalse(m_coerce.called)
def test_coerce_list(self, m_coerce):
lst = list()
self.assertIsInstance(
self.mutable_list.coerce('key', lst), MutableList)
self.assertFalse(m_coerce.called)
def test_coerce_not_acceptable_object(self, m_coerce):
m_coerce.return_value = None
obj = dict()
self.mutable_list.coerce('key', obj)
m_coerce.assert_called_once_with('key', obj)
@patch('nailgun.db.sqlalchemy.models.mutable.MutableList.changed')
class TestMutableList(TestMutableListBase):
def setUp(self):
super(TestMutableList, self).setUp()
self.standard = ['element1', 'element2']
self.mutable_list.extend(self.standard)
def _check(self, m_changed, method, *args, **kwargs):
getattr(self.mutable_list, method)(*args, **kwargs)
getattr(self.standard, method)(*args, **kwargs)
self._assert_call_list_changed_once(m_changed)
def _assert_list_not_changed(self, m_changed):
self.assertFalse(m_changed.called)
self.assertItemsEqual(self.standard, self.mutable_list)
def _assert_call_list_changed_once(self, m_changed):
m_changed.assert_called_once_with()
self.assertItemsEqual(self.standard, self.mutable_list)
def test_append(self, m_changed):
self._check(m_changed, 'append', 'element')
def test_extend(self, m_changed):
self._check(m_changed, 'extend',
('element3', 'element4', 'element5'))
def test_extend_failure(self, m_changed):
self.assertRaises(TypeError, self.mutable_list.extend, None)
self._assert_list_not_changed(m_changed)
def test_insert(self, m_changed):
self._check(m_changed, 'insert', 1, 'new_element')
def test_insert_failure(self, m_changed):
self.assertRaises(TypeError, self.mutable_list.insert, None, 'element')
self._assert_list_not_changed(m_changed)
def test_pop_default(self, m_changed):
self.assertEqual(self.standard.pop(), self.mutable_list.pop())
self._assert_call_list_changed_once(m_changed)
def test_pop_of_specified_element(self, m_changed):
self.assertEqual(self.standard.pop(0), self.mutable_list.pop(0))
self._assert_call_list_changed_once(m_changed)
def test_pop_wrong_index_type(self, m_changed):
self.assertRaises(TypeError, self.mutable_list.pop, 'str')
self._assert_list_not_changed(m_changed)
def test_pop_out_of_range(self, m_changed):
self.assertRaises(
IndexError, self.mutable_list.pop, len(self.mutable_list))
self._assert_list_not_changed(m_changed)
def test_remove(self, m_changed):
self._check(m_changed, 'remove', 'element1')
def test_remove_failure(self, m_changed):
self.assertRaises(ValueError, self.mutable_list.remove, None)
self._assert_list_not_changed(m_changed)
def test_set_item(self, m_changed):
self.mutable_list[0] = 'new_element'
self.standard[0] = 'new_element'
self._assert_call_list_changed_once(m_changed)
def test_set_item_failure(self, m_changed):
self.assertRaises(
IndexError, self.mutable_list.__setitem__,
len(self.mutable_list), 'element')
self._assert_list_not_changed(m_changed)
def test_del_item(self, m_changed):
del self.mutable_list[0]
del self.standard[0]
self._assert_call_list_changed_once(m_changed)
def test_del_item_failure(self, m_changed):
self.assertRaises(
IndexError, self.mutable_list.__delitem__, len(self.mutable_list))
self._assert_list_not_changed(m_changed)
def test_get_state(self, m_changed):
self.assertIsInstance(self.mutable_list.__getstate__(), list)
self._assert_list_not_changed(m_changed)
def test_set_state(self, m_changed):
self.mutable_list.__setstate__([])
self.standard = []
self._assert_call_list_changed_once(m_changed)
def test_set_state_failure(self, m_changed):
self.assertRaises(TypeError, self.mutable_list.__setstate__, None)
self._assert_list_not_changed(m_changed)
def test_set_slice(self, m_changed):
self._check(m_changed, '__setslice__', 0, 2, ('element',))
def test_set_slice_failure(self, m_changed):
self.assertRaises(
TypeError, self.mutable_list.__setslice__, 0, 5, None)
self._assert_list_not_changed(m_changed)
def test_set_slice_integration(self, m_changed):
self.mutable_list[0:2] = ['new_element']
self.standard[0:2] = ['new_element']
self._assert_call_list_changed_once(m_changed)
m_changed.reset_mock()
self.mutable_list[:] = ["again_new_element"]
self.standard[:] = ["again_new_element"]
self._assert_call_list_changed_once(m_changed)
def test_del_slice(self, m_changed):
self._check(m_changed, '__delslice__', 0, 2)
def test_del_slice_failure(self, m_changed):
self.assertRaises(
TypeError, self.mutable_list.__delslice__, 0, None)
self._assert_list_not_changed(m_changed)
def test_del_slice_integration(self, m_changed):
del self.mutable_list[0:2]
del self.standard[0:2]
self._assert_call_list_changed_once(m_changed)
def test_copy(self, m_changed):
clone = copy(self.mutable_list)
self.assertEqual(clone, self.mutable_list)
m_changed.assert_called_once_with()
def test_deep_copy(self, m_changed):
lst = MutableList(('element1', 'element2'))
self.mutable_list.insert(0, lst)
m_changed.reset_mock()
clone = deepcopy(self.mutable_list)
# changed should calls two times
# - root cloned list (clone)
# - mutable list element in root list (cloned lst)
self.assertEqual(m_changed.call_count, 2)
lst[0] = 'new_element'
self.assertEqual(clone[0][0], 'element1')
self.assertEqual(self.mutable_list[0][0], 'new_element')
@patch('nailgun.db.sqlalchemy.models.mutable.MutableList.changed')
class TestEmptyMutableList(TestMutableListBase):
def setUp(self):
super(TestEmptyMutableList, self).setUp()
def test_pop_default_from_empty_list(self, m_changed):
self.assertRaises(IndexError, self.mutable_list.pop)
self.assertFalse(m_changed.called)
self.assertEqual([], self.mutable_list)