Skip to content

Commit d54788b

Browse files
authored
Merge pull request #1 from Zsailer/shim
[WIP] Move over initial shim logic
2 parents a5f2843 + 51dadc0 commit d54788b

21 files changed

+980
-2
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Changelog
2+
3+
<!-- <START NEW CHANGELOG ENTRY> -->
4+
5+
6+
<!-- <END NEW CHANGELOG ENTRY> -->

LICENSE

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
BSD 3-Clause License
2+
3+
Copyright (c) 2022 Project Jupyter Contributors
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions are met:
8+
9+
1. Redistributions of source code must retain the above copyright notice, this
10+
list of conditions and the following disclaimer.
11+
12+
2. Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
16+
3. Neither the name of the copyright holder nor the names of its
17+
contributors may be used to endorse or promote products derived from
18+
this software without specific prior written permission.
19+
20+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

MANIFEST.in

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
include LICENSE
2+
3+
# added by check-manifest
4+
include *.md
5+
recursive-include docs *.md
6+
recursive-include notebook_shim *.py

README.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,30 @@
1-
# notebook_shim
2-
A shim layer for notebook traits and config
1+
# Notebook Shim
2+
3+
This project provides a way for JupyterLab and other frontends to switch to [Jupyter Server](https://github.com/jupyter/jupyter_server/) for their Python Web application backend.
4+
5+
## Basic Usage
6+
7+
Install from PyPI:
8+
```
9+
> pip install notebook_shim
10+
```
11+
This will automatically enable the extension in Jupyter Server.
12+
13+
## Usage
14+
15+
This project also includes an API for shimming traits that moved from `NotebookApp` in to `ServerApp` in Jupyter Server. This can be used by applications that subclassed `NotebookApp` to leverage the Python server backend of Jupyter Notebooks. Such extensions should *now* switch to `ExtensionApp` API in Jupyter Server and add `NotebookConfigShimMixin` in their inheritance list to properly handle moved traits.
16+
17+
For example, an application class that previously looked like:
18+
```python
19+
from notebook.notebookapp import NotebookApp
20+
21+
class MyApplication(NotebookApp):
22+
```
23+
should switch to look something like:
24+
```python
25+
from jupyter_server.extension.application import ExtensionApp
26+
from notebook_shim.shim import NotebookConfigShimMixin
27+
28+
class MyApplication(NotebookConfigShimMixin, ExtensionApp):
29+
```
30+

RELEASE.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
To create a release, update the version number in `noetbook_shim/__version__.py`, then run the following:
2+
3+
```
4+
git clean -dffx
5+
python setup.py sdist
6+
python setup.py bdist_wheel
7+
export script_version=`python setup.py --version 2>/dev/null`
8+
git commit -a -m "Release $script_version"
9+
git tag $script_version
10+
git push --all
11+
git push --tags
12+
pip install twine
13+
twine check dist/*
14+
twine upload dist/*
15+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"ServerApp": {
3+
"jpserver_extensions": {
4+
"notebook_shim": true
5+
}
6+
}
7+
}

notebook_shim/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
def _jupyter_server_extension_paths():
2+
return [
3+
{
4+
'module': 'notebook_shim.nbserver',
5+
}
6+
]

notebook_shim/_version.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "0.1.0"

notebook_shim/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pytest_plugins = ["jupyter_server.pytest_plugin"]

notebook_shim/nbserver.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""
2+
This module contains a Jupyter Server extension that attempts to
3+
make classic server and notebook extensions work in the new server.
4+
5+
Unfortunately, you'll notice that requires some major monkey-patching.
6+
The goal is that this extension will only be used as a temporary
7+
patch to transition extension authors from classic notebook server to jupyter_server.
8+
"""
9+
import os
10+
import types
11+
import inspect
12+
from functools import wraps
13+
from jupyter_core.paths import jupyter_config_path
14+
from traitlets.traitlets import is_trait
15+
16+
17+
from jupyter_server.services.config.manager import ConfigManager
18+
from .traits import NotebookAppTraits
19+
20+
21+
class ClassProxyError(Exception):
22+
pass
23+
24+
25+
def proxy(obj1, obj2, name, overwrite=False):
26+
"""Redirects a method, property, or trait from object 1 to object 2."""
27+
if hasattr(obj1, name) and overwrite is False:
28+
raise ClassProxyError(
29+
"Cannot proxy the attribute '{name}' from {cls2} because "
30+
"{cls1} already has this attribute.".format(
31+
name=name,
32+
cls1=obj1.__class__,
33+
cls2=obj2.__class__
34+
)
35+
)
36+
attr = getattr(obj2, name)
37+
38+
# First check if this thing is a trait (see traitlets)
39+
cls_attr = getattr(obj2.__class__, name)
40+
if is_trait(cls_attr) or type(attr) == property:
41+
thing = property(lambda self: getattr(obj2, name))
42+
43+
elif isinstance(attr, types.MethodType):
44+
@wraps(attr)
45+
def thing(self, *args, **kwargs):
46+
return attr(*args, **kwargs)
47+
48+
# Anything else appended on the class is just an attribute of the class.
49+
else:
50+
thing = attr
51+
52+
setattr(obj1.__class__, name, thing)
53+
54+
55+
def public_members(obj):
56+
members = inspect.getmembers(obj)
57+
return [m for m, _ in members if not m.startswith('_')]
58+
59+
60+
def diff_members(obj1, obj2):
61+
"""Return all attribute names found in obj2 but not obj1"""
62+
m1 = public_members(obj1)
63+
m2 = public_members(obj2)
64+
return set(m2).difference(m1)
65+
66+
67+
def get_nbserver_extensions(config_dirs):
68+
cm = ConfigManager(read_config_path=config_dirs)
69+
section = cm.get("jupyter_notebook_config")
70+
extensions = section.get('NotebookApp', {}).get('nbserver_extensions', {})
71+
return extensions
72+
73+
74+
def _link_jupyter_server_extension(serverapp):
75+
# Get the extension manager from the server
76+
manager = serverapp.extension_manager
77+
logger = serverapp.log
78+
79+
# Hack that patches the enabled extensions list, prioritizing
80+
# jupyter nbclassic. In the future, it would be much better
81+
# to incorporate a dependency injection system in the
82+
# Extension manager that allows extensions to list
83+
# their dependency tree and sort that way.
84+
def sorted_extensions(self):
85+
"""Dictionary with extension package names as keys
86+
and an ExtensionPackage objects as values.
87+
"""
88+
# Sort the keys and
89+
keys = sorted(self.extensions.keys())
90+
keys.remove("notebook_shim")
91+
keys = ["notebook_shim"] + keys
92+
return {key: self.extensions[key] for key in keys}
93+
94+
manager.__class__.sorted_extensions = property(sorted_extensions)
95+
96+
# Look to see if nbclassic is enabled. if so,
97+
# link the nbclassic extension here to load
98+
# its config. Then, port its config to the serverapp
99+
# for backwards compatibility.
100+
try:
101+
pkg = manager.extensions["notebook_shim"]
102+
pkg.link_point("notebook_shim", serverapp)
103+
point = pkg.extension_points["notebook_shim"]
104+
nbapp = point.app
105+
except Exception:
106+
nbapp = NotebookAppTraits()
107+
108+
# Proxy NotebookApp traits through serverapp to notebookapp.
109+
members = diff_members(serverapp, nbapp)
110+
for m in members:
111+
proxy(serverapp, nbapp, m)
112+
113+
# Find jupyter server extensions listed as notebook server extensions.
114+
jupyter_paths = jupyter_config_path()
115+
config_dirs = jupyter_paths + [serverapp.config_dir]
116+
nbserver_extensions = get_nbserver_extensions(config_dirs)
117+
118+
119+
def _load_jupyter_server_extension(serverapp):
120+
# Patch the config service manager to find the
121+
# proper path for old notebook frontend extensions
122+
config_manager = serverapp.config_manager
123+
read_config_path = config_manager.read_config_path
124+
read_config_path += [os.path.join(p, 'nbconfig')
125+
for p in jupyter_config_path()]
126+
config_manager.read_config_path = read_config_path

0 commit comments

Comments
 (0)