From 6bd99843d0bf7e00a60623dfddb17ac5e40ab5de Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 11 Nov 2014 14:45:31 +0100 Subject: [PATCH 001/107] Bump version to 0.4.dev0 --- lima/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lima/__init__.py b/lima/__init__.py index 6709773..56d6f40 100644 --- a/lima/__init__.py +++ b/lima/__init__.py @@ -6,4 +6,4 @@ from lima import schema from lima.schema import Schema -__version__ = '0.3' +__version__ = '0.4.dev0' From 287c19a7d8679fd3870ce6468c96984dab30c269 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 11 Nov 2014 14:46:35 +0100 Subject: [PATCH 002/107] Point readme badges back to develop branch. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 24f2be5..2f13a3e 100644 --- a/README.rst +++ b/README.rst @@ -77,12 +77,12 @@ See the `documentation`_ for more comprehensive install instructions. :target: https://lima.readthedocs.org :alt: Documentation Status -.. |build| image:: https://img.shields.io/travis/b6d/lima/master.svg +.. |build| image:: https://img.shields.io/travis/b6d/lima/develop.svg ?style=flat-square :target: https://travis-ci.org/b6d/lima :alt: Build Status -.. |coverage| image:: https://img.shields.io/coveralls/b6d/lima/master.svg +.. |coverage| image:: https://img.shields.io/coveralls/b6d/lima/develop.svg ?style=flat-square :target: https://coveralls.io/r/b6d/lima :alt: Test Coverage From 0c1f5d11e73a897741eae730d23519a4900d019c Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 11 Nov 2014 14:58:07 +0100 Subject: [PATCH 003/107] Update changelog. --- CHANGELOG.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 96082e3..c3511e6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,12 @@ Changelog ========= +0.4 (unreleased) +================ + +- No changes yet. + + 0.3.1 (2014-11-11) ================== From 984eeb6effd2d8744ff854b37c58acbc6df8a12a Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 11 Nov 2014 15:26:46 +0100 Subject: [PATCH 004/107] fields: Fix module globals docstrings. --- lima/fields.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index fef06b6..4845b13 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -263,12 +263,10 @@ def pack(self, val): '''A mapping of native Python types to :class:`Field` classes. This can be used to automatically create fields for objects you know the -attribute's types of. +attribute's types of.''' -''' type_mapping = TYPE_MAPPING '''An alias for :attr:`TYPE_MAPPING`. .. deprecated:: 0.3 - Will be removed in 0.4. Use :attr:`TYPE_MAPPING` instead. -''' + Will be removed in 0.4. Use :attr:`TYPE_MAPPING` instead.''' From 356028175d75247c40cab2170faabedacaeb4685 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 11 Nov 2014 15:50:40 +0100 Subject: [PATCH 005/107] Remove fields.type_mapping (use fields.TYPE_MAPPING). --- CHANGELOG.rst | 2 +- docs/api.rst | 5 +---- lima/fields.py | 6 ------ 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c3511e6..1044cda 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,7 +5,7 @@ Changelog 0.4 (unreleased) ================ -- No changes yet. +- Remove ``fields.type_mapping``. Use ``fields.TYPE_MAPPING`` instead. 0.3.1 (2014-11-11) diff --git a/docs/api.rst b/docs/api.rst index 7a38229..3851380 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -35,14 +35,11 @@ lima.fields .. automodule:: lima.fields :members: - :exclude-members: TYPE_MAPPING, type_mapping + :exclude-members: TYPE_MAPPING .. autodata:: lima.fields.TYPE_MAPPING :annotation: =dict(...) - .. autodata:: lima.fields.type_mapping - :annotation: =dict(...) - .. _api_registry: diff --git a/lima/fields.py b/lima/fields.py index 4845b13..0b2bdef 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -264,9 +264,3 @@ def pack(self, val): This can be used to automatically create fields for objects you know the attribute's types of.''' - -type_mapping = TYPE_MAPPING -'''An alias for :attr:`TYPE_MAPPING`. - -.. deprecated:: 0.3 - Will be removed in 0.4. Use :attr:`TYPE_MAPPING` instead.''' From 02890f77f87cfbccede1c6c5f439515287ddb954 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 11 Nov 2014 15:53:51 +0100 Subject: [PATCH 006/107] Add "0.4-unfinished-disclaimer" to changelog. --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1044cda..2183ed9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,10 @@ Changelog 0.4 (unreleased) ================ +.. note:: + + While unreleased, the changelog of lima 0.4 is itself subject to change. + - Remove ``fields.type_mapping``. Use ``fields.TYPE_MAPPING`` instead. From a538b0cd77a9362b2b0caaf95edc75ad7a292949 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sat, 15 Nov 2014 20:57:10 +0100 Subject: [PATCH 007/107] Factor out function to determine code for a field's value We might need this separately later on. --- lima/schema.py | 99 +++++++++++++++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 37 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index cd6325d..c7a690b 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -313,6 +313,63 @@ def __init__(self, # this defines _dump_function in self's namespace exec(code, globals(), self.__dict__) + @staticmethod + def _field_value_code_attrs(field, field_name, field_num): + '''Get code/attrs to determine a field's serialized value. + + Args: + field: A :class:`lima.fields.Field` instance. + + field_name: The name (key) of the field instance. + + field_num: A schema-wide unique number for the field. + + Returns: A tuple consisting of: a) a fragment of Python code to + determine the field value *for the specific case of a schema + instance called ``"schema"`` serializing an object called + ``"obj"``* and b) a mapping of attribute names to attribute values + that the schema instance *must have* for this code to work. + + ''' + attrs = {} + if hasattr(field, 'val'): + # add constant-field-value-shortcut to attrs + val_name = '__val_{}'.format(field_num) + attrs[val_name] = field.val + + # later, get value using this shortcut + val_code = 'schema.{}'.format(val_name) + + elif hasattr(field, 'get'): + # add value-getter-shortcut to attrs + getter_name = '__get_{}'.format(field_num) + attrs[getter_name] = field.get + + # later, get value by calling this shortcut + val_code = 'schema.{}(obj)'.format(getter_name) + + else: + # neither constant val nor getter: try to get value via attr + # (if attr is not specified, use field name as attr) + obj_attr = getattr(field, 'attr', field_name) + + if not str.isidentifier(obj_attr) or keyword.iskeyword(obj_attr): + msg = 'Not a valid attribute name: {!r}' + raise ValueError(msg.format(obj_attr)) + + # later, get value using obj_attr + val_code = 'obj.{}'.format(obj_attr) + + if hasattr(field, 'pack'): + # add pack-shortcut to attributes + packer_name = '__pack_{}'.format(field_num) + attrs[packer_name] = field.pack + + # later, pass result field value to this shortcut + val_code = 'schema.{}({})'.format(packer_name, val_code) + + return val_code, attrs + def _get_dump_function_code(self): '''Get code for a customized dump function.''' # note that even _though dump_function might *look* like a method at @@ -348,42 +405,10 @@ def _dump_function(schema, obj): # iterate over fields to fill up entries for field_num, (field_name, field) in enumerate(self._fields.items()): - - if hasattr(field, 'val'): - # add constant-field-value-shortcut to self - val_name = '__val_{}'.format(field_num) - setattr(self, val_name, field.val) - - # later, get value using this shortcut - get_val = 'schema.{}'.format(val_name) - - elif hasattr(field, 'get'): - # add getter-shortcut to self - getter_name = '__get_{}'.format(field_num) - setattr(self, getter_name, field.get) - - # later, get value by calling getter-shortcut - get_val = 'schema.{}(obj)'.format(getter_name) - - else: - # neither constant val nor getter: try to get value via attr - # (if no attr name is specified, use field name as attr name) - attr = getattr(field, 'attr', field_name) - - if not str.isidentifier(attr) or keyword.iskeyword(attr): - msg = 'Not a valid attribute name: {!r}' - raise ValueError(msg.format(attr)) - - # later, get value using attr - get_val = 'obj.{}'.format(attr) - - if hasattr(field, 'pack'): - # add pack-shortcut to self - packer_name = '__pack_{}'.format(field_num) - setattr(self, packer_name, field.pack) - - # later, wrap pass result of get_val to pack-shortcut - get_val = 'schema.{}({})'.format(packer_name, get_val) + val_code, attrs = Schema._field_value_code_attrs(field, field_name, + field_num) + for attr_name, attr in attrs.items(): + setattr(self, attr_name, attr) # try to guard against code injection via quotes in key key = str(field_name) @@ -392,7 +417,7 @@ def _dump_function(schema, obj): raise ValueError(msg.format(key)) # add entry - entries.append(entry_tpl.format(key=key, get_val=get_val)) + entries.append(entry_tpl.format(key=key, get_val=val_code)) sep = ',\n ' code = func_tpl.format(contents=sep.join(entries)) From 426577c1d1a809a6d2b268bcb1de66ea09b65857 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 16:25:41 +0100 Subject: [PATCH 008/107] Schema: Change an internal method name. --- lima/schema.py | 4 ++-- tests/test_schema.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index c7a690b..1957f93 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -308,7 +308,7 @@ def __init__(self, self.many = many # get code for the customized dump function - code = self._get_dump_function_code() + code = self._dump_function_code() # this defines _dump_function in self's namespace exec(code, globals(), self.__dict__) @@ -370,7 +370,7 @@ def _field_value_code_attrs(field, field_name, field_num): return val_code, attrs - def _get_dump_function_code(self): + def _dump_function_code(self): '''Get code for a customized dump function.''' # note that even _though dump_function might *look* like a method at # first glance, it is *not*, since it will tied to a specific Schema diff --git a/tests/test_schema.py b/tests/test_schema.py index 9a35279..bf95292 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -663,8 +663,8 @@ class TestSchema(schema.Schema): with pytest.raises(ValueError): test_schema = TestSchema() - def test_get_dump_function_code(self): - '''Test if _get_dump_function_code gets a simple function right.''' + def test_dump_function_code(self): + '''Test if _dump_function_code gets a simple function right.''' from textwrap import dedent class TestSchema(schema.Schema): @@ -681,7 +681,7 @@ def _dump_function(schema, obj): } ''' ) - assert test_schema._get_dump_function_code() == expected + assert test_schema._dump_function_code() == expected test_schema = TestSchema(ordered=True) expected = dedent( @@ -693,4 +693,4 @@ def _dump_function(schema, obj): ]) ''' ) - assert test_schema._get_dump_function_code() == expected + assert test_schema._dump_function_code() == expected From 6cb1ee26e7941152b4be157bf635e55656bf4cf2 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 17:09:13 +0100 Subject: [PATCH 009/107] Schema: refactor get-dump-function-code to remove side effects. --- lima/schema.py | 48 ++++++++++++++++++++++++++++++-------------- tests/test_schema.py | 12 +++++++---- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 1957f93..c9b25db 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -308,7 +308,10 @@ def __init__(self, self.many = many # get code for the customized dump function - code = self._dump_function_code() + code, attrs = self._dump_function_code_attrs(fields, ordered) + + for k, v in attrs.items(): + setattr(self, k, v) # this defines _dump_function in self's namespace exec(code, globals(), self.__dict__) @@ -370,16 +373,31 @@ def _field_value_code_attrs(field, field_name, field_num): return val_code, attrs - def _dump_function_code(self): - '''Get code for a customized dump function.''' - # note that even _though dump_function might *look* like a method at - # first glance, it is *not*, since it will tied to a specific Schema - # instance instead of to the Schema class like a method would be. This - # means that ten Schema objects will have ten separate dump functions - # associated with them. + @staticmethod + def _dump_function_code_attrs(fields, ordered): + '''Get code/attrs for a customized dump function + + Args: + fields: An ordered mapping of field names to fields + + ordered: If True, make the resulting function return OrderedDict + objects, else make it return ordinary dict objects + Returns: A tuple consisting of: a) a fragment of Python code to + define a dump function for a schema and b) a mapping of attribute + names to attribute values that the schema instance *must have* for + this code to work. + + Note that even though the resulting code might *look* like a method + definition at first glance, it is *not*, since it will tied to a + specific Schema instance instead of to the Schema class like a method + would be. This means that ten Schema objects will have ten separate + dump functions associated with them. + + ''' + attrs = {} # get correct templates - if self._ordered: + if ordered: func_tpl = textwrap.dedent( '''\ def _dump_function(schema, obj): @@ -404,11 +422,11 @@ def _dump_function(schema, obj): entries = [] # iterate over fields to fill up entries - for field_num, (field_name, field) in enumerate(self._fields.items()): - val_code, attrs = Schema._field_value_code_attrs(field, field_name, - field_num) - for attr_name, attr in attrs.items(): - setattr(self, attr_name, attr) + for field_num, (field_name, field) in enumerate(fields.items()): + val_code, field_attrs = Schema._field_value_code_attrs( + field, field_name, field_num + ) + attrs.update(field_attrs) # try to guard against code injection via quotes in key key = str(field_name) @@ -421,7 +439,7 @@ def _dump_function(schema, obj): sep = ',\n ' code = func_tpl.format(contents=sep.join(entries)) - return code + return code, attrs def dump(self, obj, *, many=None): '''Return a marshalled representation of obj. diff --git a/tests/test_schema.py b/tests/test_schema.py index bf95292..049812d 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -671,7 +671,9 @@ class TestSchema(schema.Schema): foo = fields.String(attr='foo_attr') bar = fields.String() - test_schema = TestSchema() + code, ns = schema.Schema._dump_function_code_attrs( + TestSchema.__fields__, ordered=False + ) expected = dedent( '''\ def _dump_function(schema, obj): @@ -681,9 +683,11 @@ def _dump_function(schema, obj): } ''' ) - assert test_schema._dump_function_code() == expected + assert code == expected - test_schema = TestSchema(ordered=True) + code, ns = schema.Schema._dump_function_code_attrs( + TestSchema.__fields__, ordered=True + ) expected = dedent( '''\ def _dump_function(schema, obj): @@ -693,4 +697,4 @@ def _dump_function(schema, obj): ]) ''' ) - assert test_schema._dump_function_code() == expected + assert code == expected From a7a9a3c15c40be2b71d03a9caf6c1b9ce12b3978 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 17:48:41 +0100 Subject: [PATCH 010/107] Schema: supply namespace-dict to custom dump-function instead of polluting the schema instance's namespace and relying on that --- lima/schema.py | 78 ++++++++++++++++++++------------------------ tests/test_schema.py | 8 ++--- 2 files changed, 39 insertions(+), 47 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index c9b25db..e1ae93d 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -307,18 +307,16 @@ def __init__(self, self._ordered = ordered self.many = many - # get code for the customized dump function - code, attrs = self._dump_function_code_attrs(fields, ordered) - - for k, v in attrs.items(): - setattr(self, k, v) + # get code/namespace for the customized dump function + code, namespace = self._dump_function_code_ns(fields, ordered) # this defines _dump_function in self's namespace exec(code, globals(), self.__dict__) + self._namespace = namespace @staticmethod - def _field_value_code_attrs(field, field_name, field_num): - '''Get code/attrs to determine a field's serialized value. + def _field_value_code_ns(field, field_name, field_num): + '''Get code/namespace-dict to determine a field's serialized value. Args: field: A :class:`lima.fields.Field` instance. @@ -328,28 +326,27 @@ def _field_value_code_attrs(field, field_name, field_num): field_num: A schema-wide unique number for the field. Returns: A tuple consisting of: a) a fragment of Python code to - determine the field value *for the specific case of a schema - instance called ``"schema"`` serializing an object called - ``"obj"``* and b) a mapping of attribute names to attribute values - that the schema instance *must have* for this code to work. + determine the field's value and b) a namespace-dict containing + shortcuts to necessary values/functions for this code fragment to + work. ''' - attrs = {} + namespace = {} if hasattr(field, 'val'): - # add constant-field-value-shortcut to attrs - val_name = '__val_{}'.format(field_num) - attrs[val_name] = field.val + # add constant-field-value-shortcut to namespace + key = 'val{}'.format(field_num) + namespace[key] = field.val # later, get value using this shortcut - val_code = 'schema.{}'.format(val_name) + val_code = 'namespace["{}"]'.format(key) elif hasattr(field, 'get'): - # add value-getter-shortcut to attrs - getter_name = '__get_{}'.format(field_num) - attrs[getter_name] = field.get + # add getter-shortcut to namespace + key = 'get{}'.format(field_num) + namespace[key] = field.get # later, get value by calling this shortcut - val_code = 'schema.{}(obj)'.format(getter_name) + val_code = 'namespace["{}"](obj)'.format(key) else: # neither constant val nor getter: try to get value via attr @@ -365,17 +362,17 @@ def _field_value_code_attrs(field, field_name, field_num): if hasattr(field, 'pack'): # add pack-shortcut to attributes - packer_name = '__pack_{}'.format(field_num) - attrs[packer_name] = field.pack + key = 'pack{}'.format(field_num) + namespace[key] = field.pack # later, pass result field value to this shortcut - val_code = 'schema.{}({})'.format(packer_name, val_code) + val_code = 'namespace["{}"]({})'.format(key, val_code) - return val_code, attrs + return val_code, namespace @staticmethod - def _dump_function_code_attrs(fields, ordered): - '''Get code/attrs for a customized dump function + def _dump_function_code_ns(fields, ordered): + '''Get code/namespace-dict for a customized dump function Args: fields: An ordered mapping of field names to fields @@ -384,23 +381,17 @@ def _dump_function_code_attrs(fields, ordered): objects, else make it return ordinary dict objects Returns: A tuple consisting of: a) a fragment of Python code to - define a dump function for a schema and b) a mapping of attribute - names to attribute values that the schema instance *must have* for - this code to work. - - Note that even though the resulting code might *look* like a method - definition at first glance, it is *not*, since it will tied to a - specific Schema instance instead of to the Schema class like a method - would be. This means that ten Schema objects will have ten separate - dump functions associated with them. + define a dump function for a schema instance and b) a + namespace-dict containing shortcuts to necessary values/functions + for this code fragment to work. ''' - attrs = {} + namespace = {} # get correct templates if ordered: func_tpl = textwrap.dedent( '''\ - def _dump_function(schema, obj): + def _dump_function(obj, namespace): return OrderedDict([ {contents} ]) @@ -410,7 +401,7 @@ def _dump_function(schema, obj): else: func_tpl = textwrap.dedent( '''\ - def _dump_function(schema, obj): + def _dump_function(obj, namespace): return {{ {contents} }} @@ -423,10 +414,10 @@ def _dump_function(schema, obj): # iterate over fields to fill up entries for field_num, (field_name, field) in enumerate(fields.items()): - val_code, field_attrs = Schema._field_value_code_attrs( + val_code, val_namespace = Schema._field_value_code_ns( field, field_name, field_num ) - attrs.update(field_attrs) + namespace.update(val_namespace) # try to guard against code injection via quotes in key key = str(field_name) @@ -439,7 +430,7 @@ def _dump_function(schema, obj): sep = ',\n ' code = func_tpl.format(contents=sep.join(entries)) - return code, attrs + return code, namespace def dump(self, obj, *, many=None): '''Return a marshalled representation of obj. @@ -460,9 +451,10 @@ def dump(self, obj, *, many=None): ''' dump_function = self._dump_function + namespace = self._namespace if many is None: many = self.many if many: - return [dump_function(self, o) for o in obj] + return [dump_function(o, namespace) for o in obj] else: - return dump_function(self, obj) + return dump_function(obj, namespace) diff --git a/tests/test_schema.py b/tests/test_schema.py index 049812d..e819a50 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -671,12 +671,12 @@ class TestSchema(schema.Schema): foo = fields.String(attr='foo_attr') bar = fields.String() - code, ns = schema.Schema._dump_function_code_attrs( + code, ns = schema.Schema._dump_function_code_ns( TestSchema.__fields__, ordered=False ) expected = dedent( '''\ - def _dump_function(schema, obj): + def _dump_function(obj, namespace): return { "foo": obj.foo_attr, "bar": obj.bar @@ -685,12 +685,12 @@ def _dump_function(schema, obj): ) assert code == expected - code, ns = schema.Schema._dump_function_code_attrs( + code, ns = schema.Schema._dump_function_code_ns( TestSchema.__fields__, ordered=True ) expected = dedent( '''\ - def _dump_function(schema, obj): + def _dump_function(obj, namespace): return OrderedDict([ ("foo", obj.foo_attr), ("bar", obj.bar) From 2d8e13ae4e88d32f4b8bbfb3d8394cadbbe59454 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 18:24:28 +0100 Subject: [PATCH 011/107] define dump_function in custom-made namespace instead of providing namespace to function as a parameter --- lima/schema.py | 39 ++++++++++++++++++++++----------------- tests/test_schema.py | 4 ++-- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index e1ae93d..e1f7ad4 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -310,9 +310,15 @@ def __init__(self, # get code/namespace for the customized dump function code, namespace = self._dump_function_code_ns(fields, ordered) - # this defines _dump_function in self's namespace - exec(code, globals(), self.__dict__) - self._namespace = namespace + # prepare namespace: provide OrderedDict and nothing else + namespace['OrderedDict'] = OrderedDict + namespace['__builtins__'] = {} + + # this defines _dump_function inside namespace + exec(code, namespace) + + # and set _dump_function attr to the new function + self._dump_function = namespace['_dump_function'] @staticmethod def _field_value_code_ns(field, field_name, field_num): @@ -334,19 +340,19 @@ def _field_value_code_ns(field, field_name, field_num): namespace = {} if hasattr(field, 'val'): # add constant-field-value-shortcut to namespace - key = 'val{}'.format(field_num) - namespace[key] = field.val + name = 'val{}'.format(field_num) + namespace[name] = field.val # later, get value using this shortcut - val_code = 'namespace["{}"]'.format(key) + val_code = name elif hasattr(field, 'get'): # add getter-shortcut to namespace - key = 'get{}'.format(field_num) - namespace[key] = field.get + name = 'get{}'.format(field_num) + namespace[name] = field.get # later, get value by calling this shortcut - val_code = 'namespace["{}"](obj)'.format(key) + val_code = '{}(obj)'.format(name) else: # neither constant val nor getter: try to get value via attr @@ -362,11 +368,11 @@ def _field_value_code_ns(field, field_name, field_num): if hasattr(field, 'pack'): # add pack-shortcut to attributes - key = 'pack{}'.format(field_num) - namespace[key] = field.pack + name = 'pack{}'.format(field_num) + namespace[name] = field.pack # later, pass result field value to this shortcut - val_code = 'namespace["{}"]({})'.format(key, val_code) + val_code = '{}({})'.format(name, val_code) return val_code, namespace @@ -391,7 +397,7 @@ def _dump_function_code_ns(fields, ordered): if ordered: func_tpl = textwrap.dedent( '''\ - def _dump_function(obj, namespace): + def _dump_function(obj): return OrderedDict([ {contents} ]) @@ -401,7 +407,7 @@ def _dump_function(obj, namespace): else: func_tpl = textwrap.dedent( '''\ - def _dump_function(obj, namespace): + def _dump_function(obj): return {{ {contents} }} @@ -451,10 +457,9 @@ def dump(self, obj, *, many=None): ''' dump_function = self._dump_function - namespace = self._namespace if many is None: many = self.many if many: - return [dump_function(o, namespace) for o in obj] + return [dump_function(o) for o in obj] else: - return dump_function(obj, namespace) + return dump_function(obj) diff --git a/tests/test_schema.py b/tests/test_schema.py index e819a50..665bae4 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -676,7 +676,7 @@ class TestSchema(schema.Schema): ) expected = dedent( '''\ - def _dump_function(obj, namespace): + def _dump_function(obj): return { "foo": obj.foo_attr, "bar": obj.bar @@ -690,7 +690,7 @@ def _dump_function(obj, namespace): ) expected = dedent( '''\ - def _dump_function(obj, namespace): + def _dump_function(obj): return OrderedDict([ ("foo", obj.foo_attr), ("bar", obj.bar) From 2e96c0fa8d6aff12d67f908411c0c1d334a0dce7 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 19:35:05 +0100 Subject: [PATCH 012/107] Schema: Move loop into generated dump-function. This speeds up serializing of collections a little. --- lima/schema.py | 25 ++++++++++--------------- tests/test_schema.py | 18 ++++++++---------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index e1f7ad4..1bf2774 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -397,20 +397,20 @@ def _dump_function_code_ns(fields, ordered): if ordered: func_tpl = textwrap.dedent( '''\ - def _dump_function(obj): - return OrderedDict([ - {contents} - ]) + def _dump_function(obj, many): + if many: + return [OrderedDict([{contents}]) for obj in obj] + return OrderedDict([{contents}]) ''' ) entry_tpl = '("{key}", {get_val})' else: func_tpl = textwrap.dedent( '''\ - def _dump_function(obj): - return {{ - {contents} - }} + def _dump_function(obj, many): + if many: + return [{{{contents}}} for obj in obj] + return {{{contents}}} ''' ) entry_tpl = '"{key}": {get_val}' @@ -433,9 +433,7 @@ def _dump_function(obj): # add entry entries.append(entry_tpl.format(key=key, get_val=val_code)) - - sep = ',\n ' - code = func_tpl.format(contents=sep.join(entries)) + code = func_tpl.format(contents=', '.join(entries)) return code, namespace def dump(self, obj, *, many=None): @@ -459,7 +457,4 @@ def dump(self, obj, *, many=None): dump_function = self._dump_function if many is None: many = self.many - if many: - return [dump_function(o) for o in obj] - else: - return dump_function(obj) + return dump_function(obj, many) diff --git a/tests/test_schema.py b/tests/test_schema.py index 665bae4..01a613e 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -676,11 +676,10 @@ class TestSchema(schema.Schema): ) expected = dedent( '''\ - def _dump_function(obj): - return { - "foo": obj.foo_attr, - "bar": obj.bar - } + def _dump_function(obj, many): + if many: + return [{"foo": obj.foo_attr, "bar": obj.bar} for obj in obj] + return {"foo": obj.foo_attr, "bar": obj.bar} ''' ) assert code == expected @@ -690,11 +689,10 @@ def _dump_function(obj): ) expected = dedent( '''\ - def _dump_function(obj): - return OrderedDict([ - ("foo", obj.foo_attr), - ("bar", obj.bar) - ]) + def _dump_function(obj, many): + if many: + return [OrderedDict([("foo", obj.foo_attr), ("bar", obj.bar)]) for obj in obj] + return OrderedDict([("foo", obj.foo_attr), ("bar", obj.bar)]) ''' ) assert code == expected From 64f103af5a54ca7a0dc22f4d0e402aa769134e4b Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 19:58:22 +0100 Subject: [PATCH 013/107] Change some comments. --- lima/schema.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 1bf2774..d164b0d 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -310,14 +310,12 @@ def __init__(self, # get code/namespace for the customized dump function code, namespace = self._dump_function_code_ns(fields, ordered) - # prepare namespace: provide OrderedDict and nothing else + # namespace for dump function: provide OrderedDict and nothing else namespace['OrderedDict'] = OrderedDict namespace['__builtins__'] = {} - # this defines _dump_function inside namespace + # define _dump_function inside namespace, then set _dump_function attr exec(code, namespace) - - # and set _dump_function attr to the new function self._dump_function = namespace['_dump_function'] @staticmethod From d822c94a8953e1b46f5f0cb252b71dc18453241c Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 20:19:23 +0100 Subject: [PATCH 014/107] schema: Update comments/docstrings. --- lima/schema.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index d164b0d..e91e2d4 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -307,32 +307,31 @@ def __init__(self, self._ordered = ordered self.many = many - # get code/namespace for the customized dump function + # get code and namespace for the customized dump function code, namespace = self._dump_function_code_ns(fields, ordered) # namespace for dump function: provide OrderedDict and nothing else namespace['OrderedDict'] = OrderedDict namespace['__builtins__'] = {} - # define _dump_function inside namespace, then set _dump_function attr + # define dump function inside namespace, then set _dump_function attr exec(code, namespace) self._dump_function = namespace['_dump_function'] @staticmethod def _field_value_code_ns(field, field_name, field_num): - '''Get code/namespace-dict to determine a field's serialized value. + '''Get code and namespace dict to determine a field's serialized value. Args: field: A :class:`lima.fields.Field` instance. - field_name: The name (key) of the field instance. + field_name: The name (key) of the field. field_num: A schema-wide unique number for the field. Returns: A tuple consisting of: a) a fragment of Python code to - determine the field's value and b) a namespace-dict containing - shortcuts to necessary values/functions for this code fragment to - work. + determine the field's value and b) a namespace dict containing + objects necessary for this code fragment to work. ''' namespace = {} @@ -376,7 +375,7 @@ def _field_value_code_ns(field, field_name, field_num): @staticmethod def _dump_function_code_ns(fields, ordered): - '''Get code/namespace-dict for a customized dump function + '''Get code and namespace dict for a customized dump function Args: fields: An ordered mapping of field names to fields @@ -384,14 +383,14 @@ def _dump_function_code_ns(fields, ordered): ordered: If True, make the resulting function return OrderedDict objects, else make it return ordinary dict objects - Returns: A tuple consisting of: a) a fragment of Python code to - define a dump function for a schema instance and b) a - namespace-dict containing shortcuts to necessary values/functions - for this code fragment to work. + Returns: A tuple consisting of: a) Python code to define a dump + function for a schema instance and b) a namespace dict containing + objects necessary for this code to work. ''' namespace = {} - # get correct templates + + # Get correct templates depending on "ordered" if ordered: func_tpl = textwrap.dedent( '''\ From 9d56824dcfb74fb3ec1a32d0ceb6a9ade467bed1 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 20:22:24 +0100 Subject: [PATCH 015/107] schema: Change namespace def of generated dump function. Add OrderedDict to namespace at the correct location (was in Schema.__init__, now at method where rest of namespace is determined). --- lima/schema.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index e91e2d4..e50c553 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -310,8 +310,7 @@ def __init__(self, # get code and namespace for the customized dump function code, namespace = self._dump_function_code_ns(fields, ordered) - # namespace for dump function: provide OrderedDict and nothing else - namespace['OrderedDict'] = OrderedDict + # namespace for dump function: don't provide any builtins namespace['__builtins__'] = {} # define dump function inside namespace, then set _dump_function attr @@ -388,7 +387,8 @@ def _dump_function_code_ns(fields, ordered): objects necessary for this code to work. ''' - namespace = {} + # Namespace must contain OrderedDict if we want ordered output. + namespace = {'OrderedDict': OrderedDict} if ordered else {} # Get correct templates depending on "ordered" if ordered: From 4c8da7c3504ba036a4248546a1507b3c3ce81874 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 20:28:11 +0100 Subject: [PATCH 016/107] Update changelog. --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2183ed9..8b10e45 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,8 @@ Changelog - Remove ``fields.type_mapping``. Use ``fields.TYPE_MAPPING`` instead. +- Overall cleanup and improvements. + 0.3.1 (2014-11-11) ================== From 101905d28bf820e572e3723d0b7efc1fce092435 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 20:30:44 +0100 Subject: [PATCH 017/107] Tests: PEP8 (where easily possible). --- tests/test_schema.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_schema.py b/tests/test_schema.py index 01a613e..9892fc7 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -35,6 +35,7 @@ class PersonSchema(schema.Schema): class NonLocalSchema(schema.Schema): foo = fields.String() + class TestHelperFunctions: '''Class collecting tests of helper functions.''' From 5beeea6cbc187f27bc7c77f80076cbfeb63dabb9 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 21:43:13 +0100 Subject: [PATCH 018/107] Schema: Call static method differently. --- lima/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lima/schema.py b/lima/schema.py index e50c553..0ffaecd 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -308,7 +308,7 @@ def __init__(self, self.many = many # get code and namespace for the customized dump function - code, namespace = self._dump_function_code_ns(fields, ordered) + code, namespace = Schema._dump_function_code_ns(fields, ordered) # namespace for dump function: don't provide any builtins namespace['__builtins__'] = {} From d722e2a29d72c1f31966f5c1cde908937d6436f8 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 22:04:23 +0100 Subject: [PATCH 019/107] Schema: Simplify dump() method. --- lima/schema.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 0ffaecd..2bf4a57 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -451,7 +451,5 @@ def dump(self, obj, *, many=None): collection of objects was marshalled) ''' - dump_function = self._dump_function - if many is None: - many = self.many - return dump_function(obj, many) + # this more or less just calls the instance-specific dump function + return self._dump_function(obj, self.many if many is None else many) From 1ccb493f6c100930ef3a14fa843edc1580639390 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Mon, 17 Nov 2014 01:58:48 +0100 Subject: [PATCH 020/107] schema: Small internal refactoring (+ tests). --- lima/schema.py | 25 +++++++++++++------------ tests/test_schema.py | 12 ++++++------ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 2bf4a57..12b10c8 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -307,15 +307,15 @@ def __init__(self, self._ordered = ordered self.many = many - # get code and namespace for the customized dump function - code, namespace = Schema._dump_function_code_ns(fields, ordered) + # get code and namespace for customized dump function + code, namespace = Schema._dump_fields_code_ns(fields, ordered) - # namespace for dump function: don't provide any builtins + # dump function namespace: don't provide any builtins namespace['__builtins__'] = {} - # define dump function inside namespace, then set _dump_function attr + # define dump function inside namespace, then set _dump_fields attr exec(code, namespace) - self._dump_function = namespace['_dump_function'] + self._dump_fields = namespace['dump_fields'] @staticmethod def _field_value_code_ns(field, field_name, field_num): @@ -373,8 +373,8 @@ def _field_value_code_ns(field, field_name, field_num): return val_code, namespace @staticmethod - def _dump_function_code_ns(fields, ordered): - '''Get code and namespace dict for a customized dump function + def _dump_fields_code_ns(fields, ordered): + '''Get code and namespace dict for a customized dump_fields function. Args: fields: An ordered mapping of field names to fields @@ -383,8 +383,8 @@ def _dump_function_code_ns(fields, ordered): objects, else make it return ordinary dict objects Returns: A tuple consisting of: a) Python code to define a dump - function for a schema instance and b) a namespace dict containing - objects necessary for this code to work. + function for fields and b) a namespace dict containing objects + necessary for this code to work. ''' # Namespace must contain OrderedDict if we want ordered output. @@ -394,7 +394,7 @@ def _dump_function_code_ns(fields, ordered): if ordered: func_tpl = textwrap.dedent( '''\ - def _dump_function(obj, many): + def dump_fields(obj, many): if many: return [OrderedDict([{contents}]) for obj in obj] return OrderedDict([{contents}]) @@ -404,7 +404,7 @@ def _dump_function(obj, many): else: func_tpl = textwrap.dedent( '''\ - def _dump_function(obj, many): + def dump_fields(obj, many): if many: return [{{{contents}}} for obj in obj] return {{{contents}}} @@ -430,6 +430,7 @@ def _dump_function(obj, many): # add entry entries.append(entry_tpl.format(key=key, get_val=val_code)) + code = func_tpl.format(contents=', '.join(entries)) return code, namespace @@ -452,4 +453,4 @@ def dump(self, obj, *, many=None): ''' # this more or less just calls the instance-specific dump function - return self._dump_function(obj, self.many if many is None else many) + return self._dump_fields(obj, self.many if many is None else many) diff --git a/tests/test_schema.py b/tests/test_schema.py index 9892fc7..66c9e5b 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -664,20 +664,20 @@ class TestSchema(schema.Schema): with pytest.raises(ValueError): test_schema = TestSchema() - def test_dump_function_code(self): - '''Test if _dump_function_code gets a simple function right.''' + def test_dump_fields_code(self): + '''Test if _dump_fields_code_ns gets a simple function right.''' from textwrap import dedent class TestSchema(schema.Schema): foo = fields.String(attr='foo_attr') bar = fields.String() - code, ns = schema.Schema._dump_function_code_ns( + code, ns = schema.Schema._dump_fields_code_ns( TestSchema.__fields__, ordered=False ) expected = dedent( '''\ - def _dump_function(obj, many): + def dump_fields(obj, many): if many: return [{"foo": obj.foo_attr, "bar": obj.bar} for obj in obj] return {"foo": obj.foo_attr, "bar": obj.bar} @@ -685,12 +685,12 @@ def _dump_function(obj, many): ) assert code == expected - code, ns = schema.Schema._dump_function_code_ns( + code, ns = schema.Schema._dump_fields_code_ns( TestSchema.__fields__, ordered=True ) expected = dedent( '''\ - def _dump_function(obj, many): + def dump_fields(obj, many): if many: return [OrderedDict([("foo", obj.foo_attr), ("bar", obj.bar)]) for obj in obj] return OrderedDict([("foo", obj.foo_attr), ("bar", obj.bar)]) From ed5b5535b59c639badee981d15f8e48bf1dd6e94 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sat, 22 Nov 2014 15:17:09 +0100 Subject: [PATCH 021/107] Factor out dynamic function definition. Add test. --- lima/schema.py | 8 +------- lima/util.py | 27 +++++++++++++++++++++++++++ tests/test_util.py | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 12b10c8..7339ed7 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -309,13 +309,7 @@ def __init__(self, # get code and namespace for customized dump function code, namespace = Schema._dump_fields_code_ns(fields, ordered) - - # dump function namespace: don't provide any builtins - namespace['__builtins__'] = {} - - # define dump function inside namespace, then set _dump_fields attr - exec(code, namespace) - self._dump_fields = namespace['dump_fields'] + self._dump_fields = util.make_function('dump_fields', code, namespace) @staticmethod def _field_value_code_ns(field, field_name, field_num): diff --git a/lima/util.py b/lima/util.py index b4557c8..7bb70fe 100644 --- a/lima/util.py +++ b/lima/util.py @@ -123,3 +123,30 @@ def ensure_only_instances_of(collection, cls): found = [obj for obj in collection if not isinstance(obj, cls)] if found: raise TypeError('No instances of {}: {!r}'.format(cls, found)) + + +def make_function(name, code, globals=None): + '''Return a function created by executing a code string in a new namespace. + + This is not much more than a wrapper around :func:`exec`. + + Args: + name: The name of the function to create. Must match the function name + in ``code``. + + code: A String containing the function definition code. The name of the + function must match ``name``. + + globals: A dict of globals to mix into the new function's namespace. + ``__builtins__`` must be provided explicitly if required. + + .. warning: + + All pitfalls of using :func:`exec` apply to this function as well. + + ''' + namespace = dict(__builtins__={}) + if globals: + namespace.update(globals) + exec(code, namespace) + return namespace[name] diff --git a/tests/test_util.py b/tests/test_util.py index e4a36c4..1a364f2 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -126,3 +126,21 @@ def test_only_instances_of(): with pytest.raises(TypeError): util.ensure_only_instances_of([1, 2, 3.3, 4], int) + +def test_make_function(): + code = 'def func_in_namespace(): return 1' + my_function = util.make_function('func_in_namespace', code) + assert(callable(my_function)) + assert(my_function() == 1) + # make sure the new name didn't leak out into globals/locals + with pytest.raises(NameError): + func_in_namespace + + code = 'def func_in_namespace(): return a' + namespace = dict(a=42) + my_function = util.make_function('func_in_namespace', code, namespace) + assert(callable(my_function)) + assert(my_function() == 42) + # make sure the new name didn't leak out of namespace + with pytest.raises(NameError): + func_in_namespace From 1b23a96d3cf934d9cd5933ec8fb595174c491c3b Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sat, 22 Nov 2014 15:40:56 +0100 Subject: [PATCH 022/107] util: Rename function parameter. to not clash with builtin name 'globals'. --- lima/util.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lima/util.py b/lima/util.py index 7bb70fe..a07ec67 100644 --- a/lima/util.py +++ b/lima/util.py @@ -125,7 +125,7 @@ def ensure_only_instances_of(collection, cls): raise TypeError('No instances of {}: {!r}'.format(cls, found)) -def make_function(name, code, globals=None): +def make_function(name, code, globals_=None): '''Return a function created by executing a code string in a new namespace. This is not much more than a wrapper around :func:`exec`. @@ -137,7 +137,7 @@ def make_function(name, code, globals=None): code: A String containing the function definition code. The name of the function must match ``name``. - globals: A dict of globals to mix into the new function's namespace. + globals_: A dict of globals to mix into the new function's namespace. ``__builtins__`` must be provided explicitly if required. .. warning: @@ -146,7 +146,7 @@ def make_function(name, code, globals=None): ''' namespace = dict(__builtins__={}) - if globals: - namespace.update(globals) + if globals_: + namespace.update(globals_) exec(code, namespace) return namespace[name] From 39e0d13e29743ca2cbfcf387c012235b9f65d9f2 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sat, 22 Nov 2014 16:18:27 +0100 Subject: [PATCH 023/107] Update changelog. (Be more specific about improvements.) --- CHANGELOG.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8b10e45..67a2b7b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,7 +11,9 @@ Changelog - Remove ``fields.type_mapping``. Use ``fields.TYPE_MAPPING`` instead. -- Overall cleanup and improvements. +- Small speed improvement when serializing collections. + +- Overall cleanup. 0.3.1 (2014-11-11) From eb255de079d713602df722fa48af242567e3cfe9 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 23 Nov 2014 12:09:00 +0100 Subject: [PATCH 024/107] util: tiny internal change. Explicit is better than implicit --- lima/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lima/schema.py b/lima/schema.py index 7339ed7..9e1b701 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -35,7 +35,7 @@ def _fields_from_bases(bases): def _fields_include(fields, include): '''Return a copy of fields with fields in include included.''' util.ensure_mapping(include) - util.ensure_only_instances_of(include, str) + util.ensure_only_instances_of(include.keys(), str) util.ensure_only_instances_of(include.values(), abc.FieldABC) result = fields.copy() result.update(include) From 731b8942cdb526aed407a5fee56915552a97d4cc Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 23 Nov 2014 12:20:14 +0100 Subject: [PATCH 025/107] schema: Replace two loops with comprehensions. --- lima/schema.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 9e1b701..47bb69b 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -46,22 +46,14 @@ def _fields_exclude(fields, remove): '''Return a copy of fields with fields mentioned in exclude missing.''' util.ensure_only_instances_of(remove, str) util.ensure_subset_of(remove, fields) - result = OrderedDict() - for k, v in fields.items(): - if k not in remove: - result[k] = v - return result + return OrderedDict([(k, v) for k, v in fields.items() if k not in remove]) def _fields_only(fields, only): '''Return a copy of fields containing only fields mentioned in only.''' util.ensure_only_instances_of(only, str) util.ensure_subset_of(only, fields) - result = OrderedDict() - for k, v in fields.items(): - if k in only: - result[k] = v - return result + return OrderedDict([(k, v) for k, v in fields.items() if k in only]) def _mangle_name(name): From efa4dfc8362e1ee3d02748f5ddf8b3d374f18359 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 14 Nov 2014 16:03:07 +0100 Subject: [PATCH 026/107] Field: Add is_oid parameter. --- lima/fields.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lima/fields.py b/lima/fields.py index 0b2bdef..756829f 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -17,9 +17,16 @@ class Field(abc.FieldABC): val: An optional constant value for the field. + is_oid: If ``True``, marks this field as a field whose value can be + used to identify an object. A schema must not end up with more than + one identifier field. + .. versionadded:: 0.3 The ``val`` parameter. + .. versionadded:: 0.4 + The ``is_oid`` parameter. + :attr:`attr`, :attr:`get` and :attr:`val` are mutually exclusive. When a :class:`Field` object ends up with two or more of the attributes @@ -36,7 +43,7 @@ class Field(abc.FieldABC): instance. ''' - def __init__(self, *, attr=None, get=None, val=None): + def __init__(self, *, attr=None, get=None, val=None, is_oid=False): if sum(v is not None for v in (attr, get, val)) > 1: raise ValueError('attr, get and val are mutually exclusive.') @@ -52,6 +59,8 @@ def __init__(self, *, attr=None, get=None, val=None): elif val is not None: self.val = val + self.is_oid = is_oid + class Boolean(Field): '''A boolean field. From cb951465708c99d1ddd6d42822e208f0bbdee6a0 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sat, 15 Nov 2014 02:56:10 +0100 Subject: [PATCH 027/107] Field: rename is_oid to oid. --- lima/fields.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index 756829f..493ceb8 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -17,15 +17,15 @@ class Field(abc.FieldABC): val: An optional constant value for the field. - is_oid: If ``True``, marks this field as a field whose value can be - used to identify an object. A schema must not end up with more than - one identifier field. + oid: If ``True``, marks this field as a field whose value can be used + to identify an object. A schema must not end up with more than one + identifier field. .. versionadded:: 0.3 The ``val`` parameter. .. versionadded:: 0.4 - The ``is_oid`` parameter. + The ``oid`` parameter. :attr:`attr`, :attr:`get` and :attr:`val` are mutually exclusive. @@ -43,7 +43,7 @@ class Field(abc.FieldABC): instance. ''' - def __init__(self, *, attr=None, get=None, val=None, is_oid=False): + def __init__(self, *, attr=None, get=None, val=None, oid=False): if sum(v is not None for v in (attr, get, val)) > 1: raise ValueError('attr, get and val are mutually exclusive.') @@ -59,7 +59,7 @@ def __init__(self, *, attr=None, get=None, val=None, is_oid=False): elif val is not None: self.val = val - self.is_oid = is_oid + self.oid = oid class Boolean(Field): From 459b62408e152219eeeb0503de04ca5f63b5a00d Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 14 Nov 2014 14:57:35 +0100 Subject: [PATCH 028/107] fields: Rename "Nested" to "Embed". Later on, we'll be able to embed or link to other objects. --- lima/fields.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index 493ceb8..4fa6278 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -148,15 +148,15 @@ def pack(val): return val.isoformat() if val is not None else None -class Nested(Field): - '''A Field referencing another object with it's respective schema. +class Embed(Field): + '''A Field to embed linked object(s). Args: schema: The schema of the referenced object. This can be specified via a schema *object,* a schema *class* (that will get instantiated immediately) or the qualified *name* of a schema class (for when the named schema has not been defined at the time of the - :class:`Nested` object's creation). If two or more schema classes + :class:`Embed` object's creation). If two or more schema classes with the same name exist in different modules, the schema class name has to be fully module-qualified (see the :ref:`entry on class names ` for clarification of these concepts). @@ -174,40 +174,39 @@ class Nested(Field): constructor when the time has come to instance it. Must be empty if ``schema`` is a :class:`lima.schema.Schema` object. - .. versionadded:: 0.3 - The ``val`` parameter. - Raises: ValueError: If ``kwargs`` are specified even if ``schema`` is a :class:`lima.schema.Schema` *object.* + TypeError: If ``schema`` has the wrong type. + Examples: :: # refer to PersonSchema class - author = Nested(schema=PersonSchema) + author = Embed(schema=PersonSchema) # refer to PersonSchema class with additional params - artists = Nested(schema=PersonSchema, exclude='email', many=True) + artists = Embed(schema=PersonSchema, exclude='email', many=True) # refer to PersonSchema object - author = Nested(schema=PersonSchema()) + author = Embed(schema=PersonSchema()) # refer to PersonSchema object with additional params - # (note that Nested() gets no kwargs) - artists = Nested(schema=PersonSchema(exclude='email', many=true)) + # (note that Embed() itself gets no kwargs) + artists = Embed(schema=PersonSchema(exclude='email', many=true)) # refer to PersonSchema per name - author = Nested(schema='PersonSchema') + author = Embed(schema='PersonSchema') # refer to PersonSchema per name with additional params - author = Nested(schema='PersonSchema', exclude='email', many=True) + author = Embed(schema='PersonSchema', exclude='email', many=True) # refer to PersonSchema per module-qualified name # (in case of ambiguity) - author = Nested(schema='project.persons.PersonSchema') + author = Embed(schema='project.persons.PersonSchema') # specify attr name as well - user = Nested(attr='login_user', schema=PersonSchema) + user = Embed(attr='login_user', schema=PersonSchema) ''' def __init__(self, *, schema, attr=None, get=None, val=None, **kwargs): @@ -261,6 +260,15 @@ def pack(self, val): return self.schema_inst.dump(val) if val is not None else None +Nested = Embed +'''A Field to embed linked object(s) + +:class:`Nested` is the old name of class :class:`Embed`. + +.. deprecated:: 0.4 + Will be removed in 0.5. Use :class:`Embed` instead''' + + TYPE_MAPPING = { bool: Boolean, float: Float, From 5813511cebb1b6fdb3fb3704c88aed9afab50444 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 14 Nov 2014 16:06:43 +0100 Subject: [PATCH 029/107] tests: Use "Embed" instead of "Nested" --- tests/test_dump.py | 34 +++++++++++++++++----------------- tests/test_fields.py | 10 +++++----- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/test_dump.py b/tests/test_dump.py index aac7add..e02f937 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -41,26 +41,26 @@ class KnightSchema(schema.Schema): name = fields.String() -class KingSchemaNestedStr(KnightSchema): +class KingSchemaEmbedStr(KnightSchema): title = fields.String() - subjects = fields.Nested(schema=__name__ + '.KnightSchema', many=True) + subjects = fields.Embed(schema=__name__ + '.KnightSchema', many=True) -class KingSchemaNestedClass(KnightSchema): +class KingSchemaEmbedClass(KnightSchema): title = fields.String() - subjects = fields.Nested(schema=KnightSchema, many=True) + subjects = fields.Embed(schema=KnightSchema, many=True) -class KingSchemaNestedObject(KnightSchema): +class KingSchemaEmbedObject(KnightSchema): some_schema_object = KnightSchema(many=True) title = fields.String() - subjects = fields.Nested(schema=some_schema_object) + subjects = fields.Embed(schema=some_schema_object) class SelfReferentialKingSchema(schema.Schema): name = fields.String() - boss = fields.Nested(schema=__name__ + '.SelfReferentialKingSchema', + boss = fields.Embed(schema=__name__ + '.SelfReferentialKingSchema', exclude='boss') @@ -155,11 +155,11 @@ def test_many_dump2(knights): @pytest.mark.parametrize('schema_cls', - [KingSchemaNestedStr, - KingSchemaNestedClass, - KingSchemaNestedObject]) -def test_dump_nested_schema(schema_cls, king, knights): - '''Test with nested Schema specified as a String''' + [KingSchemaEmbedStr, + KingSchemaEmbedClass, + KingSchemaEmbedObject]) +def test_dump_embed_schema(schema_cls, king, knights): + '''Test with embed Schema specified as a String''' king_schema = schema_cls() king.subjects = knights expected = { @@ -174,22 +174,22 @@ def test_dump_nested_schema(schema_cls, king, knights): assert king_schema.dump(king) == expected -def test_dump_nested_schema_instance_double_kwargs_error(king, knights): +def test_dump_embed_schema_instance_double_kwargs_error(king, knights): '''Test for ValueError when providing unnecssary kwargs.''' class KnightSchema(schema.Schema): name = fields.String() - nested_schema = KnightSchema(many=True) + embed_schema = KnightSchema(many=True) with pytest.raises(ValueError): class KingSchema(KnightSchema): title = fields.String() - subjects = fields.Nested(schema=nested_schema, many=True) + subjects = fields.Embed(schema=embed_schema, many=True) -def test_dump_nested_schema_self(king): - '''Test with nested Schema specified as a String''' +def test_dump_embed_schema_self(king): + '''Test with embedded Schema specified as a String''' king_schema = SelfReferentialKingSchema() king.boss = king expected = { diff --git a/tests/test_fields.py b/tests/test_fields.py index 609a6d9..2a88ff5 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -106,14 +106,14 @@ def test_datetime_pack(): assert fields.DateTime.pack(datetime) == expected -# tests of nested fields assume a lot of the other stuff also works +# tests of embedded fields assume a lot of the other stuff also works -def test_nested_by_name(): - field = fields.Nested(schema='NonExistentSchema') +def test_embed_by_name(): + field = fields.Embed(schema='NonExistentSchema') assert field.schema_name == 'NonExistentSchema' -def test_nested_error_on_illegal_schema_spec(): +def test_embed_error_on_illegal_schema_spec(): with pytest.raises(TypeError): - field = fields.Nested(schema=123) + field = fields.Embed(schema=123) From dd6ec2586af38ddcdfe091d9e355df40d1c47923 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 20:41:27 +0100 Subject: [PATCH 030/107] docs: rename "Nested Data" to "Linked Data" ... ... and replace references to fields.Nested with references to fields.Embed. --- docs/fields.rst | 2 +- docs/index.rst | 2 +- docs/{nested_data.rst => linked_data.rst} | 42 +++++++++++------------ 3 files changed, 23 insertions(+), 23 deletions(-) rename docs/{nested_data.rst => linked_data.rst} (86%) diff --git a/docs/fields.rst b/docs/fields.rst index 80f42e6..2e21fa0 100644 --- a/docs/fields.rst +++ b/docs/fields.rst @@ -209,7 +209,7 @@ Or we can change how already supported data types are marshalled: Also, don't try to override an existing instance method with a static method. Have a look at the source if in doubt (currently only - :class:`lima.fields.Nested` implements :meth:`pack` as an instance method. + :class:`lima.fields.Embed` implements :meth:`pack` as an instance method. .. _data_validation: diff --git a/docs/index.rst b/docs/index.rst index 518d0c4..61fb228 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -69,7 +69,7 @@ Documentation first_steps schemas fields - nested_data + linked_data advanced api project_info diff --git a/docs/nested_data.rst b/docs/linked_data.rst similarity index 86% rename from docs/nested_data.rst rename to docs/linked_data.rst index c725f33..a5e7be7 100644 --- a/docs/nested_data.rst +++ b/docs/linked_data.rst @@ -1,5 +1,5 @@ =========== -Nested Data +Linked Data =========== Most ORMs represent linked objects nested under an attribute of the linking @@ -35,7 +35,7 @@ To serialize this construct, we have to tell lima that a :class:`Book` object has a :class:`Person` object nested inside, designated via the :attr:`author` attribute. -For this we use a field of type :class:`lima.fields.Nested` and tell lima what +For this we use a field of type :class:`lima.fields.Embed` and tell lima what data to expect by providing the ``schema`` parameter: .. code-block:: python @@ -49,7 +49,7 @@ data to expect by providing the ``schema`` parameter: class BookSchema(Schema): title = fields.String() - author = fields.Nested(schema=PersonSchema) + author = fields.Embed(schema=PersonSchema) schema = BookSchema() schema.dump(book) @@ -57,9 +57,9 @@ data to expect by providing the ``schema`` parameter: # 'title': The Old Man and the Sea'} Along with the mandatory keyword-only argument ``schema``, -:class:`lima.fields.Nested` accepts the optional keyword-only-arguments we +:class:`lima.fields.Embed` accepts the optional keyword-only-arguments we already know (``attr`` or ``get``). All other keyword arguments provided to -:class:`lima.fields.Nested` get passed through to the constructor of the nested +:class:`lima.fields.Embed` get passed through to the constructor of the linked schema. This allows us to do stuff like the following: .. code-block:: python @@ -67,7 +67,7 @@ schema. This allows us to do stuff like the following: class BookSchema(Schema): title = fields.String() - author = fields.Nested(schema=PersonSchema, only='last_name') + author = fields.Embed(schema=PersonSchema, only='last_name') schema = BookSchema() schema.dump(book) @@ -122,17 +122,17 @@ side that links back. To overcome the problem of definition order, lima supports lazy evaluation of schemas. Just pass the *qualified name* (or the *fully module-qualified name*) -of a schema class to :class:`lima.fields.Nested` instead of the class itself: +of a schema class to :class:`lima.fields.Embed` instead of the class itself: .. code-block:: python :emphasize-lines: 2,6,12,16 class AuthorSchema(PersonSchema): - bestseller = fields.Nested(schema='BookSchema', exclude='author') + bestseller = fields.Embed(schema='BookSchema', exclude='author') class BookSchema(Schema): title = fields.String() - author = fields.Nested(schema=AuthorSchema, exclude='bestseller') + author = fields.Embed(schema=AuthorSchema, exclude='bestseller') author_schema = AuthorSchema() author_schema.dump(author) @@ -200,7 +200,7 @@ for models that link to themselves - everything works as you'd expect: self.spouse = spouse class MarriedPersonSchema(PersonSchema): - spouse = fields.Nested(schema='MarriedPersonSchema', exclude='spouse') + spouse = fields.Embed(schema='MarriedPersonSchema', exclude='spouse') One-to-many and many-to-many Relationships @@ -210,7 +210,7 @@ Until now, we've only dealt with one-to-one relations. What about one-to-many and many-to-many relations? Those link to collections of objects. We know the necessary building blocks already: Providing additional keyword -arguments to :class:`lima.fields.Nested` passes them through to the specified +arguments to :class:`lima.fields.Embed` passes them through to the specified schema's constructor. And providing ``many=True`` to a schema's construtor will have the schema marshalling collections - so: @@ -232,11 +232,11 @@ have the schema marshalling collections - so: ] class AuthorSchema(PersonSchema): - books = fields.Nested(schema='BookSchema', exclude='author', many=True) + books = fields.Embed(schema='BookSchema', exclude='author', many=True) class BookSchema(Schema): title = fields.String() - author = fields.Nested(schema=AuthorSchema, exclude='books') + author = fields.Embed(schema=AuthorSchema, exclude='books') schema = AuthorSchema() schema.dump(author) @@ -247,18 +247,18 @@ have the schema marshalling collections - so: # 'first_name': 'Virginia'} -Nested Data Recap +Linked Data Recap ================= -- You now know how to marshal nested objects (via a field of type - :class:`lima.fields.Nested`) +- You now know how to marshal linked objects (via a field of type + :class:`lima.fields.Embed`) -- You know about lazy evaluation of nested schemas and how to specify those via +- You know about lazy evaluation of linked schemas and how to specify those via qualified and fully module-qualified names. - You know how to implement two-way relationships between objects (pass - ``exclude`` or ``only`` to the nested schema through - :class:`lima.fields.Nested`) + ``exclude`` or ``only`` to the linked schema through + :class:`lima.fields.Embed`) -- You know how to marshal nested collections of objects (pass ``many=True`` to - the nested schema through :class:`lima.fields.Nested`) +- You know how to marshal linked collections of objects (pass ``many=True`` to + the linked schema through :class:`lima.fields.Embed`) From 6203e3fa768a34193dda4ff71f69908b420c85d5 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 20:43:31 +0100 Subject: [PATCH 031/107] Update changelog. --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 67a2b7b..f1255f4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,8 @@ Changelog - Remove ``fields.type_mapping``. Use ``fields.TYPE_MAPPING`` instead. +- Deprecate ``fields.Nested`` in favour of ``fields.Embed``. + - Small speed improvement when serializing collections. - Overall cleanup. From f802fb76e220fe153f58e41f0c8c249a1960e9cc Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sun, 16 Nov 2014 20:45:21 +0100 Subject: [PATCH 032/107] Docstrings: remove/change references to fields.Nested. --- lima/fields.py | 2 +- lima/registry.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index 4fa6278..86d9915 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -239,7 +239,7 @@ def pack(self, val): '''Return the output of the referenced object's schema's dump method. If the referenced object's schema was specified by name at the - :class:`Nested` field's creation, this is the time when this schema is + :class:`Embed` field's creation, this is the time when this schema is instantiated (this is done only once). Args: diff --git a/lima/registry.py b/lima/registry.py index 839df26..6f4882d 100644 --- a/lima/registry.py +++ b/lima/registry.py @@ -98,6 +98,6 @@ def get(self, name): '''A global :class:`Registry` instance. Used internally by lima to automatically keep track of created Schemas (this is -needed by :class:`lima.fields.Nested`). +needed by some field classes). ''' From 04eb9857f1dab4fc28d1c62e668c3337e055a71b Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sat, 22 Nov 2014 08:26:05 +0100 Subject: [PATCH 033/107] Add oid function to schema instances. (where oid fields are defined) --- lima/schema.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lima/schema.py b/lima/schema.py index 47bb69b..379c7ce 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -72,6 +72,13 @@ def _mangle_name(name): return name return mapping[before] + after +def _oid_field(fields): + candidates = {k: v for k, v in fields.items() if v.oid} + if not candidates: + return None + if len(candidates) == 1: + return list(candidates.items())[0] + raise ValueError('Multiple oid fields.') # TODO: better message # Schema Metaclass ############################################################ @@ -303,6 +310,15 @@ def __init__(self, code, namespace = Schema._dump_fields_code_ns(fields, ordered) self._dump_fields = util.make_function('dump_fields', code, namespace) + oid_tuple = _oid_field(fields) + if oid_tuple: + field_name, field = oid_tuple + code, namespace = Schema._dump_field_code_ns(field, field_name) + namespace['__builtins__'] = {} + exec(code, namespace) + self.oid = namespace['dump_field'] + + @staticmethod def _field_value_code_ns(field, field_name, field_num): '''Get code and namespace dict to determine a field's serialized value. @@ -358,6 +374,30 @@ def _field_value_code_ns(field, field_name, field_num): return val_code, namespace + @staticmethod + def _dump_field_code_ns(field, field_name): + '''Get code and namespace dict for a customized dump_field function + + Args: + field: The field. + + field_name: The name (key) of the field. + + Returns: A tuple consisting of: a) Python code to define the function + and b) a namespace dict containing objects necessary for this code + to work. + + ''' + func_tpl = textwrap.dedent( + '''\ + def dump_field(obj): + return {val_code} + ''' + ) + val_code, namespace = Schema._field_value_code_ns(field, field_name, 0) + code = func_tpl.format(val_code=val_code) + return code, namespace + @staticmethod def _dump_fields_code_ns(fields, ordered): '''Get code and namespace dict for a customized dump_fields function. From 1621292cd98e8f7e4fcb02e7e0b782a25e78e0d6 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sat, 22 Nov 2014 15:53:23 +0100 Subject: [PATCH 034/107] schema: use utils.make_function, refactor helper funcs ... --- lima/schema.py | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 379c7ce..415b015 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -72,13 +72,24 @@ def _mangle_name(name): return name return mapping[before] + after -def _oid_field(fields): - candidates = {k: v for k, v in fields.items() if v.oid} - if not candidates: - return None - if len(candidates) == 1: - return list(candidates.items())[0] - raise ValueError('Multiple oid fields.') # TODO: better message + +def _contains_oid_field(fields): + '''Return True if any of fields claims to be an oid field, else False.''' + return any(f.oid for f in fields.values()) + + +def _oid_name_field(fields): + '''Return name and field of the only oid field in fields. + + Raises: + ValueError: if fields doesn't contain exactly one oid field. + ''' + names = [k for k, v in fields.items() if v.oid] + if len(names) != 1: + raise ValueError('Not exactly one oid field.') + name = names[0] + return name, fields[name] + # Schema Metaclass ############################################################ @@ -306,17 +317,16 @@ def __init__(self, self._ordered = ordered self.many = many - # get code and namespace for customized dump function + # get code and namespace for customized dump function and create it code, namespace = Schema._dump_fields_code_ns(fields, ordered) self._dump_fields = util.make_function('dump_fields', code, namespace) - oid_tuple = _oid_field(fields) - if oid_tuple: - field_name, field = oid_tuple - code, namespace = Schema._dump_field_code_ns(field, field_name) - namespace['__builtins__'] = {} - exec(code, namespace) - self.oid = namespace['dump_field'] + # if oid field exists, get code for customized oid func and create it + if _contains_oid_field(fields): + name, field = _oid_name_field(fields) + code, namespace = Schema._dump_field_code_ns(field, name) + self.oid = util.make_function('dump_field', code, namespace) + @staticmethod From b1938761f7e256a4a684a45345b1e084a81d64e7 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sat, 22 Nov 2014 16:13:12 +0100 Subject: [PATCH 035/107] schema: rename fuctions. --- lima/schema.py | 27 ++++++++++++++------------- tests/test_schema.py | 6 +++--- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 415b015..692d2c1 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -78,8 +78,8 @@ def _contains_oid_field(fields): return any(f.oid for f in fields.values()) -def _oid_name_field(fields): - '''Return name and field of the only oid field in fields. +def _oid_field_item(fields): + '''Return (name, field)-tuple of the only oid field in fields. Raises: ValueError: if fields doesn't contain exactly one oid field. @@ -318,20 +318,20 @@ def __init__(self, self.many = many # get code and namespace for customized dump function and create it - code, namespace = Schema._dump_fields_code_ns(fields, ordered) + code, namespace = Schema._dump_fields_code_item(fields, ordered) self._dump_fields = util.make_function('dump_fields', code, namespace) # if oid field exists, get code for customized oid func and create it if _contains_oid_field(fields): - name, field = _oid_name_field(fields) - code, namespace = Schema._dump_field_code_ns(field, name) + name, field = _oid_field_item(fields) + code, namespace = Schema._dump_field_code_item(field, name) self.oid = util.make_function('dump_field', code, namespace) @staticmethod - def _field_value_code_ns(field, field_name, field_num): - '''Get code and namespace dict to determine a field's serialized value. + def _field_value_code_item(field, field_name, field_num): + '''Get (code, namespace)-tuple to determine a field's serialized value. Args: field: A :class:`lima.fields.Field` instance. @@ -385,8 +385,8 @@ def _field_value_code_ns(field, field_name, field_num): return val_code, namespace @staticmethod - def _dump_field_code_ns(field, field_name): - '''Get code and namespace dict for a customized dump_field function + def _dump_field_code_item(field, field_name): + '''Get (code, namespace)-tuple for a customized dump_field function. Args: field: The field. @@ -404,13 +404,14 @@ def dump_field(obj): return {val_code} ''' ) - val_code, namespace = Schema._field_value_code_ns(field, field_name, 0) + val_code, namespace = Schema._field_value_code_item(field, + field_name, 0) code = func_tpl.format(val_code=val_code) return code, namespace @staticmethod - def _dump_fields_code_ns(fields, ordered): - '''Get code and namespace dict for a customized dump_fields function. + def _dump_fields_code_item(fields, ordered): + '''Get (code, namespace)-tuple for a customized dump_fields function. Args: fields: An ordered mapping of field names to fields @@ -453,7 +454,7 @@ def dump_fields(obj, many): # iterate over fields to fill up entries for field_num, (field_name, field) in enumerate(fields.items()): - val_code, val_namespace = Schema._field_value_code_ns( + val_code, val_namespace = Schema._field_value_code_item( field, field_name, field_num ) namespace.update(val_namespace) diff --git a/tests/test_schema.py b/tests/test_schema.py index 66c9e5b..b1e415e 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -665,14 +665,14 @@ class TestSchema(schema.Schema): test_schema = TestSchema() def test_dump_fields_code(self): - '''Test if _dump_fields_code_ns gets a simple function right.''' + '''Test if _dump_fields_code_item gets a simple function right.''' from textwrap import dedent class TestSchema(schema.Schema): foo = fields.String(attr='foo_attr') bar = fields.String() - code, ns = schema.Schema._dump_fields_code_ns( + code, ns = schema.Schema._dump_fields_code_item( TestSchema.__fields__, ordered=False ) expected = dedent( @@ -685,7 +685,7 @@ def dump_fields(obj, many): ) assert code == expected - code, ns = schema.Schema._dump_fields_code_ns( + code, ns = schema.Schema._dump_fields_code_item( TestSchema.__fields__, ordered=True ) expected = dedent( From dfe8683b63d3cd13d79e52d4d55f685a5cb6c78f Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sat, 22 Nov 2014 16:21:25 +0100 Subject: [PATCH 036/107] schema: PEP8 --- lima/schema.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 692d2c1..1ab1270 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -327,8 +327,6 @@ def __init__(self, code, namespace = Schema._dump_field_code_item(field, name) self.oid = util.make_function('dump_field', code, namespace) - - @staticmethod def _field_value_code_item(field, field_name, field_num): '''Get (code, namespace)-tuple to determine a field's serialized value. From 0d97e01bc7b8196a3c97ba0018c325660bad2d88 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Sat, 22 Nov 2014 16:23:45 +0100 Subject: [PATCH 037/107] Reorder changelog. --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f1255f4..49b19a0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,11 +9,11 @@ Changelog While unreleased, the changelog of lima 0.4 is itself subject to change. -- Remove ``fields.type_mapping``. Use ``fields.TYPE_MAPPING`` instead. +- Small speed improvement when serializing collections. - Deprecate ``fields.Nested`` in favour of ``fields.Embed``. -- Small speed improvement when serializing collections. +- Remove ``fields.type_mapping``. Use ``fields.TYPE_MAPPING`` instead. - Overall cleanup. From 2d3a5a1fb9b84974d07c477472e5cab5b3e7cc3f Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 9 Dec 2014 14:49:22 +0100 Subject: [PATCH 038/107] utils: refactor docstrings & comments for suppress. --- lima/util.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/lima/util.py b/lima/util.py index a07ec67..3f3a6b0 100644 --- a/lima/util.py +++ b/lima/util.py @@ -23,13 +23,20 @@ def complain_about(name): raise +# The code for this class is taken directly from the Python 3.4 standard +# library (to support Python 3.3), licensed under the PSF License (see +# https://docs.python.org/3/license.html) class suppress: '''Context manager to suppress specified exceptions - This context manager is taken directly from the Python 3.4 standard library - to get support for Python 3.3. + After the exception is suppressed, execution proceeds with the next + statement following the with statement. - See https://docs.python.org/3.4/library/contextlib.html#contextlib.suppress + with suppress(FileNotFoundError): + os.remove(somefile) + # Execution still resumes here if the file was already removed + + Backported for Python 3.3 from Python 3.4 (see source for license info). ''' def __init__(self, *exceptions): @@ -39,6 +46,15 @@ def __enter__(self): pass def __exit__(self, exctype, excinst, exctb): + # Unlike isinstance and issubclass, CPython exception handling + # currently only looks at the concrete type hierarchy (ignoring + # the instance and subclass checking hooks). While Guido considers + # that a bug rather than a feature, it's a fairly hard one to fix + # due to various internal implementation details. suppress provides + # the simpler issubclass based semantics, rather than trying to + # exactly reproduce the limitations of the CPython interpreter. + # + # See http://bugs.python.org/issue12029 for more details return exctype is not None and issubclass(exctype, self._exceptions) From d8adf00ebb05b41384c88a91f9c9b7e4e2608239 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 9 Dec 2014 15:07:49 +0100 Subject: [PATCH 039/107] Add util.reify plus tests. PEP8. --- lima/util.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test_util.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/lima/util.py b/lima/util.py index 3f3a6b0..bfc7c16 100644 --- a/lima/util.py +++ b/lima/util.py @@ -23,6 +23,52 @@ def complain_about(name): raise +# The code for this class is taken from pyramid.decorator (with negligible +# alterations), licensed under the Repoze Public License (see +# http://www.pylonsproject.org/about/license) +class reify: + '''Like property, but saves the underlying method's result for later use. + + Use as a class method decorator. It operates almost exactly like the Python + ``@property`` decorator, but it puts the result of the method it decorates + into the instance dict after the first call, effectively replacing the + function it decorates with an instance variable. It is, in Python parlance, + a non-data descriptor. An example: + + .. code-block:: python + + class Foo(object): + @reify + def jammy(self): + print('jammy called') + return 1 + + And usage of Foo: + + f = Foo() + v = f.jammy + 'jammy called' + print(v) + 1 + print f.jammy + 1 + # jammy func not called the second time; it replaced itself with 1 + + Taken from pyramid.decorator (see source for license info). + + ''' + def __init__(self, wrapped): + self.wrapped = wrapped + self.__doc__ = wrapped.__doc__ + + def __get__(self, instance, owner): + if instance is None: + return self + val = self.wrapped(instance) + setattr(instance, self.wrapped.__name__, val) + return val + + # The code for this class is taken directly from the Python 3.4 standard # library (to support Python 3.3), licensed under the PSF License (see # https://docs.python.org/3/license.html) diff --git a/tests/test_util.py b/tests/test_util.py index 1a364f2..9d91f29 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -42,6 +42,34 @@ def test_complain_about(): assert str(e).startswith('bar') +# Adapted from Pyramid's test suite, licensed under the RPL +class TestReify: + '''Class collecting tests of helper functions.''' + + class Dummy: + pass + + def test__get__with_instance(self): + def wrapee(inst): + return 42 + decorated = util.reify(wrapee) + inst = TestReify.Dummy() + result = decorated.__get__(instance=inst, owner=...) + assert result == 42 + assert inst.__dict__['wrapee'] == 42 + + def test__get__without_instance(self): + decorated = util.reify(None) + result = decorated.__get__(instance=None, owner=...) + assert result == decorated + + def test__doc__copied(self): + def wrapee(inst): + '''My docstring''' + decorated = util.reify(wrapee) + assert decorated.__doc__ == 'My docstring' + + def test_vector_context(): '''Test if vector context boxes scalars into lists.''' assert util.vector_context([]) == [] @@ -127,6 +155,7 @@ def test_only_instances_of(): with pytest.raises(TypeError): util.ensure_only_instances_of([1, 2, 3.3, 4], int) + def test_make_function(): code = 'def func_in_namespace(): return 1' my_function = util.make_function('func_in_namespace', code) From 8e9afd6490865717987c92bcf9f4418a727e2373 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 9 Dec 2014 15:38:41 +0100 Subject: [PATCH 040/107] fields: use reify for Embed's schema_inst-attribute --- lima/fields.py | 32 ++++++++++++++++++++------------ tests/test_fields.py | 2 +- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index 86d9915..cbaf194 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -4,6 +4,7 @@ from lima import abc from lima import registry +from lima.util import reify class Field(abc.FieldABC): @@ -218,23 +219,37 @@ def __init__(self, *, schema, attr=None, get=None, val=None, **kwargs): msg = ('No keyword args must be supplied' 'if schema is a Schema object.') raise ValueError(msg) - self.schema_inst = schema + self._schema_inst = schema # in case schema is a schema class elif isinstance(schema, type) and issubclass(schema, abc.SchemaABC): - self.schema_inst = schema(**kwargs) + self._schema_inst = schema(**kwargs) # in case schema is a schema name: save args for later instantiation elif isinstance(schema, str): - self.schema_inst = None - self.schema_name = schema - self.schema_kwargs = kwargs + self._schema_inst = None + self._schema_name = schema + self._schema_kwargs = kwargs # otherwise fail else: msg = 'Illegal type for schema param: {}' raise TypeError(msg.format(type(schema))) + @reify + def schema_inst(self): + '''Return the associated Schema instance (reified method). + + If no associated Schema instance exists at call time (because only a + Schema class name was supplied to the constructor), find the Schema + class in the global registry and instantiate it. + + ''' + if not self._schema_inst: + cls = registry.global_registry.get(self._schema_name) + self._schema_inst = cls(**self._schema_kwargs) + return self._schema_inst + def pack(self, val): '''Return the output of the referenced object's schema's dump method. @@ -250,13 +265,6 @@ def pack(self, val): :meth:`lima.schema.Schema.dump` method. ''' - # if schema_inst doesn't exist yet (because a schema class name was - # supplied to the constructor), find the schema class in the global - # registry and instantiate it. - if not self.schema_inst: - cls = registry.global_registry.get(self.schema_name) - self.schema_inst = cls(**self.schema_kwargs) - return self.schema_inst.dump(val) if val is not None else None diff --git a/tests/test_fields.py b/tests/test_fields.py index 2a88ff5..90a05f1 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -110,7 +110,7 @@ def test_datetime_pack(): def test_embed_by_name(): field = fields.Embed(schema='NonExistentSchema') - assert field.schema_name == 'NonExistentSchema' + assert field._schema_name == 'NonExistentSchema' def test_embed_error_on_illegal_schema_spec(): From 2c60d8cf95dd6c59964345a7ccdba81717a7a649 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 9 Dec 2014 18:10:37 +0100 Subject: [PATCH 041/107] Add rudimentary implementation of Schema.oid() ... there's still lots of cleanup to be done --- lima/schema.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 1ab1270..e0095ba 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -325,7 +325,7 @@ def __init__(self, if _contains_oid_field(fields): name, field = _oid_field_item(fields) code, namespace = Schema._dump_field_code_item(field, name) - self.oid = util.make_function('dump_field', code, namespace) + self._oid = util.make_function('dump_field', code, namespace) @staticmethod def _field_value_code_item(field, field_name, field_num): @@ -398,7 +398,9 @@ def _dump_field_code_item(field, field_name): ''' func_tpl = textwrap.dedent( '''\ - def dump_field(obj): + def dump_field(obj, many): + if many: + return [{val_code} for obj in obj] return {val_code} ''' ) @@ -469,6 +471,23 @@ def dump_fields(obj, many): code = func_tpl.format(contents=', '.join(entries)) return code, namespace + def oid(self, obj, *, many=None): + '''Return a marshalled representation of the oid for obj. + + Args: + obj: The object (or collection of objects) to marshall. + + many: Wether obj is a single object or a collection of objects. If + ``many`` is ``None``, the value of the instance's + :attr:`many` attribute is used. + + Returns: + TODO + + ''' + # this more or less just calls the instance-specific dump function + return self._oid(obj, self.many if many is None else many) + def dump(self, obj, *, many=None): '''Return a marshalled representation of obj. From 703b151a4efc788ea149d55b37b329d48129295e Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 9 Dec 2014 18:11:43 +0100 Subject: [PATCH 042/107] Add field type "Reference", give it a base class it shares with Embed This implementation is still lacking, but it's a start. --- lima/fields.py | 130 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 92 insertions(+), 38 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index cbaf194..c481a85 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -149,12 +149,12 @@ def pack(val): return val.isoformat() if val is not None else None -class Embed(Field): - '''A Field to embed linked object(s). +class _LinkedObjectField(Field): + '''A field that references the schema of a linked object. Args: - schema: The schema of the referenced object. This can be specified via - a schema *object,* a schema *class* (that will get instantiated + schema: The schema of the linked object. This can be specified via a + schema *object,* a schema *class* (that will get instantiated immediately) or the qualified *name* of a schema class (for when the named schema has not been defined at the time of the :class:`Embed` object's creation). If two or more schema classes @@ -181,34 +181,6 @@ class Embed(Field): TypeError: If ``schema`` has the wrong type. - Examples: :: - - # refer to PersonSchema class - author = Embed(schema=PersonSchema) - - # refer to PersonSchema class with additional params - artists = Embed(schema=PersonSchema, exclude='email', many=True) - - # refer to PersonSchema object - author = Embed(schema=PersonSchema()) - - # refer to PersonSchema object with additional params - # (note that Embed() itself gets no kwargs) - artists = Embed(schema=PersonSchema(exclude='email', many=true)) - - # refer to PersonSchema per name - author = Embed(schema='PersonSchema') - - # refer to PersonSchema per name with additional params - author = Embed(schema='PersonSchema', exclude='email', many=True) - - # refer to PersonSchema per module-qualified name - # (in case of ambiguity) - author = Embed(schema='project.persons.PersonSchema') - - # specify attr name as well - user = Embed(attr='login_user', schema=PersonSchema) - ''' def __init__(self, *, schema, attr=None, get=None, val=None, **kwargs): super().__init__(attr=attr, get=get, val=val) @@ -251,23 +223,105 @@ class in the global registry and instantiate it. return self._schema_inst def pack(self, val): - '''Return the output of the referenced object's schema's dump method. + raise NotImplementedError + + +class Embed(_LinkedObjectField): + '''A Field to embed linked object(s). + + Args: + schema: The schema of the referenced object. This can be specified via + a schema *object,* a schema *class* (that will get instantiated + immediately) or the qualified *name* of a schema class (for when + the named schema has not been defined at the time of the + :class:`Embed` object's creation). If two or more schema classes + with the same name exist in different modules, the schema class + name has to be fully module-qualified (see the :ref:`entry on class + names ` for clarification of these concepts). + Schemas defined within a local namespace can not be referenced by + name. + + attr: The optional name of the corresponding attribute. + + get: An optional getter function accepting an object as its only + parameter and returning the field value. + + val: An optional constant value for the field. - If the referenced object's schema was specified by name at the - :class:`Embed` field's creation, this is the time when this schema is - instantiated (this is done only once). + kwargs: Optional keyword arguments to pass to the :class:`Schema`'s + constructor when the time has come to instance it. Must be empty if + ``schema`` is a :class:`lima.schema.Schema` object. + + Raises: + ValueError: If ``kwargs`` are specified even if ``schema`` is a + :class:`lima.schema.Schema` *object.* + + TypeError: If ``schema`` has the wrong type. + + Examples: :: + + # refer to PersonSchema class + author = Embed(schema=PersonSchema) + + # refer to PersonSchema class with additional params + artists = Embed(schema=PersonSchema, exclude='email', many=True) + + # refer to PersonSchema object + author = Embed(schema=PersonSchema()) + + # refer to PersonSchema object with additional params + # (note that Embed() itself gets no kwargs) + artists = Embed(schema=PersonSchema(exclude='email', many=true)) + + # refer to PersonSchema per name + author = Embed(schema='PersonSchema') + + # refer to PersonSchema per name with additional params + author = Embed(schema='PersonSchema', exclude='email', many=True) + + # refer to PersonSchema per module-qualified name + # (in case of ambiguity) + author = Embed(schema='project.persons.PersonSchema') + + # specify attr name as well + user = Embed(attr='login_user', schema=PersonSchema) + + ''' + def pack(self, val): + '''Return the output of the linked object's schema's dump method. Args: val: The nested object to convert. Returns: - The output of the referenced :class:`lima.schema.Schema`'s - :meth:`lima.schema.Schema.dump` method. + The output of the linked :class:`lima.schema.Schema`'s + :meth:`lima.schema.Schema.dump` method (or None if ``val`` is + None). ''' return self.schema_inst.dump(val) if val is not None else None +class Reference(_LinkedObjectField): + '''A Field to reference linked object(s). + + Constructor arguments are similar to those of :class:`Embed`. + + ''' + def pack(self, val): + '''Return the output of the linked object's schema's oid method. + + Args: + val: The nested object to convert. + + Returns: + The output of the linked :class:`lima.schema.Schema`'s + :meth:`lima.schema.Schema.oid` method (or None if ``val`` is None). + + ''' + return self.schema_inst.oid(val) if val is not None else None + + Nested = Embed '''A Field to embed linked object(s) From eba0578636f46ba5ec8bfd827de8c97a8b502e70 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 9 Dec 2014 18:34:12 +0100 Subject: [PATCH 043/107] schema: refactor static methods into functions ... so all helper functions are at the same place (intuitively this looks better) --- lima/schema.py | 292 +++++++++++++++++++++---------------------- tests/test_schema.py | 68 +++++----- 2 files changed, 180 insertions(+), 180 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index e0095ba..dc967c3 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -91,6 +91,150 @@ def _oid_field_item(fields): return name, fields[name] +def _field_value_code_item(field, field_name, field_num): + '''Get (code, namespace)-tuple to determine a field's serialized value. + + Args: + field: A :class:`lima.fields.Field` instance. + + field_name: The name (key) of the field. + + field_num: A schema-wide unique number for the field. + + Returns: + A tuple consisting of: a) a fragment of Python code to determine the + field's value and b) a namespace dict containing objects necessary for + this code fragment to work. + + ''' + namespace = {} + if hasattr(field, 'val'): + # add constant-field-value-shortcut to namespace + name = 'val{}'.format(field_num) + namespace[name] = field.val + + # later, get value using this shortcut + val_code = name + + elif hasattr(field, 'get'): + # add getter-shortcut to namespace + name = 'get{}'.format(field_num) + namespace[name] = field.get + + # later, get value by calling this shortcut + val_code = '{}(obj)'.format(name) + + else: + # neither constant val nor getter: try to get value via attr + # (if attr is not specified, use field name as attr) + obj_attr = getattr(field, 'attr', field_name) + + if not str.isidentifier(obj_attr) or keyword.iskeyword(obj_attr): + msg = 'Not a valid attribute name: {!r}' + raise ValueError(msg.format(obj_attr)) + + # later, get value using obj_attr + val_code = 'obj.{}'.format(obj_attr) + + if hasattr(field, 'pack'): + # add pack-shortcut to attributes + name = 'pack{}'.format(field_num) + namespace[name] = field.pack + + # later, pass result field value to this shortcut + val_code = '{}({})'.format(name, val_code) + + return val_code, namespace + + +def _dump_field_code_item(field, field_name): + '''Get (code, namespace)-tuple for a customized dump_field function. + + Args: + field: The field. + + field_name: The name (key) of the field. + + Returns: + A tuple consisting of: a) Python code to define the function and b) a + namespace dict containing objects necessary for this code to work. + + ''' + func_tpl = textwrap.dedent( + '''\ + def dump_field(obj, many): + if many: + return [{val_code} for obj in obj] + return {val_code} + ''' + ) + val_code, namespace = _field_value_code_item(field, field_name, 0) + code = func_tpl.format(val_code=val_code) + return code, namespace + + +def _dump_fields_code_item(fields, ordered): + '''Get (code, namespace)-tuple for a customized dump_fields function. + + Args: + fields: An ordered mapping of field names to fields + + ordered: If True, make the resulting function return OrderedDict + objects, else make it return ordinary dict objects. + + Returns: + A tuple consisting of: a) Python code to define a dump function for + fields and b) a namespace dict containing objects necessary for this + code to work. + + ''' + # Namespace must contain OrderedDict if we want ordered output. + namespace = {'OrderedDict': OrderedDict} if ordered else {} + + # Get correct templates depending on "ordered" + if ordered: + func_tpl = textwrap.dedent( + '''\ + def dump_fields(obj, many): + if many: + return [OrderedDict([{contents}]) for obj in obj] + return OrderedDict([{contents}]) + ''' + ) + entry_tpl = '("{key}", {get_val})' + else: + func_tpl = textwrap.dedent( + '''\ + def dump_fields(obj, many): + if many: + return [{{{contents}}} for obj in obj] + return {{{contents}}} + ''' + ) + entry_tpl = '"{key}": {get_val}' + + # one entry per field + entries = [] + + # iterate over fields to fill up entries + for field_num, (field_name, field) in enumerate(fields.items()): + val_code, val_namespace = _field_value_code_item(field, field_name, + field_num) + namespace.update(val_namespace) + + # try to guard against code injection via quotes in key + key = str(field_name) + if '"' in key or "'" in key: + msg = 'Quotes are not allowed in field names: {}' + raise ValueError(msg.format(key)) + + # add entry + entries.append(entry_tpl.format(key=key, get_val=val_code)) + + code = func_tpl.format(contents=', '.join(entries)) + return code, namespace + + # Schema Metaclass ############################################################ class SchemaMeta(type): @@ -318,159 +462,15 @@ def __init__(self, self.many = many # get code and namespace for customized dump function and create it - code, namespace = Schema._dump_fields_code_item(fields, ordered) + code, namespace = _dump_fields_code_item(fields, ordered) self._dump_fields = util.make_function('dump_fields', code, namespace) # if oid field exists, get code for customized oid func and create it if _contains_oid_field(fields): name, field = _oid_field_item(fields) - code, namespace = Schema._dump_field_code_item(field, name) + code, namespace = _dump_field_code_item(field, name) self._oid = util.make_function('dump_field', code, namespace) - @staticmethod - def _field_value_code_item(field, field_name, field_num): - '''Get (code, namespace)-tuple to determine a field's serialized value. - - Args: - field: A :class:`lima.fields.Field` instance. - - field_name: The name (key) of the field. - - field_num: A schema-wide unique number for the field. - - Returns: A tuple consisting of: a) a fragment of Python code to - determine the field's value and b) a namespace dict containing - objects necessary for this code fragment to work. - - ''' - namespace = {} - if hasattr(field, 'val'): - # add constant-field-value-shortcut to namespace - name = 'val{}'.format(field_num) - namespace[name] = field.val - - # later, get value using this shortcut - val_code = name - - elif hasattr(field, 'get'): - # add getter-shortcut to namespace - name = 'get{}'.format(field_num) - namespace[name] = field.get - - # later, get value by calling this shortcut - val_code = '{}(obj)'.format(name) - - else: - # neither constant val nor getter: try to get value via attr - # (if attr is not specified, use field name as attr) - obj_attr = getattr(field, 'attr', field_name) - - if not str.isidentifier(obj_attr) or keyword.iskeyword(obj_attr): - msg = 'Not a valid attribute name: {!r}' - raise ValueError(msg.format(obj_attr)) - - # later, get value using obj_attr - val_code = 'obj.{}'.format(obj_attr) - - if hasattr(field, 'pack'): - # add pack-shortcut to attributes - name = 'pack{}'.format(field_num) - namespace[name] = field.pack - - # later, pass result field value to this shortcut - val_code = '{}({})'.format(name, val_code) - - return val_code, namespace - - @staticmethod - def _dump_field_code_item(field, field_name): - '''Get (code, namespace)-tuple for a customized dump_field function. - - Args: - field: The field. - - field_name: The name (key) of the field. - - Returns: A tuple consisting of: a) Python code to define the function - and b) a namespace dict containing objects necessary for this code - to work. - - ''' - func_tpl = textwrap.dedent( - '''\ - def dump_field(obj, many): - if many: - return [{val_code} for obj in obj] - return {val_code} - ''' - ) - val_code, namespace = Schema._field_value_code_item(field, - field_name, 0) - code = func_tpl.format(val_code=val_code) - return code, namespace - - @staticmethod - def _dump_fields_code_item(fields, ordered): - '''Get (code, namespace)-tuple for a customized dump_fields function. - - Args: - fields: An ordered mapping of field names to fields - - ordered: If True, make the resulting function return OrderedDict - objects, else make it return ordinary dict objects - - Returns: A tuple consisting of: a) Python code to define a dump - function for fields and b) a namespace dict containing objects - necessary for this code to work. - - ''' - # Namespace must contain OrderedDict if we want ordered output. - namespace = {'OrderedDict': OrderedDict} if ordered else {} - - # Get correct templates depending on "ordered" - if ordered: - func_tpl = textwrap.dedent( - '''\ - def dump_fields(obj, many): - if many: - return [OrderedDict([{contents}]) for obj in obj] - return OrderedDict([{contents}]) - ''' - ) - entry_tpl = '("{key}", {get_val})' - else: - func_tpl = textwrap.dedent( - '''\ - def dump_fields(obj, many): - if many: - return [{{{contents}}} for obj in obj] - return {{{contents}}} - ''' - ) - entry_tpl = '"{key}": {get_val}' - - # one entry per field - entries = [] - - # iterate over fields to fill up entries - for field_num, (field_name, field) in enumerate(fields.items()): - val_code, val_namespace = Schema._field_value_code_item( - field, field_name, field_num - ) - namespace.update(val_namespace) - - # try to guard against code injection via quotes in key - key = str(field_name) - if '"' in key or "'" in key: - msg = 'Quotes are not allowed in field names: {}' - raise ValueError(msg.format(key)) - - # add entry - entries.append(entry_tpl.format(key=key, get_val=val_code)) - - code = func_tpl.format(contents=', '.join(entries)) - return code, namespace - def oid(self, obj, *, many=None): '''Return a marshalled representation of the oid for obj. diff --git a/tests/test_schema.py b/tests/test_schema.py index b1e415e..d40aa2b 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -54,6 +54,40 @@ def test_mangle_name(self): assert mangle('hash__foo') == '#foo' assert mangle('plus__foo') == '+foo' + def test_dump_fields_code(self): + '''Test if _dump_fields_code_item gets a simple function right.''' + from textwrap import dedent + + class TestSchema(schema.Schema): + foo = fields.String(attr='foo_attr') + bar = fields.String() + + code, ns = schema._dump_fields_code_item( + TestSchema.__fields__, ordered=False + ) + expected = dedent( + '''\ + def dump_fields(obj, many): + if many: + return [{"foo": obj.foo_attr, "bar": obj.bar} for obj in obj] + return {"foo": obj.foo_attr, "bar": obj.bar} + ''' + ) + assert code == expected + + code, ns = schema._dump_fields_code_item( + TestSchema.__fields__, ordered=True + ) + expected = dedent( + '''\ + def dump_fields(obj, many): + if many: + return [OrderedDict([("foo", obj.foo_attr), ("bar", obj.bar)]) for obj in obj] + return OrderedDict([("foo", obj.foo_attr), ("bar", obj.bar)]) + ''' + ) + assert code == expected + class TestSchemaDefinition: '''Class collecting tests of Schema class definition.''' @@ -663,37 +697,3 @@ class TestSchema(schema.Schema): with pytest.raises(ValueError): test_schema = TestSchema() - - def test_dump_fields_code(self): - '''Test if _dump_fields_code_item gets a simple function right.''' - from textwrap import dedent - - class TestSchema(schema.Schema): - foo = fields.String(attr='foo_attr') - bar = fields.String() - - code, ns = schema.Schema._dump_fields_code_item( - TestSchema.__fields__, ordered=False - ) - expected = dedent( - '''\ - def dump_fields(obj, many): - if many: - return [{"foo": obj.foo_attr, "bar": obj.bar} for obj in obj] - return {"foo": obj.foo_attr, "bar": obj.bar} - ''' - ) - assert code == expected - - code, ns = schema.Schema._dump_fields_code_item( - TestSchema.__fields__, ordered=True - ) - expected = dedent( - '''\ - def dump_fields(obj, many): - if many: - return [OrderedDict([("foo", obj.foo_attr), ("bar", obj.bar)]) for obj in obj] - return OrderedDict([("foo", obj.foo_attr), ("bar", obj.bar)]) - ''' - ) - assert code == expected From 60a3c4194f4b126fcf6cae9bf8b477619beefc16 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Wed, 10 Dec 2014 16:25:55 +0100 Subject: [PATCH 044/107] schema: rename helper functions, add docstrings --- lima/schema.py | 65 +++++++++++++++++++++++++------------------- tests/test_dump.py | 2 +- tests/test_schema.py | 8 +++--- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index dc967c3..1db5528 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -91,20 +91,30 @@ def _oid_field_item(fields): return name, fields[name] -def _field_value_code_item(field, field_name, field_num): - '''Get (code, namespace)-tuple to determine a field's serialized value. +def _cns_field_value(field, field_name, field_num): + '''Return (code, namespace)-tuple for determining a field's serialized val. Args: field: A :class:`lima.fields.Field` instance. field_name: The name (key) of the field. - field_num: A schema-wide unique number for the field. + field_num: A schema-wide unique number for the field Returns: A tuple consisting of: a) a fragment of Python code to determine the - field's value and b) a namespace dict containing objects necessary for - this code fragment to work. + field's value for an object called ``obj`` and b) a namespace dict + containing the objects necessary for this code fragment to work. + + For a field ``myfield`` that has a ``pack`` and a ``get`` callable defined, + the output of this function could look something like this: + + .. code-block:: python + + ( + 'pack3(get3(obj))', # the code + {'get3': myfield.get, 'pack3': myfield.pack} # the namespace + ) ''' namespace = {} @@ -133,22 +143,22 @@ def _field_value_code_item(field, field_name, field_num): msg = 'Not a valid attribute name: {!r}' raise ValueError(msg.format(obj_attr)) - # later, get value using obj_attr + # later, get value using this attr val_code = 'obj.{}'.format(obj_attr) if hasattr(field, 'pack'): - # add pack-shortcut to attributes + # add pack-shortcut to namespace name = 'pack{}'.format(field_num) namespace[name] = field.pack - # later, pass result field value to this shortcut + # later, pass field value to this shortcut val_code = '{}({})'.format(name, val_code) return val_code, namespace -def _dump_field_code_item(field, field_name): - '''Get (code, namespace)-tuple for a customized dump_field function. +def _cns_dump_field(field, field_name): + '''Return (code, namespace)-tuple for a customized dump_field function. Args: field: The field. @@ -168,19 +178,19 @@ def dump_field(obj, many): return {val_code} ''' ) - val_code, namespace = _field_value_code_item(field, field_name, 0) + val_code, namespace = _cns_field_value(field, field_name, 0) code = func_tpl.format(val_code=val_code) return code, namespace -def _dump_fields_code_item(fields, ordered): - '''Get (code, namespace)-tuple for a customized dump_fields function. +def _cns_dump_fields(fields, ordered): + '''Return (code, namespace)-tuple for a customized dump_fields function. Args: - fields: An ordered mapping of field names to fields + fields: An ordered mapping of field names to fields. ordered: If True, make the resulting function return OrderedDict - objects, else make it return ordinary dict objects. + objects, else make it return ordinary dicts. Returns: A tuple consisting of: a) Python code to define a dump function for @@ -197,30 +207,29 @@ def _dump_fields_code_item(fields, ordered): '''\ def dump_fields(obj, many): if many: - return [OrderedDict([{contents}]) for obj in obj] - return OrderedDict([{contents}]) + return [OrderedDict([{joined_entries}]) for obj in obj] + return OrderedDict([{joined_entries}]) ''' ) - entry_tpl = '("{key}", {get_val})' + entry_tpl = '("{key}", {val_code})' else: func_tpl = textwrap.dedent( '''\ def dump_fields(obj, many): if many: - return [{{{contents}}} for obj in obj] - return {{{contents}}} + return [{{{joined_entries}}} for obj in obj] + return {{{joined_entries}}} ''' ) - entry_tpl = '"{key}": {get_val}' + entry_tpl = '"{key}": {val_code}' # one entry per field entries = [] # iterate over fields to fill up entries for field_num, (field_name, field) in enumerate(fields.items()): - val_code, val_namespace = _field_value_code_item(field, field_name, - field_num) - namespace.update(val_namespace) + val_code, val_ns = _cns_field_value(field, field_name, field_num) + namespace.update(val_ns) # try to guard against code injection via quotes in key key = str(field_name) @@ -229,9 +238,9 @@ def dump_fields(obj, many): raise ValueError(msg.format(key)) # add entry - entries.append(entry_tpl.format(key=key, get_val=val_code)) + entries.append(entry_tpl.format(key=key, val_code=val_code)) - code = func_tpl.format(contents=', '.join(entries)) + code = func_tpl.format(joined_entries=', '.join(entries)) return code, namespace @@ -462,13 +471,13 @@ def __init__(self, self.many = many # get code and namespace for customized dump function and create it - code, namespace = _dump_fields_code_item(fields, ordered) + code, namespace = _cns_dump_fields(fields, ordered) self._dump_fields = util.make_function('dump_fields', code, namespace) # if oid field exists, get code for customized oid func and create it if _contains_oid_field(fields): name, field = _oid_field_item(fields) - code, namespace = _dump_field_code_item(field, name) + code, namespace = _cns_dump_field(field, name) self._oid = util.make_function('dump_field', code, namespace) def oid(self, obj, *, many=None): diff --git a/tests/test_dump.py b/tests/test_dump.py index e02f937..b2f9c3e 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -61,7 +61,7 @@ class KingSchemaEmbedObject(KnightSchema): class SelfReferentialKingSchema(schema.Schema): name = fields.String() boss = fields.Embed(schema=__name__ + '.SelfReferentialKingSchema', - exclude='boss') + exclude='boss') @pytest.fixture diff --git a/tests/test_schema.py b/tests/test_schema.py index d40aa2b..7e404a1 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -54,15 +54,15 @@ def test_mangle_name(self): assert mangle('hash__foo') == '#foo' assert mangle('plus__foo') == '+foo' - def test_dump_fields_code(self): - '''Test if _dump_fields_code_item gets a simple function right.''' + def test_cns_dump_fields(self): + '''Test if _cns_dump_fields gets a simple function right.''' from textwrap import dedent class TestSchema(schema.Schema): foo = fields.String(attr='foo_attr') bar = fields.String() - code, ns = schema._dump_fields_code_item( + code, ns = schema._cns_dump_fields( TestSchema.__fields__, ordered=False ) expected = dedent( @@ -75,7 +75,7 @@ def dump_fields(obj, many): ) assert code == expected - code, ns = schema._dump_fields_code_item( + code, ns = schema._cns_dump_fields( TestSchema.__fields__, ordered=True ) expected = dedent( From 408b89cf583462da0def17754040d21386188f9f Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Wed, 10 Dec 2014 16:46:48 +0100 Subject: [PATCH 045/107] test_fields: parametrize tests for linked object fields --- tests/test_fields.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 90a05f1..807560c 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -19,6 +19,13 @@ fields.DateTime ] +LINKED_OBJECT_FIELDS = [ + fields._LinkedObjectField, + fields.Reference, + fields.Embed, + fields.Nested, # to be deprecated in 0.5 +] + @pytest.mark.parametrize('cls', SIMPLE_FIELDS) def test_simple_fields(cls): @@ -106,14 +113,13 @@ def test_datetime_pack(): assert fields.DateTime.pack(datetime) == expected -# tests of embedded fields assume a lot of the other stuff also works - -def test_embed_by_name(): - field = fields.Embed(schema='NonExistentSchema') - assert field._schema_name == 'NonExistentSchema' - +@pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) +def test_linked_object_by_name(cls): + field = cls(schema='NonExistentSchemaName') + assert field._schema_name == 'NonExistentSchemaName' -def test_embed_error_on_illegal_schema_spec(): +@pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) +def test_linked_object_error_on_illegal_schema_spec(cls): with pytest.raises(TypeError): - field = fields.Embed(schema=123) + field = cls(schema=123) From 9a4cff89d763d1e7c61283fbc287a7d2326fdd30 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Wed, 10 Dec 2014 17:06:53 +0100 Subject: [PATCH 046/107] refactoring: change some names, docstrings - Field.oid -> Field.is_oid - Schema.oid -> Schema.dump_oid --- lima/fields.py | 19 ++++++++++--------- lima/schema.py | 28 ++++++++++++++++------------ 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index c481a85..f0db9ea 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -18,15 +18,15 @@ class Field(abc.FieldABC): val: An optional constant value for the field. - oid: If ``True``, marks this field as a field whose value can be used - to identify an object. A schema must not end up with more than one - identifier field. + is_oid: If ``True``, marks this field as a field whose value can be + used to identify an object. A schema must not end up with more than + one identifier field. .. versionadded:: 0.3 The ``val`` parameter. .. versionadded:: 0.4 - The ``oid`` parameter. + The ``is_oid`` parameter. :attr:`attr`, :attr:`get` and :attr:`val` are mutually exclusive. @@ -44,7 +44,7 @@ class Field(abc.FieldABC): instance. ''' - def __init__(self, *, attr=None, get=None, val=None, oid=False): + def __init__(self, *, attr=None, get=None, val=None, is_oid=False): if sum(v is not None for v in (attr, get, val)) > 1: raise ValueError('attr, get and val are mutually exclusive.') @@ -60,7 +60,7 @@ def __init__(self, *, attr=None, get=None, val=None, oid=False): elif val is not None: self.val = val - self.oid = oid + self.is_oid = is_oid class Boolean(Field): @@ -309,17 +309,18 @@ class Reference(_LinkedObjectField): ''' def pack(self, val): - '''Return the output of the linked object's schema's oid method. + '''Return the output of the linked object's schema's dump_oid method. Args: val: The nested object to convert. Returns: The output of the linked :class:`lima.schema.Schema`'s - :meth:`lima.schema.Schema.oid` method (or None if ``val`` is None). + :meth:`lima.schema.Schema.dump_oid` method (or None if ``val`` is + None). ''' - return self.schema_inst.oid(val) if val is not None else None + return self.schema_inst.dump_oid(val) if val is not None else None Nested = Embed diff --git a/lima/schema.py b/lima/schema.py index 1db5528..938f3a4 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -75,7 +75,7 @@ def _mangle_name(name): def _contains_oid_field(fields): '''Return True if any of fields claims to be an oid field, else False.''' - return any(f.oid for f in fields.values()) + return any(f.is_oid for f in fields.values()) def _oid_field_item(fields): @@ -84,7 +84,7 @@ def _oid_field_item(fields): Raises: ValueError: if fields doesn't contain exactly one oid field. ''' - names = [k for k, v in fields.items() if v.oid] + names = [k for k, v in fields.items() if v.is_oid] if len(names) != 1: raise ValueError('Not exactly one oid field.') name = names[0] @@ -470,32 +470,36 @@ def __init__(self, self._ordered = ordered self.many = many - # get code and namespace for customized dump function and create it + # get code/namespace for dump function and create it code, namespace = _cns_dump_fields(fields, ordered) self._dump_fields = util.make_function('dump_fields', code, namespace) - # if oid field exists, get code for customized oid func and create it + # if oid field exists, get code/namespace for dump oid func & create it if _contains_oid_field(fields): name, field = _oid_field_item(fields) code, namespace = _cns_dump_field(field, name) - self._oid = util.make_function('dump_field', code, namespace) + self._dump_oid = util.make_function('dump_field', code, namespace) - def oid(self, obj, *, many=None): - '''Return a marshalled representation of the oid for obj. + def dump_oid(self, obj, *, many=None): + '''Return a marshalled representation of the oid of obj. Args: - obj: The object (or collection of objects) to marshall. + obj: The object (or collection of objects) whose oid shall be + marshalled. many: Wether obj is a single object or a collection of objects. If ``many`` is ``None``, the value of the instance's :attr:`many` attribute is used. Returns: - TODO + The marshalled representation of the oid of obj + + Raises: + AttributeError: if the schema doesn't have an oid field ''' - # this more or less just calls the instance-specific dump function - return self._oid(obj, self.many if many is None else many) + # this just calls the instance-specific dump function + return self._dump_oid(obj, self.many if many is None else many) def dump(self, obj, *, many=None): '''Return a marshalled representation of obj. @@ -515,5 +519,5 @@ def dump(self, obj, *, many=None): collection of objects was marshalled) ''' - # this more or less just calls the instance-specific dump function + # this just calls the instance-specific dump function return self._dump_fields(obj, self.many if many is None else many) From 77ae3c0509e35ddae17423fa0bee5bba6baf2614 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Wed, 10 Dec 2014 18:08:31 +0100 Subject: [PATCH 047/107] schema: Cleanup --- lima/schema.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 938f3a4..a4f9a01 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -466,20 +466,20 @@ def __init__(self, with util.complain_about('only'): fields = _fields_only(fields, util.vector_context(only)) - self._fields = fields - self._ordered = ordered - self.many = many - - # get code/namespace for dump function and create it + # add instance-specific dump function to self. code, namespace = _cns_dump_fields(fields, ordered) self._dump_fields = util.make_function('dump_fields', code, namespace) - # if oid field exists, get code/namespace for dump oid func & create it + # determine oid field (if one exists) if _contains_oid_field(fields): name, field = _oid_field_item(fields) code, namespace = _cns_dump_field(field, name) self._dump_oid = util.make_function('dump_field', code, namespace) + # add instance vars to self + self._fields = fields + self.many = many + def dump_oid(self, obj, *, many=None): '''Return a marshalled representation of the oid of obj. @@ -498,7 +498,7 @@ def dump_oid(self, obj, *, many=None): AttributeError: if the schema doesn't have an oid field ''' - # this just calls the instance-specific dump function + # call the instance-specific dump_oid function (or fail) return self._dump_oid(obj, self.many if many is None else many) def dump(self, obj, *, many=None): @@ -519,5 +519,5 @@ def dump(self, obj, *, many=None): collection of objects was marshalled) ''' - # this just calls the instance-specific dump function + # call the instance-specific dump function return self._dump_fields(obj, self.many if many is None else many) From 035e319d0681e2feac34e58ff22f78263721f41f Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 10:19:23 +0100 Subject: [PATCH 048/107] fields: improve docstrings --- lima/fields.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index f0db9ea..0d69b23 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -164,12 +164,13 @@ class _LinkedObjectField(Field): Schemas defined within a local namespace can not be referenced by name. - attr: The optional name of the corresponding attribute. + attr: The optional name of the corresponding attribute containing the + linked object(s). get: An optional getter function accepting an object as its only - parameter and returning the field value. + parameter and returning the field value (the linked object). - val: An optional constant value for the field. + val: An optional constant value for the field (the linked object). kwargs: Optional keyword arguments to pass to the :class:`Schema`'s constructor when the time has come to instance it. Must be empty if @@ -230,8 +231,8 @@ class Embed(_LinkedObjectField): '''A Field to embed linked object(s). Args: - schema: The schema of the referenced object. This can be specified via - a schema *object,* a schema *class* (that will get instantiated + schema: The schema of the linked object. This can be specified via a + schema *object,* a schema *class* (that will get instantiated immediately) or the qualified *name* of a schema class (for when the named schema has not been defined at the time of the :class:`Embed` object's creation). If two or more schema classes @@ -241,12 +242,13 @@ class Embed(_LinkedObjectField): Schemas defined within a local namespace can not be referenced by name. - attr: The optional name of the corresponding attribute. + attr: The optional name of the corresponding attribute containing the + linked object(s). get: An optional getter function accepting an object as its only - parameter and returning the field value. + parameter and returning the field value (the linked object). - val: An optional constant value for the field. + val: An optional constant value for the field (the linked object). kwargs: Optional keyword arguments to pass to the :class:`Schema`'s constructor when the time has come to instance it. Must be empty if From 4c4ee0e4d55e18060abb7d2bf73be85c844bbea7 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 10:38:17 +0100 Subject: [PATCH 049/107] fields: Add missing space to message string. --- lima/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lima/fields.py b/lima/fields.py index 0d69b23..793477f 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -189,7 +189,7 @@ def __init__(self, *, schema, attr=None, get=None, val=None, **kwargs): # in case schema is a Schema object if isinstance(schema, abc.SchemaABC): if kwargs: - msg = ('No keyword args must be supplied' + msg = ('No keyword args must be supplied ' 'if schema is a Schema object.') raise ValueError(msg) self._schema_inst = schema From d22f711a40ce649f1b46dd98fc30303c162895a4 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 11:03:52 +0100 Subject: [PATCH 050/107] Remove all "oid"-faffery. This is the wrong way to continue. --- lima/fields.py | 24 ++---------------------- lima/schema.py | 45 --------------------------------------------- 2 files changed, 2 insertions(+), 67 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index 793477f..760dce4 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -18,16 +18,9 @@ class Field(abc.FieldABC): val: An optional constant value for the field. - is_oid: If ``True``, marks this field as a field whose value can be - used to identify an object. A schema must not end up with more than - one identifier field. - .. versionadded:: 0.3 The ``val`` parameter. - .. versionadded:: 0.4 - The ``is_oid`` parameter. - :attr:`attr`, :attr:`get` and :attr:`val` are mutually exclusive. When a :class:`Field` object ends up with two or more of the attributes @@ -44,7 +37,7 @@ class Field(abc.FieldABC): instance. ''' - def __init__(self, *, attr=None, get=None, val=None, is_oid=False): + def __init__(self, *, attr=None, get=None, val=None): if sum(v is not None for v in (attr, get, val)) > 1: raise ValueError('attr, get and val are mutually exclusive.') @@ -60,8 +53,6 @@ def __init__(self, *, attr=None, get=None, val=None, is_oid=False): elif val is not None: self.val = val - self.is_oid = is_oid - class Boolean(Field): '''A boolean field. @@ -311,18 +302,7 @@ class Reference(_LinkedObjectField): ''' def pack(self, val): - '''Return the output of the linked object's schema's dump_oid method. - - Args: - val: The nested object to convert. - - Returns: - The output of the linked :class:`lima.schema.Schema`'s - :meth:`lima.schema.Schema.dump_oid` method (or None if ``val`` is - None). - - ''' - return self.schema_inst.dump_oid(val) if val is not None else None + raise NotImplementedError Nested = Embed diff --git a/lima/schema.py b/lima/schema.py index a4f9a01..18b5eac 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -73,24 +73,6 @@ def _mangle_name(name): return mapping[before] + after -def _contains_oid_field(fields): - '''Return True if any of fields claims to be an oid field, else False.''' - return any(f.is_oid for f in fields.values()) - - -def _oid_field_item(fields): - '''Return (name, field)-tuple of the only oid field in fields. - - Raises: - ValueError: if fields doesn't contain exactly one oid field. - ''' - names = [k for k, v in fields.items() if v.is_oid] - if len(names) != 1: - raise ValueError('Not exactly one oid field.') - name = names[0] - return name, fields[name] - - def _cns_field_value(field, field_name, field_num): '''Return (code, namespace)-tuple for determining a field's serialized val. @@ -470,37 +452,10 @@ def __init__(self, code, namespace = _cns_dump_fields(fields, ordered) self._dump_fields = util.make_function('dump_fields', code, namespace) - # determine oid field (if one exists) - if _contains_oid_field(fields): - name, field = _oid_field_item(fields) - code, namespace = _cns_dump_field(field, name) - self._dump_oid = util.make_function('dump_field', code, namespace) - # add instance vars to self self._fields = fields self.many = many - def dump_oid(self, obj, *, many=None): - '''Return a marshalled representation of the oid of obj. - - Args: - obj: The object (or collection of objects) whose oid shall be - marshalled. - - many: Wether obj is a single object or a collection of objects. If - ``many`` is ``None``, the value of the instance's - :attr:`many` attribute is used. - - Returns: - The marshalled representation of the oid of obj - - Raises: - AttributeError: if the schema doesn't have an oid field - - ''' - # call the instance-specific dump_oid function (or fail) - return self._dump_oid(obj, self.many if many is None else many) - def dump(self, obj, *, many=None): '''Return a marshalled representation of obj. From def527a00630014ed3f82fefe49070c67b027de4 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 11:13:51 +0100 Subject: [PATCH 051/107] add test for _LinkedObjectField.pack raising NotImplementedError --- tests/test_fields.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_fields.py b/tests/test_fields.py index 807560c..519b5f6 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -123,3 +123,13 @@ def test_linked_object_by_name(cls): def test_linked_object_error_on_illegal_schema_spec(cls): with pytest.raises(TypeError): field = cls(schema=123) + + +def test_linked_object_field_pack_not_implemented(): + + class DummySchema(schema.Schema): + pass + + field = fields._LinkedObjectField(schema=DummySchema()) + with pytest.raises(NotImplementedError): + field.pack('foo') From 1de04ab89db1d4966c0b790a1ff513b57b246970 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 11:25:53 +0100 Subject: [PATCH 052/107] Remove code related to referencing fields of other schemas ... ... this will be done in a separate feature branch later on. --- lima/fields.py | 10 ---------- lima/schema.py | 26 -------------------------- tests/test_fields.py | 1 - 3 files changed, 37 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index 760dce4..7feb011 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -295,16 +295,6 @@ def pack(self, val): return self.schema_inst.dump(val) if val is not None else None -class Reference(_LinkedObjectField): - '''A Field to reference linked object(s). - - Constructor arguments are similar to those of :class:`Embed`. - - ''' - def pack(self, val): - raise NotImplementedError - - Nested = Embed '''A Field to embed linked object(s) diff --git a/lima/schema.py b/lima/schema.py index 18b5eac..a66b3ac 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -139,32 +139,6 @@ def _cns_field_value(field, field_name, field_num): return val_code, namespace -def _cns_dump_field(field, field_name): - '''Return (code, namespace)-tuple for a customized dump_field function. - - Args: - field: The field. - - field_name: The name (key) of the field. - - Returns: - A tuple consisting of: a) Python code to define the function and b) a - namespace dict containing objects necessary for this code to work. - - ''' - func_tpl = textwrap.dedent( - '''\ - def dump_field(obj, many): - if many: - return [{val_code} for obj in obj] - return {val_code} - ''' - ) - val_code, namespace = _cns_field_value(field, field_name, 0) - code = func_tpl.format(val_code=val_code) - return code, namespace - - def _cns_dump_fields(fields, ordered): '''Return (code, namespace)-tuple for a customized dump_fields function. diff --git a/tests/test_fields.py b/tests/test_fields.py index 519b5f6..fbeeca2 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -21,7 +21,6 @@ LINKED_OBJECT_FIELDS = [ fields._LinkedObjectField, - fields.Reference, fields.Embed, fields.Nested, # to be deprecated in 0.5 ] From 85d144dbb99dce6e15dac87602473c8005b7fdfc Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 11:58:47 +0100 Subject: [PATCH 053/107] fields: tiny change to docstring --- lima/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lima/fields.py b/lima/fields.py index 7feb011..b36902d 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -202,7 +202,7 @@ def __init__(self, *, schema, attr=None, get=None, val=None, **kwargs): @reify def schema_inst(self): - '''Return the associated Schema instance (reified method). + '''Return the associated Schema instance (reified). If no associated Schema instance exists at call time (because only a Schema class name was supplied to the constructor), find the Schema From 9d0054f3a30d644b7e4037dcacdbacf93009698d Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 13:04:57 +0100 Subject: [PATCH 054/107] schema: implement lazy creation of dump function a schema instance's custom dump function is now created at first access, not at schema instantiation. --- lima/schema.py | 13 ++++++++----- tests/test_schema.py | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index a66b3ac..bd10ff3 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -422,14 +422,17 @@ def __init__(self, with util.complain_about('only'): fields = _fields_only(fields, util.vector_context(only)) - # add instance-specific dump function to self. - code, namespace = _cns_dump_fields(fields, ordered) - self._dump_fields = util.make_function('dump_fields', code, namespace) - # add instance vars to self self._fields = fields + self._ordered = ordered self.many = many + @util.reify + def _dump_function(self): + '''Return instance-specific dump function (reified).''' + code, namespace = _cns_dump_fields(self._fields, self._ordered) + return util.make_function('dump_fields', code, namespace) + def dump(self, obj, *, many=None): '''Return a marshalled representation of obj. @@ -449,4 +452,4 @@ def dump(self, obj, *, many=None): ''' # call the instance-specific dump function - return self._dump_fields(obj, self.many if many is None else many) + return self._dump_function(obj, self.many if many is None else many) diff --git a/tests/test_schema.py b/tests/test_schema.py index 7e404a1..6ad1b58 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -618,14 +618,21 @@ def test_fail_on_exclude_and_only(self, person_schema_cls): person_schema = person_schema_cls(exclude=['number'], only=['name']) + +class TestLazyDumpFunctionCreation: def test_fail_on_non_identifier_attr_name(self): '''Test if providing a non-identifier attr name raises an error''' + class TestSchema(schema.Schema): foo = fields.String() foo.attr = 'this-is@not;an+identifier' + # this should succeed + test_schema = TestSchema() + + # the dump function is created at first access. this should fail. with pytest.raises(ValueError): - test_schema = TestSchema() + test_schema._dump_function def test_fail_on_non_identifier_field_name_without_attr(self): '''Test if providing a non-identifier field name raises an error ... @@ -641,8 +648,12 @@ class TestSchema(schema.Schema): } } + # this should succeed + test_schema = TestSchema() + + # the dump function is created at first access. this should fail. with pytest.raises(ValueError): - test_schema = TestSchema() + test_schema._dump_function def test_fail_on_keyword_attr_name(self): '''Test if providing a non-identifier attr name raises an error''' @@ -650,8 +661,12 @@ class TestSchema(schema.Schema): foo = fields.String() foo.attr = 'class' # 'class' is a keyword + # this should succeed + test_schema = TestSchema() + + # the dump function is created at first access. this should fail. with pytest.raises(ValueError): - test_schema = TestSchema() + test_schema._dump_function def test_fail_on_keyword_field_name_without_attr(self): '''Test if providing a non-identifier field name raises an error ... @@ -667,8 +682,12 @@ class TestSchema(schema.Schema): } } + # this should succeed + test_schema = TestSchema() + + # the dump function is created at first access. this should fail. with pytest.raises(ValueError): - test_schema = TestSchema() + test_schema._dump_function def test_succes_on_non_identifier_field_name_with_attr(self): '''Test if providing a non-identifier field name raises no error ... @@ -683,7 +702,10 @@ class TestSchema(schema.Schema): } } + # these should both succeed test_schema = TestSchema() + test_schema._dump_function + assert 'not;an-identifier' in test_schema._fields def test_fail_on_field_name_with_quotes(self): @@ -695,5 +717,9 @@ class TestSchema(schema.Schema): } } + # this should succeed + test_schema = TestSchema() + + # the dump function is created at first access. this should fail. with pytest.raises(ValueError): - test_schema = TestSchema() + test_schema._dump_function From e063fdbbd73d8184ce9e5d2618f540348d0c316d Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 14:48:59 +0100 Subject: [PATCH 055/107] tests: remove unnecessary import --- tests/test_dump.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_dump.py b/tests/test_dump.py index b2f9c3e..f1c5fa2 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -5,7 +5,7 @@ import pytest -from lima import fields, schema, registry +from lima import fields, schema class Person: @@ -182,10 +182,13 @@ class KnightSchema(schema.Schema): embed_schema = KnightSchema(many=True) + class KingSchema(KnightSchema): + title = fields.String() + # here we provide a schema instance. the kwarg "many" is unnecessary + subjects = fields.Embed(schema=embed_schema, many=True) + with pytest.raises(ValueError): - class KingSchema(KnightSchema): - title = fields.String() - subjects = fields.Embed(schema=embed_schema, many=True) + KingSchema.__fields__['subjects']._schema_inst def test_dump_embed_schema_self(king): From 43606a45bae787bcb8fc0d79883e51af57944b05 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 15:10:12 +0100 Subject: [PATCH 056/107] implement lazy eval of schema inst for linked-obj-fields ... and restructure/add tests --- lima/fields.py | 130 +++++++++++++++++++++++-------------------- tests/test_fields.py | 78 +++++++++++++++++++------- 2 files changed, 128 insertions(+), 80 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index b36902d..6480c95 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -4,7 +4,7 @@ from lima import abc from lima import registry -from lima.util import reify +from lima import util class Field(abc.FieldABC): @@ -143,17 +143,19 @@ def pack(val): class _LinkedObjectField(Field): '''A field that references the schema of a linked object. + This is to be considered an abstract class. Concrete implementations will + have to define their own :meth:`pack` methods, utilizing the associated + schema of the linked object. + Args: schema: The schema of the linked object. This can be specified via a - schema *object,* a schema *class* (that will get instantiated - immediately) or the qualified *name* of a schema class (for when - the named schema has not been defined at the time of the - :class:`Embed` object's creation). If two or more schema classes - with the same name exist in different modules, the schema class - name has to be fully module-qualified (see the :ref:`entry on class - names ` for clarification of these concepts). - Schemas defined within a local namespace can not be referenced by - name. + schema *object,* a schema *class* or the qualified *name* of a + schema class (for when the named schema has not been defined at the + time of instantiation. If two or more schema classes with the same + name exist in different modules, the schema class name has to be + fully module-qualified (see the :ref:`entry on class names + ` for clarification of these concepts). Schemas + defined within a local namespace can not be referenced by name. attr: The optional name of the corresponding attribute containing the linked object(s). @@ -167,52 +169,66 @@ class _LinkedObjectField(Field): constructor when the time has come to instance it. Must be empty if ``schema`` is a :class:`lima.schema.Schema` object. - Raises: - ValueError: If ``kwargs`` are specified even if ``schema`` is a - :class:`lima.schema.Schema` *object.* - - TypeError: If ``schema`` has the wrong type. + The schema of the linked object associated with a field of this type will + be lazily evaluated the first time it is needed. This means that incorrect + arguments might produce errors at a time after the field's instantiation. ''' def __init__(self, *, schema, attr=None, get=None, val=None, **kwargs): super().__init__(attr=attr, get=get, val=val) - # in case schema is a Schema object - if isinstance(schema, abc.SchemaABC): - if kwargs: - msg = ('No keyword args must be supplied ' - 'if schema is a Schema object.') - raise ValueError(msg) - self._schema_inst = schema - - # in case schema is a schema class - elif isinstance(schema, type) and issubclass(schema, abc.SchemaABC): - self._schema_inst = schema(**kwargs) - - # in case schema is a schema name: save args for later instantiation - elif isinstance(schema, str): - self._schema_inst = None - self._schema_name = schema - self._schema_kwargs = kwargs - - # otherwise fail - else: - msg = 'Illegal type for schema param: {}' - raise TypeError(msg.format(type(schema))) + # those will be evaluated later on (in _schema_inst) + self._schema_arg = schema + self._schema_kwargs = kwargs - @reify - def schema_inst(self): - '''Return the associated Schema instance (reified). + @util.reify + def _schema_inst(self): + '''Determine and return the associated Schema instance (reified). If no associated Schema instance exists at call time (because only a Schema class name was supplied to the constructor), find the Schema class in the global registry and instantiate it. + Returns: + A schema instance for the linked object. + + Raises: + ValueError: If ``kwargs`` were specified to the field constructor + even if a :class:`lima.schema.Schema` *instance* was provided + as the ``schema`` arg. + + TypeError: If the ``schema`` arg provided to the field constructor + has the wrong type. + ''' - if not self._schema_inst: - cls = registry.global_registry.get(self._schema_name) - self._schema_inst = cls(**self._schema_kwargs) - return self._schema_inst + with util.complain_about('Lazy evaluation of schema instance'): + + # those were supplied to field constructor + schema = self._schema_arg + kwargs = self._schema_kwargs + + # in case schema is a Schema object + if isinstance(schema, abc.SchemaABC): + if kwargs: + msg = ('No additional keyword args must be ' + 'supplied to field constructor if ' + 'schema already is a Schema object.') + raise ValueError(msg) + return schema + + # in case schema is a schema class + elif (isinstance(schema, type) and + issubclass(schema, abc.SchemaABC)): + return schema(**kwargs) + + # in case schema is a string + elif isinstance(schema, str): + cls = registry.global_registry.get(schema) + return cls(**kwargs) + + # otherwise fail + msg = 'schema arg supplied to constructor has illegal type ({})' + raise TypeError(msg.format(type(schema))) def pack(self, val): raise NotImplementedError @@ -223,15 +239,13 @@ class Embed(_LinkedObjectField): Args: schema: The schema of the linked object. This can be specified via a - schema *object,* a schema *class* (that will get instantiated - immediately) or the qualified *name* of a schema class (for when - the named schema has not been defined at the time of the - :class:`Embed` object's creation). If two or more schema classes - with the same name exist in different modules, the schema class - name has to be fully module-qualified (see the :ref:`entry on class - names ` for clarification of these concepts). - Schemas defined within a local namespace can not be referenced by - name. + schema *object,* a schema *class* or the qualified *name* of a + schema class (for when the named schema has not been defined at the + time of instantiation. If two or more schema classes with the same + name exist in different modules, the schema class name has to be + fully module-qualified (see the :ref:`entry on class names + ` for clarification of these concepts). Schemas + defined within a local namespace can not be referenced by name. attr: The optional name of the corresponding attribute containing the linked object(s). @@ -245,11 +259,9 @@ class Embed(_LinkedObjectField): constructor when the time has come to instance it. Must be empty if ``schema`` is a :class:`lima.schema.Schema` object. - Raises: - ValueError: If ``kwargs`` are specified even if ``schema`` is a - :class:`lima.schema.Schema` *object.* - - TypeError: If ``schema`` has the wrong type. + The schema of the linked object associated with a field of this type will + be lazily evaluated the first time it is needed. This means that incorrect + arguments might produce errors at a time after the field's instantiation. Examples: :: @@ -292,7 +304,7 @@ def pack(self, val): None). ''' - return self.schema_inst.dump(val) if val is not None else None + return self._schema_inst.dump(val) if val is not None else None Nested = Embed diff --git a/tests/test_fields.py b/tests/test_fields.py index fbeeca2..d231942 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -4,7 +4,7 @@ import pytest -from lima import abc, fields, schema +from lima import abc, exc, fields, schema PASSTHROUGH_FIELDS = [ @@ -112,23 +112,59 @@ def test_datetime_pack(): assert fields.DateTime.pack(datetime) == expected -@pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) -def test_linked_object_by_name(cls): - field = cls(schema='NonExistentSchemaName') - assert field._schema_name == 'NonExistentSchemaName' - - -@pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) -def test_linked_object_error_on_illegal_schema_spec(cls): - with pytest.raises(TypeError): - field = cls(schema=123) - - -def test_linked_object_field_pack_not_implemented(): - - class DummySchema(schema.Schema): - pass - - field = fields._LinkedObjectField(schema=DummySchema()) - with pytest.raises(NotImplementedError): - field.pack('foo') +class TestLinkedObjectFields: + + class LinkedSchema(schema.Schema): + foo = fields.Integer() + bar = fields.String() + + @pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) + def test_linked_object_by_schema_inst(self, cls): + schema_inst = self.LinkedSchema(many=True) + field = cls(schema=schema_inst) + assert field._schema_arg is schema_inst + assert field._schema_inst is schema_inst + assert field._schema_inst.many == True + + @pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) + def test_linked_object_by_schema_class(self, cls): + schema_cls = self.LinkedSchema + field = cls(schema=schema_cls, many=True) + assert field._schema_arg is schema_cls + assert isinstance(field._schema_inst, schema_cls) + assert field._schema_inst.many == True + + @pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) + def test_linked_object_by_schema_name(self, cls): + schema_name = self.__class__.__qualname__ + '.LinkedSchema' + field = cls(schema=schema_name, many=True) + assert field._schema_arg is schema_name + assert isinstance(field._schema_inst, self.LinkedSchema) + assert field._schema_inst.many == True + + @pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) + def test_linked_object_fail_on_unnecessary_kwargs(self, cls): + schema_inst = self.LinkedSchema() + # here we supply a kwarg, even though schema is already instantiated + field = cls(schema=schema_inst, many=True) + with pytest.raises(ValueError): + field._schema_inst # this will complain about our earlier error + + @pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) + def test_linked_object_fail_on_nonexistent_class(self, cls): + # here we supply a nonexistent schema name + field = cls(schema='NonExistentSchemaName') + with pytest.raises(exc.ClassNotFoundError): + field._schema_inst # this will complain about our earlier error + + @pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) + def test_linked_object_fail_on_illegal_schema_arg(self, cls): + # here we supply a wrong schema arg + field = cls(schema=0xbad1dea) + with pytest.raises(TypeError): + field._schema_inst # this will complain about our earlier error + + def test_linked_object_field_pack_not_implemented(self): + field = fields._LinkedObjectField(schema='ThisDoesntEvenHaveToExist') + with pytest.raises(NotImplementedError): + field.pack('foo') From 3e57c919173ce8581bf9341f2f7757d1238cd86d Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 15:21:23 +0100 Subject: [PATCH 057/107] Schema: lazy eval of dump function: add additional error info --- lima/schema.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index bd10ff3..9fbe0a7 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -430,8 +430,9 @@ def __init__(self, @util.reify def _dump_function(self): '''Return instance-specific dump function (reified).''' - code, namespace = _cns_dump_fields(self._fields, self._ordered) - return util.make_function('dump_fields', code, namespace) + with util.complain_about('Lazy evaluation of dump function'): + code, namespace = _cns_dump_fields(self._fields, self._ordered) + return util.make_function('dump_fields', code, namespace) def dump(self, obj, *, many=None): '''Return a marshalled representation of obj. From a4f47144bfb9224b1bd68f49d617e60e8ba281ab Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 15:44:25 +0100 Subject: [PATCH 058/107] util: fix docstring --- lima/util.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lima/util.py b/lima/util.py index bfc7c16..966368e 100644 --- a/lima/util.py +++ b/lima/util.py @@ -45,14 +45,14 @@ def jammy(self): And usage of Foo: - f = Foo() - v = f.jammy + >>> f = Foo() + >>> v = f.jammy 'jammy called' - print(v) + >>> print(v) 1 - print f.jammy + >>> f.jammy 1 - # jammy func not called the second time; it replaced itself with 1 + >>> # jammy func not called the second time; it replaced itself with 1 Taken from pyramid.decorator (see source for license info). From 3e27ae68ed643b7f5401766acfb9d5820cfbdf6e Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 15:45:00 +0100 Subject: [PATCH 059/107] docs: update project info --- docs/project_info.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/project_info.rst b/docs/project_info.rst index 8461e11..666bd7d 100644 --- a/docs/project_info.rst +++ b/docs/project_info.rst @@ -35,7 +35,9 @@ The lima sources include a copy of the `Read the Docs Sphinx Theme The author believes to have benefited a lot from looking at the documentation and source code of other awesome projects, among them `django `_, -`morepath `_ and +`morepath `_, +`Pyramid `_ +(:class:`lima.util.reify` was taken from there) and `SQLAlchemy `_ as well as the Python standard library itself. (Seriously, look in there!) From fa2efb95d274023f206ad713150ba09a0c90aaf1 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 15:48:18 +0100 Subject: [PATCH 060/107] Update changelog. --- CHANGELOG.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 49b19a0..c9a3272 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,13 @@ Changelog While unreleased, the changelog of lima 0.4 is itself subject to change. +- Implement lazy evaluation of some non-public schema and field attributes + (`Pyramid `_ FTW). This means some things (like + custom dump functions for schema instances) are only evaluated if really + needed, but it also means that some errors might surface at a later time + (lima mentions this when raising such exceptions). + - Small speed improvement when serializing collections. - Deprecate ``fields.Nested`` in favour of ``fields.Embed``. From 2dc66d0c398530ef1cb2f5605ff2a71825b4c3b9 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 16:41:51 +0100 Subject: [PATCH 061/107] don't create docs for internal modules any more those did clutter up the documentation of the actual API. Also, update changelog. --- CHANGELOG.rst | 3 +++ docs/api.rst | 22 ---------------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c9a3272..7e84dcc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,9 @@ Changelog While unreleased, the changelog of lima 0.4 is itself subject to change. +- Don't create docs for internal modules any more - those did clutter up the + documentation of the actual API. + - Implement lazy evaluation of some non-public schema and field attributes (`Pyramid `_ FTW). This means some things (like diff --git a/docs/api.rst b/docs/api.rst index 3851380..076953a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -41,19 +41,6 @@ lima.fields :annotation: =dict(...) -.. _api_registry: - -lima.registry -============= - -.. automodule:: lima.registry - :members: - :exclude-members: global_registry - - .. autodata:: lima.registry.global_registry - :annotation: =lima.registry.Registry() - - .. _api_schema: lima.schema @@ -61,12 +48,3 @@ lima.schema .. automodule:: lima.schema :members: - - -.. _api_util: - -lima.util -========= - -.. automodule:: lima.util - :members: From 74f1507984c9aa0af5514a5b596aec33a593d22c Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 11:34:20 +0100 Subject: [PATCH 062/107] Re-add code related to referencing fields of other schemas ... This reverts commit 1de04ab89db1d4966c0b790a1ff513b57b246970. --- lima/fields.py | 10 ++++++++++ lima/schema.py | 26 ++++++++++++++++++++++++++ tests/test_fields.py | 1 + 3 files changed, 37 insertions(+) diff --git a/lima/fields.py b/lima/fields.py index 6480c95..0d9e046 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -307,6 +307,16 @@ def pack(self, val): return self._schema_inst.dump(val) if val is not None else None +class Reference(_LinkedObjectField): + '''A Field to reference linked object(s). + + Constructor arguments are similar to those of :class:`Embed`. + + ''' + def pack(self, val): + raise NotImplementedError + + Nested = Embed '''A Field to embed linked object(s) diff --git a/lima/schema.py b/lima/schema.py index 9fbe0a7..2ff3b04 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -139,6 +139,32 @@ def _cns_field_value(field, field_name, field_num): return val_code, namespace +def _cns_dump_field(field, field_name): + '''Return (code, namespace)-tuple for a customized dump_field function. + + Args: + field: The field. + + field_name: The name (key) of the field. + + Returns: + A tuple consisting of: a) Python code to define the function and b) a + namespace dict containing objects necessary for this code to work. + + ''' + func_tpl = textwrap.dedent( + '''\ + def dump_field(obj, many): + if many: + return [{val_code} for obj in obj] + return {val_code} + ''' + ) + val_code, namespace = _cns_field_value(field, field_name, 0) + code = func_tpl.format(val_code=val_code) + return code, namespace + + def _cns_dump_fields(fields, ordered): '''Return (code, namespace)-tuple for a customized dump_fields function. diff --git a/tests/test_fields.py b/tests/test_fields.py index d231942..30dc639 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -21,6 +21,7 @@ LINKED_OBJECT_FIELDS = [ fields._LinkedObjectField, + fields.Reference, fields.Embed, fields.Nested, # to be deprecated in 0.5 ] From 4f468e0ff817420ca2e89b3c93b73335eb9cb1d6 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 17:08:53 +0100 Subject: [PATCH 063/107] Move code-generating functions from schema into util. --- lima/schema.py | 156 +------------------------------------------ lima/util.py | 156 ++++++++++++++++++++++++++++++++++++++++++- tests/test_schema.py | 34 ---------- tests/test_util.py | 40 +++++++++++ 4 files changed, 197 insertions(+), 189 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 2ff3b04..88cccd7 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -73,159 +73,6 @@ def _mangle_name(name): return mapping[before] + after -def _cns_field_value(field, field_name, field_num): - '''Return (code, namespace)-tuple for determining a field's serialized val. - - Args: - field: A :class:`lima.fields.Field` instance. - - field_name: The name (key) of the field. - - field_num: A schema-wide unique number for the field - - Returns: - A tuple consisting of: a) a fragment of Python code to determine the - field's value for an object called ``obj`` and b) a namespace dict - containing the objects necessary for this code fragment to work. - - For a field ``myfield`` that has a ``pack`` and a ``get`` callable defined, - the output of this function could look something like this: - - .. code-block:: python - - ( - 'pack3(get3(obj))', # the code - {'get3': myfield.get, 'pack3': myfield.pack} # the namespace - ) - - ''' - namespace = {} - if hasattr(field, 'val'): - # add constant-field-value-shortcut to namespace - name = 'val{}'.format(field_num) - namespace[name] = field.val - - # later, get value using this shortcut - val_code = name - - elif hasattr(field, 'get'): - # add getter-shortcut to namespace - name = 'get{}'.format(field_num) - namespace[name] = field.get - - # later, get value by calling this shortcut - val_code = '{}(obj)'.format(name) - - else: - # neither constant val nor getter: try to get value via attr - # (if attr is not specified, use field name as attr) - obj_attr = getattr(field, 'attr', field_name) - - if not str.isidentifier(obj_attr) or keyword.iskeyword(obj_attr): - msg = 'Not a valid attribute name: {!r}' - raise ValueError(msg.format(obj_attr)) - - # later, get value using this attr - val_code = 'obj.{}'.format(obj_attr) - - if hasattr(field, 'pack'): - # add pack-shortcut to namespace - name = 'pack{}'.format(field_num) - namespace[name] = field.pack - - # later, pass field value to this shortcut - val_code = '{}({})'.format(name, val_code) - - return val_code, namespace - - -def _cns_dump_field(field, field_name): - '''Return (code, namespace)-tuple for a customized dump_field function. - - Args: - field: The field. - - field_name: The name (key) of the field. - - Returns: - A tuple consisting of: a) Python code to define the function and b) a - namespace dict containing objects necessary for this code to work. - - ''' - func_tpl = textwrap.dedent( - '''\ - def dump_field(obj, many): - if many: - return [{val_code} for obj in obj] - return {val_code} - ''' - ) - val_code, namespace = _cns_field_value(field, field_name, 0) - code = func_tpl.format(val_code=val_code) - return code, namespace - - -def _cns_dump_fields(fields, ordered): - '''Return (code, namespace)-tuple for a customized dump_fields function. - - Args: - fields: An ordered mapping of field names to fields. - - ordered: If True, make the resulting function return OrderedDict - objects, else make it return ordinary dicts. - - Returns: - A tuple consisting of: a) Python code to define a dump function for - fields and b) a namespace dict containing objects necessary for this - code to work. - - ''' - # Namespace must contain OrderedDict if we want ordered output. - namespace = {'OrderedDict': OrderedDict} if ordered else {} - - # Get correct templates depending on "ordered" - if ordered: - func_tpl = textwrap.dedent( - '''\ - def dump_fields(obj, many): - if many: - return [OrderedDict([{joined_entries}]) for obj in obj] - return OrderedDict([{joined_entries}]) - ''' - ) - entry_tpl = '("{key}", {val_code})' - else: - func_tpl = textwrap.dedent( - '''\ - def dump_fields(obj, many): - if many: - return [{{{joined_entries}}} for obj in obj] - return {{{joined_entries}}} - ''' - ) - entry_tpl = '"{key}": {val_code}' - - # one entry per field - entries = [] - - # iterate over fields to fill up entries - for field_num, (field_name, field) in enumerate(fields.items()): - val_code, val_ns = _cns_field_value(field, field_name, field_num) - namespace.update(val_ns) - - # try to guard against code injection via quotes in key - key = str(field_name) - if '"' in key or "'" in key: - msg = 'Quotes are not allowed in field names: {}' - raise ValueError(msg.format(key)) - - # add entry - entries.append(entry_tpl.format(key=key, val_code=val_code)) - - code = func_tpl.format(joined_entries=', '.join(entries)) - return code, namespace - - # Schema Metaclass ############################################################ class SchemaMeta(type): @@ -457,7 +304,8 @@ def __init__(self, def _dump_function(self): '''Return instance-specific dump function (reified).''' with util.complain_about('Lazy evaluation of dump function'): - code, namespace = _cns_dump_fields(self._fields, self._ordered) + code, namespace = util._cns_dump_fields(self._fields, + self._ordered) return util.make_function('dump_fields', code, namespace) def dump(self, obj, *, many=None): diff --git a/lima/util.py b/lima/util.py index 966368e..f080645 100644 --- a/lima/util.py +++ b/lima/util.py @@ -7,8 +7,10 @@ any time without deprecation notice or upgrade path. ''' -from collections import abc +from collections import abc, OrderedDict from contextlib import contextmanager +from keyword import iskeyword +from textwrap import dedent @contextmanager @@ -212,3 +214,155 @@ def make_function(name, code, globals_=None): namespace.update(globals_) exec(code, namespace) return namespace[name] + + +def _cns_field_value(field, field_name, field_num): + '''Return (code, namespace)-tuple for determining a field's serialized val. + + Args: + field: A :class:`lima.fields.Field` instance. + + field_name: The name (key) of the field. + + field_num: A schema-wide unique number for the field + + Returns: + A tuple consisting of: a) a fragment of Python code to determine the + field's value for an object called ``obj`` and b) a namespace dict + containing the objects necessary for this code fragment to work. + + For a field ``myfield`` that has a ``pack`` and a ``get`` callable defined, + the output of this function could look something like this: + + .. code-block:: python + + ( + 'pack3(get3(obj))', # the code + {'get3': myfield.get, 'pack3': myfield.pack} # the namespace + ) + ''' + namespace = {} + if hasattr(field, 'val'): + # add constant-field-value-shortcut to namespace + name = 'val{}'.format(field_num) + namespace[name] = field.val + + # later, get value using this shortcut + val_code = name + + elif hasattr(field, 'get'): + # add getter-shortcut to namespace + name = 'get{}'.format(field_num) + namespace[name] = field.get + + # later, get value by calling this shortcut + val_code = '{}(obj)'.format(name) + + else: + # neither constant val nor getter: try to get value via attr + # (if attr is not specified, use field name as attr) + obj_attr = getattr(field, 'attr', field_name) + + if not str.isidentifier(obj_attr) or iskeyword(obj_attr): + msg = 'Not a valid attribute name: {!r}' + raise ValueError(msg.format(obj_attr)) + + # later, get value using this attr + val_code = 'obj.{}'.format(obj_attr) + + if hasattr(field, 'pack'): + # add pack-shortcut to namespace + name = 'pack{}'.format(field_num) + namespace[name] = field.pack + + # later, pass field value to this shortcut + val_code = '{}({})'.format(name, val_code) + + return val_code, namespace + + +def _cns_dump_field(field, field_name): + '''Return (code, namespace)-tuple for a customized dump_field function. + + Args: + field: The field. + + field_name: The name (key) of the field. + + Returns: + A tuple consisting of: a) Python code to define the function and b) a + namespace dict containing objects necessary for this code to work. + + ''' + func_tpl = dedent( + '''\ + def dump_field(obj, many): + if many: + return [{val_code} for obj in obj] + return {val_code} + ''' + ) + val_code, namespace = _cns_field_value(field, field_name, 0) + code = func_tpl.format(val_code=val_code) + return code, namespace + + +def _cns_dump_fields(fields, ordered): + '''Return (code, namespace)-tuple for a customized dump_fields function. + + Args: + fields: An ordered mapping of field names to fields. + + ordered: If True, make the resulting function return OrderedDict + objects, else make it return ordinary dicts. + + Returns: + A tuple consisting of: a) Python code to define a dump function for + fields and b) a namespace dict containing objects necessary for this + code to work. + + ''' + # Namespace must contain OrderedDict if we want ordered output. + namespace = {'OrderedDict': OrderedDict} if ordered else {} + + # Get correct templates depending on "ordered" + if ordered: + func_tpl = dedent( + '''\ + def dump_fields(obj, many): + if many: + return [OrderedDict([{joined_entries}]) for obj in obj] + return OrderedDict([{joined_entries}]) + ''' + ) + entry_tpl = '("{key}", {val_code})' + else: + func_tpl = dedent( + '''\ + def dump_fields(obj, many): + if many: + return [{{{joined_entries}}} for obj in obj] + return {{{joined_entries}}} + ''' + ) + entry_tpl = '"{key}": {val_code}' + + # one entry per field + entries = [] + + # iterate over fields to fill up entries + for field_num, (field_name, field) in enumerate(fields.items()): + val_code, val_ns = _cns_field_value(field, field_name, field_num) + namespace.update(val_ns) + + # try to guard against code injection via quotes in key + key = str(field_name) + if '"' in key or "'" in key: + msg = 'Quotes are not allowed in field names: {}' + raise ValueError(msg.format(key)) + + # add entry + entries.append(entry_tpl.format(key=key, val_code=val_code)) + + code = func_tpl.format(joined_entries=', '.join(entries)) + return code, namespace diff --git a/tests/test_schema.py b/tests/test_schema.py index 6ad1b58..fe5f6e2 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -54,40 +54,6 @@ def test_mangle_name(self): assert mangle('hash__foo') == '#foo' assert mangle('plus__foo') == '+foo' - def test_cns_dump_fields(self): - '''Test if _cns_dump_fields gets a simple function right.''' - from textwrap import dedent - - class TestSchema(schema.Schema): - foo = fields.String(attr='foo_attr') - bar = fields.String() - - code, ns = schema._cns_dump_fields( - TestSchema.__fields__, ordered=False - ) - expected = dedent( - '''\ - def dump_fields(obj, many): - if many: - return [{"foo": obj.foo_attr, "bar": obj.bar} for obj in obj] - return {"foo": obj.foo_attr, "bar": obj.bar} - ''' - ) - assert code == expected - - code, ns = schema._cns_dump_fields( - TestSchema.__fields__, ordered=True - ) - expected = dedent( - '''\ - def dump_fields(obj, many): - if many: - return [OrderedDict([("foo", obj.foo_attr), ("bar", obj.bar)]) for obj in obj] - return OrderedDict([("foo", obj.foo_attr), ("bar", obj.bar)]) - ''' - ) - assert code == expected - class TestSchemaDefinition: '''Class collecting tests of Schema class definition.''' diff --git a/tests/test_util.py b/tests/test_util.py index 9d91f29..73c0431 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -3,6 +3,8 @@ import pytest +from lima import fields +from lima import schema from lima import util @@ -173,3 +175,41 @@ def test_make_function(): # make sure the new name didn't leak out of namespace with pytest.raises(NameError): func_in_namespace + + +class TestCodeGenerationFunctions: + '''Class collecting tests of helper functions.''' + + def test_cns_dump_fields(self): + '''Test if _cns_dump_fields gets a simple function right.''' + from textwrap import dedent + + class TestSchema(schema.Schema): + foo = fields.String(attr='foo_attr') + bar = fields.String() + + code, ns = util._cns_dump_fields( + TestSchema.__fields__, ordered=False + ) + expected = dedent( + '''\ + def dump_fields(obj, many): + if many: + return [{"foo": obj.foo_attr, "bar": obj.bar} for obj in obj] + return {"foo": obj.foo_attr, "bar": obj.bar} + ''' + ) + assert code == expected + + code, ns = util._cns_dump_fields( + TestSchema.__fields__, ordered=True + ) + expected = dedent( + '''\ + def dump_fields(obj, many): + if many: + return [OrderedDict([("foo", obj.foo_attr), ("bar", obj.bar)]) for obj in obj] + return OrderedDict([("foo", obj.foo_attr), ("bar", obj.bar)]) + ''' + ) + assert code == expected From 632826fbadf1e2a878b9ba55249700ca12437883 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 17:21:04 +0100 Subject: [PATCH 064/107] move code-generating functions from schema into util. --- lima/schema.py | 6 ++---- lima/util.py | 30 ++++++++++++++++++------------ tests/test_util.py | 42 ++---------------------------------------- 3 files changed, 22 insertions(+), 56 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 88cccd7..f98df23 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -303,10 +303,8 @@ def __init__(self, @util.reify def _dump_function(self): '''Return instance-specific dump function (reified).''' - with util.complain_about('Lazy evaluation of dump function'): - code, namespace = util._cns_dump_fields(self._fields, - self._ordered) - return util.make_function('dump_fields', code, namespace) + with util.complain_about('Lazy creation of dump function'): + return util.dump_fields_function(self._fields, self._ordered) def dump(self, obj, *, many=None): '''Return a marshalled representation of obj. diff --git a/lima/util.py b/lima/util.py index f080645..bec877b 100644 --- a/lima/util.py +++ b/lima/util.py @@ -189,7 +189,7 @@ def ensure_only_instances_of(collection, cls): raise TypeError('No instances of {}: {!r}'.format(cls, found)) -def make_function(name, code, globals_=None): +def _make_function(name, code, globals_=None): '''Return a function created by executing a code string in a new namespace. This is not much more than a wrapper around :func:`exec`. @@ -281,8 +281,8 @@ def _cns_field_value(field, field_name, field_num): return val_code, namespace -def _cns_dump_field(field, field_name): - '''Return (code, namespace)-tuple for a customized dump_field function. +def dump_field_function(field, field_name): + '''Return a customized dump_field function. Args: field: The field. @@ -290,8 +290,7 @@ def _cns_dump_field(field, field_name): field_name: The name (key) of the field. Returns: - A tuple consisting of: a) Python code to define the function and b) a - namespace dict containing objects necessary for this code to work. + A custom dump_field function. ''' func_tpl = dedent( @@ -304,11 +303,16 @@ def dump_field(obj, many): ) val_code, namespace = _cns_field_value(field, field_name, 0) code = func_tpl.format(val_code=val_code) - return code, namespace + + # assemble function code + code = func_tpl.format(joined_entries=', '.join(entries)) + + # finally create and return function + return _make_function('dump_field', code, namespace) -def _cns_dump_fields(fields, ordered): - '''Return (code, namespace)-tuple for a customized dump_fields function. +def dump_fields_function(fields, ordered): + '''Return a customized dump_fields function. Args: fields: An ordered mapping of field names to fields. @@ -317,9 +321,7 @@ def _cns_dump_fields(fields, ordered): objects, else make it return ordinary dicts. Returns: - A tuple consisting of: a) Python code to define a dump function for - fields and b) a namespace dict containing objects necessary for this - code to work. + A custom dump_fields function. ''' # Namespace must contain OrderedDict if we want ordered output. @@ -364,5 +366,9 @@ def dump_fields(obj, many): # add entry entries.append(entry_tpl.format(key=key, val_code=val_code)) + # assemble function code code = func_tpl.format(joined_entries=', '.join(entries)) - return code, namespace + + # finally create and return function + return _make_function('dump_fields', code, namespace) + diff --git a/tests/test_util.py b/tests/test_util.py index 73c0431..aa323db 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -160,7 +160,7 @@ def test_only_instances_of(): def test_make_function(): code = 'def func_in_namespace(): return 1' - my_function = util.make_function('func_in_namespace', code) + my_function = util._make_function('func_in_namespace', code) assert(callable(my_function)) assert(my_function() == 1) # make sure the new name didn't leak out into globals/locals @@ -169,47 +169,9 @@ def test_make_function(): code = 'def func_in_namespace(): return a' namespace = dict(a=42) - my_function = util.make_function('func_in_namespace', code, namespace) + my_function = util._make_function('func_in_namespace', code, namespace) assert(callable(my_function)) assert(my_function() == 42) # make sure the new name didn't leak out of namespace with pytest.raises(NameError): func_in_namespace - - -class TestCodeGenerationFunctions: - '''Class collecting tests of helper functions.''' - - def test_cns_dump_fields(self): - '''Test if _cns_dump_fields gets a simple function right.''' - from textwrap import dedent - - class TestSchema(schema.Schema): - foo = fields.String(attr='foo_attr') - bar = fields.String() - - code, ns = util._cns_dump_fields( - TestSchema.__fields__, ordered=False - ) - expected = dedent( - '''\ - def dump_fields(obj, many): - if many: - return [{"foo": obj.foo_attr, "bar": obj.bar} for obj in obj] - return {"foo": obj.foo_attr, "bar": obj.bar} - ''' - ) - assert code == expected - - code, ns = util._cns_dump_fields( - TestSchema.__fields__, ordered=True - ) - expected = dedent( - '''\ - def dump_fields(obj, many): - if many: - return [OrderedDict([("foo", obj.foo_attr), ("bar", obj.bar)]) for obj in obj] - return OrderedDict([("foo", obj.foo_attr), ("bar", obj.bar)]) - ''' - ) - assert code == expected From 215731d77635fc2b08b6324e6c7d3dcf97a58cfc Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 11 Dec 2014 18:17:48 +0100 Subject: [PATCH 065/107] util: fix error in dump_field_function --- lima/util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lima/util.py b/lima/util.py index bec877b..48e61bd 100644 --- a/lima/util.py +++ b/lima/util.py @@ -302,10 +302,9 @@ def dump_field(obj, many): ''' ) val_code, namespace = _cns_field_value(field, field_name, 0) - code = func_tpl.format(val_code=val_code) # assemble function code - code = func_tpl.format(joined_entries=', '.join(entries)) + code = func_tpl.format(val_code=val_code) # finally create and return function return _make_function('dump_field', code, namespace) From 5bdbcc4ab88a4d51f9b3ee8a52b4e36721a2ccaa Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 12 Dec 2014 10:37:36 +0100 Subject: [PATCH 066/107] Move code-generating functions back into schema ... It looks like only Schema objects will generate their dump functions after all, so it makes no sense to keep them in a common module. --- lima/schema.py | 189 ++++++++++++++++++++++++++++++++++++++++++- lima/util.py | 184 ----------------------------------------- tests/test_schema.py | 18 +++++ tests/test_util.py | 19 ----- 4 files changed, 204 insertions(+), 206 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index f98df23..3d59a28 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -1,7 +1,7 @@ '''Schema class and related code.''' -import keyword -import textwrap from collections import OrderedDict +from keyword import iskeyword +from textwrap import dedent from lima import abc from lima import exc @@ -73,6 +73,189 @@ def _mangle_name(name): return mapping[before] + after +def _make_function(name, code, globals_=None): + '''Return a function created by executing a code string in a new namespace. + + This is not much more than a wrapper around :func:`exec`. + + Args: + name: The name of the function to create. Must match the function name + in ``code``. + + code: A String containing the function definition code. The name of the + function must match ``name``. + + globals_: A dict of globals to mix into the new function's namespace. + ``__builtins__`` must be provided explicitly if required. + + .. warning: + + All pitfalls of using :func:`exec` apply to this function as well. + + ''' + namespace = dict(__builtins__={}) + if globals_: + namespace.update(globals_) + exec(code, namespace) + return namespace[name] + + +def _cns_field_value(field, field_name, field_num): + '''Return (code, namespace)-tuple for determining a field's serialized val. + + Args: + field: A :class:`lima.fields.Field` instance. + + field_name: The name (key) of the field. + + field_num: A schema-wide unique number for the field + + Returns: + A tuple consisting of: a) a fragment of Python code to determine the + field's value for an object called ``obj`` and b) a namespace dict + containing the objects necessary for this code fragment to work. + + For a field ``myfield`` that has a ``pack`` and a ``get`` callable defined, + the output of this function could look something like this: + + .. code-block:: python + + ( + 'pack3(get3(obj))', # the code + {'get3': myfield.get, 'pack3': myfield.pack} # the namespace + ) + ''' + namespace = {} + if hasattr(field, 'val'): + # add constant-field-value-shortcut to namespace + name = 'val{}'.format(field_num) + namespace[name] = field.val + + # later, get value using this shortcut + val_code = name + + elif hasattr(field, 'get'): + # add getter-shortcut to namespace + name = 'get{}'.format(field_num) + namespace[name] = field.get + + # later, get value by calling this shortcut + val_code = '{}(obj)'.format(name) + + else: + # neither constant val nor getter: try to get value via attr + # (if attr is not specified, use field name as attr) + obj_attr = getattr(field, 'attr', field_name) + + if not str.isidentifier(obj_attr) or iskeyword(obj_attr): + msg = 'Not a valid attribute name: {!r}' + raise ValueError(msg.format(obj_attr)) + + # later, get value using this attr + val_code = 'obj.{}'.format(obj_attr) + + if hasattr(field, 'pack'): + # add pack-shortcut to namespace + name = 'pack{}'.format(field_num) + namespace[name] = field.pack + + # later, pass field value to this shortcut + val_code = '{}({})'.format(name, val_code) + + return val_code, namespace + + +def _dump_field_function(field, field_name): + '''Return a customized dump_field function. + + Args: + field: The field. + + field_name: The name (key) of the field. + + Returns: + A custom dump_field function. + + ''' + func_tpl = dedent( + '''\ + def dump_field(obj, many): + if many: + return [{val_code} for obj in obj] + return {val_code} + ''' + ) + val_code, namespace = _cns_field_value(field, field_name, 0) + + # assemble function code + code = func_tpl.format(val_code=val_code) + + # finally create and return function + return _make_function('dump_field', code, namespace) + + +def _dump_fields_function(fields, ordered): + '''Return a customized dump_fields function. + + Args: + fields: An ordered mapping of field names to fields. + + ordered: If True, make the resulting function return OrderedDict + objects, else make it return ordinary dicts. + + Returns: + A custom dump_fields function. + + ''' + # Namespace must contain OrderedDict if we want ordered output. + namespace = {'OrderedDict': OrderedDict} if ordered else {} + + # Get correct templates depending on "ordered" + if ordered: + func_tpl = dedent( + '''\ + def dump_fields(obj, many): + if many: + return [OrderedDict([{joined_entries}]) for obj in obj] + return OrderedDict([{joined_entries}]) + ''' + ) + entry_tpl = '("{key}", {val_code})' + else: + func_tpl = dedent( + '''\ + def dump_fields(obj, many): + if many: + return [{{{joined_entries}}} for obj in obj] + return {{{joined_entries}}} + ''' + ) + entry_tpl = '"{key}": {val_code}' + + # one entry per field + entries = [] + + # iterate over fields to fill up entries + for field_num, (field_name, field) in enumerate(fields.items()): + val_code, val_ns = _cns_field_value(field, field_name, field_num) + namespace.update(val_ns) + + # try to guard against code injection via quotes in key + key = str(field_name) + if '"' in key or "'" in key: + msg = 'Quotes are not allowed in field names: {}' + raise ValueError(msg.format(key)) + + # add entry + entries.append(entry_tpl.format(key=key, val_code=val_code)) + + # assemble function code + code = func_tpl.format(joined_entries=', '.join(entries)) + + # finally create and return function + return _make_function('dump_fields', code, namespace) + + # Schema Metaclass ############################################################ class SchemaMeta(type): @@ -304,7 +487,7 @@ def __init__(self, def _dump_function(self): '''Return instance-specific dump function (reified).''' with util.complain_about('Lazy creation of dump function'): - return util.dump_fields_function(self._fields, self._ordered) + return _dump_fields_function(self._fields, self._ordered) def dump(self, obj, *, many=None): '''Return a marshalled representation of obj. diff --git a/lima/util.py b/lima/util.py index 48e61bd..114e40e 100644 --- a/lima/util.py +++ b/lima/util.py @@ -187,187 +187,3 @@ def ensure_only_instances_of(collection, cls): found = [obj for obj in collection if not isinstance(obj, cls)] if found: raise TypeError('No instances of {}: {!r}'.format(cls, found)) - - -def _make_function(name, code, globals_=None): - '''Return a function created by executing a code string in a new namespace. - - This is not much more than a wrapper around :func:`exec`. - - Args: - name: The name of the function to create. Must match the function name - in ``code``. - - code: A String containing the function definition code. The name of the - function must match ``name``. - - globals_: A dict of globals to mix into the new function's namespace. - ``__builtins__`` must be provided explicitly if required. - - .. warning: - - All pitfalls of using :func:`exec` apply to this function as well. - - ''' - namespace = dict(__builtins__={}) - if globals_: - namespace.update(globals_) - exec(code, namespace) - return namespace[name] - - -def _cns_field_value(field, field_name, field_num): - '''Return (code, namespace)-tuple for determining a field's serialized val. - - Args: - field: A :class:`lima.fields.Field` instance. - - field_name: The name (key) of the field. - - field_num: A schema-wide unique number for the field - - Returns: - A tuple consisting of: a) a fragment of Python code to determine the - field's value for an object called ``obj`` and b) a namespace dict - containing the objects necessary for this code fragment to work. - - For a field ``myfield`` that has a ``pack`` and a ``get`` callable defined, - the output of this function could look something like this: - - .. code-block:: python - - ( - 'pack3(get3(obj))', # the code - {'get3': myfield.get, 'pack3': myfield.pack} # the namespace - ) - ''' - namespace = {} - if hasattr(field, 'val'): - # add constant-field-value-shortcut to namespace - name = 'val{}'.format(field_num) - namespace[name] = field.val - - # later, get value using this shortcut - val_code = name - - elif hasattr(field, 'get'): - # add getter-shortcut to namespace - name = 'get{}'.format(field_num) - namespace[name] = field.get - - # later, get value by calling this shortcut - val_code = '{}(obj)'.format(name) - - else: - # neither constant val nor getter: try to get value via attr - # (if attr is not specified, use field name as attr) - obj_attr = getattr(field, 'attr', field_name) - - if not str.isidentifier(obj_attr) or iskeyword(obj_attr): - msg = 'Not a valid attribute name: {!r}' - raise ValueError(msg.format(obj_attr)) - - # later, get value using this attr - val_code = 'obj.{}'.format(obj_attr) - - if hasattr(field, 'pack'): - # add pack-shortcut to namespace - name = 'pack{}'.format(field_num) - namespace[name] = field.pack - - # later, pass field value to this shortcut - val_code = '{}({})'.format(name, val_code) - - return val_code, namespace - - -def dump_field_function(field, field_name): - '''Return a customized dump_field function. - - Args: - field: The field. - - field_name: The name (key) of the field. - - Returns: - A custom dump_field function. - - ''' - func_tpl = dedent( - '''\ - def dump_field(obj, many): - if many: - return [{val_code} for obj in obj] - return {val_code} - ''' - ) - val_code, namespace = _cns_field_value(field, field_name, 0) - - # assemble function code - code = func_tpl.format(val_code=val_code) - - # finally create and return function - return _make_function('dump_field', code, namespace) - - -def dump_fields_function(fields, ordered): - '''Return a customized dump_fields function. - - Args: - fields: An ordered mapping of field names to fields. - - ordered: If True, make the resulting function return OrderedDict - objects, else make it return ordinary dicts. - - Returns: - A custom dump_fields function. - - ''' - # Namespace must contain OrderedDict if we want ordered output. - namespace = {'OrderedDict': OrderedDict} if ordered else {} - - # Get correct templates depending on "ordered" - if ordered: - func_tpl = dedent( - '''\ - def dump_fields(obj, many): - if many: - return [OrderedDict([{joined_entries}]) for obj in obj] - return OrderedDict([{joined_entries}]) - ''' - ) - entry_tpl = '("{key}", {val_code})' - else: - func_tpl = dedent( - '''\ - def dump_fields(obj, many): - if many: - return [{{{joined_entries}}} for obj in obj] - return {{{joined_entries}}} - ''' - ) - entry_tpl = '"{key}": {val_code}' - - # one entry per field - entries = [] - - # iterate over fields to fill up entries - for field_num, (field_name, field) in enumerate(fields.items()): - val_code, val_ns = _cns_field_value(field, field_name, field_num) - namespace.update(val_ns) - - # try to guard against code injection via quotes in key - key = str(field_name) - if '"' in key or "'" in key: - msg = 'Quotes are not allowed in field names: {}' - raise ValueError(msg.format(key)) - - # add entry - entries.append(entry_tpl.format(key=key, val_code=val_code)) - - # assemble function code - code = func_tpl.format(joined_entries=', '.join(entries)) - - # finally create and return function - return _make_function('dump_fields', code, namespace) - diff --git a/tests/test_schema.py b/tests/test_schema.py index fe5f6e2..a90137e 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -54,6 +54,24 @@ def test_mangle_name(self): assert mangle('hash__foo') == '#foo' assert mangle('plus__foo') == '+foo' + def test_make_function(self): + code = 'def func_in_namespace(): return 1' + my_function = schema._make_function('func_in_namespace', code) + assert(callable(my_function)) + assert(my_function() == 1) + # make sure the new name didn't leak out into globals/locals + with pytest.raises(NameError): + func_in_namespace + + code = 'def func_in_namespace(): return a' + namespace = dict(a=42) + my_function = schema._make_function('func_in_namespace', code, namespace) + assert(callable(my_function)) + assert(my_function() == 42) + # make sure the new name didn't leak out of namespace + with pytest.raises(NameError): + func_in_namespace + class TestSchemaDefinition: '''Class collecting tests of Schema class definition.''' diff --git a/tests/test_util.py b/tests/test_util.py index aa323db..56ba59a 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -156,22 +156,3 @@ def test_only_instances_of(): with pytest.raises(TypeError): util.ensure_only_instances_of([1, 2, 3.3, 4], int) - - -def test_make_function(): - code = 'def func_in_namespace(): return 1' - my_function = util._make_function('func_in_namespace', code) - assert(callable(my_function)) - assert(my_function() == 1) - # make sure the new name didn't leak out into globals/locals - with pytest.raises(NameError): - func_in_namespace - - code = 'def func_in_namespace(): return a' - namespace = dict(a=42) - my_function = util._make_function('func_in_namespace', code, namespace) - assert(callable(my_function)) - assert(my_function() == 42) - # make sure the new name didn't leak out of namespace - with pytest.raises(NameError): - func_in_namespace From dd9a64b8352e02a462ddc0f04f47a648799fd656 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 12 Dec 2014 10:43:02 +0100 Subject: [PATCH 067/107] util: remove unnecessary imports --- lima/util.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lima/util.py b/lima/util.py index 114e40e..d7f7ad4 100644 --- a/lima/util.py +++ b/lima/util.py @@ -7,10 +7,8 @@ any time without deprecation notice or upgrade path. ''' -from collections import abc, OrderedDict +from collections import abc from contextlib import contextmanager -from keyword import iskeyword -from textwrap import dedent @contextmanager From a032171c13c7edb8851460064099878b265cb29c Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 12 Dec 2014 11:32:28 +0100 Subject: [PATCH 068/107] Schema: add cache for dump field functions (and method to access it) --- lima/schema.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lima/schema.py b/lima/schema.py index 3d59a28..0a2dadc 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -479,6 +479,7 @@ def __init__(self, fields = _fields_only(fields, util.vector_context(only)) # add instance vars to self + self._dump_field_functions = {} self._fields = fields self._ordered = ordered self.many = many @@ -489,6 +490,16 @@ def _dump_function(self): with util.complain_about('Lazy creation of dump function'): return _dump_fields_function(self._fields, self._ordered) + def _dump_field_function(self, field_name): + '''Return instance-specific dump function for a single field.''' + if field_name in self._dump_field_functions: + return self._dump_field_functions[field_name] + + with util.complain_about('Lazy creation of dump function for field'): + f = _dump_field_function(self._fields[field_name], field_name) + self._dump_field_functions[field_name] = f + return f + def dump(self, obj, *, many=None): '''Return a marshalled representation of obj. From 681470d3adba9f55ed57c47db26373f9dca53ea4 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 12 Dec 2014 11:33:55 +0100 Subject: [PATCH 069/107] schema: fix whitespace in docstring --- lima/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lima/schema.py b/lima/schema.py index 0a2dadc..0deb669 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -409,7 +409,7 @@ class Schema(abc.SchemaABC, metaclass=SchemaMeta): ordered: An optional boolean indicating if the :meth:`Schema.dump` method should output :class:`collections.OrderedDict` objects - instead of simple :class:`dict` objects. Defaults to ``False``. + instead of simple :class:`dict` objects. Defaults to ``False``. This does not influence how nested fields are serialized. many: An optional boolean indicating if the new Schema will be From e97f7bac62bdf7b712fba26f6d1b8eb32101a2ca Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 12 Dec 2014 11:34:24 +0100 Subject: [PATCH 070/107] tests: don't parametrize existing tests for linked object fields --- tests/test_fields.py | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 30dc639..f14b1ee 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -19,14 +19,6 @@ fields.DateTime ] -LINKED_OBJECT_FIELDS = [ - fields._LinkedObjectField, - fields.Reference, - fields.Embed, - fields.Nested, # to be deprecated in 0.5 -] - - @pytest.mark.parametrize('cls', SIMPLE_FIELDS) def test_simple_fields(cls): '''Test creation of simple fields.''' @@ -113,55 +105,49 @@ def test_datetime_pack(): assert fields.DateTime.pack(datetime) == expected -class TestLinkedObjectFields: +class TestLinkedObjectField: class LinkedSchema(schema.Schema): foo = fields.Integer() bar = fields.String() - @pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) - def test_linked_object_by_schema_inst(self, cls): + def test_linked_object_by_schema_inst(self): schema_inst = self.LinkedSchema(many=True) - field = cls(schema=schema_inst) + field = fields._LinkedObjectField(schema=schema_inst) assert field._schema_arg is schema_inst assert field._schema_inst is schema_inst assert field._schema_inst.many == True - @pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) - def test_linked_object_by_schema_class(self, cls): + def test_linked_object_by_schema_class(self): schema_cls = self.LinkedSchema - field = cls(schema=schema_cls, many=True) + field = fields._LinkedObjectField(schema=schema_cls, many=True) assert field._schema_arg is schema_cls assert isinstance(field._schema_inst, schema_cls) assert field._schema_inst.many == True - @pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) - def test_linked_object_by_schema_name(self, cls): + def test_linked_object_by_schema_name(self): schema_name = self.__class__.__qualname__ + '.LinkedSchema' - field = cls(schema=schema_name, many=True) + field = fields._LinkedObjectField(schema=schema_name, many=True) assert field._schema_arg is schema_name assert isinstance(field._schema_inst, self.LinkedSchema) assert field._schema_inst.many == True - @pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) - def test_linked_object_fail_on_unnecessary_kwargs(self, cls): + def test_linked_object_fail_on_unnecessary_kwargs(self): schema_inst = self.LinkedSchema() # here we supply a kwarg, even though schema is already instantiated - field = cls(schema=schema_inst, many=True) + field = fields._LinkedObjectField(schema=schema_inst, many=True) with pytest.raises(ValueError): field._schema_inst # this will complain about our earlier error - @pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) - def test_linked_object_fail_on_nonexistent_class(self, cls): + def test_linked_object_fail_on_nonexistent_class(self): # here we supply a nonexistent schema name - field = cls(schema='NonExistentSchemaName') + field = fields._LinkedObjectField(schema='NonExistentSchemaName') with pytest.raises(exc.ClassNotFoundError): field._schema_inst # this will complain about our earlier error - @pytest.mark.parametrize('cls', LINKED_OBJECT_FIELDS) - def test_linked_object_fail_on_illegal_schema_arg(self, cls): + def test_linked_object_fail_on_illegal_schema_arg(self): # here we supply a wrong schema arg - field = cls(schema=0xbad1dea) + field = fields._LinkedObjectField(schema=0xbad1dea) with pytest.raises(TypeError): field._schema_inst # this will complain about our earlier error From e3b3e3dde2b9b48ab112564de7c5b1c50b1717fd Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 12 Dec 2014 12:01:04 +0100 Subject: [PATCH 071/107] fields: Add implementation of Reference field type ... ... this will have to be cleaned up and tested still. --- lima/fields.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lima/fields.py b/lima/fields.py index 0d9e046..6a1dc26 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -313,8 +313,29 @@ class Reference(_LinkedObjectField): Constructor arguments are similar to those of :class:`Embed`. ''' + def __init__(self, + *, + schema, + field_name, + attr=None, + get=None, + val=None, + **kwargs): + super().__init__(schema=schema, attr=attr, get=get, val=val, **kwargs) + self._field_name = field_name + + @util.reify + def _dump_field_function(self): + return self._schema_inst._dump_field_function(self._field_name) + + @util.reify + def _many(self): + return self._schema_inst.many + def pack(self, val): - raise NotImplementedError + if val is None: + return None + return self._dump_field_function(val, self._many) Nested = Embed From bb5af7f61fa3cee46c2299fc84eac7a1d664aad4 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 12 Dec 2014 12:09:41 +0100 Subject: [PATCH 072/107] schema: import modules instead of module contents --- lima/schema.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 0deb669..b72827f 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -1,7 +1,7 @@ '''Schema class and related code.''' +import keyword +import textwrap from collections import OrderedDict -from keyword import iskeyword -from textwrap import dedent from lima import abc from lima import exc @@ -147,7 +147,7 @@ def _cns_field_value(field, field_name, field_num): # (if attr is not specified, use field name as attr) obj_attr = getattr(field, 'attr', field_name) - if not str.isidentifier(obj_attr) or iskeyword(obj_attr): + if not str.isidentifier(obj_attr) or keyword.iskeyword(obj_attr): msg = 'Not a valid attribute name: {!r}' raise ValueError(msg.format(obj_attr)) @@ -177,7 +177,7 @@ def _dump_field_function(field, field_name): A custom dump_field function. ''' - func_tpl = dedent( + func_tpl = textwrap.dedent( '''\ def dump_field(obj, many): if many: @@ -212,7 +212,7 @@ def _dump_fields_function(fields, ordered): # Get correct templates depending on "ordered" if ordered: - func_tpl = dedent( + func_tpl = textwrap.dedent( '''\ def dump_fields(obj, many): if many: @@ -222,7 +222,7 @@ def dump_fields(obj, many): ) entry_tpl = '("{key}", {val_code})' else: - func_tpl = dedent( + func_tpl = textwrap.dedent( '''\ def dump_fields(obj, many): if many: From 99feba8a4eabd4697fc92b18627dbfb408d5ba3a Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 12 Dec 2014 12:42:55 +0100 Subject: [PATCH 073/107] Add read-only properties Schema.many and Schema.ordered --- CHANGELOG.rst | 2 ++ lima/schema.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7e84dcc..9903b61 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,8 @@ Changelog While unreleased, the changelog of lima 0.4 is itself subject to change. +- Add read-only properties ``Schema.many`` and ``Schema.ordered``. + - Don't create docs for internal modules any more - those did clutter up the documentation of the actual API. diff --git a/lima/schema.py b/lima/schema.py index b72827f..cf96345 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -482,7 +482,17 @@ def __init__(self, self._dump_field_functions = {} self._fields = fields self._ordered = ordered - self.many = many + self._many = many + + @property + def many(self): + '''Read-only property: does schema dump collections by default?''' + return self._many + + @property + def ordered(self): + '''Read-only property: does schema dump ordered dicts?''' + return self._ordered @util.reify def _dump_function(self): From a603c0dcacfc66e2adba9597084b781d8e1e9a51 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 12 Dec 2014 12:48:06 +0100 Subject: [PATCH 074/107] tests: pep8 --- tests/test_fields.py | 7 ++++--- tests/test_schema.py | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index f14b1ee..5ea8422 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -19,6 +19,7 @@ fields.DateTime ] + @pytest.mark.parametrize('cls', SIMPLE_FIELDS) def test_simple_fields(cls): '''Test creation of simple fields.''' @@ -116,21 +117,21 @@ def test_linked_object_by_schema_inst(self): field = fields._LinkedObjectField(schema=schema_inst) assert field._schema_arg is schema_inst assert field._schema_inst is schema_inst - assert field._schema_inst.many == True + assert field._schema_inst.many is True def test_linked_object_by_schema_class(self): schema_cls = self.LinkedSchema field = fields._LinkedObjectField(schema=schema_cls, many=True) assert field._schema_arg is schema_cls assert isinstance(field._schema_inst, schema_cls) - assert field._schema_inst.many == True + assert field._schema_inst.many is True def test_linked_object_by_schema_name(self): schema_name = self.__class__.__qualname__ + '.LinkedSchema' field = fields._LinkedObjectField(schema=schema_name, many=True) assert field._schema_arg is schema_name assert isinstance(field._schema_inst, self.LinkedSchema) - assert field._schema_inst.many == True + assert field._schema_inst.many is True def test_linked_object_fail_on_unnecessary_kwargs(self): schema_inst = self.LinkedSchema() diff --git a/tests/test_schema.py b/tests/test_schema.py index a90137e..8f64254 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -65,7 +65,8 @@ def test_make_function(self): code = 'def func_in_namespace(): return a' namespace = dict(a=42) - my_function = schema._make_function('func_in_namespace', code, namespace) + my_function = schema._make_function('func_in_namespace', + code, namespace) assert(callable(my_function)) assert(my_function() == 42) # make sure the new name didn't leak out of namespace From 2ec359daa949c3225e28075b5f83f7442963a556 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 12 Dec 2014 13:05:22 +0100 Subject: [PATCH 075/107] update CHANGELOG. --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9903b61..ec146af 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,8 @@ Changelog While unreleased, the changelog of lima 0.4 is itself subject to change. +- Add new field type ``fields.Reference``. + - Add read-only properties ``Schema.many`` and ``Schema.ordered``. - Don't create docs for internal modules any more - those did clutter up the From 58d094c4e4048dddd589f02f19bae38d0efa4043 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 12 Dec 2014 14:46:48 +0100 Subject: [PATCH 076/107] Breaking Change: remove "many"-arg from Schema.dump This makes internals less messy and seems cleaner overall: A schema instance now serializes either collections of objects, or single objects, but not both - the "many" arg is now gone from Schema.dump --- lima/fields.py | 6 +--- lima/schema.py | 86 +++++++++++++++++++++++++--------------------- tests/test_dump.py | 7 ++-- 3 files changed, 52 insertions(+), 47 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index 6a1dc26..1171bde 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -328,14 +328,10 @@ def __init__(self, def _dump_field_function(self): return self._schema_inst._dump_field_function(self._field_name) - @util.reify - def _many(self): - return self._schema_inst.many - def pack(self, val): if val is None: return None - return self._dump_field_function(val, self._many) + return self._dump_field_function(val) Nested = Embed diff --git a/lima/schema.py b/lima/schema.py index cf96345..7bc40d3 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -165,26 +165,26 @@ def _cns_field_value(field, field_name, field_num): return val_code, namespace -def _dump_field_function(field, field_name): - '''Return a customized dump_field function. +def _dump_field_function(field, field_name, many): + '''Return a customized function that dumps a single field. Args: field: The field. field_name: The name (key) of the field. + many: If True(ish), the resulting function will expect collections of + objects, otherwise it will expect a single object. + Returns: A custom dump_field function. ''' - func_tpl = textwrap.dedent( - '''\ - def dump_field(obj, many): - if many: - return [{val_code} for obj in obj] - return {val_code} - ''' - ) + if many: + func_tpl = 'def dump_field(obj): return {val_code}' + else: + func_tpl = 'def dump_field(objs): return [{val_code} for obj in objs]' + val_code, namespace = _cns_field_value(field, field_name, 0) # assemble function code @@ -194,43 +194,49 @@ def dump_field(obj, many): return _make_function('dump_field', code, namespace) -def _dump_fields_function(fields, ordered): - '''Return a customized dump_fields function. +def _dump_fields_function(fields, ordered, many): + '''Return a customized function that dumps all specified fields. Args: fields: An ordered mapping of field names to fields. - ordered: If True, make the resulting function return OrderedDict - objects, else make it return ordinary dicts. + ordered: If True(ish), the resulting function will return OrderedDict + objects, otherwise it will return ordinary dicts. + + many: If True(ish), the resulting function will expect collections of + objects, otherwise it will expect a single object. Returns: A custom dump_fields function. ''' - # Namespace must contain OrderedDict if we want ordered output. - namespace = {'OrderedDict': OrderedDict} if ordered else {} - - # Get correct templates depending on "ordered" + # Get correct templates & namespace depending on "ordered" and "many" args if ordered: - func_tpl = textwrap.dedent( - '''\ - def dump_fields(obj, many): - if many: - return [OrderedDict([{joined_entries}]) for obj in obj] - return OrderedDict([{joined_entries}]) - ''' - ) + if many: + func_tpl = ( + 'def dump_fields(objs):\n' + ' return [OrderedDict([{joined_entries}]) for obj in objs]' + ) + else: + func_tpl = ( + 'def dump_fields(obj):\n' + ' return OrderedDict([{joined_entries}])' + ) entry_tpl = '("{key}", {val_code})' + namespace = {'OrderedDict': OrderedDict} else: - func_tpl = textwrap.dedent( - '''\ - def dump_fields(obj, many): - if many: - return [{{{joined_entries}}} for obj in obj] - return {{{joined_entries}}} - ''' - ) + if many: + func_tpl = ( + 'def dump_fields(objs):\n' + ' return [{{{joined_entries}}} for obj in objs]' + ) + else: + func_tpl = ( + 'def dump_fields(obj):\n' + ' return {{{joined_entries}}}' + ) entry_tpl = '"{key}": {val_code}' + namespace = {} # one entry per field entries = [] @@ -479,8 +485,8 @@ def __init__(self, fields = _fields_only(fields, util.vector_context(only)) # add instance vars to self - self._dump_field_functions = {} self._fields = fields + self._dump_field_functions = {} self._ordered = ordered self._many = many @@ -498,7 +504,8 @@ def ordered(self): def _dump_function(self): '''Return instance-specific dump function (reified).''' with util.complain_about('Lazy creation of dump function'): - return _dump_fields_function(self._fields, self._ordered) + return _dump_fields_function(self._fields, + self._ordered, self._many) def _dump_field_function(self, field_name): '''Return instance-specific dump function for a single field.''' @@ -506,11 +513,12 @@ def _dump_field_function(self, field_name): return self._dump_field_functions[field_name] with util.complain_about('Lazy creation of dump function for field'): - f = _dump_field_function(self._fields[field_name], field_name) + f = _dump_field_function(self._fields[field_name], + field_name, self._many) self._dump_field_functions[field_name] = f return f - def dump(self, obj, *, many=None): + def dump(self, obj): '''Return a marshalled representation of obj. Args: @@ -529,4 +537,4 @@ def dump(self, obj, *, many=None): ''' # call the instance-specific dump function - return self._dump_function(obj, self.many if many is None else many) + return self._dump_function(obj) diff --git a/tests/test_dump.py b/tests/test_dump.py index f1c5fa2..7969021 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -134,7 +134,7 @@ def test_constant_value_field_dump(king): assert constant_value_schema.dump(king) == expected -def test_many_dump1(knights): +def test_many_dump(knights): multi_person_schema = PersonSchema(only=['name'], many=True) expected = [ {'name': 'Bedevere'}, @@ -144,14 +144,15 @@ def test_many_dump1(knights): assert multi_person_schema.dump(knights) == expected -def test_many_dump2(knights): +def test_dump_fail_on_unexpected_collection(knights): multi_person_schema = PersonSchema(only=['name'], many=False) expected = [ {'name': 'Bedevere'}, {'name': 'Lancelot'}, {'name': 'Galahad'}, ] - assert multi_person_schema.dump(knights, many=True) == expected + with pytest.raises(Exception): + multi_person_schema.dump(knights) @pytest.mark.parametrize('schema_cls', From bd2d7afc1b0532c2f173900cfe097084a09eb130 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 12 Dec 2014 21:25:12 +0100 Subject: [PATCH 077/107] Update changelog. --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ec146af..7e390ce 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,10 @@ Changelog While unreleased, the changelog of lima 0.4 is itself subject to change. +- **Breaking Change:** The ``Schema.dump`` method no longer supports the + ``many`` argument. Schema instances now either serialize collections of + objects or single objects, but not both. + - Add new field type ``fields.Reference``. - Add read-only properties ``Schema.many`` and ``Schema.ordered``. From 33cbe6ef6a8698c1bd8ecfc6bf67f1fe25f344a4 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 16 Dec 2014 22:07:15 +0100 Subject: [PATCH 078/107] schema: improve docstring --- lima/schema.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lima/schema.py b/lima/schema.py index 7bc40d3..c781746 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -386,7 +386,12 @@ def __new__(metacls, name, bases, namespace): @classmethod def __prepare__(metacls, name, bases): - '''Return an OrderedDict as the class namespace.''' + '''Return an OrderedDict as the class namespace. + + This allows us to keep track of the order in which fields were defined + for a schema. + + ''' return OrderedDict() From 27f7013012312b446f548e49839064a5365acf31 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 9 Jan 2015 21:52:49 +0100 Subject: [PATCH 079/107] multiple changes - rename a few functions/methods - reify some field attributes - add docstrings --- lima/fields.py | 69 +++++++++++++++++++++++++++++--------------- lima/schema.py | 56 +++++++++++++++++++---------------- tests/test_schema.py | 12 ++++---- 3 files changed, 83 insertions(+), 54 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index 1171bde..efabe01 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -141,7 +141,7 @@ def pack(val): class _LinkedObjectField(Field): - '''A field that references the schema of a linked object. + '''A base class for fields that represent linked objects. This is to be considered an abstract class. Concrete implementations will have to define their own :meth:`pack` methods, utilizing the associated @@ -157,13 +157,11 @@ class _LinkedObjectField(Field): ` for clarification of these concepts). Schemas defined within a local namespace can not be referenced by name. - attr: The optional name of the corresponding attribute containing the - linked object(s). + attr: See :class:`Field`. - get: An optional getter function accepting an object as its only - parameter and returning the field value (the linked object). + get: See :class:`Field`. - val: An optional constant value for the field (the linked object). + val: See :class:`Field`. kwargs: Optional keyword arguments to pass to the :class:`Schema`'s constructor when the time has come to instance it. Must be empty if @@ -247,13 +245,11 @@ class Embed(_LinkedObjectField): ` for clarification of these concepts). Schemas defined within a local namespace can not be referenced by name. - attr: The optional name of the corresponding attribute containing the - linked object(s). + attr: See :class:`Field`. - get: An optional getter function accepting an object as its only - parameter and returning the field value (the linked object). + get: See :class:`Field`. - val: An optional constant value for the field (the linked object). + val: See :class:`Field`. kwargs: Optional keyword arguments to pass to the :class:`Schema`'s constructor when the time has come to instance it. Must be empty if @@ -292,25 +288,42 @@ class Embed(_LinkedObjectField): user = Embed(attr='login_user', schema=PersonSchema) ''' + @util.reify + def _pack_func(self): + '''Return the associated schema's dump fields *function* (reified).''' + return self._schema_inst._dump_fields_func + def pack(self, val): - '''Return the output of the linked object's schema's dump method. + '''Return the marshalled representation of val. Args: - val: The nested object to convert. + val: The linked object to embed. Returns: - The output of the linked :class:`lima.schema.Schema`'s - :meth:`lima.schema.Schema.dump` method (or None if ``val`` is - None). + The marshalled representation of val determined using the the + associated schema object's (internal) dump fields *function* - or + None if ``val`` is None. ''' - return self._schema_inst.dump(val) if val is not None else None + return self._pack_func(val) if val is not None else None class Reference(_LinkedObjectField): '''A Field to reference linked object(s). - Constructor arguments are similar to those of :class:`Embed`. + Args: + schema: The schema of the linked object (see :class:`Embed`). + + field_name: The field of schema, that shall act as reference to the linked object. + + attr: see :class:`Embed`. + + get: see :class:`Embed`. + + val: see :class:`Embed`. + + kwargs: see :class:`Embed`. + ''' def __init__(self, @@ -325,13 +338,23 @@ def __init__(self, self._field_name = field_name @util.reify - def _dump_field_function(self): - return self._schema_inst._dump_field_function(self._field_name) + def _pack_func(self): + '''Return the associated schema's dump field *function* (reified).''' + return self._schema_inst._dump_field_func(self._field_name) def pack(self, val): - if val is None: - return None - return self._dump_field_function(val) + '''Return the marshalled representation of val. + + Args: + val: The nested object to marshall. + + Returns: + The marshalled representation of val, determined using the the + associated schema object's (internal) dump field *function* - or + None if ``val`` is None. + + ''' + return self._pack_func(val) if val is not None else None Nested = Embed diff --git a/lima/schema.py b/lima/schema.py index c781746..7385d94 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -100,8 +100,8 @@ def _make_function(name, code, globals_=None): return namespace[name] -def _cns_field_value(field, field_name, field_num): - '''Return (code, namespace)-tuple for determining a field's serialized val. +def _field_val_cns(field, field_name, field_num): + '''Return (code, namespace)-tuple for determining a field's value. Args: field: A :class:`lima.fields.Field` instance. @@ -165,7 +165,7 @@ def _cns_field_value(field, field_name, field_num): return val_code, namespace -def _dump_field_function(field, field_name, many): +def _dump_field_func(field, field_name, many): '''Return a customized function that dumps a single field. Args: @@ -177,16 +177,17 @@ def _dump_field_function(field, field_name, many): objects, otherwise it will expect a single object. Returns: - A custom dump_field function. + A custom function that expects an object (or a collection of objects + depending on ``many``), and returns a single field's value per object. ''' + val_code, namespace = _field_val_cns(field, field_name, 0) + if many: func_tpl = 'def dump_field(obj): return {val_code}' else: func_tpl = 'def dump_field(objs): return [{val_code} for obj in objs]' - val_code, namespace = _cns_field_value(field, field_name, 0) - # assemble function code code = func_tpl.format(val_code=val_code) @@ -194,8 +195,8 @@ def _dump_field_function(field, field_name, many): return _make_function('dump_field', code, namespace) -def _dump_fields_function(fields, ordered, many): - '''Return a customized function that dumps all specified fields. +def _dump_fields_func(fields, ordered, many): + '''Return a customized function that dumps multiple fields. Args: fields: An ordered mapping of field names to fields. @@ -207,7 +208,8 @@ def _dump_fields_function(fields, ordered, many): objects, otherwise it will expect a single object. Returns: - A custom dump_fields function. + A custom function that expects an object (or a collectionof objects + depending on ``many``), and returns multiple fields' values per object. ''' # Get correct templates & namespace depending on "ordered" and "many" args @@ -243,7 +245,7 @@ def _dump_fields_function(fields, ordered, many): # iterate over fields to fill up entries for field_num, (field_name, field) in enumerate(fields.items()): - val_code, val_ns = _cns_field_value(field, field_name, field_num) + val_code, val_ns = _field_val_cns(field, field_name, field_num) namespace.update(val_ns) # try to guard against code injection via quotes in key @@ -491,7 +493,7 @@ def __init__(self, # add instance vars to self self._fields = fields - self._dump_field_functions = {} + self._dump_field_funcs = {} # cache for funcs dumping single fields self._ordered = ordered self._many = many @@ -506,21 +508,25 @@ def ordered(self): return self._ordered @util.reify - def _dump_function(self): - '''Return instance-specific dump function (reified).''' - with util.complain_about('Lazy creation of dump function'): - return _dump_fields_function(self._fields, - self._ordered, self._many) - - def _dump_field_function(self, field_name): - '''Return instance-specific dump function for a single field.''' + def _dump_fields_func(self): + '''Return instance-specific dump function for all fields (reified).''' + with util.complain_about('Lazy creation of dump fields function'): + return _dump_fields_func(self._fields, self._ordered, self._many) + + def _dump_field_func(self, field_name): + '''Return instance-specific dump function for a single field. + + Functions are created when requested for the first time and get cached + for subsequent calls of _dump_field_function. + + ''' if field_name in self._dump_field_functions: - return self._dump_field_functions[field_name] + return self._dump_field_funcs[field_name] - with util.complain_about('Lazy creation of dump function for field'): - f = _dump_field_function(self._fields[field_name], - field_name, self._many) - self._dump_field_functions[field_name] = f + with util.complain_about('Lazy creation of dump field function'): + f = _dump_field_funcs(self._fields[field_name], + field_name, self._many) + self._dump_field_funcs[field_name] = f return f def dump(self, obj): @@ -542,4 +548,4 @@ def dump(self, obj): ''' # call the instance-specific dump function - return self._dump_function(obj) + return self._dump_fields_func(obj) diff --git a/tests/test_schema.py b/tests/test_schema.py index 8f64254..080f9a2 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -617,7 +617,7 @@ class TestSchema(schema.Schema): # the dump function is created at first access. this should fail. with pytest.raises(ValueError): - test_schema._dump_function + test_schema._dump_fields_func def test_fail_on_non_identifier_field_name_without_attr(self): '''Test if providing a non-identifier field name raises an error ... @@ -638,7 +638,7 @@ class TestSchema(schema.Schema): # the dump function is created at first access. this should fail. with pytest.raises(ValueError): - test_schema._dump_function + test_schema._dump_fields_func def test_fail_on_keyword_attr_name(self): '''Test if providing a non-identifier attr name raises an error''' @@ -651,7 +651,7 @@ class TestSchema(schema.Schema): # the dump function is created at first access. this should fail. with pytest.raises(ValueError): - test_schema._dump_function + test_schema._dump_fields_func def test_fail_on_keyword_field_name_without_attr(self): '''Test if providing a non-identifier field name raises an error ... @@ -672,7 +672,7 @@ class TestSchema(schema.Schema): # the dump function is created at first access. this should fail. with pytest.raises(ValueError): - test_schema._dump_function + test_schema._dump_fields_func def test_succes_on_non_identifier_field_name_with_attr(self): '''Test if providing a non-identifier field name raises no error ... @@ -689,7 +689,7 @@ class TestSchema(schema.Schema): # these should both succeed test_schema = TestSchema() - test_schema._dump_function + test_schema._dump_fields_func assert 'not;an-identifier' in test_schema._fields @@ -707,4 +707,4 @@ class TestSchema(schema.Schema): # the dump function is created at first access. this should fail. with pytest.raises(ValueError): - test_schema._dump_function + test_schema._dump_fields_func From 56ef357d5e4f58955b5d1ed033d5bb5d056dd8c2 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 9 Jan 2015 22:09:49 +0100 Subject: [PATCH 080/107] update/fix some docstrings --- lima/fields.py | 2 +- lima/schema.py | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index efabe01..9f726bf 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -314,7 +314,7 @@ class Reference(_LinkedObjectField): Args: schema: The schema of the linked object (see :class:`Embed`). - field_name: The field of schema, that shall act as reference to the linked object. + field_name: The field of schema that shall act as reference to the linked object. attr: see :class:`Embed`. diff --git a/lima/schema.py b/lima/schema.py index 7385d94..7a82fd4 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -499,12 +499,12 @@ def __init__(self, @property def many(self): - '''Read-only property: does schema dump collections by default?''' + '''Read-only property: does the dump method expect collections?''' return self._many @property def ordered(self): - '''Read-only property: does schema dump ordered dicts?''' + '''Read-only property: does the dump method return ordered dicts?''' return self._ordered @util.reify @@ -533,18 +533,15 @@ def dump(self, obj): '''Return a marshalled representation of obj. Args: - obj: The object (or collection of objects) to marshall. - - many: Wether obj is a single object or a collection of objects. If - ``many`` is ``None``, the value of the instance's - :attr:`many` attribute is used. + obj: The object (or collection of objects, depending on the + schema's :attr:`many` property) to marshall. Returns: A representation of ``obj`` in the form of a JSON-serializable dict - (or :class:`collections.OrderedDict` if the Schema was created with - ``ordered==True``), with each entry corresponding to one of the - :class:`Schema`'s fields. (Or a list of such dicts in case a - collection of objects was marshalled) + (or :class:`collections.OrderedDict`, depending on the schema's + :attr:`ordered` property), with each entry corresponding to one of + the schema's fields. (Or a list of such dicts in case a collection + of objects was marshalled) ''' # call the instance-specific dump function From 078abb03d835df5b82e70bf00c8de226e188be82 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 9 Jan 2015 22:27:28 +0100 Subject: [PATCH 081/107] schema: test setting of many & ordered property --- tests/test_schema.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_schema.py b/tests/test_schema.py index 080f9a2..7c089b9 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -603,6 +603,28 @@ def test_fail_on_exclude_and_only(self, person_schema_cls): person_schema = person_schema_cls(exclude=['number'], only=['name']) + def test_schema_many_property(self, person_schema_cls): + '''test if schema.many gets set and is read-only''' + person_schema = person_schema_cls() + assert person_schema.many == False + person_schema = person_schema_cls(many=False) + assert person_schema.many == False + person_schema = person_schema_cls(many=True) + assert person_schema.many == True + with pytest.raises(AttributeError): + person_schema.many = False + + def test_schema_ordered_property(self, person_schema_cls): + '''test if schema.ordered gets set and is read-only''' + person_schema = person_schema_cls() + assert person_schema.ordered == False + person_schema = person_schema_cls(ordered=False) + assert person_schema.ordered == False + person_schema = person_schema_cls(ordered=True) + assert person_schema.ordered == True + with pytest.raises(AttributeError): + person_schema.ordered = False + class TestLazyDumpFunctionCreation: def test_fail_on_non_identifier_attr_name(self): From 55c96dfefaf8b18f98458d2f724e4cf144f887f5 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Fri, 9 Jan 2015 22:31:04 +0100 Subject: [PATCH 082/107] fields: pep8 --- lima/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lima/fields.py b/lima/fields.py index 9f726bf..2e4a658 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -314,7 +314,7 @@ class Reference(_LinkedObjectField): Args: schema: The schema of the linked object (see :class:`Embed`). - field_name: The field of schema that shall act as reference to the linked object. + field_name: The field of schema to act as reference to the linked object. attr: see :class:`Embed`. From 9e899d1a7b4ccca90a54fb4310aa28486b92f434 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Mon, 12 Jan 2015 13:29:05 +0100 Subject: [PATCH 083/107] add/improve tests --- tests/test_dump.py | 141 +++++++++++++++++++++++---------------------- 1 file changed, 71 insertions(+), 70 deletions(-) diff --git a/tests/test_dump.py b/tests/test_dump.py index 7969021..a2a331b 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -53,14 +53,13 @@ class KingSchemaEmbedClass(KnightSchema): class KingSchemaEmbedObject(KnightSchema): some_schema_object = KnightSchema(many=True) - title = fields.String() subjects = fields.Embed(schema=some_schema_object) -class SelfReferentialKingSchema(schema.Schema): +class KingSchemaEmbedSelf(schema.Schema): name = fields.String() - boss = fields.Embed(schema=__name__ + '.SelfReferentialKingSchema', + boss = fields.Embed(schema=__name__ + '.KingSchemaEmbedSelf', exclude='boss') @@ -78,81 +77,112 @@ def knights(): ] -def test_simple_dump(king): - person_schema = PersonSchema() +def test_dump_single_unordered(king): + person_schema = PersonSchema(many=False, ordered=False) + result = person_schema.dump(king) expected = { 'title': 'King', 'name': 'Arthur', 'number': 1, 'born': '0501-01-01' } + assert type(result) == dict + assert result == expected + + +def test_dump_single_ordered(king): + person_schema = PersonSchema(many=False, ordered=True) + result = person_schema.dump(king) + expected = OrderedDict( + [ + ('title', 'King'), + ('name', 'Arthur'), + ('number', 1), + ('born', '0501-01-01'), + ] + ) + assert type(result) == OrderedDict + assert result == expected - assert person_schema.dump(king) == expected + +def test_dump_many_unordered(knights): + person_schema = PersonSchema(many=True, ordered=False) + result = person_schema.dump(knights) + expected = [ + dict(title='Sir', name='Bedevere', number=2, born='0502-02-02'), + dict(title='Sir', name='Lancelot', number=3, born='0503-03-03'), + dict(title='Sir', name='Galahad', number=4, born='0504-04-04'), + ] + assert all(type(x) == dict for x in result) + assert result == expected + + +def test_dump_many_ordered(knights): + person_schema = PersonSchema(many=True, ordered=True) + result = person_schema.dump(knights) + expected = [ + OrderedDict([('title', 'Sir'), ('name', 'Bedevere'), + ('number', 2), ('born', '0502-02-02')]), + OrderedDict([('title', 'Sir'), ('name', 'Lancelot'), + ('number', 3), ('born', '0503-03-03')]), + OrderedDict([('title', 'Sir'), ('name', 'Galahad'), + ('number', 4), ('born', '0504-04-04')]), + ] + assert all(type(x) == OrderedDict for x in result) + assert result == expected -def test_simple_dump_exclude(king): +def test_field_exclude_dump(king): person_schema = PersonSchema(exclude=['born']) + result = person_schema.dump(king) expected = { 'title': 'King', 'name': 'Arthur', 'number': 1, } - - assert person_schema.dump(king) == expected + assert result == expected -def test_simple_dump_only(king): +def test_field_only_dump(king): person_schema = PersonSchema(only=['name']) + result = person_schema.dump(king) expected = { 'name': 'Arthur', } - - assert person_schema.dump(king) == expected + assert result == expected def test_attr_field_dump(king): attr_schema = DifferentAttrSchema() + result = attr_schema.dump(king) expected = { 'date_of_birth': '0501-01-01' } - assert attr_schema.dump(king) == expected + assert result == expected def test_getter_field_dump(king): getter_schema = GetterSchema() + result = getter_schema.dump(king) expected = { 'full_name': 'King Arthur' } - assert getter_schema.dump(king) == expected + assert result == expected def test_constant_value_field_dump(king): constant_value_schema = ConstantValueSchema() + result = constant_value_schema.dump(king) expected = { 'constant': '2014-10-20' } - assert constant_value_schema.dump(king) == expected - - -def test_many_dump(knights): - multi_person_schema = PersonSchema(only=['name'], many=True) - expected = [ - {'name': 'Bedevere'}, - {'name': 'Lancelot'}, - {'name': 'Galahad'}, - ] - assert multi_person_schema.dump(knights) == expected + assert result == expected def test_dump_fail_on_unexpected_collection(knights): - multi_person_schema = PersonSchema(only=['name'], many=False) - expected = [ - {'name': 'Bedevere'}, - {'name': 'Lancelot'}, - {'name': 'Galahad'}, - ] + person_schema = PersonSchema(many=False) with pytest.raises(Exception): - multi_person_schema.dump(knights) + person_schema.dump(knights) @pytest.mark.parametrize('schema_cls', @@ -160,7 +190,6 @@ def test_dump_fail_on_unexpected_collection(knights): KingSchemaEmbedClass, KingSchemaEmbedObject]) def test_dump_embed_schema(schema_cls, king, knights): - '''Test with embed Schema specified as a String''' king_schema = schema_cls() king.subjects = knights expected = { @@ -175,56 +204,28 @@ def test_dump_embed_schema(schema_cls, king, knights): assert king_schema.dump(king) == expected -def test_dump_embed_schema_instance_double_kwargs_error(king, knights): - '''Test for ValueError when providing unnecssary kwargs.''' +def test_dump_embed_schema_instance_double_kwargs_error(): - class KnightSchema(schema.Schema): - name = fields.String() + class EmbedSchema(schema.Schema): + some_field = fields.String() embed_schema = KnightSchema(many=True) - class KingSchema(KnightSchema): - title = fields.String() + class EmbeddingSchema(schema.Schema): + another_field = fields.String() # here we provide a schema instance. the kwarg "many" is unnecessary - subjects = fields.Embed(schema=embed_schema, many=True) + incorrect_embed_field = fields.Embed(schema=embed_schema, many=True) + # the incorrect field is constructed lazily. we'll have to access it with pytest.raises(ValueError): - KingSchema.__fields__['subjects']._schema_inst + EmbeddingSchema.__fields__['incorrect_embed_field']._schema_inst def test_dump_embed_schema_self(king): - '''Test with embedded Schema specified as a String''' - king_schema = SelfReferentialKingSchema() + king_schema = KingSchemaEmbedSelf() king.boss = king expected = { 'name': 'Arthur', 'boss': {'name': 'Arthur'}, } assert king_schema.dump(king) == expected - - -def test_ordered(king): - '''Test dumping to OrderedDicts''' - person_schema_unordered = PersonSchema(ordered=False) - expected_unordered = { - 'title': 'King', - 'name': 'Arthur', - 'number': 1, - 'born': '0501-01-01', - } - person_schema_ordered = PersonSchema(ordered=True) - expected_ordered = OrderedDict( - [ - ('title', 'King'), - ('name', 'Arthur'), - ('number', 1), - ('born', '0501-01-01'), - ] - ) - result_unordered = person_schema_unordered.dump(king) - result_ordered = person_schema_ordered.dump(king) - - assert result_unordered.__class__ == dict - assert result_ordered.__class__ == OrderedDict - assert result_unordered == expected_unordered - assert result_ordered == expected_ordered From 9de8f839cad22d8a6604b46a91e9b4c5aa8af39f Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Mon, 12 Jan 2015 14:24:45 +0100 Subject: [PATCH 084/107] fields: pep8 --- lima/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lima/fields.py b/lima/fields.py index 2e4a658..6673eaf 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -314,7 +314,7 @@ class Reference(_LinkedObjectField): Args: schema: The schema of the linked object (see :class:`Embed`). - field_name: The field of schema to act as reference to the linked object. + field_name: The schema field to act as reference to the linked object. attr: see :class:`Embed`. From c8006f4fc9eed6282c0c314d5a16f109f3d17d0d Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Mon, 12 Jan 2015 14:28:28 +0100 Subject: [PATCH 085/107] test_dump: remove incorrect docstr. --- tests/test_dump.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_dump.py b/tests/test_dump.py index a2a331b..d4508e8 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -1,5 +1,3 @@ -'''tests for schema.Schema.dump module''' - from collections import OrderedDict from datetime import date, datetime From b4a5f622f92661e3cc673cfee64c7b5598874e33 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Mon, 12 Jan 2015 15:40:07 +0100 Subject: [PATCH 086/107] schema: fix two errors when dumping single fields, change docstring --- lima/schema.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lima/schema.py b/lima/schema.py index 7a82fd4..9ca686f 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -184,9 +184,9 @@ def _dump_field_func(field, field_name, many): val_code, namespace = _field_val_cns(field, field_name, 0) if many: - func_tpl = 'def dump_field(obj): return {val_code}' - else: func_tpl = 'def dump_field(objs): return [{val_code} for obj in objs]' + else: + func_tpl = 'def dump_field(obj): return {val_code}' # assemble function code code = func_tpl.format(val_code=val_code) @@ -493,7 +493,7 @@ def __init__(self, # add instance vars to self self._fields = fields - self._dump_field_funcs = {} # cache for funcs dumping single fields + self._dump_field_func_cache = {} # dict of funcs dumping single fields self._ordered = ordered self._many = many @@ -517,17 +517,17 @@ def _dump_field_func(self, field_name): '''Return instance-specific dump function for a single field. Functions are created when requested for the first time and get cached - for subsequent calls of _dump_field_function. + for subsequent calls of this method. ''' - if field_name in self._dump_field_functions: - return self._dump_field_funcs[field_name] + if field_name in self._dump_field_func_cache: + return self._dump_field_func_cache[field_name] with util.complain_about('Lazy creation of dump field function'): - f = _dump_field_funcs(self._fields[field_name], - field_name, self._many) - self._dump_field_funcs[field_name] = f - return f + func = _dump_field_func(self._fields[field_name], + field_name, self._many) + self._dump_field_func_cache[field_name] = func + return func def dump(self, obj): '''Return a marshalled representation of obj. From 457476114358d0d97960368fd1d6be859bafbb21 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Mon, 12 Jan 2015 17:15:34 +0100 Subject: [PATCH 087/107] schema: rename _dump_fields_func to _dump_fields --- lima/fields.py | 2 +- lima/schema.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index 6673eaf..669acde 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -291,7 +291,7 @@ class Embed(_LinkedObjectField): @util.reify def _pack_func(self): '''Return the associated schema's dump fields *function* (reified).''' - return self._schema_inst._dump_fields_func + return self._schema_inst._dump_fields def pack(self, val): '''Return the marshalled representation of val. diff --git a/lima/schema.py b/lima/schema.py index 9ca686f..dc30806 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -508,7 +508,7 @@ def ordered(self): return self._ordered @util.reify - def _dump_fields_func(self): + def _dump_fields(self): '''Return instance-specific dump function for all fields (reified).''' with util.complain_about('Lazy creation of dump fields function'): return _dump_fields_func(self._fields, self._ordered, self._many) @@ -545,4 +545,4 @@ def dump(self, obj): ''' # call the instance-specific dump function - return self._dump_fields_func(obj) + return self._dump_fields(obj) From 58fbc0dbac8afe2a396046fc883e54b44a952bce Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Mon, 12 Jan 2015 17:16:40 +0100 Subject: [PATCH 088/107] cleanup and add tests --- tests/test_fields.py | 173 ++++++++++++++++++++++++++++++++++++------- tests/test_schema.py | 48 ++++++------ 2 files changed, 171 insertions(+), 50 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 5ea8422..5bc2db2 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -106,53 +106,172 @@ def test_datetime_pack(): assert fields.DateTime.pack(datetime) == expected -class TestLinkedObjectField: +class SomeClass: + '''Arbitrary class (to test linked object fields).''' + def __init__(self, name, number): + self.name = name + self.number = number - class LinkedSchema(schema.Schema): - foo = fields.Integer() - bar = fields.String() - def test_linked_object_by_schema_inst(self): - schema_inst = self.LinkedSchema(many=True) - field = fields._LinkedObjectField(schema=schema_inst) - assert field._schema_arg is schema_inst - assert field._schema_inst is schema_inst - assert field._schema_inst.many is True +class SomeSchema(schema.Schema): + '''Schema for SomeClass (to test linked object fields).''' + name = fields.String() + number = fields.Integer() - def test_linked_object_by_schema_class(self): - schema_cls = self.LinkedSchema - field = fields._LinkedObjectField(schema=schema_cls, many=True) - assert field._schema_arg is schema_cls - assert isinstance(field._schema_inst, schema_cls) - assert field._schema_inst.many is True - def test_linked_object_by_schema_name(self): - schema_name = self.__class__.__qualname__ + '.LinkedSchema' - field = fields._LinkedObjectField(schema=schema_name, many=True) - assert field._schema_arg is schema_name - assert isinstance(field._schema_inst, self.LinkedSchema) +class TestLinkedObjectField: + '''Tests for _LinkedObjectField, the base class for Embed and Reference''' + + @pytest.mark.parametrize( + 'schema_arg', + [SomeSchema(), SomeSchema, __name__ + '.SomeSchema'] + ) + def test_linked_object_field(self, schema_arg): + field = fields._LinkedObjectField(schema=schema_arg) + assert field._schema_arg is schema_arg + assert isinstance(field._schema_inst, SomeSchema) + + @pytest.mark.parametrize( + 'field', + [ + fields._LinkedObjectField( + schema=SomeSchema(many=True, only='number') + ), + fields._LinkedObjectField( + schema=SomeSchema, many=True, only='number' + ), + fields._LinkedObjectField( + schema=__name__ + '.SomeSchema', many=True, only='number' + ) + ] + ) + def test_linked_object_field_with_kwargs(self, field): + assert isinstance(field._schema_inst, SomeSchema) assert field._schema_inst.many is True + assert list(field._schema_inst._fields.keys()) == ['number'] def test_linked_object_fail_on_unnecessary_kwargs(self): - schema_inst = self.LinkedSchema() - # here we supply a kwarg, even though schema is already instantiated + schema_inst = SomeSchema() + # "many" is already defined for a schema instance. providing it again + # when embedding will raise an error on lazy eval + field = fields._LinkedObjectField(schema=schema_inst, many=True) + with pytest.raises(ValueError): + field._schema_inst # this will complain about our earlier error + + def test_linked_object_fail_on_unnecessary_kwargs(self): + schema_inst = SomeSchema() + # "many" is already defined for a schema instance. providing it again + # when embedding will raise an error on lazy eval field = fields._LinkedObjectField(schema=schema_inst, many=True) with pytest.raises(ValueError): field._schema_inst # this will complain about our earlier error def test_linked_object_fail_on_nonexistent_class(self): - # here we supply a nonexistent schema name + # nonexistent schema name will raise an error on lazy eval field = fields._LinkedObjectField(schema='NonExistentSchemaName') with pytest.raises(exc.ClassNotFoundError): field._schema_inst # this will complain about our earlier error def test_linked_object_fail_on_illegal_schema_arg(self): - # here we supply a wrong schema arg - field = fields._LinkedObjectField(schema=0xbad1dea) + # wrong "schema" arg type will raise an error on lazy eval + field = fields._LinkedObjectField(schema=123) with pytest.raises(TypeError): field._schema_inst # this will complain about our earlier error def test_linked_object_field_pack_not_implemented(self): - field = fields._LinkedObjectField(schema='ThisDoesntEvenHaveToExist') + field = fields._LinkedObjectField(schema=SomeSchema) + # pack method is not implemented with pytest.raises(NotImplementedError): field.pack('foo') + + +class TestEmbed: + '''Tests for Embed, a class for embedding linked objects.''' + + @pytest.mark.parametrize( + 'schema_arg', + [SomeSchema(), SomeSchema, __name__ + '.SomeSchema'] + ) + def test_pack(self, schema_arg): + field = fields.Embed(schema=schema_arg) + result = field.pack(SomeClass('one', 1)) + expected = {'name': 'one', 'number': 1} + assert result == expected + + @pytest.mark.parametrize( + 'field', + [ + fields.Embed( + schema=SomeSchema(many=True, only='number') + ), + fields.Embed( + schema=SomeSchema, many=True, only='number' + ), + fields.Embed( + schema=__name__ + '.SomeSchema', many=True, only='number' + ) + ] + ) + def test_pack_with_kwargs(self, field): + result = field.pack([SomeClass('one', 1), SomeClass('two', 2)]) + expected = [{'number': 1}, {'number': 2}] + assert result == expected + + +class TestReference: + '''Tests for Reference, a class for referencing linked objects.''' + + @pytest.mark.parametrize( + 'schema_arg', + [SomeSchema(), SomeSchema, __name__ + '.SomeSchema'] + ) + def test_pack(self, schema_arg): + field = fields.Reference(schema=schema_arg, field_name='number') + result = field.pack(SomeClass('one', 1)) + expected = 1 + assert result == expected + + @pytest.mark.parametrize( + 'field', + [ + fields.Reference( + schema=SomeSchema(many=True, only='number'), + field_name = 'number' + ), + fields.Reference( + schema=SomeSchema, many=True, only='number', + field_name = 'number' + ), + fields.Reference( + schema=__name__ + '.SomeSchema', many=True, only='number', + field_name = 'number' + ) + ] + ) + def test_pack_with_kwargs(self, field): + result = field.pack([SomeClass('one', 1), SomeClass('two', 2)]) + expected = [1, 2] + assert result == expected + + @pytest.mark.parametrize( + 'field', + [ + fields.Reference( + schema=SomeSchema(many=True, exclude='number'), + field_name = 'number' + ), + fields.Reference( + schema=SomeSchema, many=True, exclude='number', + field_name = 'number' + ), + fields.Reference( + schema=__name__ + '.SomeSchema', many=True, exclude='number', + field_name = 'number' + ) + ] + ) + def test_fail_on_missing_field_name(self, field): + # field 'number' is no field of the field's associated schema instance since it + # was excluded + with pytest.raises(KeyError): + result = field.pack([SomeClass('one', 1), SomeClass('two', 2)]) diff --git a/tests/test_schema.py b/tests/test_schema.py index 7c089b9..87df0a6 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -627,6 +627,7 @@ def test_schema_ordered_property(self, person_schema_cls): class TestLazyDumpFunctionCreation: + def test_fail_on_non_identifier_attr_name(self): '''Test if providing a non-identifier attr name raises an error''' @@ -634,12 +635,12 @@ class TestSchema(schema.Schema): foo = fields.String() foo.attr = 'this-is@not;an+identifier' - # this should succeed test_schema = TestSchema() - - # the dump function is created at first access. this should fail. + # dump funcs are created at first access. the next lines should fail with pytest.raises(ValueError): - test_schema._dump_fields_func + test_schema._dump_fields + with pytest.raises(ValueError): + test_schema._dump_field_func('foo') def test_fail_on_non_identifier_field_name_without_attr(self): '''Test if providing a non-identifier field name raises an error ... @@ -655,12 +656,14 @@ class TestSchema(schema.Schema): } } - # this should succeed test_schema = TestSchema() - - # the dump function is created at first access. this should fail. + # dump funcs are created at first access. the next lines should fail with pytest.raises(ValueError): - test_schema._dump_fields_func + test_schema._dump_fields + with pytest.raises(ValueError): + test_schema._dump_field_func('not@an-identifier') + + # todo: fail on keyerror def test_fail_on_keyword_attr_name(self): '''Test if providing a non-identifier attr name raises an error''' @@ -668,12 +671,12 @@ class TestSchema(schema.Schema): foo = fields.String() foo.attr = 'class' # 'class' is a keyword - # this should succeed test_schema = TestSchema() - - # the dump function is created at first access. this should fail. + # dump funcs are created at first access. the next lines should fail with pytest.raises(ValueError): - test_schema._dump_fields_func + test_schema._dump_fields + with pytest.raises(ValueError): + test_schema._dump_field_func('foo') def test_fail_on_keyword_field_name_without_attr(self): '''Test if providing a non-identifier field name raises an error ... @@ -689,12 +692,12 @@ class TestSchema(schema.Schema): } } - # this should succeed test_schema = TestSchema() - - # the dump function is created at first access. this should fail. + # dump funcs are created at first access. the next lines should fail + with pytest.raises(ValueError): + test_schema._dump_fields with pytest.raises(ValueError): - test_schema._dump_fields_func + test_schema._dump_field_func('class') def test_succes_on_non_identifier_field_name_with_attr(self): '''Test if providing a non-identifier field name raises no error ... @@ -709,10 +712,10 @@ class TestSchema(schema.Schema): } } - # these should both succeed + # these should all succeed test_schema = TestSchema() - test_schema._dump_fields_func - + test_schema._dump_fields + test_schema._dump_field_func('not;an-identifier') assert 'not;an-identifier' in test_schema._fields def test_fail_on_field_name_with_quotes(self): @@ -724,9 +727,8 @@ class TestSchema(schema.Schema): } } - # this should succeed test_schema = TestSchema() - - # the dump function is created at first access. this should fail. + # dump funcs are created at first access. the next lines should fail with pytest.raises(ValueError): - test_schema._dump_fields_func + test_schema._dump_fields + From fcc2ab6d6daad7a03707a00ddbb2f9f3f25c563c Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Mon, 12 Jan 2015 17:48:49 +0100 Subject: [PATCH 089/107] allow quotes in field names (and update changelog/docs) --- CHANGELOG.rst | 2 ++ docs/advanced.rst | 3 --- lima/schema.py | 14 +++++--------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7e390ce..bad527a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,8 @@ Changelog needed, but it also means that some errors might surface at a later time (lima mentions this when raising such exceptions). +- Allow quotes in field names. + - Small speed improvement when serializing collections. - Deprecate ``fields.Nested`` in favour of ``fields.Embed``. diff --git a/docs/advanced.rst b/docs/advanced.rst index 6e5ddef..602a8fb 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -110,9 +110,6 @@ This enables us to do the following: explicitly how the data for these fields should be determined (see :ref:`field_data_sources`). - Also, quotes in field names are currently not allowed in lima, regardless - of how they are specified. - Advanced Topics Recap ===================== diff --git a/lima/schema.py b/lima/schema.py index dc30806..2da389e 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -224,7 +224,7 @@ def _dump_fields_func(fields, ordered, many): 'def dump_fields(obj):\n' ' return OrderedDict([{joined_entries}])' ) - entry_tpl = '("{key}", {val_code})' + entry_tpl = '({field_name!r}, {val_code})' namespace = {'OrderedDict': OrderedDict} else: if many: @@ -237,7 +237,7 @@ def _dump_fields_func(fields, ordered, many): 'def dump_fields(obj):\n' ' return {{{joined_entries}}}' ) - entry_tpl = '"{key}": {val_code}' + entry_tpl = '{field_name!r}: {val_code}' namespace = {} # one entry per field @@ -248,14 +248,10 @@ def _dump_fields_func(fields, ordered, many): val_code, val_ns = _field_val_cns(field, field_name, field_num) namespace.update(val_ns) - # try to guard against code injection via quotes in key - key = str(field_name) - if '"' in key or "'" in key: - msg = 'Quotes are not allowed in field names: {}' - raise ValueError(msg.format(key)) - # add entry - entries.append(entry_tpl.format(key=key, val_code=val_code)) + entries.append( + entry_tpl.format(field_name=field_name, val_code=val_code) + ) # assemble function code code = func_tpl.format(joined_entries=', '.join(entries)) From 1e71e9df8b16cf9fb4422fdb4a5b9825b9d086e3 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Mon, 12 Jan 2015 18:03:13 +0100 Subject: [PATCH 090/107] remove obsolete test, add another test --- tests/test_schema.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index 87df0a6..1da2bb7 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -663,8 +663,6 @@ class TestSchema(schema.Schema): with pytest.raises(ValueError): test_schema._dump_field_func('not@an-identifier') - # todo: fail on keyerror - def test_fail_on_keyword_attr_name(self): '''Test if providing a non-identifier attr name raises an error''' class TestSchema(schema.Schema): @@ -678,6 +676,16 @@ class TestSchema(schema.Schema): with pytest.raises(ValueError): test_schema._dump_field_func('foo') + def test_fail_on_nonexistent_field(self): + '''Test if providing a non-identifier attr name raises an error''' + class TestSchema(schema.Schema): + foo = fields.String() + + test_schema = TestSchema() + # dump funcs are created at first access. the next lines should fail + with pytest.raises(KeyError): + test_schema._dump_field_func('this_field_does_not_exist') + def test_fail_on_keyword_field_name_without_attr(self): '''Test if providing a non-identifier field name raises an error ... @@ -717,18 +725,3 @@ class TestSchema(schema.Schema): test_schema._dump_fields test_schema._dump_field_func('not;an-identifier') assert 'not;an-identifier' in test_schema._fields - - def test_fail_on_field_name_with_quotes(self): - '''Test if providing a field name with quotes raises an error ...''' - class TestSchema(schema.Schema): - __lima_args__ = { - 'include': { - 'field_with_"quotes"': fields.String(attr='foo') - } - } - - test_schema = TestSchema() - # dump funcs are created at first access. the next lines should fail - with pytest.raises(ValueError): - test_schema._dump_fields - From a093f90815c6c4248b5c1f28434eb283c30e68a3 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Mon, 12 Jan 2015 18:07:34 +0100 Subject: [PATCH 091/107] test caching of dynamically created functions --- tests/test_schema.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_schema.py b/tests/test_schema.py index 1da2bb7..8441c3f 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -725,3 +725,18 @@ class TestSchema(schema.Schema): test_schema._dump_fields test_schema._dump_field_func('not;an-identifier') assert 'not;an-identifier' in test_schema._fields + + def test_function_caching(self): + '''Test if lazily created functions are cached''' + + class TestSchema(schema.Schema): + foo = fields.String() + + test_schema = TestSchema() + fn1 = test_schema._dump_fields + fn2 = test_schema._dump_fields + assert fn1 is fn2 # after first eval, the same obj should be returned + + fn1 = test_schema._dump_field_func('foo') + fn2 = test_schema._dump_field_func('foo') + assert fn1 is fn2 # after first eval, the same obj should be returned From 7925ba1b963b5ccd39875218a92db29c0f435c2e Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Mon, 12 Jan 2015 18:32:29 +0100 Subject: [PATCH 092/107] test dumping of fields with exotic names --- tests/test_dump.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_dump.py b/tests/test_dump.py index d4508e8..b412f62 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -227,3 +227,38 @@ def test_dump_embed_schema_self(king): 'boss': {'name': 'Arthur'}, } assert king_schema.dump(king) == expected + + +def test_dump_exotic_field_names(): + + exotic_names = [ + '', # empty string + '"', # single quote + "'", # double quote + '\u2665', # unicode heart symbol + 'print(123)', # valid python code + 'print("123\'', # invalid python code + ] + + class ExoticFieldNamesSchema(schema.Schema): + __lima_args__ = { + 'include': { + name: fields.String(attr='foo') for name in exotic_names + } + } + + class Foo: + def __init__(self): + self.foo = 'foobar' + obj = Foo() + + exotic_field_names_schema = ExoticFieldNamesSchema() + result = exotic_field_names_schema.dump(obj) + expected = {name: 'foobar' for name in exotic_names} + assert result == expected + + for name in exotic_names: + dump_field_func = exotic_field_names_schema._dump_field_func(name) + result = dump_field_func(obj) + expected = 'foobar' + assert result == expected From d7b24ca1b2fef439ec3483f96647755978c2948a Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Mon, 12 Jan 2015 18:47:57 +0100 Subject: [PATCH 093/107] docs: update docs on removed "many" param of Schema.dump --- docs/schemas.rst | 12 ++---------- lima/schema.py | 3 +++ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/docs/schemas.rst b/docs/schemas.rst index ecda39e..44830d8 100644 --- a/docs/schemas.rst +++ b/docs/schemas.rst @@ -269,18 +269,10 @@ Consider this: ] Instead of looping over this collection ourselves, we can ask the schema object -to do this for us - either for a single call (by specifying ``many=True`` to -the :meth:`dump` method), or for every call of :meth:`dump` (by specifying -``many=True`` to the schema's constructor): +to do this for us by specifying ``many=True`` to the schema's constructor): .. code-block:: python - :emphasize-lines: 2,7 - - person_schema = PersonSchema(only='last_name') - person_schema.dump(persons, many=True) - # [{'last_name': 'Hemingway'}, - # {'last_name': 'Woolf'}, - # {'last_name': 'Zweig'}] + :emphasize-lines: 1 many_persons_schema = PersonSchema(only='last_name', many=True) many_persons_schema.dump(persons) diff --git a/lima/schema.py b/lima/schema.py index 2da389e..afb8188 100644 --- a/lima/schema.py +++ b/lima/schema.py @@ -539,6 +539,9 @@ def dump(self, obj): the schema's fields. (Or a list of such dicts in case a collection of objects was marshalled) + .. versionchanged:: 0.4 + Removed the ``many`` parameter of this method. + ''' # call the instance-specific dump function return self._dump_fields(obj) From 2c20a6dc3dc8581c8847b874247c433afc3d08f9 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Mon, 12 Jan 2015 19:07:49 +0100 Subject: [PATCH 094/107] fields: extend docstring on Embed.pack --- lima/fields.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lima/fields.py b/lima/fields.py index 669acde..ecad54a 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -304,6 +304,10 @@ def pack(self, val): associated schema object's (internal) dump fields *function* - or None if ``val`` is None. + Note that overriding the associated schema's + :meth:`~lima.schema.Schema.dump` *method* does not affect the result of + this method. This behaviour might change in the future. + ''' return self._pack_func(val) if val is not None else None From f520ddaf0487650a8e02a13dab4a76512636a47a Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 13 Jan 2015 14:49:33 +0100 Subject: [PATCH 095/107] Update/rephrase changelog. --- CHANGELOG.rst | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bad527a..01875bd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,32 +10,40 @@ Changelog While unreleased, the changelog of lima 0.4 is itself subject to change. - **Breaking Change:** The ``Schema.dump`` method no longer supports the - ``many`` argument. Schema instances now either serialize collections of - objects or single objects, but not both. + ``many`` argument. This makes ``many`` consistent with ``ordered`` and + simplifies internals. -- Add new field type ``fields.Reference``. +- Improve support for serializing linked data: -- Add read-only properties ``Schema.many`` and ``Schema.ordered``. + - Add new field type ``fields.Reference`` for references to linked objects. -- Don't create docs for internal modules any more - those did clutter up the - documentation of the actual API. + - Add new name for ``fields.Nested``: ``fields.Embed``. Deprecate + ``fields.Nested`` in favour of ``fields.Embed``. -- Implement lazy evaluation of some non-public schema and field attributes - (`Pyramid `_ FTW). This means some things (like - custom dump functions for schema instances) are only evaluated if really - needed, but it also means that some errors might surface at a later time - (lima mentions this when raising such exceptions). +- Add read-only properties ``many`` and ``ordered`` for schema objects. + +- Don't generate docs for internal modules any more - those did clutter up the + documentation of the actual API (the docstrings remain though). + +- Implement lazy evaluation and caching of some attributes (affects methods: + ``Schema.dump``, ``Embed.pack`` and ``Reference.pack``). This means stuff is + only evaluated if and when really needed, but it also means: + + - The very first time data is dumped/packed by a Schema/Embed/Reference + object, there will be a tiny delay. Keep objects around to mitigate this + effect. + + - Some errors might surface at a later time. lima mentions this when + raising exceptions though. - Allow quotes in field names. - Small speed improvement when serializing collections. -- Deprecate ``fields.Nested`` in favour of ``fields.Embed``. - -- Remove ``fields.type_mapping``. Use ``fields.TYPE_MAPPING`` instead. +- Remove deprecated field ``fields.type_mapping``. Use ``fields.TYPE_MAPPING`` + instead. -- Overall cleanup. +- Overall cleanup, improvements and bug fixes. 0.3.1 (2014-11-11) From aef67088da80c73f9acd90f0508ede8889c71378 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 13 Jan 2015 14:54:11 +0100 Subject: [PATCH 096/107] license: we have 2015 now. --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 7af9a76..7257704 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014, Bernhard Weitzhofer +Copyright (c) 2014-2015, Bernhard Weitzhofer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From b06013085e59a9267512391153cfe5cf4a830ac5 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Tue, 13 Jan 2015 18:30:06 +0100 Subject: [PATCH 097/107] fields: improve docstrings --- lima/fields.py | 49 +++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/lima/fields.py b/lima/fields.py index ecad54a..4a61fef 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -233,7 +233,7 @@ def pack(self, val): class Embed(_LinkedObjectField): - '''A Field to embed linked object(s). + '''A Field to embed linked objects. Args: schema: The schema of the linked object. This can be specified via a @@ -255,10 +255,6 @@ class Embed(_LinkedObjectField): constructor when the time has come to instance it. Must be empty if ``schema`` is a :class:`lima.schema.Schema` object. - The schema of the linked object associated with a field of this type will - be lazily evaluated the first time it is needed. This means that incorrect - arguments might produce errors at a time after the field's instantiation. - Examples: :: # refer to PersonSchema class @@ -300,31 +296,35 @@ def pack(self, val): val: The linked object to embed. Returns: - The marshalled representation of val determined using the the - associated schema object's (internal) dump fields *function* - or - None if ``val`` is None. + The marshalled representation of ``val`` (or ``None`` if ``val`` is + ``None``). - Note that overriding the associated schema's - :meth:`~lima.schema.Schema.dump` *method* does not affect the result of - this method. This behaviour might change in the future. + Note that the return value is determined using an (internal) dump + fields *function* of the associated schema object. This means that + overriding the associated schema's :meth:`~lima.schema.Schema.dump` + *method* has no effect on the result of this method. ''' return self._pack_func(val) if val is not None else None class Reference(_LinkedObjectField): - '''A Field to reference linked object(s). + '''A Field to reference linked objects. Args: - schema: The schema of the linked object (see :class:`Embed`). - field_name: The schema field to act as reference to the linked object. + schema: A schema for the linked object (see :class:`Embed` for details + on how to specify this schema). One field of this schema will act + as reference to the linked object. + + field_name: The name of the field to act as reference to the linked + object. - attr: see :class:`Embed`. + attr: see :class:`Field`. - get: see :class:`Embed`. + get: see :class:`Field`. - val: see :class:`Embed`. + val: see :class:`Field`. kwargs: see :class:`Embed`. @@ -347,15 +347,20 @@ def _pack_func(self): return self._schema_inst._dump_field_func(self._field_name) def pack(self, val): - '''Return the marshalled representation of val. + '''Return value of reference field of marshalled representation of val. Args: - val: The nested object to marshall. + val: The nested object to get the reference to. Returns: - The marshalled representation of val, determined using the the - associated schema object's (internal) dump field *function* - or - None if ``val`` is None. + The value of the reference-field of the marshalled representation + of val (see ``field_name`` argument of constructor) or ``None`` if + ``val`` is ``None``. + + Note that the return value is determined using an (internal) dump field + *function* of the associated schema object. This means that overriding + the associated schema's :meth:`~lima.schema.Schema.dump` *method* has + no effect on the result of this method. ''' return self._pack_func(val) if val is not None else None From ba2a7a4dfa191a5ea775cafc9444ca62b3e428c8 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Wed, 14 Jan 2015 19:14:57 +0100 Subject: [PATCH 098/107] docs: phrasing --- docs/schemas.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/schemas.rst b/docs/schemas.rst index 44830d8..7831d07 100644 --- a/docs/schemas.rst +++ b/docs/schemas.rst @@ -75,9 +75,9 @@ exclusive): .. warning:: Having to provide ``"only"`` on Schema definition hints at bad design - why - would you add a lot of fields just to remove them quickly afterwards? Have - a look at :ref:`schema_objects` for the preferred way to selectively - remove fields. + would you add a lot of fields just to remove all but one of them + afterwards? Have a look at :ref:`schema_objects` for the preferred way to + selectively remove fields. And finally, we can't just *exclude* fields, we can *include* them too. So here is a user schema with fields provided via ``__lima_args__``: From 671ed2becd301d25d81071d1c5b3cf554404341f Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 15 Jan 2015 12:49:11 +0100 Subject: [PATCH 099/107] docs: rework chapter on linked data --- docs/linked_data.rst | 312 ++++++++++++++++++++++++++----------------- 1 file changed, 193 insertions(+), 119 deletions(-) diff --git a/docs/linked_data.rst b/docs/linked_data.rst index a5e7be7..c0c4fa4 100644 --- a/docs/linked_data.rst +++ b/docs/linked_data.rst @@ -2,118 +2,159 @@ Linked Data =========== -Most ORMs represent linked objects nested under an attribute of the linking -object. As an example, lets model the relationship between a book and its -author: +Lets model a relationship between a book and a book review: .. code-block:: python - :emphasize-lines: 10,14 + :emphasize-lines: 16 - class Person: - def __init__(self, first_name, last_name): - self.first_name = first_name - self.last_name = last_name - - # A book links to its author via a nested Person object class Book: - def __init__(self, title, author=None): - self.title = title + def __init__(self, isbn, author, title): + self.isbn = isbn self.author = author + self.title = title - person = Person('Ernest', 'Hemingway') - book = Book('The Old Man and the Sea') - book.author = person + # A review links to a book via the "book" attribute + class Review: + def __init__(self, rating, text, book=None): + self.rating = rating + self.text = text + self.book = book + book = Book('0-684-80122-1', 'Hemingway', 'The Old Man and the Sea') + review = Review(10, 'Has lots of sharks.') + review.book = book -One-way Relationships -===================== +To serialize this construct, we have to tell lima that a :class:`Review` object +links to a :class:`Book` object via the :attr:`book` attribute (many ORMs +represent related objects in a similar way). -Currently, this relationship is one way only: *From* a book *to* its author. -The author doesn't know anything about books yet (well, in our model at least). -To serialize this construct, we have to tell lima that a :class:`Book` object -has a :class:`Person` object nested inside, designated via the :attr:`author` -attribute. +Embedding linked Objects +======================== -For this we use a field of type :class:`lima.fields.Embed` and tell lima what -data to expect by providing the ``schema`` parameter: +We can use a field of type :class:`lima.fields.Embed` to *embed* the serialized +book into the serialization of the review. For this to work we have to tell the +:class:`~lima.fields.Embed` field what to expect by providing the ``schema`` +parameter: .. code-block:: python - :emphasize-lines: 9,13 + :emphasize-lines: 9,15-17 from lima import fields, Schema - class PersonSchema(Schema): - first_name = fields.String() - last_name = fields.String() - class BookSchema(Schema): + isbn = fields.String() + author = fields.String() title = fields.String() - author = fields.Embed(schema=PersonSchema) - schema = BookSchema() - schema.dump(book) - # {'author': {'first_name': 'Ernest', 'last_name': 'Hemingway'}, - # 'title': The Old Man and the Sea'} + class ReviewSchema(Schema): + book = fields.Embed(schema=BookSchema) + rating = fields.Integer() + text = fields.String() + + review_schema = ReviewSchema() + review_schema.dump(review) + # {'book': {'author': 'Hemingway', + # 'isbn': '0-684-80122-1', + # 'title': 'The Old Man and the Sea'}, + # 'rating': 10, + # 'text': 'Has lots of sharks.'} Along with the mandatory keyword-only argument ``schema``, -:class:`lima.fields.Embed` accepts the optional keyword-only-arguments we -already know (``attr`` or ``get``). All other keyword arguments provided to -:class:`lima.fields.Embed` get passed through to the constructor of the linked -schema. This allows us to do stuff like the following: +:class:`~lima.fields.Embed` accepts the optional keyword-only-arguments we +already know (``attr``, ``get``, ``val``). All other keyword arguments provided +to :class:`~lima.fields.Embed` get passed through to the constructor of the +associated schema. This allows us to do stuff like the following: .. code-block:: python - :emphasize-lines: 3, 7 + :emphasize-lines: 4-6,10-11 - class BookSchema(Schema): - title = fields.String() - author = fields.Embed(schema=PersonSchema, only='last_name') + class ReviewSchemaPartialBook(Schema): + rating = fields.Integer() + text = fields.String() + partial_book = fields.Embed(attr='book', + schema=BookSchema, + exclude='isbn') + + review_schema_partial_book = ReviewSchemaPartialBook() + review_schema_partial_book.dump(review) + # {'partial_book': {'author': 'Hemingway', + # 'title': 'The Old Man and the Sea'}, + # 'rating': 10, + # 'text': 'Has lots of sharks.'} + + +Referencing linked Objects +========================== + +Embedding linked objects is not always what we want. If we just want to +reference linked objects, we can use a field of type +:class:`lima.fields.Reference`. This field type yields the value of a single +field of the linked object's serialization. + +Referencing is similar to embedding save one key difference: In addition to the +schema of the linked object we also provide the name of the field that acts as +our reference to the linked object. We may, for example, reference a book via +its ISBN like this: + +.. code-block:: python + :emphasize-lines: 2,8 + + class ReferencingReviewSchema(Schema): + book = fields.Reference(schema=BookSchema, field_name='isbn') + rating = fields.Integer() + text = fields.String() - schema = BookSchema() - schema.dump(book) - # {'author': {'last_name': 'Hemingway'}, - # 'title': The Old Man and the Sea'} + referencing_review_schema = ReferencingReviewSchema() + referencing_review_schema.dump(review) + # {'book': '0-684-80122-1', + # 'rating': 10, + # 'text': 'Has lots of sharks.'} Two-way Relationships ===================== -If not only a book should link to its author, but an author should also link to -his/her bestselling book, we can adapt our model like this: +Up until now, we've only dealt with one-way relationships (*From* a review *to* +its book). If not only a review should link to its book, but a book should +also link to it's top rated review, we can adapt our model like this: .. code-block:: python - :emphasize-lines: 5,11,15-16 + :emphasize-lines: 7,13,18-19 - # authors link to their bestselling book - class Author(Person): - def __init__(self, first_name, last_name, bestseller=None): - super().__init__(first_name, last_name) - self.bestseller = bestseller - - # books link to their authors + # books now link to their top review class Book: - def __init__(self, title, author=None): - self.title = title + def __init__(self, isbn, author, title, top_review=None): + self.isbn = isbn self.author = author + self.title = title + self.top_review = top_review + + class Review: + def __init__(self, rating, text, book=None): + self.rating = rating + self.text = text + self.book = book + + book = Book('0-684-80122-1', 'Hemingway', 'The Old Man and the Sea') + review = Review(4, "Why doesn't he just kill ALL the sharks?") + + book.top_review = review + review.book = book - author = Author('Ernest', 'Hemingway') - book = Book('The Old Man and the Sea') - book.author = author - author.bestseller = book If we want to construct schemas for models like this, we will have to adress two problems: -1. **Definition order:** If we define our :class:`AuthorSchema` first, its - :attr:`bestseller` attribute will have to reference a :class:`BookSchema` - - but this doesn't exist yet, since we decided to define :class:`AuthorSchema` - first. If we decide to define :class:`BookSchema` first instead, we run into - the same problem with its :attr:`author` attribute. +1. **Definition order:** If we define our :class:`BookSchema` first, its + :attr:`top_review` attribute will have to reference a :class:`ReviewSchema` + - but this doesn't exist yet, since we decided to define :class:`BookSchema` + first. If we decide to define :class:`ReviewSchema` first instead, we run + into the same problem with its :attr:`book` attribute. -2. **Recursion:** An author links to a book that links to an author that links - to a book that links to an author that links to a book that links to an - author that links to a book that links to an author that links to a book - that links to an author ``RuntimeError: maximum recursion depth exceeded`` +2. **Recursion:** A review links to a book that links to a review that links to + a book that links to a review that links to a book that links to a review + that links to a book ``RuntimeError: maximum recursion depth exceeded`` lima makes it easy to deal with those problems: @@ -122,28 +163,37 @@ side that links back. To overcome the problem of definition order, lima supports lazy evaluation of schemas. Just pass the *qualified name* (or the *fully module-qualified name*) -of a schema class to :class:`lima.fields.Embed` instead of the class itself: +of a schema class to :class:`~lima.fields.Embed` instead of the class itself: .. code-block:: python - :emphasize-lines: 2,6,12,16 - - class AuthorSchema(PersonSchema): - bestseller = fields.Embed(schema='BookSchema', exclude='author') + :emphasize-lines: 5,8,17-18,22-24 class BookSchema(Schema): + isbn = fields.String() + author = fields.String() title = fields.String() - author = fields.Embed(schema=AuthorSchema, exclude='bestseller') + top_review = fields.Embed(schema='ReviewSchema', exclude='book') - author_schema = AuthorSchema() - author_schema.dump(author) - # {'first_name': 'Ernest', - # 'last_name': 'Hemingway', - # 'bestseller': {'title': The Old Man and the Sea'} + class ReviewSchema(Schema): + book = fields.Embed(schema=BookSchema, exclude='review') + rating = fields.Integer() + text = fields.String() book_schema = BookSchema() book_schema.dump(book) - # {'author': {'first_name': 'Ernest', 'last_name': 'Hemingway'}, - # 'title': The Old Man and the Sea'} + # {'author': 'Hemingway', + # 'isbn': '0-684-80122-1', + # 'title': The Old Man and the Sea' + # 'top_review': {'rating': 4, + # 'text': "Why doesn't he just kill ALL the sharks?"}} + + review_schema = ReviewSchema() + review_schema.dump(review) + # {'book': {'author': 'Hemingway', + # 'isbn': '0-684-80122-1', + # 'title': 'The Old Man and the Sea'}, + # 'rating': 4, + # 'text': "Why doesn't he just kill ALL the sharks?"} .. _on_class_names: @@ -159,15 +209,15 @@ of a schema class to :class:`lima.fields.Embed` instead of the class itself: of the time, it's the same as the class's :attr:`__name__` attribute (except if you define classes within classes or functions ...). If you define ``class Foo: pass`` at the top level of your module, the class's - qualified name is simply *Foo*. Qualified names were introduced with + qualified name is simply ``Foo``. Qualified names were introduced with Python 3.3 via `PEP 3155 `_ The fully module-qualified name This is the qualified name of the class prefixed with the full name of the module the class is defined in. If you define ``class Qux: pass`` - within a class :class:`Baz` (resulting in the qualified name *Baz.Qux*) - at the top level of your ``foo.bar`` module, the class's fully - module-qualified name is *foo.bar.Baz.Qux*. + within a class :class:`Baz` (resulting in the qualified name + ``Baz.Qux``) at the top level of your ``foo.bar`` module, the class's + fully module-qualified name is ``foo.bar.Baz.Qux``. .. warning:: @@ -192,14 +242,17 @@ By the way, there's nothing stopping us from using the idioms we just learned for models that link to themselves - everything works as you'd expect: .. code-block:: python - :emphasize-lines: 4,7 + :emphasize-lines: 5,10 - class MarriedPerson(Person): + class MarriedPerson: def __init__(self, first_name, last_name, spouse=None): - super().__init__(first_name, last_name) + self.first_name = first_name + self.last_name = last_name self.spouse = spouse class MarriedPersonSchema(PersonSchema): + first_name = fields.String() + last_name = fields.String() spouse = fields.Embed(schema='MarriedPersonSchema', exclude='spouse') @@ -210,49 +263,70 @@ Until now, we've only dealt with one-to-one relations. What about one-to-many and many-to-many relations? Those link to collections of objects. We know the necessary building blocks already: Providing additional keyword -arguments to :class:`lima.fields.Embed` passes them through to the specified -schema's constructor. And providing ``many=True`` to a schema's construtor will -have the schema marshalling collections - so: +arguments to :class:`~lima.fields.Embed` (or :class:`~lima.fields.Reference` +respectively) passes them through to the specified schema's constructor. And +providing ``many=True`` to a schema's construtor will have the schema +marshalling collections - so: .. code-block:: python - :emphasize-lines: 5,15,19 - - # authors link to their books now - class Author(Person): - def __init__(self, first_name, last_name, books=None): - super().__init__(first_name, last_name) - self.books = books - - author = Author('Virginia', 'Woolf') - author.books = [ - Book('Mrs Dalloway', author), - Book('To the Lighthouse', author), - Book('Orlando', author) - ] + :emphasize-lines: 7,16-20,26-28,31,39-43 - class AuthorSchema(PersonSchema): - books = fields.Embed(schema='BookSchema', exclude='author', many=True) + # books now have a list of reviews + class Book: + def __init__(self, isbn, author, title): + self.isbn = isbn + self.author = author + self.title = title + self.reviews = [] + + class Review: + def __init__(self, rating, text, book=None): + self.rating = rating + self.text = text + self.book = book + + book = Book('0-684-80122-1', 'Hemingway', 'The Old Man and the Sea') + book.reviews = [ + Review(10, 'Has lots of sharks.', book), + Review(4, "Why doesn't he just kill ALL the sharks?", book), + Review(8, 'Better than the movie!', book), + ] class BookSchema(Schema): + isbn = fields.String() + author = fields.String() title = fields.String() - author = fields.Embed(schema=AuthorSchema, exclude='books') + reviews = fields.Embed(schema='ReviewSchema', + many=True, + exclude='book') - schema = AuthorSchema() - schema.dump(author) - # {'books': [{'title': 'Mrs Dalloway'}, - # {'title': 'To the Lighthouse'}, - # {'title': 'Orlando'}], - # 'last_name': 'Woolf', - # 'first_name': 'Virginia'} + class ReviewSchema(Schema): + book = fields.Embed(schema=BookSchema, exclude='reviews') + rating = fields.Integer() + text = fields.String() + + book_schema = BookSchema() + book_schema.dump(book) + # {'author': 'Hemingway', + # 'isbn': '0-684-80122-1', + # 'reviews': [ + # {'rating': 4, 'text': "Why doesn't he just kill ALL the sharks?"}, + # {'rating': 10, 'text': 'Has lots of sharks.'}, + # {'rating': 8, 'text': 'Better than the movie!'}, + # ], + # 'title': The Old Man and the Sea' Linked Data Recap ================= -- You now know how to marshal linked objects (via a field of type +- You now know how to marshal embedded linked objects (via a field of type :class:`lima.fields.Embed`) +- You now know how to marshal references to linked objects (via a field of + type :class:`lima.fields.References`) + - You know about lazy evaluation of linked schemas and how to specify those via qualified and fully module-qualified names. From 706a47b1222785929045db0379803805dac32c15 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 15 Jan 2015 13:31:37 +0100 Subject: [PATCH 100/107] docs: fix ambiguities, split up long code blocks --- docs/linked_data.rst | 53 ++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/docs/linked_data.rst b/docs/linked_data.rst index c0c4fa4..332c8f3 100644 --- a/docs/linked_data.rst +++ b/docs/linked_data.rst @@ -117,19 +117,20 @@ Two-way Relationships Up until now, we've only dealt with one-way relationships (*From* a review *to* its book). If not only a review should link to its book, but a book should -also link to it's top rated review, we can adapt our model like this: +also link to it's most popular review, we can adapt our model like this: .. code-block:: python - :emphasize-lines: 7,13,18-19 + :emphasize-lines: 7,19-20 - # books now link to their top review + # books now link to their most popular review class Book: - def __init__(self, isbn, author, title, top_review=None): + def __init__(self, isbn, author, title, pop_review=None): self.isbn = isbn self.author = author self.title = title - self.top_review = top_review + self.pop_review = pop_review + # unchanged: reviews still link to their books class Review: def __init__(self, rating, text, book=None): self.rating = rating @@ -139,7 +140,7 @@ also link to it's top rated review, we can adapt our model like this: book = Book('0-684-80122-1', 'Hemingway', 'The Old Man and the Sea') review = Review(4, "Why doesn't he just kill ALL the sharks?") - book.top_review = review + book.pop_review = review review.book = book @@ -147,7 +148,7 @@ If we want to construct schemas for models like this, we will have to adress two problems: 1. **Definition order:** If we define our :class:`BookSchema` first, its - :attr:`top_review` attribute will have to reference a :class:`ReviewSchema` + :attr:`pop_review` attribute will have to reference a :class:`ReviewSchema` - but this doesn't exist yet, since we decided to define :class:`BookSchema` first. If we decide to define :class:`ReviewSchema` first instead, we run into the same problem with its :attr:`book` attribute. @@ -166,26 +167,31 @@ schemas. Just pass the *qualified name* (or the *fully module-qualified name*) of a schema class to :class:`~lima.fields.Embed` instead of the class itself: .. code-block:: python - :emphasize-lines: 5,8,17-18,22-24 + :emphasize-lines: 5,8 class BookSchema(Schema): isbn = fields.String() author = fields.String() title = fields.String() - top_review = fields.Embed(schema='ReviewSchema', exclude='book') + pop_review = fields.Embed(schema='ReviewSchema', exclude='book') class ReviewSchema(Schema): - book = fields.Embed(schema=BookSchema, exclude='review') + book = fields.Embed(schema=BookSchema, exclude='pop_review') rating = fields.Integer() text = fields.String() +Now embedding works both ways: + +.. code-block:: python + :emphasize-lines: 5-6,11-13 + book_schema = BookSchema() book_schema.dump(book) # {'author': 'Hemingway', # 'isbn': '0-684-80122-1', - # 'title': The Old Man and the Sea' - # 'top_review': {'rating': 4, - # 'text': "Why doesn't he just kill ALL the sharks?"}} + # 'pop_review': {'rating': 4, + # 'text': "Why doesn't he just kill ALL the sharks?"}, + # 'title': The Old Man and the Sea'} review_schema = ReviewSchema() review_schema.dump(review) @@ -250,7 +256,7 @@ for models that link to themselves - everything works as you'd expect: self.last_name = last_name self.spouse = spouse - class MarriedPersonSchema(PersonSchema): + class MarriedPersonSchema(Schema): first_name = fields.String() last_name = fields.String() spouse = fields.Embed(schema='MarriedPersonSchema', exclude='spouse') @@ -266,11 +272,11 @@ We know the necessary building blocks already: Providing additional keyword arguments to :class:`~lima.fields.Embed` (or :class:`~lima.fields.Reference` respectively) passes them through to the specified schema's constructor. And providing ``many=True`` to a schema's construtor will have the schema -marshalling collections - so: +marshalling collections - so if our model looks like this: .. code-block:: python - :emphasize-lines: 7,16-20,26-28,31,39-43 + :emphasize-lines: 7,16-20 # books now have a list of reviews class Book: @@ -293,6 +299,11 @@ marshalling collections - so: Review(8, 'Better than the movie!', book), ] +... we wourld define our schemas like this: + +.. code-block:: python + :emphasize-lines: 5-7,10 + class BookSchema(Schema): isbn = fields.String() author = fields.String() @@ -306,15 +317,19 @@ marshalling collections - so: rating = fields.Integer() text = fields.String() +... which enables us to serialize a book object with many reviews: + +.. code-block:: python + :emphasize-lines: 5-8 + book_schema = BookSchema() book_schema.dump(book) # {'author': 'Hemingway', # 'isbn': '0-684-80122-1', # 'reviews': [ - # {'rating': 4, 'text': "Why doesn't he just kill ALL the sharks?"}, # {'rating': 10, 'text': 'Has lots of sharks.'}, - # {'rating': 8, 'text': 'Better than the movie!'}, - # ], + # {'rating': 4, 'text': "Why doesn't he just kill ALL the sharks?"}, + # {'rating': 8, 'text': 'Better than the movie!'}], # 'title': The Old Man and the Sea' From eea7d4c88dc561d2ce045520d2765fbbb3778984 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 15 Jan 2015 14:48:52 +0100 Subject: [PATCH 101/107] docs: add section on hyperlinks in chapter on linked data --- docs/advanced.rst | 6 ++++-- docs/linked_data.rst | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/advanced.rst b/docs/advanced.rst index 602a8fb..f31b55d 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -65,6 +65,8 @@ like. exotic column types. There is still work to be done. +.. _field_name_mangling: + Field Name Mangling =================== @@ -73,8 +75,8 @@ Fields provided via class attributes have a drawback: class attribute names have to be valid Python identifiers. lima implements a simple name mangling mechanism to allow the specification of -some common non-Python-identifier field names (like JSON-LD's ``"@id"``) as -class attributes. +some common non-Python-identifier field names (like `JSON-LD +`_'s ``"@id"``) as class attributes. The following table shows how name prefixes will be replaced by lima when specifying fields as class attributes (note that every one of those prefixes diff --git a/docs/linked_data.rst b/docs/linked_data.rst index 332c8f3..f2cfb63 100644 --- a/docs/linked_data.rst +++ b/docs/linked_data.rst @@ -112,6 +112,42 @@ its ISBN like this: # 'text': 'Has lots of sharks.'} +Hyperlinks +========== + +One application of :class:`~lima.fields.Reference` is linking to ressources via +hyperlinks in RESTful Web Services. Here is a quick sketch: + +.. code-block:: python + :emphasize-lines: 6,12,18 + + # your framework should provide something like this + def book_url(book): + return 'https://my.service/books/{}'.format(book.isbn) + + class BookSchema(Schema): + url = fields.String(get=book_url) + isbn = fields.String() + author = fields.String() + title = fields.String() + + class ReviewSchema(Schema): + book = fields.Reference(schema=BookSchema, field_name='url') + rating = fields.Integer() + text = fields.String() + + review_schema = ReviewSchema() + review_schema.dump(review) + # {'book': 'https://my.service/books/0-684-80122-1', + # 'rating': 10, + # 'text': 'Has lots of sharks.'} + +If you want to do `JSON-LD `_ and you want to have fields +with names like ``"@id"`` or ``"@context"``, have a look at the section on +:ref:`field_name_mangling` for an easy way to accomplish this. + + + Two-way Relationships ===================== From 73a75c20a8312fd0d210ccce015706d7418c13eb Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 15 Jan 2015 15:00:08 +0100 Subject: [PATCH 102/107] rename arg "field_name" to "field" for Reference field constructor --- docs/linked_data.rst | 4 ++-- lima/fields.py | 11 +++++------ tests/test_fields.py | 16 ++++++++-------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/docs/linked_data.rst b/docs/linked_data.rst index f2cfb63..698dece 100644 --- a/docs/linked_data.rst +++ b/docs/linked_data.rst @@ -101,7 +101,7 @@ its ISBN like this: :emphasize-lines: 2,8 class ReferencingReviewSchema(Schema): - book = fields.Reference(schema=BookSchema, field_name='isbn') + book = fields.Reference(schema=BookSchema, field='isbn') rating = fields.Integer() text = fields.String() @@ -132,7 +132,7 @@ hyperlinks in RESTful Web Services. Here is a quick sketch: title = fields.String() class ReviewSchema(Schema): - book = fields.Reference(schema=BookSchema, field_name='url') + book = fields.Reference(schema=BookSchema, field='url') rating = fields.Integer() text = fields.String() diff --git a/lima/fields.py b/lima/fields.py index 4a61fef..422ef8a 100644 --- a/lima/fields.py +++ b/lima/fields.py @@ -317,8 +317,7 @@ class Reference(_LinkedObjectField): on how to specify this schema). One field of this schema will act as reference to the linked object. - field_name: The name of the field to act as reference to the linked - object. + field: The name of the field to act as reference to the linked object. attr: see :class:`Field`. @@ -333,18 +332,18 @@ class Reference(_LinkedObjectField): def __init__(self, *, schema, - field_name, + field, attr=None, get=None, val=None, **kwargs): super().__init__(schema=schema, attr=attr, get=get, val=val, **kwargs) - self._field_name = field_name + self._field = field @util.reify def _pack_func(self): '''Return the associated schema's dump field *function* (reified).''' - return self._schema_inst._dump_field_func(self._field_name) + return self._schema_inst._dump_field_func(self._field) def pack(self, val): '''Return value of reference field of marshalled representation of val. @@ -354,7 +353,7 @@ def pack(self, val): Returns: The value of the reference-field of the marshalled representation - of val (see ``field_name`` argument of constructor) or ``None`` if + of val (see ``field`` argument of constructor) or ``None`` if ``val`` is ``None``. Note that the return value is determined using an (internal) dump field diff --git a/tests/test_fields.py b/tests/test_fields.py index 5bc2db2..88c56a0 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -226,7 +226,7 @@ class TestReference: [SomeSchema(), SomeSchema, __name__ + '.SomeSchema'] ) def test_pack(self, schema_arg): - field = fields.Reference(schema=schema_arg, field_name='number') + field = fields.Reference(schema=schema_arg, field='number') result = field.pack(SomeClass('one', 1)) expected = 1 assert result == expected @@ -236,15 +236,15 @@ def test_pack(self, schema_arg): [ fields.Reference( schema=SomeSchema(many=True, only='number'), - field_name = 'number' + field = 'number' ), fields.Reference( schema=SomeSchema, many=True, only='number', - field_name = 'number' + field = 'number' ), fields.Reference( schema=__name__ + '.SomeSchema', many=True, only='number', - field_name = 'number' + field = 'number' ) ] ) @@ -258,19 +258,19 @@ def test_pack_with_kwargs(self, field): [ fields.Reference( schema=SomeSchema(many=True, exclude='number'), - field_name = 'number' + field = 'number' ), fields.Reference( schema=SomeSchema, many=True, exclude='number', - field_name = 'number' + field = 'number' ), fields.Reference( schema=__name__ + '.SomeSchema', many=True, exclude='number', - field_name = 'number' + field = 'number' ) ] ) - def test_fail_on_missing_field_name(self, field): + def test_fail_on_missing_field_arg(self, field): # field 'number' is no field of the field's associated schema instance since it # was excluded with pytest.raises(KeyError): From 097a47c63f496b45f0125da5a819ceb89c724be0 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 15 Jan 2015 15:05:10 +0100 Subject: [PATCH 103/107] docs: Use Wikipedia's capitalization of "Web service" --- docs/linked_data.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/linked_data.rst b/docs/linked_data.rst index 698dece..aec0438 100644 --- a/docs/linked_data.rst +++ b/docs/linked_data.rst @@ -116,7 +116,7 @@ Hyperlinks ========== One application of :class:`~lima.fields.Reference` is linking to ressources via -hyperlinks in RESTful Web Services. Here is a quick sketch: +hyperlinks in RESTful Web services. Here is a quick sketch: .. code-block:: python :emphasize-lines: 6,12,18 From 824242db3985f53fd60e2dd458f97eafcf1ae0e1 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 15 Jan 2015 17:31:20 +0100 Subject: [PATCH 104/107] clean up/add tests in test_dump.py --- tests/test_dump.py | 303 +++++++++++++++++++++++++++++---------------- 1 file changed, 199 insertions(+), 104 deletions(-) diff --git a/tests/test_dump.py b/tests/test_dump.py index b412f62..0aedd7e 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -6,7 +6,10 @@ from lima import fields, schema -class Person: +# model ----------------------------------------------------------------------- + +class Knight: + '''A knight.''' def __init__(self, title, name, number, born): self.title = title self.name = name @@ -14,98 +17,129 @@ def __init__(self, title, name, number, born): self.born = born -class PersonSchema(schema.Schema): +class King(Knight): + '''A king is a knight with subjects.''' + def __init__(self, title, name, number, born, subjects=None): + super().__init__(title, name, number, born) + self.subjects = subjects if subjects is not None else [] + + +# schemas --------------------------------------------------------------------- + +class KnightSchema(schema.Schema): title = fields.String() name = fields.String() number = fields.Integer() born = fields.Date() -class DifferentAttrSchema(schema.Schema): +class FieldWithAttrArgSchema(schema.Schema): date_of_birth = fields.Date(attr='born') -class GetterSchema(schema.Schema): - some_getter = lambda obj: '{} {}'.format(obj.title, obj.name) +class FieldWithGetterArgSchema(schema.Schema): + full_name = fields.String( + get=lambda obj: '{} {}'.format(obj.title, obj.name) + ) - full_name = fields.String(get=some_getter) +class FieldWithValArgSchema(schema.Schema): + constant_date = fields.Date(val=date(2014, 10, 20)) -class ConstantValueSchema(schema.Schema): - constant = fields.Date(val=date(2014, 10, 20)) +class KingWithEmbeddedSubjectsObjSchema(KnightSchema): + subjects = fields.Embed(schema=KnightSchema(many=True)) -class KnightSchema(schema.Schema): - name = fields.String() +class KingWithEmbeddedSubjectsClassSchema(KnightSchema): + subjects = fields.Embed(schema=KnightSchema, many=True) -class KingSchemaEmbedStr(KnightSchema): - title = fields.String() + +class KingWithEmbeddedSubjectsStrSchema(KnightSchema): subjects = fields.Embed(schema=__name__ + '.KnightSchema', many=True) -class KingSchemaEmbedClass(KnightSchema): - title = fields.String() - subjects = fields.Embed(schema=KnightSchema, many=True) +class KingWithReferencedSubjectsObjSchema(KnightSchema): + subjects = fields.Reference(schema=KnightSchema(many=True), field='name') -class KingSchemaEmbedObject(KnightSchema): - some_schema_object = KnightSchema(many=True) - title = fields.String() - subjects = fields.Embed(schema=some_schema_object) +class KingWithReferencedSubjectsClassSchema(KnightSchema): + subjects = fields.Reference(schema=KnightSchema, field='name', many=True) -class KingSchemaEmbedSelf(schema.Schema): - name = fields.String() +class KingWithReferencedSubjectsStrSchema(KnightSchema): + subjects = fields.Reference(schema=__name__ + '.KnightSchema', + field='name', many=True) + + +class KingSchemaEmbedSelf(KnightSchema): boss = fields.Embed(schema=__name__ + '.KingSchemaEmbedSelf', exclude='boss') +class KingSchemaReferenceSelf(KnightSchema): + boss = fields.Reference(schema=__name__ + '.KingSchemaEmbedSelf', + field='name') + + +# fixtures -------------------------------------------------------------------- + @pytest.fixture -def king(): - return Person('King', 'Arthur', 1, date(501, 1, 1)) +def bedevere(): + return Knight('Sir', 'Bedevere', 2, date(502, 2, 2)) @pytest.fixture -def knights(): - return [ - Person('Sir', 'Bedevere', 2, date(502, 2, 2)), - Person('Sir', 'Lancelot', 3, date(503, 3, 3)), - Person('Sir', 'Galahad', 4, date(504, 4, 4)), - ] +def lancelot(): + return Knight('Sir', 'Lancelot', 3, date(503, 3, 3)) -def test_dump_single_unordered(king): - person_schema = PersonSchema(many=False, ordered=False) - result = person_schema.dump(king) +@pytest.fixture +def galahad(): + return Knight('Sir', 'Galahad', 4, date(504, 4, 4)) + + +@pytest.fixture +def knights(bedevere, lancelot, galahad): + return [bedevere, lancelot, galahad] + + +@pytest.fixture +def arthur(knights): + return King('King', 'Arthur', 1, date(501, 1, 1), knights) + + +# tests ----------------------------------------------------------------------- + +def test_dump_single_unordered(lancelot): + knight_schema = KnightSchema(many=False, ordered=False) + result = knight_schema.dump(lancelot) expected = { - 'title': 'King', - 'name': 'Arthur', - 'number': 1, - 'born': '0501-01-01' + 'title': 'Sir', + 'name': 'Lancelot', + 'number': 3, + 'born': '0503-03-03' } assert type(result) == dict assert result == expected -def test_dump_single_ordered(king): - person_schema = PersonSchema(many=False, ordered=True) - result = person_schema.dump(king) - expected = OrderedDict( - [ - ('title', 'King'), - ('name', 'Arthur'), - ('number', 1), - ('born', '0501-01-01'), - ] - ) +def test_dump_single_ordered(lancelot): + knight_schema = KnightSchema(many=False, ordered=True) + result = knight_schema.dump(lancelot) + expected = OrderedDict([ + ('title', 'Sir'), + ('name', 'Lancelot'), + ('number', 3), + ('born', '0503-03-03'), + ]) assert type(result) == OrderedDict assert result == expected def test_dump_many_unordered(knights): - person_schema = PersonSchema(many=True, ordered=False) - result = person_schema.dump(knights) + knight_schema = KnightSchema(many=True, ordered=False) + result = knight_schema.dump(knights) expected = [ dict(title='Sir', name='Bedevere', number=2, born='0502-02-02'), dict(title='Sir', name='Lancelot', number=3, born='0503-03-03'), @@ -116,8 +150,8 @@ def test_dump_many_unordered(knights): def test_dump_many_ordered(knights): - person_schema = PersonSchema(many=True, ordered=True) - result = person_schema.dump(knights) + knight_schema = KnightSchema(many=True, ordered=True) + result = knight_schema.dump(knights) expected = [ OrderedDict([('title', 'Sir'), ('name', 'Bedevere'), ('number', 2), ('born', '0502-02-02')]), @@ -130,88 +164,144 @@ def test_dump_many_ordered(knights): assert result == expected -def test_field_exclude_dump(king): - person_schema = PersonSchema(exclude=['born']) - result = person_schema.dump(king) +def test_field_exclude_dump(lancelot): + knight_schema = KnightSchema(exclude=['born', 'number']) + result = knight_schema.dump(lancelot) expected = { - 'title': 'King', - 'name': 'Arthur', - 'number': 1, + 'title': 'Sir', + 'name': 'Lancelot', } assert result == expected -def test_field_only_dump(king): - person_schema = PersonSchema(only=['name']) - result = person_schema.dump(king) +def test_field_only_dump(lancelot): + knight_schema = KnightSchema(only=['name', 'number']) + result = knight_schema.dump(lancelot) expected = { - 'name': 'Arthur', + 'name': 'Lancelot', + 'number': 3, } assert result == expected -def test_attr_field_dump(king): - attr_schema = DifferentAttrSchema() - result = attr_schema.dump(king) +def test_dump_field_with_attr_arg(lancelot): + attr_schema = FieldWithAttrArgSchema() + result = attr_schema.dump(lancelot) expected = { - 'date_of_birth': '0501-01-01' + 'date_of_birth': '0503-03-03' } assert result == expected -def test_getter_field_dump(king): - getter_schema = GetterSchema() - result = getter_schema.dump(king) +def test_dump_field_with_getter_arg(lancelot): + getter_schema = FieldWithGetterArgSchema() + result = getter_schema.dump(lancelot) expected = { - 'full_name': 'King Arthur' + 'full_name': 'Sir Lancelot' } assert result == expected -def test_constant_value_field_dump(king): - constant_value_schema = ConstantValueSchema() - result = constant_value_schema.dump(king) +def test_dump_field_with_val_arg(lancelot): + val_schema = FieldWithValArgSchema() + result = val_schema.dump(lancelot) expected = { - 'constant': '2014-10-20' + 'constant_date': '2014-10-20' } assert result == expected -def test_dump_fail_on_unexpected_collection(knights): - person_schema = PersonSchema(many=False) - with pytest.raises(Exception): - person_schema.dump(knights) +def test_fail_on_unexpected_collection(knights): + knight_schema = KnightSchema(many=False) + with pytest.raises(AttributeError): + knight_schema.dump(knights) -@pytest.mark.parametrize('schema_cls', - [KingSchemaEmbedStr, - KingSchemaEmbedClass, - KingSchemaEmbedObject]) -def test_dump_embed_schema(schema_cls, king, knights): - king_schema = schema_cls() - king.subjects = knights +@pytest.mark.parametrize( + 'king_schema_cls', + [KingWithEmbeddedSubjectsObjSchema, + KingWithEmbeddedSubjectsClassSchema, + KingWithEmbeddedSubjectsStrSchema] +) +def test_dump_embedding_schema(king_schema_cls, arthur): + king_schema = king_schema_cls() expected = { 'title': 'King', 'name': 'Arthur', + 'number': 1, + 'born': '0501-01-01', 'subjects': [ - {'name': 'Bedevere'}, - {'name': 'Lancelot'}, - {'name': 'Galahad'}, + dict(title='Sir', name='Bedevere', number=2, born='0502-02-02'), + dict(title='Sir', name='Lancelot', number=3, born='0503-03-03'), + dict(title='Sir', name='Galahad', number=4, born='0504-04-04'), ] } - assert king_schema.dump(king) == expected + assert king_schema.dump(arthur) == expected -def test_dump_embed_schema_instance_double_kwargs_error(): +@pytest.mark.parametrize( + 'king_schema_cls', + [KingWithReferencedSubjectsObjSchema, + KingWithReferencedSubjectsClassSchema, + KingWithReferencedSubjectsStrSchema] +) +def test_dump_referencing_schema(king_schema_cls, arthur): + king_schema = king_schema_cls() + expected = { + 'title': 'King', + 'name': 'Arthur', + 'number': 1, + 'born': '0501-01-01', + 'subjects': ['Bedevere', 'Lancelot', 'Galahad'] + } + assert king_schema.dump(arthur) == expected + + +def test_embed_self_schema(arthur): + # a king is his own boss + arthur.boss = arthur + king_schema = KingSchemaEmbedSelf() + result = king_schema.dump(arthur) + expected = { + 'title': 'King', + 'name': 'Arthur', + 'number': 1, + 'born': '0501-01-01', + 'boss': { + 'title': 'King', + 'name': 'Arthur', + 'number': 1, + 'born': '0501-01-01', + } + } + assert result == expected + + +def test_reference_self_schema(arthur): + # a king is his own boss + arthur.boss = arthur + king_schema = KingSchemaReferenceSelf() + result = king_schema.dump(arthur) + expected = { + 'title': 'King', + 'name': 'Arthur', + 'number': 1, + 'born': '0501-01-01', + 'boss': 'Arthur', + } + assert result == expected + + +def test_fail_on_unnecessary_keywords(): class EmbedSchema(schema.Schema): some_field = fields.String() - embed_schema = KnightSchema(many=True) + embed_schema = EmbedSchema(many=True) class EmbeddingSchema(schema.Schema): another_field = fields.String() - # here we provide a schema instance. the kwarg "many" is unnecessary + # here we provide a schema _instance_. the kwarg "many" is unnecessary incorrect_embed_field = fields.Embed(schema=embed_schema, many=True) # the incorrect field is constructed lazily. we'll have to access it @@ -219,18 +309,24 @@ class EmbeddingSchema(schema.Schema): EmbeddingSchema.__fields__['incorrect_embed_field']._schema_inst -def test_dump_embed_schema_self(king): - king_schema = KingSchemaEmbedSelf() - king.boss = king - expected = { - 'name': 'Arthur', - 'boss': {'name': 'Arthur'}, - } - assert king_schema.dump(king) == expected +def test_fail_on_unnecessary_arg(): + class EmbedSchema(schema.Schema): + some_field = fields.String() -def test_dump_exotic_field_names(): + embed_schema = EmbedSchema(many=True) + class EmbeddingSchema(schema.Schema): + another_field = fields.String() + # here we provide a schema _instance_. the kwarg "many" is unnecessary + incorrect_embed_field = fields.Embed(schema=embed_schema, many=True) + + # the incorrect field is constructed lazily. we'll have to access it + with pytest.raises(ValueError): + EmbeddingSchema.__fields__['incorrect_embed_field']._schema_inst + + +def test_dump_exotic_field_names(): exotic_names = [ '', # empty string '"', # single quote @@ -242,16 +338,15 @@ def test_dump_exotic_field_names(): class ExoticFieldNamesSchema(schema.Schema): __lima_args__ = { - 'include': { - name: fields.String(attr='foo') for name in exotic_names - } + 'include': {name: fields.String(attr='foo') + for name in exotic_names} } class Foo: def __init__(self): self.foo = 'foobar' - obj = Foo() + obj = Foo() exotic_field_names_schema = ExoticFieldNamesSchema() result = exotic_field_names_schema.dump(obj) expected = {name: 'foobar' for name in exotic_names} From 63351806ae1d776f91313cc44b59fa62d2785ce9 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 15 Jan 2015 17:49:18 +0100 Subject: [PATCH 105/107] Bump version number to 0.4 --- lima/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lima/__init__.py b/lima/__init__.py index 56d6f40..c3d00c2 100644 --- a/lima/__init__.py +++ b/lima/__init__.py @@ -6,4 +6,4 @@ from lima import schema from lima.schema import Schema -__version__ = '0.4.dev0' +__version__ = '0.4' From 204c6e6682bb985d41849e6b7ff3d8c3950c17ac Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 15 Jan 2015 17:50:38 +0100 Subject: [PATCH 106/107] Update changelog. --- CHANGELOG.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 01875bd..e313430 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,13 +2,9 @@ Changelog ========= -0.4 (unreleased) +0.4 (2015-01-15) ================ -.. note:: - - While unreleased, the changelog of lima 0.4 is itself subject to change. - - **Breaking Change:** The ``Schema.dump`` method no longer supports the ``many`` argument. This makes ``many`` consistent with ``ordered`` and simplifies internals. From 627edc33a34a1b4456cd42ee5a26e48d005a0456 Mon Sep 17 00:00:00 2001 From: Bernhard Weitzhofer Date: Thu, 15 Jan 2015 17:52:18 +0100 Subject: [PATCH 107/107] Point README badges to master (stable) branch. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 2f13a3e..24f2be5 100644 --- a/README.rst +++ b/README.rst @@ -77,12 +77,12 @@ See the `documentation`_ for more comprehensive install instructions. :target: https://lima.readthedocs.org :alt: Documentation Status -.. |build| image:: https://img.shields.io/travis/b6d/lima/develop.svg +.. |build| image:: https://img.shields.io/travis/b6d/lima/master.svg ?style=flat-square :target: https://travis-ci.org/b6d/lima :alt: Build Status -.. |coverage| image:: https://img.shields.io/coveralls/b6d/lima/develop.svg +.. |coverage| image:: https://img.shields.io/coveralls/b6d/lima/master.svg ?style=flat-square :target: https://coveralls.io/r/b6d/lima :alt: Test Coverage