From 2bdc8c9df214f111ca6628e157dd2061312b41fc Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Sun, 8 Feb 2026 16:50:31 +0100 Subject: [PATCH 1/8] Copy binder config from jupyter_collaboration. --- .github/workflows/binder-badge.yml | 14 ++++++++++++++ binder/environment.yml | 20 ++++++++++++++++++++ binder/jupyter_config.py | 9 +++++++++ binder/postBuild | 26 ++++++++++++++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 .github/workflows/binder-badge.yml create mode 100644 binder/environment.yml create mode 100644 binder/jupyter_config.py create mode 100755 binder/postBuild diff --git a/.github/workflows/binder-badge.yml b/.github/workflows/binder-badge.yml new file mode 100644 index 00000000..87e9cd29 --- /dev/null +++ b/.github/workflows/binder-badge.yml @@ -0,0 +1,14 @@ +name: Binder Badge +on: + pull_request_target: + types: [opened] + +jobs: + binder: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: jupyterlab/maintainer-tools/.github/actions/binder-link@v1 + with: + github_token: ${{ secrets.github_token }} diff --git a/binder/environment.yml b/binder/environment.yml new file mode 100644 index 00000000..d5fc8eb1 --- /dev/null +++ b/binder/environment.yml @@ -0,0 +1,20 @@ +name: example-environment +channels: + - conda-forge + - nodefaults +dependencies: + - jupyterlab >=4.0.0 + - nodejs >=25,<26 + - python >=3.11,<3.12 + - yarn >=3,<4 + # build + - hatchling >=1.5.0 + - hatch-jupyter-builder >=0.3.2 + - hatch-nodejs-version + # dependencies + - jupyter_server >=2.0.0 + # Use pip to get the the latest version + - pip: + - jupyter_server_fileid >=0.7.0 + - jupyter_ydoc >=1.0.0 + - ypy-websocket >=0.12.0 diff --git a/binder/jupyter_config.py b/binder/jupyter_config.py new file mode 100644 index 00000000..e5cb76ba --- /dev/null +++ b/binder/jupyter_config.py @@ -0,0 +1,9 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. +import logging + +c.ServerApp.log_level = logging.DEBUG + +c.ContentsManager.allow_hidden = True +# Use advance file ID service for out of band rename support +c.FileIdExtension.file_id_manager_class = "jupyter_server_fileid.manager.LocalFileIdManager" diff --git a/binder/postBuild b/binder/postBuild new file mode 100755 index 00000000..aaa4e964 --- /dev/null +++ b/binder/postBuild @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +source activate ${NB_PYTHON_PREFIX} + +set -euxo pipefail + +yarn || yarn + +python -m pip install -vv -e . --no-build-isolation +jupyter server extension enable nbdime + +mkdir -p ~/.jupyter/ + +cp binder/jupyter_config.py ~/.jupyter/ + +# FIXME until jupyter-server is the default on binder +cp ${NB_PYTHON_PREFIX}/bin/jupyter-lab ${NB_PYTHON_PREFIX}/bin/jupyter-notebook + +jupyter troubleshoot +jupyter notebook --show-config +jupyter lab --show-config +jupyter labextension list +jupyter server extension list From 6034a959519456140c4ff439ee47786d53ce9adc Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Tue, 10 Feb 2026 09:40:34 +0100 Subject: [PATCH 2/8] cleanup --- binder/environment.yml | 7 +------ binder/jupyter_config.py | 2 -- binder/postBuild | 3 --- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/binder/environment.yml b/binder/environment.yml index d5fc8eb1..f38e4f92 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -5,7 +5,7 @@ channels: dependencies: - jupyterlab >=4.0.0 - nodejs >=25,<26 - - python >=3.11,<3.12 + - python >=3.14 - yarn >=3,<4 # build - hatchling >=1.5.0 @@ -13,8 +13,3 @@ dependencies: - hatch-nodejs-version # dependencies - jupyter_server >=2.0.0 - # Use pip to get the the latest version - - pip: - - jupyter_server_fileid >=0.7.0 - - jupyter_ydoc >=1.0.0 - - ypy-websocket >=0.12.0 diff --git a/binder/jupyter_config.py b/binder/jupyter_config.py index e5cb76ba..9720647c 100644 --- a/binder/jupyter_config.py +++ b/binder/jupyter_config.py @@ -5,5 +5,3 @@ c.ServerApp.log_level = logging.DEBUG c.ContentsManager.allow_hidden = True -# Use advance file ID service for out of band rename support -c.FileIdExtension.file_id_manager_class = "jupyter_server_fileid.manager.LocalFileIdManager" diff --git a/binder/postBuild b/binder/postBuild index aaa4e964..cb36ec80 100755 --- a/binder/postBuild +++ b/binder/postBuild @@ -16,9 +16,6 @@ mkdir -p ~/.jupyter/ cp binder/jupyter_config.py ~/.jupyter/ -# FIXME until jupyter-server is the default on binder -cp ${NB_PYTHON_PREFIX}/bin/jupyter-lab ${NB_PYTHON_PREFIX}/bin/jupyter-notebook - jupyter troubleshoot jupyter notebook --show-config jupyter lab --show-config From baa56f0efd388e74f1b477badb239ea68f461a07 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Tue, 10 Feb 2026 10:01:03 +0100 Subject: [PATCH 3/8] setuptools --- binder/environment.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/binder/environment.yml b/binder/environment.yml index f38e4f92..d0797290 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -11,5 +11,7 @@ dependencies: - hatchling >=1.5.0 - hatch-jupyter-builder >=0.3.2 - hatch-nodejs-version + # setuptools 82 removed pkg_resources which is needed by pytest_tornado + - setuptools < 82.0.0 # dependencies - jupyter_server >=2.0.0 From 3d9498b3083aa91e5b6fa26e83d57a5dc3e2a76f Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Tue, 10 Feb 2026 10:30:55 +0100 Subject: [PATCH 4/8] debug --- .github/workflows/tests.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 05bc3bfd..3d3b368a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}-${{hashFiles('**/requirements.txt')}} restore-keys: | ${{ runner.os }}-pip- - - name: Install dependencies + - name: Install dependencies (docs) run: | sudo apt-get install -y pandoc python -m pip install --upgrade pip @@ -49,7 +49,7 @@ jobs: key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}-${{hashFiles('**/requirements.txt')}} restore-keys: | ${{ runner.os }}-pip- - - name: Install dependencies + - name: Install dependencies (js) run: | python -m pip install --upgrade pip @@ -116,9 +116,9 @@ jobs: key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}-${{hashFiles('**/requirements.txt')}} restore-keys: | ${{ runner.os }}-pip- - - name: Install dependencies + - name: Install dependencies (pytest) run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip 'setuptools<82' python -m pip install jupyterlab~=3.0 python -m pip install --upgrade --upgrade-strategy=eager ".[test]" python -m pip install jupyter_server${{ matrix.jupyter_server-version }} @@ -213,11 +213,11 @@ jobs: path: | ${{ github.workspace }}/pw-browsers key: ${{ runner.os }}-${{ hashFiles('ui-tests/package-lock.json') }} - + - name: Install browser working-directory: ui-tests run: npx playwright install chromium - + - name: Run playwright tests working-directory: ui-tests run: | From b40a1f46653183e8e6cfb001fbd534975b71d49e Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Tue, 10 Feb 2026 10:54:16 +0100 Subject: [PATCH 5/8] re-pin setuptools --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3d3b368a..a2b960a8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -122,6 +122,7 @@ jobs: python -m pip install jupyterlab~=3.0 python -m pip install --upgrade --upgrade-strategy=eager ".[test]" python -m pip install jupyter_server${{ matrix.jupyter_server-version }} + python -m pip install 'setuptools<82' - name: Test with pytest (Linux) if: startsWith(matrix.os, 'ubuntu') run: | From 83f348d04d2b78d734571e6176551e42225178c2 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Tue, 10 Feb 2026 11:38:25 +0100 Subject: [PATCH 6/8] try to get rid of pytest-tornado (issue with setuptools 82+) --- binder/environment.yml | 2 -- nbdime/tests/conftest.py | 31 ++++++++++++++++++++-------- nbdime/tests/test_web.py | 44 +++++++++++++++++----------------------- pyproject.toml | 1 - 4 files changed, 42 insertions(+), 36 deletions(-) diff --git a/binder/environment.yml b/binder/environment.yml index d0797290..f38e4f92 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -11,7 +11,5 @@ dependencies: - hatchling >=1.5.0 - hatch-jupyter-builder >=0.3.2 - hatch-nodejs-version - # setuptools 82 removed pkg_resources which is needed by pytest_tornado - - setuptools < 82.0.0 # dependencies - jupyter_server >=2.0.0 diff --git a/nbdime/tests/conftest.py b/nbdime/tests/conftest.py index aae7374e..93d7df3e 100644 --- a/nbdime/tests/conftest.py +++ b/nbdime/tests/conftest.py @@ -10,6 +10,7 @@ import glob import io import re +import threading from collections.abc import Sequence from subprocess import Popen, TimeoutExpired import sys @@ -220,18 +221,32 @@ def nbdime_base_url(): @fixture -def app(nbdime_base_url, filespath): - """This is a fixture used by the pytest-tornado plugin. - - It is indirectly called by all tests that use the `gen_test` - test mark. - """ +def web_server(nbdime_base_url, filespath): + """Start a Tornado web server in a background thread and yield the base URL.""" + from tornado import ioloop from nbdime.webapp.nbdimeserver import init_app - return init_app( + + port_holder = {} + + def on_port(port): + port_holder['port'] = port + + app, server = init_app( + on_port=on_port, base_url=nbdime_base_url, port=0, cwd=filespath, - )[0] + ) + loop = ioloop.IOLoop.current() + thread = threading.Thread(target=loop.start, daemon=True) + thread.start() + + base_url = 'http://localhost:%d' % port_holder['port'] + yield base_url + + server.stop() + loop.add_callback(loop.stop) + thread.join(timeout=5) @fixture diff --git a/nbdime/tests/test_web.py b/nbdime/tests/test_web.py index 9ebd5fbb..a32cdf1c 100644 --- a/nbdime/tests/test_web.py +++ b/nbdime/tests/test_web.py @@ -7,9 +7,9 @@ import json import pytest +import requests from tornado import ioloop from tornado.httputil import url_concat -from tornado.escape import json_encode, json_decode import nbformat import nbdime.webapp.nbdiffweb @@ -73,26 +73,23 @@ def test_merge_web(filespath, unique_port, reset_log, ioloop_patch): @pytest.mark.timeout(timeout=WEB_TEST_TIMEOUT) -@pytest.mark.gen_test -def test_fetch_diff(http_client, base_url, nbdime_base_url): +def test_fetch_diff(web_server, nbdime_base_url): url = url_concat( - base_url + nbdime_base_url + '/diff', + web_server + nbdime_base_url + '/diff', dict(base=diff_a, remote=diff_b)) - response = yield http_client.fetch(url) - assert response.code == 200 + response = requests.get(url) + assert response.status_code == 200 @pytest.mark.timeout(timeout=WEB_TEST_TIMEOUT) -@pytest.mark.gen_test -def test_api_diff(http_client, base_url, nbdime_base_url, diff_validator, filespath, auth_header): +def test_api_diff(web_server, nbdime_base_url, diff_validator, filespath, auth_header): post_data = dict(base=diff_a, remote=diff_b) - body = json_encode(post_data) - url = base_url + nbdime_base_url + '/api/diff' - response = yield http_client.fetch(url, method='POST', headers=auth_header, body=body) - assert response.code == 200 + url = web_server + nbdime_base_url + '/api/diff' + response = requests.post(url, json=post_data, headers=auth_header) + assert response.status_code == 200 # Check that response is sane: - data = json_decode(response.body) + data = response.json() # Check that base is as expected: expected_base = nbformat.read(os.path.join(filespath, diff_a), as_version=4) assert json.dumps(data['base'], sort_keys=True) == json.dumps(expected_base, sort_keys=True) @@ -101,26 +98,23 @@ def test_api_diff(http_client, base_url, nbdime_base_url, diff_validator, filesp @pytest.mark.timeout(timeout=WEB_TEST_TIMEOUT) -@pytest.mark.gen_test -def test_fetch_merge(http_client, base_url, nbdime_base_url): +def test_fetch_merge(web_server, nbdime_base_url): url = url_concat( - base_url + nbdime_base_url + '/merge', + web_server + nbdime_base_url + '/merge', dict(base=merge_a, local=merge_b, remote=merge_c)) - response = yield http_client.fetch(url) - assert response.code == 200 + response = requests.get(url) + assert response.status_code == 200 @pytest.mark.timeout(timeout=WEB_TEST_TIMEOUT) -@pytest.mark.gen_test -def test_api_merge(http_client, base_url, nbdime_base_url, merge_validator, filespath, auth_header): +def test_api_merge(web_server, nbdime_base_url, merge_validator, filespath, auth_header): post_data = dict(base=merge_a, local=merge_b, remote=merge_c) - body = json_encode(post_data) - url = base_url + nbdime_base_url + '/api/merge' - response = yield http_client.fetch(url, method='POST', headers=auth_header, body=body) - assert response.code == 200 + url = web_server + nbdime_base_url + '/api/merge' + response = requests.post(url, json=post_data, headers=auth_header) + assert response.status_code == 200 # Check that response is sane: - data = json_decode(response.body) + data = response.json() # Check that base is as expected: expected_base = nbformat.read(os.path.join(filespath, merge_a), as_version=4) assert json.dumps(data['base'], sort_keys=True) == json.dumps(expected_base, sort_keys=True) diff --git a/pyproject.toml b/pyproject.toml index 73ab6069..f8ed0cdf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,6 @@ test = [ "pytest>=6.0", "pytest-cov", "pytest-timeout", - "pytest-tornado", "jupyter_server[test]", "jsonschema", "mock", From 5719bc7c008e2624d11f7def7dc74a0eeef5208e Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Tue, 10 Feb 2026 11:40:49 +0100 Subject: [PATCH 7/8] try another postbuild --- binder/postBuild | 63 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/binder/postBuild b/binder/postBuild index cb36ec80..87e1a1b1 100755 --- a/binder/postBuild +++ b/binder/postBuild @@ -1,23 +1,56 @@ -#!/usr/bin/env bash +#!/usr/bin/env python3 +""" perform a development install of nbdime -# Copyright (c) Jupyter Development Team. -# Distributed under the terms of the Modified BSD License. + On Binder, this will run _after_ the environment has been fully created from + the environment.yml in this directory. -source activate ${NB_PYTHON_PREFIX} + This script should also run locally on Linux/MacOS/Windows: -set -euxo pipefail + python3 binder/postBuild +""" +import subprocess +import sys +from pathlib import Path -yarn || yarn -python -m pip install -vv -e . --no-build-isolation -jupyter server extension enable nbdime +ROOT = Path.cwd() -mkdir -p ~/.jupyter/ +def _(*args, **kwargs): + """ Run a command, echoing the args -cp binder/jupyter_config.py ~/.jupyter/ + fails hard if something goes wrong + """ + print("\n\t", " ".join(args), "\n") + return_code = subprocess.call(args, **kwargs) + if return_code != 0: + print("\nERROR", return_code, " ".join(args)) + sys.exit(return_code) -jupyter troubleshoot -jupyter notebook --show-config -jupyter lab --show-config -jupyter labextension list -jupyter server extension list +# verify the environment is self-consistent before even starting +_(sys.executable, "-m", "pip", "check") + +# install the labextension +_(sys.executable, "-m", "pip", "install", "-e", ".") +_(sys.executable, "-m", "jupyter", "labextension", "develop", "--overwrite", ".") +_( + sys.executable, + "-m", + "jupyter", + "server", + "extension", + "enable", + "{{ python_name }}", +) + +# verify the environment the extension didn't break anything +_(sys.executable, "-m", "pip", "check") + +# list the extensions +_("jupyter", "server", "extension", "list") + +# initially list installed extensions to determine if there are any surprises +_("jupyter", "labextension", "list") + + +print("JupyterLab with nbdime is ready to run with:\n") +print("\tjupyter lab\n") From 9680a5d4b74e27d0202073368d9deabf2ae2d72b Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Tue, 10 Feb 2026 11:44:04 +0100 Subject: [PATCH 8/8] python to bash --- binder/postBuild | 81 ++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 48 deletions(-) diff --git a/binder/postBuild b/binder/postBuild index 87e1a1b1..5a713a98 100755 --- a/binder/postBuild +++ b/binder/postBuild @@ -1,56 +1,41 @@ -#!/usr/bin/env python3 -""" perform a development install of nbdime - - On Binder, this will run _after_ the environment has been fully created from - the environment.yml in this directory. - - This script should also run locally on Linux/MacOS/Windows: - - python3 binder/postBuild -""" -import subprocess -import sys -from pathlib import Path - - -ROOT = Path.cwd() - -def _(*args, **kwargs): - """ Run a command, echoing the args - - fails hard if something goes wrong - """ - print("\n\t", " ".join(args), "\n") - return_code = subprocess.call(args, **kwargs) - if return_code != 0: - print("\nERROR", return_code, " ".join(args)) - sys.exit(return_code) +#!/usr/bin/env bash +# perform a development install of nbdime +# +# On Binder, this will run _after_ the environment has been fully created from +# the environment.yml in this directory. +# +# This script should also run locally on Linux/MacOS: +# +# bash binder/postBuild + +set -euox pipefail # verify the environment is self-consistent before even starting -_(sys.executable, "-m", "pip", "check") +pip check # install the labextension -_(sys.executable, "-m", "pip", "install", "-e", ".") -_(sys.executable, "-m", "jupyter", "labextension", "develop", "--overwrite", ".") -_( - sys.executable, - "-m", - "jupyter", - "server", - "extension", - "enable", - "{{ python_name }}", -) - -# verify the environment the extension didn't break anything -_(sys.executable, "-m", "pip", "check") +pip install -e . +jupyter labextension develop --overwrite . +jupyter server extension enable nbdime -# list the extensions -_("jupyter", "server", "extension", "list") +# +mkdir -p ~/.jupyter/ -# initially list installed extensions to determine if there are any surprises -_("jupyter", "labextension", "list") +cp binder/jupyter_config.py ~/.jupyter/ +# verify the extension didn't break anything +pip check -print("JupyterLab with nbdime is ready to run with:\n") -print("\tjupyter lab\n") +# list the extensions +jupyter troubleshoot +jupyter notebook --show-config +jupyter lab --show-config +jupyter server extension list +jupyter labextension list + + +echo "" +echo "JupyterLab with nbdime is ready to run with:" +echo "" +echo " jupyter lab" +echo ""