Skip to content

Commit

Permalink
Merge pull request #129 from mikeywaites/feature/deffered-roles
Browse files Browse the repository at this point in the history
deferred roles
  • Loading branch information
jackqu7 authored Feb 6, 2017
2 parents 58e7069 + 20407c4 commit 3ec3628
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 18 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.0-rc8
1.0.0-rc9
2 changes: 1 addition & 1 deletion dependencies/test.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pytest==2.7.3
pytest==2.8
pytest-cov==1.8.1
pytest_spec==0.2.24
sqlalchemy==1.0.4
Expand Down
55 changes: 45 additions & 10 deletions kim/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# This module is part of Kim and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php

import warnings
import weakref
import six
import inspect
Expand All @@ -13,7 +14,7 @@

from .exception import MapperError, MappingInvalid
from .field import Field, FieldError, FieldInvalid
from .role import whitelist, Role
from .role import whitelist, blacklist, Role
from .utils import recursive_defaultdict, attr_or_key
from .pipelines.base import Pipe

Expand Down Expand Up @@ -407,34 +408,66 @@ def _get_obj(self):
else:
return self._get_mapper_type()()

def _get_role(self, name_or_role):
def _get_role(self, name_or_role, deferred_role=None):
"""Resolve a string to a role and check it exists, or check a
directly passed role is a Role instance and return it.
You may also affect the fields returned from a role at read time
using ``deferred_role``. deferred_role is used to provide the intersection
between the role specified at ``name_or_role`` and the ``deferred_role``.
class FooMapper(Mapper):
__type__ = dict
name = field.String()
id = field.String()
secret = field.String()
__roles__ = {
'overview': whitelist('id', 'name'),
}
mapper._get_role('overview', deferred_role=whitelist('id'))
Deferred roles can be used for things like allowing end users to provide a list
of fields they want back from your API but only if they appear in a role you've
specified.
:param deferred_role: provide a role containing fields to dynamically change the
permitted fields for the role specified in ``name_or_role``
:param name_or_role: role name as a string or a Role instance
:raises: :class:`.MapperError`
:returns: Role instance
"""
if isinstance(name_or_role, six.string_types):
try:
return self.roles[name_or_role]
role = self.roles[name_or_role]
except KeyError:
raise MapperError("Role '%s' not found on %s" % (
name_or_role, self.__class__.__name__))
elif isinstance(name_or_role, Role):
return name_or_role
role = name_or_role
else:
raise MapperError('role must be string or Role instance, got %s'
% type(name_or_role))

# If deferred_role is not None, return the intersection of the
# role and the deffered_role
if deferred_role is not None:
if not isinstance(deferred_role, Role):
raise MapperError('deferred_role must be instance of Role')

return role & deferred_role
else:
return role

def _field_in_data(self, field):
for key in self.data.keys():
if key == field.name:
return True
return False

def _get_fields(self, name_or_role, for_marshal=False):
def _get_fields(self, name_or_role, deferred_role=None, for_marshal=False):
"""Returns a list of :class:`.Field` instances providing they are
registered in the specified :class:`Role`.
Expand All @@ -445,7 +478,7 @@ def _get_fields(self, name_or_role, for_marshal=False):
:returns: list of :class:`.Field`
"""

role = self._get_role(name_or_role)
role = self._get_role(name_or_role, deferred_role=deferred_role)

fields = [f for name, f in six.iteritems(self.fields) if name in role]

Expand Down Expand Up @@ -542,7 +575,7 @@ def get_mapper_session(self, data, output):

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

def serialize(self, role='__default__', raw=False):
def serialize(self, role='__default__', raw=False, deferred_role=None):
"""Serialize ``self.obj`` into a dict according to the fields
defined on this Mapper.
Expand All @@ -562,7 +595,7 @@ def serialize(self, role='__default__', raw=False):
else:
data = self._get_obj()

for field in self._get_fields(role):
for field in self._get_fields(role, deferred_role=deferred_role):
field.serialize(self.get_mapper_session(data, output))

return output
Expand Down Expand Up @@ -734,7 +767,7 @@ def get_mapper(self, data=None, obj=None):
})
return self.mapper(**self.mapper_params)

def serialize(self, objs, role='__default__'):
def serialize(self, objs, role='__default__', deferred_role=None):
"""Serializes each item in ``objs`` creating a new mapper each time.
:param objs: iterable of objects to serialize
Expand All @@ -745,7 +778,9 @@ def serialize(self, objs, role='__default__'):

output = [] # TODO should this be user defined?
for obj in objs:
output.append(self.get_mapper(obj=obj).serialize(role=role))
output.append(self.get_mapper(obj=obj).serialize(
role=role,
deferred_role=deferred_role))

return output

Expand Down
46 changes: 46 additions & 0 deletions kim/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,52 @@ def __or__(self, other):

return Role(*[k for k in result], whitelist=whitelist)

def __and__(self, other):
"""Override handling of producing the intersection of two Roles to provide
native support for merging whitelist and blacklist roles correctly.
This overloading allows users to produce the intersection of two roles that
may, on one side, want to allow fields and on the other exclude them.
.. codeblock:: python
>>>from kim.role import whitelist, blacklist
>>>my_role = whitelist('foo', 'bar') & blacklist('foo', 'baz')
>>>my_role
Role('bar')
:param other: another instance of :py:class:``.Role``
:raises: :py:class:`.RoleError``
:rtype: :py:class:``.Role``
:returns: a new :py:class:``.Role`` containng the set of field names
"""
if not isinstance(other, Role):
raise RoleError('intersection of built types is '
'not supported with roles')

whitelist = True

if self.whitelist and other.whitelist:
# both roles are whitelists, return the union of both sets
result = super(Role, self).__and__(other)

elif self.whitelist and not other.whitelist:
# we need to remove the fields in self(whitelist)
# that appear in other(blacklist)
result = super(Role, self).__sub__(other)

elif not self.whitelist and other.whitelist:
# Same as above, except we are keeping the fields from other
result = other.__sub__(self)

else: # both roles are blacklist, union them and set whitelist=False
whitelist = False
result = super(Role, self).__or__(other)

return Role(*[k for k in result], whitelist=whitelist)


class whitelist(Role):
""" Whitelists are roles that define a list of fields that are
Expand Down
10 changes: 7 additions & 3 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from kim import field
from kim.mapper import PolymorphicMapper
from kim.role import whitelist
from kim.role import whitelist, blacklist


class TestType(object):
Expand Down Expand Up @@ -56,7 +56,9 @@ class EventMapper(SchedulableMapper):

__roles__ = {
'public': SchedulableMapper.__roles__['public']
| whitelist('location')
| whitelist('location'),
'event_only_role': whitelist('id', 'location'),
'event_blacklist': blacklist('location')
}


Expand All @@ -71,5 +73,7 @@ class TaskMapper(SchedulableMapper):

__roles__ = {
'public': SchedulableMapper.__roles__['public']
| whitelist('status')
| whitelist('status'),
'task_only_role': whitelist('id', 'status'),
'task_blacklist': blacklist('status')
}
129 changes: 127 additions & 2 deletions tests/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

from kim.exception import MappingInvalid, MapperError
from kim.mapper import Mapper, PolymorphicMapper
from kim.role import blacklist
from kim.field import Integer, Collection, String, Field
from kim.role import whitelist, blacklist
from kim.field import Integer, Collection, String
from kim.pipelines import marshaling
from kim.pipelines import serialization

Expand Down Expand Up @@ -576,6 +576,113 @@ def test_serialize_polymorphic_mapper_with_role():
}


def test_serialize_polymorphic_child_mapper_with_role():


obj = TestType(id=2, name='bob', object_type='event', location='London')

mapper = EventMapper(obj=obj)
data = mapper.serialize(role='event_only_role')

assert data == {
'id': 2,
'location': 'London'
}


def test_serialize_polymorphic_child_mapper_with_deferred_role():


obj = TestType(id=2, name='bob', object_type='event', location='London')

mapper = EventMapper(obj=obj)
data = mapper.serialize(role='event_only_role', deferred_role=whitelist('id'))

assert data == {
'id': 2,
}


def test_serialize_polymorphic_child_mapper_deferred_role_fields_across_types():


obj = TestType(id=2, name='bob', object_type='event', location='London')
obj2 = TestType(id=3, name='bob', object_type='task', status='failed')

mapper = SchedulableMapper(obj=obj)
data = mapper.serialize(
role='public', deferred_role=whitelist('id', 'status', 'location'))

assert data == {
'id': 2,
'location': 'London'
}

mapper = SchedulableMapper(obj=obj2)
data = mapper.serialize(
role='public', deferred_role=whitelist('id', 'status', 'location'))

assert data == {
'id': 3,
'status': 'failed'
}


def test_serialize_polymorphic_child_mapper_deferred_role_disallowed_fields():


obj = TestType(id=2, name='bob', object_type='event', location='London')
obj2 = TestType(id=3, name='bob', object_type='task', status='failed')

mapper = SchedulableMapper(obj=obj)
data = mapper.serialize(
role='name_only', deferred_role=whitelist('id', 'name'))

assert data == {
'name': 'bob'
}


def test_serialize_polymorphic_child_mapper_deferred_role_blacklist():


obj = TestType(id=2, name='bob', object_type='event', location='London')

mapper = SchedulableMapper(obj=obj)
data = mapper.serialize(
role='public', deferred_role=blacklist('id'))

assert data == {
'name': 'bob',
'location': 'London',
}


def test_serialize_polymorphic_child_mapper_existing_blacklist_with_deferred():


obj = TestType(id=2, name='bob', object_type='event', location='London')

mapper = SchedulableMapper(obj=obj)
data = mapper.serialize(
role='event_blacklist', deferred_role=whitelist('id'))

assert data == {
'id': 2,
}


def test_serialize_polymorphic_child_mapper_deferred_role_requires_role():


obj = TestType(id=2, name='bob', object_type='event', location='London')

mapper = SchedulableMapper(obj=obj)
with pytest.raises(MapperError):
mapper.serialize(role='public', deferred_role='foo')



def test_serialize_polymorphic_mapper_many():

obj1 = TestType(id=2, name='bob', location='London', object_type='event')
Expand All @@ -595,3 +702,21 @@ def test_serialize_polymorphic_mapper_many():
'status': 'Done'
}
]


def test_serialize_polymorphic_mapper_many_with_deferred_role():

obj1 = TestType(id=2, name='bob', location='London', object_type='event')
obj2 = TestType(id=3, name='fred', status='Done', object_type='task')

result = SchedulableMapper.many().serialize(
[obj1, obj2], role='public', deferred_role=whitelist('id'))

assert result == [
{
'id': 2,
},
{
'id': 3,
}
]
Loading

0 comments on commit 3ec3628

Please sign in to comment.