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

Monkeypatching jupyter_client.kernelspec #104

Closed
wants to merge 11 commits into from
55 changes: 44 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,42 @@ conda install -n r_env r-irkernel
For other languages, their [corresponding kernels](https://github.com/jupyter/jupyter/wiki/Jupyter-kernels)
must be installed.

### Limitations
### Advanced usage: `jupyter_client` patching

By default, this extension works _only_ with Jupyter notebooks
and JupyterLab. This is because these notebook applications allow
the kernel modification mechanism to be customized by specifying
an alternate provider in `jupyter_notebook_config.json`. Other
Jupyter tools, such as `nbconvert`, `console`, and even
`jupyter kernelspec list` do not provide a comparable mechanism
for customization (yet; see below).

In this version of `nb_conda_kernels`, however, we have provided
mechanism to _patch_ `jupyter_client` itself to replace its
`KernelSpecManager` class with the conda-aware sublcass we have
developed. This allows all of these other Jupyter tools to take
advantage of this functionality. To try it, simply type this
command from within the environment where `nb_conda_kernels`
is installed:
```shell
python -m nb_conda_kernels patch
```
From there, you can verify that
```shell
jupyter kernelspec list
```
can now engage `nb_conda_kernels` and see the additional kernels.

This extension works _only_ with Jupyter notebooks and
JupyterLab. Unfortunately, it does not currently work with
Jupyter Console, `nbconvert`, and other tools. This is because
these tools were not designed to allow for the use of custom
KernelSpecs.
_NOTE_: if the `jupyter_client` is upgraded or reinstalled, the
patch must be reapplied. There is currently no way for `nb_conda_kernels`
to automatically reapply the patch.

A new [kernel discovery system](https://jupyter-client.readthedocs.io/en/latest/kernel_providers.html)
is being developed for Jupyter 6.0 that should enable the
wider Jupyter ecosystem to take advantage of these external
kernels. This package will require modification to
function properly in this new system.
function properly in this new system, but the patching
approach will no longer be required.

## Configuration

Expand Down Expand Up @@ -95,11 +118,12 @@ This package introduces two additional configuration options:

```shell
python setup.py develop
python -m nb_conda_kernels.install --enable
python -m nb_conda_kernels enable
```
If you want to use the jupyter_client patch, do this:
```shell
python -m nb_conda_kernels patch
```

Note: there is no longer any need to supply a
`--prefix` argument to the installer.

4. In order to properly exercise the package, the
tests assume a number of requirements:
Expand All @@ -121,6 +145,15 @@ This package introduces two additional configuration options:

## Changelog

### 2.2.1 (unreleased)

- Includes current active environment in the conda environment
list, even though it is already available through the normal
kernel search path (#115)
- Added experimental support for monkeypatching jupyter_client
for broader support of nb_conda_kernels in other jupyter
applications (not on by default) (#104)

### 2.2.0

- Perform full activation of kernel conda environments
Expand Down
1 change: 0 additions & 1 deletion nb_conda_kernels/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# flake8: noqa
from .manager import CondaKernelSpecManager
from ._version import get_versions
__version__ = get_versions()['version']
del get_versions
131 changes: 126 additions & 5 deletions nb_conda_kernels/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,126 @@
from jupyter_client import kernelspec
from .manager import CondaKernelSpecManager
kernelspec.KernelSpecManager = CondaKernelSpecManager
from jupyter_client.kernelspecapp import KernelSpecApp # noqa
KernelSpecApp.launch_instance()
import argparse
import logging

from .patch import status as patch_status, patch
from .install import status as install_status, enable, disable


log = logging.getLogger('nb_conda_kernels')
log.addHandler(logging.StreamHandler())
log.setLevel(logging.INFO)


# Arguments for command line
parser = argparse.ArgumentParser(
prog="python -m nb_conda_kernels",
description="Manages the nb_conda_kernels notebook extension.")
parser.add_argument(
"-v", "--verbose",
help="show more output",
action="store_true")
subparsers = parser.add_subparsers(dest='command')
subparsers.add_parser(
"status",
help="Print the status of the nb_conda_kernels installation.")
list_p = subparsers.add_parser(
"list",
help="List the kernels visible to nb_conda_kernels.")
list_p.add_argument(
"--json",
help="return JSON output",
action="store_true")
subparsers.add_parser(
"enable",
help=("Modify the Jupyter Notebook configuration so that it uses "
"nb_conda_kernels for kernel discovery. This is the original "
"approach to enabling nb_conda_kernels, and works only with "
"notebooks. To use nb_conda_kernels with the Jupyter console "
"or nbconvert, use 'patch'. If the patch is already present, "
"the configuration is not changed, since it would be redundant."))
subparsers.add_parser(
"patch",
help=("Patch jupyter_client to use nb_conda_kernels. For notebooks, "
"this provides the same functionality as 'enable', but this "
"also enables it to work with other Jupyter applications."))
subparsers.add_parser(
"disable",
help=("Remove nb_conda_kernels from operation, by removing the "
"configuration setting and the patch, if either are present."))
subparsers.add_parser(
"unpatch",
help=("Remove the nb_conda_kernels patch from jupyter_client. Unlike "
"'disable', this does not attempt to remove the notebook"
"configuration setting as well."))


def main(**kwargs):
if kwargs.get('verbose'):
log.setLevel(logging.DEBUG)
command = kwargs.get('command')

if command == 'list':
from jupyter_client import kernelspec
from .manager import CondaKernelSpecManager
kernelspec.KernelSpecManager = CondaKernelSpecManager
from jupyter_client.kernelspecapp import ListKernelSpecs
lk = ListKernelSpecs()
lk.json_output = bool(kwargs.get('json'))
lk.start()
return 0

p_status = patch_status('debug' if command == 'patch' else 'warning')
i_status = install_status()
desired = None

if command == 'enable':
desired = 'NOTEBOOKS ONLY'
log.info('Enabling nb_conda_kernels for notebooks...')
if p_status:
log.info('Already enabled by patch; no change made.')
desired = 'ENABLED'
elif i_status:
log.info('Already enabled for notebooks; no change made.')
else:
enable()
i_status = install_status()

elif command == 'disable':
desired = 'DISABLED'
log.info('Disabling nb_conda_kernels...')
if not i_status and not p_status:
log.info('Already disabled; no change made.')
if p_status:
log.info('Removing jupyter_client patch...')
if patch(uninstall=True):
p_status = patch_status()
if i_status:
log.info('Removing notebook configuration...')
disable()
i_status = install_status()

elif command == 'patch':
desired = 'ENABLED'
log.info('Patching jupyter_client.kernelspec...')
if p_status:
log.info('Patch already applied; no change made.')
elif patch():
p_status = patch_status()

elif command == 'unpatch':
desired = 'NOTEBOOKS ONLY' if i_status else 'DISABLED'
log.info('Unpatching jupyter_client.kernelspec...')
if not p_status:
log.info('Patch not detected; no change made.')
elif patch(uninstall=True):
p_status = patch_status()

mode_g = 'ENABLED' if p_status else ('NOTEBOOKS ONLY' if i_status else 'DISABLED')
print('nb_conda_kernels status: {}'.format(mode_g))
mode = 'ENABLED' if p_status else 'DISABLED'
print(' - jupyter_client patch: {}'.format(mode))
mode = 'ENABLED' if i_status else 'DISABLED'
print(' - notebook configuration: {}'.format(mode))
return desired is not None and mode_g != desired


main(**(parser.parse_args().__dict__))
Loading