diff --git a/tests/test_nested_attributes.py b/tests/test_nested_attributes.py index 61178ec..2809acb 100644 --- a/tests/test_nested_attributes.py +++ b/tests/test_nested_attributes.py @@ -1,10 +1,11 @@ """Integration tests for nested attributes.""" -# pylint: disable=missing-docstring,no-self-use,attribute-defined-outside-init,no-member,misplaced-comparison-constant - +# pylint: disable=missing-docstring,no-self-use,attribute-defined-outside-init,no-member +# pylint: disable=unused-variable,misplaced-comparison-constant from unittest.mock import patch import logging +import pytest from expecter import expect import yorm @@ -276,21 +277,15 @@ def test_list_in_dict_append_triggers_save(self): """) == top.__mapper__.text -@patch('yorm.settings.fake', True) -class TestAliases: - - @yorm.attr(var4=NestedList3) - @yorm.attr(var5=StatusDictionary) - @yorm.sync("fake/path") - class Sample: - - def __repr__(self): - return "".format(id(self)) +def describe_aliases(): - def setup_method(self, _): - self.sample = self.Sample() + @pytest.fixture + def sample(tmpdir): + cls = type('Sample', (), {}) + path = str(tmpdir.join("sample.yml")) + attrs = dict(var4=NestedList3, var5=StatusDictionary) + return yorm.sync(cls(), path, attrs) - @staticmethod def _log_ref(name, var, ref): logging.info("%s: %r", name, var) logging.info("%s_ref: %r", name, ref) @@ -299,36 +294,36 @@ def _log_ref(name, var, ref): assert id(ref) == id(var) assert ref == var - def test_alias_list(self): - var4_ref = self.sample.var4 - self._log_ref('var4', self.sample.var4, var4_ref) - assert [] == self.sample.var4 + def test_alias_list(sample): + var4_ref = sample.var4 + _log_ref('var4', sample.var4, var4_ref) + assert [] == sample.var4 logging.info("Appending 42 to var4_ref...") var4_ref.append(42) - self._log_ref('var4', self.sample.var4, var4_ref) - assert [42] == self.sample.var4 + _log_ref('var4', sample.var4, var4_ref) + assert [42] == sample.var4 logging.info("Appending 2015 to var4_ref...") var4_ref.append(2015) - assert [42, 2015] == self.sample.var4 + assert [42, 2015] == sample.var4 - def test_alias_dict(self): - var5_ref = self.sample.var5 - self._log_ref('var5', self.sample.var5, var5_ref) - assert {'status': False, 'checked': 0} == self.sample.var5 + def test_alias_dict(sample): + var5_ref = sample.var5 + _log_ref('var5', sample.var5, var5_ref) + assert {'status': False, 'checked': 0} == sample.var5 logging.info("Setting status=True in var5_ref...") var5_ref['status'] = True - self._log_ref('var5', self.sample.var5, var5_ref) - assert {'status': True, 'checked': 0} == self.sample.var5 + _log_ref('var5', sample.var5, var5_ref) + assert {'status': True, 'checked': 0} == sample.var5 logging.info("Setting status=False in var5_ref...") var5_ref['status'] = False - self._log_ref('var5', self.sample.var5, var5_ref) - assert {'status': False, 'checked': 0} == self.sample.var5 + _log_ref('var5', sample.var5, var5_ref) + assert {'status': False, 'checked': 0} == sample.var5 - def test_alias_dict_in_list(self): + def test_alias_dict_in_list(): top = Top() top.nested_list.append(None) ref1 = top.nested_list[0] @@ -338,7 +333,7 @@ def test_alias_dict_in_list(self): assert id(ref2) == id(top.nested_list[0].nested_dict_3) assert id(ref3) == id(top.nested_list[0].nested_dict_3.nested_list_3) - def test_alias_list_in_dict(self): + def test_alias_list_in_dict(): top = Top() logging.info("Updating nested attribute...") top.nested_dict.number = 1 @@ -348,7 +343,7 @@ def test_alias_list_in_dict(self): assert id(ref1) == id(top.nested_dict) assert id(ref2) == id(top.nested_dict.nested_list_2) - def test_custom_init_is_invoked(self): - self.sample.__mapper__.text = "var5:\n checked: 42" + def test_custom_init_is_invoked(sample): + sample.__mapper__.text = "var5:\n checked: 42" with expect.raises(RuntimeError): - print(self.sample.var5) + print(sample.var5) diff --git a/yorm/bases/mappable.py b/yorm/bases/mappable.py index bc70d73..084bc3a 100644 --- a/yorm/bases/mappable.py +++ b/yorm/bases/mappable.py @@ -8,9 +8,6 @@ log = logging.getLogger(__name__) -_LOAD_BEFORE_METHODS = set() -_STORE_AFTER_METHODS = set() - def load_before(method): """Decorator for methods that should load before call.""" @@ -18,8 +15,6 @@ def load_before(method): if getattr(method, '_load_before', False): return method - _LOAD_BEFORE_METHODS.add(method.__name__) - @functools.wraps(method) def wrapped(self, *args, **kwargs): """Decorated method.""" @@ -45,8 +40,6 @@ def save_after(method): if getattr(method, '_save_after', False): return method - _STORE_AFTER_METHODS.add(method.__name__) - @functools.wraps(method) def wrapped(self, *args, **kwargs): """Decorated method.""" @@ -103,27 +96,22 @@ def __setitem__(self, key, value): def __delitem__(self, key): super().__delitem__(key) - @load_before @save_after def append(self, *args, **kwargs): super().append(*args, **kwargs) - @load_before @save_after def extend(self, *args, **kwargs): super().extend(*args, **kwargs) - @load_before @save_after def insert(self, *args, **kwargs): super().insert(*args, **kwargs) - @load_before @save_after def remove(self, *args, **kwargs): super().remove(*args, **kwargs) - @load_before @save_after def pop(self, *args, **kwargs): super().pop(*args, **kwargs) @@ -132,27 +120,45 @@ def pop(self, *args, **kwargs): def clear(self, *args, **kwargs): super().clear(*args, **kwargs) - @load_before @save_after def sort(self, *args, **kwargs): super().sort(*args, **kwargs) - @load_before @save_after def reverse(self, *args, **kwargs): super().reverse(*args, **kwargs) - @load_before @save_after def popitem(self, *args, **kwargs): super().popitem(*args, **kwargs) - @load_before @save_after def update(self, *args, **kwargs): super().update(*args, **kwargs) +_LOAD_BEFORE_METHODS = [ + '__getattribute__', + '__iter__', + '__getitem__', +] +_SAVE_AFTER_METHODS = [ + '__setattr__', + '__setitem__', + '__delitem__', + 'append', + 'extend', + 'insert', + 'remove', + 'pop', + 'clear', + 'sort', + 'reverse', + 'popitem', + 'update', +] + + def patch_methods(instance): log.debug("Patching methods on: %r", instance) cls = instance.__class__ @@ -167,7 +173,7 @@ def patch_methods(instance): setattr(cls, name, modified_method) log.trace("Patched to load before call: %s", name) - for name in _STORE_AFTER_METHODS: + for name in _SAVE_AFTER_METHODS: try: method = getattr(cls, name) except AttributeError: diff --git a/yorm/tests/test_bases_mappable.py b/yorm/tests/test_bases_mappable.py index dfc4d72..feb4fa8 100644 --- a/yorm/tests/test_bases_mappable.py +++ b/yorm/tests/test_bases_mappable.py @@ -252,12 +252,12 @@ def test_delitem(self): def test_append(self): self.sample.append('foo') - assert 2 == self.sample.__mapper__.load.call_count + assert 1 == self.sample.__mapper__.load.call_count assert 1 == self.sample.__mapper__.save.call_count def test_insert(self): self.sample.insert('foo') - assert 2 == self.sample.__mapper__.load.call_count + assert 1 == self.sample.__mapper__.load.call_count assert 1 == self.sample.__mapper__.save.call_count def test_iter(self): diff --git a/yorm/tests/test_decorators.py b/yorm/tests/test_decorators.py index cdb50e7..4309e50 100644 --- a/yorm/tests/test_decorators.py +++ b/yorm/tests/test_decorators.py @@ -29,40 +29,47 @@ def to_data(cls, _): return None -@patch('yorm.diskutils.write', Mock()) -@patch('yorm.diskutils.stamp', Mock()) -@patch('yorm.diskutils.read', Mock(return_value="")) -class TestSyncObject: - """Unit tests for the `sync_object` function.""" +def describe_sync(): - class Sample: - """Sample class.""" + def describe_object(): - def test_no_attrs(self): - """Verify mapping can be enabled with no attributes.""" - sample = decorators.sync(self.Sample(), "sample.yml") - assert "sample.yml" == sample.__mapper__.path - assert {} == sample.__mapper__.attrs + @pytest.fixture + def instance(): + cls = type('Sample', (), {}) + instance = cls() + return instance - def test_with_attrs(self): - """Verify mapping can be enabled with with attributes.""" - attrs = {'var1': MockConverter} - sample = decorators.sync(self.Sample(), "sample.yml", attrs) - assert "sample.yml" == sample.__mapper__.path - assert {'var1': MockConverter} == sample.__mapper__.attrs + @pytest.fixture + def path(tmpdir): + tmpdir.chdir() + return "sample.yml" - def test_multiple(self): - """Verify mapping cannot be enabled twice.""" - sample = decorators.sync(self.Sample(), "sample.yml") - with pytest.raises(TypeError): - decorators.sync(sample, "sample.yml") + def with_no_attrs(instance, path): + sample = decorators.sync(instance, path) - @patch('yorm.diskutils.exists', Mock(return_value=True)) - def test_init_existing(self): - """Verify an existing file is read.""" - with patch('yorm.diskutils.read', Mock(return_value="abc: 123")): - sample = decorators.sync(self.Sample(), "s.yml", auto_track=True) - assert 123 == sample.abc + expect(sample.__mapper__.path) == "sample.yml" + expect(sample.__mapper__.attrs) == {} + + def with_attrs(instance, path): + attrs = {'var1': MockConverter} + sample = decorators.sync(instance, path, attrs) + + expect(sample.__mapper__.path) == "sample.yml" + expect(sample.__mapper__.attrs) == {'var1': MockConverter} + + def cannot_be_called_twice(instance, path): + sample = decorators.sync(instance, path) + + with pytest.raises(TypeError): + decorators.sync(instance, path) + + @patch('yorm.diskutils.exists', Mock(return_value=True)) + @patch('yorm.diskutils.read', Mock(return_value="abc: 123")) + @patch('yorm.diskutils.stamp', Mock()) + def reads_existing_files(instance, path): + sample = decorators.sync(instance, path, auto_track=True) + + expect(sample.abc) == 123 @patch('yorm.diskutils.write', Mock()) @@ -71,9 +78,16 @@ def test_init_existing(self): class TestSyncInstances: """Unit tests for the `sync_instances` decorator.""" - @decorators.sync("sample.yml", auto_track=True) + @decorators.sync("sample.yml") class SampleDecorated: - """Sample decorated class using a single path.""" + """Sample decorated class.""" + + def __repr__(self): + return "".format(id(self)) + + @decorators.sync("sample.yml", auto_track=True) + class SampleDecoratedAutoTrack: + """Sample decorated class with automatic attribute tracking.""" def __repr__(self): return "".format(id(self)) @@ -124,8 +138,9 @@ class SampleDecoratedWithAttributes: def test_no_attrs(self): """Verify mapping can be enabled with no attributes.""" sample = self.SampleDecorated() - assert "sample.yml" == sample.__mapper__.path - assert {} == sample.__mapper__.attrs + + expect(sample.__mapper__.path) == "sample.yml" + expect(sample.__mapper__.attrs) == {} def test_with_attrs(self): """Verify mapping can be enabled with with attributes.""" @@ -137,7 +152,7 @@ def test_with_attrs(self): def test_init_existing(self): """Verify an existing file is read.""" with patch('yorm.diskutils.read', Mock(return_value="abc: 123")): - sample = self.SampleDecorated() + sample = self.SampleDecoratedAutoTrack() assert 123 == sample.abc @patch('uuid.uuid4', Mock(return_value=Mock(hex='abc123')))