From 0172a6ce484cc1b948529d788ec0d01ed5a177d7 Mon Sep 17 00:00:00 2001 From: tim-d-blue Date: Wed, 18 Mar 2015 20:22:29 -0400 Subject: [PATCH 1/4] this branch adds a 'roles' parameter to export_loop so that multiple roles can be used on serialization for a simpler means to combine roles on export --- docs/usage/exporting.rst | 38 +++++++++++++++++++++++ schematics/models.py | 8 ++--- schematics/transforms.py | 54 +++++++++++++++++++++++++++----- tests/test_serialize.py | 67 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 12 deletions(-) diff --git a/docs/usage/exporting.rst b/docs/usage/exporting.rst index 52e17de3..b3c14357 100644 --- a/docs/usage/exporting.rst +++ b/docs/usage/exporting.rst @@ -312,6 +312,44 @@ results without having to specify ``role`` in the export function. }] } +Optionally a list of roles can be provided for export instead. The combination +of the all the roles will be made and used to filter fields. Blacklist roles +override whitelist roles in this case. + +:: + + class User(Model): + id = IntType(default=42) + name = StringType() + email = StringType() + password = StringType() + + class Options: + roles = { + 'public': whitelist('id', 'name', 'email'), + 'admin': whitelist('password') + 'no_private': blacklist('password', 'email') + } + +The ``password`` field will combined with the ``id``, ``name`` and ``email`` +fields when ``roles`` is set to both ``public`` and ``admin``. + + >>> favorites.to_primitive(roles=['public', 'admin']) + { + 'id': 42, + 'name': 'Arthur', + 'email': 'adent@hitchhiker.gal', + 'password': 'dolphins' + } + +Adding in the ``no_private`` role will then blacklist the ``email`` and +``password`` fields. + + >>> favorites.to_primitive(roles=['public', 'admin', 'no_private']) + { + 'id': 42, + 'name': 'Arthur' + } .. _exporting_serializable: diff --git a/schematics/models.py b/schematics/models.py index 96bc56d7..26923816 100644 --- a/schematics/models.py +++ b/schematics/models.py @@ -292,7 +292,7 @@ def convert(self, raw_data, **kw): def to_native(self, role=None, context=None): return to_native(self.__class__, self, role=role, context=context) - def to_primitive(self, role=None, context=None): + def to_primitive(self, role=None, context=None, roles=None): """Return data as it would be validated. No filtering of output unless role is defined. @@ -300,10 +300,10 @@ def to_primitive(self, role=None, context=None): Filter output by a specific role """ - return to_primitive(self.__class__, self, role=role, context=context) + return to_primitive(self.__class__, self, role=role, context=context, roles=roles) - def serialize(self, role=None, context=None): - return self.to_primitive(role=role, context=context) + def serialize(self, role=None, context=None, roles=None): + return self.to_primitive(role=role, context=context, roles=roles) def flatten(self, role=None, prefix=""): """ diff --git a/schematics/transforms.py b/schematics/transforms.py index 14421121..ccfff647 100644 --- a/schematics/transforms.py +++ b/schematics/transforms.py @@ -121,7 +121,7 @@ def import_loop(cls, instance_or_dict, field_converter, context=None, def export_loop(cls, instance_or_dict, field_converter, - role=None, raise_error_on_role=False, print_none=False): + role=None, raise_error_on_role=False, print_none=False, roles=None): """ The export_loop function is intended to be a general loop definition that can be used for any form of data shaping, such as application of roles or @@ -144,12 +144,24 @@ def export_loop(cls, instance_or_dict, field_converter, :param print_none: This function overrides ``serialize_when_none`` values found either on ``cls`` or an instance. + :param roles: + A list of roles, will determine a dynamic role aggregated from a list of + individual roles. Blacklisted fields will override whitelisted fields. + Supplying this parameter supersedes the role parameter. """ data = {} # Translate `role` into `gottago` function gottago = wholelist() - if hasattr(cls, '_options') and role in cls._options.roles: + + if hasattr(cls, '_options') and roles: + if raise_error_on_role: + for r in roles: + if not cls._options.roles.get(r, None): + error_msg = u'%s Model has no role "%s"' + raise ValueError(error_msg % (cls.__name__, r)) + gottago = combinedlist([cls._options.roles.get(r, None) for r in roles]) + elif hasattr(cls, '_options') and role in cls._options.roles: gottago = cls._options.roles[role] elif role and raise_error_on_role: error_msg = u'%s Model has no role "%s"' @@ -374,6 +386,31 @@ def blacklist(*field_list): return Role(Role.blacklist, field_list) +def combinedlist(roles): + """ + Returns a function that operates as a whitelist for the combined set of + fields in each of the specified roles. Blacklisted fields will override + whitelisted fields. + + :param roles: list of Roles + """ + combined_roles = None + + for role in roles: + if role and role.function.func_name == "whitelist": + if combined_roles: + combined_roles = combined_roles + role + else: + combined_roles = role + if not combined_roles: + combined_roles = wholelist() + for role in roles: + if role and role.function.func_name == "blacklist": + combined_roles = combined_roles - role + + return combined_roles + + ### # Import and export functions ### @@ -393,16 +430,16 @@ def field_converter(field, value, mapping=None): def to_native(cls, instance_or_dict, role=None, raise_error_on_role=True, - context=None): + context=None, roles=None): field_converter = lambda field, value: field.to_native(value, context=context) data = export_loop(cls, instance_or_dict, field_converter, - role=role, raise_error_on_role=raise_error_on_role) + role=role, raise_error_on_role=raise_error_on_role, roles=roles) return data def to_primitive(cls, instance_or_dict, role=None, raise_error_on_role=True, - context=None): + context=None, roles=None): """ Implements serialization as a mechanism to convert ``Model`` instances into dictionaries keyed by field_names with the converted data as the values. @@ -422,18 +459,19 @@ def to_primitive(cls, instance_or_dict, role=None, raise_error_on_role=True, :param raise_error_on_role: This parameter enforces strict behavior which requires substructures to have the same role definition as their parent structures. + :param roles: list of Roles to combine and use to filter fields. """ field_converter = lambda field, value: field.to_primitive(value, context=context) data = export_loop(cls, instance_or_dict, field_converter, - role=role, raise_error_on_role=raise_error_on_role) + role=role, raise_error_on_role=raise_error_on_role, roles=roles) return data def serialize(cls, instance_or_dict, role=None, raise_error_on_role=True, - context=None): + context=None, roles=None): return to_primitive(cls, instance_or_dict, role, raise_error_on_role, - context) + context, roles=roles) EMPTY_LIST = "[]" diff --git a/tests/test_serialize.py b/tests/test_serialize.py index 264df0e2..b99aefa3 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -911,3 +911,70 @@ class Options: }, ] } + + +def test_multiple_roles(): + + class Child(Model): + id = IntType(default=84) + name = StringType() + ssn = StringType() + + class Options: + roles = { + 'create': whitelist('name', 'ssn'), + 'public': whitelist('id', 'name'), + 'no_private': blacklist('ssn') + } + + class User(Model): + id = IntType(default=42) + name = StringType() + email = StringType() + password = StringType() + child = ModelType(Child) + + class Options: + roles = { + 'create': whitelist('name', 'email', 'password', 'child'), + 'public': whitelist('id', 'name', 'email'), + 'no_private': blacklist('email', 'password', 'child') + } + + child = Child({'name': 'Random', 'ssn': '000-11-1212'}) + user = User({'name': 'Arthur', 'email': 'adent@hitchhiker.gal', + 'password': 'dolphins', 'child': child}) + + d = user.serialize(roles=['public']) + + assert d == { + 'id': 42, + 'name': 'Arthur', + 'email': 'adent@hitchhiker.gal' + } + + d = user.serialize(roles=['public', 'create']) + + assert d == { + 'id': 42, + 'name': 'Arthur', + 'email': 'adent@hitchhiker.gal', + 'password': 'dolphins', + 'child': { + 'id': 84, + 'name': 'Random', + 'ssn': '000-11-1212' + } + } + + d = user.serialize(roles=['public', 'create', 'no_private']) + + assert d == { + 'id': 42, + 'name': 'Arthur' + } + + try: + user.serialize(roles=['public', 'create', 'NOT_A_ROLE']) + except ValueError as ve: + assert ve.message == 'User Model has no role "NOT_A_ROLE"' From 10cf855d21a303f4205675acd92b75710deeee57 Mon Sep 17 00:00:00 2001 From: tim-d-blue Date: Wed, 18 Mar 2015 20:40:12 -0400 Subject: [PATCH 2/4] update doc and add a test for to_primitive --- docs/usage/exporting.rst | 4 ++-- tests/test_serialize.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/usage/exporting.rst b/docs/usage/exporting.rst index b3c14357..d6c655c6 100644 --- a/docs/usage/exporting.rst +++ b/docs/usage/exporting.rst @@ -334,7 +334,7 @@ override whitelist roles in this case. The ``password`` field will combined with the ``id``, ``name`` and ``email`` fields when ``roles`` is set to both ``public`` and ``admin``. - >>> favorites.to_primitive(roles=['public', 'admin']) + >>> user.to_primitive(roles=['public', 'admin']) { 'id': 42, 'name': 'Arthur', @@ -345,7 +345,7 @@ fields when ``roles`` is set to both ``public`` and ``admin``. Adding in the ``no_private`` role will then blacklist the ``email`` and ``password`` fields. - >>> favorites.to_primitive(roles=['public', 'admin', 'no_private']) + >>> user.to_primitive(roles=['public', 'admin', 'no_private']) { 'id': 42, 'name': 'Arthur' diff --git a/tests/test_serialize.py b/tests/test_serialize.py index b99aefa3..511fbf3e 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -978,3 +978,11 @@ class Options: user.serialize(roles=['public', 'create', 'NOT_A_ROLE']) except ValueError as ve: assert ve.message == 'User Model has no role "NOT_A_ROLE"' + + d = user.to_primitive(roles=['public', 'create', 'no_private']) + + assert d == { + 'id': 42, + 'name': 'Arthur' + } + From b75542d042d5b4cac34cb470fc5a23f3133fb24a Mon Sep 17 00:00:00 2001 From: tim-d-blue Date: Wed, 18 Mar 2015 21:08:28 -0400 Subject: [PATCH 3/4] account for py3 --- schematics/transforms.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/schematics/transforms.py b/schematics/transforms.py index ccfff647..c46a7323 100644 --- a/schematics/transforms.py +++ b/schematics/transforms.py @@ -397,7 +397,10 @@ def combinedlist(roles): combined_roles = None for role in roles: - if role and role.function.func_name == "whitelist": + if role and ((hasattr(role.function, "func_name") and + role.function.func_name == "whitelist") + or (hasattr(role.function, "__name__") and + role.function.__name__ == "whitelist")): if combined_roles: combined_roles = combined_roles + role else: @@ -405,7 +408,10 @@ def combinedlist(roles): if not combined_roles: combined_roles = wholelist() for role in roles: - if role and role.function.func_name == "blacklist": + if role and ((hasattr(role.function, "func_name") and + role.function.func_name == "blacklist") + or (hasattr(role.function, "__name__") and + role.function.__name__ == "blacklist")): combined_roles = combined_roles - role return combined_roles From 7b960a4890ac13c5b25395d549b9eb70af1f9aed Mon Sep 17 00:00:00 2001 From: tim-d-blue Date: Wed, 18 Mar 2015 21:25:15 -0400 Subject: [PATCH 4/4] update exception test for py3 --- tests/test_serialize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_serialize.py b/tests/test_serialize.py index 511fbf3e..a3aa2583 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -977,7 +977,7 @@ class Options: try: user.serialize(roles=['public', 'create', 'NOT_A_ROLE']) except ValueError as ve: - assert ve.message == 'User Model has no role "NOT_A_ROLE"' + assert ve.args[0] == 'User Model has no role "NOT_A_ROLE"' d = user.to_primitive(roles=['public', 'create', 'no_private'])