diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py
index b1ad88c6a..9689aed9e 100644
--- a/IPython/html/services/contents/filemanager.py
+++ b/IPython/html/services/contents/filemanager.py
@@ -16,12 +16,37 @@ from tornado import web
from .manager import ContentsManager
from IPython import nbformat
from IPython.utils.io import atomic_writing
+from IPython.utils.importstring import import_item
from IPython.utils.path import ensure_dir_exists
-from IPython.utils.traitlets import Unicode, Bool, TraitError
-from IPython.utils.py3compat import getcwd, str_to_unicode
+from IPython.utils.traitlets import Any, Unicode, Bool, TraitError
+from IPython.utils.py3compat import getcwd, str_to_unicode, string_types
from IPython.utils import tz
from IPython.html.utils import is_hidden, to_os_path, to_api_path
+_script_exporter = None
+
+def _post_save_script(model, os_path, contents_manager, **kwargs):
+ """convert notebooks to Python script after save with nbconvert
+
+ replaces `ipython notebook --script`
+ """
+ from IPython.nbconvert.exporters.script import ScriptExporter
+
+ if model['type'] != 'notebook':
+ return
+
+ global _script_exporter
+ if _script_exporter is None:
+ _script_exporter = ScriptExporter(parent=contents_manager)
+ log = contents_manager.log
+
+ base, ext = os.path.splitext(os_path)
+ py_fname = base + '.py'
+ script, resources = _script_exporter.from_filename(os_path)
+ script_fname = base + resources.get('output_extension', '.txt')
+ log.info("Saving script /%s", to_api_path(script_fname, contents_manager.root_dir))
+ with io.open(script_fname, 'w', encoding='utf-8') as f:
+ f.write(script)
class FileContentsManager(ContentsManager):
@@ -64,13 +89,56 @@ class FileContentsManager(ContentsManager):
with atomic_writing(os_path, *args, **kwargs) as f:
yield f
- save_script = Bool(False, config=True, help='DEPRECATED, IGNORED')
+ save_script = Bool(False, config=True, help='DEPRECATED, use post_save_hook')
def _save_script_changed(self):
self.log.warn("""
- Automatically saving notebooks as scripts has been removed.
- Use `ipython nbconvert --to python [notebook]` instead.
+ `--script` is deprecated. You can trigger nbconvert via pre- or post-save hooks:
+
+ ContentsManager.pre_save_hook
+ FileContentsManager.post_save_hook
+
+ A post-save hook has been registered that calls:
+
+ ipython nbconvert --to script [notebook]
+
+ which behaves similarly to `--script`.
""")
+ self.post_save_hook = _post_save_script
+
+ post_save_hook = Any(None, config=True,
+ help="""Python callable or importstring thereof
+
+ to be called on the path of a file just saved.
+
+ This can be used to process the file on disk,
+ such as converting the notebook to a script or HTML via nbconvert.
+
+ It will be called as (all arguments passed by keyword):
+
+ hook(os_path=os_path, model=model, contents_manager=instance)
+
+ path: the filesystem path to the file just written
+ model: the model representing the file
+ contents_manager: this ContentsManager instance
+ """
+ )
+ def _post_save_hook_changed(self, name, old, new):
+ if new and isinstance(new, string_types):
+ self.post_save_hook = import_item(self.post_save_hook)
+ elif new:
+ if not callable(new):
+ raise TraitError("post_save_hook must be callable")
+
+ def run_post_save_hook(self, model, os_path):
+ """Run the post-save hook if defined, and log errors"""
+ if self.post_save_hook:
+ try:
+ self.log.debug("Running post-save hook on %s", os_path)
+ self.post_save_hook(os_path=os_path, model=model, contents_manager=self)
+ except Exception:
+ self.log.error("Post-save hook failed on %s", os_path, exc_info=True)
+
def _root_dir_changed(self, name, old, new):
"""Do a bit of validation of the root_dir."""
if not os.path.isabs(new):
@@ -407,6 +475,8 @@ class FileContentsManager(ContentsManager):
if 'content' not in model and model['type'] != 'directory':
raise web.HTTPError(400, u'No file content provided')
+ self.run_pre_save_hook(model=model, path=path)
+
# One checkpoint should always exist
if self.file_exists(path) and not self.list_checkpoints(path):
self.create_checkpoint(path)
@@ -436,6 +506,9 @@ class FileContentsManager(ContentsManager):
model = self.get(path, content=False)
if validation_message:
model['message'] = validation_message
+
+ self.run_post_save_hook(model=model, os_path=os_path)
+
return model
def update(self, model, path):
diff --git a/IPython/html/services/contents/manager.py b/IPython/html/services/contents/manager.py
index 5e091f29e..c61894442 100644
--- a/IPython/html/services/contents/manager.py
+++ b/IPython/html/services/contents/manager.py
@@ -14,7 +14,9 @@ from tornado.web import HTTPError
from IPython.config.configurable import LoggingConfigurable
from IPython.nbformat import sign, validate, ValidationError
from IPython.nbformat.v4 import new_notebook
-from IPython.utils.traitlets import Instance, Unicode, List
+from IPython.utils.importstring import import_item
+from IPython.utils.traitlets import Instance, Unicode, List, Any, TraitError
+from IPython.utils.py3compat import string_types
copy_pat = re.compile(r'\-Copy\d*\.')
@@ -60,6 +62,41 @@ class ContentsManager(LoggingConfigurable):
help="The base name used when creating untitled directories."
)
+ pre_save_hook = Any(None, config=True,
+ help="""Python callable or importstring thereof
+
+ To be called on a contents model prior to save.
+
+ This can be used to process the structure,
+ such as removing notebook outputs or other side effects that
+ should not be saved.
+
+ It will be called as (all arguments passed by keyword):
+
+ hook(path=path, model=model, contents_manager=self)
+
+ model: the model to be saved. Includes file contents.
+ modifying this dict will affect the file that is stored.
+ path: the API path of the save destination
+ contents_manager: this ContentsManager instance
+ """
+ )
+ def _pre_save_hook_changed(self, name, old, new):
+ if new and isinstance(new, string_types):
+ self.pre_save_hook = import_item(self.pre_save_hook)
+ elif new:
+ if not callable(new):
+ raise TraitError("pre_save_hook must be callable")
+
+ def run_pre_save_hook(self, model, path, **kwargs):
+ """Run the pre-save hook if defined, and log errors"""
+ if self.pre_save_hook:
+ try:
+ self.log.debug("Running pre-save hook on %s", path)
+ self.pre_save_hook(model=model, path=path, contents_manager=self, **kwargs)
+ except Exception:
+ self.log.error("Pre-save hook failed on %s", path, exc_info=True)
+
# ContentsManager API part 1: methods that must be
# implemented in subclasses.
@@ -142,7 +179,11 @@ class ContentsManager(LoggingConfigurable):
raise NotImplementedError('must be implemented in a subclass')
def save(self, model, path):
- """Save the file or directory and return the model with no content."""
+ """Save the file or directory and return the model with no content.
+
+ Save implementations should call self.run_pre_save_hook(model=model, path=path)
+ prior to writing any data.
+ """
raise NotImplementedError('must be implemented in a subclass')
def update(self, model, path):