Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use jupyter nbextension/serverextension for installation/activation #589

Merged
merged 8 commits into from
Dec 22, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions nbgrader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,27 @@
A system for assigning and grading notebooks.
"""

import os
from ._version import version_info, __version__


def _jupyter_nbextension_paths():
return [
dict(
section="tree",
src=os.path.join('nbextensions','static','assignment_list'),
dest="assignment_list",
require="assignment_list/main"
),
dict(
section="notebook",
src=os.path.join('nbextensions','static','create_assignment'),
dest="create_assignment",
require="create_assignment/main"
),
]

def _jupyter_server_extension_paths():
return [
dict(module="nbgrader.nbextensions.assignment_list")
]
292 changes: 14 additions & 278 deletions nbgrader/apps/extensionapp.py
Original file line number Diff line number Diff line change
@@ -1,299 +1,35 @@
import io
import os.path
import json
import sys
import six

from jupyter_core.paths import jupyter_config_dir
from notebook.nbextensions import InstallNBExtensionApp, UninstallNBExtensionApp, EnableNBExtensionApp, DisableNBExtensionApp, install_nbextension
from traitlets import Unicode
from traitlets.config import Config
from traitlets.config.application import catch_config_error
from traitlets.config.application import Application
from traitlets.config.loader import JSONFileConfigLoader, ConfigFileNotFound

from .baseapp import NbGrader, format_excepthook

class ExtensionInstallApp(InstallNBExtensionApp, NbGrader):

name = u'nbgrader-extension-install'
description = u'Install the nbgrader extensions'

examples = """

To install all the extensions, run:

nbgrader extension install

If you want to install the extensions for only your user environment and
not systemwide, use:

nbgrader extension install --user

If you don't want to have to reinstall the extensions when nbgrader is
updated, use:

nbgrader extension install --symlink

To install only a specific extension, you can pass the name of the
extension you want to install as an argument, e.g.:

nbgrader extension install create_assignment
nbgrader extension install assignment_list

"""

destination = Unicode('')

def _classes_default(self):
return [ExtensionInstallApp, InstallNBExtensionApp]

def excepthook(self, etype, evalue, tb):
format_excepthook(etype, evalue, tb)

def install_extensions(self):
install_nbextension(
self.extra_args[0],
overwrite=self.overwrite,
symlink=self.symlink,
user=self.user,
sys_prefix=self.sys_prefix,
prefix=self.prefix,
nbextensions_dir=self.nbextensions_dir,
logger=self.log)

def start(self):
nbextensions_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'nbextensions'))
extra_args = self.extra_args[:]

# install the create_assignment extension
if len(extra_args) == 0 or "create_assignment" in extra_args:
self.log.info("Installing create_assignment extension")
self.extra_args = [os.path.join(nbextensions_dir, 'static', 'create_assignment')]
self.install_extensions()

# install the assignment_list extension
if sys.platform != 'win32' and (len(extra_args) == 0 or "assignment_list" in extra_args):
self.log.info("Installing assignment_list extension")
self.extra_args = [os.path.join(nbextensions_dir, 'static', 'assignment_list')]
self.install_extensions()


class ExtensionUninstallApp(UninstallNBExtensionApp, NbGrader):

name = u'nbgrader-extension-uninstall'
description = u'Uninstall the nbgrader extensions'

examples = """

To uninstall all the nbgrader extensions that are installed systemwide,
run:

nbgrader extension uninstall

If you want to uninstall the extensions installed in your user
environment, use:

nbgrader extension uninstall --user

To uninstall only a specific extension, you can pass the name of the
extension you want to uninstall as an argument, e.g.:

nbgrader extension uninstall create_assignment
nbgrader extension uninstall assignment_list

"""

destination = Unicode('')

def _classes_default(self):
return [ExtensionUninstallApp, UninstallNBExtensionApp]

def excepthook(self, etype, evalue, tb):
format_excepthook(etype, evalue, tb)

def start(self):
nbextensions_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'nbextensions'))
extra_args = self.extra_args[:]

# install the create_assignment extension
if len(extra_args) == 0 or "create_assignment" in extra_args:
self.log.info("Uninstalling create_assignment extension")
self.extra_args = ["create_assignment"]
self.uninstall_extensions()

# install the assignment_list extension
if sys.platform != 'win32' and (len(extra_args) == 0 or "assignment_list" in extra_args):
self.log.info("Uninstalling assignment_list extension")
self.extra_args = ["assignment_list"]
self.uninstall_extensions()


class ExtensionActivateApp(EnableNBExtensionApp, NbGrader):
_compat_message = """
The installation of the nbgrader extensions are now managed through the
`jupyter nbextension` and `jupyter serverextension` commands.

name = u'nbgrader-extension-activate'
description = u'Activate the nbgrader extension'
To install and enable the nbextensions (assignment_list and create_assignment) run:

flags = {}
aliases = {}
$ jupyter nbextension install --sys-prefix --py nbgrader
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to make it so the different extensions can be installed separately? It will be a regression if this is no longer possible, and I know there are people who want to install e.g. just the "assignment list" extension for their students on a server but not the "create assignment" extension (so students can't easily modify their assignments).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the commands are run using the --py approach, then it has to be done all together. However, you can do things separately like this:

jupyter nbextension install --sys-prefix --py nbgrader
jupyter nbextension enable --sys-prefix create_assignment/main

I can update the documentation in the PR for that usage case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so just to be clear, all the extensions will always be installed, but we can give people different instructions for how to selectively enable extensions?

It would be great if you can update the documentation to be really explicit about that -- i.e., have one example for how to install and activate all the extensions, and then another for how to activate just one of the extensions.

$ jupyter nbextension enable --sys-prefix --py nbgrader

To install the server extension (assignment_list) run:

examples = """

To activate all the nbgrader extensions:

nbgrader extension activate

To activate only a specific extension, you can pass the name of the
extension you want to activate as an argument, e.g.:

nbgrader extension activate create_assignment
nbgrader extension activate assignment_list

"""

def _classes_default(self):
return [ExtensionActivateApp, EnableNBExtensionApp]

def enable_server_extension(self, extension):
loader = JSONFileConfigLoader('jupyter_notebook_config.json', jupyter_config_dir())
try:
config = loader.load_config()
except ConfigFileNotFound:
config = Config()

if 'server_extensions' not in config.NotebookApp:
config.NotebookApp.server_extensions = []
if extension not in config.NotebookApp.server_extensions:
config.NotebookApp.server_extensions.append(extension)

# save the updated config
with io.open(os.path.join(jupyter_config_dir(), 'jupyter_notebook_config.json'), 'w+') as f:
f.write(six.u(json.dumps(config, indent=2)))

def start(self):
if len(self.extra_args) == 0 or "create_assignment" in self.extra_args:
self.section = "notebook"
self.toggle_nbextension("create_assignment/main")

if sys.platform != 'win32' and (len(self.extra_args) == 0 or "assignment_list" in self.extra_args):
self.log.info("Activating assignment_list server extension")
self.enable_server_extension('nbgrader.nbextensions.assignment_list')

self.section = "tree"
self.toggle_nbextension("assignment_list/main")

self.log.info("Done. You may need to restart the Jupyter notebook server for changes to take effect.")


class ExtensionDeactivateApp(DisableNBExtensionApp, NbGrader):

name = u'nbgrader-extension-deactivate'
description = u'Deactivate the nbgrader extension'

flags = {}
aliases = {}

examples = """

To deactivate all the nbgrader extensions:

nbgrader extension deactivate

To deactivate only a specific extension, you can pass the name of the
extension you want to deactivate as an argument, e.g.:

nbgrader extension deactivate create_assignment
nbgrader extension deactivate assignment_list

"""

def _classes_default(self):
return [ExtensionDeactivateApp, DisableNBExtensionApp]

def _recursive_get(self, obj, key_list):
if obj is None or len(key_list) == 0:
return obj
return self._recursive_get(obj.get(key_list[0], None), key_list[1:])

def disable_server_extension(self, extension):
loader = JSONFileConfigLoader('jupyter_notebook_config.json', jupyter_config_dir())
try:
config = loader.load_config()
except ConfigFileNotFound:
config = Config()

if 'server_extensions' not in config.NotebookApp:
return
if extension not in config.NotebookApp.server_extensions:
return

config.NotebookApp.server_extensions.remove(extension)

# save the updated config
with io.open(os.path.join(jupyter_config_dir(), 'jupyter_notebook_config.json'), 'w+') as f:
f.write(six.u(json.dumps(config, indent=2)))

def start(self):
if len(self.extra_args) == 0 or "create_assignment" in self.extra_args:
self.log.info("Deactivating create_assignment nbextension")
self.section = "notebook"
self.toggle_nbextension("create_assignment/main")

if sys.platform != 'win32' and (len(self.extra_args) == 0 or "assignment_list" in self.extra_args):
self.log.info("Deactivating assignment_list server extension")
self.disable_server_extension('nbgrader.nbextensions.assignment_list')

self.log.info("Deactivating assignment_list nbextension")
self.section = "tree"
self.toggle_nbextension("assignment_list/main")

self.log.info("Done. You may need to restart the Jupyter notebook server for changes to take effect.")
$ jupyter serverextension enable --sys-prefix --py nbgrader
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this have to be activated separately from the nbextension? They go hand in hand and one won't work without the other. Can we make it so the assignment list extension can be fully activated with one command?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direction we are moving is to keep nbextension/jupyterlab extensions well separated from the server extensions. A couple of reasons for this:

  • A single server could be serving notebooks to multiple frontends (notebook, nteract, jupyterlab)
  • We are in the process of separating the notebook server into its own project and repo that won't share any code with the frontends
  • We are also moving to a different approach for installing all of these things that will make it all easier for users. While we do this, we want to avoid making things more complicated with our existing stuff.

Obviously, in nbgrader, we could wrap all that and expose an endpoint that does it all automatically. For that I think the best approach is to create a conda-forge feedstock with post install scripts that do everything. This is what we are doing with the vega package and the install process is then trivial (no post install commands needed). Are you ok with that approach? Here is the vega-feedstock that has the needed scripts:

https://github.com/conda-forge/vega-feedstock

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, that makes sense. Having it be done with conda automatically helps, though not everybody installs nbgrader with conda.

I think it's fine if it requires 3 commands to install and activate the assignment list extension as long as we're clear about it. The thing I'm more concerned about is how to install/activate the two extensions (assignment list and create assignment) separately.


To install for all users, replace `--sys-prefix` by `--system`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the default if --sys-prefix is not included?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! Right now the defaults for serverextensions and nbextensions are different, which we consider to be a bug. However fixing that bug is an API change so it is getting into the code base slowly. Because of this I always give one of the following explicitely: --sys-prefix, --system or --user.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, makes sense.

To install only for the current user replace `--sys-prefix` by `--user`.
"""

class ExtensionApp(NbGrader):

name = u'nbgrader extension'
description = u'Utilities for managing the nbgrader extension'
examples = ""

subcommands = dict(
install=(
ExtensionInstallApp,
"Install the extensions."
),
uninstall=(
ExtensionUninstallApp,
"Uninstall the extensions."
),
activate=(
ExtensionActivateApp,
"Activate the extensions."
),
deactivate=(
ExtensionDeactivateApp,
"Deactivate the extensions."
)
)

def _classes_default(self):
classes = super(ExtensionApp, self)._classes_default()

# include all the apps that have configurable options
for appname, (app, help) in self.subcommands.items():
if len(app.class_traits(config=True)) > 0:
classes.append(app)

return classes

@catch_config_error
def initialize(self, argv=None):
super(ExtensionApp, self).initialize(argv)

def start(self):
# check: is there a subapp given?
if self.subapp is None:
self.print_help()
sys.exit(1)

# This starts subapps
for line in _compat_message.split('\n'):
self.log.info(line)
super(ExtensionApp, self).start()
4 changes: 4 additions & 0 deletions nbgrader/docs/source/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ filesystem
formatters
formgrade
formgrader
frontend
gif
Gradebook
gradebook
Expand Down Expand Up @@ -85,6 +86,8 @@ mikebolt
minrk
namespace
nbconvert
nbextension
nbextensions
nbgrader
neuroscience
np
Expand Down Expand Up @@ -117,6 +120,7 @@ resize
rst
runtime
sansary
serverextension
smeylan
spellcheck
SQL
Expand Down
Loading