Repository: jupyterhub/jupyterhub

Issue:
Fix JUPYTERHUB_SINGLEUSER_APP after Notebook 7 release
### Bug description

With `notebook 6.5.4` it was possible to specify JUPYTERHUB_SINGLEUSER_APP='notebook' to run `Jupyter Notebook` instead of `JupyterLab`.

#### Expected behaviour

Jupyter Notebook is run in singleuser

#### Actual behaviour

`jupyterhub-singleuser` fails

### How to reproduce

Working image: `jupyter/base-notebook:notebook-6.5.4`
Failing image: `jupyter/base-notebook:notebook-7.0.0`

1. Run image: `docker run -it --rm jupyter/base-notebook:notebook-7.0.0 bash`
2. Run: `JUPYTERHUB_SINGLEUSER_APP='notebook' JUPYTERHUB_SERVICE_URL="127.0.0.1" jupyterhub-singleuser`

JupyterHub is not running inside the image, but I don't think that's the problem.

Output with Jupyter Notebook 7:
```
Traceback (most recent call last):
  File "/opt/conda/bin/jupyterhub-singleuser", line 6, in <module>
    from jupyterhub.singleuser import main
  File "/opt/conda/lib/python3.11/site-packages/jupyterhub/singleuser/__init__.py", line 67, in <module>
    from .app import SingleUserNotebookApp, main
  File "/opt/conda/lib/python3.11/site-packages/jupyterhub/singleuser/app.py", line 31, in <module>
    App = import_item(JUPYTERHUB_SINGLEUSER_APP)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/conda/lib/python3.11/site-packages/traitlets/utils/importstring.py", line 30, in import_item
    module = __import__(package, fromlist=[obj])
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ModuleNotFoundError: No module named 'notebook.notebookapp'
```

Output with Jupyter Notebook 6:
```
[I 2023-07-25 20:59:48.574 SingleUserNotebookApp mixins:547] Starting jupyterhub single-user server version 4.0.1
[I 2023-07-25 20:59:48.574 SingleUserNotebookApp mixins:561] Extending notebook.notebookapp.NotebookApp from notebook 6.5.4
[W 2023-07-25 20:59:48.578 SingleUserNotebookApp configurable:200] Config option `open_browser` not recognized by `SingleUserNotebookApp`.  Did you mean `browser`?
JUPYTERHUB_API_TOKEN env is required to run jupyterhub-singleuser. Did you launch it manually?
```

### Your personal set up



 - OS:
 <!-- [e.g. ubuntu 20.04, macOS 11.0] -->
 - Version(s):
 <!-- e.g. jupyterhub --version, python --version --->

<details><summary>Full environment</summary>
<!-- For reproduction, it's useful to have the full environment. For example, the output of `pip freeze` or `conda list` --->

```
# paste output of `pip freeze` or `conda list` here
```
</details>

<details><summary>Configuration</summary>
<!--
For JupyterHub, especially include information such as what Spawner and Authenticator are being used.
Be careful not to share any sensitive information.
You can paste jupyterhub_config.py below.
To exclude lots of comments and empty lines from auto-generated jupyterhub_config.py, you can do:
    grep -v '\(^#\|^[[:space:]]*$\)' jupyterhub_config.py
-->

```python
# jupyterhub_config.py
```
</details>

<details><summary>Logs</summary>
<!--
Errors are often logged by jupytehub. How you get logs depends on your deployment.
With kubernetes it might be:

    kubectl get pod # hub pod name starts with hub...
    kubectl logs hub-...
    # or for a single-user server
    kubectl logs jupyter-username

Or the-littlest-jupyterhub:

    journalctl -u jupyterhub
    # or for a single-user server
    journalctl -u jupyter-username
-->

```
# paste relevant logs here, if any
```
</details>



Discussion:
Thank you for opening your first issue in this project! Engagement like this is essential for open source projects! :hugs:
<br>If you haven't done so already, check out [Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md). Also, please try to follow the issue template as it helps other other community members to contribute more effectively.
![welcome](https://raw.githubusercontent.com/jupyterhub/.github/master/images/welcome.jpg)
You can meet the other [Jovyans](https://jupyter.readthedocs.io/en/latest/community/content-community.html?highlight=jovyan#what-is-a-jovyan) by joining our [Discourse forum](http://discourse.jupyter.org/). There is also an intro thread there where you can stop by and say Hi! :wave:
<br>Welcome to the Jupyter community! :tada:

I've tried setting `export JUPYTERHUB_SINGLEUSER_APP='notebook.notebookapp.NotebookApp'` (as in [docs](https://jupyterhub.readthedocs.io/en/stable/howto/configuration/config-user-env.html#switching-back-to-the-classic-notebook)), the error stays the same (there is an internal map which allows to use short `notebook` when setting this env variable).
https://github.com/jupyterhub/jupyterhub/blob/3fbdf02cc5c6574432dd28710ed2fc64b2353081/jupyterhub/singleuser/app.py#L17-L24

Do you know what the correct import is for notebook 7?
It seems to be `JUPYTERHUB_SINGLEUSER_APP='notebook.app.JupyterNotebookApp'`
Here is the source code:
https://github.com/jupyter/notebook/blob/main/notebook/app.py#L227

I tried to launch it. It fails with this error: `AttributeError: 'JupyterNotebookApp' object has no attribute 'login_handler_class'`.

An example of such a field in a `jupyter_server.serverapp.ServerApp`
https://github.com/jupyter-server/jupyter_server/blob/main/jupyter_server/serverapp.py#L1534
@jtpio Is this something we need to fix in JupyterHub, notebook, or both?
Normally Notebook 7 should work with `jupyter labhub` according to the discussion in https://github.com/jupyter/notebook/issues/6904.

Otherwise maybe the mapping should be `"notebook": "jupyter_server.serverapp.ServerApp"` since Notebook 7 is based on Jupyter Server?

Resolving patch:
diff --git a/jupyterhub/singleuser/app.py b/jupyterhub/singleuser/app.py
--- a/jupyterhub/singleuser/app.py
+++ b/jupyterhub/singleuser/app.py
@@ -6,7 +6,7 @@
 .. versionchanged:: 2.0
 
     Default app changed to launch `jupyter labhub`.
-    Use JUPYTERHUB_SINGLEUSER_APP=notebook.notebookapp.NotebookApp for the legacy 'classic' notebook server.
+    Use JUPYTERHUB_SINGLEUSER_APP='notebook' for the legacy 'classic' notebook server (requires notebook<7).
 """
 import os
 
@@ -27,7 +27,25 @@
     JUPYTERHUB_SINGLEUSER_APP.replace("_", "-"), JUPYTERHUB_SINGLEUSER_APP
 )
 
+
 if JUPYTERHUB_SINGLEUSER_APP:
+    if JUPYTERHUB_SINGLEUSER_APP in {"notebook", _app_shortcuts["notebook"]}:
+        # better error for notebook v7, which uses jupyter-server
+        # when the legacy notebook server is requested
+        try:
+            from notebook import __version__
+        except ImportError:
+            # will raise later
+            pass
+        else:
+            # check if this failed because of notebook v7
+            _notebook_major_version = int(__version__.split(".", 1)[0])
+            if _notebook_major_version >= 7:
+                raise ImportError(
+                    f"JUPYTERHUB_SINGLEUSER_APP={JUPYTERHUB_SINGLEUSER_APP} is not valid with notebook>=7 (have notebook=={__version__}).\n"
+                    f"Leave $JUPYTERHUB_SINGLEUSER_APP unspecified (or use the default JUPYTERHUB_SINGLEUSER_APP=jupyter-server), "
+                    'and set `c.Spawner.default_url = "/tree"` to make notebook v7 the default UI.'
+                )
     App = import_item(JUPYTERHUB_SINGLEUSER_APP)
 else:
     App = None


Test patch: