Andy Mikhailenko avatar Andy Mikhailenko committed e9a49f0

Fixed issue #1: manipulation would not insert proper values for callable defaults.

Comments (0)

Files changed (4)

monk/manipulation.py

 Data manipulation
 =================
 """
+import types
+
+
 def merged(spec, data, value_processor=lambda x:x):
     """ Returns a dictionary based on `spec` + `data`.
 
             else:
                 value = spec[key]
 
-            # special handling of dict and list instances
+            # special handling of dict instances, list instances and callables
             if (spec[key] == dict or isinstance(spec[key], dict)) and isinstance(value, dict):
                 # nested dictionary
                 value = merged(spec.get(key, {}), value)
-            if (spec[key] == list or isinstance(spec[key], list)) and isinstance(value, list):
+            elif (spec[key] == list or isinstance(spec[key], list)) and isinstance(value, list):
                 # nested list
                 item_spec = spec[key][0] if spec[key] else None
                 if isinstance(item_spec, type):
                 else:
                     # probably default list item like [1]
                     pass
+            elif isinstance(spec[key], (types.FunctionType, types.BuiltinFunctionType)):
+                # default value is obtained from a function with no arguments;
+                # (It is expected that the callable does not have side effects.)
+                if hasattr(value, '__call__'):
+                    # FIXME this is unreliable: the value may be already
+                    # a result of calling the function from spec which, in
+                    # turn, can be callable. Instead of checking for __call__
+                    # we should check if the value was obtained from data or
+                    # from the spec. This is problematic at the moment because
+                    # nested structures are simply assigned to `value` if
+                    # `value` is None or is not in the data, and *then* the
+                    # structure is recursively merged (at which point the
+                    # information on the source of given chunk of data is lost)
+                    value = value()
             else:
                 # some value from spec that can be checked for type
                 pass

unittests/test_manipulation.py

         Data is preserved though won't validate.
         """
         assert {'a': [123]} == merged({'a': unicode}, {'a': [123]})
+
+    def test_callable(self):
+        """ Callable defaults.
+        """
+        spec = {'text': lambda: u'hello'}
+        data = {}
+        expected = {'text': u'hello'}
+        assert merged(spec, data) == expected
+
+    def test_callable_nested(self):
+        """ Nested callable defaults.
+        """
+        spec = {'content': {'text': lambda: u'hello'}}
+        data = {}
+        expected = {'content': {'text': u'hello'}}
+        assert merged(spec, data) == expected

unittests/test_modeling.py

             event.validate()
 
 
+    def test_callable_defaults_custom_func_nested(self):
+        # Issue #1  https://bitbucket.org/neithere/monk/issue/1/callable-defaults-in-nested-structures
+        class Event(modeling.Document):
+            structure = {
+                'content': {
+                    'text': lambda: u'hello'
+                }
+            }
+
+        event = Event(content=dict(text=u'albatross'))
+        event.validate()
+        assert isinstance(event.content.text, unicode)
+        assert event.content.text == u'albatross'
+
+        event = Event()
+        event.validate()
+        assert isinstance(event.content.text, unicode)
+        assert event.content.text == u'hello'
+
+        with pytest.raises(TypeError):
+            event = Event(content=dict(text=123))
+            event.validate()
+
+
 class TestMongo:
 
     DATABASE = 'test_monk'

unittests/test_validation.py

     def test_dbref(self):
         validate_structure({'a': pymongo.dbref.DBRef}, {'a': None})
         validate_structure({'a': pymongo.dbref.DBRef},
-                                {'a': pymongo.dbref.DBRef('a', 'b')})
+                           {'a': pymongo.dbref.DBRef('a', 'b')})
 
     def test_valid_document(self):
         "a complex document"
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.