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

Commit

Permalink
Merge branch 'develop' into feature/referenced-attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
jacebrowning committed Apr 18, 2015
2 parents e3e2f0c + 52bcf03 commit b771be3
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 121 deletions.
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,14 @@ pep8: depends-ci

.PHONY: pep257
pep257: depends-ci
# D102: docstring missing (checked by PyLint)
# D102: Docstring missing (checked by PyLint)
# D202: No blank lines allowed *after* function docstring
$(PEP257) $(PACKAGE) --ignore=D102,D202

.PHONY: pylint
pylint: depends-ci
$(PYLINT) $(PACKAGE) --rcfile=.pylintrc
# R0912: Too many branches (checked by IDE)
$(PYLINT) $(PACKAGE) --rcfile=.pylintrc --disable=R0912

.PHONY: fix
fix: depends-dev
Expand All @@ -164,7 +165,7 @@ fix: depends-dev

PYTEST_CORE_OPTS := --doctest-modules
PYTEST_COV_OPTS := --cov=$(PACKAGE) --cov-report=term-missing --cov-report=html
PYTEST_CAPTURELOG_OPTS := --log-format="%(name)-25s %(funcName)-20s %(lineno)3d %(levelname)s: %(message)s"
PYTEST_CAPTURELOG_OPTS := --log-format="%(name)-26s %(funcName)-20s %(lineno)3d %(levelname)s: %(message)s"
PYTEST_OPTS := $(PYTEST_CORE_OPTS) $(PYTEST_COV_OPTS) $(PYTEST_CAPTURELOG_OPTS)

.PHONY: test
Expand Down
2 changes: 2 additions & 0 deletions yorm/base/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
"""Base classes for mapping and conversion."""

MESSAGE = "method must be implemented in subclasses"
30 changes: 30 additions & 0 deletions yorm/base/container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Base classes for containers."""

import abc

from .. import common
from . import MESSAGE
from .convertible import Convertible
from .mappable import Mappable


log = common.logger(__name__)


class Container(Convertible, Mappable):

"""Base class for mutable types."""

@abc.abstractclassmethod
def default(cls):
"""Create an empty container."""
raise NotImplementedError(MESSAGE)

@abc.abstractmethod
def apply(self, data): # pylint: disable=E0213
"""Update the container's values with the loaded data."""
raise NotImplementedError(MESSAGE)

def format(self): # pylint: disable=E0213
"""Convert the container's values to data optimized for dumping."""
return self.to_data(self)
20 changes: 9 additions & 11 deletions yorm/base/convertible.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,22 @@
import abc

from .. import common
from .mappable import Mappable
from . import MESSAGE


log = common.logger(__name__)


class Convertible(Mappable, metaclass=abc.ABCMeta):
class Convertible(metaclass=abc.ABCMeta):

"""Base class for attribute converters."""

TYPE = None # type for inferred converters (set in subclasses)
DEFAULT = None # default value for conversion (set in subclasses)

@abc.abstractclassmethod
def to_value(cls, obj): # pylint: disable=E0213
"""Convert the loaded value back to its original attribute type."""
raise NotImplementedError("method must be implemented in subclasses")
def to_value(cls, obj):
"""Convert the loaded data back into the attribute's type."""
raise NotImplementedError(MESSAGE)

@abc.abstractclassmethod
def to_data(cls, obj): # pylint: disable=E0213
"""Convert the attribute's value for optimal dumping to YAML."""
raise NotImplementedError("method must be implemented in subclasses")
def to_data(cls, obj):
"""Convert the attribute's value to data optimized for dumping."""
raise NotImplementedError(MESSAGE)
2 changes: 1 addition & 1 deletion yorm/base/mappable.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Base class for mapped objects."""
"""Base classes for mapping."""

import abc
import functools
Expand Down
179 changes: 100 additions & 79 deletions yorm/converters/containers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""Converter classes for abstract container types."""

from .. import common
from ..base.convertible import Convertible
from ..base.container import Container
from . import standard

log = common.logger(__name__)


class Dictionary(Convertible, dict):
class Dictionary(Container, dict):

"""Base class for a dictionary of attribute converters."""

Expand All @@ -21,22 +21,21 @@ def default(cls):
return cls.__new__(cls)

@classmethod
def to_value(cls, obj): # pylint: disable=E0213
"""Convert all loaded values back to its original attribute types."""
def to_value(cls, data):
value = cls.default()

# Convert object attributes to a dictionary
attrs = common.ATTRS[cls].copy()
if isinstance(obj, cls):
if isinstance(data, cls):
dictionary = {}
for k, v in obj.items():
for k, v in data.items():
if k in attrs:
dictionary[k] = v
for k, v in obj.__dict__.items():
for k, v in data.__dict__.items():
if k in attrs:
dictionary[k] = v
else:
dictionary = cls.to_dict(obj)
dictionary = to_dict(data)

# Map object attributes to converters
for name, data in dictionary.items():
Expand All @@ -45,19 +44,28 @@ def to_value(cls, obj): # pylint: disable=E0213
except KeyError:
converter = standard.match(name, data, nested=True)
common.ATTRS[cls][name] = converter
value[name] = converter.to_value(data)

# Convert the loaded data
if issubclass(converter, Container):
container = converter()
container.apply(data)
value[name] = container
else:
value[name] = converter.to_value(data)

# Create default values for unmapped converters
for name, converter in attrs.items():
value[name] = converter.to_value(None)
if issubclass(converter, Container):
value[name] = converter()
else:
value[name] = converter.to_value(None)
log.warn("added missing nested key '%s'...", name)

return value

@classmethod
def to_data(cls, obj): # pylint: disable=E0213
"""Convert all attribute values for optimal dumping to YAML."""
value = cls.to_value(obj)
def to_data(cls, value):
value = cls.to_value(value)

data = {}

Expand All @@ -66,42 +74,23 @@ def to_data(cls, obj): # pylint: disable=E0213

return data

@staticmethod
def to_dict(obj):
"""Convert a dictionary-like object to a dictionary.
>>> Dictionary.to_dict({'key': 42})
{'key': 42}
>>> Dictionary.to_dict("key=42")
{'key': '42'}
>>> Dictionary.to_dict("key")
{'key': None}
>>> Dictionary.to_dict(None)
{}
"""
if isinstance(obj, dict):
return obj
elif isinstance(obj, str):
text = obj.strip()
parts = text.split('=')
if len(parts) == 2:
return {parts[0]: parts[1]}
else:
return {text: None}
else:
return {}
def apply(self, data):
value = self.to_value(data)
self.clear()
self.update(value)


class List(Convertible, list):
class List(Container, list):

"""Base class for a homogeneous list of attribute converters."""

ALL = 'all'

@common.classproperty
def item_type(cls): # pylint: disable=E0213
"""Get the converter class for all items."""
return common.ATTRS[cls].get(cls.ALL)

@classmethod
def default(cls):
"""Create an uninitialized object."""
Expand All @@ -112,59 +101,91 @@ def default(cls):

return cls.__new__(cls)

@common.classproperty
def item_type(cls): # pylint: disable=E0213
"""Get the converter class for all items."""
return common.ATTRS[cls].get(cls.ALL)

@classmethod
def to_value(cls, obj): # pylint: disable=E0213
"""Convert all loaded values back to the original attribute type."""
def to_value(cls, data):
value = cls.default()

for item in cls.to_list(obj):
value.append(cls.item_type.to_value(item))
for item in to_list(data):
if issubclass(cls.item_type, Container):
container = cls.item_type() # pylint: disable=E1120
container.apply(item)
value.append(container)
else:
value.append(cls.item_type.to_value(item))

return value

@classmethod
def to_data(cls, obj): # pylint: disable=E0213
"""Convert all attribute values for optimal dumping to YAML."""
value = cls.to_value(obj)
def to_data(cls, value):
value = cls.to_value(value)

data = []

for item in value:
data.append(cls.item_type.to_data(item))
if value:
for item in value:
data.append(cls.item_type.to_data(item))

return data

@staticmethod
def to_list(obj):
"""Convert a list-like object to a list.
def apply(self, data):
value = self.to_value(data)
self[:] = value[:]

>>> List.to_list([1, 2, 3])
[1, 2, 3]

>>> List.to_list("a,b,c")
['a', 'b', 'c']
def to_dict(obj):
"""Convert a dictionary-like object to a dictionary.
>>> List.to_list("item")
['item']
>>> to_dict({'key': 42})
{'key': 42}
>>> List.to_list(None)
[]
>>> to_dict("key=42")
{'key': '42'}
"""
if isinstance(obj, list):
return obj
elif isinstance(obj, str):
text = obj.strip()
if ',' in text and ' ' not in text:
return text.split(',')
else:
return text.split()
elif obj is not None:
return [obj]
>>> to_dict("key")
{'key': None}
>>> to_dict(None)
{}
"""
if isinstance(obj, dict):
return obj
elif isinstance(obj, str):
text = obj.strip()
parts = text.split('=')
if len(parts) == 2:
return {parts[0]: parts[1]}
else:
return {text: None}
else:
return {}


def to_list(obj):
"""Convert a list-like object to a list.
>>> to_list([1, 2, 3])
[1, 2, 3]
>>> to_list("a,b,c")
['a', 'b', 'c']
>>> to_list("item")
['item']
>>> to_list(None)
[]
"""
if isinstance(obj, list):
return obj
elif isinstance(obj, str):
text = obj.strip()
if ',' in text and ' ' not in text:
return text.split(',')
else:
return []
return text.split()
elif obj is not None:
return [obj]
else:
return []
5 changes: 4 additions & 1 deletion yorm/converters/standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@

class Object(Convertible): # pylint: disable=W0223

"""Base class for standard types (mapped directly to YAML)."""
"""Base class for immutable types."""

TYPE = None # type for inferred converters (set in subclasses)
DEFAULT = None # default value for conversion (set in subclasses)

@classmethod
def to_value(cls, obj):
Expand Down

0 comments on commit b771be3

Please sign in to comment.