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

Commit

Permalink
Merge pull request #104 from jacebrowning/strict-option
Browse files Browse the repository at this point in the history
Disable attribute inference by default
  • Loading branch information
jacebrowning committed Feb 23, 2016
2 parents e5f2247 + 3294478 commit a3345f2
Show file tree
Hide file tree
Showing 19 changed files with 193 additions and 173 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Added preliminary support for JSON serialization (@pr0xmeh).
- Renamed `yorm.converters` to `yorm.types`.
- Now maintaining the signature on mapped objects.
- Disabled attribute inference unless `strict=False`.

## 0.5 (2015/09/25)

Expand Down
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
2 changes: 1 addition & 1 deletion tests/test_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def test_from_top_constants():

def test_from_top_clases():
from yorm import Mappable # base class for mapped objects
from yorm import Converter, Convertible # base class for types
from yorm import Converter, Container # base class for types


def test_from_top_decorators():
Expand Down
2 changes: 1 addition & 1 deletion yorm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys

__project__ = 'YORM'
__version__ = '0.6.dev5'
__version__ = '0.6.dev7'

VERSION = __project__ + '-' + __version__

Expand Down
4 changes: 1 addition & 3 deletions yorm/bases/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""Base classes for mapping and conversion."""

from .container import Container
from .converter import Converter
from .mappable import Mappable
from .convertible import Convertible
from .converter import Converter, Container
7 changes: 0 additions & 7 deletions yorm/bases/container.py

This file was deleted.

38 changes: 31 additions & 7 deletions yorm/bases/converter.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,51 @@
"""Base classes for converters."""
"""Converter classes."""

import abc
from abc import ABCMeta, abstractclassmethod, abstractmethod

from .. import common
from . import Mappable


log = common.logger(__name__)


class Converter(metaclass=abc.ABCMeta):
"""Base class for immutable attribute converters."""
class Converter(metaclass=ABCMeta):
"""Base class for attribute converters."""

@abc.abstractclassmethod
@abstractclassmethod
def create_default(cls):
"""Create a default value for an attribute."""
raise NotImplementedError(common.OVERRIDE_MESSAGE)

@abc.abstractclassmethod
@abstractclassmethod
def to_value(cls, data):
"""Convert loaded data to an attribute's value."""
raise NotImplementedError(common.OVERRIDE_MESSAGE)

@abc.abstractclassmethod
@abstractclassmethod
def to_data(cls, value):
"""Convert an attribute to data optimized for dumping."""
raise NotImplementedError(common.OVERRIDE_MESSAGE)


class Container(Mappable, Converter, metaclass=ABCMeta):
"""Base class for mutable attribute converters."""

@classmethod
def create_default(cls):
return cls.__new__(cls)

@classmethod
def to_value(cls, data):
value = cls.create_default()
value.update_value(data, strict=False)
return value

@abstractmethod
def update_value(self, data, strict): # pragma: no cover (abstract method)
"""Update the attribute's value from loaded data."""
raise NotImplementedError(common.OVERRIDE_MESSAGE)

def format_data(self):
"""Format the attribute to data optimized for dumping."""
return self.to_data(self)
32 changes: 0 additions & 32 deletions yorm/bases/convertible.py

This file was deleted.

8 changes: 4 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.store_after_fetch:
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 All @@ -70,6 +69,7 @@ def _private_call(method, args, prefix='_'):
return False


# TODO: move these methods inside of `Container`
class Mappable(metaclass=abc.ABCMeta):
"""Base class for objects with attributes mapped to file."""

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
80 changes: 33 additions & 47 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,15 +80,16 @@ 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)
self.deleted = False
self.store_after_fetch = False
self._activity = False
self._timestamp = 0
self._fake = ""
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,17 +177,19 @@ 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)
if all((isinstance(attr, converter),
issubclass(converter, Container))):
attr.update_value(data)
attr.update_value(data, strict=self.strict)
else:
attr = converter.to_value(data)
setattr(self._obj, name, attr)
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 All @@ -248,7 +247,7 @@ def store(self):

# Set meta attributes
self.modified = True
self.auto_store = self.auto
self.store_after_fetch = self.auto

def delete(self):
"""Delete the object's file from the file system."""
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)

0 comments on commit a3345f2

Please sign in to comment.