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

Commit

Permalink
Merge fad419d into e5f2247
Browse files Browse the repository at this point in the history
  • Loading branch information
jacebrowning committed Feb 23, 2016
2 parents e5f2247 + fad419d commit 20b7a34
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 59 deletions.
4 changes: 2 additions & 2 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def __repr__(self):
return "<auto off {}>".format(id(self))


@yorm.sync("sample.yml")
@yorm.sync("sample.yml", strict=False)
class SampleEmptyDecorated:
"""Sample class using standard attribute types."""

Expand Down Expand Up @@ -348,7 +348,7 @@ def test_nesting(self, tmpdir):
_sample = SampleNested()
attrs = {'count': Integer,
'results': StatusDictionaryList}
sample = yorm.sync(_sample, "sample.yml", attrs)
sample = yorm.sync(_sample, "sample.yml", attrs, strict=False)

# check defaults
assert 0 == sample.count
Expand Down
7 changes: 3 additions & 4 deletions yorm/bases/mappable.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import functools

from .. import common
from ..mapper import get_mapper


log = common.logger(__name__)
Expand All @@ -22,11 +21,11 @@ def fetch_before(method):
def wrapped(self, *args, **kwargs):
"""Decorated method."""
if not _private_call(method, args):
mapper = get_mapper(self)
mapper = common.get_mapper(self)
if mapper and mapper.modified:
log.debug("Fetching before call: %s", method.__name__)
mapper.fetch()
if mapper.auto_store:
if mapper.auto_store: # TODO: determine why this needs a separate variable
mapper.store()
mapper.modified = False

Expand All @@ -49,7 +48,7 @@ def wrapped(self, *args, **kwargs):
result = method(self, *args, **kwargs)

if not _private_call(method, args):
mapper = get_mapper(self)
mapper = common.get_mapper(self)
if mapper and mapper.auto:
log.debug("Storing after call: %s", method.__name__)
mapper.store()
Expand Down
18 changes: 18 additions & 0 deletions yorm/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

# CONSTANTS ####################################################################

MAPPER = '__mapper__'

PRINT_VERBOSITY = 0 # minimum verbosity to using `print`
STR_VERBOSITY = 3 # minimum verbosity to use verbose `__str__`
Expand Down Expand Up @@ -49,3 +50,20 @@ def __init__(self, getter):

def __get__(self, instance, owner):
return self.getter(owner)


# FUNCTIONS ####################################################################


def get_mapper(obj):
"""Get the `Mapper` instance attached to an object."""
try:
return object.__getattribute__(obj, MAPPER)
except AttributeError:
return None


def set_mapper(obj, mapper):
"""Attach a `Mapper` instance to an object."""
setattr(obj, MAPPER, mapper)
return mapper
74 changes: 30 additions & 44 deletions yorm/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,12 @@
import functools
from pprint import pformat

from . import common, diskutils, exceptions, settings
from . import common, diskutils, exceptions, types, settings
from .bases import Container

MAPPER = '__mapper__'

log = common.logger(__name__)


def get_mapper(obj):
"""Get `Mapper` instance attached to an object."""
try:
return object.__getattribute__(obj, MAPPER)
except AttributeError:
return None


def set_mapper(obj, path, attrs, auto=True):
"""Create and attach a `Mapper` instance to an object."""
mapper = Mapper(obj, path, attrs, auto=auto)
setattr(obj, MAPPER, mapper)
return mapper


def file_required(create=False):
"""Decorator for methods that require the file to exist.
Expand Down Expand Up @@ -97,11 +80,12 @@ class Mapper:
"""

def __init__(self, obj, path, attrs, auto=True):
def __init__(self, obj, path, attrs, *, auto=True, strict=True):
self._obj = obj
self.path = path
self.attrs = attrs
self.auto = auto
self.strict = strict

self.auto_store = False
self.exists = diskutils.exists(self.path)
Expand Down Expand Up @@ -159,7 +143,7 @@ def text(self, text):
self._fake = text
else:
self._write(text)
log.trace("Text wrote: \n%s", text[:-1])
log.trace("Text wrote: \n%s", text.rstrip())
self.modified = True

def create(self):
Expand Down Expand Up @@ -193,11 +177,13 @@ def fetch(self):
try:
converter = self.attrs[name]
except KeyError:
# TODO: determine if runtime import is the best way to avoid
# cyclic import
from .types import match
converter = match(name, data)
self.attrs[name] = converter
if self.strict:
msg = "Ignored unknown file attribute: %s = %r"
log.warning(msg, name, data)
continue
else:
converter = types.match(name, data)
self.attrs[name] = converter

# Convert the loaded attribute
attr = getattr(self._obj, name, None)
Expand All @@ -214,13 +200,26 @@ def fetch(self):
for name, converter in attrs2.items():
if not hasattr(self._obj, name):
value = converter.to_value(None)
msg = "Fetched default value for missing attribute: %s = %r"
msg = "Default value for missing object attribute: %s = %r"
log.warning(msg, name, value)
setattr(self._obj, name, value)

# Set meta attributes
self.modified = False

def _remap(self, obj, root):
"""Restore mapping on nested attributes."""
if isinstance(obj, Container):
common.set_mapper(obj, root)

if isinstance(obj, dict):
for obj2 in obj.values():
self._remap(obj2, root)
else:
assert isinstance(obj, list)
for obj2 in obj:
self._remap(obj2, root)

@file_required(create=True)
@prevent_recursion
def store(self):
Expand All @@ -233,11 +232,11 @@ def store(self):
try:
value = getattr(self._obj, name)
except AttributeError:
value = None
msg = "Storing default data for missing attribute '%s'..."
log.warning(msg, name)

data2 = converter.to_data(value)
data2 = converter.to_data(None)
msg = "Default data for missing object attribute: %s = %r"
log.warning(msg, name, data2)
else:
data2 = converter.to_data(value)

log.trace("Data to store: %s = %r", name, data2)
data[name] = data2
Expand Down Expand Up @@ -277,16 +276,3 @@ def _write(self, text):
self._fake = text
else:
diskutils.write(text, self.path)

def _remap(self, obj, root):
"""Restore mapping on nested attributes."""
if isinstance(obj, Container):
setattr(obj, MAPPER, root)

if isinstance(obj, dict):
for obj2 in obj.values():
self._remap(obj2, root)
else:
assert isinstance(obj, list)
for obj2 in obj:
self._remap(obj2, root)
2 changes: 2 additions & 0 deletions yorm/test/test_bases_mappable.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ def test_error_unexpected_yaml(self):

def test_new(self):
"""Verify new attributes are added to the object."""
self.sample.__mapper__.strict = False
text = strip("""
new: 42
""")
Expand All @@ -182,6 +183,7 @@ def test_new(self):

def test_new_unknown(self):
"""Verify an exception is raised on new attributes w/ unknown types"""
self.sample.__mapper__.strict = False
text = strip("""
new: !!timestamp 2001-12-15T02:59:43.1Z
""")
Expand Down
24 changes: 23 additions & 1 deletion yorm/test/test_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def mapper(tmpdir, obj, attrs, request):
yorm.settings.fake = True
elif "real" in path:
tmpdir.chdir()
yield Mapper(obj, path, attrs)
yield Mapper(obj, path, attrs, strict=True)
yorm.settings.fake = backup


Expand Down Expand Up @@ -99,6 +99,28 @@ def it_adds_missing_attributes(obj, mapper):
expect(obj.var2) == 0
expect(obj.var3) == 0

def it_ignores_new_attributes(obj, mapper):
mapper.create()
mapper.text = "var4: foo"

mapper.fetch()
with expect.raises(AttributeError):
print(obj.var4)

def it_infers_types_on_new_attributes_when_not_strict(obj, mapper):
mapper.strict = False
mapper.create()
mapper.text = "var4: foo"

mapper.fetch()
expect(obj.var4) == "foo"

obj.var4 = 42
mapper.store()

mapper.fetch()
expect(obj.var4) == "42"

def it_raises_an_exception_after_delete(mapper):
mapper.delete()

Expand Down
4 changes: 2 additions & 2 deletions yorm/test/test_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def test_multiple(self):
def test_init_existing(self):
"""Verify an existing file is read."""
with patch('yorm.diskutils.read', Mock(return_value="abc: 123")):
sample = utilities.sync(self.Sample(), "sample.yml")
sample = utilities.sync(self.Sample(), "sample.yml", strict=False)
assert 123 == sample.abc

@patch('yorm.diskutils.exists', Mock(return_value=False))
Expand All @@ -124,7 +124,7 @@ class TestSyncInstances:

"""Unit tests for the `sync_instances` decorator."""

@utilities.sync("sample.yml")
@utilities.sync("sample.yml", strict=False)
class SampleDecorated:

"""Sample decorated class using a single path."""
Expand Down
14 changes: 8 additions & 6 deletions yorm/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from . import common, exceptions
from .bases.mappable import patch_methods
from .mapper import get_mapper, set_mapper
from .mapper import Mapper

log = common.logger(__name__)

Expand All @@ -28,14 +28,15 @@ def sync(*args, **kwargs):
return sync_object(*args, **kwargs)


def sync_object(instance, path, attrs=None, existing=None, auto=True):
def sync_object(instance, path, attrs=None, existing=None, **kwargs):
"""Enable YAML mapping on an object.
:param instance: object to patch with YAML mapping behavior
:param path: file path for dump/load
:param attrs: dictionary of attribute names mapped to converter classes
:param existing: indicate if file is expected to exist or not
:param auto: automatically store attributes to file
:param strict: ignore new attributes in files
"""
log.info("Mapping %r to %s...", instance, path)
Expand All @@ -44,7 +45,8 @@ def sync_object(instance, path, attrs=None, existing=None, auto=True):
patch_methods(instance)

attrs = attrs or common.attrs[instance.__class__]
mapper = set_mapper(instance, path, attrs, auto=auto)
mapper = Mapper(instance, path, attrs, **kwargs)
common.set_mapper(instance, mapper)
_check_existance(mapper, existing)

if mapper.auto:
Expand Down Expand Up @@ -148,7 +150,7 @@ def update_object(instance, existing=True, force=True):
log.info("Manually updating %r from file...", instance)
_check_base(instance, mappable=True)

mapper = get_mapper(instance)
mapper = common.get_mapper(instance)
_check_existance(mapper, existing)

if mapper.modified or force:
Expand All @@ -166,7 +168,7 @@ def update_file(instance, existing=None, force=True):
log.info("Manually saving %r to file...", instance)
_check_base(instance, mappable=True)

mapper = get_mapper(instance)
mapper = common.get_mapper(instance)
_check_existance(mapper, existing)

if mapper.auto or force:
Expand All @@ -177,7 +179,7 @@ def update_file(instance, existing=None, force=True):

def synced(obj):
"""Determine if an object is already mapped to a file."""
return bool(get_mapper(obj))
return bool(common.get_mapper(obj))


def _check_base(obj, mappable=True):
Expand Down

0 comments on commit 20b7a34

Please sign in to comment.