Skip to content
This repository has been archived by the owner on Oct 3, 2019. It is now read-only.

Commit

Permalink
Merge c9637ba into fa46d43
Browse files Browse the repository at this point in the history
  • Loading branch information
jacebrowning committed Oct 22, 2016
2 parents fa46d43 + c9637ba commit 89b1f01
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 88 deletions.
65 changes: 30 additions & 35 deletions tests/test_nested_attributes.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 "<sample {}>".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)
Expand All @@ -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]
Expand All @@ -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
Expand All @@ -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)
40 changes: 23 additions & 17 deletions yorm/bases/mappable.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,13 @@

log = logging.getLogger(__name__)

_LOAD_BEFORE_METHODS = set()
_STORE_AFTER_METHODS = set()


def load_before(method):
"""Decorator for methods that should load before call."""

if getattr(method, '_load_before', False):
return method

_LOAD_BEFORE_METHODS.add(method.__name__)

@functools.wraps(method)
def wrapped(self, *args, **kwargs):
"""Decorated method."""
Expand All @@ -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."""
Expand Down Expand Up @@ -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)
Expand All @@ -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__
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions yorm/tests/test_bases_mappable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
83 changes: 49 additions & 34 deletions yorm/tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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 "<decorated {}>".format(id(self))

@decorators.sync("sample.yml", auto_track=True)
class SampleDecoratedAutoTrack:
"""Sample decorated class with automatic attribute tracking."""

def __repr__(self):
return "<decorated {}>".format(id(self))
Expand Down Expand Up @@ -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."""
Expand All @@ -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')))
Expand Down

0 comments on commit 89b1f01

Please sign in to comment.