Uploaded Test files

This commit is contained in:
Batuhan Berk Başoğlu 2020-11-12 11:05:57 -05:00
parent f584ad9d97
commit 2e81cb7d99
16627 changed files with 2065359 additions and 102444 deletions

View file

@ -0,0 +1,7 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from .bundlerextensions import main
if __name__ == '__main__':
main()

View file

@ -0,0 +1,307 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import sys
import os
from ..extensions import BaseExtensionApp, _get_config_dir, GREEN_ENABLED, RED_DISABLED
from .._version import __version__
from notebook.config_manager import BaseJSONConfigManager
from jupyter_core.paths import jupyter_config_path
from traitlets.utils.importstring import import_item
from traitlets import Bool
BUNDLER_SECTION = "notebook"
BUNDLER_SUBSECTION = "bundlerextensions"
def _get_bundler_metadata(module):
"""Gets the list of bundlers associated with a Python package.
Returns a tuple of (the module, [{
'name': 'unique name of the bundler',
'label': 'file menu item label for the bundler',
'module_name': 'dotted package/module name containing the bundler',
'group': 'download or deploy parent menu item'
}])
Parameters
----------
module : str
Importable Python module exposing the
magic-named `_jupyter_bundlerextension_paths` function
"""
m = import_item(module)
if not hasattr(m, '_jupyter_bundlerextension_paths'):
raise KeyError('The Python module {} does not contain a valid bundlerextension'.format(module))
bundlers = m._jupyter_bundlerextension_paths()
return m, bundlers
def _set_bundler_state(name, label, module_name, group, state,
user=True, sys_prefix=False, logger=None):
"""Set whether a bundler is enabled or disabled.
Returns True if the final state is the one requested.
Parameters
----------
name : string
Unique name of the bundler
label : string
Human-readable label for the bundler menu item in the notebook UI
module_name : string
Dotted module/package name containing the bundler
group : string
'download' or 'deploy' indicating the parent menu containing the label
state : bool
The state in which to leave the extension
user : bool [default: True]
Whether to update the user's .jupyter/nbconfig directory
sys_prefix : bool [default: False]
Whether to update the sys.prefix, i.e. environment. Will override
`user`.
logger : Jupyter logger [optional]
Logger instance to use
"""
user = False if sys_prefix else user
config_dir = os.path.join(
_get_config_dir(user=user, sys_prefix=sys_prefix), 'nbconfig')
cm = BaseJSONConfigManager(config_dir=config_dir)
if logger:
logger.info("{} {} bundler {}...".format(
"Enabling" if state else "Disabling",
name,
module_name
))
if state:
cm.update(BUNDLER_SECTION, {
BUNDLER_SUBSECTION: {
name: {
"label": label,
"module_name": module_name,
"group" : group
}
}
})
else:
cm.update(BUNDLER_SECTION, {
BUNDLER_SUBSECTION: {
name: None
}
})
return (cm.get(BUNDLER_SECTION)
.get(BUNDLER_SUBSECTION, {})
.get(name) is not None) == state
def _set_bundler_state_python(state, module, user, sys_prefix, logger=None):
"""Enables or disables bundlers defined in a Python package.
Returns a list of whether the state was achieved for each bundler.
Parameters
----------
state : Bool
Whether the extensions should be enabled
module : str
Importable Python module exposing the
magic-named `_jupyter_bundlerextension_paths` function
user : bool
Whether to enable in the user's nbconfig directory.
sys_prefix : bool
Enable/disable in the sys.prefix, i.e. environment
logger : Jupyter logger [optional]
Logger instance to use
"""
m, bundlers = _get_bundler_metadata(module)
return [_set_bundler_state(name=bundler["name"],
label=bundler["label"],
module_name=bundler["module_name"],
group=bundler["group"],
state=state,
user=user, sys_prefix=sys_prefix,
logger=logger)
for bundler in bundlers]
def enable_bundler_python(module, user=True, sys_prefix=False, logger=None):
"""Enables bundlers defined in a Python package.
Returns whether each bundle defined in the packaged was enabled or not.
Parameters
----------
module : str
Importable Python module exposing the
magic-named `_jupyter_bundlerextension_paths` function
user : bool [default: True]
Whether to enable in the user's nbconfig directory.
sys_prefix : bool [default: False]
Whether to enable in the sys.prefix, i.e. environment. Will override
`user`
logger : Jupyter logger [optional]
Logger instance to use
"""
return _set_bundler_state_python(True, module, user, sys_prefix,
logger=logger)
def disable_bundler_python(module, user=True, sys_prefix=False, logger=None):
"""Disables bundlers defined in a Python package.
Returns whether each bundle defined in the packaged was enabled or not.
Parameters
----------
module : str
Importable Python module exposing the
magic-named `_jupyter_bundlerextension_paths` function
user : bool [default: True]
Whether to enable in the user's nbconfig directory.
sys_prefix : bool [default: False]
Whether to enable in the sys.prefix, i.e. environment. Will override
`user`
logger : Jupyter logger [optional]
Logger instance to use
"""
return _set_bundler_state_python(False, module, user, sys_prefix,
logger=logger)
class ToggleBundlerExtensionApp(BaseExtensionApp):
"""A base class for apps that enable/disable bundlerextensions"""
name = "jupyter bundlerextension enable/disable"
version = __version__
description = "Enable/disable a bundlerextension in configuration."
user = Bool(True, config=True, help="Apply the configuration only for the current user (default)")
_toggle_value = None
def _config_file_name_default(self):
"""The default config file name."""
return 'jupyter_notebook_config'
def toggle_bundler_python(self, module):
"""Toggle some extensions in an importable Python module.
Returns a list of booleans indicating whether the state was changed as
requested.
Parameters
----------
module : str
Importable Python module exposing the
magic-named `_jupyter_bundlerextension_paths` function
"""
toggle = (enable_bundler_python if self._toggle_value
else disable_bundler_python)
return toggle(module,
user=self.user,
sys_prefix=self.sys_prefix,
logger=self.log)
def start(self):
if not self.extra_args:
sys.exit('Please specify an bundlerextension/package to enable or disable')
elif len(self.extra_args) > 1:
sys.exit('Please specify one bundlerextension/package at a time')
if self.python:
self.toggle_bundler_python(self.extra_args[0])
else:
raise NotImplementedError('Cannot install bundlers from non-Python packages')
class EnableBundlerExtensionApp(ToggleBundlerExtensionApp):
"""An App that enables bundlerextensions"""
name = "jupyter bundlerextension enable"
description = """
Enable a bundlerextension in frontend configuration.
Usage
jupyter bundlerextension enable [--system|--sys-prefix]
"""
_toggle_value = True
class DisableBundlerExtensionApp(ToggleBundlerExtensionApp):
"""An App that disables bundlerextensions"""
name = "jupyter bundlerextension disable"
description = """
Disable a bundlerextension in frontend configuration.
Usage
jupyter bundlerextension disable [--system|--sys-prefix]
"""
_toggle_value = None
class ListBundlerExtensionApp(BaseExtensionApp):
"""An App that lists and validates nbextensions"""
name = "jupyter nbextension list"
version = __version__
description = "List all nbextensions known by the configuration system"
def list_nbextensions(self):
"""List all the nbextensions"""
config_dirs = [os.path.join(p, 'nbconfig') for p in jupyter_config_path()]
print("Known bundlerextensions:")
for config_dir in config_dirs:
head = u' config dir: {}'.format(config_dir)
head_shown = False
cm = BaseJSONConfigManager(parent=self, config_dir=config_dir)
data = cm.get('notebook')
if 'bundlerextensions' in data:
if not head_shown:
# only show heading if there is an nbextension here
print(head)
head_shown = True
for bundler_id, info in data['bundlerextensions'].items():
label = info.get('label')
module = info.get('module_name')
if label is None or module is None:
msg = u' {} {}'.format(bundler_id, RED_DISABLED)
else:
msg = u' "{}" from {} {}'.format(
label, module, GREEN_ENABLED
)
print(msg)
def start(self):
"""Perform the App's functions as configured"""
self.list_nbextensions()
class BundlerExtensionApp(BaseExtensionApp):
"""Base jupyter bundlerextension command entry point"""
name = "jupyter bundlerextension"
version = __version__
description = "Work with Jupyter bundler extensions"
examples = """
jupyter bundlerextension list # list all configured bundlers
jupyter bundlerextension enable --py <packagename> # enable all bundlers in a Python package
jupyter bundlerextension disable --py <packagename> # disable all bundlers in a Python package
"""
subcommands = dict(
enable=(EnableBundlerExtensionApp, "Enable a bundler extension"),
disable=(DisableBundlerExtensionApp, "Disable a bundler extension"),
list=(ListBundlerExtensionApp, "List bundler extensions")
)
def start(self):
"""Perform the App's functions as configured"""
super(BundlerExtensionApp, self).start()
# The above should have called a subcommand and raised NoStart; if we
# get here, it didn't, so we should self.log.info a message.
subcmds = ", ".join(sorted(self.subcommands))
sys.exit("Please supply at least one subcommand: %s" % subcmds)
main = BundlerExtensionApp.launch_instance
if __name__ == '__main__':
main()

View file

@ -0,0 +1,88 @@
"""Tornado handler for bundling notebooks."""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from ipython_genutils.importstring import import_item
from tornado import web, gen
from notebook.utils import maybe_future, url2path
from notebook.base.handlers import IPythonHandler
from notebook.services.config import ConfigManager
from . import tools
class BundlerHandler(IPythonHandler):
def initialize(self):
"""Make tools module available on the handler instance for compatibility
with existing bundler API and ease of reference."""
self.tools = tools
def get_bundler(self, bundler_id):
"""
Get bundler metadata from config given a bundler ID.
Parameters
----------
bundler_id: str
Unique bundler ID within the notebook/bundlerextensions config section
Returns
-------
dict
Bundler metadata with label, group, and module_name attributes
Raises
------
KeyError
If the bundler ID is unknown
"""
cm = ConfigManager()
return cm.get('notebook').get('bundlerextensions', {})[bundler_id]
@web.authenticated
@gen.coroutine
def get(self, path):
"""Bundle the given notebook.
Parameters
----------
path: str
Path to the notebook (path parameter)
bundler: str
Bundler ID to use (query parameter)
"""
bundler_id = self.get_query_argument('bundler')
model = self.contents_manager.get(path=url2path(path))
try:
bundler = self.get_bundler(bundler_id)
except KeyError as e:
raise web.HTTPError(400, 'Bundler %s not enabled' %
bundler_id) from e
module_name = bundler['module_name']
try:
# no-op in python3, decode error in python2
module_name = str(module_name)
except UnicodeEncodeError:
# Encode unicode as utf-8 in python2 else import_item fails
module_name = module_name.encode('utf-8')
try:
bundler_mod = import_item(module_name)
except ImportError as e:
raise web.HTTPError(500, 'Could not import bundler %s ' %
bundler_id) from e
# Let the bundler respond in any way it sees fit and assume it will
# finish the request
yield maybe_future(bundler_mod.bundle(self, model))
_bundler_id_regex = r'(?P<bundler_id>[A-Za-z0-9_]+)'
default_handlers = [
(r"/bundle/(.*)", BundlerHandler)
]

View file

@ -0,0 +1,47 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import os
import io
import tarfile
import nbformat
def _jupyter_bundlerextension_paths():
"""Metadata for notebook bundlerextension"""
return [{
# unique bundler name
"name": "tarball_bundler",
# module containing bundle function
"module_name": "notebook.bundler.tarball_bundler",
# human-readable menu item label
"label" : "Notebook Tarball (tar.gz)",
# group under 'deploy' or 'download' menu
"group" : "download",
}]
def bundle(handler, model):
"""Create a compressed tarball containing the notebook document.
Parameters
----------
handler : tornado.web.RequestHandler
Handler that serviced the bundle request
model : dict
Notebook model from the configured ContentManager
"""
notebook_filename = model['name']
notebook_content = nbformat.writes(model['content']).encode('utf-8')
notebook_name = os.path.splitext(notebook_filename)[0]
tar_filename = '{}.tar.gz'.format(notebook_name)
info = tarfile.TarInfo(notebook_filename)
info.size = len(notebook_content)
with io.BytesIO() as tar_buffer:
with tarfile.open(tar_filename, "w:gz", fileobj=tar_buffer) as tar:
tar.addfile(info, io.BytesIO(notebook_content))
handler.set_attachment_header(tar_filename)
handler.set_header('Content-Type', 'application/gzip')
# Return the buffer value as the response
handler.finish(tar_buffer.getvalue())

View file

@ -0,0 +1 @@
Used to test globbing.

View file

@ -0,0 +1,6 @@
{
"nbformat_minor": 0,
"cells": [],
"nbformat": 4,
"metadata": {}
}

View file

@ -0,0 +1 @@
Used to test globbing.

View file

@ -0,0 +1,80 @@
"""Test the bundlers API."""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import io
from os.path import join as pjoin
from notebook.tests.launchnotebook import NotebookTestBase
from nbformat import write
from nbformat.v4 import (
new_notebook, new_markdown_cell, new_code_cell, new_output,
)
from unittest.mock import patch
def bundle(handler, model):
"""Bundler test stub. Echo the notebook path."""
handler.finish(model['path'])
class BundleAPITest(NotebookTestBase):
"""Test the bundlers web service API"""
@classmethod
def setup_class(cls):
"""Make a test notebook. Borrowed from nbconvert test. Assumes the class
teardown will clean it up in the end."""
super(BundleAPITest, cls).setup_class()
nbdir = cls.notebook_dir
nb = new_notebook()
nb.cells.append(new_markdown_cell(u'Created by test'))
cc1 = new_code_cell(source=u'print(2*6)')
cc1.outputs.append(new_output(output_type="stream", text=u'12'))
nb.cells.append(cc1)
with io.open(pjoin(nbdir, 'testnb.ipynb'), 'w',
encoding='utf-8') as f:
write(nb, f, version=4)
def test_missing_bundler_arg(self):
"""Should respond with 400 error about missing bundler arg"""
resp = self.request('GET', 'bundle/fake.ipynb')
self.assertEqual(resp.status_code, 400)
self.assertIn('Missing argument bundler', resp.text)
def test_notebook_not_found(self):
"""Shoudl respond with 404 error about missing notebook"""
resp = self.request('GET', 'bundle/fake.ipynb',
params={'bundler': 'fake_bundler'})
self.assertEqual(resp.status_code, 404)
self.assertIn('Not Found', resp.text)
def test_bundler_not_enabled(self):
"""Should respond with 400 error about disabled bundler"""
resp = self.request('GET', 'bundle/testnb.ipynb',
params={'bundler': 'fake_bundler'})
self.assertEqual(resp.status_code, 400)
self.assertIn('Bundler fake_bundler not enabled', resp.text)
def test_bundler_import_error(self):
"""Should respond with 500 error about failure to load bundler module"""
with patch('notebook.bundler.handlers.BundlerHandler.get_bundler') as mock:
mock.return_value = {'module_name': 'fake_module'}
resp = self.request('GET', 'bundle/testnb.ipynb',
params={'bundler': 'fake_bundler'})
mock.assert_called_with('fake_bundler')
self.assertEqual(resp.status_code, 500)
self.assertIn('Could not import bundler fake_bundler', resp.text)
def test_bundler_invoke(self):
"""Should respond with 200 and output from test bundler stub"""
with patch('notebook.bundler.handlers.BundlerHandler.get_bundler') as mock:
mock.return_value = {'module_name': 'notebook.bundler.tests.test_bundler_api'}
resp = self.request('GET', 'bundle/testnb.ipynb',
params={'bundler': 'stub_bundler'})
mock.assert_called_with('stub_bundler')
self.assertEqual(resp.status_code, 200)
self.assertIn('testnb.ipynb', resp.text)

View file

@ -0,0 +1,124 @@
"""Test the bundler tools."""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import unittest
import os
import shutil
import tempfile
import notebook.bundler.tools as tools
HERE = os.path.abspath(os.path.dirname(__file__))
class TestBundlerTools(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.mkdtemp()
def tearDown(self):
shutil.rmtree(self.tmp, ignore_errors=True)
def test_get_no_cell_references(self):
'''Should find no references in a regular HTML comment.'''
no_references = tools.get_cell_reference_patterns({'source':'''!<--
a
b
c
-->''', 'cell_type':'markdown'})
self.assertEqual(len(no_references), 0)
def test_get_cell_reference_patterns_comment_multiline(self):
'''Should find two references and ignore a comment within an HTML comment.'''
cell = {'cell_type':'markdown', 'source':'''<!--associate:
a
b/
#comment
-->'''}
references = tools.get_cell_reference_patterns(cell)
self.assertTrue('a' in references and 'b/' in references, str(references))
self.assertEqual(len(references), 2, str(references))
def test_get_cell_reference_patterns_comment_trailing_filename(self):
'''Should find three references within an HTML comment.'''
cell = {'cell_type':'markdown', 'source':'''<!--associate:c
a
b/
#comment
-->'''}
references = tools.get_cell_reference_patterns(cell)
self.assertTrue('a' in references and 'b/' in references and 'c' in references, str(references))
self.assertEqual(len(references), 3, str(references))
def test_get_cell_reference_patterns_precode(self):
'''Should find no references in a fenced code block in a *code* cell.'''
self.assertTrue(tools.get_cell_reference_patterns)
no_references = tools.get_cell_reference_patterns({'source':'''```
foo
bar
baz
```
''', 'cell_type':'code'})
self.assertEqual(len(no_references), 0)
def test_get_cell_reference_patterns_precode_mdcomment(self):
'''Should find two references and ignore a comment in a fenced code block.'''
cell = {'cell_type':'markdown', 'source':'''```
a
b/
#comment
```'''}
references = tools.get_cell_reference_patterns(cell)
self.assertTrue('a' in references and 'b/' in references, str(references))
self.assertEqual(len(references), 2, str(references))
def test_get_cell_reference_patterns_precode_backticks(self):
'''Should find three references in a fenced code block.'''
cell = {'cell_type':'markdown', 'source':'''```c
a
b/
#comment
```'''}
references = tools.get_cell_reference_patterns(cell)
self.assertTrue('a' in references and 'b/' in references and 'c' in references, str(references))
self.assertEqual(len(references), 3, str(references))
def test_glob_dir(self):
'''Should expand to single file in the resources/ subfolder.'''
self.assertIn(os.path.join('resources', 'empty.ipynb'),
tools.expand_references(HERE, ['resources/empty.ipynb']))
def test_glob_subdir(self):
'''Should expand to all files in the resources/ subfolder.'''
self.assertIn(os.path.join('resources', 'empty.ipynb'),
tools.expand_references(HERE, ['resources/']))
def test_glob_splat(self):
'''Should expand to all contents under this test/ directory.'''
globs = tools.expand_references(HERE, ['*'])
self.assertIn('test_bundler_tools.py', globs, globs)
self.assertIn('resources', globs, globs)
def test_glob_splatsplat_in_middle(self):
'''Should expand to test_file.txt deep under this test/ directory.'''
globs = tools.expand_references(HERE, ['resources/**/test_file.txt'])
self.assertIn(os.path.join('resources', 'subdir', 'test_file.txt'), globs, globs)
def test_glob_splatsplat_trailing(self):
'''Should expand to all descendants of this test/ directory.'''
globs = tools.expand_references(HERE, ['resources/**'])
self.assertIn(os.path.join('resources', 'empty.ipynb'), globs, globs)
self.assertIn(os.path.join('resources', 'subdir', 'test_file.txt'), globs, globs)
def test_glob_splatsplat_leading(self):
'''Should expand to test_file.txt under any path.'''
globs = tools.expand_references(HERE, ['**/test_file.txt'])
self.assertIn(os.path.join('resources', 'subdir', 'test_file.txt'), globs, globs)
self.assertIn(os.path.join('resources', 'another_subdir', 'test_file.txt'), globs, globs)
def test_copy_filelist(self):
'''Should copy select files from source to destination'''
globs = tools.expand_references(HERE, ['**/test_file.txt'])
tools.copy_filelist(HERE, self.tmp, globs)
self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'resources', 'subdir', 'test_file.txt')))
self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'resources', 'another_subdir', 'test_file.txt')))
self.assertFalse(os.path.isfile(os.path.join(self.tmp, 'resources', 'empty.ipynb')))

View file

@ -0,0 +1,72 @@
"""Test the bundlerextension CLI."""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import os
import shutil
import unittest
from unittest.mock import patch
from ipython_genutils.tempdir import TemporaryDirectory
from ipython_genutils import py3compat
from traitlets.tests.utils import check_help_all_output
import notebook.nbextensions as nbextensions
from notebook.config_manager import BaseJSONConfigManager
from ..bundlerextensions import (_get_config_dir, enable_bundler_python,
disable_bundler_python)
def test_help_output():
check_help_all_output('notebook.bundler.bundlerextensions')
check_help_all_output('notebook.bundler.bundlerextensions', ['enable'])
check_help_all_output('notebook.bundler.bundlerextensions', ['disable'])
class TestBundlerExtensionCLI(unittest.TestCase):
"""Tests the bundlerextension CLI against the example zip_bundler."""
def setUp(self):
"""Build an isolated config environment."""
td = TemporaryDirectory()
self.test_dir = py3compat.cast_unicode(td.name)
self.data_dir = os.path.join(self.test_dir, 'data')
self.config_dir = os.path.join(self.test_dir, 'config')
self.system_data_dir = os.path.join(self.test_dir, 'system_data')
self.system_path = [self.system_data_dir]
# Use temp directory, not real user or system config paths
self.patch_env = patch.dict('os.environ', {
'JUPYTER_CONFIG_DIR': self.config_dir,
'JUPYTER_DATA_DIR': self.data_dir,
})
self.patch_env.start()
self.patch_system_path = patch.object(nbextensions,
'SYSTEM_JUPYTER_PATH', self.system_path)
self.patch_system_path.start()
def tearDown(self):
"""Remove the test config environment."""
shutil.rmtree(self.test_dir, ignore_errors=True)
self.patch_env.stop()
self.patch_system_path.stop()
def test_enable(self):
"""Should add the bundler to the notebook configuration."""
enable_bundler_python('notebook.bundler.zip_bundler')
config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
cm = BaseJSONConfigManager(config_dir=config_dir)
bundlers = cm.get('notebook').get('bundlerextensions', {})
self.assertEqual(len(bundlers), 1)
self.assertIn('notebook_zip_download', bundlers)
def test_disable(self):
"""Should remove the bundler from the notebook configuration."""
self.test_enable()
disable_bundler_python('notebook.bundler.zip_bundler')
config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
cm = BaseJSONConfigManager(config_dir=config_dir)
bundlers = cm.get('notebook').get('bundlerextensions', {})
self.assertEqual(len(bundlers), 0)

View file

@ -0,0 +1,230 @@
"""Set of common tools to aid bundler implementations."""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import os
import shutil
import errno
import nbformat
import fnmatch
import glob
def get_file_references(abs_nb_path, version):
"""Gets a list of files referenced either in Markdown fenced code blocks
or in HTML comments from the notebook. Expands patterns expressed in
gitignore syntax (https://git-scm.com/docs/gitignore). Returns the
fully expanded list of filenames relative to the notebook dirname.
Parameters
----------
abs_nb_path: str
Absolute path of the notebook on disk
version: int
Version of the notebook document format to use
Returns
-------
list
Filename strings relative to the notebook path
"""
ref_patterns = get_reference_patterns(abs_nb_path, version)
expanded = expand_references(os.path.dirname(abs_nb_path), ref_patterns)
return expanded
def get_reference_patterns(abs_nb_path, version):
"""Gets a list of reference patterns either in Markdown fenced code blocks
or in HTML comments from the notebook.
Parameters
----------
abs_nb_path: str
Absolute path of the notebook on disk
version: int
Version of the notebook document format to use
Returns
-------
list
Pattern strings from the notebook
"""
notebook = nbformat.read(abs_nb_path, version)
referenced_list = []
for cell in notebook.cells:
references = get_cell_reference_patterns(cell)
if references:
referenced_list = referenced_list + references
return referenced_list
def get_cell_reference_patterns(cell):
'''
Retrieves the list of references from a single notebook cell. Looks for
fenced code blocks or HTML comments in Markdown cells, e.g.,
```
some.csv
foo/
!foo/bar
```
or
<!--associate:
some.csv
foo/
!foo/bar
-->
Parameters
----------
cell: dict
Notebook cell object
Returns
-------
list
Reference patterns found in the cell
'''
referenced = []
# invisible after execution: unrendered HTML comment
if cell.get('cell_type').startswith('markdown') and cell.get('source').startswith('<!--associate:'):
lines = cell.get('source')[len('<!--associate:'):].splitlines()
for line in lines:
if line.startswith('-->'):
break
# Trying to go out of the current directory leads to
# trouble when deploying
if line.find('../') < 0 and not line.startswith('#'):
referenced.append(line)
# visible after execution: rendered as a code element within a pre element
elif cell.get('cell_type').startswith('markdown') and cell.get('source').find('```') >= 0:
source = cell.get('source')
offset = source.find('```')
lines = source[offset + len('```'):].splitlines()
for line in lines:
if line.startswith('```'):
break
# Trying to go out of the current directory leads to
# trouble when deploying
if line.find('../') < 0 and not line.startswith('#'):
referenced.append(line)
# Clean out blank references
return [ref for ref in referenced if ref.strip()]
def expand_references(root_path, references):
"""Expands a set of reference patterns by evaluating them against the
given root directory. Expansions are performed against patterns
expressed in the same manner as in gitignore
(https://git-scm.com/docs/gitignore).
NOTE: Temporarily changes the current working directory when called.
Parameters
----------
root_path: str
Assumed root directory for the patterns
references: list
Reference patterns from get_reference_patterns expressed with
forward-slash directory separators
Returns
-------
list
Filename strings relative to the root path
"""
# Use normpath to convert to platform specific slashes, but be sure
# to retain a trailing slash which normpath pulls off
normalized_references = []
for ref in references:
normalized_ref = os.path.normpath(ref)
# un-normalized separator
if ref.endswith('/'):
normalized_ref += os.sep
normalized_references.append(normalized_ref)
references = normalized_references
globbed = []
negations = []
must_walk = []
for pattern in references:
if pattern and pattern.find(os.sep) < 0:
# simple shell glob
cwd = os.getcwd()
os.chdir(root_path)
if pattern.startswith('!'):
negations = negations + glob.glob(pattern[1:])
else:
globbed = globbed + glob.glob(pattern)
os.chdir(cwd)
elif pattern:
must_walk.append(pattern)
for pattern in must_walk:
pattern_is_negation = pattern.startswith('!')
if pattern_is_negation:
testpattern = pattern[1:]
else:
testpattern = pattern
for root, _, filenames in os.walk(root_path):
for filename in filenames:
joined = os.path.join(root[len(root_path) + 1:], filename)
if testpattern.endswith(os.sep):
if joined.startswith(testpattern):
if pattern_is_negation:
negations.append(joined)
else:
globbed.append(joined)
elif testpattern.find('**') >= 0:
# path wildcard
ends = testpattern.split('**')
if len(ends) == 2:
if joined.startswith(ends[0]) and joined.endswith(ends[1]):
if pattern_is_negation:
negations.append(joined)
else:
globbed.append(joined)
else:
# segments should be respected
if fnmatch.fnmatch(joined, testpattern):
if pattern_is_negation:
negations.append(joined)
else:
globbed.append(joined)
for negated in negations:
try:
globbed.remove(negated)
except ValueError as err:
pass
return set(globbed)
def copy_filelist(src, dst, src_relative_filenames):
"""Copies the given list of files, relative to src, into dst, creating
directories along the way as needed and ignore existence errors.
Skips any files that do not exist. Does not create empty directories
from src in dst.
Parameters
----------
src: str
Root of the source directory
dst: str
Root of the destination directory
src_relative_filenames: list
Filenames relative to src
"""
for filename in src_relative_filenames:
# Only consider the file if it exists in src
if os.path.isfile(os.path.join(src, filename)):
parent_relative = os.path.dirname(filename)
if parent_relative:
# Make sure the parent directory exists
parent_dst = os.path.join(dst, parent_relative)
try:
os.makedirs(parent_dst)
except OSError as exc:
if exc.errno == errno.EEXIST:
pass
else:
raise exc
shutil.copy2(os.path.join(src, filename), os.path.join(dst, filename))

View file

@ -0,0 +1,59 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import os
import io
import zipfile
import notebook.bundler.tools as tools
def _jupyter_bundlerextension_paths():
"""Metadata for notebook bundlerextension"""
return [{
'name': 'notebook_zip_download',
'label': 'IPython Notebook bundle (.zip)',
'module_name': 'notebook.bundler.zip_bundler',
'group': 'download'
}]
def bundle(handler, model):
"""Create a zip file containing the original notebook and files referenced
from it. Retain the referenced files in paths relative to the notebook.
Return the zip as a file download.
Assumes the notebook and other files are all on local disk.
Parameters
----------
handler : tornado.web.RequestHandler
Handler that serviced the bundle request
model : dict
Notebook model from the configured ContentManager
"""
abs_nb_path = os.path.join(handler.settings['contents_manager'].root_dir,
model['path'])
notebook_filename = model['name']
notebook_name = os.path.splitext(notebook_filename)[0]
# Headers
zip_filename = os.path.splitext(notebook_name)[0] + '.zip'
handler.set_attachment_header(zip_filename)
handler.set_header('Content-Type', 'application/zip')
# Get associated files
ref_filenames = tools.get_file_references(abs_nb_path, 4)
# Prepare the zip file
zip_buffer = io.BytesIO()
zipf = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
zipf.write(abs_nb_path, notebook_filename)
notebook_dir = os.path.dirname(abs_nb_path)
for nb_relative_filename in ref_filenames:
# Build absolute path to file on disk
abs_fn = os.path.join(notebook_dir, nb_relative_filename)
# Store file under path relative to notebook
zipf.write(abs_fn, nb_relative_filename)
zipf.close()
# Return the buffer value as the response
handler.finish(zip_buffer.getvalue())