Repository: ipython/ipython

Issue:
Inconsistent behavior between FileContentsManager and Contents Manager API docs
Discrepancies with [IPEP-27](https://github.com/ipython/ipython/wiki/IPEP-27:-Contents-Service#contents-api) noted while working on a custom subclass of `ContentsManager`:
- Where IPEP-27 lists `'modified'` as an expected dictionary key, `FileContentsManager` uses `'last_modified'` instead.
- IPEP-27 specifies that all three of `'contents'`, `'format'`, and `'mimetype'` will be null if `contents` is null.  In many (possibly all?) cases however, `'mimetype'` is non-null when `'contents` is null.

Cf. https://github.com/ipython/ipython/pull/7140.



Discussion:

Resolving patch:
diff --git a/IPython/html/services/contents/handlers.py b/IPython/html/services/contents/handlers.py
--- a/IPython/html/services/contents/handlers.py
+++ b/IPython/html/services/contents/handlers.py
@@ -25,6 +25,56 @@ def sort_key(model):
     }.get(model['type'], '9')
     return u'%s%s' % (type_key, iname)
 
+
+def validate_model(model, expect_content):
+    """
+    Validate a model returned by a ContentsManager method.
+
+    If expect_content is True, then we expect non-null entries for 'content'
+    and 'format'.
+    """
+    required_keys = {
+        "name",
+        "path",
+        "type",
+        "writable",
+        "created",
+        "last_modified",
+        "mimetype",
+        "content",
+        "format",
+    }
+    missing = required_keys - set(model.keys())
+    if missing:
+        raise web.HTTPError(
+            500,
+            u"Missing Model Keys: {missing}".format(missing=missing),
+        )
+
+    maybe_none_keys = ['content', 'format']
+    if model['type'] == 'file':
+        # mimetype should be populated only for file models
+        maybe_none_keys.append('mimetype')
+    if expect_content:
+        errors = [key for key in maybe_none_keys if model[key] is None]
+        if errors:
+            raise web.HTTPError(
+                500,
+                u"Keys unexpectedly None: {keys}".format(keys=errors),
+            )
+    else:
+        errors = {
+            key: model[key]
+            for key in maybe_none_keys
+            if model[key] is not None
+        }
+        if errors:
+            raise web.HTTPError(
+                500,
+                u"Keys unexpectedly not None: {keys}".format(keys=errors),
+            )
+
+
 class ContentsHandler(IPythonHandler):
 
     SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
@@ -72,6 +122,7 @@ def get(self, path=''):
             # group listing by type, then by name (case-insensitive)
             # FIXME: sorting should be done in the frontends
             model['content'].sort(key=sort_key)
+        validate_model(model, expect_content=True)
         self._finish_model(model, location=False)
 
     @web.authenticated
@@ -83,6 +134,7 @@ def patch(self, path=''):
         if model is None:
             raise web.HTTPError(400, u'JSON body missing')
         model = cm.update(model, path)
+        validate_model(model, expect_content=False)
         self._finish_model(model)
 
     def _copy(self, copy_from, copy_to=None):
@@ -93,6 +145,7 @@ def _copy(self, copy_from, copy_to=None):
         ))
         model = self.contents_manager.copy(copy_from, copy_to)
         self.set_status(201)
+        validate_model(model, expect_content=False)
         self._finish_model(model)
 
     def _upload(self, model, path):
@@ -100,6 +153,7 @@ def _upload(self, model, path):
         self.log.info(u"Uploading file to %s", path)
         model = self.contents_manager.new(model, path)
         self.set_status(201)
+        validate_model(model, expect_content=False)
         self._finish_model(model)
         
     def _new_untitled(self, path, type='', ext=''):
@@ -107,12 +161,14 @@ def _new_untitled(self, path, type='', ext=''):
         self.log.info(u"Creating new %s in %s", type or 'file', path)
         model = self.contents_manager.new_untitled(path=path, type=type, ext=ext)
         self.set_status(201)
+        validate_model(model, expect_content=False)
         self._finish_model(model)
 
     def _save(self, model, path):
         """Save an existing file."""
         self.log.info(u"Saving file at %s", path)
         model = self.contents_manager.save(model, path)
+        validate_model(model, expect_content=False)
         self._finish_model(model)
 
     @web.authenticated


Test patch: