-
Notifications
You must be signed in to change notification settings - Fork 317
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
Changes from all commits
ae86a1c
1cfa2d9
7656001
029924e
6df9412
182d50e
2af8de6
f143547
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
$ 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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: There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the default if There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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:I can update the documentation in the PR for that usage case.
There was a problem hiding this comment.
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.