diff --git a/.binder/environment.yml b/.binder/environment.yml index e4195d0..12ea5e2 100644 --- a/.binder/environment.yml +++ b/.binder/environment.yml @@ -3,58 +3,57 @@ name: jupyterlab-deck-demo channels: - conda-forge - nodefaults + - conda-forge/label/jupyterlab_fonts_alpha + - conda-forge/label/jupyterlite_core_rc + - conda-forge/label/jupyterlite_pyodide_kernel_alpha dependencies: - - python >=3.10,<3.11 + - python >=3.11,<3.12 ### environment-base.yml ### - doit-with-toml - - ipywidgets >=8 - - jupyterlab >=3.4.8,<4 - - jupyterlab-fonts >=2.1.1 + - ipywidgets >=7 + - jupyterlab >=3.5,<5.0.0a0 + - jupyterlab-fonts >=3.0.0a3 + - notebook >=6.5,<8.0.0a0 - pip - - python >=3.7,<3.11 + - python >=3.8,<3.13 + - python-dotenv ### environment-base.yml ### ### environment-build.yml ### # runtimes - - nodejs >=16,<17 + - nodejs >=20,<21 # host app - - ipywidgets >=8 + - ipywidgets >=7 # build - - flit >=3.7.1 + - flit >=3.9.0,<4.0.0 - twine ### environment-build.yml ### ### environment-lint.yml ### # formatters - black - - isort - ssort - - docformatter + - ruff - robotframework-tidy >=3.3 # linters - robotframework-robocop >=2.6 - - pyflakes ### environment-lint.yml ### ### environment-docs.yml ### - # demo - - ipydrawio - - jupyter-videochat - - jupyterlab-myst - - jupyterlab-webrtc-docprovider # docs - docutils >=0.18 + - mdit-py-plugins <0.4.0 + - myst-nb - pydata-sphinx-theme - sphinx >=5.1,<6 - sphinx-autobuild - sphinx-copybutton - - myst-nb # check - hunspell - hunspell-en - pytest-check-links - # lite cruft - - pkginfo - - pip: - - jupyterlite ==0.1.0b14 + # lite + - python-libarchive-c + - jupyterlite-core ==0.2.0rc1 + - jupyterlite-pyodide-kernel ==0.2.0a2 ### environment-docs.yml ### ### environment-test.yml ### # test @@ -62,11 +61,11 @@ dependencies: - pytest-html ### environment-test.yml ### ### environment-robot.yml ### - - robotframework >=6 + - robotframework >=6.1 - robotframework-pabot # browser - - firefox + - firefox 115.* - geckodriver - - robotframework-jupyterlibrary >=0.4.1 + - robotframework-jupyterlibrary >=0.5.0 - lxml ### environment-robot.yml ### diff --git a/.github/environment-base.yml b/.github/environment-base.yml index d9d64f7..891f6e1 100644 --- a/.github/environment-base.yml +++ b/.github/environment-base.yml @@ -1,9 +1,11 @@ dependencies: ### environment-base.yml ### - doit-with-toml - - ipywidgets >=8 - - jupyterlab >=3.4.8,<4 - - jupyterlab-fonts >=2.1.1 + - ipywidgets >=7 + - jupyterlab >=3.5,<5.0.0a0 + - jupyterlab-fonts >=3.0.0a3 + - notebook >=6.5,<8.0.0a0 - pip - - python >=3.7,<3.11 + - python >=3.8,<3.13 + - python-dotenv ### environment-base.yml ### diff --git a/.github/environment-build.yml b/.github/environment-build.yml index ab4ba6b..888177e 100644 --- a/.github/environment-build.yml +++ b/.github/environment-build.yml @@ -3,23 +3,28 @@ name: jupyterlab-deck-test channels: - conda-forge - nodefaults + - conda-forge/label/jupyterlab_fonts_alpha + - conda-forge/label/jupyterlite_core_rc + - conda-forge/label/jupyterlite_pyodide_kernel_alpha dependencies: - - python >=3.10,<3.11 + - python >=3.10,<3.13 ### environment-base.yml ### - doit-with-toml - - ipywidgets >=8 - - jupyterlab >=3.4.8,<4 - - jupyterlab-fonts >=2.1.1 + - ipywidgets >=7 + - jupyterlab >=3.5,<5.0.0a0 + - jupyterlab-fonts >=3.0.0a3 + - notebook >=6.5,<8.0.0a0 - pip - - python >=3.7,<3.11 + - python >=3.8,<3.13 + - python-dotenv ### environment-base.yml ### ### environment-build.yml ### # runtimes - - nodejs >=16,<17 + - nodejs >=20,<21 # host app - - ipywidgets >=8 + - ipywidgets >=7 # build - - flit >=3.7.1 + - flit >=3.9.0,<4.0.0 - twine ### environment-build.yml ### diff --git a/.github/environment-docs.yml b/.github/environment-docs.yml index 2443064..0bb6205 100644 --- a/.github/environment-docs.yml +++ b/.github/environment-docs.yml @@ -3,45 +3,46 @@ name: jupyterlab-deck-docs channels: - conda-forge - nodefaults + - conda-forge/label/jupyterlab_fonts_alpha + - conda-forge/label/jupyterlite_core_rc + - conda-forge/label/jupyterlite_pyodide_kernel_alpha dependencies: - - python >=3.10,<3.11 + - python >=3.10,<3.13 ### environment-base.yml ### - doit-with-toml - - ipywidgets >=8 - - jupyterlab >=3.4.8,<4 - - jupyterlab-fonts >=2.1.1 + - ipywidgets >=7 + - jupyterlab >=3.5,<5.0.0a0 + - jupyterlab-fonts >=3.0.0a3 + - notebook >=6.5,<8.0.0a0 - pip - - python >=3.7,<3.11 + - python >=3.8,<3.13 + - python-dotenv ### environment-base.yml ### ### environment-build.yml ### # runtimes - - nodejs >=16,<17 + - nodejs >=20,<21 # host app - - ipywidgets >=8 + - ipywidgets >=7 # build - - flit >=3.7.1 + - flit >=3.9.0,<4.0.0 - twine ### environment-build.yml ### ### environment-docs.yml ### - # demo - - ipydrawio - - jupyter-videochat - - jupyterlab-myst - - jupyterlab-webrtc-docprovider # docs - docutils >=0.18 + - mdit-py-plugins <0.4.0 + - myst-nb - pydata-sphinx-theme - sphinx >=5.1,<6 - sphinx-autobuild - sphinx-copybutton - - myst-nb # check - hunspell - hunspell-en - pytest-check-links - # lite cruft - - pkginfo - - pip: - - jupyterlite ==0.1.0b14 + # lite + - python-libarchive-c + - jupyterlite-core ==0.2.0rc1 + - jupyterlite-pyodide-kernel ==0.2.0a2 ### environment-docs.yml ### diff --git a/.github/environment-lint.yml b/.github/environment-lint.yml index 2b948a1..89d5fec 100644 --- a/.github/environment-lint.yml +++ b/.github/environment-lint.yml @@ -3,43 +3,46 @@ name: jupyterlab-deck-lint channels: - conda-forge - nodefaults + - conda-forge/label/jupyterlab_fonts_alpha + - conda-forge/label/jupyterlite_core_rc + - conda-forge/label/jupyterlite_pyodide_kernel_alpha dependencies: - - python >=3.10,<3.11 + - python >=3.10,<3.13 ### environment-base.yml ### - doit-with-toml - - ipywidgets >=8 - - jupyterlab >=3.4.8,<4 - - jupyterlab-fonts >=2.1.1 + - ipywidgets >=7 + - jupyterlab >=3.5,<5.0.0a0 + - jupyterlab-fonts >=3.0.0a3 + - notebook >=6.5,<8.0.0a0 - pip - - python >=3.7,<3.11 + - python >=3.8,<3.13 + - python-dotenv ### environment-base.yml ### ### environment-build.yml ### # runtimes - - nodejs >=16,<17 + - nodejs >=20,<21 # host app - - ipywidgets >=8 + - ipywidgets >=7 # build - - flit >=3.7.1 + - flit >=3.9.0,<4.0.0 - twine ### environment-build.yml ### ### environment-lint.yml ### # formatters - black - - isort - ssort - - docformatter + - ruff - robotframework-tidy >=3.3 # linters - robotframework-robocop >=2.6 - - pyflakes ### environment-lint.yml ### ### environment-robot.yml ### - - robotframework >=6 + - robotframework >=6.1 - robotframework-pabot # browser - - firefox + - firefox 115.* - geckodriver - - robotframework-jupyterlibrary >=0.4.1 + - robotframework-jupyterlibrary >=0.5.0 - lxml ### environment-robot.yml ### diff --git a/.github/environment-robot.yml b/.github/environment-robot.yml index 6512b3f..131db57 100644 --- a/.github/environment-robot.yml +++ b/.github/environment-robot.yml @@ -1,10 +1,10 @@ dependencies: ### environment-robot.yml ### - - robotframework >=6 + - robotframework >=6.1 - robotframework-pabot # browser - - firefox + - firefox 115.* - geckodriver - - robotframework-jupyterlibrary >=0.4.1 + - robotframework-jupyterlibrary >=0.5.0 - lxml ### environment-robot.yml ### diff --git a/.github/environment-test-lab35.yml b/.github/environment-test-lab35.yml new file mode 100644 index 0000000..3ec62e3 --- /dev/null +++ b/.github/environment-test-lab35.yml @@ -0,0 +1,37 @@ +name: jupyterlab-deck-test-35 + +channels: + - conda-forge + - nodefaults + - conda-forge/label/jupyterlab_fonts_alpha + - conda-forge/label/jupyterlite_core_rc + - conda-forge/label/jupyterlite_pyodide_kernel_alpha + +dependencies: + # a more precise python pin is added in CI + - jupyterlab >=3.5,<3.6.0a0 + - notebook <7.0.0a0 + ### environment-base.yml ### + - doit-with-toml + - ipywidgets >=7 + - jupyterlab >=3.5,<5.0.0a0 + - jupyterlab-fonts >=3.0.0a3 + - notebook >=6.5,<8.0.0a0 + - pip + - python >=3.8,<3.13 + - python-dotenv + ### environment-base.yml ### + ### environment-test.yml ### + # test + - pytest-cov + - pytest-html + ### environment-test.yml ### + ### environment-robot.yml ### + - robotframework >=6.1 + - robotframework-pabot + # browser + - firefox 115.* + - geckodriver + - robotframework-jupyterlibrary >=0.5.0 + - lxml + ### environment-robot.yml ### diff --git a/.github/environment-test.yml b/.github/environment-test.yml index 9edc47e..83a0174 100644 --- a/.github/environment-test.yml +++ b/.github/environment-test.yml @@ -3,24 +3,29 @@ name: jupyterlab-deck-test channels: - conda-forge - nodefaults + - conda-forge/label/jupyterlab_fonts_alpha + - conda-forge/label/jupyterlite_core_rc + - conda-forge/label/jupyterlite_pyodide_kernel_alpha dependencies: # a more precise python pin is added in CI ### environment-base.yml ### - doit-with-toml - - ipywidgets >=8 - - jupyterlab >=3.4.8,<4 - - jupyterlab-fonts >=2.1.1 + - ipywidgets >=7 + - jupyterlab >=3.5,<5.0.0a0 + - jupyterlab-fonts >=3.0.0a3 + - notebook >=6.5,<8.0.0a0 - pip - - python >=3.7,<3.11 + - python >=3.8,<3.13 + - python-dotenv ### environment-base.yml ### ### environment-build.yml ### # runtimes - - nodejs >=16,<17 + - nodejs >=20,<21 # host app - - ipywidgets >=8 + - ipywidgets >=7 # build - - flit >=3.7.1 + - flit >=3.9.0,<4.0.0 - twine ### environment-build.yml ### ### environment-test.yml ### @@ -29,11 +34,11 @@ dependencies: - pytest-html ### environment-test.yml ### ### environment-robot.yml ### - - robotframework >=6 + - robotframework >=6.1 - robotframework-pabot # browser - - firefox + - firefox 115.* - geckodriver - - robotframework-jupyterlibrary >=0.4.1 + - robotframework-jupyterlibrary >=0.5.0 - lxml ### environment-robot.yml ### diff --git a/.github/requirements-build.txt b/.github/requirements-build.txt index 107e3f4..587173c 100644 --- a/.github/requirements-build.txt +++ b/.github/requirements-build.txt @@ -1,5 +1,6 @@ black -doit +doit[toml] flit -jupyterlab ==3.* +jupyterlab >=4.0.7,<5 pip +ruff diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07209ed..d984874 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - '*' + workflow_dispatch: env: PYTHONUNBUFFERED: '1' @@ -15,7 +16,7 @@ env: # our stuff ROBOT_RETRIES: '3' - CACHE_EPOCH: '1' + CACHE_EPOCH: '4' DOIT_N_BUILD: '-n4' PABOT_PROCESSES: '3' @@ -26,14 +27,14 @@ jobs: strategy: matrix: os: [ubuntu] - python-version: ['3.10'] + python-version: ['3.11'] defaults: run: shell: bash -l {0} env: BUILDING_IN_CI: '1' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # configure builtin providers - name: setup (python) @@ -45,7 +46,7 @@ jobs: - name: setup (node) uses: actions/setup-node@v3 with: - node-version: '16' + node-version: '20' # restore caches - name: cache (pip) @@ -111,7 +112,7 @@ jobs: strategy: matrix: os: [ubuntu] - python-version: ['3.10'] + python-version: ['3.11'] env: WITH_JS_COV: 1 defaults: @@ -119,7 +120,7 @@ jobs: shell: bash -l {0} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: cache (conda) uses: actions/cache@v3 @@ -143,37 +144,42 @@ jobs: with: environment-file: .binder/environment.yml miniforge-variant: Mambaforge - use-only-tar-bz2: true use-mamba: true - - name: Lint + - name: lint run: doit lint - - name: Dev + - name: dist + env: + WITH_JS_COV: 0 + run: doit dist + + - name: build + run: doit build + + - name: dev run: doit dev - - name: Test (with cov) + - name: test latest (with cov) run: doit test:robot - - name: Docs + - name: dev (legacy) + run: doit legacy:pip + + - name: test legacy + run: doit legacy:robot + + - name: docs run: doit docs - - name: Check + - name: check run: doit check - - name: Upload (report) - if: always() - uses: actions/upload-artifact@v3 - with: - name: jupyterlab-deck-nyc-${{ github.run_number }} - path: ./build/reports/nyc/ - - - name: upload (atest) + - name: upload (reports) if: always() uses: actions/upload-artifact@v3 with: - name: |- - jupyterlab-deck-test-cov-${{ matrix.os }}-${{matrix.python-version }}-${{ github.run_number }} + name: jupyterlab-deck-lint-reports-${{ github.run_number }} path: ./build/reports - uses: codecov/codecov-action@v3 @@ -190,11 +196,11 @@ jobs: fail-fast: false matrix: os: ['ubuntu', 'macos', 'windows'] - python-version: ['3.7', '3.10'] + python-version: ['3.8', '3.11'] include: - - python-version: '3.7' + - python-version: '3.8' CI_ARTIFACT: 'sdist' - - python-version: '3.10' + - python-version: '3.11' CI_ARTIFACT: 'wheel' env: TESTING_IN_CI: '1' @@ -204,7 +210,7 @@ jobs: git config --global core.autocrlf false - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: cache (conda) uses: actions/cache@v3 @@ -221,7 +227,6 @@ jobs: miniforge-variant: Mambaforge python-version: ${{ matrix.python-version }} environment-file: .github/environment-test.yml - use-only-tar-bz2: true use-mamba: true - name: download (dist) @@ -240,26 +245,20 @@ jobs: shell: cmd /C CALL {0} run: doit dev - - name: test (unix) + - name: test latest (unix) if: matrix.os != 'windows' shell: bash -l {0} run: doit test - - name: test (windows) + - name: test latest (windows) if: matrix.os == 'windows' shell: cmd /C CALL {0} run: doit test - - uses: codecov/codecov-action@v3 - with: - directory: ./build/reports/coverage-xml/ - verbose: true - flags: back-end - - - name: upload (atest) + - name: upload (reports) if: always() uses: actions/upload-artifact@v3 with: name: |- - jupyterlab-deck-test-${{ matrix.os }}-${{matrix.python-version }}-${{ github.run_number }} + jupyterlab-deck-reports-${{ matrix.os }}-${{matrix.python-version }}-${{ github.run_number }} path: ./build/reports diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index baac2bd..afd5c9a 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -15,7 +15,7 @@ env: # our stuff ROBOT_RETRIES: '3' - CACHE_EPOCH: '0' + CACHE_EPOCH: '4' PABOT_PROCESSES: '3' jobs: @@ -32,7 +32,7 @@ jobs: shell: bash -l {0} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: cache (conda) uses: actions/cache@v3 @@ -56,22 +56,21 @@ jobs: with: environment-file: .binder/environment.yml miniforge-variant: Mambaforge - use-only-tar-bz2: true use-mamba: true - - name: Lint + - name: lint run: doit lint - - name: Dev + - name: dev run: doit dev - - name: Test (pytest) + - name: test latest (pytest) run: doit test:pytest - - name: Test (robot with cov) + - name: test latest (robot with cov) run: doit test:robot - - name: Site + - name: site run: doit site - uses: actions/upload-pages-artifact@v1 diff --git a/.gitignore b/.gitignore index 9798997..f3382e5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,12 +4,14 @@ _output/ .binder/*.requirements.txt .cache/ .coverage +.env .ipynb_checkpoints/ .pabotsuitenames -.venv/ +.venv*/ .yarn-packages/ *.doit.* *.egg-info +*.log *.tsbuildinfo build/ dist/ diff --git a/.readthedocs.yml b/.readthedocs.yml index 47c3adb..43880d3 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -3,6 +3,13 @@ build: os: ubuntu-20.04 tools: python: mambaforge-4.10 + jobs: + pre_build: + - doit setup + - doit build + - doit dist + - doit dev + - doit lite sphinx: builder: html configuration: docs/conf.py diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 index ce91b1c..0000000 --- a/.yarnrc +++ /dev/null @@ -1,6 +0,0 @@ -disable-self-update-check true -ignore-optional true -network-timeout "300000" -registry "https://registry.npmjs.org/" -yarn-offline-mirror "./.yarn-packages" -yarn-offline-mirror-pruning true diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..8402834 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,22 @@ +enableInlineBuilds: false +enableTelemetry: false +httpTimeout: 60000 +nodeLinker: node-modules +npmRegistryServer: https://registry.npmjs.org/ +installStatePath: ./build/.cache/yarn/install-state.gz +cacheFolder: ./build/.cache/yarn/cache +logFilters: + - code: YN0006 + level: discard + - code: YN0002 + level: discard + - code: YN0007 + level: discard + - code: YN0013 + level: discard + - code: YN0019 + level: discard + - code: YN0008 + level: discard + - code: YN0032 + level: discard diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d3b471..fe76642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ ## Changelog -### `0.1.4` (unreleased) +### `0.2.0a0` -> TBD +- [#36] adds support for Jupyter Notebook 7 and JupyterLab 4 + +[#36]: https://github.com/deathbeds/jupyterlab-deck/issues/36 ### `0.1.3` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff99b68..729c70b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,22 @@ See other available tasks with: doit list ``` +### Legacy + +Support for JupyterLab 3 is verified with the `legacy` subtasks. + +Run all legacy tasks: + +```bash +doit legacy +``` + +Run an isolated JupyterLab 3 application: + +```bash +doit serve:lab:legacy +``` + ### Releasing - Start a [release issue](https://github.com/jupyterlab-deck/issues) diff --git a/README.md b/README.md index 28434c5..a9856a6 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,11 @@ [binder-badge]: https://mybinder.org/badge_logo.svg [binder]: https://mybinder.org/v2/gh/deathbeds/jupyterlab-deck/HEAD?urlpath=lab/tree/examples/README.ipynb -[ci-badge]: https://img.shields.io/github/workflow/status/deathbeds/jupyterlab-deck/CI +[ci-badge]: + https://img.shields.io/github/actions/workflow/status/deathbeds/jupyterlab-deck/ci.yml [ci]: https://github.com/deathbeds/jupyterlab-deck/actions?query=branch%3Amain [reports-badge]: - https://img.shields.io/github/workflow/status/deathbeds/jupyterlab-deck/pages?label=reports + https://img.shields.io/github/actions/workflow/status/deathbeds/jupyterlab-deck/pages.yml?label=reports [reports]: https://deathbeds.github.io/jupyterlab-deck/lab/index.html?path=README.ipynb [rtd-badge]: https://img.shields.io/readthedocs/jupyterlab-deck [rtd]: https://jupyterlab-deck.rtfd.io @@ -230,9 +231,10 @@ restore the part to the default layout. ### Does it work with `notebook 7`? -**Not yet.** Navigating multiple documents during the same presentation will probably +**Mostly.** Navigating multiple documents during the same presentation will probably never work, as this is incompatible with the one-document-at-a-time design constraint of -the Notebook UX. +the Notebook UX. Each skip to another document will open a new browser tab, though deck +should be installed. ### Will it generate PowerPoint? diff --git a/_scripts/labextension.py b/_scripts/labextension.py new file mode 100644 index 0000000..b7bf22c --- /dev/null +++ b/_scripts/labextension.py @@ -0,0 +1,27 @@ +"""A custom `jupyter-labextension` to enable the semi-weird directory layout.""" +import importlib +import sys +from pathlib import Path + +from jupyterlab import federated_labextensions +from jupyterlab.labextensions import LabExtensionApp + +HERE = Path(__file__).parent.resolve() +ROOT = HERE.parent +NODE_MODULES = ROOT / "node_modules" +BUILDER = NODE_MODULES / "@jupyterlab/builder/lib/build-labextension.js" + + +def _get_labextension_metadata(module): + module = "jupyterlab_deck" + m = importlib.import_module(module) + return m, m._jupyter_labextension_paths() + + +federated_labextensions._get_labextension_metadata = _get_labextension_metadata +federated_labextensions._ensure_builder = lambda *_: str(BUILDER) + +main = LabExtensionApp.launch_instance + +if __name__ == "__main__": + sys.exit(main()) diff --git a/atest/fixtures/jupyter_config.json b/atest/fixtures/jupyter_config.json index b2ef6be..a4da239 100644 --- a/atest/fixtures/jupyter_config.json +++ b/atest/fixtures/jupyter_config.json @@ -1,14 +1,22 @@ { "LabApp": { + "expose_app_in_browser": true, "log_level": "DEBUG", "open_browser": false }, + "JupyterNotebookApp": { + "expose_app_in_browser": true + }, "ServerApp": { "tornado_settings": { "page_config_data": { + "buildAvailable": false, "buildCheck": false, - "buildAvailable": false + "exposeAppInBrowser": true } } + }, + "LanguageServerManager": { + "autodetect": false } } diff --git a/atest/fixtures/overrides.json b/atest/fixtures/overrides.json new file mode 100644 index 0000000..5560581 --- /dev/null +++ b/atest/fixtures/overrides.json @@ -0,0 +1,13 @@ +{ + "@jupyterlab/apputils-extension:notification": { + "checkForUpdates": false, + "doNotDisturbMode": true, + "fetchNews": "false" + }, + "@jupyterlab/apputils-extension:palette": { + "modal": false + }, + "@jupyterlab/extensionmanager-extension:plugin": { + "enabled": false + } + } diff --git a/atest/fixtures/page_config.json b/atest/fixtures/page_config.json new file mode 100644 index 0000000..bd3c81f --- /dev/null +++ b/atest/fixtures/page_config.json @@ -0,0 +1,5 @@ +{ + "disabledExtensions": { + "@jupyterlab/apputils-extension:notification": true + } +} diff --git a/atest/resources/CodeMirror.resource b/atest/resources/CodeMirror.resource new file mode 100644 index 0000000..b07c716 --- /dev/null +++ b/atest/resources/CodeMirror.resource @@ -0,0 +1,38 @@ +*** Settings *** +Documentation Keywords for working with CodeMirror + +Library JupyterLibrary + + +*** Variables *** +${CM JS TO STRING} view.state.doc.toString() + + +*** Keywords *** +Initialize CodeMirror + [Documentation] Fix apparently-broken CSS/JS variable updates. + IF "${JD_APP_UNDER_TEST}" == "nb" + Update Globals For JupyterLab 4 + ELSE + Update Globals For JupyterLab Version + END + Set Suite Variable ${CM CSS EDITOR} ${CM CSS EDITOR} children=${TRUE} + Set Suite Variable ${CM JS INSTANCE} ${CM JS INSTANCE} children=${TRUE} + IF "${CM JS INSTANCE}" == "${CM6 JS INSTANCE}" + Set Suite Variable ${CM JS TO STRING} view.state.doc.toString() children=${TRUE} + ELSE + Set Suite Variable ${CM JS TO STRING} getValue() children=${TRUE} + END + +Return CodeMirror Method + [Documentation] Construct and a method call against in the CodeMirror attached to the element + ... that matches a ``css`` selector with the given ``js`` code. + ... The CodeMirror editor instance will be available as `cm`. + [Arguments] ${css} ${js} + + ${result} = Execute JavaScript + ... return (() => { + ... const cm = document.querySelector(`${css}`)${CM JS INSTANCE}; + ... return cm.${js}; + ... }).call(this); + RETURN ${result} diff --git a/atest/resources/Docs.resource b/atest/resources/Docs.resource index 96d5c4f..fd3cf0f 100644 --- a/atest/resources/Docs.resource +++ b/atest/resources/Docs.resource @@ -47,3 +47,4 @@ Set Up Interactive Suite [Documentation] Prepare for this suite. [Arguments] ${screens} Set Attempt Screenshot Directory lab${/}${screens} + Initialize CodeMirror diff --git a/atest/resources/Fixtures.resource b/atest/resources/Fixtures.resource index 6723a1b..ab49f44 100644 --- a/atest/resources/Fixtures.resource +++ b/atest/resources/Fixtures.resource @@ -33,7 +33,7 @@ Clean Examples Open Example [Documentation] Open an example - [Arguments] ${name}=README.ipynb + [Arguments] ${name}=README.ipynb ${switch_window}=${EMPTY} Maybe Open JupyterLab Sidebar File Browser ${selectors} = Create List ... ${CSS_LAB_FILES_HOME} @@ -43,3 +43,7 @@ Open Example Wait Until Element Is Visible css:${sel} Double Click Element css:${sel} END + IF "${switch_window}" + Sleep 5s + Switch Window NEW + END diff --git a/atest/resources/Lab.resource b/atest/resources/Lab.resource index bdc68ff..4009750 100644 --- a/atest/resources/Lab.resource +++ b/atest/resources/Lab.resource @@ -1,6 +1,7 @@ *** Settings *** -Documentation Keywords for working with decks. +Documentation Keywords for working with the Lab shell. +Resource ./CodeMirror.resource Resource ./LabSelectors.resource Resource ./Screenshots.resource Library Collections @@ -8,6 +9,23 @@ Library JupyterLibrary *** Keywords *** +Initialize JupyterLab + [Documentation] Get the web app set up for testing. + ${executable_path} = Get GeckoDriver Executable Path + Open JupyterLab executable_path=${executable_path} + Initialize CodeMirror + Set Window Size 1366 768 + Reload Page + Wait For JupyterLab Splash Screen + +Plugins Should Be Disabled + [Documentation] Check that some JupyterLab extensions are disabled by config + [Arguments] @{plugins} + ${disabled} = Get JupyterLab Page Info disabledExtensions + FOR ${plugin} IN @{plugins} + Should Contain ${disabled} ${plugin} msg=${plugin} was not disabled + END + Add And Activate Cell With Keyboard [Documentation] Add a cell with the keyboard. ${index} = Get Active Cell Index @@ -40,12 +58,13 @@ Set Cell Type Make Markdown Cell [Documentation] Turn the current cell into markdown. [Arguments] ${code} ${expect}=${EMPTY} ${new}=${TRUE} ${screenshot}=${EMPTY} + Initialize CodeMirror ${index} = Get Active Cell Index IF ${new} Add And Activate Cell With Keyboard ${index} = Set Variable ${index.__add__(1)} END - ${cm} = Set Variable ${JLAB CSS ACTIVE DOC CELLS}:nth-child(${index}) .CodeMirror + ${cm} = Set Variable ${JLAB CSS ACTIVE DOC CELLS}:nth-child(${index}) ${CM CSS EDITOR} Set CodeMirror Value ${cm} ${code} Set Cell Type ${index} markdown IF ${expect.__len__()} Render Markdown Cell ${index} ${expect} @@ -78,19 +97,21 @@ Wait Until Cell Is Not Active Maybe Open Cell Metadata JSON [Documentation] Ensure the Cell Metadata viewer is open. - ${el} = Get WebElements ${CSS_LAB_CELL_META_JSON_CM_HIDDEN} + + ${el} = Get WebElements ${CSS_LAB_CELL_META_JSON} ${CM CSS EDITOR} IF not ${el.__len__()} RETURN Click Element ${CSS_LAB_ADVANCED_COLLAPSE} - Wait Until Page Does Not Contain Element ${CSS_LAB_CELL_META_JSON_CM_HIDDEN} + Wait Until Page Does Not Contain Element ${CSS_LAB_CELL_META_JSON_HIDDEN} ${CM CSS EDITOR} Wait Until Cell Metadata Contains [Documentation] Ensure a string appears in the Cell Metadata JSON [Arguments] ${text} ${attempts}=5 ${timeout}=0.1s ${inverse}=${FALSE} + ${ok} = Set Variable ${FALSE} FOR ${i} IN RANGE ${attempts} - ${src} = Return CodeMirror Method ${CSS_LAB_CELL_META_JSON_CM} getValue() + ${src} = Return CodeMirror Method ${CSS_LAB_CELL_META_JSON} ${CM CSS EDITOR} ${CM JS TO STRING} ${contains} = Set Variable ${src.__contains__('''${text}''')} IF ${inverse} and not ${contains} ${ok} = Set Variable ${TRUE} @@ -111,9 +132,10 @@ Wait Until Cell Metadata Does Not Contain [Arguments] ${text} ${attempts}=5 ${timeout}=0.1s Wait Until Cell Metadata Contains text=${text} attempts=${attempts} timeout=${timeout} inverse=${TRUE} -Return CodeMirror Method - [Documentation] Construct and a method call against in the CodeMirror attached to the element - ... that matches a ``css`` selector with the given ``js`` code. - [Arguments] ${css} ${js} - ${result} = Execute JavaScript return document.querySelector(`${css}`).CodeMirror.${js} - RETURN ${result} +Maybe Expand Panel With Title + [Documentation] Ensure a collapsed panel in a sidebar is expanded + [Arguments] ${label} + ${els} = Get WebElements + ... xpath:${XP_LAB4_COLLAPSED_PANEL_TITLE}\[contains(., '${label}')] + IF not ${els.__len__()} RETURN + Click Element ${els[0]} diff --git a/atest/resources/LabSelectors.resource b/atest/resources/LabSelectors.resource index 5abc490..d398dcd 100644 --- a/atest/resources/LabSelectors.resource +++ b/atest/resources/LabSelectors.resource @@ -4,53 +4,60 @@ Documentation Selectors that should maybe go upstream. *** Variables *** # # lumino ## -${CSS_LM_MOD_ACTIVE} .lm-mod-active -${CSS_LM_MENU_ITEM_LABEL} .lm-Menu-itemLabel -${CSS_LM_CLOSE_ICON} .lm-TabBar-tabCloseIcon +${CSS_LM_MOD_ACTIVE} .lm-mod-active +${CSS_LM_MENU_ITEM_LABEL} .lm-Menu-itemLabel +${CSS_LM_CLOSE_ICON} .lm-TabBar-tabCloseIcon # # lab # # # mod -${CSS_LAB_MOD_DISABLED} .jp-mod-disabled -${CSS_LAB_MOD_CMD} .jp-mod-commandMode -${CSS_LAB_MOD_ACTIVE} .jp-mod-active -${CSS_LAB_MOD_EDIT} .jp-mod-editMode -${CSS_LAB_MOD_RENDERED} .jp-mod-rendered -${CSS_LAB_MOD_HIDDEN} .lm-mod-hidden +${CSS_LAB_MOD_DISABLED} .jp-mod-disabled +${CSS_LAB_MOD_CMD} .jp-mod-commandMode +${CSS_LAB_MOD_ACTIVE} .jp-mod-active +${CSS_LAB_MOD_EDIT} .jp-mod-editMode +${CSS_LAB_MOD_RENDERED} .jp-mod-rendered +${CSS_LAB_MOD_HIDDEN} .lm-mod-hidden # files -${CSS_LAB_FILES_HOME} .jp-BreadCrumbs-home -${CSS_LAB_FILES_DIR_ITEM} .jp-DirListing-item +${CSS_LAB_FILES_HOME} .jp-BreadCrumbs-home +${CSS_LAB_FILES_DIR_ITEM} .jp-DirListing-item # docpanel -${CSS_LAB_NOT_INTERNAL_ANCHOR} a[href*\="#"]:not([href^="https"]):not(${CSS_LAB_INTERNAL_ANCHOR}) -${CSS_LAB_TAB_NOT_CURRENT} .lm-DockPanel .lm-TabBar-tab:not(.jp-mod-current) +${CSS_LAB_NOT_INTERNAL_ANCHOR} a[href*\="#"]:not([href^="https"]):not(${CSS_LAB_INTERNAL_ANCHOR}) +${CSS_LAB_TAB_NOT_CURRENT} .lm-DockPanel .lm-TabBar-tab:not(.jp-mod-current) # docs -${CSS_LAB_SPINNER} .jp-Spinner -${CSS_LAB_INTERNAL_ANCHOR} .jp-InternalAnchorLink +${CSS_LAB_SPINNER} .jp-Spinner +${CSS_LAB_INTERNAL_ANCHOR} .jp-InternalAnchorLink # meta -${CSS_LAB_ADVANCED_COLLAPSE} .jp-NotebookTools .jp-Collapse-header -${CSS_LAB_CELL_META_JSON_CM} .jp-MetadataEditorTool .CodeMirror -${CSS_LAB_CELL_META_JSON_CM_HIDDEN} ${CSS_LAB_MOD_HIDDEN} ${CSS_LAB_CELL_META_JSON_CM} +${CSS_LAB_ADVANCED_COLLAPSE} .jp-NotebookTools .jp-Collapse-header +${CSS_LAB_CELL_META_JSON} .jp-MetadataEditorTool +${CSS_LAB_CELL_META_JSON_HIDDEN} ${CSS_LAB_MOD_HIDDEN} ${CSS_LAB_CELL_META_JSON} # notebook -${CSS_LAB_NB_TOOLBAR} .jp-NotebookPanel-toolbar -${CSS_LAB_NB_TOOLBAR_CELLTYPE} .jp-Notebook-toolbarCellType select -${CSS_LAB_CELL_MARKDOWN} .jp-MarkdownCell -${CSS_LAB_CELL_CODE} .jp-CodeCell -${CSS_LAB_CELL_RAW} .jp-RawCell +${CSS_LAB_NB_TOOLBAR} .jp-NotebookPanel-toolbar +${CSS_LAB_NB_TOOLBAR_CELLTYPE} .jp-Notebook-toolbarCellType select +${CSS_LAB_CELL_MARKDOWN} .jp-MarkdownCell +${CSS_LAB_CELL_CODE} .jp-CodeCell +${CSS_LAB_CELL_RAW} .jp-RawCell &{CSS_LAB_CELL_TYPE} -... code=${CSS_LAB_CELL_CODE} -... markdown=${CSS_LAB_CELL_MARKDOWN} -... raw=${CSS_LAB_CELL_RAW} +... code=${CSS_LAB_CELL_CODE} +... markdown=${CSS_LAB_CELL_MARKDOWN} +... raw=${CSS_LAB_CELL_RAW} # icons -${CSS_LAB_ICON_ELLIPSES} [data-icon="ui-components:ellipses"] -${CSS_LAB_ICON_CARET_LEFT} [data-icon="ui-components:caret-left"] +${CSS_LAB_ICON_ELLIPSES} [data-icon="ui-components:ellipses"] +${CSS_LAB_ICON_CARET_LEFT} [data-icon="ui-components:caret-left"] # markdown -${CSS_LAB_EDITOR} .jp-FileEditor -${CSS_LAB_MARKDOWN_VIEWER} .jp-MarkdownViewer -${CSS_LAB_CMD_MARKDOWN_PREVIEW} [data-command="fileeditor:markdown-preview"] +${CSS_LAB_EDITOR} .jp-FileEditor +${CSS_LAB_MARKDOWN_VIEWER} .jp-MarkdownViewer +${CSS_LAB_CMD_MARKDOWN_PREVIEW} [data-command="fileeditor:markdown-preview"] + +# lab 7 +${XP_LAB4_COLLAPSED_PANEL} //*[contains(@class, 'jp-Collapse-header-collapsed')] +${XP_LAB4_COLLAPSED_PANEL_TITLE} ${XP_LAB4_COLLAPSED_PANEL}//*[contains(@class, 'jp-Collapser-title')] + +# rfjl bugs +${CM CSS EDITOR} .CodeMirror diff --git a/atest/resources/Notebook.resource b/atest/resources/Notebook.resource new file mode 100644 index 0000000..c4cefb1 --- /dev/null +++ b/atest/resources/Notebook.resource @@ -0,0 +1,17 @@ +*** Settings *** +Documentation Keywords for working with the Notebook shell. + +Resource ./CodeMirror.resource +Resource ./Server.resource +Library JupyterLibrary + + +*** Keywords *** +Initialize Jupyter Notebook + [Documentation] Get the web app set up for testing. + ${executable_path} = Get GeckoDriver Executable Path + Open Notebook executable_path=${executable_path} + Initialize CodeMirror + Set Window Size 1366 768 + Reload Page + Wait Until Element Is Visible css:${JNB CSS TREE LIST} timeout=10s diff --git a/atest/resources/Screenshots.resource b/atest/resources/Screenshots.resource index 77fabd2..227edec 100644 --- a/atest/resources/Screenshots.resource +++ b/atest/resources/Screenshots.resource @@ -34,4 +34,6 @@ Empty Screenshot Trash Capture Page Screenshot And Tag With Error [Documentation] Capture a screenshot if not going to the trash ${path} = Capture Page Screenshot - IF "__trash__" not in "${path}" Set Tags screenshot:unexpected + IF "__trash__" not in "${path}" + Run Keyword And Ignore Error Set Tags screenshot:unexpected + END diff --git a/atest/resources/Server.resource b/atest/resources/Server.resource new file mode 100644 index 0000000..c746e67 --- /dev/null +++ b/atest/resources/Server.resource @@ -0,0 +1,108 @@ +*** Settings *** +Documentation Keywords for testing jupyterlab-fonts + +Library BuiltIn +Library Collections +Library String +Library OperatingSystem +Library JupyterLibrary +Library shutil +Library uuid + + +*** Variables *** +${JUPYTERLAB_EXE} ["jupyter-lab"] +${JSCOV} ${EMPTY} +&{ETC_OVERRIDES} +... jupyter_config.json=jupyter_config.json +... overrides.json=labconfig${/}default_setting_overrides.json +... page_config.json=labconfig${/}page_config.json +${FIXTURES} ${ROOT}${/}atest${/}fixtures + + +*** Keywords *** +Initialize Jupyter Server + [Documentation] Set up server with command as defined in atest.py. + [Arguments] ${home_dir} + ${port} = Get Unused Port + ${token} = Generate Random String 64 + ${base url} = Set Variable /jl@d/ + @{args} = Build Custom JupyterLab Args ${port} ${token} ${base url} + ${rest_args} = Get Slice From List ${args} 1 + ${config} = Initialize Jupyter Server Config ${home_dir} + ${lab} = Start New Jupyter Server + ... ${args[0]} + ... ${port} + ... ${base url} + ... ${config["cwd"]} + ... ${token} + ... @{rest_args} + ... &{config} + Wait For Jupyter Server To Be Ready ${lab} + RETURN ${lab} + +Initialize Jupyter Server Config + [Documentation] Prepare keyword arguments to launch a custom jupyter server. + [Arguments] ${home_dir} + ${notebook_dir} = Set Variable ${home_dir}${/}work + ${app_data} = Get Windows App Data ${home_dir} + &{config} = Create Dictionary + ... stdout=${OUTPUT DIR}${/}lab.log + ... stderr=STDOUT + ... cwd=${notebook_dir} + ... env:HOME=${home_dir} + ... env:APPDATA=${app_data} + ... env:JUPYTER_PREFER_ENV_PATH=0 + RETURN ${config} + +Get Windows App Data + [Documentation] Get an overloaded appdata directory. + [Arguments] ${home_dir} + RETURN ${home_dir}${/}AppData${/}Roaming + +Build Custom JupyterLab Args + [Documentation] Generate some args + [Arguments] ${port} ${token} ${base url} + @{args} = Loads ${JUPYTERLAB_EXE} + ${config} = Normalize Path ${ROOT}${/}atest${/}fixtures${/}jupyter_config.json + @{args} = Set Variable + ... @{args} + ... --no-browser + ... --debug + ... --expose-app-in-browser + ... --port\=${port} + ... --IdentityProvider.token\=${token} + ... --ServerApp.base_url\=${base url} + Log ${args} + RETURN @{args} + +Initialize Fake Home + [Documentation] Populate a fake HOME + ${home_dir} = Set Variable ${OUTPUT_DIR}${/}.home + ${local} = Get XDG Local Path ${home_dir} + ${etc} = Set Variable ${local}${/}etc${/}jupyter + FOR ${src} ${dest} IN &{ETC_OVERRIDES} + OperatingSystem.Copy File ${FIXTURES}${/}${src} ${etc}${/}${dest} + END + Create Directory ${home_dir}${/}work + RETURN ${home_dir} + +Get XDG Local Path + [Documentation] Get the root of the XDG local data for this platform. + [Arguments] ${home_dir} + IF "${OS}" == "Windows" + ${app_data} = Get Windows App Data ${home_dir} + ${local} = Set Variable ${app_data}${/}Python + ELSE + ${local} = Set Variable ${home_dir}${/}.local + END + RETURN ${local} + +Get GeckoDriver Executable Path + [Documentation] Find geckodriver + IF "${OS}" == "Windows" + ${executable_path} = Which geckodriver.exe + ELSE + ${executable_path} = Which geckodriver + END + RETURN ${executable_path} diff --git a/atest/resources/Sidebar.resource b/atest/resources/Sidebar.resource index 53bb515..d915862 100644 --- a/atest/resources/Sidebar.resource +++ b/atest/resources/Sidebar.resource @@ -12,6 +12,8 @@ Make Cell Layer With Sidebar [Arguments] ${idx} ${layer} ${screenshot}=${EMPTY} Click Element css:${JLAB CSS ACTIVE DOC CELLS}:nth-child(${idx}) Maybe Open JupyterLab Sidebar Property Inspector + Maybe Expand Panel With Title Advanced Tools + Maybe Expand Panel With Title Common Tools Maybe Open Cell Metadata JSON Select From List By Value css:${CSS_DECK_LAYER_SELECT} ${layer} IF '${layer}' != '-' @@ -26,6 +28,8 @@ Use Cell Style Preset [Arguments] ${idx} ${preset} ${expect}=${EMPTY} ${screenshot}=${EMPTY} Click Element css:${JLAB CSS ACTIVE DOC CELLS}:nth-child(${idx}) Maybe Open JupyterLab Sidebar Property Inspector + Maybe Expand Panel With Title Advanced Tools + Maybe Expand Panel With Title Common Tools Maybe Open Cell Metadata JSON Select From List By Value css:${CSS_DECK_PRESET_SELECT} ${preset} Click Element css:${CSS_DECK_TOOL_PRESET} button diff --git a/atest/suites/__init__.robot b/atest/suites/__init__.robot index 080d79b..1e1ca2a 100644 --- a/atest/suites/__init__.robot +++ b/atest/suites/__init__.robot @@ -20,5 +20,6 @@ Set Up Root Suite Tear Down Root Suite [Documentation] Do global suite teardown. Close All Browsers + Terminate All Jupyter Servers Terminate All Processes Empty Screenshot Trash diff --git a/atest/suites/lab/00-smoke.robot b/atest/suites/lab/00-smoke.robot index 4e2978e..7039a03 100644 --- a/atest/suites/lab/00-smoke.robot +++ b/atest/suites/lab/00-smoke.robot @@ -3,6 +3,7 @@ Documentation JupyterLab is not broken. Library JupyterLibrary Resource ../../resources/Coverage.resource +Resource ../../resources/Lab.resource Resource ../../resources/Screenshots.resource Suite Setup Set Attempt Screenshot Directory lab${/}smoke @@ -14,3 +15,5 @@ Force Tags suite:smoke JupyterLab Opens [Documentation] JupyterLab opens. Capture Page Screenshot 00-smoke.png + Plugins Should Be Disabled + ... @jupyterlab/apputils-extension:notification diff --git a/atest/suites/lab/__init__.robot b/atest/suites/lab/__init__.robot index 52888bb..b0700fd 100644 --- a/atest/suites/lab/__init__.robot +++ b/atest/suites/lab/__init__.robot @@ -1,9 +1,10 @@ *** Settings *** Documentation Tests for JupyterLab. -Library uuid Library JupyterLibrary Resource ../../resources/Coverage.resource +Resource ../../resources/Lab.resource +Resource ../../resources/Server.resource Resource ../../resources/LabSelectors.resource Suite Setup Set Up Lab Suite @@ -12,35 +13,13 @@ Suite Teardown Tear Down Lab Suite Force Tags app:lab -*** Variables *** -${LOG_DIR} ${OUTPUT_DIR}${/}logs - - *** Keywords *** Set Up Lab Suite [Documentation] Ensure a testable server is running - ${port} = Get Unused Port - ${base_url} = Set Variable /@rf/ - ${token} = UUID4 - Create Directory ${LOG_DIR} - Wait For New Jupyter Server To Be Ready - ... jupyter-lab - ... ${port} - ... ${base_url} - ... ${NONE} # notebook_dir - ... ${token.__str__()} - ... --config\=${ROOT}${/}atest${/}fixtures${/}jupyter_config.json - ... --no-browser - ... --debug - ... --port\=${port} - ... --NotebookApp.token\='${token.__str__()}' - ... --NotebookApp.base_url\='${base_url}' - ... stdout=${LOG_DIR}${/}lab.log - Open JupyterLab - Disable JupyterLab Modal Command Palette - Set Window Size 1366 768 - Reload Page - Wait For JupyterLab Splash Screen + Set Suite Variable ${JD_APP_UNDER_TEST} lab children=${TRUE} + ${home_dir} = Initialize Fake Home + Initialize Jupyter Server ${home_dir} + Initialize JupyterLab Tear Down Lab Suite [Documentation] Do clean up stuff diff --git a/atest/suites/nb/00-smoke.robot b/atest/suites/nb/00-smoke.robot new file mode 100644 index 0000000..99d82bb --- /dev/null +++ b/atest/suites/nb/00-smoke.robot @@ -0,0 +1,19 @@ +*** Settings *** +Documentation Jupyter Notebook is not broken. + +Library JupyterLibrary +Resource ../../resources/Lab.resource +Resource ../../resources/Coverage.resource +Resource ../../resources/Screenshots.resource + +Suite Setup Set Attempt Screenshot Directory nb${/}smoke + +Force Tags suite:smoke + + +*** Test Cases *** +Jupyter Notebook Opens + [Documentation] Jupyter Notebook opens. + Capture Page Screenshot 00-smoke.png + Plugins Should Be Disabled + ... @jupyterlab/apputils-extension:notification diff --git a/atest/suites/nb/01-examples.robot b/atest/suites/nb/01-examples.robot new file mode 100644 index 0000000..eb6d5f9 --- /dev/null +++ b/atest/suites/nb/01-examples.robot @@ -0,0 +1,54 @@ +*** Settings *** +Documentation The examples work in Notebook. + +Library OperatingSystem +Library JupyterLibrary +Resource ../../resources/Fixtures.resource +Resource ../../resources/Deck.resource +Resource ../../resources/Screenshots.resource + +Suite Setup Set Up Example Suite +Suite Teardown Clean Examples + +Force Tags suite:examples + + +*** Test Cases *** +The README Notebook Can Be Navigated + [Documentation] All slides and fragments are reachable. + [Tags] activity:notebook + Visit All Example Slides And Fragments ${README_IPYNB} + [Teardown] Reset Example Test + + +*** Keywords *** +Visit All Example Slides And Fragments + [Documentation] The given file in `examples` operates as expected. + [Arguments] ${example}=README.ipynb + ${stem} = Set Variable ${example.lower().replace(" ", "_")} + Open Example ${example} switch_window=README + Capture Page Screenshot ${stem}-00-before-deck.png + IF ${example.endswith('.ipynb')} + Really Start Deck With Notebook Toolbar Button + ELSE IF ${example.endswith('.md')} + Start Markdown Deck From Editor ${example} + ELSE + Execute JupyterLab Command Start Deck + END + Capture Page Screenshot ${stem}-01-deck.png + Visit Slides And Fragments With Remote ${example} ${stem}-02-walk + Stop Deck With Remote + Capture Page Screenshot ${stem}-03-after-deck.png + +Set Up Example Suite + [Documentation] Prepare for this suite. + Set Attempt Screenshot Directory lab${/}examples + Copy Examples + +Reset Example Test + [Documentation] Clean up after each test. + Maybe Open JupyterLab Sidebar Commands + Execute JupyterLab Command Stop Deck + Execute JupyterLab Command Save + Capture Page Coverage + Switch Window title:Home diff --git a/atest/suites/nb/__init__.robot b/atest/suites/nb/__init__.robot new file mode 100644 index 0000000..2d3be95 --- /dev/null +++ b/atest/suites/nb/__init__.robot @@ -0,0 +1,28 @@ +*** Settings *** +Documentation Tests for Notebook. + +Library JupyterLibrary +Resource ../../resources/Coverage.resource +Resource ../../resources/Lab.resource +Resource ../../resources/Notebook.resource +Resource ../../resources/Server.resource +Resource ../../resources/LabSelectors.resource + +Suite Setup Set Up Notebook Suite +Suite Teardown Tear Down Notebook Suite + +Force Tags app:nb + + +*** Keywords *** +Set Up Notebook Suite + [Documentation] Ensure a testable server is running + Set Suite Variable ${JD_APP_UNDER_TEST} nb children=${TRUE} + ${home_dir} = Initialize Fake Home + Initialize Jupyter Server ${home_dir} + Initialize Jupyter Notebook + +Tear Down Notebook Suite + [Documentation] Do clean up stuff + Maybe Accept A JupyterLab Prompt + Capture Page Coverage diff --git a/docs/conf.py b/docs/conf.py index 8c88dd2..468df39 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,25 +1,19 @@ -"""documentation for jupyterlab-deck""" +"""documentation for jupyterlab-deck.""" import datetime -import json -import os -import subprocess from pathlib import Path import tomli -os.environ.update(IN_SPHINX="1") - CONF_PY = Path(__file__) HERE = CONF_PY.parent ROOT = HERE.parent PYPROJ = ROOT / "pyproject.toml" PROJ_DATA = tomli.loads(PYPROJ.read_text(encoding="utf-8")) -RTD = json.loads(os.environ.get("READTHEDOCS", "False").lower()) # metadata author = PROJ_DATA["project"]["authors"][0]["name"] project = PROJ_DATA["project"]["name"] -copyright = f"{datetime.date.today().year}, {author}" +copyright = f"{datetime.datetime.now(tz=datetime.timezone.utc).date().year}, {author}" # The full version, including alpha/beta/rc tags release = PROJ_DATA["project"]["version"] @@ -73,7 +67,7 @@ html_theme_options = { "github_url": PROJ_DATA["project"]["urls"]["Source"], "use_edit_page_button": True, - "logo": dict(text=PROJ_DATA["project"]["name"]), + "logo": {"text": PROJ_DATA["project"]["name"]}, "icon_links": [ { "name": "PyPI", @@ -102,7 +96,3 @@ } html_sidebars = {"**": []} - - -def setup(app): - subprocess.check_call(["doit", "lite"], cwd=ROOT) diff --git a/docs/dictionary.txt b/docs/dictionary.txt index ab934bf..445e423 100644 --- a/docs/dictionary.txt +++ b/docs/dictionary.txt @@ -1,3 +1,6 @@ +0a0 +0b0 +0rc0 Changelog ci composable @@ -15,6 +18,7 @@ MathJax MermaidJS npm PRs +PyData pypi PyPI README @@ -25,7 +29,9 @@ spacebar submodule subslide Subslide +subtasks TBD +themey UI un UX diff --git a/dodo.py b/dodo.py index 6f3f26a..480fb10 100644 --- a/dodo.py +++ b/dodo.py @@ -1,4 +1,4 @@ -"""automation for jupyterlab-deck""" +"""automation for jupyterlab-deck.""" import json import os import platform @@ -11,11 +11,22 @@ import doit.tools +DOT_ENV = Path(".env") + +dotenv_loaded = {} + +if DOT_ENV.exists(): + dotenv_loaded = __import__("dotenv").dotenv_values(DOT_ENV) + os.environ.update(dotenv_loaded) + class C: NPM_NAME = "@deathbeds/jupyterlab-deck" - OLD_VERSION = "0.1.3" - VERSION = "0.1.4" + OLD_VERSION = "0.1.4" + VERSION = "0.2.0a0" + JS_VERSION = ( + VERSION.replace("a", "-alpha.").replace("b", "-beta.").replace("rc", "-rc.") + ) PACKAGE_JSON = "package.json" PYPROJECT_TOML = "pyproject.toml" PABOT_DEFAULTS = [ @@ -24,9 +35,11 @@ class C: "png,log,txt,svg,ipynb,json", ] PLATFORM = platform.system() - PY_VERSION = "{}.{}".format(sys.version_info[0], sys.version_info[1]) + PY_VERSION = f"{sys.version_info[0]}.{sys.version_info[1]}" ROBOT_DRYRUN = "--dryrun" NYC = ["jlpm", "nyc", "report"] + HISTORY = "conda-meta/history" + CONDA_RUN = ["conda", "run", "--no-capture-output", "--prefix"] class P: @@ -35,8 +48,10 @@ class P: BINDER = ROOT / ".binder" DOCS = ROOT / "docs" CI = ROOT / ".github" + SCRIPTS = ROOT / "_scripts" DEMO_ENV_YAML = BINDER / "environment.yml" TEST_ENV_YAML = CI / "environment-test.yml" + TEST_35_ENV_YAML = CI / "environment-test-lab35.yml" DOCS_ENV_YAML = CI / "environment-docs.yml" BASE_ENV_YAML = CI / "environment-base.yml" BUILD_ENV_YAML = CI / "environment-build.yml" @@ -54,9 +69,10 @@ class P: ], DOCS_ENV_YAML: [BUILD_ENV_YAML, BASE_ENV_YAML], TEST_ENV_YAML: [BASE_ENV_YAML, BUILD_ENV_YAML, ROBOT_ENV_YAML], + TEST_35_ENV_YAML: [BASE_ENV_YAML, TEST_ENV_YAML, ROBOT_ENV_YAML], LINT_ENV_YAML: [BASE_ENV_YAML, BUILD_ENV_YAML, ROBOT_ENV_YAML], } - YARNRC = ROOT / ".yarnrc" + YARNRC = ROOT / ".yarnrc.yml" YARN_LOCK = ROOT / "yarn.lock" JS = ROOT / "js" JS_META = JS / "_meta" @@ -67,7 +83,8 @@ class P: EXT_JS_WEBPACK = EXT_JS_PKG / "webpack.config.js" EXT_JS_LICENSE = EXT_JS_PKG / "LICENSE" EXT_JS_README = EXT_JS_PKG / "README.md" - PY_SRC = ROOT / "src/jupyterlab_deck" + SRC = ROOT / "src" + PY_SRC = ROOT / "jupyterlab_deck" PYPROJECT_TOML = ROOT / C.PYPROJECT_TOML DOCS_STATIC = DOCS / "_static" DOCS_PY = [*DOCS.glob("*.py")] @@ -80,59 +97,77 @@ class P: PAGES_LITE = ROOT / "pages-lite" PAGES_LITE_CONFIG = PAGES_LITE / "jupyter_lite_config.json" PAGES_LITE_JSON = PAGES_LITE / "jupyter-lite.json" - ESLINTRC = JS / ".eslintrc.js" ALL_PLUGIN_SCHEMA = [*JS.glob("*/schema/*.json")] ATEST = ROOT / "atest" ROBOT_SUITES = ATEST / "suites" + SCRIPT_LABEXT = SCRIPTS / "labextension.py" + ATEST_JP_CONFIG = ATEST / "fixtures/jupyter_config.json" + + +def _fromenv(name, default, *, coerce=None, lower=None): + lower = True if lower is None else lower + raw = os.environ.get(name, default) + raw = raw.lower() if lower else raw + coerce = coerce or bool + return coerce(json.loads(raw)) class E: - IN_CI = bool(json.loads(os.environ.get("CI", "false").lower())) - BUILDING_IN_CI = bool(json.loads(os.environ.get("BUILDING_IN_CI", "false").lower())) - TESTING_IN_CI = bool(json.loads(os.environ.get("TESTING_IN_CI", "false").lower())) - IN_RTD = bool(json.loads(os.environ.get("READTHEDOCS", "False").lower())) - IN_BINDER = bool(json.loads(os.environ.get("IN_BINDER", "0"))) + IN_CI = _fromenv("CI", "false") + BUILDING_IN_CI = _fromenv("BUILDING_IN_CI", "false") + TESTING_IN_CI = _fromenv("TESTING_IN_CI", "false") + IN_RTD = _fromenv("READTHEDOCS", "False") + IN_BINDER = _fromenv("IN_BINDER", "0") LOCAL = not (IN_BINDER or IN_CI or IN_RTD) - ROBOT_RETRIES = json.loads(os.environ.get("ROBOT_RETRIES", "0")) - ROBOT_ARGS = json.loads(os.environ.get("ROBOT_ARGS", "[]")) - PABOT_ARGS = json.loads(os.environ.get("PABOT_ARGS", "[]")) - WITH_JS_COV = bool(json.loads(os.environ.get("WITH_JS_COV", "0"))) - PABOT_PROCESSES = int(json.loads(os.environ.get("PABOT_PROCESSES", "4"))) + ROBOT_RETRIES = _fromenv("ROBOT_RETRIES", "0", coerce=int) + ROBOT_ATTEMPT = _fromenv("ROBOT_ATTEMPT", "0", coerce=int) + ROBOT_ARGS = _fromenv("ROBOT_ARGS", "[]", coerce=list, lower=False) + PABOT_ARGS = _fromenv("PABOT_ARGS", "[]", coerce=list, lower=False) + WITH_JS_COV = _fromenv("WITH_JS_COV", "0", coerce=int) + PABOT_PROCESSES = _fromenv("PABOT_PROCESSES", "4", coerce=int) + MOZ_HEADLESS = _fromenv("MOZ_HEADLESS", "1", coerce=int) class B: + BUILD = P.ROOT / "build" ENV = P.ROOT / ".venv" if E.LOCAL else Path(sys.prefix) - HISTORY = [ENV / "conda-meta/history"] if E.LOCAL else [] + HISTORY = [ENV / C.HISTORY] if E.LOCAL else [] + ENV_LEGACY = BUILD / ".venv-legacy" + HISTORY_LEGACY = ENV_LEGACY / C.HISTORY NODE_MODULES = P.ROOT / "node_modules" - YARN_INTEGRITY = NODE_MODULES / ".yarn-integrity" + YARN_INTEGRITY = NODE_MODULES / ".yarn-state.yml" JS_META_TSBUILDINFO = P.JS_META / ".src.tsbuildinfo" - BUILD = P.ROOT / "build" DIST = P.ROOT / "dist" DOCS = BUILD / "docs" DOCS_BUILDINFO = DOCS / ".buildinfo" LITE = BUILD / "lite" - STATIC = P.PY_SRC / f"_d/share/jupyter/labextensions/{C.NPM_NAME}" + STATIC = P.SRC / f"_d/share/jupyter/labextensions/{C.NPM_NAME}" STATIC_PKG_JSON = STATIC / C.PACKAGE_JSON WHEEL = DIST / f"jupyterlab_deck-{C.VERSION}-py3-none-any.whl" - SDIST = DIST / f"jupyterlab-deck-{C.VERSION}.tar.gz" + SDIST = DIST / f"jupyterlab_deck-{C.VERSION}.tar.gz" LITE_SHASUMS = LITE / "SHA256SUMS" STYLELINT_CACHE = BUILD / ".stylelintcache" - NPM_TARBALL = DIST / f"deathbeds-jupyterlab-deck-{C.VERSION}.tgz" + NPM_TARBALL = DIST / f"deathbeds-jupyterlab-deck-{C.JS_VERSION}.tgz" DIST_HASH_DEPS = [NPM_TARBALL, WHEEL, SDIST] DIST_SHASUMS = DIST / "SHA256SUMS" ENV_PKG_JSON = ENV / f"share/jupyter/labextensions/{C.NPM_NAME}/{C.PACKAGE_JSON}" PIP_FROZEN = BUILD / "pip-freeze.txt" + PIP_FROZEN_LEGACY = BUILD / "pip-freeze-legacy.txt" REPORTS = BUILD / "reports" ROBOCOV = BUILD / "__robocov__" REPORTS_NYC = REPORTS / "nyc" REPORTS_NYC_LCOV = REPORTS_NYC / "lcov.info" REPORTS_COV_XML = REPORTS / "coverage-xml" PYTEST_HTML = REPORTS / "pytest.html" + PYTEST_HTML_LEGACY = REPORTS / "pytest-legacy.html" PYTEST_COV_XML = REPORTS_COV_XML / "pytest.coverage.xml" + PYTEST_COV_XML_LEGACY = REPORTS_COV_XML / "pytest-legacy.coverage.xml" HTMLCOV_HTML = REPORTS / "htmlcov/index.html" + HTMLCOV_HTML_LEGACY = REPORTS / "htmlcov-legacy/index.html" ROBOT = REPORTS / "robot" - ROBOT_SCREENSHOTS = ROBOT / "screenshots" - ROBOT_LOG_HTML = ROBOT / "log.html" + ROBOT_LATEST = ROBOT / "latest" + ROBOT_LEGACY = ROBOT / "legacy" + ROBOT_LOG_HTML = ROBOT_LATEST / "log.html" PAGES_LITE = BUILD / "pages-lite" PAGES_LITE_SHASUMS = PAGES_LITE / "SHA256SUMS" SPELLING = BUILD / "spelling" @@ -142,20 +177,26 @@ class B: class L: ALL_DOCS_MD = [*P.DOCS.rglob("*.md")] ALL_PY_SRC = [*P.PY_SRC.rglob("*.py")] - ALL_BLACK = [P.DODO, *ALL_PY_SRC, *P.DOCS_PY] - ALL_CSS = [*P.DOCS_STATIC.rglob("*.css"), *P.JS.glob("*/style/**/*.css")] + ALL_PY_SCRIPTS = [*P.SCRIPTS.rglob("*.py")] + ALL_BLACK = [P.DODO, *ALL_PY_SRC, *P.DOCS_PY, *ALL_PY_SCRIPTS] + ALL_CSS_SRC = [*P.JS.glob("*/style/**/*.css")] + ALL_CSS = [*P.DOCS_STATIC.rglob("*.css"), *ALL_CSS_SRC] ALL_JSON = [ - *P.ROOT.glob(".json"), + *P.ALL_PLUGIN_SCHEMA, + *P.EXAMPLES.glob("*.json"), *P.JS.glob("*.json"), *P.JS.glob("*/src/**/*.json"), - *P.ALL_PLUGIN_SCHEMA, + *P.PAGES_LITE.glob("*.json"), + *P.ROOT.glob(".json"), ] ALL_MD = [ - *P.ROOT.glob("*.md"), - *P.DOCS.rglob("*.md"), *P.CI.rglob("*.md"), + *P.DOCS.rglob("*.md"), + *P.EXAMPLES.glob("*.md"), *P.EXAMPLES.glob("*.md"), *P.EXT_JS_PKG.glob("*.md"), + *P.PAGES_LITE.glob("*.md"), + *P.ROOT.glob("*.md"), ] ALL_TS = [*P.JS.glob("*/src/**/*.ts"), *P.JS.glob("*/src/**/*.tsx")] ALL_YML = [*P.BINDER.glob("*.yml"), *P.CI.rglob("*.yml"), *P.ROOT.glob("*.yml")] @@ -165,11 +206,13 @@ class L: class U: + @staticmethod def do(args, **kwargs): cwd = kwargs.pop("cwd", P.ROOT) shell = kwargs.pop("shell", False) return doit.tools.CmdAction(args, shell=shell, cwd=cwd, **kwargs) + @staticmethod def source_date_epoch(): return ( subprocess.check_output(["git", "log", "-1", "--format=%ct"]) @@ -177,6 +220,7 @@ def source_date_epoch(): .strip() ) + @staticmethod def hash_files(hashfile, *hash_deps): from hashlib import sha256 @@ -191,11 +235,18 @@ def hash_files(hashfile, *hash_deps): print(output) hashfile.write_text(output) - def pip_list(): - B.PIP_FROZEN.write_bytes( - subprocess.check_output([sys.executable, "-m", "pip", "freeze"]) + @staticmethod + def pip_list(frozen_file=None, pip_args=None): + frozen_file = frozen_file or B.PIP_FROZEN + pip_args = pip_args or [sys.executable, "-m", "pip"] + frozen_file.parent.mkdir(exist_ok=True, parents=True) + frozen_file.write_bytes( + subprocess.check_output([*pip_args, "list", "--format=freeze"]), ) + with frozen_file.open("a", encoding="utf-8") as fd: + fd.write(f"\n# {time.time()}\n") + @staticmethod def copy_one(src, dest): if not dest.parent.exists(): dest.parent.mkdir(parents=True) @@ -203,18 +254,20 @@ def copy_one(src, dest): dest.unlink() shutil.copy2(src, dest) + @staticmethod def copy_some(dest, srcs): for src in srcs: U.copy_one(src, dest / src.name) + @staticmethod def clean_some(*paths): - for path in paths: if path.is_dir(): shutil.rmtree(path) elif path.exists(): path.unlink() + @staticmethod def ensure_version(path: Path): text = path.read_text(encoding="utf-8") if path.name == C.PACKAGE_JSON: @@ -239,7 +292,9 @@ def ensure_version(path: Path): print(f"Patching {path} with: {expected}") path.write_text(new_text) + return None + @staticmethod def update_env_fragments(dest_env: Path, src_envs: typing.List[Path]): dest_text = dest_env.read_text(encoding="utf-8") print(f"... adding packages to {dest_env.relative_to(P.ROOT)}") @@ -256,54 +311,82 @@ def update_env_fragments(dest_env: Path, src_envs: typing.List[Path]): f" {src_chunk.strip()}", pattern, f" {dest_chunks[2].strip()}", - ] + ], ) dest_env.write_text(dest_text.strip() + "\n") - def make_robot_tasks(extra_args=None): + @staticmethod + def make_robot_tasks(lab_env: Path, out_root: Path, extra_args=None): extra_args = extra_args or [] name = "robot" - file_dep = [*B.HISTORY, *L.ALL_ROBOT] + file_dep = [lab_env / C.HISTORY, *L.ALL_ROBOT] if C.ROBOT_DRYRUN in extra_args: name = f"{name}:dryrun" else: - file_dep += [B.PIP_FROZEN, *L.ALL_PY_SRC, *L.ALL_TS, *L.ALL_JSON] - out_dir = B.ROBOT / U.get_robot_stem(attempt=1, extra_args=extra_args) + file_dep += [*L.ALL_PY_SRC, *L.ALL_TS, *L.ALL_JSON] + if lab_env == B.ENV: + file_dep += [B.PIP_FROZEN] + else: + file_dep += [B.PIP_FROZEN_LEGACY] + out_dir = out_root / U.get_robot_stem( + attempt=1, + extra_args=extra_args, + ) targets = [ out_dir / "output.xml", out_dir / "log.html", out_dir / "report.html", ] actions = [] - if E.WITH_JS_COV and C.ROBOT_DRYRUN not in extra_args: + + if ( + out_root.name == "latest" + and E.WITH_JS_COV + and C.ROBOT_DRYRUN not in extra_args + ): targets += [B.REPORTS_NYC_LCOV] actions += [ (U.clean_some, [B.ROBOCOV, B.REPORTS_NYC]), (doit.tools.create_folder, [B.ROBOCOV]), ] - yield dict( - name=name, - uptodate=[ - doit.tools.config_changed(dict(cov=E.WITH_JS_COV, args=E.ROBOT_ARGS)) + + yield { + "name": name, + "uptodate": [ + doit.tools.config_changed({"cov": E.WITH_JS_COV, "args": E.ROBOT_ARGS}), ], - file_dep=file_dep, - actions=[*actions, (U.run_robot_with_retries, [extra_args])], - targets=targets, - ) + "file_dep": file_dep, + "actions": [ + *actions, + (U.run_robot_with_retries, [lab_env, out_root, extra_args]), + ], + "targets": targets, + } - def run_robot_with_retries(extra_args=None): - attempt = 0 - fail_count = -1 + @staticmethod + def run_robot_with_retries(lab_env, out_root, extra_args=None): extra_args = [*(extra_args or []), *E.ROBOT_ARGS] is_dryrun = C.ROBOT_DRYRUN in extra_args - retries = 0 if is_dryrun else E.ROBOT_RETRIES + fail_count = -1 + + retries = E.ROBOT_RETRIES + attempt = E.ROBOT_ATTEMPT + + if is_dryrun: + retries = 0 + attempt = 0 while fail_count != 0 and attempt <= retries: attempt += 1 - print("attempt {} of {}...".format(attempt, retries + 1), flush=True) + print(f"attempt {attempt} of {retries + 1}...", flush=True) start_time = time.time() - fail_count = U.run_robot(attempt=attempt, extra_args=extra_args) + fail_count = U.run_robot( + lab_env=lab_env, + out_root=out_root, + attempt=attempt, + extra_args=extra_args, + ) print( fail_count, "failed in", @@ -320,22 +403,30 @@ def run_robot_with_retries(extra_args=None): print(f"did not generate any coverage files in {B.ROBOCOV}") fail_count = -2 else: - subprocess.call( - [*C.NYC, f"--report-dir={B.REPORTS_NYC}", f"--temp-dir={B.ROBOCOV}"] + [ + *C.NYC, + f"--report-dir={B.REPORTS_NYC}", + f"--temp-dir={B.ROBOCOV}", + ], ) - final = B.ROBOT / "output.xml" + final = out_root / "output.xml" all_robot = [ str(p) - for p in B.ROBOT.rglob("output.xml") + for p in out_root.rglob("output.xml") if p != final and "dry_run" not in str(p) and "pabot_results" not in str(p) ] + runner = ["python"] + + if lab_env != B.ENV: + runner = [*C.CONDA_RUN, str(lab_env), *runner] + subprocess.call( [ - "python", + *runner, "-m", "robot.rebot", "--name", @@ -344,21 +435,28 @@ def run_robot_with_retries(extra_args=None): "--merge", *all_robot, ], - cwd=B.ROBOT, + cwd=out_root, ) - if B.ROBOT_SCREENSHOTS.exists(): - shutil.rmtree(B.ROBOT_SCREENSHOTS) + screens = out_root / "screenshots" + + if screens.exists(): + shutil.rmtree(screens) - B.ROBOT_SCREENSHOTS.mkdir() + screens.mkdir(parents=True) - for screen_root in B.ROBOT.glob("*/screenshots/*"): - shutil.copytree(screen_root, B.ROBOT_SCREENSHOTS / screen_root.name) + for screen_root in out_root.glob("*/screenshots/*"): + shutil.copytree(screen_root, screens / screen_root.name) return fail_count == 0 - def get_robot_stem(attempt=0, extra_args=None, browser="headlessfirefox"): - """get the directory in B.ROBOT for this platform/app""" + @staticmethod + def get_robot_stem( + attempt=0, + extra_args=None, + browser="headlessfirefox", + ): + """Get the directory in B.ROBOT for this platform/app.""" extra_args = extra_args or [] browser = browser.replace("headless", "") @@ -370,18 +468,44 @@ def get_robot_stem(attempt=0, extra_args=None, browser="headlessfirefox"): return stem - def run_robot(attempt=0, extra_args=None): + @staticmethod + def prep_robot(out_dir: Path): + if out_dir.exists(): + print(f">>> trying to clean out {out_dir}", flush=True) + try: + shutil.rmtree(out_dir) + except Exception as err: + print( + f"... error, hopefully harmless: {err}", + flush=True, + ) + + if not out_dir.exists(): + print( + f">>> trying to prepare output directory: {out_dir}", + flush=True, + ) + try: + out_dir.mkdir(parents=True) + except Exception as err: + print( + f"... Error, hopefully harmless: {err}", + flush=True, + ) + + @staticmethod + def run_robot(out_root: Path, lab_env: Path, attempt=0, extra_args=None): import lxml.etree as ET extra_args = extra_args or [] stem = U.get_robot_stem(attempt=attempt, extra_args=extra_args) - out_dir = B.ROBOT / stem + out_dir = out_root / stem if attempt > 1: extra_args += ["--loglevel", "TRACE"] prev_stem = U.get_robot_stem(attempt=attempt - 1, extra_args=extra_args) - previous = B.ROBOT / prev_stem / "output.xml" + previous = out_root / prev_stem / "output.xml" if previous.exists(): extra_args += ["--rerunfailed", str(previous)] @@ -392,6 +516,10 @@ def run_robot(attempt=0, extra_args=None): *E.PABOT_ARGS, ] + if lab_env == B.ENV_LEGACY: + runner = [*C.CONDA_RUN, str(lab_env), *runner] + extra_args += ["--exclude", "app:nb"] + if C.ROBOT_DRYRUN in extra_args: runner = ["robot"] @@ -412,39 +540,12 @@ def run_robot(attempt=0, extra_args=None): *extra_args, ] - if out_dir.exists(): - print(">>> trying to clean out {}".format(out_dir), flush=True) - try: - shutil.rmtree(out_dir) - except Exception as err: - print( - "... error, hopefully harmless: {}".format(err), - flush=True, - ) - - if not out_dir.exists(): - print( - ">>> trying to prepare output directory: {}".format(out_dir), flush=True - ) - try: - out_dir.mkdir(parents=True) - except Exception as err: - print( - "... Error, hopefully harmless: {}".format(err), - flush=True, - ) + str_args = [*map(str, [*args, P.ROBOT_SUITES])] - str_args = [ - *map( - str, - [ - *args, - P.ROBOT_SUITES, - ], - ) - ] print(">>> ", " ".join(str_args), flush=True) + U.prep_robot(out_dir) + proc = subprocess.Popen(str_args, cwd=P.ATEST) try: @@ -465,9 +566,11 @@ def run_robot(attempt=0, extra_args=None): return fail_count + @staticmethod def rel(*paths): return [p.relative_to(P.ROOT) for p in paths] + @staticmethod def check_one_spell(html: Path, findings: Path): proc = subprocess.Popen( [ @@ -476,7 +579,6 @@ def check_one_spell(html: Path, findings: Path): "-p", P.DOCS_DICTIONARY, "-l", - "-L", "-H", str(html), ], @@ -491,22 +593,75 @@ def check_one_spell(html: Path, findings: Path): print("...", html) print(out_text) return False + return None + @staticmethod def rewrite_links(path: Path): text = path.read_text(encoding="utf-8") text = text.replace(".md", ".html") text = text.replace(".ipynb", ".ipynb.html") path.write_text(text) + @staticmethod + def lab(lab_env: Path): + fake_home = lab_env / ".fake_home" + if fake_home.exists(): + shutil.rmtree(fake_home) + fake_home.mkdir(parents=True) + + env = dict(**os.environ) + env["HOME"] = str(fake_home) + + run_args = [*C.CONDA_RUN, str(lab_env)] + args = [*run_args, "jupyter", "lab", "--config", P.ATEST_JP_CONFIG] + + str_args = list(map(str, args)) + print(">>>", "\t".join(str_args)) + proc = subprocess.Popen(str_args, stdin=subprocess.PIPE, env=env) + + try: + proc.wait() + except KeyboardInterrupt: + print("attempting to stop lab, you may want to check your process monitor") + proc.terminate() + proc.communicate(b"y\n") + + proc.wait() + return True + + @staticmethod + def make_pytest_tasks(file_dep, pytest_html, htmlcov, pytest_cov_xml): + yield { + "name": "pytest", + "file_dep": file_dep, + "actions": [ + [ + "pytest", + "--pyargs", + P.PY_SRC.name, + f"--cov={P.PY_SRC.name}", + "--cov-branch", + "--no-cov-on-fail", + "--cov-fail-under=100", + "--cov-report=term-missing:skip-covered", + f"--cov-report=html:{htmlcov.parent}", + f"--html={pytest_html}", + "--self-contained-html", + f"--cov-report=xml:{pytest_cov_xml}", + ], + ], + "targets": [pytest_html, htmlcov, pytest_cov_xml], + } + def task_env(): for env_dest, env_src in P.ENV_INHERIT.items(): - yield dict( - name=f"conda:{env_dest.name}", - targets=[env_dest], - file_dep=[*env_src], - actions=[(U.update_env_fragments, [env_dest, env_src])], - ) + yield { + "name": f"conda:{env_dest.name}", + "targets": [env_dest], + "file_dep": [*env_src], + "actions": [(U.update_env_fragments, [env_dest, env_src])], + } def task_setup(): @@ -515,48 +670,97 @@ def task_setup(): dedupe = [] if E.LOCAL: - dedupe = [["jlpm", "yarn-deduplicate", "-s", "fewer", "--fail"]] - yield dict( - name="conda", - file_dep=[P.DEMO_ENV_YAML], - targets=[*B.HISTORY], - actions=[ - ["mamba", "env", "update", "--prefix", B.ENV, "--file", P.DEMO_ENV_YAML] + dedupe = [["jlpm", "yarn-berry-deduplicate", "-s", "fewer", "--fail"]] + yield { + "name": "conda", + "file_dep": [P.DEMO_ENV_YAML], + "targets": [*B.HISTORY], + "actions": [ + [ + "mamba", + "env", + "update", + "--prefix", + B.ENV, + "--file", + P.DEMO_ENV_YAML, + ], ], - ) + } if E.LOCAL or not B.YARN_INTEGRITY.exists(): - yield dict( - name="yarn", - file_dep=[ + yield { + "name": "yarn", + "file_dep": [ P.YARNRC, *B.HISTORY, *P.ALL_PACKAGE_JSONS, *([P.YARN_LOCK] if P.YARN_LOCK.exists() else []), ], - actions=[ - ["jlpm", *([] if E.LOCAL else ["--frozen-lockfile"])], + "actions": [ + ["jlpm", *([] if E.LOCAL else ["--immutable"])], *dedupe, ], - targets=[B.YARN_INTEGRITY], - ) + "targets": [B.YARN_INTEGRITY], + } -def task_watch(): - yield dict( - name="js", - actions=[["jlpm", "lerna", "run", "watch", "--stream", "--parallel"]], - file_dep=[B.YARN_INTEGRITY], +def task_legacy(): + yield { + "name": "conda", + "file_dep": [P.TEST_35_ENV_YAML], + "targets": [B.HISTORY_LEGACY], + "actions": [ + [ + "mamba", + "env", + "update", + "--prefix", + B.ENV_LEGACY, + "--file", + P.TEST_35_ENV_YAML, + ], + ], + } + + legacy_pip = [*C.CONDA_RUN, B.ENV_LEGACY, "python", "-m", "pip"] + + yield { + "name": "pip", + "file_dep": [B.HISTORY_LEGACY, B.WHEEL], + "targets": [B.PIP_FROZEN_LEGACY], + "actions": [ + [*legacy_pip, "install", "--no-deps", "--ignore-installed", B.WHEEL], + [*legacy_pip, "check"], + (U.pip_list, [B.PIP_FROZEN_LEGACY, legacy_pip]), + ], + } + + yield from U.make_pytest_tasks( + file_dep=[B.PIP_FROZEN_LEGACY], + pytest_html=B.PYTEST_HTML_LEGACY, + htmlcov=B.HTMLCOV_HTML_LEGACY, + pytest_cov_xml=B.PYTEST_COV_XML_LEGACY, ) + yield from U.make_robot_tasks(lab_env=B.ENV_LEGACY, out_root=B.ROBOT_LEGACY) + + +def task_watch(): + yield { + "name": "js", + "actions": [["jlpm", "lerna", "run", "watch", "--stream", "--parallel"]], + "file_dep": [B.YARN_INTEGRITY], + } + def task_docs(): - yield dict( - name="sphinx", - file_dep=[*P.DOCS_PY, *L.ALL_MD, *B.HISTORY, B.WHEEL, B.LITE_SHASUMS], - actions=[["sphinx-build", "-b", "html", "docs", "build/docs"]], - targets=[B.DOCS_BUILDINFO], - ) + yield { + "name": "sphinx", + "file_dep": [*P.DOCS_PY, *L.ALL_MD, *B.HISTORY, B.WHEEL, B.LITE_SHASUMS], + "actions": [["sphinx-build", "-b", "html", "docs", "build/docs"]], + "targets": [B.DOCS_BUILDINFO], + } @doit.create_after("docs") @@ -572,21 +776,21 @@ def task_check(): for example in P.EXAMPLES.glob("*.ipynb"): out_html = B.EXAMPLE_HTML / f"{example.name}.html" all_spell += [out_html] - yield dict( - name=f"nbconvert:{example.name}", - actions=[ + yield { + "name": f"nbconvert:{example.name}", + "actions": [ (doit.tools.create_folder, [B.EXAMPLE_HTML]), ["jupyter", "nbconvert", "--to=html", "--output", out_html, example], (U.rewrite_links, [out_html]), ], - file_dep=[example], - targets=[out_html], - ) - - yield dict( - name="links", - file_dep=[B.DOCS_BUILDINFO, *all_html], - actions=[ + "file_dep": [example], + "targets": [out_html], + } + + yield { + "name": "links", + "file_dep": [B.DOCS_BUILDINFO, *all_html], + "actions": [ [ "pytest-check-links", "-vv", @@ -594,22 +798,22 @@ def task_check(): "--check-links-ignore", "http.*", *all_html, - ] + ], ], - ) + } for html_path in all_spell: stem = html_path.relative_to(P.ROOT) report = B.SPELLING / f"{stem}.txt" - yield dict( - name=f"spelling:{stem}", - actions=[ + yield { + "name": f"spelling:{stem}", + "actions": [ (doit.tools.create_folder, [report.parent]), (U.check_one_spell, [html_path, report]), ], - file_dep=[html_path, P.DOCS_DICTIONARY], - targets=[report], - ) + "file_dep": [html_path, P.DOCS_DICTIONARY], + "targets": [report], + } def task_dist(): @@ -632,34 +836,34 @@ def build_with_sde(): ) return rc == 0 - yield dict( - name="flit", - file_dep=[*L.ALL_PY_SRC, P.PYPROJECT_TOML, B.STATIC_PKG_JSON], - actions=[build_with_sde], - targets=[B.WHEEL, B.SDIST], - ) + yield { + "name": "flit", + "file_dep": [*L.ALL_PY_SRC, P.PYPROJECT_TOML, B.STATIC_PKG_JSON], + "actions": [build_with_sde], + "targets": [B.WHEEL, B.SDIST], + } - yield dict( - name="npm", - file_dep=[ + yield { + "name": "npm", + "file_dep": [ B.JS_META_TSBUILDINFO, *P.ALL_PACKAGE_JSONS, P.EXT_JS_README, P.EXT_JS_LICENSE, ], - targets=[B.NPM_TARBALL], - actions=[ + "targets": [B.NPM_TARBALL], + "actions": [ (doit.tools.create_folder, [B.DIST]), U.do(["npm", "pack", P.EXT_JS_PKG], cwd=B.DIST), ], - ) + } - yield dict( - name="hash", - file_dep=[*B.DIST_HASH_DEPS], - targets=[B.DIST_SHASUMS], - actions=[(U.hash_files, [B.DIST_SHASUMS, *B.DIST_HASH_DEPS])], - ) + yield { + "name": "hash", + "file_dep": [*B.DIST_HASH_DEPS], + "targets": [B.DIST_SHASUMS], + "actions": [(U.hash_files, [B.DIST_SHASUMS, *B.DIST_HASH_DEPS])], + } def task_dev(): @@ -668,21 +872,28 @@ def task_dev(): pip_args = [ci_artifact] py_dep = [ci_artifact] else: - py_dep = [B.ENV_PKG_JSON] + py_dep = [B.STATIC_PKG_JSON] pip_args = [ "-e", ".", "--ignore-installed", "--no-deps", + "--no-build-isolation", + "--disable-pip-version-check", ] - yield dict( - name="ext", - actions=[ - ["jupyter", "labextension", "develop", "--overwrite", "."], + yield { + "name": "ext", + "actions": [ + ["python", P.SCRIPT_LABEXT, "develop", "--debug", "--overwrite", "."], ], - file_dep=[B.STATIC_PKG_JSON, *P.ALL_PLUGIN_SCHEMA], - targets=[B.ENV_PKG_JSON], - ) + "file_dep": [ + B.STATIC_PKG_JSON, + *P.ALL_PLUGIN_SCHEMA, + P.SCRIPT_LABEXT, + B.PIP_FROZEN, + ], + "targets": [B.ENV_PKG_JSON], + } check = [] @@ -690,48 +901,32 @@ def task_dev(): # avoid sphinx-rtd-theme check = [[sys.executable, "-m", "pip", "check"]] - yield dict( - name="py", - file_dep=py_dep, - targets=[B.PIP_FROZEN], - actions=[ + yield { + "name": "pip", + "file_dep": py_dep, + "targets": [B.PIP_FROZEN], + "actions": [ [sys.executable, "-m", "pip", "install", "-vv", *pip_args], *check, - (doit.tools.create_folder, [B.BUILD]), U.pip_list, ], - ) + } def task_test(): - file_dep = [B.STATIC_PKG_JSON, *L.ALL_PY_SRC] + file_dep = [B.PIP_FROZEN] - if E.TESTING_IN_CI: - file_dep = [] + if not E.TESTING_IN_CI: + file_dep += [B.STATIC_PKG_JSON, *L.ALL_PY_SRC] - yield dict( - name="pytest", - file_dep=[B.PIP_FROZEN, *file_dep], - actions=[ - [ - "pytest", - "--pyargs", - P.PY_SRC.name, - f"--cov={P.PY_SRC.name}", - "--cov-branch", - "--no-cov-on-fail", - "--cov-fail-under=100", - "--cov-report=term-missing:skip-covered", - f"--cov-report=html:{B.HTMLCOV_HTML.parent}", - f"--html={B.PYTEST_HTML}", - "--self-contained-html", - f"--cov-report=xml:{B.PYTEST_COV_XML}", - ] - ], - targets=[B.PYTEST_HTML, B.HTMLCOV_HTML, B.PYTEST_COV_XML], + yield from U.make_pytest_tasks( + file_dep=file_dep, + pytest_html=B.PYTEST_HTML, + htmlcov=B.HTMLCOV_HTML, + pytest_cov_xml=B.PYTEST_COV_XML, ) - yield from U.make_robot_tasks() + yield from U.make_robot_tasks(lab_env=B.ENV, out_root=B.ROBOT_LATEST) def task_lint(): @@ -743,24 +938,17 @@ def task_lint(): path = pkg_json.parent.relative_to(P.ROOT) name = f"js:{C.PACKAGE_JSON}:{path}" pkg_json_tasks += [f"lint:{name}"] - yield dict( - uptodate=[version_uptodate], - name=f"js:version:{path}", - file_dep=[pkg_json], - actions=[(U.ensure_version, [pkg_json])], - ) - yield dict( - name=name, - task_dep=[f"lint:js:version:{path}"], - file_dep=[pkg_json, B.YARN_INTEGRITY], - actions=[["jlpm", "prettier-package-json", "--write", *U.rel(pkg_json)]], - ) - - yield dict( - name="js:prettier", - file_dep=[*L.ALL_PRETTIER, B.YARN_INTEGRITY], - task_dep=pkg_json_tasks, - actions=[ + yield { + "name": name, + "file_dep": [pkg_json, B.YARN_INTEGRITY], + "actions": [["jlpm", "prettier-package-json", "--write", *U.rel(pkg_json)]], + } + + yield { + "name": "js:prettier", + "file_dep": [*L.ALL_PRETTIER, B.YARN_INTEGRITY], + "task_dep": pkg_json_tasks, + "actions": [ [ "jlpm", "stylelint", @@ -778,120 +966,128 @@ def task_lint(): *U.rel(*L.ALL_PRETTIER), ], ], - ) + } - yield dict( - name="js:eslint", - task_dep=["lint:js:prettier"], - file_dep=[*L.ALL_TS, P.ESLINTRC, B.YARN_INTEGRITY], - actions=[ + yield { + "name": "js:eslint", + "task_dep": ["lint:js:prettier"], + "file_dep": [*L.ALL_TS, *P.ALL_PACKAGE_JSONS, B.YARN_INTEGRITY], + "actions": [ [ "jlpm", "eslint", "--cache", "--cache-location", *U.rel(B.BUILD / ".eslintcache"), - "--config", - *U.rel(P.ESLINTRC), "--ext", ".js,.jsx,.ts,.tsx", *([] if E.IN_CI else ["--fix"]), *U.rel(P.JS), - ] + ], ], - ) + } - yield dict( - name="version:py", - uptodate=[version_uptodate], - file_dep=[P.PYPROJECT_TOML], - actions=[(U.ensure_version, [P.PYPROJECT_TOML])], - ) + yield { + "name": "version:py", + "uptodate": [version_uptodate], + "file_dep": [P.PYPROJECT_TOML], + "actions": [(U.ensure_version, [P.PYPROJECT_TOML])], + } check = ["--check"] if E.IN_CI else [] rel_black = U.rel(*L.ALL_BLACK) - yield dict( - name="py:black", - file_dep=[*L.ALL_BLACK, *B.HISTORY, P.PYPROJECT_TOML], - task_dep=["lint:version:py"], - actions=[ - ["isort", *check, *rel_black], + yield { + "name": "py:black", + "file_dep": [*L.ALL_BLACK, *B.HISTORY, P.PYPROJECT_TOML], + "task_dep": ["lint:version:py"], + "actions": [ ["ssort", *check, *rel_black], ["black", *check, *rel_black], + ["ruff", "--fix-only", *rel_black], ], - ) + } - yield dict( - name="py:pyflakes", - file_dep=[*L.ALL_BLACK, *B.HISTORY, P.PYPROJECT_TOML], - task_dep=["lint:py:black"], - actions=[["pyflakes", *rel_black]], - ) + yield { + "name": "py:ruff", + "file_dep": [*L.ALL_BLACK, *B.HISTORY, P.PYPROJECT_TOML], + "task_dep": ["lint:py:black"], + "actions": [["ruff", *rel_black]], + } - yield dict( - name="robot:tidy", - file_dep=[*L.ALL_ROBOT, *B.HISTORY], - actions=[["robotidy", *U.rel(P.ATEST)]], - ) + yield { + "name": "robot:tidy", + "file_dep": [*L.ALL_ROBOT, *B.HISTORY], + "actions": [["robotidy", *U.rel(P.ATEST)]], + } - yield dict( - name="robot:cop", - task_dep=["lint:robot:tidy"], - file_dep=[*L.ALL_ROBOT, *B.HISTORY], - actions=[["robocop", *U.rel(P.ATEST)]], - ) + yield { + "name": "robot:cop", + "task_dep": ["lint:robot:tidy"], + "file_dep": [*L.ALL_ROBOT, *B.HISTORY], + "actions": [["robocop", *U.rel(P.ATEST)]], + } - yield from U.make_robot_tasks(extra_args=[C.ROBOT_DRYRUN]) + yield from U.make_robot_tasks( + lab_env=B.ENV, + out_root=B.ROBOT_LATEST, + extra_args=[C.ROBOT_DRYRUN], + ) def task_build(): for dest in [P.EXT_JS_README, P.EXT_JS_LICENSE]: src = P.ROOT / dest.name - yield dict( - name=f"meta:js:{dest.name}", - file_dep=[src], - actions=[(U.copy_one, [src, dest])], - targets=[dest], - ) - - uptodate = [doit.tools.config_changed(dict(WITH_JS_COV=E.WITH_JS_COV))] - - ext_dep = [*P.JS_PACKAGE_JSONS, P.EXT_JS_WEBPACK] + yield { + "name": f"meta:js:{dest.name}", + "file_dep": [src], + "actions": [(U.copy_one, [src, dest])], + "targets": [dest], + } + + uptodate = [doit.tools.config_changed({"WITH_JS_COV": E.WITH_JS_COV})] + + ext_dep = [ + *P.JS_PACKAGE_JSONS, + P.EXT_JS_WEBPACK, + *L.ALL_CSS_SRC, + *L.ALL_TS, + *L.ALL_CSS_SRC, + ] if E.WITH_JS_COV: ext_task = "labextension:build:cov" else: ext_task = "labextension:build" ext_dep += [B.JS_META_TSBUILDINFO] - yield dict( - uptodate=uptodate, - name="js", - actions=[["jlpm", "lerna", "run", "build"]], - file_dep=[*L.ALL_TS, B.YARN_INTEGRITY], - targets=[B.JS_META_TSBUILDINFO], - ) - - yield dict( - uptodate=uptodate, - name="ext", - actions=[["jlpm", "lerna", "run", ext_task]], - file_dep=ext_dep, - targets=[B.STATIC_PKG_JSON], - ) + yield { + "uptodate": uptodate, + "name": "js", + "actions": [["jlpm", "lerna", "run", "build"]], + "file_dep": [*L.ALL_TS, B.YARN_INTEGRITY], + "targets": [B.JS_META_TSBUILDINFO], + } + + yield { + "uptodate": uptodate, + "name": "ext", + "actions": [["jlpm", "lerna", "run", ext_task]], + "file_dep": ext_dep, + "targets": [B.STATIC_PKG_JSON], + } def task_site(): - yield dict( - name="build", - file_dep=[ + yield { + "name": "build", + "file_dep": [ P.PAGES_LITE_CONFIG, P.PAGES_LITE_JSON, B.ENV_PKG_JSON, B.ROBOT_LOG_HTML, B.PIP_FROZEN, ], - targets=[B.PAGES_LITE_SHASUMS], - actions=[ + "targets": [B.PAGES_LITE_SHASUMS], + "actions": [ U.do( ["jupyter", "lite", "--debug", "build"], cwd=P.PAGES_LITE, @@ -901,22 +1097,21 @@ def task_site(): cwd=P.PAGES_LITE, ), ], - ) + } def task_lite(): - - yield dict( - name="build", - file_dep=[ + yield { + "name": "build", + "file_dep": [ P.LITE_CONFIG, P.LITE_JSON, B.ENV_PKG_JSON, *P.ALL_EXAMPLES, B.PIP_FROZEN, ], - targets=[B.LITE_SHASUMS], - actions=[ + "targets": [B.LITE_SHASUMS], + "actions": [ U.do( ["jupyter", "lite", "--debug", "build"], cwd=P.EXAMPLES, @@ -926,32 +1121,37 @@ def task_lite(): cwd=P.EXAMPLES, ), ], - ) + } def task_serve(): + yield { + "name": "lab", + "uptodate": [lambda: False], + "file_dep": [B.ENV_PKG_JSON, B.PIP_FROZEN], + "actions": [doit.tools.PythonInteractiveAction(U.lab, [B.ENV])], + } - import subprocess - - def lab(): - proc = subprocess.Popen( - list(map(str, ["jupyter", "lab", "--no-browser", "--debug"])), - stdin=subprocess.PIPE, - ) - - try: - proc.wait() - except KeyboardInterrupt: - print("attempting to stop lab, you may want to check your process monitor") - proc.terminate() - proc.communicate(b"y\n") + yield { + "name": "lab:legacy", + "uptodate": [lambda: False], + "file_dep": [B.PIP_FROZEN_LEGACY], + "actions": [doit.tools.PythonInteractiveAction(U.lab, [B.ENV_LEGACY])], + } - proc.wait() - return True - yield dict( - name="lab", - uptodate=[lambda: False], - file_dep=[B.ENV_PKG_JSON, B.PIP_FROZEN], - actions=[doit.tools.PythonInteractiveAction(lab)], - ) +# otherwise it goes.... somewhere +{ + os.environ.update({k: f"{v}"}) + for k, v in { + "JUPYTER_PLATFORM_DIRS": 1, + "MOZ_HEADLESS": E.MOZ_HEADLESS, + "NX_CACHE_DIRECTORY": P.ROOT / "build/.cache/nx", + "NX_PROJECT_GRAPH_CACHE_DIRECTORY": P.ROOT / "build/.cache/nx-graph", + "PYDEVD_DISABLE_FILE_VALIDATION": 1, + }.items() + if k not in os.environ +} + +if dotenv_loaded: + os.environ.update(dotenv_loaded) diff --git a/examples/History.ipynb b/examples/History.ipynb index 17b8be6..ff1cfc2 100644 --- a/examples/History.ipynb +++ b/examples/History.ipynb @@ -25,11 +25,11 @@ "tags": [] }, "source": [ - "```{note}\n", - "Before we go any further, **PRETTY PLEASE**\n", - "- f11 to view full-screen\n", - "- ctrl+ up to something comfortable to you (maybe around 200%)\n", - "```" + "> **Note**\n", + "> \n", + "> Before we go any further, **PRETTY PLEASE**\n", + "> - f11 to view full-screen\n", + "> - ctrl+ up to something comfortable to you (maybe around 200%)" ] }, { @@ -303,7 +303,7 @@ "source": [ "### `jupyterlab-drawio`\n", "\n", - "> `jupyterlab-drawio` demonstrated a direct embedding of the core `mxgraph`, editor inside JupyterLab." + "> `jupyterlab-drawio` demonstrated a direct embedding of the core `mxgraph` editor inside JupyterLab." ] }, { @@ -472,10 +472,9 @@ "source": [ "## Thanks!\n", "\n", - " \n", - "```{hint}\n", - "[Back to the main Deck](./README.ipynb#Documents)\n", - "```\n", + "> **Hint**\n", + ">\n", + "> [Back to the main Deck](./README.ipynb#Documents)\n", "\n", "... or learn more, below..." ] @@ -519,7 +518,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.6" + "version": "3.11.5" }, "toc-autonumbering": true }, diff --git a/examples/Layers.ipynb b/examples/Layers.ipynb index 553ff60..e3ad645 100644 --- a/examples/Layers.ipynb +++ b/examples/Layers.ipynb @@ -133,9 +133,9 @@ "tags": [] }, "source": [ - "```{note}\n", - "`fragment` layer with a `top` of `60%`\n", - "```" + "> **Note**\n", + "> \n", + "> `fragment` layer with a `top` of `60%`" ] }, { @@ -173,9 +173,9 @@ "tags": [] }, "source": [ - "```{warning}\n", - "`slide` layer with a `top` of `30%`\n", - "```" + "> **Note**\n", + "> \n", + "> `slide` layer with a `top` of `30%`" ] }, { @@ -214,9 +214,9 @@ "tags": [] }, "source": [ - "```{hint}\n", - "`stack` layer with a `top` of `5%`\n", - "```" + "> **Hint**\n", + "> \n", + "> `stack` layer with a `top` of `5%`" ] }, { @@ -260,7 +260,7 @@ }, "source": [ "- `top`, `bottom`, `right`, `left`, `width` and `height` do what they say\n", - " - using relative values, `0` to `100%`, is the most *reliable" + " - using relative values, `0` to `100%`, is the most *reliable*" ] }, { @@ -301,9 +301,9 @@ "source": [ "- `pointer-events` can be set to `\"none\"` which makes an element **non-interactive**\n", "\n", - "```{danger}\n", - "It's **pretty hard** to fix elements like this while presenting!\n", - "```" + "> **Danger**\n", + "> \n", + "> It's **pretty hard** to fix elements like this while presenting!" ] }, { @@ -335,9 +335,9 @@ "id": "2b365b99-8c62-4a0e-9179-bb9b3337aa14", "metadata": {}, "source": [ - "```{note}\n", - "The slide above has `\"zoom\": \"200%\"`... but always remember to crank up your browser zoom, anyway.\n", - "```" + "> **Note**\n", + ">\n", + "> The slide above has `\"zoom\": \"200%\"`... but always remember to crank up your browser zoom, anyway." ] } ], @@ -361,7 +361,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.6" + "version": "3.11.5" } }, "nbformat": 4, diff --git a/examples/README.ipynb b/examples/README.ipynb index 229519e..e46e40b 100644 --- a/examples/README.ipynb +++ b/examples/README.ipynb @@ -4,6 +4,7 @@ "cell_type": "markdown", "id": "b9ef2cd6-b100-4e32-856e-831034e5c829", "metadata": { + "editable": true, "slideshow": { "slide_type": "slide" }, @@ -20,11 +21,11 @@ "id": "10231e4c-f9b0-484b-b77c-28aff72c8408", "metadata": {}, "source": [ - "```{note}\n", - "Before we go any further, **PRETTY PLEASE**\n", - "- f11 to view full-screen\n", - "- ctrl+ up to something comfortable to you (maybe around 200%)\n", - "```" + "> **Note**\n", + "> \n", + "> Before we go any further, **PRETTY PLEASE**\n", + "> - f11 to view full-screen\n", + "> - ctrl+ up to something comfortable to you (maybe around 200%)" ] }, { @@ -54,6 +55,7 @@ "cell_type": "markdown", "id": "8c7c3ba2-6741-4be8-b0b5-677c7f50afd7", "metadata": { + "editable": true, "slideshow": { "slide_type": "slide" }, @@ -238,9 +240,9 @@ "source": [ "### This is a Subslide\n", "\n", - "```{hint}\n", - "- go back to the `slide` above with ↑ or shift space\n", - "```" + "> **Hint**\n", + ">\n", + "> go back to the `slide` above with ↑ or shift space\n" ] }, { @@ -561,9 +563,9 @@ "tags": [] }, "source": [ - "```{hint}\n", - "For more themey goodness, try [jupyterlab-fonts](https://github.com/deathbeds/jupyterlab-fonts).\n", - "```" + "> **Hint**\n", + "> \n", + "> For more themey goodness, try [jupyterlab-fonts](https://github.com/deathbeds/jupyterlab-fonts)." ] }, { @@ -633,6 +635,7 @@ "cell_type": "markdown", "id": "3f5bdf51-553c-4ca9-ba02-08a09e4cef28", "metadata": { + "editable": true, "slideshow": { "slide_type": "fragment" }, @@ -666,7 +669,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.6" + "version": "3.11.5" }, "toc-autonumbering": true, "toc-showcode": false, diff --git a/examples/jupyter-lite.json b/examples/jupyter-lite.json index ed9a244..2215ec8 100644 --- a/examples/jupyter-lite.json +++ b/examples/jupyter-lite.json @@ -2,11 +2,6 @@ "jupyter-lite-schema-version": 0, "jupyter-config-data": { "appName": "jupyterlab-deck", - "collaborative": true, - "exposeAppInBrowser": true, - "disabledExtensions": [ - "@jupyterlite/javascript-kernel-extension", - "jupyterlab-videochat:rooms-server" - ] + "exposeAppInBrowser": true } } diff --git a/js/.eslintrc.js b/js/.eslintrc.js deleted file mode 100644 index b50941c..0000000 --- a/js/.eslintrc.js +++ /dev/null @@ -1,91 +0,0 @@ -module.exports = { - env: { - browser: true, - es6: true, - commonjs: true, - node: true, - }, - globals: { JSX: 'readonly' }, - root: true, - extends: [ - 'eslint:recommended', - 'plugin:import/errors', - 'plugin:import/warnings', - 'plugin:import/typescript', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended', - 'prettier', - 'plugin:react/recommended', - ], - ignorePatterns: [ - '**/node_modules/**/*', - '**/lib/**/*', - '**/_*.ts', - '**/_*.d.ts', - '**/typings/**/*.d.ts', - '**/dist/*', - 'js/.eslintrc.js', - ], - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'js/tsconfig.eslint.json', - }, - plugins: ['@typescript-eslint', 'import'], - rules: { - '@typescript-eslint/camelcase': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-empty-interface': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }], - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-namespace': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], - '@typescript-eslint/no-use-before-define': 'off', - '@typescript-eslint/no-var-requires': 'off', - 'no-case-declarations': 'warn', - 'no-control-regex': 'warn', - 'no-inner-declarations': 'off', - 'no-prototype-builtins': 'off', - 'no-undef': 'warn', - 'no-useless-escape': 'off', - 'prefer-const': 'off', - 'import/no-unresolved': 'off', - // the default, but for reference... - 'import/order': [ - 'warn', - { - groups: [ - 'builtin', - 'external', - 'parent', - 'sibling', - 'index', - 'object', - 'unknown', - ], - pathGroups: [ - { pattern: 'react/**', group: 'builtin', order: 'after' }, - { pattern: 'codemirror/**', group: 'external', order: 'before' }, - { pattern: '@lumino/**', group: 'builtin', order: 'before' }, - { pattern: '@jupyterlab/**', group: 'external', order: 'after' }, - ], - 'newlines-between': 'always', - alphabetize: { order: 'asc' }, - }, - ], - // deviations from jupyterlab, should probably be fixed - 'import/no-cycle': 'off', // somehow we lapsed here... ~200 cycles now - 'import/export': 'off', // we do class/interface + NS pun exports _all over_ - '@typescript-eslint/triple-slash-reference': 'off', - 'no-async-promise-executor': 'off', - 'prefer-spread': 'off', - 'react/display-name': 'off', - }, - settings: { - react: { - version: 'detect', - }, - }, -}; diff --git a/js/_meta/package.json b/js/_meta/package.json index beca711..0332ce9 100644 --- a/js/_meta/package.json +++ b/js/_meta/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@deathbeds/jupyterlab-deck-metapackage", - "version": "0.1.4", + "version": "0.0.0", "description": "JupyterLab Deck - Metapackage", "license": "BSD-3-Clause", "author": "jupyterlab-deck contributors", @@ -20,6 +20,6 @@ }, "types": "lib/index.d.ts", "dependencies": { - "@deathbeds/jupyterlab-deck": "file:../jupyterlab-deck" + "@deathbeds/jupyterlab-deck": "workspace:^" } } diff --git a/js/jupyterlab-deck/README.md b/js/jupyterlab-deck/README.md index f031938..a9856a6 100644 --- a/js/jupyterlab-deck/README.md +++ b/js/jupyterlab-deck/README.md @@ -7,10 +7,11 @@ [binder-badge]: https://mybinder.org/badge_logo.svg [binder]: https://mybinder.org/v2/gh/deathbeds/jupyterlab-deck/HEAD?urlpath=lab/tree/examples/README.ipynb -[ci-badge]: https://img.shields.io/github/workflow/status/deathbeds/jupyterlab-deck/CI +[ci-badge]: + https://img.shields.io/github/actions/workflow/status/deathbeds/jupyterlab-deck/ci.yml [ci]: https://github.com/deathbeds/jupyterlab-deck/actions?query=branch%3Amain [reports-badge]: - https://img.shields.io/github/workflow/status/deathbeds/jupyterlab-deck/pages?label=reports + https://img.shields.io/github/actions/workflow/status/deathbeds/jupyterlab-deck/pages.yml?label=reports [reports]: https://deathbeds.github.io/jupyterlab-deck/lab/index.html?path=README.ipynb [rtd-badge]: https://img.shields.io/readthedocs/jupyterlab-deck [rtd]: https://jupyterlab-deck.rtfd.io @@ -164,7 +165,7 @@ following _scopes_: ### Design Tools -> In [Deck mode](#deck-mode), click the _ellipsis_ icon in the bottom right corner +> In [Deck mode](#deck-mode), click the _ellipsis_ icon in the bottom left corner The design tools offer lightweight buttons to: @@ -230,9 +231,10 @@ restore the part to the default layout. ### Does it work with `notebook 7`? -**Not yet.** Navigating multiple documents during the same presentation will probably +**Mostly.** Navigating multiple documents during the same presentation will probably never work, as this is incompatible with the one-document-at-a-time design constraint of -the Notebook UX. +the Notebook UX. Each skip to another document will open a new browser tab, though deck +should be installed. ### Will it generate PowerPoint? diff --git a/js/jupyterlab-deck/package.json b/js/jupyterlab-deck/package.json index 388be4b..c5ef383 100644 --- a/js/jupyterlab-deck/package.json +++ b/js/jupyterlab-deck/package.json @@ -1,6 +1,6 @@ { "name": "@deathbeds/jupyterlab-deck", - "version": "0.1.4", + "version": "0.2.0-alpha.0", "description": "Lightweight presentations for JupyterLab", "license": "BSD-3-Clause", "author": "jupyterlab-deck contributors", @@ -14,28 +14,30 @@ }, "main": "lib/index.js", "scripts": { - "labextension:build": "jupyter labextension build .", - "labextension:build:cov": "tsc -b src/tsconfig.cov.json && jupyter labextension build .", - "watch": "jupyter labextension watch ." + "labextension": "python ../../_scripts/labextension.py", + "labextension:build": "jlpm labextension build --debug .", + "labextension:build:cov": "tsc -b src/tsconfig.cov.json && jlpm labextension:build", + "watch": "jlpm labextension watch ." }, "types": "lib/index.d.ts", "dependencies": { - "@jupyterlab/application": "3", - "@jupyterlab/apputils": "3", - "@jupyterlab/markdownviewer": "3", - "@jupyterlab/notebook": "3", - "@jupyterlab/statusbar": "3", - "@jupyterlab/ui-components": "3", + "@jupyterlab/application": "3 || 4", + "@jupyterlab/apputils": "3 || 4", + "@jupyterlab/markdownviewer": "3 || 4", + "@jupyterlab/notebook": "3 || 4", + "@jupyterlab/statusbar": "3 || 4", + "@jupyterlab/ui-components": "3 || 4", "d3-drag": "3" }, "devDependencies": { - "@deathbeds/jupyterlab-fonts": "^2.1.1", - "@jupyterlab/builder": "^3.4.8", + "@deathbeds/jupyterlab-fonts": "^3.0.0-alpha.3", + "@jupyter-notebook/application": "^7.0.5", + "@jupyterlab/builder": "^4.0.7", "@types/d3-drag": "3" }, "jupyterlab": { "extension": "lib/plugin.js", - "outputDir": "../../src/jupyterlab_deck/_d/share/jupyter/labextensions/@deathbeds/jupyterlab-deck", + "outputDir": "../../src/_d/share/jupyter/labextensions/@deathbeds/jupyterlab-deck", "schemaDir": "schema", "webpackConfig": "./webpack.config.js", "sharedPackages": { diff --git a/js/jupyterlab-deck/src/labcompat.ts b/js/jupyterlab-deck/src/labcompat.ts new file mode 100644 index 0000000..c7a1df7 --- /dev/null +++ b/js/jupyterlab-deck/src/labcompat.ts @@ -0,0 +1,28 @@ +import type { ICellModel } from '@jupyterlab/cells'; +import type { INotebookModel } from '@jupyterlab/notebook'; +import { toArray } from '@lumino/algorithm'; +import { JSONExt } from '@lumino/coreutils'; +import type { DockPanel, TabBar, Widget } from '@lumino/widgets'; + +const { emptyArray } = JSONExt; + +export function getTabBars(dockPanel: DockPanel): TabBar[] { + if (!dockPanel) { + return emptyArray as any as TabBar[]; + } + + return toArray(dockPanel.tabBars()); +} + +export function getCellModels(notebookModel: INotebookModel): ICellModel[] { + if (!notebookModel) { + return emptyArray as any as ICellModel[]; + } + + return toArray(notebookModel.cells); +} + +export function getSelectedWidget(dockPanel: DockPanel): Widget | null { + const selectedWidgets = toArray(dockPanel.selectedWidgets()); + return selectedWidgets.length ? selectedWidgets[0] : null; +} diff --git a/js/jupyterlab-deck/src/manager.ts b/js/jupyterlab-deck/src/manager.ts index 85c379c..95ea949 100644 --- a/js/jupyterlab-deck/src/manager.ts +++ b/js/jupyterlab-deck/src/manager.ts @@ -1,15 +1,16 @@ import { IFontManager } from '@deathbeds/jupyterlab-fonts'; import { GlobalStyles } from '@deathbeds/jupyterlab-fonts/lib/_schema'; -import { LabShell } from '@jupyterlab/application'; +import type { INotebookShell } from '@jupyter-notebook/application'; +import { LabShell, JupyterFrontEnd, ILabShell } from '@jupyterlab/application'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { StatusBar } from '@jupyterlab/statusbar'; import { TranslationBundle } from '@jupyterlab/translation'; -import { each } from '@lumino/algorithm'; import { CommandRegistry } from '@lumino/commands'; import { Signal, ISignal } from '@lumino/signaling'; import { Widget, DockPanel } from '@lumino/widgets'; import { ICONS } from './icons'; +import { getSelectedWidget, getTabBars } from './labcompat'; import { IDeckManager, DATA, @@ -37,12 +38,14 @@ export class DeckManager implements IDeckManager { protected _activeChanged = new Signal(this); protected _activeWidget: Widget | null = null; protected _presenters: IPresenter[] = []; - protected _appStarted: Promise; + protected _appStarted: () => Promise; protected _commands: CommandRegistry; protected _remote: DeckRemote | null = null; protected _designTools: DesignTools | null = null; protected _settings: Promise; - protected _shell: LabShell; + protected _labShell?: LabShell | null; + protected _dockPanel: DockPanel | null = null; + protected _shell: JupyterFrontEnd.IShell; protected _statusbar: StatusBar | null; protected _statusBarWasEnabled = false; protected _styleCache = new Map(); @@ -59,13 +62,17 @@ export class DeckManager implements IDeckManager { this._appStarted = options.appStarted; this._commands = options.commands; this._shell = options.shell; + this._labShell = options.labShell || null; this._statusbar = options.statusbar; this._trans = options.translator; this._settings = options.settings; this._fonts = options.fonts; + if (this._labShell) { + this._dockPanel = (this._shell as any)._dockPanel; + this._labShell.activeChanged.connect(this._onActiveWidgetChanged, this); + this._labShell.layoutModified.connect(this._addDeckStylesLater, this); + } - this._shell.activeChanged.connect(this._onActiveWidgetChanged, this); - this._shell.layoutModified.connect(this._addDeckStylesLater, this); this._addCommands(); this._addKeyBindings(); this._settings @@ -126,7 +133,7 @@ export class DeckManager implements IDeckManager { /** enable deck mode */ public start = async (force: boolean = false): Promise => { - await this._appStarted; + await this._appStarted(); const wasActive = this._active; /* istanbul ignore if */ @@ -144,7 +151,7 @@ export class DeckManager implements IDeckManager { } } - const { _shell, _activeWidget } = this; + const { _labShell, _shell, _activeWidget } = this; if (!wasActive) { this._active = true; @@ -153,10 +160,18 @@ export class DeckManager implements IDeckManager { this._statusBarWasEnabled = this._statusbar.isVisible; this._statusbar.hide(); } - _shell.presentationMode = false; + if (_labShell) { + _labShell.presentationMode = false; + } document.body.dataset[DATA.deckMode] = DATA.presenting; - each(this._dockpanel.tabBars(), (bar) => bar.hide()); - _shell.mode = 'single-document'; + if (this._dockPanel) { + for (const bar of getTabBars(this._dockPanel)) { + bar.hide(); + } + } + if (_labShell) { + _labShell.mode = 'single-document'; + } this._remote = new DeckRemote({ manager: this }); this._designTools = new DesignTools({ manager: this }); window.addEventListener('resize', this._addDeckStylesLater); @@ -166,6 +181,8 @@ export class DeckManager implements IDeckManager { await this._onActiveWidgetChanged(); if (_activeWidget) { + // TODO: hoist to an appropriate upstream + await (this._fonts as any)._stylist.ensureJss(); const presenter = this._getPresenter(_activeWidget); if (presenter) { this._activePresenter = presenter; @@ -175,14 +192,16 @@ export class DeckManager implements IDeckManager { } } - _shell.expandLeft(); - _shell.expandRight(); - _shell.collapseLeft(); - _shell.collapseRight(); - setTimeout(() => { - _shell.collapseLeft(); - _shell.collapseRight(); - }, 1000); + if (_labShell) { + _labShell.expandLeft(); + _labShell.expandRight(); + _labShell.collapseLeft(); + _labShell.collapseRight(); + setTimeout(() => { + _labShell.collapseLeft(); + _labShell.collapseRight(); + }, 1000); + } if (!wasActive) { this._addDeckStyles(); @@ -201,7 +220,15 @@ export class DeckManager implements IDeckManager { return; } - const { _activeWidget, _shell, _statusbar, _remote, _layover, _designTools } = this; + const { + _activeWidget, + _labShell, + _statusbar, + _remote, + _layover, + _designTools, + _dockPanel, + } = this; /* istanbul ignore if */ if (_layover) { @@ -219,7 +246,11 @@ export class DeckManager implements IDeckManager { _statusbar.show(); } - each(this._dockpanel.tabBars(), (bar) => bar.show()); + if (_dockPanel) { + for (const bar of getTabBars(_dockPanel)) { + bar.show(); + } + } if (_remote) { _remote.dispose(); @@ -229,14 +260,23 @@ export class DeckManager implements IDeckManager { _designTools.dispose(); this._designTools = null; } - _shell.presentationMode = false; - _shell.mode = 'multiple-document'; + if (_labShell) { + _labShell.presentationMode = false; + _labShell.mode = 'multiple-document'; + } window.removeEventListener('resize', this._addDeckStylesLater); delete document.body.dataset[DATA.deckMode]; this._activeWidget = null; this._active = false; this._activeWidgetStack = []; - void this._settings.then((settings) => settings.set('active', false)); + void this._settings.then(async (settings) => { + await settings.set('active', false); + this._shell.update(); + const _main = (this._shell as any)._main; + if (_main && typeof _main.update == 'function') { + _main.update(); + } + }); }; /** move around */ @@ -426,10 +466,6 @@ export class DeckManager implements IDeckManager { } } - protected get _dockpanel(): DockPanel { - return (this._shell as any)._dockPanel as DockPanel; - } - /** handle the active widget changing */ protected async _onActiveWidgetChanged(): Promise { if (!this._active) { @@ -460,7 +496,7 @@ export class DeckManager implements IDeckManager { if (this._activeWidgetStack.includes(_shellActiveWidget)) { this._activeWidgetStack.splice( this._activeWidgetStack.indexOf(_shellActiveWidget), - 1 + 1, ); } const presenter = this._getPresenter(_shellActiveWidget); @@ -479,12 +515,15 @@ export class DeckManager implements IDeckManager { } protected get _shellActiveWidget(): Widget | null { - if (this._shell.activeWidget) { - return this._shell.activeWidget; + const { _labShell, _shell, _dockPanel } = this; + if (_labShell && _dockPanel) { + if (_labShell.activeWidget) { + return _labShell.activeWidget; + } + return getSelectedWidget(_dockPanel); + } else { + return (_shell as INotebookShell).currentWidget || null; } - const selected = this._dockpanel.selectedWidgets(); - const widget = selected.next(); - return widget || null; } protected async _onSettingsChanged() { @@ -568,11 +607,12 @@ export class DeckManager implements IDeckManager { export namespace DeckManager { export interface IOptions { commands: CommandRegistry; - shell: LabShell; + labShell: ILabShell | null; + shell: JupyterFrontEnd.IShell; translator: TranslationBundle; statusbar: StatusBar | null; settings: Promise; - appStarted: Promise; + appStarted: () => Promise; fonts: IFontManager; } export interface IExtent { diff --git a/js/jupyterlab-deck/src/markdown/presenter.ts b/js/jupyterlab-deck/src/markdown/presenter.ts index 282752e..6e153d6 100644 --- a/js/jupyterlab-deck/src/markdown/presenter.ts +++ b/js/jupyterlab-deck/src/markdown/presenter.ts @@ -56,7 +56,7 @@ export class SimpleMarkdownPresenter implements IPresenter { public async go( panel: MarkdownDocument, direction: TDirection, - alternate?: TDirection + alternate?: TDirection, ): Promise { await panel.content.ready; let index = this._activeSlide.get(panel) || 1; diff --git a/js/jupyterlab-deck/src/notebook/extension.ts b/js/jupyterlab-deck/src/notebook/extension.ts index 0c8fa8c..3406d5f 100644 --- a/js/jupyterlab-deck/src/notebook/extension.ts +++ b/js/jupyterlab-deck/src/notebook/extension.ts @@ -21,7 +21,7 @@ export class NotebookDeckExtension createNew( panel: NotebookPanel, - context: DocumentRegistry.IContext + context: DocumentRegistry.IContext, ): IDisposable { const button = new CommandToolbarButton({ commands: this._commands, diff --git a/js/jupyterlab-deck/src/notebook/metadata.tsx b/js/jupyterlab-deck/src/notebook/metadata.tsx index 1dcc4fe..32298a1 100644 --- a/js/jupyterlab-deck/src/notebook/metadata.tsx +++ b/js/jupyterlab-deck/src/notebook/metadata.tsx @@ -1,8 +1,14 @@ import { ISettings, Stylist } from '@deathbeds/jupyterlab-fonts'; +import { + deleteCellMetadata, + setCellMetadata, + getCellMetadata, + getPanelMetadata, +} from '@deathbeds/jupyterlab-fonts/lib/labcompat'; import { VDomModel, VDomRenderer } from '@jupyterlab/apputils'; import { Cell, ICellModel } from '@jupyterlab/cells'; import { INotebookTools, NotebookTools } from '@jupyterlab/notebook'; -import { JSONExt, ReadonlyPartialJSONObject } from '@lumino/coreutils'; +import { JSONExt } from '@lumino/coreutils'; import { PanelLayout } from '@lumino/widgets'; import React from 'react'; @@ -133,7 +139,7 @@ export class DeckCellEditor extends VDomRenderer { presetOptions.push( + , ); } return presetOptions; @@ -146,7 +152,7 @@ export class DeckCellEditor extends VDomRenderer { layerOptions.push( + , ); } return layerOptions; @@ -163,8 +169,9 @@ export namespace DeckCellEditor { update() { this._activeMeta = - (this._activeCell?.model.metadata.get(META.deck) as any as ICellDeckMetadata) || - JSONExt.emptyObject; + (this._activeCell?.model + ? (getCellMetadata(this._activeCell?.model, META.deck) as ICellDeckMetadata) + : null) || JSONExt.emptyObject; this.stateChanged.emit(void 0); } @@ -190,7 +197,7 @@ export namespace DeckCellEditor { return; } let meta = { - ...((this._activeCell.model.metadata.get(META.fonts) || + ...((getCellMetadata(this._activeCell.model, META.fonts) || JSONExt.emptyObject) as ISettings), }; for (const preset of this._manager.stylePresets) { @@ -216,8 +223,8 @@ export namespace DeckCellEditor { for (let [key, value] of Object.entries(preset.styles)) { presenting[key] = value; } - this._activeCell.model.metadata.delete(META.fonts); - this._activeCell.model.metadata.set(META.fonts, meta as any); + deleteCellMetadata(this._activeCell.model, META.fonts); + setCellMetadata(this._activeCell.model, META.fonts, meta); this.forceStyle(); this._notebookTools.update(); return; @@ -230,7 +237,9 @@ export namespace DeckCellEditor { return; } let stylist = (this._manager.fonts as any)._stylist as Stylist; - let meta = panel.model?.metadata.get(META.fonts) || JSONExt.emptyObject; + let meta = + (panel.model ? getPanelMetadata(panel.model, META.fonts) : null) || + JSONExt.emptyObject; stylist.stylesheet(meta as ISettings, panel); } @@ -249,9 +258,9 @@ export namespace DeckCellEditor { protected _setDeckMetadata(newMeta: ICellDeckMetadata, cell: Cell) { if (Object.keys(newMeta).length) { - cell.model.metadata.set(META.deck, newMeta as ReadonlyPartialJSONObject); + setCellMetadata(cell.model, META.deck, newMeta); } else { - cell.model.metadata.delete(META.deck); + deleteCellMetadata(cell.model, META.deck); } } diff --git a/js/jupyterlab-deck/src/notebook/presenter.ts b/js/jupyterlab-deck/src/notebook/presenter.ts index 2c92129..26db5ef 100644 --- a/js/jupyterlab-deck/src/notebook/presenter.ts +++ b/js/jupyterlab-deck/src/notebook/presenter.ts @@ -1,5 +1,11 @@ import { ISettings, Stylist } from '@deathbeds/jupyterlab-fonts'; import type { GlobalStyles } from '@deathbeds/jupyterlab-fonts/lib/_schema'; +import { + getCellMetadata, + getPanelMetadata, + setCellMetadata, + deleteCellMetadata, +} from '@deathbeds/jupyterlab-fonts/lib/labcompat'; import { Cell, ICellModel } from '@jupyterlab/cells'; import { INotebookModel, @@ -7,13 +13,13 @@ import { Notebook, NotebookPanel, } from '@jupyterlab/notebook'; -import { toArray } from '@lumino/algorithm'; import { CommandRegistry } from '@lumino/commands'; import { JSONExt } from '@lumino/coreutils'; import { ElementExt } from '@lumino/domutils'; import { ISignal, Signal } from '@lumino/signaling'; import { Widget } from '@lumino/widgets'; +import { getCellModels } from '../labcompat'; import { DIRECTION, IPresenter, @@ -35,6 +41,7 @@ import type { Layover } from '../tools/layover'; import { NotebookMetaTools } from './metadata'; const emptyMap = Object.freeze(new Map()); +const { emptyObject, emptyArray } = JSONExt; /** An presenter for working with notebooks */ export class NotebookPresenter implements IPresenter { @@ -127,7 +134,8 @@ export class NotebookPresenter implements IPresenter { public getSlideType(panel: NotebookPanel): TSlideType { let { activeCell } = panel.content; if (activeCell) { - const meta = (activeCell.model.metadata.get(META.slideshow) || {}) as any; + const meta = (getCellMetadata(activeCell.model, META.slideshow) || + emptyObject) as any; return (meta[META.slideType] || null) as TSlideType; } return null; @@ -140,12 +148,15 @@ export class NotebookPresenter implements IPresenter { return; } let oldMeta = - (activeCell.model.metadata.get(META.slideshow) as Record) || null; + ((getCellMetadata(activeCell.model, META.slideshow) || emptyObject) as Record< + string, + any + >) || null; if (slideType == null) { if (oldMeta == null) { - activeCell.model.metadata.delete(META.slideshow); + deleteCellMetadata(activeCell.model, META.slideshow); } else { - activeCell.model.metadata.set(META.slideshow, { + setCellMetadata(activeCell.model, META.slideshow, { ...oldMeta, [META.slideType]: slideType, }); @@ -154,7 +165,7 @@ export class NotebookPresenter implements IPresenter { if (oldMeta == null) { oldMeta = {}; } - activeCell.model.metadata.set(META.slideshow, { + setCellMetadata(activeCell.model, META.slideshow, { ...oldMeta, [META.slideType]: slideType, }); @@ -168,7 +179,7 @@ export class NotebookPresenter implements IPresenter { public getLayerScope(panel: NotebookPanel): string | null { let { activeCell } = panel.content; if (activeCell) { - const meta = (activeCell.model.metadata.get(META.deck) || {}) as any; + const meta = (getCellMetadata(activeCell.model, META.deck) || emptyObject) as any; return (meta[META.layer] || null) as TLayerScope; } return null; @@ -180,12 +191,15 @@ export class NotebookPresenter implements IPresenter { return; } let oldMeta = - (activeCell.model.metadata.get(META.deck) as Record) || null; + ((getCellMetadata(activeCell.model, META.deck) || emptyObject) as Record< + string, + any + >) || null; if (layerScope == null) { if (oldMeta == null) { - activeCell.model.metadata.delete(META.layer); + deleteCellMetadata(activeCell.model, META.deck); } else { - activeCell.model.metadata.set(META.deck, { + setCellMetadata(activeCell.model, META.deck, { ...oldMeta, [META.layer]: layerScope, }); @@ -194,7 +208,7 @@ export class NotebookPresenter implements IPresenter { if (oldMeta == null) { oldMeta = {}; } - activeCell.model.metadata.set(META.deck, { + setCellMetadata(activeCell.model, META.deck, { ...oldMeta, [META.layer]: layerScope, }); @@ -225,19 +239,18 @@ export class NotebookPresenter implements IPresenter { public preparePanel(panel: NotebookPanel) { let notebook = panel.content; let oldSetFragment = notebook.setFragment; - notebook.setFragment = (fragment: string): void => { - oldSetFragment.call(notebook, fragment); + notebook.setFragment = async (fragment: string): Promise => { + await oldSetFragment.call(notebook, fragment); if (this._manager.activePresenter === this) { - void Promise.all(notebook.widgets.map((widget) => widget.ready)).then(() => { - this._activateByAnchor(notebook, fragment); - }); + await Promise.all(notebook.widgets.map((widget) => widget.ready)); + this._activateByAnchor(notebook, fragment); } }; } protected _makeDeckTools(notebookTools: INotebookTools) { const tool = new NotebookMetaTools({ manager: this._manager, notebookTools }); - notebookTools.addItem({ tool, section: 'common', rank: 3 }); + notebookTools.addItem({ tool, section: 'commonToolsSection', rank: 3 }); } protected _addWindowListeners() { @@ -335,14 +348,14 @@ export class NotebookPresenter implements IPresenter { back: back != null, }; } - return JSONExt.emptyObject; + return emptyObject; } /** move around */ public go = async ( panel: NotebookPanel, direction: TDirection, - alternate?: TDirection + alternate?: TDirection, ): Promise => { const notebookModel = panel.content.model; /* istanbul ignore if */ @@ -375,7 +388,11 @@ export class NotebookPresenter implements IPresenter { } else { console.warn( EMOJI, - this._manager.__(`Cannot go "%1" from cell %2`, direction, `${activeCellIndex}`) + this._manager.__( + `Cannot go "%1" from cell %2`, + direction, + `${activeCellIndex}`, + ), ); } }; @@ -406,7 +423,7 @@ export class NotebookPresenter implements IPresenter { let { activeCellIndex } = notebook; - let cell = notebookModel.cells.get(activeCellIndex); + let cell = getCellModels(notebookModel)[activeCellIndex]; let layerIndex: number | null = null; @@ -465,7 +482,7 @@ export class NotebookPresenter implements IPresenter { cell.removeClass(CSS.layer); if (activeExtent.visible.includes(idx)) { cell.addClass(CSS.visible); - cell.editorWidget.update(); + cell.editorWidget?.update(); } else { cell.removeClass(CSS.visible); } @@ -482,7 +499,7 @@ export class NotebookPresenter implements IPresenter { ElementExt.scrollIntoViewIfNeeded( notebook.node, - notebook.widgets[activeCellIndex].node + notebook.widgets[activeCellIndex].node, ); this._activeChanged.emit(void 0); if (this._manager.layover) { @@ -496,7 +513,8 @@ export class NotebookPresenter implements IPresenter { return; } let stylist = (this._manager.fonts as any)._stylist as Stylist; - let meta = panel.model?.metadata.get(META.fonts) || JSONExt.emptyObject; + let meta = + (panel.model ? getPanelMetadata(panel.model, META.fonts) : null) || emptyObject; stylist.stylesheet(meta as ISettings, panel); this._manager.layover?.render(); } @@ -512,16 +530,18 @@ export class NotebookPresenter implements IPresenter { protected _getCellStyles(cell: Cell) { try { - const meta = cell.model.metadata.get(META.fonts) as any; + const meta = (getCellMetadata(cell.model, META.fonts) || emptyObject) as any; const styles = meta.styles[META.nullSelector][META.presentingCell]; return styles; } catch { - return JSONExt.emptyObject; + return emptyObject; } } protected _setCellStyles(cell: Cell, styles: GlobalStyles | null) { - let meta = (cell.model.metadata.get(META.fonts) || {}) as ISettings; + let meta = { + ...(getCellMetadata(cell.model, META.fonts) || emptyObject), + } as ISettings; if (!meta.styles) { meta.styles = {}; } @@ -529,23 +549,22 @@ export class NotebookPresenter implements IPresenter { meta.styles[META.nullSelector] = {}; } (meta.styles[META.nullSelector] as any)[META.presentingCell] = styles; - cell.model.metadata.set(META.fonts, JSONExt.emptyObject as any); - cell.model.metadata.set(META.fonts, { ...meta } as any); + setCellMetadata(cell.model, META.fonts, emptyObject); + setCellMetadata(cell.model, META.fonts, { ...meta }); this._forceStyle(); } /** Get the nbconvert-compatible `slide_type` from metadata. */ protected _getSlideType(cell: ICellModel): TSlideType { return ( - ((cell.metadata.get('slideshow') || JSONExt.emptyObject) as any)['slide_type'] || - null + (getCellMetadata(cell, META.slideshow) || emptyObject)[META.slideType] || null ); } protected _initExtent( index: number, slideType: TSlideType, - extent: Partial = JSONExt.emptyObject + extent: Partial = emptyObject, ): NotebookPresenter.IExtent { return { parent: null, @@ -564,7 +583,7 @@ export class NotebookPresenter implements IPresenter { protected _lastOnScreenOf( index: number, - extents: NotebookPresenter.TExtentMap + extents: NotebookPresenter.TExtentMap, ): null | NotebookPresenter.IExtent { let e = extents.get(index); /* istanbul ignore if */ @@ -576,8 +595,8 @@ export class NotebookPresenter implements IPresenter { /** Get layer metadata from `jupyterlab-deck` namespace */ protected _getCellDeckMetadata(cell: ICellModel): ICellDeckMetadata { - return (cell.metadata.get(META.deck) || - JSONExt.emptyObject) as any as ICellDeckMetadata; + return (getCellMetadata(cell, META.deck) || + emptyObject) as any as ICellDeckMetadata; } _numSort(a: number, b: number): number { @@ -586,7 +605,7 @@ export class NotebookPresenter implements IPresenter { protected _getLayers( notebookModel: INotebookModel | null, - extents: NotebookPresenter.TExtentMap + extents: NotebookPresenter.TExtentMap, ): NotebookPresenter.TLayerMap { if (!notebookModel) { return emptyMap; @@ -636,7 +655,7 @@ export class NotebookPresenter implements IPresenter { let start = -1; let end = -1; - for (const cell of toArray(notebookModel.cells)) { + for (const cell of getCellModels(notebookModel)) { i++; let { layer } = this._getCellDeckMetadata(cell); if (!layer) { @@ -715,7 +734,7 @@ export class NotebookPresenter implements IPresenter { * - what are the notes */ protected _getExtents( - notebookModel: INotebookModel | null + notebookModel: INotebookModel | null, ): NotebookPresenter.TExtentMap { /* istanbul ignore if */ if (!notebookModel) { @@ -735,7 +754,7 @@ export class NotebookPresenter implements IPresenter { }; let index = -1; - for (const cell of toArray(notebookModel.cells)) { + for (const cell of getCellModels(notebookModel)) { index++; let slideType = this._getSlideType(cell); let { layer } = this._getCellDeckMetadata(cell); @@ -785,10 +804,10 @@ export class NotebookPresenter implements IPresenter { stacks.onScreen.unshift(extent); stacks.nulls.unshift(extent); extent.onScreen.unshift( - ...(a0?.onScreen || /* istanbul ignore next */ JSONExt.emptyArray) + ...(a0?.onScreen || /* istanbul ignore next */ emptyArray), ); extent.visible.unshift( - ...(a0?.visible || /* istanbul ignore next */ JSONExt.emptyArray) + ...(a0?.visible || /* istanbul ignore next */ emptyArray), ); break; case 'slide': @@ -864,8 +883,8 @@ export class NotebookPresenter implements IPresenter { } stacks.nulls = []; stacks.onScreen.unshift(extent); - extent.onScreen.unshift(...(a0?.onScreen || JSONExt.emptyArray)); - extent.visible.unshift(index, ...(a0?.visible || JSONExt.emptyArray)); + extent.onScreen.unshift(...(a0?.onScreen || emptyArray)); + extent.visible.unshift(index, ...(a0?.visible || emptyArray)); stacks.fragments.unshift(extent); break; case 'notes': diff --git a/js/jupyterlab-deck/src/plugin.ts b/js/jupyterlab-deck/src/plugin.ts index eee686a..2da0f87 100644 --- a/js/jupyterlab-deck/src/plugin.ts +++ b/js/jupyterlab-deck/src/plugin.ts @@ -21,21 +21,21 @@ import '../style/index.css'; const plugin: JupyterFrontEndPlugin = { id: `${NS}:plugin`, - requires: [ITranslator, ILabShell, ISettingRegistry, ILayoutRestorer, IFontManager], - optional: [ICommandPalette, IStatusBar], + requires: [ITranslator, ISettingRegistry, IFontManager], + optional: [ILabShell, ILayoutRestorer, ICommandPalette, IStatusBar], provides: IDeckManager, autoStart: true, activate: ( app: JupyterFrontEnd, translator: ITranslator, - shell: ILabShell, settings: ISettingRegistry, - restorer: ILayoutRestorer, fonts: IFontManager, + labShell?: ILabShell, + restorer?: ILayoutRestorer, palette?: ICommandPalette, - statusbar?: IStatusBar + statusbar?: IStatusBar, ) => { - const { commands } = app; + const { commands, shell } = app; const theStatusBar = statusbar instanceof StatusBar ? statusbar : /* istanbul ignore next */ null; @@ -43,11 +43,13 @@ const plugin: JupyterFrontEndPlugin = { const manager = new DeckManager({ commands, shell, + labShell: labShell || null, translator: (translator || /* istanbul ignore next */ nullTranslator).load(NS), statusbar: theStatusBar, fonts, settings: settings.load(PLUGIN_ID), - appStarted: Promise.all([app.started, restorer.restored]).then(() => void 0), + appStarted: async () => + await Promise.all([app.started, ...(restorer ? [restorer.restored] : [])]), }); const { __ } = manager; @@ -72,7 +74,7 @@ const notebookPlugin: JupyterFrontEndPlugin = { activate: ( app: JupyterFrontEnd, notebookTools: INotebookTools, - decks: IDeckManager + decks: IDeckManager, ) => { const { commands } = app; const presenter = new NotebookPresenter({ @@ -84,7 +86,7 @@ const notebookPlugin: JupyterFrontEndPlugin = { app.docRegistry.addWidgetExtension( 'Notebook', - new NotebookDeckExtension({ commands, presenter }) + new NotebookDeckExtension({ commands, presenter }), ); }, }; diff --git a/js/jupyterlab-deck/src/tokens.ts b/js/jupyterlab-deck/src/tokens.ts index ca668ca..ca87902 100644 --- a/js/jupyterlab-deck/src/tokens.ts +++ b/js/jupyterlab-deck/src/tokens.ts @@ -229,12 +229,12 @@ export namespace META { * nbconvert, notebook, and lab UI **/ export const SLIDE_TYPES = ['slide', 'subslide', null, 'fragment', 'notes', 'skip']; -export type TSlideType = typeof SLIDE_TYPES[number]; +export type TSlideType = (typeof SLIDE_TYPES)[number]; /** The scope of extents that will have this layer */ export const LAYER_SCOPES = ['deck', 'stack', 'slide', 'fragment']; -export type TLayerScope = typeof LAYER_SCOPES[number]; +export type TLayerScope = (typeof LAYER_SCOPES)[number]; export type TSelectLabels = Record; diff --git a/js/jupyterlab-deck/src/tools/design.tsx b/js/jupyterlab-deck/src/tools/design.tsx index 5a8d2d5..d951147 100644 --- a/js/jupyterlab-deck/src/tools/design.tsx +++ b/js/jupyterlab-deck/src/tools/design.tsx @@ -47,7 +47,7 @@ export class DesignTools extends VDomRenderer { this.makeButton( ellipsesIcon, __('Show Design Tools'), - () => (model.showMore = true) + () => (model.showMore = true), ), ]; } @@ -56,7 +56,7 @@ export class DesignTools extends VDomRenderer { this.makeButton( caretLeftIcon, __('Hide Design Tools'), - () => (model.showMore = false) + () => (model.showMore = false), ), ]; @@ -70,8 +70,8 @@ export class DesignTools extends VDomRenderer { () => { let { manager } = this.model; manager.layover ? manager.hideLayover() : manager.showLayover(); - } - ) + }, + ), ); } @@ -94,7 +94,7 @@ export class DesignTools extends VDomRenderer { items.push(
    {slideTypes} -
+ , ); } @@ -117,7 +117,7 @@ export class DesignTools extends VDomRenderer { items.push(
    {layerScopes} -
+ , ); } @@ -126,7 +126,7 @@ export class DesignTools extends VDomRenderer { items.push( this.makeSlider('z-index', currentPartStyles), this.makeSlider('zoom', currentPartStyles), - this.makeSlider('opacity', currentPartStyles) + this.makeSlider('opacity', currentPartStyles), ); } @@ -135,7 +135,7 @@ export class DesignTools extends VDomRenderer { makeSlideTypeItem = ( slideType: TSlideType, - currentSlideType: TSlideType + currentSlideType: TSlideType, ): JSX.Element => { let { __ } = this.model.manager; let slideTypeKey = slideType == null ? 'null' : slideType; @@ -146,7 +146,7 @@ export class DesignTools extends VDomRenderer { label, () => this.model.manager.setSlideType(slideType), '', - [] + [], ); return (
  • @@ -157,7 +157,7 @@ export class DesignTools extends VDomRenderer { makeLayerScopeItem = ( layerScope: TLayerScope | null, - currentLayerScope: TLayerScope | null + currentLayerScope: TLayerScope | null, ): JSX.Element => { let { __ } = this.model.manager; let layerScopeKey = layerScope == null ? 'null' : layerScope; @@ -168,7 +168,7 @@ export class DesignTools extends VDomRenderer { label, () => this.model.manager.setLayerScope(layerScope), '', - [] + [], ); return (
  • {button}
  • @@ -177,7 +177,7 @@ export class DesignTools extends VDomRenderer { makeSlider = ( attr: DesignTools.TSliderAttr, - styles: GlobalStyles | null + styles: GlobalStyles | null, ): JSX.Element => { const config = DesignTools.SLIDER_CONFIG[attr]; const { suffix, className, icon } = config; @@ -239,7 +239,7 @@ export class DesignTools extends VDomRenderer { title: string, onClick: () => void, className: string = '', - children: JSX.Element[] = [] + children: JSX.Element[] = [], ) { return ( - + , ); } return
      {stack}
    ; @@ -90,7 +90,7 @@ export class DeckRemote extends VDomRenderer { icon: LabIcon, title: string, onClick: () => void, - className: string = '' + className: string = '', ) { return (