Skip to content

Commit 3ec3628

Browse files
authored
Merge pull request #129 from mikeywaites/feature/deffered-roles
deferred roles
2 parents 58e7069 + 20407c4 commit 3ec3628

File tree

7 files changed

+271
-18
lines changed

7 files changed

+271
-18
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.0-rc8
1+
1.0.0-rc9

dependencies/test.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pytest==2.7.3
1+
pytest==2.8
22
pytest-cov==1.8.1
33
pytest_spec==0.2.24
44
sqlalchemy==1.0.4

kim/mapper.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# This module is part of Kim and is released under
66
# the MIT License: http://www.opensource.org/licenses/mit-license.php
77

8+
import warnings
89
import weakref
910
import six
1011
import inspect
@@ -13,7 +14,7 @@
1314

1415
from .exception import MapperError, MappingInvalid
1516
from .field import Field, FieldError, FieldInvalid
16-
from .role import whitelist, Role
17+
from .role import whitelist, blacklist, Role
1718
from .utils import recursive_defaultdict, attr_or_key
1819
from .pipelines.base import Pipe
1920

@@ -407,34 +408,66 @@ def _get_obj(self):
407408
else:
408409
return self._get_mapper_type()()
409410

410-
def _get_role(self, name_or_role):
411+
def _get_role(self, name_or_role, deferred_role=None):
411412
"""Resolve a string to a role and check it exists, or check a
412413
directly passed role is a Role instance and return it.
413414
415+
You may also affect the fields returned from a role at read time
416+
using ``deferred_role``. deferred_role is used to provide the intersection
417+
between the role specified at ``name_or_role`` and the ``deferred_role``.
418+
419+
class FooMapper(Mapper):
420+
__type__ = dict
421+
name = field.String()
422+
id = field.String()
423+
secret = field.String()
424+
425+
__roles__ = {
426+
'overview': whitelist('id', 'name'),
427+
}
428+
429+
mapper._get_role('overview', deferred_role=whitelist('id'))
430+
431+
Deferred roles can be used for things like allowing end users to provide a list
432+
of fields they want back from your API but only if they appear in a role you've
433+
specified.
434+
435+
:param deferred_role: provide a role containing fields to dynamically change the
436+
permitted fields for the role specified in ``name_or_role``
414437
:param name_or_role: role name as a string or a Role instance
415438
416439
:raises: :class:`.MapperError`
417440
:returns: Role instance
418441
"""
419442
if isinstance(name_or_role, six.string_types):
420443
try:
421-
return self.roles[name_or_role]
444+
role = self.roles[name_or_role]
422445
except KeyError:
423446
raise MapperError("Role '%s' not found on %s" % (
424447
name_or_role, self.__class__.__name__))
425448
elif isinstance(name_or_role, Role):
426-
return name_or_role
449+
role = name_or_role
427450
else:
428451
raise MapperError('role must be string or Role instance, got %s'
429452
% type(name_or_role))
430453

454+
# If deferred_role is not None, return the intersection of the
455+
# role and the deffered_role
456+
if deferred_role is not None:
457+
if not isinstance(deferred_role, Role):
458+
raise MapperError('deferred_role must be instance of Role')
459+
460+
return role & deferred_role
461+
else:
462+
return role
463+
431464
def _field_in_data(self, field):
432465
for key in self.data.keys():
433466
if key == field.name:
434467
return True
435468
return False
436469

437-
def _get_fields(self, name_or_role, for_marshal=False):
470+
def _get_fields(self, name_or_role, deferred_role=None, for_marshal=False):
438471
"""Returns a list of :class:`.Field` instances providing they are
439472
registered in the specified :class:`Role`.
440473
@@ -445,7 +478,7 @@ def _get_fields(self, name_or_role, for_marshal=False):
445478
:returns: list of :class:`.Field`
446479
"""
447480

448-
role = self._get_role(name_or_role)
481+
role = self._get_role(name_or_role, deferred_role=deferred_role)
449482

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

@@ -542,7 +575,7 @@ def get_mapper_session(self, data, output):
542575

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

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

565-
for field in self._get_fields(role):
598+
for field in self._get_fields(role, deferred_role=deferred_role):
566599
field.serialize(self.get_mapper_session(data, output))
567600

568601
return output
@@ -734,7 +767,7 @@ def get_mapper(self, data=None, obj=None):
734767
})
735768
return self.mapper(**self.mapper_params)
736769

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

746779
output = [] # TODO should this be user defined?
747780
for obj in objs:
748-
output.append(self.get_mapper(obj=obj).serialize(role=role))
781+
output.append(self.get_mapper(obj=obj).serialize(
782+
role=role,
783+
deferred_role=deferred_role))
749784

750785
return output
751786

kim/role.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,52 @@ def __or__(self, other):
129129

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

132+
def __and__(self, other):
133+
"""Override handling of producing the intersection of two Roles to provide
134+
native support for merging whitelist and blacklist roles correctly.
135+
136+
This overloading allows users to produce the intersection of two roles that
137+
may, on one side, want to allow fields and on the other exclude them.
138+
139+
.. codeblock:: python
140+
141+
>>>from kim.role import whitelist, blacklist
142+
>>>my_role = whitelist('foo', 'bar') & blacklist('foo', 'baz')
143+
>>>my_role
144+
Role('bar')
145+
146+
:param other: another instance of :py:class:``.Role``
147+
:raises: :py:class:`.RoleError``
148+
149+
:rtype: :py:class:``.Role``
150+
:returns: a new :py:class:``.Role`` containng the set of field names
151+
152+
"""
153+
if not isinstance(other, Role):
154+
raise RoleError('intersection of built types is '
155+
'not supported with roles')
156+
157+
whitelist = True
158+
159+
if self.whitelist and other.whitelist:
160+
# both roles are whitelists, return the union of both sets
161+
result = super(Role, self).__and__(other)
162+
163+
elif self.whitelist and not other.whitelist:
164+
# we need to remove the fields in self(whitelist)
165+
# that appear in other(blacklist)
166+
result = super(Role, self).__sub__(other)
167+
168+
elif not self.whitelist and other.whitelist:
169+
# Same as above, except we are keeping the fields from other
170+
result = other.__sub__(self)
171+
172+
else: # both roles are blacklist, union them and set whitelist=False
173+
whitelist = False
174+
result = super(Role, self).__or__(other)
175+
176+
return Role(*[k for k in result], whitelist=whitelist)
177+
132178

133179
class whitelist(Role):
134180
""" Whitelists are roles that define a list of fields that are

tests/fixtures.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from kim import field
22
from kim.mapper import PolymorphicMapper
3-
from kim.role import whitelist
3+
from kim.role import whitelist, blacklist
44

55

66
class TestType(object):
@@ -56,7 +56,9 @@ class EventMapper(SchedulableMapper):
5656

5757
__roles__ = {
5858
'public': SchedulableMapper.__roles__['public']
59-
| whitelist('location')
59+
| whitelist('location'),
60+
'event_only_role': whitelist('id', 'location'),
61+
'event_blacklist': blacklist('location')
6062
}
6163

6264

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

7274
__roles__ = {
7375
'public': SchedulableMapper.__roles__['public']
74-
| whitelist('status')
76+
| whitelist('status'),
77+
'task_only_role': whitelist('id', 'status'),
78+
'task_blacklist': blacklist('status')
7579
}

tests/test_functional.py

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
from kim.exception import MappingInvalid, MapperError
55
from kim.mapper import Mapper, PolymorphicMapper
6-
from kim.role import blacklist
7-
from kim.field import Integer, Collection, String, Field
6+
from kim.role import whitelist, blacklist
7+
from kim.field import Integer, Collection, String
88
from kim.pipelines import marshaling
99
from kim.pipelines import serialization
1010

@@ -576,6 +576,113 @@ def test_serialize_polymorphic_mapper_with_role():
576576
}
577577

578578

579+
def test_serialize_polymorphic_child_mapper_with_role():
580+
581+
582+
obj = TestType(id=2, name='bob', object_type='event', location='London')
583+
584+
mapper = EventMapper(obj=obj)
585+
data = mapper.serialize(role='event_only_role')
586+
587+
assert data == {
588+
'id': 2,
589+
'location': 'London'
590+
}
591+
592+
593+
def test_serialize_polymorphic_child_mapper_with_deferred_role():
594+
595+
596+
obj = TestType(id=2, name='bob', object_type='event', location='London')
597+
598+
mapper = EventMapper(obj=obj)
599+
data = mapper.serialize(role='event_only_role', deferred_role=whitelist('id'))
600+
601+
assert data == {
602+
'id': 2,
603+
}
604+
605+
606+
def test_serialize_polymorphic_child_mapper_deferred_role_fields_across_types():
607+
608+
609+
obj = TestType(id=2, name='bob', object_type='event', location='London')
610+
obj2 = TestType(id=3, name='bob', object_type='task', status='failed')
611+
612+
mapper = SchedulableMapper(obj=obj)
613+
data = mapper.serialize(
614+
role='public', deferred_role=whitelist('id', 'status', 'location'))
615+
616+
assert data == {
617+
'id': 2,
618+
'location': 'London'
619+
}
620+
621+
mapper = SchedulableMapper(obj=obj2)
622+
data = mapper.serialize(
623+
role='public', deferred_role=whitelist('id', 'status', 'location'))
624+
625+
assert data == {
626+
'id': 3,
627+
'status': 'failed'
628+
}
629+
630+
631+
def test_serialize_polymorphic_child_mapper_deferred_role_disallowed_fields():
632+
633+
634+
obj = TestType(id=2, name='bob', object_type='event', location='London')
635+
obj2 = TestType(id=3, name='bob', object_type='task', status='failed')
636+
637+
mapper = SchedulableMapper(obj=obj)
638+
data = mapper.serialize(
639+
role='name_only', deferred_role=whitelist('id', 'name'))
640+
641+
assert data == {
642+
'name': 'bob'
643+
}
644+
645+
646+
def test_serialize_polymorphic_child_mapper_deferred_role_blacklist():
647+
648+
649+
obj = TestType(id=2, name='bob', object_type='event', location='London')
650+
651+
mapper = SchedulableMapper(obj=obj)
652+
data = mapper.serialize(
653+
role='public', deferred_role=blacklist('id'))
654+
655+
assert data == {
656+
'name': 'bob',
657+
'location': 'London',
658+
}
659+
660+
661+
def test_serialize_polymorphic_child_mapper_existing_blacklist_with_deferred():
662+
663+
664+
obj = TestType(id=2, name='bob', object_type='event', location='London')
665+
666+
mapper = SchedulableMapper(obj=obj)
667+
data = mapper.serialize(
668+
role='event_blacklist', deferred_role=whitelist('id'))
669+
670+
assert data == {
671+
'id': 2,
672+
}
673+
674+
675+
def test_serialize_polymorphic_child_mapper_deferred_role_requires_role():
676+
677+
678+
obj = TestType(id=2, name='bob', object_type='event', location='London')
679+
680+
mapper = SchedulableMapper(obj=obj)
681+
with pytest.raises(MapperError):
682+
mapper.serialize(role='public', deferred_role='foo')
683+
684+
685+
579686
def test_serialize_polymorphic_mapper_many():
580687

581688
obj1 = TestType(id=2, name='bob', location='London', object_type='event')
@@ -595,3 +702,21 @@ def test_serialize_polymorphic_mapper_many():
595702
'status': 'Done'
596703
}
597704
]
705+
706+
707+
def test_serialize_polymorphic_mapper_many_with_deferred_role():
708+
709+
obj1 = TestType(id=2, name='bob', location='London', object_type='event')
710+
obj2 = TestType(id=3, name='fred', status='Done', object_type='task')
711+
712+
result = SchedulableMapper.many().serialize(
713+
[obj1, obj2], role='public', deferred_role=whitelist('id'))
714+
715+
assert result == [
716+
{
717+
'id': 2,
718+
},
719+
{
720+
'id': 3,
721+
}
722+
]

0 commit comments

Comments
 (0)