Skip to content

Commit

Permalink
Merge pull request #122 from mikeywaites/feature/memoize
Browse files Browse the repository at this point in the history
Feature/memoize
  • Loading branch information
jackqu7 authored Oct 13, 2016
2 parents dcfc7f1 + ce6e614 commit 4d3c16d
Show file tree
Hide file tree
Showing 15 changed files with 649 additions and 11 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.0-rc5
1.0.0-rc6
2 changes: 2 additions & 0 deletions dependencies/dev.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
Sphinx==1.3.1
sphinx_bootstrap_theme==0.4.5
ipdb
arrow
1 change: 0 additions & 1 deletion dependencies/test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ pytest-cov==1.8.1
pytest_spec==0.2.24
sqlalchemy==1.0.4
mock==1.3.0
ipdb
25 changes: 25 additions & 0 deletions kim/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ def __init__(self, *args, **field_opts):
.format(self.__class__.__name__, e.message)
raise FieldError(msg)

self._old_value = None
self._new_value = None
set_creation_order(self)

def get_error(self, error_type):
Expand Down Expand Up @@ -276,6 +278,29 @@ def serialize(self, mapper_session, **opts):

self.serialize_pipeline(mapper_session, self).run(**opts)

def set_old_value(self, old_value, new_value):
"""Store the existing value for this field. This method assumes the
field handles a scalar type.
"""
self._old_value = old_value if old_value != new_value else None

def set_new_value(self, new_value, old_value):
"""Store the existing value for this field. This method assumes the
field handles a scalar type.
"""
self._new_value = new_value if new_value != old_value else None

def get_changes(self):
"""returns a dict of containing the changes that occurred for this
field.
"""

return {'old_value': self._old_value, 'new_value': self._new_value}

def has_changed(self):

return self._new_value is not None


class String(Field):
""":class:`.String` represents a value that must be valid
Expand Down
16 changes: 16 additions & 0 deletions kim/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ def __init__(self, obj=None, data=None, partial=False, raw=False,
self.raw = raw
self.partial = partial
self.parent = parent
self._changes = {}

@property
def initial_errors(self):
Expand Down Expand Up @@ -542,6 +543,18 @@ def get_mapper_session(self, data, output):

return MapperSession(self, data, output, partial=self.partial)

def set_field_changes(self, field_name, changes):
"""Store a change object for a field at ``field_name`` inside the
mappers changes object.
"""

self._changes[field_name] = changes

def get_changes(self):
"""return all the field change objects for this mapper
"""
return self._changes

def serialize(self, role='__default__', raw=False):
"""Serialize ``self.obj`` into a dict according to the fields
defined on this Mapper.
Expand Down Expand Up @@ -593,6 +606,9 @@ def marshal(self, role='__default__'):
# handle errors from nested mappers.
self.errors[field.name] = e.errors

if field.has_changed():
self.set_field_changes(field.name, field.get_changes())

# Call top level mapper validator for validations involving more
# than one field
try:
Expand Down
16 changes: 15 additions & 1 deletion kim/pipelines/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,11 +265,25 @@ def update_output_to_source(session):
"""

source = session.field.opts.source
old_value = attr_or_key(session.output, source)

session.field.set_new_value(
new_value=session.data, old_value=old_value)

try:

if source == '__self__':
attr_or_key_update(session.output, session.data)
else:
set_attr_or_key(session.output, session.field.opts.source, session.data)
session.field.set_old_value(
old_value=old_value, new_value=session.data)
set_attr_or_key(session.output,
session.field.opts.source,
session.data)

except (TypeError, AttributeError):
raise FieldError('output does not support attribute or '
'key based set operations')

session.field.set_new_value(
new_value=session.data, old_value=old_value)
22 changes: 22 additions & 0 deletions kim/pipelines/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,28 @@ def marshall_collection(session):
except IndexError:
pass

"""
TODO(mike) Kill is_wrapped with Bale Fire.
Create a new session using the data from the array[i] object.
We then call marshal() on the wrapped field passing the new
session. We use the session of the mapper the collection is attached
too. This is weird in the case a collection of Nested fields where
you'd expect to use the Nested fields mapper.
The reason for this is the Field._is_wrapped is used
in :func:`get_data_from_name` to specify that the data has
already been resolved by the collection. This allows
collection to wrap any field. Nested will later
instantiate its mapper using the data passed from collection.
This solution is A) extremely confusing and hard to follow B) weird and
probably quite brittle.
..seealso:
:func: `pipelines.base.get_data_from_name`
:func: `pipelines.nested.marshal_nested`
"""
mapper_session = session.mapper.get_mapper_session(datum, _output)
wrapped_field.marshal(mapper_session, parent_session=session)

Expand Down
4 changes: 3 additions & 1 deletion kim/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ def _set_attr_or_key(obj, name, value):
else:
setattr(obj, name, value)

return value


def attr_or_key(obj, name):
"""attempt to use getattr to access an attribute of obj, if that fails
Expand All @@ -57,7 +59,7 @@ def set_attr_or_key(obj, name, value):
components = name.split('.')
for component in components[:-1]:
obj = _attr_or_key(obj, component)
_set_attr_or_key(obj, components[-1], value)
return _set_attr_or_key(obj, components[-1], value)


def attr_or_key_update(obj, value):
Expand Down
32 changes: 27 additions & 5 deletions tests/test_pipelines/test_base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest

from kim.mapper import Mapper, MapperSession
from kim.field import Field, FieldInvalid, FieldError
from kim.pipelines.base import (
Session,
Expand Down Expand Up @@ -152,9 +153,14 @@ def test_update_output_to_source_with_object():
class MyObject(object):
pass

class MyMapper(Mapper):
pass

output = MyObject()
field = Field(source='source', required=True)
session = Session(field, data, output)
mapper_session = MapperSession(
MyMapper(data=data), data=data, output=output)
session = Session(field, data, output, mapper_session=mapper_session)
session.data = data['source']

update_output_to_source(session)
Expand All @@ -166,12 +172,18 @@ def test_update_output_to_source_with_object_dot_notiation():
class MyObject(object):
pass

class MyMapper(Mapper):
pass

output = MyObject()
output.nested = MyObject()
data = {'name': 'mike'}

field = Field(source='nested.source', required=True)
session = Session(field, {'name': 'mike'}, output)
session.data = 'mike'
mapper_session = MapperSession(
MyMapper(data=data), data=data, output=output)
session = Session(field, data, output, mapper_session=mapper_session)
session.data = data['name']

update_output_to_source(session)
assert output.nested.source == 'mike'
Expand All @@ -185,10 +197,15 @@ def test_update_output_to_source_with_dict():
'nested': {'foo': 'bar'}
}

class MyMapper(Mapper):
pass

output = {}

field = Field(source='source', required=True)
session = Session(field, data, output)
mapper_session = MapperSession(
MyMapper(data=data), data=data, output=output)
session = Session(field, data, output, mapper_session=mapper_session)
session.data = data['source']
update_output_to_source(session)
assert output == {'source': 'mike'}
Expand All @@ -202,8 +219,13 @@ def test_update_output_to_source_invalid_output_type():
'nested': {'foo': 'bar'}
}

class MyMapper(Mapper):
pass

output = 1
field = Field(source='source', required=True)
session = Session(field, data, output)
mapper_session = MapperSession(
MyMapper(data=data), data=data, output=output)
session = Session(field, data, output, mapper_session=mapper_session)
with pytest.raises(FieldError):
update_output_to_source(session)
50 changes: 50 additions & 0 deletions tests/test_pipelines/test_boolean.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,56 @@ def test_boolean_input():
assert output == {'is_active': True}


def test_boolean_memoize_no_existing_value():
"""ensure field sets only the new_value when the field has no
exsiting value.
"""

field = Boolean(name='is_active', required=True)

output = {}
mapper_session = get_mapper_session(
data={'is_active': True, 'email': 'mike@mike.com'},
output=output)

field.marshal(mapper_session)
assert field._old_value is None
assert field._new_value is True


def test_boolean_memoize_no_change():
"""ensure field sets no changes when the field value remains the same
"""

field = Boolean(name='is_active', required=True)

output = {'is_active': False}
mapper_session = get_mapper_session(
data={'is_active': False, 'email': 'mike@mike.com'},
output=output)

field.marshal(mapper_session)
assert field._old_value is None
assert field._new_value is None


def test_boolean_memoize_new_value():
"""ensure field sets both old value and new value when the field has an
existing value and a new value is provided.
"""

field = Boolean(name='is_active', required=True)

output = {'is_active': False}
mapper_session = get_mapper_session(
data={'is_active': True, 'email': 'mike@mike.com'},
output=output)

field.marshal(mapper_session)
assert field._old_value is False
assert field._new_value is True


def test_boolean_input_with_allow_none():

field = Boolean(name='is_active', required=False, allow_none=True)
Expand Down
Loading

0 comments on commit 4d3c16d

Please sign in to comment.