add pre/post-save hooks

- `ContentsManager.pre_save_hook` runs on the path and model with content
- `FileContentsManager.post_save_hook` runs on the filesystem path and model without content

- use pre_save_hook for things like stripping output
- use post_save_hook for things like nbconvert --to python
pull/37/head
Min RK 11 years ago
parent a84f35373f
commit 58c0a97ec3

@ -16,8 +16,9 @@ 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.traitlets import Any, Unicode, Bool, TraitError
from IPython.utils.py3compat import getcwd, str_to_unicode
from IPython.utils import tz
from IPython.html.utils import is_hidden, to_os_path, to_api_path
@ -71,6 +72,40 @@ class FileContentsManager(ContentsManager):
Use `ipython nbconvert --to python [notebook]` instead.
""")
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
This can be used to process the file on disk,
such as converting the notebook to other formats, such as Python 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):
@ -404,6 +439,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)
@ -433,6 +470,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):

@ -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):

Loading…
Cancel
Save