From faf66f4f042967b3163442d928dd5a67234b88ae Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Fri, 22 Sep 2023 09:21:49 -0500 Subject: [PATCH] update with review comments --- .github/workflows/test.yaml | 54 ++++------ CONTRIBUTING.md | 197 ++++++++++++++++++++++++++++++------ README.md | 28 +++-- labextension/README.md | 10 +- labextension/src/index.ts | 195 +++++++++++++++++++++++------------ labextension/src/tokens.ts | 70 +++++++++---- pyproject.toml | 12 ++- 7 files changed, 401 insertions(+), 165 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 47865474..a6d6952e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,6 +26,8 @@ on: env: # avoid warnings about config paths JUPYTER_PLATFORM_DIRS: "1" + # avoid looking at every version of pip ever released + PIP_DISABLE_PIP_VERSION_CHECK: "1" jobs: build: @@ -57,12 +59,7 @@ jobs: path: ./dist test: - name: |- - ${{ matrix.os }} - ${{ matrix.python-version }} - s${{ matrix.jupyter_server-version }} - lab${{ matrix.jupyterlab-version }} - nb${{ matrix.notebook-version }} + name: ${{ matrix.os }} ${{ matrix.python-version }} ${{ matrix.pip-extras }} needs: [build] timeout-minutes: 30 runs-on: ${{ matrix.os }} @@ -75,31 +72,16 @@ jobs: matrix: os: [ubuntu-22.04] python-version: ["3.8", "3.11"] - jupyter_server-version: ["1", "2"] - jupyterlab-version: ["3", "4"] - include: - # let jupyterlab-version 3 and 4 imply notebook-version 6 and 7 - # respectively, to avoid doubling the amount of test runs - - jupyterlab-version: "3" - notebook-version: "6" - - jupyterlab-version: "4" - notebook-version: "7" + pip-extras: ["lab", "classic"] + exclude: # windows should work for all test variations, but a limited selection # is run to avoid doubling the amount of test runs - - os: windows-2022 - python-version: "3.8" - jupyter_server-version: "1" - jupyterlab-version: "3" - notebook-version: "6" - os: windows-2022 python-version: "3.11" - jupyter_server-version: "2" - jupyterlab-version: "4" - notebook-version: "7" - exclude: - # jupyterlab-version 4 requires jupyter_server-version 2 - - jupyter_server-version: "1" - jupyterlab-version: "4" + pip-extras: classic + - os: windows-2022 + python-version: "3.8" + pip-extras: lab steps: - uses: actions/checkout@v4 @@ -123,7 +105,7 @@ jobs: # # Pytest options are set in `pyproject.toml`. run: | - pip install -vv $(ls ./dist/*.whl)\[acceptance\] 'jupyterlab~=${{ matrix.jupyterlab-version }}.0' 'jupyter_server~=${{ matrix.jupyter_server-version }}.0' 'notebook~=${{ matrix.notebook-version }}.0' + pip install -vv $(ls ./dist/*.whl)\[acceptance,${{ matrix.pip-extras }}\] - name: List Python packages run: | @@ -136,19 +118,19 @@ jobs: jupyter server extension list 2>&1 | grep -iE "jupyter_server_proxy.*OK" - - name: Check server extension for notebook v6 - if: matrix.notebook-version == '6' + if: contains(matrix.pip-extras, 'classic') run: | jupyter serverextension list jupyter serverextension list 2>&1 | grep -iE "jupyter_server_proxy.*OK" - - name: Check frontend extension for notebook v6 - if: matrix.notebook-version == '6' + if: contains(matrix.pip-extras, 'classic') run: | jupyter nbextension list PYTHONUNBUFFERED=1 jupyter nbextension list 2>&1 | grep -A1 -iE '.*jupyter_server_proxy.*enabled' | grep -B1 -iE "Validating.*OK" - name: Check frontend extension for notebook v7+ - if: matrix.notebook-version != '6' + if: ${{ !contains(matrix.pip-extras, 'classic') }} run: | jupyter notebook extension list jupyter notebook extension list 2>&1 | grep -iE 'jupyter_server_proxy.*OK.*' @@ -158,12 +140,12 @@ jobs: jupyter lab extension list jupyter lab extension list 2>&1 | grep -iE 'jupyter_server_proxy.*OK.*' + # we have installed a pre-built wheel and configured code coverage to + # inspect "jupyter_server_proxy", by re-locating to another directory, + # there is no confusion about "jupyter_server_proxy" referring to our + # installed package rather than the local directory - name: Run tests run: | - # we have installed a pre-built wheel and configured code coverage to - # inspect "jupyter_server_proxy", by re-locating to another directory, - # there is no confusion about "jupyter_server_proxy" referring to our - # installed package rather than the local directory mkdir build cd build pytest -c ../pyproject.toml ../tests @@ -173,7 +155,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: |- - tests-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.jupyterlab-version }}-${{ github.run_number }} + tests-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.pip-extras }}-${{ github.run_number }} path: | ./build/pytest ./build/coverage diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d78be2a9..020fc417 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,30 @@ the README.md file. ## Local development setup +### `conda` + +
+ + + +Optional, but especially recommended on non-Linux platforms... + + + +Using the `conda` (or `mamba` or `micromamba`) package manager with packages from +[`conda-forge`](https://conda-forge.org/feedstock-outputs) can help isolate development +environments on nearly any operating system and architecture. + +For example, after installing [`mambaforge`](https://conda-forge.org/miniforge), +create a new environment with all heavy development and test dependencies: + +```yaml +mamba create --name=jupyter-server-proxy --channel=conda-forge "python=3.11" "nodejs=20" pip git geckodriver firefox +mamba activate jupyter-server-proxy +``` + +
+ ### Python package ```bash @@ -14,14 +38,16 @@ the README.md file. git clone https://github.com/jupyterhub/jupyter-server-proxy.git # Change directory to the jupyter-server-proxy directory cd jupyter-server-proxy -# Install package in development mode -pip install -e ".[test]" -# Link your development version of the extension with JupyterLab -jupyter labextension develop --overwrite . +# Install package in development mode, with the latest Jupyter clients +pip install -e ".[test,lab]" +# Link your development version of the extension with JupyterLab and Notebook +jlpm labextension develop --overwrite . # Server extension must be manually installed in develop mode jupyter server extension enable jupyter_server_proxy ``` +## Testing + Run the tests: ```bash @@ -32,21 +58,32 @@ These generate test and coverage reports in `build/pytest` and `build/coverage`. ### Acceptance tests -If you have `robotframework-jupyterlibary` installed, the acceptance tests will run. +In `tests/acceptance`, a number of +[`.robot` files](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html) +emulate a user starting a Jupyter server, opening a real browser, clicking on +screen elements, and seeing several working proxy servers in supported Jupyter clients. -To install these in addition to the [Python package](#python-package) test -dependencies, run: +These tests are slower and more resource intensive than the unit and integration +tests, and generate additional screenshots, browser logs, server logs, and report +HTML in `build/robot`. -```bash -pip install -e ".[acceptance]" -``` +#### Extra browser dependencies -In addition, compatible versions of: +Compatible versions of [`geckodriver`](https://github.com/mozilla/geckodriver) +and [`firefox`](https://www.mozilla.org/en-US/firefox) need to be on `$PATH`. -- `geckodriver` -- `firefox` +These can be provisioned by [a `conda`-compatible installer](#conda), a system +package manager, or as a last resort, direct binary downloads. -Need to be on your `$PATH` and compatible with each other. +#### Acceptance test dependencies + +To install the additional dependencies beyond the [Python package](#python-package) +test dependencies, and run the tests against the latest Jupyter clients: + +```bash +pip install -e ".[test,acceptance,lab]" +pytest +``` To run _only_ the acceptance tests, use the `-k` switch: @@ -54,53 +91,138 @@ To run _only_ the acceptance tests, use the `-k` switch: pytest -k acceptance ``` -These are slower than the rest of the `pytest` tests, and generate screenshots, -browser logs, server logs, and report HTML in `build/robot`. +#### Older Jupyter Clients + +To run the acceptance tests against the previous major versions of Notebook +and JupyterLab, it is advisable to use a separate, isolated environment, testing the +as-built assets from `pyproject-build`. + +After creating and activating such an environment with `virtualenv` or [`conda`](#conda): + +```bash +pip install --find-links ./dist/ --no-index-url jupyter-server-proxy[test,acceptance,classic] +``` + +## Frontend Development + +To support a wide range of clients, both JupyterLab and Notebook Classic extensions +are built and distributed, each with their own quirks. + +### JupyterLab/Notebook extension + +The `./labextension/` directory contains the extension for the +[`lumino`](https://github.com/jupyterlab/lumino/)-based JupyterLab and Notebook +clients. + +#### `nodejs` + +Building this extension requires a compatible version of +[`nodejs`](https://nodejs.org/en/download/package-manager), with a supported, long +term support (LTS) release recommended. -### JupyterLab extension +#### `jlpm` -The `jlpm` command is JupyterLab's pinned version of `yarn` that is -installed with JupyterLab. +The `jlpm` command is a vendored, pinned version of the [`yarn`](https://yarnpkg.com) +package manager. Installed with JupyterLab, it performs commands such +as installing `npm` dependencies listed in `labextension/package.json`, building +and watching the extension from source, and formatting web-related source code files. -> You may use `yarn` or `npm run` instead of `jlpm` below. +#### The built Lab extension + +During a [`pyproject-build`](https://pypi.org/project/build/) +of the python package, a temporary JupyterLab and `jlpm` will be installed as part +of the `build-system`, executing roughly the commands: ```bash cd labextension # Change to the root of the labextension -jlpm # Install dependencies (or `npm i`) +jlpm # Install dependencies jlpm build:prod # Build: - # - `labextension/lib` - # - `jupyter_server_proxy/labextension` + # - `labextension/lib` with type checking + # - `jupyter_server_proxy/labextension` with minimization +``` + +During `pip install`, the built assets are copied to the user's +`{sys.prefix}/share/jupyter/labextensions/@jupyterhub/jupyter-server-proxy` to be +found by the application at startup. + +#### Developing the Lab extension + +For fine-grained access to the `jlpm` command and various build steps: + +```bash +pip install -e .[lab] # Ensure a compatible jlpm +cd labextension # Change to the root of the labextension +jlpm jlpm install:extension # Symlink into `{sys.prefix}/share/jupyter/labextensions` ``` -You can watch the source directory and automatically rebuild the `labextension/lib` +Watch the source directory and automatically rebuild the `labextension/lib` and `jupyter_server_proxy/labextension` folders: ```bash cd labextension # Watch the source directory in one terminal, automatically rebuilding when needed -jlpm run watch +jlpm watch # Run JupyterLab in another terminal jupyter lab ``` -With the watch command running, every saved change will immediately be built locally -and available in your running JupyterLab. Refresh JupyterLab to load the change in -your browser (you may need to wait several seconds for the extension to be rebuilt). +While running `jlpm watch`, every saved change to a `.ts` file will immediately be +built locally and available in your running Jupyter client. "Hard" refresh JupyterLab or Notebook +with CTRL-F5 or ⌘-F5 to load the change in your browser +(you may need to wait several seconds for the extension to be fully rebuilt). + +#### Source Maps + +By default, the `jlpm build` and `jlpm watch` commands generate +[source maps](https://firefox-source-docs.mozilla.org/devtools-user/debugger/how_to/use_a_source_map/) +for this extension to improve debugging using the browser development tools, +often revealed by pressing F12. -By default, the `jlpm run build` command generates the source maps for this -extension to make it easier to debug using the browser dev tools. To also generate -source maps for the JupyterLab core extensions, you can run the following command: +To also generate source maps for the JupyterLab core application, run the following command: ```bash jupyter lab build --minimize=False ``` -### Documentation +### Notebook Classic extension + +The files in `jupyter_server_proxy/static` extend the Notebook Classic application's +_Tree_ page. + +#### RequireJS + +The Notebook Classic extension uses the [`require.js`](https://requirejs.org) +dependency injection system, and presently uses no dependencies beyond what is +provided by Notebook Classic. + +#### The built Classic extension + +During a user's `pip install`, the static assets are copied to +`{sys.prefix}/share/jupyter/nbextensions/jupyter_server_proxy`, to be +found by the application at startup. + +#### Developing the Classic extension + +While this extension is served as-is once installed, for live development the +extension assets must be linked: + +```bash +pip install -e ".[classic]" +jupyter nbextension install --symlink --sys-prefix --py jupyter_server_proxy +``` + +After making changes, "hard" refresh the browser application, usually with +CTRL-F5 or ⌘-F5. + +## Documentation The documentation uses a fairly standard [Sphinx](https://www.sphinx-doc.org) build chain, and requires `make` on Linux/MacOS, which cannot be installed with -`pip`. +`pip` + +> `make` is available from [`conda-forge`](#conda) as `make` for Linux/OSX, and `m2-make` +> on Windows In addition to any system packages, building the documentation requires additional packages. To install the needed packages: @@ -132,3 +254,12 @@ make linkcheck ```bash make devenv ``` + +## Linting + +During continuous integration (CI) the `pre-commit` package is used to run a +number of checks, with each tool in a private virtual environment. If it is able to, +the CI bot will push to a PR with fixes. + +By installing `pre-commit` with `pip` or `conda`, you can have this same experience, +or inspect the configuration to try to recreate it yourself locally. diff --git a/README.md b/README.md index af6252e8..bfdebc8e 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,6 @@ Server via the container. ## Install -### Requirements - -Either `jupyterlab>=3` or `notebook` is required. - ### Python package #### `pip` @@ -67,17 +63,27 @@ pip install jupyter-server-proxy #### `conda` ```bash -conda install jupyter-server-proxy -c conda-forge +conda install -c conda-forge jupyter-server-proxy ``` -### JupyterLab extension +### Jupyter Client Extensions + +A JupyterLab and Notebook extension is bundled with the Python package to +provide: + +- servers in the _New_ dropwdown of the Notebook Tree view +- launch buttons in JupyterLab's Launcher panel for registered server processes. + - ![a screenshot of the JupyterLab Launcher](docs/source/_static/images/labextension-launcher.png "launch proxied servers as JupyterLab panels or new browser tabs") -A JupyterLab extension is bundled with the Python package to provide launch -buttons in JupyterLab's Launcher panel for registered server processes. +#### Client compatibility -![](docs/source/_static/images/labextension-launcher.png) +For historical compatibility ranges, see the table below: -Clicking on them opens the proxied application in a new browser window. +| `jupyter-server-proxy` | `notebook` | `jupyterlab` | +| :--------------------: | :--------: | :----------: | +| `4.1.x` | `>=6,<8` | `>=3,<5` | +| `4.0.x` | `>=6,<7` | `>=3,<4` | +| `3.x` | `>=6,<7` | `>=2,<4` | ## Disable @@ -88,7 +94,7 @@ jupyter serverextension disable --sys-prefix jupyter_server_proxy jupyter server extension disable --sys-prefix jupyter_server_proxy ``` -### Notebook classic extension +### Notebook Classic extension ```bash jupyter nbextension disable --sys-prefix --py jupyter_server_proxy diff --git a/labextension/README.md b/labextension/README.md index 75074e28..6f002f9b 100644 --- a/labextension/README.md +++ b/labextension/README.md @@ -1,7 +1,8 @@ # `@jupyterhub/jupyter-server-proxy` -A JupyterLab extension that adds items to the JupyterLab [Launcher] representing the -configured server processes managed by the python package `jupyter-server-proxy` (required). +A pre-built JupyterLab extension that adds items to the JupyterLab [Launcher] +to open server processes managed by the python package +[`jupyter-server-proxy`](https://pypi.org/project/jupyter-server-proxy). [launcher]: https://jupyterlab.readthedocs.io/en/stable/extension/extension_points.html#launcher @@ -20,9 +21,12 @@ pip install jupyter-server-proxy or ```bash -conda install jupyter-server-proxy +conda install -c conda-forge jupyter-server-proxy ``` > As a _prebuilt_ extension, it will "just work," only a simple page reload should be required > to see launcher items. However, a full restart of `jupyter_server` or `notebook` is required > to reload the `jupyter_server_proxy` serverextension which provides most of the functionality. + +For a full development and testing installation, see the +[contributing guide](https://github.com/jupyterhub/jupyter-server-proxy/blob/main/CONTRIBUTING.md). diff --git a/labextension/src/index.ts b/labextension/src/index.ts index 9a8228f2..e706d5a8 100644 --- a/labextension/src/index.ts +++ b/labextension/src/index.ts @@ -1,91 +1,125 @@ -import type { Widget, Menu } from "@lumino/widgets"; +/** + * The main extension file, which provides plugins that can be loaded into + * a Lumino application such as JupyterLab or Notebook 7. + * + * Outside of this file, of note is `./tokens.ts`, which provides a number of + * run-time constants and compile-type interfaces for type-checking. + * + * Imports (mostly) adhere to the ES `import` semantics, exceptions noted below + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import + */ + +// this is a type-only import for low-level components of the UI +// @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html +import type { + // the base class for all on-screen components + Widget, + // the Lumino component for menu bars + Menu, +} from "@lumino/widgets"; import { + // the dependency-injection (DI) Token required to request a dependency for... + ILayoutRestorer, + // any lumino-based Jupyter application... JupyterFrontEnd, + // to be extended with... JupyterFrontEndPlugin, - ILayoutRestorer, } from "@jupyterlab/application"; import { + // a DI token for the toolbar, used to extend Notebook's Tree IToolbarWidgetRegistry, + // the concrete class used to create iframes in the JupyterLab main area IFrame, + // a wrapper for the iframe that handles house-keeping boilerplate MainAreaWidget, + // a tracker which handles iframe placement housekeeping when JupyterLab is reloaded WidgetTracker, } from "@jupyterlab/apputils"; +// utilities for working with URLs and the `jupyter-config-data` script import { PageConfig, URLExt } from "@jupyterlab/coreutils"; +// a DI token for the file browser, present in JupyterLab and Notebook 7 import { IDefaultFileBrowser } from "@jupyterlab/filebrowser"; +// a DI token for the card-based Launcher, present in JupyterLab import { ILauncher } from "@jupyterlab/launcher"; +// the application-wide configuration for making HTTP requests with headers, etc. import { ServerConnection } from "@jupyterlab/services"; +// local imports from `tokens.ts` for immutable constants import { CommandIDs, IOpenArgs, IServerProcess, IServersInfo, + NAME, NS, argSchema, + sandbox, } from "./tokens"; -// top level constants that won't change during the application lifecycle +/* + * top level constants that won't change during the application lifecycle + */ const baseUrl = PageConfig.getBaseUrl(); +/** + * The Notebook 7 sub-application: `tree`, `notebook`, `editor`, `terminal`, etc. + */ const notebookPage = PageConfig.getOption("notebookPage"); +/** + * Whether the current application is `/tree`: otherwise we don't do anything + */ +const isTree = notebookPage === "tree"; +/** + * Whether this is a notebook app at all + */ const isNotebook7 = !!notebookPage; -const isTree = isNotebook7 && notebookPage === "tree"; -/** Create a new iframe widget. */ -function newServerProxyWidget( - id: string, - url: string, - text: string, -): MainAreaWidget