2.0: jupyterlab by default

swaps from default nbclassic and opt-in to lab, to now default to lab and opt-in to nbclassic

defaults to jupyterlab *if* lab 3.1 is available,
so should still work without configuration if lab is unavailable (or too old)
This commit is contained in:
Min RK
2021-09-23 10:49:41 +02:00
parent e200783c59
commit 4082006039
6 changed files with 165 additions and 23 deletions

View File

@@ -38,9 +38,9 @@ jobs:
# Tests everything when JupyterHub works against a dedicated mysql or
# postgresql server.
#
# jupyter_server:
# nbclassic:
# Tests everything when the user instances are started with
# jupyter_server instead of notebook.
# notebook instead of jupyter_server.
#
# ssl:
# Tests everything using internal SSL connections instead of
@@ -48,7 +48,7 @@ jobs:
#
# main_dependencies:
# Tests everything when the we use the latest available dependencies
# from: ipytraitlets.
# from: traitlets.
#
# NOTE: Since only the value of these parameters are presented in the
# GitHub UI when the workflow run, we avoid using true/false as
@@ -56,6 +56,7 @@ jobs:
include:
- python: "3.6"
oldest_dependencies: oldest_dependencies
nbclassic: nbclassic
- python: "3.6"
subdomain: subdomain
- python: "3.7"
@@ -65,7 +66,7 @@ jobs:
- python: "3.8"
db: postgres
- python: "3.8"
jupyter_server: jupyter_server
nbclassic: nbclassic
- python: "3.9"
main_dependencies: main_dependencies
@@ -130,9 +131,9 @@ jobs:
if [ "${{ matrix.main_dependencies }}" != "" ]; then
pip install git+https://github.com/ipython/traitlets#egg=traitlets --force
fi
if [ "${{ matrix.jupyter_server }}" != "" ]; then
pip uninstall notebook --yes
pip install jupyter_server
if [ "${{ matrix.nbclassic }}" != "" ]; then
pip uninstall jupyter_server --yes
pip install notebook
fi
if [ "${{ matrix.db }}" == "mysql" ]; then
pip install mysql-connector-python

View File

@@ -7,8 +7,8 @@ codecov
coverage
cryptography
html5lib # needed for beautifulsoup
jupyterlab >=3
mock
notebook
pre-commit
pytest>=3.3
pytest-asyncio

View File

@@ -76,13 +76,26 @@ c.InteractiveShellApp.extensions.append("cython")
### Example: Enable a Jupyter notebook configuration setting for all users
:::{note}
These examples configure the Jupyter ServerApp,
which is used by JupyterLab, the default in JupyterHub 2.0.
If you are using the classing Jupyter Notebook server,
the same things should work,
with the following substitutions:
- Where you see `jupyter_server_config`, use `jupyter_notebook_config`
- Where you see `NotebookApp`, use `ServerApp`
:::
To enable Jupyter notebook's internal idle-shutdown behavior (requires
notebook ≥ 5.4), set the following in the `/etc/jupyter/jupyter_notebook_config.py`
notebook ≥ 5.4), set the following in the `/etc/jupyter/jupyter_server_config.py`
file:
```python
# shutdown the server after no activity for an hour
c.NotebookApp.shutdown_no_activity_timeout = 60 * 60
c.ServerApp.shutdown_no_activity_timeout = 60 * 60
# shutdown kernels after no activity for 20 minutes
c.MappingKernelManager.cull_idle_timeout = 20 * 60
# check for idle kernels every two minutes
@@ -112,8 +125,8 @@ Assuming I have a Python 2 and Python 3 environment that I want to make
sure are available, I can install their specs system-wide (in /usr/local) with:
```bash
/path/to/python3 -m IPython kernel install --prefix=/usr/local
/path/to/python2 -m IPython kernel install --prefix=/usr/local
/path/to/python3 -m ipykernel install --prefix=/usr/local
/path/to/python2 -m ipykernel install --prefix=/usr/local
```
## Multi-user hosts vs. Containers
@@ -176,12 +189,38 @@ The number of named servers per user can be limited by setting
c.JupyterHub.named_server_limit_per_user = 5
```
## Switching to Jupyter Server
## Switching back to classic notebook
[Jupyter Server](https://jupyter-server.readthedocs.io/en/latest/) is a new Tornado Server backend for Jupyter web applications (e.g. JupyterLab 3.0 uses this package as its default backend).
By default the single-user server launches JupyterLab,
which is based on [Jupyter Server][].
This is the default server when running JupyterHub ≥ 2.0.
You can switch to using the legacy Jupyter Notebook server by setting the `JUPYTERHUB_SINGLEUSER_APP` environment variable
(in the single-user environment) to:
By default, the single-user notebook server uses the (old) `NotebookApp` from the [notebook](https://github.com/jupyter/notebook) package. You can switch to using Jupyter Server's `ServerApp` backend (this will likely become the default in future releases) by setting the `JUPYTERHUB_SINGLEUSER_APP` environment variable to:
```bash
export JUPYTERHUB_SINGLEUSER_APP='notebook.notebookapp.NotebookApp'
```
[jupyter server]: https://jupyter-server.readthedocs.io
[jupyter notebook]: https://jupyter-notebook.readthedocs.io
:::{versionchanged} 2.0
JupyterLab is now the default singleuser UI, if available,
which is based on the [Jupyter Server][],
no longer the legacy [Jupyter Notebook][] server.
JupyterHub prior to 2.0 launched the legacy notebook server (`jupyter notebook`),
and Jupyter server could be selected by specifying
```python
# jupyterhub_config.py
c.Spawner.cmd = ["jupyter-labhub"]
```
or for an otherwise customized Jupyter Server app,
set the environment variable:
```bash
export JUPYTERHUB_SINGLEUSER_APP='jupyter_server.serverapp.ServerApp'
```
:::

View File

@@ -1,7 +1,12 @@
"""Make a single-user app based on the environment:
- $JUPYTERHUB_SINGLEUSER_APP, the base Application class, to be wrapped in JupyterHub authentication.
default: notebook.notebookapp.NotebookApp
default: jupyter_server.serverapp.ServerApp
.. versionchanged:: 2.0
Default app changed to launch `jupyter labhub`.
Use JUPYTERHUB_SINGLEUSER_APP=notebook.notebookapp.NotebookApp for the legacy 'classic' notebook server.
"""
import os
@@ -9,12 +14,55 @@ from traitlets import import_item
from .mixins import make_singleuser_app
JUPYTERHUB_SINGLEUSER_APP = (
os.environ.get("JUPYTERHUB_SINGLEUSER_APP") or "notebook.notebookapp.NotebookApp"
)
JUPYTERHUB_SINGLEUSER_APP = os.environ.get("JUPYTERHUB_SINGLEUSER_APP")
if JUPYTERHUB_SINGLEUSER_APP:
App = import_item(JUPYTERHUB_SINGLEUSER_APP)
else:
App = None
_import_error = None
for JUPYTERHUB_SINGLEUSER_APP in (
"jupyter_server.serverapp.ServerApp",
"notebook.notebookapp.NotebookApp",
):
try:
App = import_item(JUPYTERHUB_SINGLEUSER_APP)
except ImportError as e:
continue
if _import_error is None:
_import_error = e
else:
break
if App is None:
raise _import_error
SingleUserNotebookApp = make_singleuser_app(App)
main = SingleUserNotebookApp.launch_instance
def main():
"""Launch a jupyterhub single-user server"""
if not os.environ.get("JUPYTERHUB_SINGLEUSER_APP"):
# app not specified, launch jupyter-labhub by default,
# if jupyterlab is recent enough (3.1).
# This is a minimally extended ServerApp that does:
# 1. ensure lab extension is enabled, and
# 2. set default URL to `/lab`
import re
_version_pat = re.compile(r"(\d+)\.(\d+)")
try:
import jupyterlab
from jupyterlab.labhubapp import SingleUserLabApp
m = _version_pat.match(jupyterlab.__version__)
except Exception:
m = None
if m is not None:
version_tuple = tuple(int(v) for v in m.groups())
if version_tuple >= (3, 1):
return SingleUserLabApp.launch_instance()
return SingleUserNotebookApp.launch_instance()

View File

@@ -1,6 +1,10 @@
"""Tests for jupyterhub.singleuser"""
import os
import sys
from contextlib import contextmanager
from subprocess import CalledProcessError
from subprocess import check_output
from unittest import mock
from urllib.parse import urlparse
import pytest
@@ -14,6 +18,12 @@ from .utils import async_requests
from .utils import AsyncSession
@contextmanager
def nullcontext():
"""Python 3.7+ contextlib.nullcontext, backport for 3.6"""
yield
@pytest.mark.parametrize(
"access_scopes, server_name, expect_success",
[
@@ -171,3 +181,47 @@ def test_version():
[sys.executable, '-m', 'jupyterhub.singleuser', '--version']
).decode('utf8', 'replace')
assert jupyterhub.__version__ in out
@pytest.mark.parametrize(
"JUPYTERHUB_SINGLEUSER_APP",
[
"",
"notebook.notebookapp.NotebookApp",
"jupyter_server.serverapp.ServerApp",
],
)
def test_singleuser_app_class(JUPYTERHUB_SINGLEUSER_APP):
try:
import jupyter_server # noqa
except ImportError:
have_server = False
expect_error = "jupyter_server" in JUPYTERHUB_SINGLEUSER_APP
else:
have_server = True
expect_error = False
if expect_error:
ctx = pytest.raises(CalledProcessError)
else:
ctx = nullcontext()
with mock.patch.dict(
os.environ,
{
"JUPYTERHUB_SINGLEUSER_APP": JUPYTERHUB_SINGLEUSER_APP,
},
):
with ctx:
out = check_output(
[sys.executable, '-m', 'jupyterhub.singleuser', '--help-all']
).decode('utf8', 'replace')
if expect_error:
return
# use help-all output to check inheritance
if 'NotebookApp' in JUPYTERHUB_SINGLEUSER_APP or not have_server:
assert '--NotebookApp.' in out
assert '--ServerApp.' not in out
else:
assert '--ServerApp.' in out
assert '--NotebookApp.' not in out

View File

@@ -7,6 +7,6 @@ MAINTAINER Project Jupyter <jupyter@googlegroups.com>
ADD install_jupyterhub /tmp/install_jupyterhub
ARG JUPYTERHUB_VERSION=main
# install pinned jupyterhub and ensure notebook is installed
# install pinned jupyterhub and ensure jupyterlab is installed
RUN python3 /tmp/install_jupyterhub && \
python3 -m pip install notebook
python3 -m pip install jupyterlab