├── .github └── workflows │ ├── build.yml │ ├── check-release.yml │ ├── enforce-label.yml │ ├── prep-release.yml │ ├── publish-release.yml │ └── update-integration-tests.yml ├── .gitignore ├── .prettierignore ├── .travis.yml ├── .yarnrc.yml ├── LICENSE ├── README.md ├── RELEASE.md ├── babel.config.js ├── build.sh ├── install.json ├── jupyterlab_sos └── __init__.py ├── package.json ├── pyproject.toml ├── schema └── plugin.json ├── setup.py ├── src ├── codemirror-sos.ts ├── execute.ts ├── index.ts ├── manager.ts └── selectors.tsx ├── style ├── base.css ├── index.css ├── index.js └── sos_icon.svg ├── test ├── conftest.py ├── test_frontend.py ├── test_magics.py ├── test_utils.py └── test_workflow.py ├── tsconfig.json ├── tsconfig.test.json └── tsconfig.tsbuildinfo /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: '*' 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Base Setup 22 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 23 | 24 | - name: Install dependencies 25 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 26 | 27 | - name: Lint the extension 28 | run: | 29 | set -eux 30 | jlpm 31 | jlpm run lint:check 32 | 33 | - name: Test the extension 34 | run: | 35 | set -eux 36 | jlpm run test 37 | 38 | - name: Build the extension 39 | run: | 40 | set -eux 41 | python -m pip install .[test] 42 | 43 | jupyter labextension list 44 | jupyter labextension list 2>&1 | grep -ie "jupyterlab-sos.*OK" 45 | python -m jupyterlab.browser_check 46 | 47 | - name: Package the extension 48 | run: | 49 | set -eux 50 | 51 | pip install build 52 | python -m build 53 | pip uninstall -y "jupyterlab_sos" jupyterlab 54 | 55 | - name: Upload extension packages 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: extension-artifacts 59 | path: dist/jupyterlab_sos* 60 | if-no-files-found: error 61 | 62 | test_isolated: 63 | needs: build 64 | runs-on: ubuntu-latest 65 | 66 | steps: 67 | - name: Install Python 68 | uses: actions/setup-python@v5 69 | with: 70 | python-version: '3.9' 71 | architecture: 'x64' 72 | - uses: actions/download-artifact@v4 73 | with: 74 | name: extension-artifacts 75 | - name: Install and Test 76 | run: | 77 | set -eux 78 | # Remove NodeJS, twice to take care of system and locally installed node versions. 79 | sudo rm -rf $(which node) 80 | sudo rm -rf $(which node) 81 | 82 | pip install "jupyterlab>=4.0.0,<5" jupyterlab_sos*.whl 83 | 84 | 85 | jupyter labextension list 86 | jupyter labextension list 2>&1 | grep -ie "jupyterlab-sos.*OK" 87 | python -m jupyterlab.browser_check --no-browser-test 88 | 89 | integration-tests: 90 | name: Integration tests 91 | needs: build 92 | runs-on: ubuntu-latest 93 | 94 | env: 95 | PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers 96 | 97 | steps: 98 | - name: Checkout 99 | uses: actions/checkout@v4 100 | 101 | - name: Base Setup 102 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 103 | 104 | - name: Download extension package 105 | uses: actions/download-artifact@v4 106 | with: 107 | name: extension-artifacts 108 | 109 | - name: Install the extension 110 | run: | 111 | set -eux 112 | python -m pip install "jupyterlab>=4.0.0,<5" jupyterlab_sos*.whl 113 | 114 | - name: Install dependencies 115 | working-directory: ui-tests 116 | env: 117 | YARN_ENABLE_IMMUTABLE_INSTALLS: 0 118 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 119 | run: jlpm install 120 | 121 | - name: Set up browser cache 122 | uses: actions/cache@v4 123 | with: 124 | path: | 125 | ${{ github.workspace }}/pw-browsers 126 | key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} 127 | 128 | - name: Install browser 129 | run: jlpm playwright install chromium 130 | working-directory: ui-tests 131 | 132 | - name: Execute integration tests 133 | working-directory: ui-tests 134 | run: | 135 | jlpm playwright test 136 | 137 | - name: Upload Playwright Test report 138 | if: always() 139 | uses: actions/upload-artifact@v4 140 | with: 141 | name: jupyterlab_sos-playwright-tests 142 | path: | 143 | ui-tests/test-results 144 | ui-tests/playwright-report 145 | 146 | check_links: 147 | name: Check Links 148 | runs-on: ubuntu-latest 149 | timeout-minutes: 15 150 | steps: 151 | - uses: actions/checkout@v4 152 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 153 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 154 | -------------------------------------------------------------------------------- /.github/workflows/check-release.yml: -------------------------------------------------------------------------------- 1 | name: Check Release 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | branches: ["*"] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | check_release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Base Setup 19 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 20 | - name: Check Release 21 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 22 | with: 23 | 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | - name: Upload Distributions 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: jupyterlab_sos-releaser-dist-${{ github.run_number }} 30 | path: .jupyter_releaser_checkout/dist 31 | -------------------------------------------------------------------------------- /.github/workflows/enforce-label.yml: -------------------------------------------------------------------------------- 1 | name: Enforce PR label 2 | 3 | on: 4 | pull_request: 5 | types: [labeled, unlabeled, opened, edited, synchronize] 6 | jobs: 7 | enforce-label: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | pull-requests: write 11 | steps: 12 | - name: enforce-triage-label 13 | uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1 14 | -------------------------------------------------------------------------------- /.github/workflows/prep-release.yml: -------------------------------------------------------------------------------- 1 | name: "Step 1: Prep Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version_spec: 6 | description: "New Version Specifier" 7 | default: "next" 8 | required: false 9 | branch: 10 | description: "The branch to target" 11 | required: false 12 | post_version_spec: 13 | description: "Post Version Specifier" 14 | required: false 15 | # silent: 16 | # description: "Set a placeholder in the changelog and don't publish the release." 17 | # required: false 18 | # type: boolean 19 | since: 20 | description: "Use PRs with activity since this date or git reference" 21 | required: false 22 | since_last_stable: 23 | description: "Use PRs with activity since the last stable git tag" 24 | required: false 25 | type: boolean 26 | jobs: 27 | prep_release: 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: write 31 | steps: 32 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 33 | 34 | - name: Prep Release 35 | id: prep-release 36 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | version_spec: ${{ github.event.inputs.version_spec }} 40 | # silent: ${{ github.event.inputs.silent }} 41 | post_version_spec: ${{ github.event.inputs.post_version_spec }} 42 | branch: ${{ github.event.inputs.branch }} 43 | since: ${{ github.event.inputs.since }} 44 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 45 | 46 | - name: "** Next Step **" 47 | run: | 48 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" 49 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: "Step 2: Publish Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | branch: 6 | description: "The target branch" 7 | required: false 8 | release_url: 9 | description: "The URL of the draft GitHub release" 10 | required: false 11 | steps_to_skip: 12 | description: "Comma separated list of steps to skip" 13 | required: false 14 | 15 | jobs: 16 | publish_release: 17 | runs-on: ubuntu-latest 18 | environment: release 19 | permissions: 20 | id-token: write 21 | steps: 22 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 23 | 24 | - uses: actions/create-github-app-token@v1 25 | id: app-token 26 | with: 27 | app-id: ${{ vars.APP_ID }} 28 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 29 | 30 | - name: Populate Release 31 | id: populate-release 32 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 33 | with: 34 | token: ${{ steps.app-token.outputs.token }} 35 | branch: ${{ github.event.inputs.branch }} 36 | release_url: ${{ github.event.inputs.release_url }} 37 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }} 38 | 39 | - name: Finalize Release 40 | id: finalize-release 41 | env: 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2 44 | with: 45 | token: ${{ steps.app-token.outputs.token }} 46 | release_url: ${{ steps.populate-release.outputs.release_url }} 47 | 48 | - name: "** Next Step **" 49 | if: ${{ success() }} 50 | run: | 51 | echo "Verify the final release" 52 | echo ${{ steps.finalize-release.outputs.release_url }} 53 | 54 | - name: "** Failure Message **" 55 | if: ${{ failure() }} 56 | run: | 57 | echo "Failed to Publish the Draft Release Url:" 58 | echo ${{ steps.populate-release.outputs.release_url }} 59 | -------------------------------------------------------------------------------- /.github/workflows/update-integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Update Playwright Snapshots 2 | 3 | on: 4 | issue_comment: 5 | types: [created, edited] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | update-snapshots: 13 | if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, 'please update snapshots') }} 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: React to the triggering comment 18 | run: | 19 | gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions --raw-field 'content=+1' 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | token: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Checkout the branch from the PR that triggered the job 29 | run: gh pr checkout ${{ github.event.issue.number }} 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: Base Setup 34 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 35 | 36 | - name: Install dependencies 37 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 38 | 39 | - name: Install extension 40 | run: | 41 | set -eux 42 | jlpm 43 | python -m pip install . 44 | 45 | - uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@v1 46 | with: 47 | github_token: ${{ secrets.GITHUB_TOKEN }} 48 | # Playwright knows how to start JupyterLab server 49 | start_server_script: 'null' 50 | test_folder: ui-tests 51 | npm_client: jlpm 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.log 5 | .eslintcache 6 | .stylelintcache 7 | *.egg-info/ 8 | .ipynb_checkpoints 9 | *.tsbuildinfo 10 | jupyterlab_sos/labextension 11 | # Version file is handled by hatchling 12 | jupyterlab_sos/_version.py 13 | 14 | # Created by https://www.gitignore.io/api/python 15 | # Edit at https://www.gitignore.io/?templates=python 16 | 17 | ### Python ### 18 | # Byte-compiled / optimized / DLL files 19 | __pycache__/ 20 | *.py[cod] 21 | *$py.class 22 | 23 | # C extensions 24 | *.so 25 | 26 | # Distribution / packaging 27 | .Python 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | pip-wheel-metadata/ 41 | share/python-wheels/ 42 | .installed.cfg 43 | *.egg 44 | MANIFEST 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .nox/ 60 | .coverage 61 | .coverage.* 62 | .cache 63 | nosetests.xml 64 | coverage/ 65 | coverage.xml 66 | *.cover 67 | .hypothesis/ 68 | .pytest_cache/ 69 | 70 | # Translations 71 | *.mo 72 | *.pot 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # Mr Developer 100 | .mr.developer.cfg 101 | .project 102 | .pydevproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | .dmypy.json 110 | dmypy.json 111 | 112 | # Pyre type checker 113 | .pyre/ 114 | 115 | # End of https://www.gitignore.io/api/python 116 | 117 | # OSX files 118 | .DS_Store 119 | 120 | # Yarn cache 121 | .yarn/ 122 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | !/package.json 6 | jupyterlab_sos 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | group: edge 3 | os: 4 | - linux 5 | # travis does not support python on osx yet (https://github.com/travis-ci/travis-ci/issues/4729) 6 | language: python 7 | python: 8 | - "3.8" 9 | addons: 10 | chrome: stable 11 | before_install: 12 | - sudo apt-get update 13 | - sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce 14 | - wget https://repo.continuum.io/miniconda/Miniconda3-4.5.11-Linux-x86_64.sh -O miniconda.sh 15 | - bash miniconda.sh -b -p $HOME/miniconda 16 | - export PATH="$HOME/miniconda/bin:$PATH" 17 | - hash -r 18 | - conda config --set always_yes yes --set changeps1 no 19 | - conda update -q conda 20 | #- conda info -a 21 | - pip install docker rq pyyaml psutil tqdm nose fasteners pygments networkx pydot pydotplus 22 | - pip install entrypoints jupyter coverage codacy-coverage pytest pytest-cov python-coveralls 23 | - conda install -q pandas numpy 24 | - conda install -c r r-essentials r-feather 25 | - conda install -c conda-forge feather-format nodejs=13.13.0 26 | # SoS Notebook 27 | - pip install jedi notebook nbconvert nbformat pyyaml psutil tqdm scipy markdown matplotlib jupyterlab 28 | - sudo apt-get install libmagickwand-dev libmagickcore-dev graphviz 29 | - pip install pygments ipython wand graphviz 30 | - pip install git+https://github.com/vatlab/sos.git 31 | - pip install git+https://github.com/vatlab/sos-notebook.git 32 | - pip install git+https://github.com/vatlab/sos-bash.git 33 | - pip install git+https://github.com/vatlab/sos-python.git 34 | - pip install git+https://github.com/vatlab/sos-r.git 35 | - python -m sos_notebook.install 36 | - pip install selenium 37 | - google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 http://localhost & 38 | - wget https://chromedriver.storage.googleapis.com/81.0.4044.69/chromedriver_linux64.zip -P ~/ 39 | - unzip ~/chromedriver_linux64.zip -d ~/ 40 | - rm ~/chromedriver_linux64.zip 41 | - sudo mv -f ~/chromedriver /usr/local/share/ 42 | - sudo chmod +x /usr/local/share/chromedriver 43 | - sudo ln -s /usr/local/share/chromedriver /usr/local/bin/chromedriver 44 | 45 | sudo: required 46 | install: 47 | - jupyter labextension install transient-display-data 48 | - npm install 49 | - npm run build 50 | - jupyter labextension install 51 | script: 52 | - cd test && pytest test_frontend.py -v && pytest test_magics.py -v && pytest test_workflow.py -v 53 | after_success: 54 | - coverage combine 55 | - coveralls 56 | 57 | notifications: 58 | email: 59 | recipients: 60 | - ben.bob@gmail.com 61 | - junma80@gmail.com 62 | on_success: never 63 | on_failure: always 64 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Bo Peng 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jupyterlab_sos 2 | 3 | [![Github Actions Status](https://github.com/vatlab/jupyterlab-sos/workflows/Build/badge.svg)](https://github.com/vatlab/jupyterlab-sos/actions/workflows/build.yml) 4 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/vatlab/jupyterlab-sos/main?urlpath=lab) 5 | 6 | 7 | JupyterLab extension for SoS workflow engine and polyglot notebook 8 | 9 | ## Requirements 10 | 11 | - JupyterLab >= 4.0.0 12 | 13 | ## Install 14 | 15 | To install the extension, execute: 16 | 17 | ```bash 18 | pip install jupyterlab_sos 19 | ``` 20 | 21 | ## Uninstall 22 | 23 | To remove the extension, execute: 24 | 25 | ```bash 26 | pip uninstall jupyterlab_sos 27 | ``` 28 | 29 | ## Contributing 30 | 31 | ### Development install 32 | 33 | Note: You will need NodeJS to build the extension package. 34 | 35 | The `jlpm` command is JupyterLab's pinned version of 36 | [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use 37 | `yarn` or `npm` in lieu of `jlpm` below. 38 | 39 | ```bash 40 | # Clone the repo to your local environment 41 | # Change directory to the jupyterlab_sos directory 42 | # Install package in development mode 43 | pip install -e "." 44 | # Link your development version of the extension with JupyterLab 45 | jupyter labextension develop . --overwrite 46 | # Rebuild extension Typescript source after making changes 47 | jlpm build 48 | ``` 49 | 50 | You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. 51 | 52 | ```bash 53 | # Watch the source directory in one terminal, automatically rebuilding when needed 54 | jlpm watch 55 | # Run JupyterLab in another terminal 56 | jupyter lab 57 | ``` 58 | 59 | With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). 60 | 61 | By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command: 62 | 63 | ```bash 64 | jupyter lab build --minimize=False 65 | ``` 66 | 67 | ### Development uninstall 68 | 69 | ```bash 70 | pip uninstall jupyterlab_sos 71 | ``` 72 | 73 | In development mode, you will also need to remove the symlink created by `jupyter labextension develop` 74 | command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` 75 | folder is located. Then you can remove the symlink named `jupyterlab-sos` within that folder. 76 | 77 | ### Testing the extension 78 | 79 | #### Frontend tests 80 | 81 | This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. 82 | 83 | To execute them, execute: 84 | 85 | ```sh 86 | jlpm 87 | jlpm test 88 | ``` 89 | 90 | #### Integration tests 91 | 92 | This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests). 93 | More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. 94 | 95 | More information are provided within the [ui-tests](./ui-tests/README.md) README. 96 | 97 | ### Packaging the extension 98 | 99 | See [RELEASE](RELEASE.md) 100 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a new release of jupyterlab_sos 2 | 3 | The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). 4 | 5 | ## Manual release 6 | 7 | ### Python package 8 | 9 | This extension can be distributed as Python packages. All of the Python 10 | packaging instructions are in the `pyproject.toml` file to wrap your extension in a 11 | Python package. Before generating a package, you first need to install some tools: 12 | 13 | ```bash 14 | pip install build twine hatch 15 | ``` 16 | 17 | Bump the version using `hatch`. By default this will create a tag. 18 | See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. 19 | 20 | ```bash 21 | hatch version 22 | ``` 23 | 24 | Make sure to clean up all the development files before building the package: 25 | 26 | ```bash 27 | jlpm clean:all 28 | ``` 29 | 30 | You could also clean up the local git repository: 31 | 32 | ```bash 33 | git clean -dfX 34 | ``` 35 | 36 | To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: 37 | 38 | ```bash 39 | python -m build 40 | ``` 41 | 42 | > `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. 43 | 44 | Then to upload the package to PyPI, do: 45 | 46 | ```bash 47 | twine upload dist/* 48 | ``` 49 | 50 | ### NPM package 51 | 52 | To publish the frontend part of the extension as a NPM package, do: 53 | 54 | ```bash 55 | npm login 56 | npm publish --access public 57 | ``` 58 | 59 | ## Automated releases with the Jupyter Releaser 60 | 61 | The extension repository should already be compatible with the Jupyter Releaser. But 62 | the GitHub repository and the package managers need to be properly set up. Please 63 | follow the instructions of the Jupyter Releaser [checklist](https://jupyter-releaser.readthedocs.io/en/latest/how_to_guides/convert_repo_from_repo.html). 64 | 65 | Here is a summary of the steps to cut a new release: 66 | 67 | - Go to the Actions panel 68 | - Run the "Step 1: Prep Release" workflow 69 | - Check the draft changelog 70 | - Run the "Step 2: Publish Release" workflow 71 | 72 | > [!NOTE] 73 | > Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) 74 | > for more information. 75 | 76 | ## Publishing to `conda-forge` 77 | 78 | If the package is not on conda forge yet, check the documentation to learn how to add it: https://conda-forge.org/docs/maintainer/adding_pkgs.html 79 | 80 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. 81 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@jupyterlab/testutils/lib/babel.config'); 2 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | npm install 2 | npm run build 3 | jupyter labextension install 4 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "jupyterlab_sos", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_sos" 5 | } 6 | -------------------------------------------------------------------------------- /jupyterlab_sos/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from ._version import __version__ 3 | except ImportError: 4 | # Fallback when using the package in dev mode without installing 5 | # in editable mode with pip. It is highly recommended to install 6 | # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs 7 | import warnings 8 | warnings.warn("Importing 'jupyterlab_sos' outside a proper installation.") 9 | __version__ = "dev" 10 | 11 | 12 | def _jupyter_labextension_paths(): 13 | return [{ 14 | "src": "labextension", 15 | "dest": "jupyterlab-sos" 16 | }] 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyterlab-sos", 3 | "version": "0.11.0", 4 | "description": "JupyterLab extension for SoS workflow engine and polyglot notebook", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/vatlab/jupyterlab-sos", 11 | "bugs": { 12 | "url": "https://github.com/vatlab/jupyterlab-sos/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": { 16 | "name": "Bo Peng", 17 | "email": "ben.bob@gmail.com" 18 | }, 19 | "files": [ 20 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 21 | "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", 22 | "src/**/*.{ts,tsx}", 23 | "schema/*.json" 24 | ], 25 | "main": "lib/index.js", 26 | "types": "lib/index.d.ts", 27 | "style": "style/index.css", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/vatlab/jupyterlab-sos.git" 31 | }, 32 | "scripts": { 33 | "build": "jlpm build:lib && jlpm build:labextension:dev", 34 | "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", 35 | "build:labextension": "jupyter labextension build .", 36 | "build:labextension:dev": "jupyter labextension build --development True .", 37 | "build:lib": "tsc --sourceMap", 38 | "build:lib:prod": "tsc", 39 | "clean": "jlpm clean:lib", 40 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 41 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 42 | "clean:labextension": "rimraf jupyterlab_sos/labextension jupyterlab_sos/_version.py", 43 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", 44 | "eslint": "jlpm eslint:check --fix", 45 | "eslint:check": "eslint . --cache --ext .ts,.tsx", 46 | "install:extension": "jlpm build", 47 | "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", 48 | "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", 49 | "prettier": "jlpm prettier:base --write --list-different", 50 | "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", 51 | "prettier:check": "jlpm prettier:base --check", 52 | "stylelint": "jlpm stylelint:check --fix", 53 | "stylelint:check": "stylelint --cache \"style/**/*.css\"", 54 | "test": "jest --coverage", 55 | "watch": "run-p watch:src watch:labextension", 56 | "watch:src": "tsc -w --sourceMap", 57 | "watch:labextension": "jupyter labextension watch ." 58 | }, 59 | "dependencies": { 60 | "@jupyterlab/application": "^4.0.5", 61 | "@jupyterlab/apputils": "^4.1.5", 62 | "@jupyterlab/cells": "^4.0.5", 63 | "@jupyterlab/codemirror": "^4.0.5", 64 | "@jupyterlab/console": "^4.0.5", 65 | "@jupyterlab/coreutils": "^6.0.5", 66 | "@jupyterlab/docregistry": "^4.0.5", 67 | "@jupyterlab/notebook": "^4.0.5", 68 | "@jupyterlab/services": "^7.0.5", 69 | "@jupyterlab/settingregistry": "^4.0.5", 70 | "@jupyterlab/ui-components": "^4.0.5", 71 | "@lumino/algorithm": "^2.0.0", 72 | "@lumino/commands": "^2.0.1", 73 | "@lumino/disposable": "^2.0.0", 74 | "react": "^18.2.0", 75 | "transient-display-data": "^0.4.3" 76 | }, 77 | "devDependencies": { 78 | "@jupyterlab/builder": "^4.0.0", 79 | "@jupyterlab/testutils": "^4.0.0", 80 | "@types/jest": "^29.2.0", 81 | "@types/json-schema": "^7.0.11", 82 | "@types/react": "^18.0.26", 83 | "@types/react-addons-linked-state-mixin": "^0.14.22", 84 | "@typescript-eslint/eslint-plugin": "^6.1.0", 85 | "@typescript-eslint/parser": "^6.1.0", 86 | "codemirror": "^5.53.2", 87 | "css-loader": "^6.7.1", 88 | "eslint": "^8.36.0", 89 | "eslint-config-prettier": "^8.8.0", 90 | "eslint-plugin-prettier": "^5.0.0", 91 | "jest": "^29.2.0", 92 | "npm-run-all": "^4.1.5", 93 | "prettier": "^3.0.0", 94 | "rimraf": "^5.0.1", 95 | "source-map-loader": "^1.0.2", 96 | "style-loader": "^3.3.1", 97 | "stylelint": "^15.10.1", 98 | "stylelint-config-prettier": "^9.0.3", 99 | "stylelint-config-recommended": "^13.0.0", 100 | "stylelint-config-standard": "^34.0.0", 101 | "stylelint-csstree-validator": "^3.0.0", 102 | "stylelint-prettier": "^4.0.0", 103 | "typedoc": "^0.15.4", 104 | "typescript": "~5.0.2", 105 | "yjs": "^13.5.40" 106 | }, 107 | "sideEffects": [ 108 | "style/*.css", 109 | "style/index.js" 110 | ], 111 | "styleModule": "style/index.js", 112 | "publishConfig": { 113 | "access": "public" 114 | }, 115 | "jupyterlab": { 116 | "extension": true, 117 | "schemaDir": "schema", 118 | "outputDir": "jupyterlab_sos/labextension" 119 | }, 120 | "eslintConfig": { 121 | "extends": [ 122 | "eslint:recommended", 123 | "plugin:@typescript-eslint/eslint-recommended", 124 | "plugin:@typescript-eslint/recommended", 125 | "plugin:prettier/recommended" 126 | ], 127 | "parser": "@typescript-eslint/parser", 128 | "parserOptions": { 129 | "project": "tsconfig.json", 130 | "sourceType": "module" 131 | }, 132 | "plugins": [ 133 | "@typescript-eslint" 134 | ], 135 | "rules": { 136 | "@typescript-eslint/naming-convention": [ 137 | "error", 138 | { 139 | "selector": "interface", 140 | "format": [ 141 | "PascalCase" 142 | ], 143 | "custom": { 144 | "regex": "^I[A-Z]", 145 | "match": true 146 | } 147 | } 148 | ], 149 | "@typescript-eslint/no-unused-vars": [ 150 | "warn", 151 | { 152 | "args": "none" 153 | } 154 | ], 155 | "@typescript-eslint/no-explicit-any": "off", 156 | "@typescript-eslint/no-namespace": "off", 157 | "@typescript-eslint/no-use-before-define": "off", 158 | "@typescript-eslint/quotes": [ 159 | "error", 160 | "single", 161 | { 162 | "avoidEscape": true, 163 | "allowTemplateLiterals": false 164 | } 165 | ], 166 | "curly": [ 167 | "error", 168 | "all" 169 | ], 170 | "eqeqeq": "error", 171 | "prefer-arrow-callback": "error" 172 | } 173 | }, 174 | "eslintIgnore": [ 175 | "node_modules", 176 | "dist", 177 | "coverage", 178 | "**/*.d.ts", 179 | "tests", 180 | "**/__tests__", 181 | "ui-tests" 182 | ], 183 | "prettier": { 184 | "singleQuote": true, 185 | "trailingComma": "none", 186 | "arrowParens": "avoid", 187 | "endOfLine": "auto", 188 | "overrides": [ 189 | { 190 | "files": "package.json", 191 | "options": { 192 | "tabWidth": 4 193 | } 194 | } 195 | ] 196 | }, 197 | "stylelint": { 198 | "extends": [ 199 | "stylelint-config-recommended", 200 | "stylelint-config-standard", 201 | "stylelint-prettier/recommended" 202 | ], 203 | "plugins": [ 204 | "stylelint-csstree-validator" 205 | ], 206 | "rules": { 207 | "csstree/validator": true, 208 | "property-no-vendor-prefix": null, 209 | "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$", 210 | "selector-no-vendor-prefix": null, 211 | "value-no-vendor-prefix": null 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version>=0.3.2"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "jupyterlab_sos" 7 | readme = "README.md" 8 | license = { file = "LICENSE" } 9 | requires-python = ">=3.8" 10 | classifiers = [ 11 | "Framework :: Jupyter", 12 | "Framework :: Jupyter :: JupyterLab", 13 | "Framework :: Jupyter :: JupyterLab :: 4", 14 | "Framework :: Jupyter :: JupyterLab :: Extensions", 15 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 16 | "License :: OSI Approved :: BSD License", 17 | "Programming Language :: Python", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.8", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Programming Language :: Python :: 3.12", 24 | ] 25 | dependencies = [ 26 | "sos", 27 | "sos-notebook", 28 | "jupyterlab>=4.2.0", 29 | "transient-display-data", 30 | ] 31 | dynamic = ["version", "description", "authors", "urls", "keywords"] 32 | 33 | [tool.hatch.version] 34 | source = "nodejs" 35 | 36 | [tool.hatch.metadata.hooks.nodejs] 37 | fields = ["description", "authors", "urls"] 38 | 39 | [tool.hatch.build.targets.sdist] 40 | artifacts = ["jupyterlab_sos/labextension"] 41 | exclude = [".github", "binder"] 42 | 43 | [tool.hatch.build.targets.wheel.shared-data] 44 | "jupyterlab_sos/labextension" = "share/jupyter/labextensions/jupyterlab-sos" 45 | "install.json" = "share/jupyter/labextensions/jupyterlab-sos/install.json" 46 | 47 | [tool.hatch.build.hooks.version] 48 | path = "jupyterlab_sos/_version.py" 49 | 50 | [tool.hatch.build.hooks.jupyter-builder] 51 | dependencies = ["hatch-jupyter-builder>=0.5"] 52 | build-function = "hatch_jupyter_builder.npm_builder" 53 | ensured-targets = [ 54 | "jupyterlab_sos/labextension/static/style.js", 55 | "jupyterlab_sos/labextension/package.json", 56 | ] 57 | skip-if-exists = ["jupyterlab_sos/labextension/static/style.js"] 58 | 59 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 60 | build_cmd = "build:prod" 61 | npm = ["jlpm"] 62 | 63 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] 64 | build_cmd = "install:extension" 65 | npm = ["jlpm"] 66 | source_dir = "src" 67 | build_dir = "jupyterlab_sos/labextension" 68 | 69 | [tool.jupyter-releaser.options] 70 | version_cmd = "hatch version" 71 | 72 | [tool.jupyter-releaser.hooks] 73 | before-build-npm = [ 74 | "python -m pip install 'jupyterlab>=4.0.0,<5'", 75 | "jlpm", 76 | "jlpm build:prod" 77 | ] 78 | before-build-python = ["jlpm clean:all"] 79 | 80 | [tool.check-wheel-contents] 81 | ignore = ["W002"] 82 | -------------------------------------------------------------------------------- /schema/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "jupyterlab-sos", 3 | "description": "SoS extension settings.", 4 | "jupyter.lab.toolbars": { 5 | "Cell": [ 6 | { 7 | "name": "kernel_selector" 8 | } 9 | ] 10 | }, 11 | "jupyter.lab.shortcuts": [ 12 | { 13 | "command": "sos:toggle_output", 14 | "keys": [ 15 | "Ctrl Shift O" 16 | ], 17 | "selector": ".jp-Notebook.jp-mod-editMode" 18 | }, 19 | { 20 | "command": "sos:toggle_kernel", 21 | "keys": [ 22 | "Ctrl Shift S" 23 | ], 24 | "selector": ".jp-Notebook.jp-mod-editMode" 25 | }, 26 | { 27 | "command": "sos:toggle_markdown", 28 | "keys": [ 29 | "Ctrl Shift M" 30 | ], 31 | "selector": ".jp-Notebook.jp-mod-editMode" 32 | }, 33 | { 34 | "command": "notebook:run-in-console", 35 | "keys": [ 36 | "Ctrl Shift Enter" 37 | ], 38 | "selector": ".jp-Notebook.jp-mod-editMode" 39 | } 40 | ], 41 | "properties": { 42 | "sos.kernel_codemirror_mode": { 43 | "title": "CodeMirror mode for SoS kernels", 44 | "description": "SoS action - codemirror mode map. The mode can be a single string or an object.", 45 | "default": { 46 | "python": { 47 | "name": "python", 48 | "version": 3 49 | }, 50 | "python2": { 51 | "name": "python", 52 | "version": 2 53 | }, 54 | "python3": { 55 | "name": "python", 56 | "version": 3 57 | }, 58 | "r": "r", 59 | "report": "report", 60 | "pandoc": "markdown", 61 | "download": "markdown", 62 | "markdown": "markdown", 63 | "ruby": "ruby", 64 | "sas": "sas", 65 | "bash": "shell", 66 | "sh": "shell", 67 | "julia": "julia", 68 | "run": "shell", 69 | "javascript": "javascript", 70 | "typescript": { 71 | "name": "javascript", 72 | "typescript": true 73 | }, 74 | "octave": "octave", 75 | "matlab": "octave", 76 | "mllike": "mllike", 77 | "clike": "clike", 78 | "html": "htmlembedded", 79 | "xml": "xml", 80 | "yaml": "yaml", 81 | "json": { 82 | "name": "javascript", 83 | "jsonMode": true 84 | }, 85 | "stex": "stex" 86 | } 87 | } 88 | }, 89 | "additionalProperties": false, 90 | "type": "object" 91 | } -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | __import__("setuptools").setup() 2 | -------------------------------------------------------------------------------- /src/codemirror-sos.ts: -------------------------------------------------------------------------------- 1 | import CodeMirror from 'codemirror'; 2 | 3 | import "codemirror/lib/codemirror"; 4 | import "codemirror/mode/python/python"; 5 | import "codemirror/mode/r/r"; 6 | import "codemirror/mode/markdown/markdown"; 7 | import "codemirror/addon/mode/loadmode" 8 | 9 | import { Manager } from "./manager"; 10 | 11 | var sosKeywords = ["input", "output", "depends", "parameter"]; 12 | var sosActionWords = [ 13 | "script", 14 | "download", 15 | "run", 16 | "bash", 17 | "sh", 18 | "csh", 19 | "tcsh", 20 | "zsh", 21 | "python", 22 | "python2", 23 | "python3", 24 | "R", 25 | "node", 26 | "julia", 27 | "matlab", 28 | "octave", 29 | "ruby", 30 | "perl", 31 | "report", 32 | "pandoc", 33 | "docker_build", 34 | "Rmarkdown" 35 | ]; 36 | var sosMagicWords = [ 37 | "cd", 38 | "capture", 39 | "clear", 40 | "debug", 41 | "dict", 42 | "expand", 43 | "get", 44 | "matplotlib", 45 | "paste", 46 | "preview", 47 | "pull", 48 | "push", 49 | "put", 50 | "render", 51 | "rerun", 52 | "run", 53 | "save", 54 | "sandbox", 55 | "set", 56 | "sessioninfo", 57 | "sosrun", 58 | "sossave", 59 | "shutdown", 60 | "taskinfo", 61 | "tasks", 62 | "toc", 63 | "use", 64 | "with" 65 | ]; 66 | var sosFunctionWords = ["sos_run", "logger", "get_output"]; 67 | 68 | export const sosHintWords = sosKeywords 69 | .concat(sosActionWords) 70 | .concat(sosFunctionWords) 71 | .concat(sosMagicWords); 72 | 73 | var sosDirectives = sosKeywords.map(x => x + ":"); 74 | var sosActions = sosActionWords.map(x => new RegExp("^\\s*" + x + ":")); 75 | var sosMagics = sosMagicWords.map(x => "%" + x); 76 | 77 | 78 | function findMode(mode: string): any { 79 | let modeMap = Manager.manager.get_config('sos.kernel_codemirror_mode'); 80 | if (modeMap) { 81 | if (mode in modeMap) { 82 | return modeMap[mode]; 83 | } else if (typeof mode === 'string' && mode.toLowerCase() in modeMap) { 84 | return modeMap[mode.toLowerCase()] 85 | } 86 | } 87 | return null; 88 | } 89 | 90 | function findModeFromFilename(filename: string): any { 91 | var val = filename, m, mode; 92 | if (m = /.+\.([^.]+)$/.exec(val)) { 93 | var info = (CodeMirror as any).findModeByExtension(m[1]); 94 | if (info) { 95 | mode = info.mode; 96 | } 97 | } else if (/\//.test(val)) { 98 | var info = (CodeMirror as any).findModeByMIME(val); 99 | if (info) { 100 | mode = info.mode; 101 | } 102 | } else { 103 | mode = val; 104 | } 105 | return mode; 106 | } 107 | 108 | function markExpr(python_mode: any) { 109 | return { 110 | startState: function () { 111 | return { 112 | in_python: false, 113 | sigil: false, 114 | matched: true, 115 | python_state: (CodeMirror as any).startState(python_mode) 116 | }; 117 | }, 118 | 119 | copyState: function (state: any) { 120 | return { 121 | in_python: state.in_python, 122 | sigil: state.sigil, 123 | matched: state.matched, 124 | python_state: (CodeMirror as any).copyState( 125 | python_mode, 126 | state.python_state 127 | ) 128 | }; 129 | }, 130 | 131 | token: function (stream: any, state: any) { 132 | if (state.in_python) { 133 | if (stream.match(state.sigil.right)) { 134 | state.in_python = false; 135 | state.python_state = (CodeMirror as any).startState(python_mode); 136 | return "sos-sigil"; 137 | } 138 | let it = null; 139 | try { 140 | it = python_mode.token(stream, state.python_state); 141 | } catch (error) { 142 | return ( 143 | "sos-interpolated error" + (state.matched ? "" : " sos-unmatched") 144 | ); 145 | } 146 | if (it == "variable" || it == "builtin") { 147 | let ct = stream.current(); 148 | // warn users in the use of input and output in {} 149 | if (ct === "input" || ct === "output") it += " error"; 150 | } 151 | return ( 152 | (it ? "sos-interpolated " + it : "sos-interpolated") + 153 | (state.matched ? "" : " sos-unmatched") 154 | ); 155 | } else { 156 | // remove the double brace case, the syntax highlighter 157 | // does not have to worry (highlight) }}, although it would 158 | // probably mark an error for single } 159 | if (state.sigil.left === "{" && stream.match(/\{\{/)) return null; 160 | if (stream.match(state.sigil.left)) { 161 | state.in_python = true; 162 | // let us see if there is any right sigil till the end of the editor. 163 | try { 164 | let rest = stream.string.slice(stream.pos); 165 | if (!rest.includes(state.sigil.right)) { 166 | state.matched = false; 167 | for (let idx = 1; idx < 5; ++idx) { 168 | if (stream.lookAhead(idx).includes(state.sigil.right)) { 169 | state.matched = true; 170 | break; 171 | } 172 | } 173 | } 174 | } catch (error) { 175 | // only codemirror 5.27.0 supports this function 176 | } 177 | return "sos-sigil" + (state.matched ? "" : " sos-unmatched"); 178 | } 179 | while (stream.next() && !stream.match(state.sigil.left, false)) { } 180 | return null; 181 | } 182 | } 183 | }; 184 | } 185 | 186 | (CodeMirror as any).modeURL = "codemirror/mode/%N/%N.js"; 187 | 188 | export function sos_mode(conf: CodeMirror.EditorConfiguration, parserConf: any) { 189 | let sosPythonConf: any = {}; 190 | for (let prop in parserConf) { 191 | if (parserConf.hasOwnProperty(prop)) { 192 | sosPythonConf[prop] = parserConf[prop]; 193 | } 194 | } 195 | sosPythonConf.name = "python"; 196 | sosPythonConf.version = 3; 197 | sosPythonConf.extra_keywords = sosActionWords.concat(sosFunctionWords); 198 | // this is the SoS flavored python mode with more identifiers 199 | let base_mode: any = null; 200 | if ("base_mode" in parserConf && parserConf.base_mode) { 201 | let spec = findMode(parserConf.base_mode); 202 | if (spec) { 203 | let modename = spec; 204 | if (typeof spec != "string") { 205 | modename = spec.name; 206 | } 207 | if (!CodeMirror.modes.hasOwnProperty(modename)) { 208 | console.log(`Load codemirror mode ${modename}`); 209 | (CodeMirror as any).requireMode(modename, function () { }, {}); 210 | } 211 | base_mode = CodeMirror.getMode(conf, spec); 212 | // base_mode = CodeMirror.getMode(conf, mode); 213 | } else { 214 | base_mode = CodeMirror.getMode(conf, parserConf.base_mode); 215 | } 216 | // } else { 217 | // console.log( 218 | // `No base mode is found for ${parserConf.base_mode}. Python mode used.` 219 | // ); 220 | } 221 | 222 | // if there is a user specified base mode, this is the single cell mode 223 | if (base_mode) { 224 | var python_mode = (CodeMirror as any).getMode( 225 | {}, 226 | { 227 | name: "python", 228 | version: 3 229 | } 230 | ); 231 | var overlay_mode = markExpr(python_mode); 232 | return { 233 | startState: function () { 234 | return { 235 | sos_mode: true, 236 | base_state: (CodeMirror as any).startState(base_mode), 237 | overlay_state: (CodeMirror as any).startState(overlay_mode), 238 | // for overlay 239 | basePos: 0, 240 | baseCur: null, 241 | overlayPos: 0, 242 | overlayCur: null, 243 | streamSeen: null 244 | }; 245 | }, 246 | 247 | copyState: function (state: any) { 248 | return { 249 | sos_mode: state.sos_mode, 250 | base_state: (CodeMirror as any).copyState( 251 | base_mode, 252 | state.base_state 253 | ), 254 | overlay_state: (CodeMirror as any).copyState( 255 | overlay_mode, 256 | state.overlay_state 257 | ), 258 | // for overlay 259 | basePos: state.basePos, 260 | baseCur: null, 261 | overlayPos: state.overlayPos, 262 | overlayCur: null 263 | }; 264 | }, 265 | 266 | token: function (stream: any, state: any) { 267 | if (state.sos_mode) { 268 | if (stream.sol()) { 269 | let sl = stream.peek(); 270 | if (sl == "!") { 271 | stream.skipToEnd(); 272 | return "meta"; 273 | } else if (sl == "#") { 274 | stream.skipToEnd(); 275 | return "comment"; 276 | } 277 | for (var i = 0; i < sosMagics.length; i++) { 278 | if (stream.match(sosMagics[i])) { 279 | if (sosMagics[i] === "%expand") { 280 | // %expand, %expand --in R 281 | if (stream.eol() || stream.match(/\s*(-i\s*\S+|--in\s*\S+)?$/, false)) { 282 | state.overlay_state.sigil = { 283 | left: "{", 284 | right: "}" 285 | }; 286 | } else { 287 | let found = stream.match(/\s+(\S+)\s+(\S+)\s*(-i\s*\S+|--in\s*\S+)?$/, false); 288 | if (found) { 289 | state.overlay_state.sigil = { 290 | left: found[1].match(/^.*[A-Za-z]$/) ? found[1] + ' ' : found[1], 291 | right: found[2].match(/^[A-Za-z].*$/) ? ' ' + found[2] : found[2] 292 | }; 293 | } else { 294 | state.overlay_state.sigil = false; 295 | } 296 | } 297 | } 298 | // the rest of the lines will be processed as Python code 299 | return "meta"; 300 | } 301 | } 302 | state.sos_mode = false; 303 | } else { 304 | stream.skipToEnd(); 305 | return null; 306 | } 307 | } 308 | 309 | if (state.overlay_state.sigil) { 310 | if ( 311 | stream != state.streamSeen || 312 | Math.min(state.basePos, state.overlayPos) < stream.start 313 | ) { 314 | state.streamSeen = stream; 315 | state.basePos = state.overlayPos = stream.start; 316 | } 317 | 318 | if (stream.start == state.basePos) { 319 | state.baseCur = base_mode.token(stream, state.base_state); 320 | state.basePos = stream.pos; 321 | } 322 | if (stream.start == state.overlayPos) { 323 | stream.pos = stream.start; 324 | state.overlayCur = overlay_mode.token( 325 | stream, 326 | state.overlay_state 327 | ); 328 | state.overlayPos = stream.pos; 329 | } 330 | stream.pos = Math.min(state.basePos, state.overlayPos); 331 | 332 | // state.overlay.combineTokens always takes precedence over combine, 333 | // unless set to null 334 | return state.overlayCur ? state.overlayCur : state.baseCur; 335 | } else { 336 | return base_mode.token(stream, state.base_state); 337 | } 338 | }, 339 | 340 | indent: function (state: any, textAfter: string) { 341 | // inner indent 342 | if (!state.sos_mode) { 343 | if (!base_mode.indent) return CodeMirror.Pass; 344 | // inner mode will autoamtically indent + 4 345 | return base_mode.indent(state.base_state, textAfter); 346 | } else { 347 | // sos mode has no indent 348 | return 0; 349 | } 350 | }, 351 | 352 | innerMode: function (state: any) { 353 | return state.sos_mode 354 | ? { 355 | state: state.base_state, 356 | mode: base_mode 357 | } 358 | : null; 359 | }, 360 | 361 | lineComment: "#", 362 | fold: "indent" 363 | }; 364 | } else { 365 | // this is SoS mode 366 | base_mode = (CodeMirror as any).getMode(conf, sosPythonConf); 367 | overlay_mode = markExpr(base_mode); 368 | return { 369 | startState: function () { 370 | return { 371 | sos_state: null, 372 | base_state: (CodeMirror as any).startState(base_mode), 373 | overlay_state: (CodeMirror as any).startState(overlay_mode), 374 | inner_mode: null, 375 | inner_state: null, 376 | // for overlay 377 | basePos: 0, 378 | baseCur: null, 379 | overlayPos: 0, 380 | overlayCur: null, 381 | streamSeen: null 382 | }; 383 | }, 384 | 385 | copyState: function (state: any) { 386 | return { 387 | sos_state: state.sos_state, 388 | base_state: (CodeMirror as any).copyState( 389 | base_mode, 390 | state.base_state 391 | ), 392 | overlay_state: (CodeMirror as any).copyState( 393 | overlay_mode, 394 | state.overlay_state 395 | ), 396 | inner_mode: state.inner_mode, 397 | inner_state: 398 | state.inner_mode && 399 | (CodeMirror as any).copyState( 400 | state.inner_mode, 401 | state.inner_state 402 | ), 403 | // for overlay 404 | basePos: state.basePos, 405 | baseCur: null, 406 | overlayPos: state.overlayPos, 407 | overlayCur: null 408 | }; 409 | }, 410 | 411 | token: function (stream: any, state: any) { 412 | if (stream.sol()) { 413 | let sl = stream.peek(); 414 | if (sl == "[") { 415 | // header, move to the end 416 | if (stream.match(/^\[.*\]$/, false)) { 417 | // if there is : 418 | if (stream.match(/^\[[\s\w_,-]+:/)) { 419 | state.sos_state = "header_option"; 420 | return "header line-section-header"; 421 | } else if (stream.match(/^\[[\s\w,-]+\]$/)) { 422 | // reset state 423 | state.sos_state = null; 424 | state.inner_mode = null; 425 | return "header line-section-header"; 426 | } 427 | } 428 | } else if (sl == "!") { 429 | stream.eatWhile(/\S/); 430 | return "meta"; 431 | } else if (sl == "#") { 432 | stream.skipToEnd(); 433 | return "comment"; 434 | } else if (sl == "%") { 435 | stream.eatWhile(/\S/); 436 | return "meta"; 437 | } else if ( 438 | state.sos_state && 439 | state.sos_state.startsWith("entering ") 440 | ) { 441 | // the second parameter is starting column 442 | let mode = findMode(state.sos_state.slice(9).toLowerCase()); 443 | if (mode) { 444 | state.inner_mode = CodeMirror.getMode(conf, mode); 445 | state.inner_state = (CodeMirror as any).startState( 446 | state.inner_mode, 447 | stream.indentation() 448 | ); 449 | state.sos_state = null; 450 | } else { 451 | state.sos_state = 'unknown_language'; 452 | } 453 | state.sos_indent = stream.indentation(); 454 | } 455 | if (stream.indentation() === 0 && 456 | ((state.inner_mode && 457 | stream.indentation() < state.sos_indent 458 | ) || state.sos_state == 'unknown_language')) { 459 | state.inner_mode = null; 460 | state.sos_state = null; 461 | } 462 | for (var i = 0; i < sosDirectives.length; i++) { 463 | if (stream.match(sosDirectives[i])) { 464 | // the rest of the lines will be processed as Python code 465 | state.sos_state = "directive_option"; 466 | return "keyword strong"; 467 | } 468 | } 469 | for (var i = 0; i < sosActions.length; i++) { 470 | if (stream.match(sosActions[i])) { 471 | // switch to submode? 472 | if (stream.eol()) { 473 | // really 474 | let mode = findMode(stream.current().slice(0, -1)); 475 | if (mode) { 476 | state.sos_state = 477 | "entering " + stream.current().slice(0, -1); 478 | } else { 479 | state.sos_state = "entering unknown_language"; 480 | } 481 | } else { 482 | state.sos_state = "start " + stream.current().slice(0, -1); 483 | } 484 | state.overlay_state.sigil = false; 485 | return "builtin strong"; 486 | } 487 | } 488 | // if unknown action 489 | if (stream.match(/\w+:/)) { 490 | state.overlay_state.sigil = false; 491 | state.sos_state = "start " + stream.current().slice(0, -1); 492 | return "builtin strong"; 493 | } 494 | 495 | } else if (state.sos_state == "header_option") { 496 | // stuff after : 497 | if (stream.peek() == "]") { 498 | // move next 499 | stream.next(); 500 | // ] is the last char 501 | if (stream.eol()) { 502 | state.sos_state = null; 503 | state.inner_mode = null; 504 | return "header line-section-header"; 505 | } else { 506 | stream.backUp(1); 507 | let it = base_mode.token(stream, state.base_state); 508 | return it ? it + " sos-option" : null; 509 | } 510 | } else { 511 | let it = base_mode.token(stream, state.base_state); 512 | return it ? it + " sos-option" : null; 513 | } 514 | } else if (state.sos_state == "directive_option") { 515 | // stuff after input:, R: etc 516 | if (stream.peek() == ",") { 517 | // move next 518 | stream.next(); 519 | // , is the last char, continue option line 520 | if (stream.eol()) { 521 | stream.backUp(1); 522 | let it = base_mode.token(stream, state.base_state); 523 | return it ? it + " sos-option" : null; 524 | } 525 | stream.backUp(1); 526 | } else if (stream.eol()) { 527 | // end of line stops option mode 528 | state.sos_state = null; 529 | state.inner_mode = null; 530 | } 531 | let it = base_mode.token(stream, state.base_state); 532 | return it ? it + " sos-option" : null; 533 | } else if (state.sos_state && state.sos_state.startsWith("start ")) { 534 | // try to understand option expand= 535 | if (stream.match(/^.*expand\s*=\s*True/, false)) { 536 | // highlight {} 537 | state.overlay_state.sigil = { 538 | left: "{", 539 | right: "}" 540 | }; 541 | } else { 542 | let found = stream.match(/^.*expand\s*=\s*"(\S+) (\S+)"/, false); 543 | if (!found) 544 | found = stream.match(/^.*expand\s*=\s*'(\S+) (\S+)'/, false); 545 | if (found) { 546 | state.overlay_state.sigil = { 547 | left: found[1].match(/^.*[A-Za-z]$/) ? found[1] + ' ' : found[1], 548 | right: found[2].match(/^[A-Za-z].*$/) ? ' ' + found[2] : found[2] 549 | }; 550 | } 551 | } 552 | let mode_string = state.sos_state.slice(6).toLowerCase(); 553 | // for report, we need to find "output" option 554 | if (mode_string === "report" && 555 | stream.match(/^.*output\s*=\s*/, false)) { 556 | let found = stream.match(/^.*output\s*=\s*[rRbufF]*"""([^"]+)"""/, false); 557 | if (!found) 558 | found = stream.match(/^.*output\s*=\s*[rRbufF]*'''([^.]+)'''/, false); 559 | if (!found) 560 | found = stream.match(/^.*output\s*=\s*[rRbufF]*"([^"]+)"/, false); 561 | if (!found) 562 | found = stream.match(/^.*output\s*=\s*[rRbufF]*'([^']+)'/, false); 563 | 564 | // found[1] is the filename 565 | state.sos_state = 'start ' + findModeFromFilename(found ? found[1] : found); 566 | } 567 | let token = base_mode.token(stream, state.base_state); 568 | // if it is end of line, ending the starting switch mode 569 | if (stream.eol() && stream.peek() !== ",") { 570 | // really 571 | let mode = findMode(state.sos_state.slice(6).toLowerCase()); 572 | if (mode) { 573 | state.sos_state = "entering " + state.sos_state.slice(6); 574 | } else { 575 | state.sos_state = "entering unknown_language"; 576 | } 577 | } 578 | return token + " sos-option"; 579 | } 580 | // can be start of line but not special 581 | if (state.sos_state == "unknown_language") { 582 | // we still handle {} in no man unknown_language 583 | if (state.overlay_state.sigil) { 584 | return overlay_mode.token(stream, state.overlay_state); 585 | } else { 586 | stream.skipToEnd(); 587 | return null; 588 | } 589 | } else if (state.inner_mode) { 590 | let it = "sos_script "; 591 | if (!state.overlay_state.sigil) { 592 | let st = state.inner_mode.token(stream, state.inner_state); 593 | return st ? it + st : null; 594 | } else { 595 | // overlay mode, more complicated 596 | if ( 597 | stream != state.streamSeen || 598 | Math.min(state.basePos, state.overlayPos) < stream.start 599 | ) { 600 | state.streamSeen = stream; 601 | state.basePos = state.overlayPos = stream.start; 602 | } 603 | 604 | if (stream.start == state.basePos) { 605 | state.baseCur = state.inner_mode.token( 606 | stream, 607 | state.inner_state 608 | ); 609 | state.basePos = stream.pos; 610 | } 611 | if (stream.start == state.overlayPos) { 612 | stream.pos = stream.start; 613 | state.overlayCur = overlay_mode.token( 614 | stream, 615 | state.overlay_state 616 | ); 617 | state.overlayPos = stream.pos; 618 | } 619 | stream.pos = Math.min(state.basePos, state.overlayPos); 620 | // state.overlay.combineTokens always takes precedence over combine, 621 | // unless set to null 622 | return ( 623 | (state.overlayCur ? state.overlayCur : state.baseCur) + 624 | " sos-script" 625 | ); 626 | } 627 | } else { 628 | return base_mode.token(stream, state.base_state); 629 | } 630 | }, 631 | 632 | indent: function (state: any, textAfter: string) { 633 | // inner indent 634 | if (state.inner_mode) { 635 | if (!state.inner_mode.indent) return CodeMirror.Pass; 636 | return state.inner_mode.indent(state.inner_mode, textAfter) + 2; 637 | } else { 638 | return base_mode.indent(state.base_state, textAfter); 639 | } 640 | }, 641 | 642 | innerMode: function (state: any) { 643 | return state.inner_mode 644 | ? null 645 | : { 646 | state: state.base_state, 647 | mode: base_mode 648 | }; 649 | }, 650 | 651 | lineComment: "#", 652 | fold: "indent", 653 | electricInput: /^\s*[\}\]\)]$/ 654 | }; 655 | } 656 | }; 657 | -------------------------------------------------------------------------------- /src/execute.ts: -------------------------------------------------------------------------------- 1 | import { 2 | // Notebook, 3 | NotebookPanel 4 | } from "@jupyterlab/notebook"; 5 | 6 | import { KernelMessage, Kernel } from "@jupyterlab/services"; 7 | 8 | import { Cell, ICellModel, IMarkdownCellModel} from "@jupyterlab/cells"; 9 | 10 | import { ConsolePanel } from "@jupyterlab/console"; 11 | 12 | import { JSONObject } from '@lumino/coreutils'; 13 | 14 | import { Manager } from "./manager"; 15 | import { changeCellKernel, hideLanSelector } from "./selectors"; 16 | 17 | export function wrapExecutor(panel: NotebookPanel) { 18 | let kernel = panel.sessionContext.session?.kernel; 19 | 20 | // override kernel execute with the wrapper. 21 | // however, this function can be called multiple times for kernel 22 | // restart etc, so we should be careful 23 | if (kernel && !kernel.hasOwnProperty("orig_execute")) { 24 | (kernel as any)["orig_execute"] = kernel.requestExecute; 25 | kernel.requestExecute = my_execute; 26 | console.log("executor patched"); 27 | } 28 | } 29 | 30 | export function wrapConsoleExecutor(panel: ConsolePanel) { 31 | let kernel = panel.sessionContext.session?.kernel; 32 | 33 | // override kernel execute with the wrapper. 34 | // however, this function can be called multiple times for kernel 35 | // restart etc, so we should be careful 36 | if (!kernel.hasOwnProperty("orig_execute")) { 37 | (kernel as any)["orig_execute"] = kernel.requestExecute; 38 | kernel.requestExecute = my_execute; 39 | console.log("console executor patched"); 40 | } 41 | } 42 | 43 | function scanHeaderLines(cells: ReadonlyArray) { 44 | let TOC = ""; 45 | for (let i = 0; i < cells.length; ++i) { 46 | let cell = cells[i].model; 47 | if (cell.type === "markdown") { 48 | var lines = (cell as IMarkdownCellModel).sharedModel.getSource().split("\n"); 49 | for (let l = 0; l < lines.length; ++l) { 50 | if (lines[l].match("^#+ ")) { 51 | TOC += lines[l] + "\n"; 52 | } 53 | } 54 | } 55 | } 56 | return TOC; 57 | } 58 | 59 | // get the workflow part of text from a cell 60 | function getCellWorkflow(cell: ICellModel) { 61 | var lines = cell.sharedModel.getSource().split("\n"); 62 | var workflow = ""; 63 | var l; 64 | for (l = 0; l < lines.length; ++l) { 65 | if (lines[l].startsWith("%include") || lines[l].startsWith("%from")) { 66 | workflow += lines[l] + "\n"; 67 | continue; 68 | } else if ( 69 | lines[l].startsWith("#") || 70 | lines[l].startsWith("%") || 71 | lines[l].trim() === "" || 72 | lines[l].startsWith("!") 73 | ) { 74 | continue; 75 | } else if (lines[l].startsWith("[") && lines[l].endsWith("]")) { 76 | // include comments before section header 77 | let c = l - 1; 78 | let comment = ""; 79 | while (c >= 0 && lines[c].startsWith("#")) { 80 | comment = lines[c] + "\n" + comment; 81 | c -= 1; 82 | } 83 | workflow += comment + lines.slice(l).join("\n") + "\n\n"; 84 | break; 85 | } 86 | } 87 | return workflow; 88 | } 89 | 90 | // get workflow from notebook 91 | function getNotebookWorkflow(panel: NotebookPanel) { 92 | let cells = panel.content.widgets; 93 | let workflow = ""; 94 | for (let i = 0; i < cells.length; ++i) { 95 | let cell = cells[i].model; 96 | if ( 97 | cell.type === "code" && 98 | (!cell.getMetadata('kernel') || cell.getMetadata('kernel') === "SoS") 99 | ) { 100 | workflow += getCellWorkflow(cell); 101 | } 102 | } 103 | if (workflow != "") { 104 | workflow = "#!/usr/bin/env sos-runner\n#fileformat=SOS1.0\n\n" + workflow; 105 | } 106 | return workflow; 107 | } 108 | 109 | function getNotebookContent(panel: NotebookPanel) { 110 | let cells = panel.content.widgets; 111 | let workflow = "#!/usr/bin/env sos-runner\n#fileformat=SOS1.0\n\n"; 112 | 113 | for (let i = 0; i < cells.length; ++i) { 114 | let cell = cells[i].model; 115 | if (cell.type === "code" ) { 116 | workflow += `# cell ${i + 1}, kernel=${cell.getMetadata('kernel')}\n${cell.sharedModel.getSource()}\n\n` 117 | } 118 | } 119 | return workflow; 120 | } 121 | 122 | function my_execute( 123 | content: KernelMessage.IExecuteRequestMsg['content'], 124 | disposeOnDone: boolean = true, 125 | metadata?: JSONObject 126 | ): Kernel.IShellFuture< 127 | KernelMessage.IExecuteRequestMsg, 128 | KernelMessage.IExecuteReplyMsg 129 | > { 130 | let code = content.code; 131 | 132 | metadata.sos = {}; 133 | let panel = Manager.currentNotebook; 134 | if ( 135 | code.match( 136 | /^%sosrun($|\s)|^%run($|\s)|^%convert($|\s)|^%preview\s.*(-w|--workflow).*$/m 137 | ) 138 | ) { 139 | if (code.match(/^%convert\s.*(-a|--all).*$/m)) { 140 | metadata.sos["workflow"] = getNotebookContent(panel); 141 | } else { 142 | metadata.sos["workflow"] = getNotebookWorkflow(panel); 143 | } 144 | } 145 | metadata.sos["path"] = panel.context.path; 146 | metadata.sos["use_panel"] = Manager.consolesOfNotebook(panel).length > 0; 147 | 148 | metadata.sos["use_iopub"] = true; 149 | 150 | let info = Manager.manager.get_info(panel); 151 | 152 | // find the cell that is being executed... 153 | let cells = panel.content.widgets; 154 | 155 | if (code.match(/^%toc/m)) { 156 | metadata.sos["toc"] = scanHeaderLines(cells); 157 | } 158 | 159 | let cell = panel.content.widgets.find(x => x.model.id === metadata.cellId); 160 | if (cell) { 161 | // check * 162 | // let prompt = cell.node.querySelector(".jp-InputArea-prompt"); 163 | // if (!prompt || prompt.textContent.indexOf("*") === -1) continue; 164 | // use cell kernel if meta exists, otherwise use nb.metadata["sos"].default_kernel 165 | if (info.autoResume) { 166 | metadata.sos["rerun"] = true; 167 | info.autoResume = false; 168 | } 169 | metadata.sos["cell_id"] = cell.model.id; 170 | metadata.sos["cell_kernel"] = cell.model.getMetadata('kernel') as string; 171 | if (metadata.sos["cell_kernel"] === "Markdown") { 172 | // fold the input of markdown cells 173 | cell.inputHidden = true; 174 | } 175 | } else { 176 | let labconsole = Manager.currentConsole.console; 177 | let last_cell = labconsole.cells.get(labconsole.cells.length - 1); 178 | let kernel = last_cell.model.getMetadata('kernel'); 179 | kernel = kernel ? kernel.toString() : "SoS"; 180 | 181 | // change the color of console cell 182 | changeCellKernel(last_cell, kernel, info); 183 | changeCellKernel(labconsole.promptCell, kernel, info); 184 | 185 | // hide the drop down box 186 | hideLanSelector(last_cell); 187 | metadata.sos["cell_kernel"] = kernel; 188 | metadata.sos["cell_id"] = -1; 189 | content.silent = false; 190 | content.store_history = true; 191 | } 192 | return this.orig_execute(content, disposeOnDone, metadata); 193 | } 194 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JupyterFrontEnd, 3 | JupyterFrontEndPlugin 4 | } from '@jupyterlab/application'; 5 | 6 | import { each } from '@lumino/algorithm'; 7 | 8 | import { IDisposable, DisposableDelegate } from '@lumino/disposable'; 9 | 10 | import { Cell, CodeCell } from '@jupyterlab/cells'; 11 | 12 | import { Kernel } from '@jupyterlab/services'; 13 | 14 | import { DocumentRegistry } from '@jupyterlab/docregistry'; 15 | 16 | import { ISettingRegistry } from '@jupyterlab/settingregistry'; 17 | 18 | import { KernelMessage } from '@jupyterlab/services'; 19 | 20 | import { ICommandPalette, IToolbarWidgetRegistry } from '@jupyterlab/apputils'; 21 | 22 | import { IEditorLanguageRegistry } from '@jupyterlab/codemirror'; 23 | 24 | import { 25 | NotebookPanel, 26 | INotebookModel, 27 | INotebookTracker 28 | } from '@jupyterlab/notebook'; 29 | 30 | import { IConsoleTracker } from '@jupyterlab/console'; 31 | 32 | // import { 33 | // sosHintWords, sos_mode 34 | // } from "./codemirror-sos"; 35 | 36 | import { 37 | addLanSelector, 38 | updateCellStyles, 39 | changeCellKernel, 40 | changeStyleOnKernel, 41 | saveKernelInfo, 42 | toggleDisplayOutput, 43 | toggleCellKernel, 44 | toggleMarkdownCell, 45 | KernelSwitcher, 46 | markSoSNotebookPanel 47 | } from './selectors'; 48 | 49 | import { wrapExecutor, wrapConsoleExecutor } from './execute'; 50 | 51 | // define and register SoS CodeMirror mode 52 | import './codemirror-sos'; 53 | 54 | import '../style/index.css'; 55 | 56 | import { Manager } from './manager'; 57 | 58 | /* 59 | * Define SoS File msg_type 60 | */ 61 | const SOS_MIME_TYPE = 'text/x-sos'; 62 | 63 | function registerSoSFileType(app: JupyterFrontEnd) { 64 | app.docRegistry.addFileType({ 65 | name: 'SoS', 66 | displayName: 'SoS File', 67 | extensions: ['.sos'], 68 | mimeTypes: [SOS_MIME_TYPE], 69 | iconClass: 'jp-MaterialIcon sos_icon' 70 | }); 71 | } 72 | 73 | function formatDuration(ms: number): string { 74 | let res = []; 75 | let seconds: number = Math.floor(ms / 1000); 76 | let day: number = Math.floor(seconds / 86400); 77 | if (day > 0) { 78 | res.push(day + ' day'); 79 | } 80 | let hh = Math.floor((seconds % 86400) / 3600); 81 | if (hh > 0) { 82 | res.push(hh + ' hr'); 83 | } 84 | let mm = Math.floor((seconds % 3600) / 60); 85 | if (mm > 0) { 86 | res.push(mm + ' min'); 87 | } 88 | let ss = seconds % 60; 89 | if (ss > 0) { 90 | res.push(ss + ' sec'); 91 | } 92 | let ret = res.join(' '); 93 | if (ret === '') { 94 | return '0 sec'; 95 | } else { 96 | return ret; 97 | } 98 | } 99 | 100 | function update_duration() { 101 | setInterval(function () { 102 | document 103 | .querySelectorAll("[id^='status_duration_']") 104 | .forEach((item: Element) => { 105 | if (item.className != 'running') { 106 | return; 107 | } 108 | (item as HTMLElement).innerText = 109 | 'Ran for ' + 110 | formatDuration( 111 | +new Date() - +new Date(parseFloat(item.getAttribute('datetime'))) 112 | ); 113 | }); 114 | }, 5000); 115 | } 116 | 117 | /* When a notebook is opened with multiple workflow or task tables, 118 | * the tables have display_id but the ID maps will not be properly 119 | * setup so that the tables cannot be updated with another 120 | * update_display_data message. To fix this problem, we will have 121 | * to manually populate the 122 | * output_area._display_id_targets 123 | * structure. 124 | */ 125 | function fix_display_id(cell) { 126 | if (cell.outputArea._displayIdMap.size > 0) { 127 | return; 128 | } 129 | for (let idx = 0; idx < cell.outputArea.model.length; ++idx) { 130 | let output = cell.outputArea.model.get(idx); 131 | if (output.type != 'display_data' || !output.data['text/html']) { 132 | continue; 133 | } 134 | // the HTML should look like 135 | // 136 | if (!output.data || !output.data['text/html']) { 137 | continue; 138 | } 139 | let id = output.data['text/html'].match(/id="([^"]*)"/); 140 | if (!id || !id[1]) { 141 | continue; 142 | } 143 | let targets = cell.outputArea._displayIdMap.get(id[1]) || []; 144 | targets.push(idx); 145 | let target_id = id[1]; 146 | if (target_id.match('^task_.*')) { 147 | target_id = target_id.split('_').slice(0, -1).join('_'); 148 | } 149 | cell.outputArea._displayIdMap.set(target_id, targets); 150 | } 151 | } 152 | 153 | function add_data_to_cell(cell, data, display_id) { 154 | if (data.output_type === 'update_display_data') { 155 | fix_display_id(cell); 156 | let targets = cell.outputArea._displayIdMap.get(display_id); 157 | if (!targets) { 158 | // something wrong 159 | console.log('Failed to rebuild displayIdMap'); 160 | return; 161 | } 162 | data.output_type = 'display_data'; 163 | for (let index of targets) { 164 | cell.outputArea.model.set(index, data); 165 | } 166 | } else { 167 | cell.outputArea.model.add(data); 168 | let targets = cell.outputArea._displayIdMap.get(display_id) || []; 169 | targets.push(cell.outputArea.model.length - 1); 170 | cell.outputArea._displayIdMap.set(display_id, targets); 171 | } 172 | } 173 | 174 | // add workflow status indicator table 175 | function update_workflow_status(info, panel) { 176 | // find the cell 177 | let cell_id = info.cell_id; 178 | let cell = panel.content.widgets.find(x => x.model.id == cell_id); 179 | if (!cell) { 180 | console.log(`Cannot find cell by ID ${info.cell_id}`); 181 | return; 182 | } 183 | 184 | // if there is an existing status table, try to retrieve its information 185 | // if the new data does not have it 186 | let has_status_table = document.getElementById(`workflow_${cell_id}`); 187 | if (!has_status_table && info.status != 'pending') { 188 | return; 189 | } 190 | let timer_text = ''; 191 | if (info.start_time) { 192 | // convert from python time to JS time. 193 | info.start_time = info.start_time * 1000; 194 | } 195 | if (info.status == 'purged') { 196 | if (!has_status_table) { 197 | return; 198 | } 199 | let data = { 200 | output_type: 'update_display_data', 201 | transient: { display_id: `workflow_${cell_id}` }, 202 | metadata: {}, 203 | data: { 204 | 'text/html': '' 205 | } 206 | }; 207 | add_data_to_cell(cell, data, `workflow_${cell_id}`); 208 | } 209 | if (has_status_table) { 210 | // if we already have timer, let us try to "fix" it in the notebook 211 | let timer = document.getElementById(`status_duration_${cell_id}`); 212 | timer_text = timer.innerText; 213 | if ( 214 | timer_text === '' && 215 | (info.status === 'completed' || 216 | info.status === 'failed' || 217 | info.status === 'aborted') 218 | ) { 219 | timer_text = 'Ran for < 5 seconds'; 220 | } 221 | if (!info.start_time) { 222 | info.start_time = timer.getAttribute('datetime'); 223 | } 224 | // 225 | if (!info.workflow_id) { 226 | info.workflow_id = document.getElementById( 227 | `workflow_id_${cell_id}` 228 | ).innerText; 229 | } 230 | if (!info.workflow_name) { 231 | info.workflow_name = document.getElementById( 232 | `workflow_name_${cell_id}` 233 | ).innerText; 234 | } 235 | if (!info.index) { 236 | info.index = document.getElementById( 237 | `workflow_index_${cell_id}` 238 | ).innerText; 239 | } 240 | } 241 | // new and existing, check icon 242 | let status_class = { 243 | pending: 'fa-square-o', 244 | running: 'fa-spinner fa-pulse fa-spin', 245 | completed: 'fa-check-square-o', 246 | failed: 'fa-times-circle-o', 247 | aborted: 'fa-frown-o' 248 | }; 249 | 250 | // look for status etc and update them. 251 | let onmouseover = `onmouseover='this.classList="fa fa-2x fa-fw fa-trash"'`; 252 | let onmouseleave = `onmouseleave='this.classList="fa fa-2x fa-fw ${status_class[info.status] 253 | }"'`; 254 | let onclick = `onclick="cancel_workflow(this.id.substring(21))"`; 255 | 256 | let data = { 257 | output_type: has_status_table ? 'update_display_data' : 'display_data', 258 | transient: { display_id: `workflow_${cell_id}` }, 259 | metadata: {}, 260 | data: { 261 | 'text/html': ` 262 |
263 | 264 | 269 | 273 | 278 | 282 | 287 | 288 |
265 | 268 | 270 |
${info.workflow_name
 271 |         }
272 |
274 | Workflow ID
275 |
${info.workflow_id
 276 |         }
277 |
279 | Index
280 |
#${info.index}
281 |
283 | ${info.status}
284 |
286 |
289 | ` 290 | } 291 | }; 292 | add_data_to_cell(cell, data, `workflow_${cell_id}`); 293 | } 294 | 295 | function update_task_status(info, panel) { 296 | // find the cell 297 | //console.log(info); 298 | // special case, purge by tag, there is no task_id 299 | if (!info.task_id && info.tag && info.status == 'purged') { 300 | // find all elements by tag 301 | let elems = document.getElementsByClassName(`task_tag_${info.tag}`); 302 | if (!elems) { 303 | return; 304 | } 305 | let cell_elems = Array.from(elems).map(x => x.closest('.jp-CodeCell')); 306 | let cells = cell_elems.map(cell_elem => 307 | panel.content.widgets.find(x => x.node == cell_elem) 308 | ); 309 | let display_ids = Array.from(elems).map(x => 310 | x.closest('.task_table').id.split('_').slice(0, -1).join('_') 311 | ); 312 | 313 | for (let i = 0; i < cells.length; ++i) { 314 | let data = { 315 | output_type: 'update_display_data', 316 | transient: { display_id: display_ids[i] }, 317 | metadata: {}, 318 | data: { 319 | 'text/html': '' 320 | } 321 | }; 322 | add_data_to_cell(cells[i], data, display_ids[i]); 323 | } 324 | return; 325 | } 326 | 327 | let elem_id = `${info.queue}_${info.task_id}`; 328 | // convert between Python and JS float time 329 | if (info.start_time) { 330 | info.start_time = info.start_time * 1000; 331 | } 332 | // find the status table 333 | let cell_id = info.cell_id; 334 | let cell = null; 335 | let has_status_table; 336 | 337 | if (cell_id) { 338 | cell = panel.content.widgets.find(x => x.model.id == cell_id); 339 | has_status_table = document.getElementById(`task_${elem_id}_${cell_id}`); 340 | if (!has_status_table && info.status != 'pending') { 341 | // if there is already a table inside, with cell_id that is different from before... 342 | has_status_table = document.querySelector(`[id^="task_${elem_id}"]`); 343 | if (has_status_table) { 344 | cell_id = has_status_table.id.split('_').slice(-1)[0]; 345 | cell = panel.content.widgets.find(x => x.model.id == cell_id); 346 | } 347 | } 348 | if (info.update_only && !has_status_table) { 349 | console.log( 350 | `Cannot find cell by cell ID ${info.cell_id} or task ID ${info.task_id} to update` 351 | ); 352 | return; 353 | } 354 | } else { 355 | has_status_table = document.querySelector(`[id^="task_${elem_id}"]`); 356 | let elem = has_status_table.closest('.jp-CodeCell'); 357 | cell = panel.content.widgets.find(x => x.node == elem); 358 | cell_id = cell.model.id; 359 | } 360 | 361 | if (!cell) { 362 | console.log(`Cannot find cell by ID ${info.cell_id}`); 363 | return; 364 | } 365 | 366 | if (info.status == 'purged') { 367 | if (has_status_table) { 368 | let data = { 369 | output_type: 'update_display_data', 370 | transient: { display_id: `task_${elem_id}` }, 371 | metadata: {}, 372 | data: { 373 | 'text/html': '' 374 | } 375 | }; 376 | add_data_to_cell(cell, data, `task_${elem_id}`); 377 | } 378 | return; 379 | } 380 | // if there is an existing status table, try to retrieve its information 381 | // the new data does not have it 382 | let timer_text = ''; 383 | if (has_status_table) { 384 | // if we already have timer, let us try to "fix" it in the notebook 385 | let timer = document.getElementById( 386 | `status_duration_${elem_id}_${cell_id}` 387 | ); 388 | if (!timer) { 389 | // we could be opening an previous document with different cell_id 390 | timer = document.querySelector(`[id^="status_duration_${elem_id}"]`); 391 | } 392 | if (timer) { 393 | timer_text = timer.innerText; 394 | if ( 395 | timer_text === '' && 396 | (info.status === 'completed' || 397 | info.status === 'failed' || 398 | info.status === 'aborted') 399 | ) { 400 | timer_text = 'Ran for < 5 seconds'; 401 | } 402 | if (!info.start_time) { 403 | info.start_time = timer.getAttribute('datetime'); 404 | } 405 | if (!info.tags) { 406 | let tags = document.getElementById(`status_tags_${elem_id}_${cell_id}`); 407 | if (!tags) { 408 | tags = document.querySelector(`[id^="status_tags_${elem_id}"]`); 409 | } 410 | if (tags) { 411 | info.tags = tags.innerText; 412 | } 413 | } 414 | } 415 | } 416 | 417 | let status_class = { 418 | pending: 'fa-square-o', 419 | submitted: 'fa-spinner', 420 | running: 'fa-spinner fa-pulse fa-spin', 421 | completed: 'fa-check-square-o', 422 | failed: 'fa-times-circle-o', 423 | aborted: 'fa-frown-o', 424 | missing: 'fa-question' 425 | }; 426 | 427 | // look for status etc and update them. 428 | let id_elems = 429 | `
${info.task_id}` +
 430 |     `
` + 431 | `` + 432 | `` + 433 | `` + 434 | `` + 435 | `
`; 436 | 437 | let tags = info.tags.split(/\s+/g); 438 | let tags_elems = ''; 439 | for (let ti = 0; ti < tags.length; ++ti) { 440 | let tag = tags[ti]; 441 | if (!tag) { 442 | continue; 443 | } 444 | tags_elems += 445 | `
${tag}` +
 446 |       `
` + 447 | `` + 448 | `` + 449 | `` + 450 | `
`; 451 | } 452 | 453 | let data = { 454 | output_type: has_status_table ? 'update_display_data' : 'display_data', 455 | transient: { display_id: `task_${elem_id}` }, 456 | metadata: {}, 457 | data: { 458 | 'text/html': ` 459 | 460 | 461 | 466 | 469 | 472 | 476 | 480 | 481 |
462 | 465 | 467 |
${id_elems}
468 |
470 |
${tags_elems}
471 |
473 |
475 |
477 |
${info.status
 478 |         }
479 |
482 | ` 483 | } 484 | }; 485 | add_data_to_cell(cell, data, `task_${elem_id}`); 486 | } 487 | 488 | /* 489 | * SoS frontend Comm 490 | */ 491 | function on_frontend_msg(msg: KernelMessage.ICommMsgMsg) { 492 | let data: any = msg.content.data; 493 | let panel = Manager.manager.notebook_of_comm(msg.content.comm_id); 494 | let msg_type = msg.metadata.msg_type; 495 | let info = Manager.manager.get_info(panel); 496 | console.log(`Received ${msg_type}`); 497 | 498 | if (msg_type === 'kernel-list') { 499 | info.updateLanguages(data); 500 | let unknownTasks = updateCellStyles(panel, info); 501 | if (unknownTasks) { 502 | info.sos_comm.send({ 503 | 'update-task-status': unknownTasks 504 | }); 505 | } 506 | console.log(`kernel list updated ${data}`); 507 | } else if (msg_type === 'cell-kernel') { 508 | // jupyter lab does not yet handle panel cell 509 | if (data[0] === '') { 510 | return; 511 | } 512 | let cell = panel.content.widgets.find(x => x.model.id == data[0]); 513 | if (!cell) { 514 | return; 515 | } 516 | if (cell.model.getMetadata('kernel') !== info.DisplayName.get(data[1])) { 517 | changeCellKernel(cell, info.DisplayName.get(data[1]), info); 518 | saveKernelInfo(); 519 | } else if ( 520 | cell.model.getMetadata('tags') && 521 | (cell.model.getMetadata('tags') as Array).indexOf('report_output') >= 522 | 0 523 | ) { 524 | // #639 525 | // if kernel is different, changeStyleOnKernel would set report_output. 526 | // otherwise we mark report_output 527 | let op = cell.node.getElementsByClassName( 528 | 'jp-Cell-outputWrapper' 529 | ) as HTMLCollectionOf; 530 | for (let i = 0; i < op.length; ++i) { 531 | op.item(i).classList.add('report-output'); 532 | } 533 | } 534 | /* } else if (msg_type === "preview-input") { 535 | cell = window.my_panel.cell; 536 | cell.clear_input(); 537 | cell.set_text(data); 538 | cell.clear_output(); 539 | } else if (msg_type === "preview-kernel") { 540 | changeStyleOnKernel(window.my_panel.cell, data); 541 | */ 542 | } else if (msg_type === 'highlight-workflow') { 543 | let elem = document.getElementById(data[1]) as HTMLTextAreaElement; 544 | // CodeMirror.fromTextArea(elem, { 545 | // mode: "sos" 546 | // }); 547 | // if in a regular notebook, we use static version of the HTML 548 | // to replace the codemirror js version. 549 | if (data[0]) { 550 | let cell = panel.content.widgets.find(x => x.model.id == data[0]); 551 | 552 | let cm_node = elem.parentElement.lastElementChild; 553 | add_data_to_cell( 554 | cell, 555 | { 556 | output_type: 'update_display_data', 557 | transient: { display_id: data[1] }, 558 | metadata: {}, 559 | data: { 560 | 'text/html': cm_node.outerHTML 561 | } 562 | }, 563 | data[1] 564 | ); 565 | cm_node.remove(); 566 | } 567 | } else if (msg_type === 'tasks-pending') { 568 | let cell = panel.content.widgets[data[0]]; 569 | info.pendingCells.set(cell.model.id, data[1]); 570 | } else if (msg_type === 'remove-task') { 571 | let item = document.querySelector(`[id^="table_${data[0]}_${data[1]}"]`); 572 | if (item) { 573 | item.parentNode.removeChild(item); 574 | } 575 | } else if (msg_type === 'task_status') { 576 | update_task_status(data, panel); 577 | if (data.status === 'running') { 578 | update_duration(); 579 | } 580 | } else if (msg_type == 'workflow_status') { 581 | update_workflow_status(data, panel); 582 | if (data.status === 'running') { 583 | update_duration(); 584 | } 585 | // if this is a terminal status, try to execute the 586 | // next pending workflow 587 | if ( 588 | data.status === 'completed' || 589 | data.status === 'canceled' || 590 | data.status === 'failed' 591 | ) { 592 | // find all cell_ids with pending workflows 593 | let elems = document.querySelectorAll("[id^='status_duration_']"); 594 | let pending = Array.from(elems) 595 | .filter(item => { 596 | return ( 597 | item.className == 'pending' && !item.id.substring(16).includes('_') 598 | ); 599 | }) 600 | .map(item => { 601 | return item.id.substring(16); 602 | }); 603 | if (pending) { 604 | (window).execute_workflow(pending); 605 | } 606 | } 607 | } else if (msg_type === 'paste-table') { 608 | //let idx = panel.content.activeCellIndex; 609 | //let cm = panel.content.widgets[idx].editor; 610 | // cm.replaceRange(data, cm.getCursor()); 611 | } else if (msg_type == 'print') { 612 | let cell = panel.content.widgets.find(x => x.model.id == data[0]); 613 | 614 | (cell as CodeCell).outputArea.model.add({ 615 | output_type: 'stream', 616 | name: 'stdout', 617 | text: data[1] 618 | }); 619 | } else if (msg_type === 'alert') { 620 | alert(data); 621 | } else if (msg_type === 'notebook-version') { 622 | // right now no upgrade, just save version to notebook 623 | (panel.content.model.metadata['sos'] as any)['version'] = data; 624 | } 625 | } 626 | 627 | function connectSoSComm(panel: NotebookPanel, renew: boolean = false) { 628 | let info = Manager.manager.get_info(panel); 629 | if (info.sos_comm && !renew) return; 630 | if (!panel.context.sessionContext.session) return; 631 | try { 632 | let sos_comm = 633 | panel.context.sessionContext.session?.kernel.createComm('sos_comm'); 634 | if (!sos_comm) { 635 | console.log(`Failed to connect to sos_comm. Will try later.`); 636 | return null; 637 | } 638 | Manager.manager.register_comm(sos_comm, panel); 639 | sos_comm.open('initial'); 640 | sos_comm.onMsg = on_frontend_msg; 641 | 642 | if (panel.content.model.getMetadata('sos')) { 643 | sos_comm.send({ 644 | 'notebook-version': (panel.content.model.getMetadata('sos') as any)[ 645 | 'version' 646 | ], 647 | 'list-kernel': (panel.content.model.getMetadata('sos') as any)['kernels'] 648 | }); 649 | } else { 650 | sos_comm.send({ 651 | 'notebook-version': '', 652 | 'list-kernel': [] 653 | }); 654 | } 655 | 656 | console.log('sos comm registered'); 657 | } catch (err) { 658 | // if the kernel is for the notebook console, an exception 659 | // 'Comms are disabled on this kernel connection' will be thrown 660 | console.log(err); 661 | return; 662 | } 663 | } 664 | 665 | 666 | (window).task_action = async function (param) { 667 | if (!param.action) { 668 | return; 669 | } 670 | 671 | let commands = Manager.commands; 672 | let path = Manager.currentNotebook.context.path; 673 | 674 | let code = 675 | `%task ${param.action}` + 676 | (param.task ? ` ${param.task}` : '') + 677 | (param.tag ? ` -t ${param.tag}` : '') + 678 | (param.queue ? ` -q ${param.queue}` : ''); 679 | 680 | await commands.execute('console:open', { 681 | activate: false, 682 | insertMode: 'split-bottom', 683 | path 684 | }); 685 | await commands.execute('console:inject', { 686 | activate: false, 687 | code, 688 | path 689 | }); 690 | }; 691 | 692 | (window).cancel_workflow = function (cell_id) { 693 | console.log('Cancel workflow ' + cell_id); 694 | let info = Manager.manager.get_info(Manager.currentNotebook); 695 | info.sos_comm.send({ 696 | 'cancel-workflow': [cell_id] 697 | }); 698 | }; 699 | 700 | (window).execute_workflow = function (cell_ids) { 701 | console.log('Run workflows ' + cell_ids); 702 | let info = Manager.manager.get_info(Manager.currentNotebook); 703 | info.sos_comm.send({ 704 | 'execute-workflow': cell_ids 705 | }); 706 | }; 707 | 708 | export class SoSWidgets 709 | implements DocumentRegistry.IWidgetExtension { 710 | /** 711 | * The createNew function does not return whatever created. It is just a registery that Will 712 | * be called when a notebook is created/opened, and the toolbar is created. it is therefore 713 | * a perfect time to insert SoS language selector and create comms during this time. 714 | */ 715 | createNew( 716 | panel: NotebookPanel, 717 | context: DocumentRegistry.IContext 718 | ): IDisposable { 719 | // register notebook to get language info, or get existing info 720 | // unfortunately, for new notebook, language info is currently empty 721 | let info = Manager.manager.get_info(panel); 722 | 723 | // this is a singleton class 724 | context.sessionContext.ready.then(() => { 725 | void context.sessionContext.session?.kernel?.info.then(kernel_info => { 726 | const lang = kernel_info.language_info; 727 | const kernel_name = context.sessionContext.session.kernel.name; 728 | if (lang.name === 'sos') { 729 | console.log(`session ready with kernel sos`); 730 | info.LanguageName.set(kernel_name, 'sos'); 731 | // if this is not a sos kernel, remove all buttons 732 | if (panel.content.model.getMetadata('sos')) { 733 | info.updateLanguages( 734 | (panel.content.model.getMetadata('sos') as any)['kernels'] 735 | ); 736 | } else { 737 | panel.content.model.setMetadata('sos', { 738 | kernels: [['SoS', kernel_name, 'sos', '', '']], 739 | version: '' 740 | }); 741 | } 742 | if (!info.sos_comm) { 743 | connectSoSComm(panel); 744 | wrapExecutor(panel); 745 | } 746 | updateCellStyles(panel, info); 747 | markSoSNotebookPanel(panel.node, true); 748 | } else { 749 | markSoSNotebookPanel(panel.node, false); 750 | } 751 | }) 752 | }); 753 | 754 | context.sessionContext.kernelChanged.connect(() => { 755 | void context.sessionContext.session?.kernel?.info.then(kernel_info => { 756 | const lang = kernel_info.language_info; 757 | const kernel_name = context.sessionContext.session.kernel.name; 758 | info.LanguageName.set(kernel_name, 'sos'); 759 | 760 | console.log(`kernel changed to ${lang.name}`); 761 | if (lang.name === 'sos') { 762 | if (panel.content.model.getMetadata('sos')) { 763 | info.updateLanguages( 764 | (panel.content.model.getMetadata('sos') as any)['kernels'] 765 | ); 766 | } else { 767 | panel.content.model.setMetadata('sos', { 768 | kernels: [['SoS', kernel_name, 'sos', '', '']], 769 | version: '' 770 | }); 771 | } 772 | if (!info.sos_comm) { 773 | connectSoSComm(panel); 774 | wrapExecutor(panel); 775 | } 776 | updateCellStyles(panel, info); 777 | markSoSNotebookPanel(panel.node, true); 778 | } else { 779 | markSoSNotebookPanel(panel.node,false); 780 | } 781 | }); 782 | }); 783 | 784 | context.sessionContext.statusChanged.connect((sender, status) => { 785 | // if a sos notebook is restarted 786 | if ( 787 | (status === 'busy' || status === 'starting') && 788 | panel.context.sessionContext.kernelDisplayName === 'SoS' 789 | ) { 790 | connectSoSComm(panel); 791 | wrapExecutor(panel); 792 | } 793 | }); 794 | 795 | panel.content.model.cells.changed.connect((list, changed) => { 796 | let cur_kernel = panel.context.sessionContext.kernelPreference.name; 797 | if (!cur_kernel) { 798 | return; 799 | } 800 | if (cur_kernel.toLowerCase() === 'sos') { 801 | each(changed.newValues, cellmodel => { 802 | let idx = changed.newIndex; // panel.content.widgets.findIndex(x => x.model.id == cellmodel.id); 803 | let cell = panel.content.widgets[idx]; 804 | 805 | if (changed.type !== 'add' && changed.type !== 'set') { 806 | return; 807 | } 808 | let kernel = 'SoS'; 809 | if (cell.model.getMetadata('kernel')) { 810 | kernel = cell.model.getMetadata('kernel') as string; 811 | } else { 812 | // find the kernel of a cell before this one to determine the default 813 | // kernel of a new cell #18 814 | if (idx > 0) { 815 | for (idx = idx - 1; idx >= 0; --idx) { 816 | if (panel.content.widgets[idx].model.type === 'code') { 817 | kernel = panel.content.widgets[idx].model.getMetadata( 818 | 'kernel' 819 | ) as string; 820 | break; 821 | } 822 | } 823 | } 824 | cell.model.setMetadata('kernel', kernel); 825 | } 826 | addLanSelector(cell, info); 827 | changeStyleOnKernel(cell, kernel, info); 828 | }); 829 | } 830 | }); 831 | 832 | panel.content.activeCellChanged.connect((sender: any, cell: Cell) => { 833 | // this event is triggered both when a cell gets focus, and 834 | // also when a new notebook is created etc when cell does not exist 835 | if (cell && cell.model.type === 'code' && info.sos_comm) { 836 | if (info.sos_comm.isDisposed) { 837 | // this happens after kernel restart #53 838 | connectSoSComm(panel, true); 839 | } 840 | let cell_kernel = cell.model.getMetadata('kernel') as string; 841 | info.sos_comm.send({ 842 | 'set-editor-kernel': cell_kernel 843 | }); 844 | } 845 | }); 846 | 847 | return new DisposableDelegate(() => { }); 848 | } 849 | } 850 | 851 | function registerSoSWidgets(app: JupyterFrontEnd) { 852 | app.docRegistry.addWidgetExtension('Notebook', new SoSWidgets()); 853 | } 854 | 855 | (window).filterDataFrame = function (id) { 856 | var input = document.getElementById('search_' + id) as HTMLInputElement; 857 | var filter = input.value.toUpperCase(); 858 | var table = document.getElementById('dataframe_' + id) as HTMLTableElement; 859 | var tr = table.getElementsByTagName('tr'); 860 | 861 | // Loop through all table rows, and hide those who do not match the search query 862 | for (var i = 1; i < tr.length; i++) { 863 | for (var j = 0; j < tr[i].cells.length; ++j) { 864 | var matched = false; 865 | if (tr[i].cells[j].innerHTML.toUpperCase().indexOf(filter) !== -1) { 866 | tr[i].style.display = ''; 867 | matched = true; 868 | break; 869 | } 870 | if (!matched) { 871 | tr[i].style.display = 'none'; 872 | } 873 | } 874 | } 875 | }; 876 | 877 | (window).sortDataFrame = function (id, n, dtype) { 878 | var table = document.getElementById('dataframe_' + id) as HTMLTableElement; 879 | 880 | var tb = table.tBodies[0]; // use `` to ignore `` and `` rows 881 | var tr = Array.prototype.slice.call(tb.rows, 0); // put rows into array 882 | 883 | var fn = 884 | dtype === 'numeric' 885 | ? function (a, b) { 886 | return parseFloat(a.cells[n].textContent) <= 887 | parseFloat(b.cells[n].textContent) 888 | ? -1 889 | : 1; 890 | } 891 | : function (a, b) { 892 | var c = a.cells[n].textContent 893 | .trim() 894 | .localeCompare(b.cells[n].textContent.trim()); 895 | return c > 0 ? 1 : c < 0 ? -1 : 0; 896 | }; 897 | var isSorted = function (array, fn) { 898 | if (array.length < 2) { 899 | return 1; 900 | } 901 | var direction = fn(array[0], array[1]); 902 | for (var i = 1; i < array.length - 1; ++i) { 903 | var d = fn(array[i], array[i + 1]); 904 | if (d === 0) { 905 | continue; 906 | } else if (direction === 0) { 907 | direction = d; 908 | } else if (direction !== d) { 909 | return 0; 910 | } 911 | } 912 | return direction; 913 | }; 914 | 915 | var sorted = isSorted(tr, fn); 916 | var i; 917 | 918 | if (sorted === 1 || sorted === -1) { 919 | // if sorted already, reverse it 920 | for (i = tr.length - 1; i >= 0; --i) { 921 | tb.appendChild(tr[i]); // append each row in order 922 | } 923 | } else { 924 | tr = tr.sort(fn); 925 | for (i = 0; i < tr.length; ++i) { 926 | tb.appendChild(tr[i]); // append each row in order 927 | } 928 | } 929 | }; 930 | 931 | /** 932 | * Initialization data for the sos-extension extension. 933 | */ 934 | const PLUGIN_ID = 'jupyterlab-sos:plugin'; 935 | const extension: JupyterFrontEndPlugin = { 936 | id: 'vatlab/jupyterlab-extension:sos', 937 | autoStart: true, 938 | requires: [ 939 | INotebookTracker, 940 | IConsoleTracker, 941 | ICommandPalette, 942 | IEditorLanguageRegistry, 943 | IToolbarWidgetRegistry, 944 | ISettingRegistry 945 | ], 946 | activate: async ( 947 | app: JupyterFrontEnd, 948 | notebook_tracker: INotebookTracker, 949 | console_tracker: IConsoleTracker, 950 | palette: ICommandPalette, 951 | editor_language_registry: IEditorLanguageRegistry, 952 | toolbarRegistry: IToolbarWidgetRegistry, 953 | settingRegistry: ISettingRegistry | null 954 | ) => { 955 | registerSoSFileType(app); 956 | registerSoSWidgets(app); 957 | Manager.set_trackers(notebook_tracker, console_tracker); 958 | Manager.set_commands(app.commands); 959 | 960 | // Toolbar 961 | // - Define a custom toolbar item 962 | toolbarRegistry.addFactory( 963 | 'Cell', 964 | 'kernel_selector', 965 | (cell: Cell) => new KernelSwitcher() 966 | ); 967 | 968 | let settings = null; 969 | if (settingRegistry) { 970 | settings = await settingRegistry.load(PLUGIN_ID); 971 | Manager.manager.update_config(settings); 972 | } 973 | 974 | console_tracker.widgetAdded.connect((sender, panel) => { 975 | const labconsole = panel.console; 976 | 977 | labconsole.promptCellCreated.connect(panel => { 978 | if (Manager.currentNotebook) { 979 | let info = Manager.manager.get_info(Manager.currentNotebook); 980 | addLanSelector(panel.promptCell, info); 981 | } 982 | }); 983 | labconsole.sessionContext.statusChanged.connect( 984 | (sender, status: Kernel.Status) => { 985 | if ( 986 | status == 'busy' && 987 | panel.console.sessionContext?.kernelDisplayName === 'SoS' 988 | ) { 989 | console.log(`connected to sos kernel`); 990 | // connectSoSComm(panel, true); 991 | wrapConsoleExecutor(panel); 992 | } 993 | } 994 | ); 995 | }); 996 | 997 | // defineSoSCodeMirrorMode(editor_language_handler); 998 | editor_language_registry.addLanguage({ 999 | name: 'sos', 1000 | mime: 'text/x-sos', 1001 | load: async () => { 1002 | const m = await import('@codemirror/lang-python'); 1003 | return m.python(); 1004 | } 1005 | }); 1006 | 1007 | // add an command to toggle output 1008 | const command_toggle_output: string = 'sos:toggle_output'; 1009 | app.commands.addCommand(command_toggle_output, { 1010 | label: 'Toggle cell output tags', 1011 | execute: () => { 1012 | // get current notebook and toggle current cell 1013 | toggleDisplayOutput(notebook_tracker.activeCell); 1014 | } 1015 | }); 1016 | 1017 | // add an command to toggle output 1018 | const command_toggle_kernel: string = 'sos:toggle_kernel'; 1019 | app.commands.addCommand(command_toggle_kernel, { 1020 | label: 'Toggle cell kernel', 1021 | execute: () => { 1022 | // get current notebook and toggle current cell 1023 | toggleCellKernel( 1024 | notebook_tracker.activeCell, 1025 | notebook_tracker.currentWidget 1026 | ); 1027 | } 1028 | }); 1029 | 1030 | // add an command to toggle output 1031 | const command_toggle_markdown: string = 'sos:toggle_markdown'; 1032 | app.commands.addCommand(command_toggle_markdown, { 1033 | label: 'Toggle cell kernel', 1034 | execute: () => { 1035 | // get current notebook and toggle current cell 1036 | toggleMarkdownCell( 1037 | notebook_tracker.activeCell, 1038 | notebook_tracker.currentWidget 1039 | ); 1040 | } 1041 | }); 1042 | // app.commands.addKeyBinding({ 1043 | // keys: ["Ctrl Shift O"], 1044 | // selector: ".jp-Notebook.jp-mod-editMode", 1045 | // command: "sos:toggle_output" 1046 | // }); 1047 | // app.commands.addKeyBinding({ 1048 | // keys: ["Ctrl Shift Enter"], 1049 | // selector: ".jp-Notebook.jp-mod-editMode", 1050 | // command: "notebook:run-in-console" 1051 | // }); 1052 | 1053 | // Add the command to the palette. 1054 | palette.addItem({ 1055 | command: command_toggle_output, 1056 | category: 'Cell output' 1057 | }); 1058 | palette.addItem({ 1059 | command: command_toggle_kernel, 1060 | category: 'Toggle kernel' 1061 | }); 1062 | palette.addItem({ 1063 | command: command_toggle_markdown, 1064 | category: 'Toggle markdown' 1065 | }); 1066 | 1067 | console.log('JupyterLab extension sos-extension is activated!'); 1068 | } 1069 | }; 1070 | 1071 | export default extension; 1072 | -------------------------------------------------------------------------------- /src/manager.ts: -------------------------------------------------------------------------------- 1 | import { NotebookPanel, INotebookTracker } from "@jupyterlab/notebook"; 2 | 3 | import { ConsolePanel, IConsoleTracker } from "@jupyterlab/console"; 4 | 5 | import { CommandRegistry } from "@lumino/commands"; 6 | 7 | import { ISettingRegistry } from '@jupyterlab/settingregistry'; 8 | 9 | import { Kernel } from "@jupyterlab/services"; 10 | // 11 | export class NotebookInfo { 12 | notebook: NotebookPanel; 13 | KernelList: Array; 14 | 15 | sos_comm: Kernel.IComm; 16 | 17 | BackgroundColor: Map; 18 | DisplayName: Map; 19 | KernelName: Map; 20 | LanguageName: Map; 21 | CodeMirrorMode: Map; 22 | KernelOptions: Map; 23 | 24 | autoResume: boolean; 25 | pendingCells: Map; 26 | /** create an info object from metadata of the notebook 27 | */ 28 | constructor(notebook: NotebookPanel) { 29 | this.notebook = notebook; 30 | this.KernelList = new Array(); 31 | this.autoResume = false; 32 | this.sos_comm = null; 33 | 34 | this.BackgroundColor = new Map(); 35 | this.DisplayName = new Map(); 36 | this.KernelName = new Map(); 37 | this.LanguageName = new Map(); 38 | this.KernelOptions = new Map(); 39 | this.CodeMirrorMode = new Map(); 40 | 41 | this.pendingCells = new Map(); 42 | 43 | let data = [["SoS", "sos", "", ""]]; 44 | if (notebook.model.getMetadata("sos")) 45 | data = (notebook.model.getMetadata('sos') as any)["kernels"]; 46 | // fill the look up tables with language list passed from the kernel 47 | for (let i = 0; i < data.length; i++) { 48 | // BackgroundColor is color 49 | this.BackgroundColor.set(data[i][0], data[i][3]); 50 | this.BackgroundColor.set(data[i][1], data[i][3]); 51 | // DisplayName 52 | this.DisplayName.set(data[i][0], data[i][0]); 53 | this.DisplayName.set(data[i][1], data[i][0]); 54 | // Name 55 | this.KernelName.set(data[i][0], data[i][1]); 56 | this.KernelName.set(data[i][1], data[i][1]); 57 | // LanguageName 58 | this.LanguageName.set(data[i][0], data[i][2]); 59 | this.LanguageName.set(data[i][1], data[i][2]); 60 | 61 | // if codemirror mode ... 62 | if (data[i].length >= 5 && data[i][4]) { 63 | this.CodeMirrorMode.set(data[i][0], data[i][4]); 64 | } 65 | 66 | this.KernelList.push(data[i][0]); 67 | } 68 | } 69 | 70 | updateLanguages(data: Array>) { 71 | for (let i = 0; i < data.length; i++) { 72 | // BackgroundColor is color 73 | this.BackgroundColor.set(data[i][0], data[i][3]); 74 | // by kernel name? For compatibility ... 75 | if (!(data[i][1] in this.BackgroundColor)) { 76 | this.BackgroundColor.set(data[i][1], data[i][3]); 77 | } 78 | // DisplayName 79 | this.DisplayName.set(data[i][0], data[i][0]); 80 | if (!(data[i][1] in this.DisplayName)) { 81 | this.DisplayName.set(data[i][1], data[i][0]); 82 | } 83 | // Name 84 | this.KernelName.set(data[i][0], data[i][1]); 85 | if (!(data[i][1] in this.KernelName)) { 86 | this.KernelName.set(data[i][1], data[i][1]); 87 | } 88 | // Language Name 89 | this.LanguageName.set(data[i][0], data[i][2]); 90 | if (!(data[i][2] in this.LanguageName)) { 91 | this.LanguageName.set(data[i][2], data[i][2]); 92 | } 93 | // if codemirror mode ... 94 | if (data[i].length > 4 && data[i][4]) { 95 | this.CodeMirrorMode.set(data[i][0], data[i][4]); 96 | } 97 | // if options ... 98 | if (data[i].length > 5) { 99 | this.KernelOptions.set(data[i][0], data[i][5]); 100 | } 101 | 102 | if (this.KernelList.indexOf(data[i][0]) === -1) 103 | this.KernelList.push(data[i][0]); 104 | } 105 | 106 | // add css to window 107 | let css_text = this.KernelList.map( 108 | // add language specific css 109 | (lan: string) => { 110 | if (this.BackgroundColor.get(lan)) { 111 | let css_name = safe_css_name(`sos_lan_${lan}`); 112 | return `.jp-CodeCell.${css_name} .jp-InputPrompt, 113 | .jp-CodeCell.${css_name} .jp-OutputPrompt { 114 | background: ${this.BackgroundColor.get(lan)}; 115 | } 116 | `; 117 | } else { 118 | return null; 119 | } 120 | } 121 | ) 122 | .filter(Boolean) 123 | .join("\n"); 124 | var css = document.createElement("style"); 125 | // css.type = "text/css"; 126 | css.innerHTML = css_text; 127 | document.body.appendChild(css); 128 | } 129 | 130 | public show() { 131 | console.log(this.KernelList); 132 | } 133 | } 134 | 135 | export function safe_css_name(name) { 136 | return name.replace(/[^a-z0-9_]/g, function(s) { 137 | var c = s.charCodeAt(0); 138 | if (c == 32) return "-"; 139 | if (c >= 65 && c <= 90) return "_" + s.toLowerCase(); 140 | return "__" + ("000" + c.toString(16)).slice(-4); 141 | }); 142 | } 143 | 144 | export class Manager { 145 | // global registry for notebook info 146 | private static _instance: Manager; 147 | // used to track the current notebook widget 148 | private static _notebook_tracker: INotebookTracker; 149 | private static _console_tracker: IConsoleTracker; 150 | private static _commands: CommandRegistry; 151 | private _info: Map; 152 | private _settings: ISettingRegistry.ISettings; 153 | 154 | private constructor() { 155 | if (!this._info) { 156 | this._info = new Map(); 157 | } 158 | } 159 | 160 | public static set_trackers( 161 | notebook_tracker: INotebookTracker, 162 | console_tracker: IConsoleTracker 163 | ) { 164 | this._notebook_tracker = notebook_tracker; 165 | this._console_tracker = console_tracker; 166 | } 167 | 168 | public static set_commands(commands: CommandRegistry) { 169 | this._commands = commands; 170 | } 171 | 172 | static get currentNotebook() { 173 | return this._notebook_tracker.currentWidget; 174 | } 175 | 176 | public static consolesOfNotebook(panel: NotebookPanel): Array { 177 | return this._console_tracker.filter(value => { 178 | return value.console.sessionContext.path === panel.context.path; 179 | }); 180 | } 181 | 182 | static get currentConsole(): ConsolePanel { 183 | return this._console_tracker.currentWidget; 184 | } 185 | 186 | static get commands() { 187 | return this._commands; 188 | } 189 | 190 | static get manager() { 191 | if (this._instance === null || this._instance === undefined) 192 | this._instance = new Manager(); 193 | return this._instance; 194 | } 195 | 196 | // register notebook info to the global registry 197 | public get_info(notebook: NotebookPanel): NotebookInfo { 198 | if (!this._info.has(notebook)) { 199 | console.log("Creating a new notebook info"); 200 | this._info.set(notebook, new NotebookInfo(notebook)); 201 | } 202 | return this._info.get(notebook); 203 | } 204 | 205 | public register_comm(comm: Kernel.IComm, notebook: NotebookPanel) { 206 | this.get_info(notebook).sos_comm = comm; 207 | } 208 | 209 | // this is the same as get_info, 210 | public notebook_of_comm(comm_id: string): NotebookPanel { 211 | for (let [panel, info] of Array.from(this._info.entries())) 212 | if (info.sos_comm && info.sos_comm.commId === comm_id) return panel; 213 | } 214 | 215 | public update_config(settings: ISettingRegistry.ISettings): void { 216 | this._settings = settings; 217 | } 218 | 219 | public get_config(key: string): any { 220 | // sos.kernel_codemirror_mode 221 | return this._settings.get(key).composite; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/selectors.tsx: -------------------------------------------------------------------------------- 1 | import { NotebookPanel, NotebookActions } from '@jupyterlab/notebook'; 2 | 3 | import { Cell } from '@jupyterlab/cells'; 4 | 5 | // import { CodeMirrorEditor } from '@jupyterlab/codemirror'; 6 | 7 | import { NotebookInfo } from './manager'; 8 | 9 | import { Manager, safe_css_name } from './manager'; 10 | 11 | import { HTMLSelect } from '@jupyterlab/ui-components'; 12 | 13 | import { ReactWidget } from '@jupyterlab/apputils'; 14 | 15 | import React from 'react'; 16 | 17 | const CELL_LANGUAGE_DROPDOWN_CLASS = 'jp-CelllanguageDropDown'; 18 | const SOS_NOTEBOOK_CLASS = 'jp-SoSNotebook'; 19 | 20 | export function markSoSNotebookPanel(panel: HTMLElement, is_sos: boolean): void { 21 | if (is_sos) { 22 | panel.classList.add(SOS_NOTEBOOK_CLASS); 23 | } else { 24 | panel.classList.remove(SOS_NOTEBOOK_CLASS); 25 | } 26 | } 27 | 28 | 29 | export function saveKernelInfo() { 30 | let panel = Manager.currentNotebook; 31 | let info = Manager.manager.get_info(panel); 32 | 33 | let used_kernels = new Set(); 34 | let cells = panel.content.model.cells; 35 | for (var i = cells.length - 1; i >= 0; --i) { 36 | let cell = cells.get(i); 37 | if (cell.type === 'code' && cell.getMetadata('kernel')) { 38 | used_kernels.add(cell.getMetadata('kernel') as string); 39 | } 40 | } 41 | let sos_info = panel.content.model.getMetadata('sos'); 42 | sos_info['kernels'] = Array.from(used_kernels.values()) 43 | .sort() 44 | .map(function (x) { 45 | return [ 46 | info.DisplayName.get(x as string), 47 | info.KernelName.get(x as string), 48 | info.LanguageName.get(x as string) || '', 49 | info.BackgroundColor.get(x as string) || '', 50 | info.CodeMirrorMode.get(x as string) || '' 51 | ]; 52 | }); 53 | panel.content.model.setMetadata('sos', sos_info); 54 | } 55 | 56 | export function hideLanSelector(cell) { 57 | let nodes = cell.node.getElementsByClassName( 58 | CELL_LANGUAGE_DROPDOWN_CLASS 59 | ) as HTMLCollectionOf; 60 | if (nodes.length > 0) { 61 | nodes[0].style.display = 'none'; 62 | } 63 | } 64 | 65 | export function toggleDisplayOutput(cell) { 66 | if (cell.model.type === 'markdown') { 67 | // switch between hide_output and "" 68 | if ( 69 | cell.model.metadata['tags'] && 70 | (cell.model.metadata['tags'] as Array).indexOf('hide_output') >= 0 71 | ) { 72 | // if report_output on, remove it 73 | remove_tag(cell, 'hide_output'); 74 | } else { 75 | add_tag(cell, 'hide_output'); 76 | } 77 | } else if (cell.model.type === 'code') { 78 | // switch between report_output and "" 79 | if ( 80 | cell.model.metadata['tags'] && 81 | (cell.model.metadata['tags'] as Array).indexOf('report_output') >= 82 | 0 83 | ) { 84 | // if report_output on, remove it 85 | remove_tag(cell, 'report_output'); 86 | } else { 87 | add_tag(cell, 'report_output'); 88 | } 89 | } 90 | } 91 | 92 | export function toggleCellKernel(cell: Cell, panel: NotebookPanel) { 93 | if (cell.model.type === 'markdown') { 94 | // markdown, to code 95 | // NotebookActions.changeCellType(panel.content, 'code'); 96 | return; 97 | } else if (cell.model.type === 'code') { 98 | // switch to the next used kernel 99 | let kernels = (panel.content.model.metadata['sos'] as any)['kernels']; 100 | // current kernel 101 | let kernel = cell.model.getMetadata('kernel'); 102 | 103 | if (kernels.length == 1) { 104 | return; 105 | } 106 | // index of kernel 107 | for (let i = 0; i < kernels.length; ++i) { 108 | if (kernels[i][0] === kernel) { 109 | let info: NotebookInfo = Manager.manager.get_info(panel); 110 | let next = (i + 1) % kernels.length; 111 | // notebook_1.NotebookActions.changeCellType(panel.content, 'markdown'); 112 | changeCellKernel(cell, kernels[next][0], info); 113 | break; 114 | } 115 | } 116 | } 117 | } 118 | 119 | export function toggleMarkdownCell(cell: Cell, panel: NotebookPanel) { 120 | if (cell.model.type === 'markdown') { 121 | // markdown, to code 122 | NotebookActions.changeCellType(panel.content, 'code'); 123 | } else { 124 | NotebookActions.changeCellType(panel.content, 'markdown'); 125 | } 126 | } 127 | 128 | function remove_tag(cell, tag) { 129 | let taglist = cell.model.metadata['tags'] as string[]; 130 | let new_list: string[] = []; 131 | for (let i = 0; i < taglist.length; i++) { 132 | if (taglist[i] != tag) { 133 | new_list.push(taglist[i]); 134 | } 135 | } 136 | cell.model.metadata.set('tags', new_list); 137 | let op = cell.node.getElementsByClassName( 138 | 'jp-Cell-outputWrapper' 139 | ) as HTMLCollectionOf; 140 | for (let i = 0; i < op.length; ++i) { 141 | op.item(i).classList.remove(tag); 142 | } 143 | } 144 | 145 | function add_tag(cell, tag) { 146 | let taglist = cell.model.metadata['tags'] as string[]; 147 | if (taglist) { 148 | taglist.push(tag); 149 | } else { 150 | taglist = [tag]; 151 | } 152 | cell.model.metadata.set('tags', taglist); 153 | let op = cell.node.getElementsByClassName( 154 | 'jp-Cell-outputWrapper' 155 | ) as HTMLCollectionOf; 156 | for (let i = 0; i < op.length; ++i) { 157 | op.item(i).classList.add(tag); 158 | } 159 | } 160 | 161 | export function addLanSelector(cell: Cell, info: NotebookInfo) { 162 | if (!cell.model.getMetadata('kernel')) { 163 | cell.model.setMetadata('kernel', 'SoS'); 164 | } 165 | let kernel = cell.model.getMetadata('kernel') as string; 166 | 167 | let nodes = cell.node.getElementsByClassName( 168 | CELL_LANGUAGE_DROPDOWN_CLASS 169 | ) as HTMLCollectionOf; 170 | if (nodes.length > 0) { 171 | // use the existing dropdown box 172 | let select = nodes 173 | .item(0) 174 | .getElementsByTagName('select')[0] as HTMLSelectElement; 175 | // update existing 176 | for (let lan of info.KernelList) { 177 | // ignore if already exists 178 | if (select.options.namedItem(lan)) continue; 179 | let option = document.createElement('option'); 180 | option.value = lan; 181 | option.id = lan; 182 | option.textContent = lan; 183 | select.appendChild(option); 184 | } 185 | select.value = kernel ? kernel : 'SoS'; 186 | } 187 | } 188 | 189 | export function changeCellKernel( 190 | cell: Cell, 191 | kernel: string, 192 | info: NotebookInfo 193 | ) { 194 | cell.model.setMetadata('kernel', kernel); 195 | let nodes = cell.node.getElementsByClassName( 196 | CELL_LANGUAGE_DROPDOWN_CLASS 197 | ) as HTMLCollectionOf; 198 | // use the existing dropdown box 199 | let select = nodes.item(0) as HTMLSelectElement; 200 | if (select) { 201 | select.value = kernel; 202 | } 203 | changeStyleOnKernel(cell, kernel, info); 204 | } 205 | 206 | export function changeStyleOnKernel( 207 | cell: Cell, 208 | kernel: string, 209 | info: NotebookInfo 210 | ) { 211 | // Note: JupyterLab does not yet support tags 212 | if ( 213 | cell.model.metadata['tags'] && 214 | (cell.model.metadata['tags'] as Array).indexOf('report_output') >= 0 215 | ) { 216 | let op = cell.node.getElementsByClassName( 217 | 'jp-Cell-outputWrapper' 218 | ) as HTMLCollectionOf; 219 | for (let i = 0; i < op.length; ++i) 220 | op.item(i).classList.add('report-output'); 221 | } else { 222 | let op = cell.node.getElementsByClassName( 223 | 'jp-Cell-outputWrapper' 224 | ) as HTMLCollectionOf; 225 | for (let i = 0; i < op.length; ++i) 226 | op.item(i).classList.remove('report-output'); 227 | } 228 | for (let className of Array.from(cell.node.classList)) { 229 | if (className.startsWith('sos_lan_')) { 230 | cell.node.classList.remove(className); 231 | } 232 | } 233 | cell.node.classList.add(safe_css_name(`sos_lan_${kernel}`)); 234 | // cell.user_highlight = { 235 | // name: 'sos', 236 | // base_mode: info.LanguageName[kernel] || info.KernelName[kernel] || kernel, 237 | // }; 238 | // //console.log(`Set cell code mirror mode to ${cell.user_highlight.base_mode}`) 239 | // let base_mode: string = 240 | // info.CodeMirrorMode.get(kernel) || 241 | // info.LanguageName.get(kernel) || 242 | // info.KernelName.get(kernel) || 243 | // kernel; 244 | // if (!base_mode || base_mode === 'sos') { 245 | // (cell.inputArea.editorWidget.editor as CodeMirrorEditor).setOption( 246 | // 'mode', 247 | // 'sos' 248 | // ); 249 | // } else { 250 | // (cell.inputArea.editorWidget.editor as CodeMirrorEditor).setOption('mode', { 251 | // name: 'sos', 252 | // base_mode: base_mode, 253 | // }); 254 | // } 255 | } 256 | 257 | export function updateCellStyles( 258 | panel: NotebookPanel, 259 | info: NotebookInfo 260 | ): Array { 261 | var cells = panel.content.widgets; 262 | 263 | // setting up background color and selection according to notebook metadata 264 | for (let i = 0; i < cells.length; ++i) { 265 | addLanSelector(cells[i], info); 266 | if (cells[i].model.type === 'code') { 267 | changeStyleOnKernel( 268 | cells[i], 269 | cells[i].model.getMetadata('kernel') as string, 270 | info 271 | ); 272 | } 273 | } 274 | 275 | let panels = Manager.consolesOfNotebook(panel); 276 | for (let i = 0; i < panels.length; ++i) { 277 | addLanSelector(panels[i].console.promptCell, info); 278 | changeStyleOnKernel( 279 | panels[i].console.promptCell, 280 | panels[i].console.promptCell.model.getMetadata('kernel') as string, 281 | info 282 | ); 283 | } 284 | let tasks = document.querySelectorAll('[id^="task_status_"]'); 285 | let unknownTasks = []; 286 | for (let i = 0; i < tasks.length; ++i) { 287 | // status_localhost_5ea9232779ca1959 288 | if (tasks[i].id.match('^task_status_icon_.*')) { 289 | tasks[i].className = 'fa fa-fw fa-2x fa-refresh fa-spin'; 290 | unknownTasks.push(tasks[i].id.substring(17)); 291 | } 292 | } 293 | return unknownTasks; 294 | } 295 | 296 | export class KernelSwitcher extends ReactWidget { 297 | constructor() { 298 | super(); 299 | // this.state = { 300 | // is_sos: false; 301 | // } 302 | } 303 | 304 | handleChange = (event: React.ChangeEvent): void => { 305 | let cell = Manager.currentNotebook.content.activeCell; 306 | 307 | let kernel = event.target.value; 308 | cell.model.setMetadata('kernel', kernel); 309 | let panel = Manager.currentNotebook; 310 | let info: NotebookInfo = Manager.manager.get_info(panel); 311 | info.sos_comm.send({ 'set-editor-kernel': kernel }); 312 | // change style 313 | changeStyleOnKernel(cell, kernel, info); 314 | // set global meta data 315 | saveKernelInfo(); 316 | this.update(); 317 | }; 318 | 319 | handleKeyDown = (event: React.KeyboardEvent): void => { }; 320 | 321 | render(): JSX.Element { 322 | let panel = Manager.currentNotebook; 323 | let info = Manager.manager.get_info(panel); 324 | 325 | let cell = panel.content.activeCell; 326 | 327 | const optionChildren = info.KernelList.map(lan => { 328 | return ( 329 | 332 | ); 333 | }); 334 | let kernel = cell.model.getMetadata('kernel') as string; 335 | 336 | return ( 337 | 345 | {optionChildren} 346 | 347 | ); 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /style/base.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vatlab/jupyterlab-sos/90a3592b2f8a19cae240b9212106cf96fb3d920d/style/base.css -------------------------------------------------------------------------------- /style/index.css: -------------------------------------------------------------------------------- 1 | .sos_icon { 2 | background-image: url('./sos_icon.svg'); 3 | background-repeat: no-repeat; 4 | background-position: center center; 5 | } 6 | 7 | .cm-sos-interpolated { 8 | background-color: rgb(223, 144, 207, 0.4); 9 | } 10 | 11 | .cm-sos-sigil { 12 | background-color: rgb(223, 144, 207, 0.4); 13 | } 14 | 15 | .cm-sos-unmatched { 16 | background-color: orange; 17 | } 18 | 19 | .jp-OutputArea-prompt:empty { 20 | padding: 0px; 21 | } 22 | 23 | .jp-NotebookPanel.jp-SoSNotebook .jp-CelllanguageDropDown { 24 | display: block; 25 | } 26 | 27 | .jp-CelllanguageDropDown { 28 | display: none; 29 | } 30 | 31 | .jp-CodeCell .jp-InputArea .jp-CelllanguageDropDown { 32 | width: 70pt; 33 | background: none; 34 | z-index: 1000; 35 | right: 8pt; 36 | font-size: 80%; 37 | } 38 | 39 | .jp-CodeCell .jp-cell-menu .jp-CelllanguageDropDown { 40 | width: 70pt; 41 | background: none; 42 | font-size: 80%; 43 | margin-left: 5px; 44 | margin-right: 5px; 45 | border: 0px; 46 | } 47 | 48 | .sos_logging { 49 | font-family: monospace; 50 | margin: -0.4em; 51 | padding-left: 0.4em; 52 | } 53 | 54 | .sos_hint { 55 | color: rgba(0, 0, 0, .4); 56 | font-family: monospace; 57 | } 58 | 59 | .sos_debug { 60 | color: blue; 61 | } 62 | 63 | .sos_trace { 64 | color: darkcyan; 65 | } 66 | 67 | .sos_hilight { 68 | color: green; 69 | } 70 | 71 | .sos_info { 72 | color: black; 73 | } 74 | 75 | .sos_warning { 76 | color: black; 77 | background: #fdd 78 | } 79 | 80 | .sos_error { 81 | color: black; 82 | background: #fdd 83 | } 84 | 85 | .report_output { 86 | border-right-width: 13px; 87 | border-right-color: #aaaaaa; 88 | border-right-style: solid; 89 | } 90 | 91 | .sos_hint { 92 | color: gray; 93 | font-family: monospace; 94 | } 95 | 96 | 97 | .session_info td { 98 | text-align: left; 99 | } 100 | 101 | .session_info th { 102 | text-align: left; 103 | } 104 | 105 | .session_section { 106 | text-align: left; 107 | font-weight: bold; 108 | font-size: 120%; 109 | } 110 | 111 | 112 | .one_liner { 113 | overflow: hidden; 114 | height: 15px; 115 | } 116 | 117 | .one_liner:hover { 118 | height: auto; 119 | width: auto; 120 | } 121 | 122 | .dataframe_container { 123 | max-height: 400px 124 | } 125 | 126 | .dataframe_input { 127 | border: 1px solid #ddd; 128 | margin-bottom: 5px; 129 | } 130 | 131 | .scatterplot_by_rowname div.xAxis div.tickLabel { 132 | transform: translateY(15px) translateX(15px) rotate(45deg); 133 | -ms-transform: translateY(15px) translateX(15px) rotate(45deg); 134 | -moz-transform: translateY(15px) translateX(15px) rotate(45deg); 135 | -webkit-transform: translateY(15px) translateX(15px) rotate(45deg); 136 | -o-transform: translateY(15px) translateX(15px) rotate(45deg); 137 | /*rotation-point:50% 50%;*/ 138 | /*rotation:270deg;*/ 139 | } 140 | 141 | .sos_dataframe td, 142 | .sos_dataframe th { 143 | white-space: nowrap; 144 | } 145 | 146 | pre.section-header.CodeMirror-line { 147 | border-top: 1px dotted #cfcfcf 148 | } 149 | 150 | 151 | .jp-CodeCell .cm-header-1, 152 | .jp-CodeCell .cm-header-2, 153 | .jp-CodeCell .cm-header-3, 154 | .jp-CodeCell .cm-header-4, 155 | .jp-CodeCell .cm-header-5, 156 | .jp-CodeCell .cm-header-6 { 157 | font-size: 100%; 158 | font-style: normal; 159 | font-weight: normal; 160 | font-family: monospace; 161 | } 162 | 163 | /* jp-NotebooklanguageDropDown */ 164 | /* jp-CelllanguageDropDown */ 165 | 166 | /* sos generated static TOC */ 167 | 168 | .jp-OutputArea .toc { 169 | padding: 0px; 170 | overflow-y: auto; 171 | font-weight: normal; 172 | white-space: nowrap; 173 | overflow-x: auto; 174 | } 175 | 176 | .jp-OutputArea .toc ul.toc-item { 177 | list-style-type: none; 178 | padding-left: 1em; 179 | } 180 | 181 | .jp-OutputArea .toc-item-highlight-select { 182 | background-color: Gold 183 | } 184 | 185 | .jp-OutputArea .toc-item-highlight-execute { 186 | background-color: red 187 | } 188 | 189 | .jp-OutputArea .lev1 { 190 | margin-left: 5px 191 | } 192 | 193 | .jp-OutputArea .lev2 { 194 | margin-left: 10px 195 | } 196 | 197 | .jp-OutputArea .lev3 { 198 | margin-left: 10px 199 | } 200 | 201 | .jp-OutputArea .lev4 { 202 | margin-left: 10px 203 | } 204 | 205 | .jp-OutputArea .lev5 { 206 | margin-left: 10px 207 | } 208 | 209 | .jp-OutputArea .lev6 { 210 | margin-left: 10px 211 | } 212 | 213 | .jp-OutputArea .lev7 { 214 | margin-left: 10px 215 | } 216 | 217 | .jp-OutputArea .lev8 { 218 | margin-left: 10px 219 | } 220 | 221 | 222 | table.workflow_table, 223 | table.task_table { 224 | border: 0px; 225 | } 226 | 227 | 228 | table.workflow_table i, 229 | table.task_table i { 230 | margin-right: 5px; 231 | } 232 | 233 | td.workflow_name { 234 | width: 10em; 235 | text-align: left; 236 | } 237 | 238 | td.workflow_name pre, 239 | td.task_name pre { 240 | font-size: 1.2em; 241 | } 242 | 243 | td.workflow_id, 244 | td.task_id { 245 | width: 15em; 246 | text-align: left; 247 | } 248 | 249 | td.task_tags { 250 | text-align: left; 251 | max-width: 33em; 252 | } 253 | 254 | td.task_id { 255 | text-align: left; 256 | } 257 | 258 | td.task_id span, 259 | td.task_tags span { 260 | display: inline-flex; 261 | } 262 | 263 | td.task_tags span pre { 264 | padding-right: 0.5em; 265 | } 266 | 267 | td.task_tags i { 268 | margin-right: 0px; 269 | } 270 | 271 | .task_id_actions, 272 | .task_tag_actions { 273 | display: none; 274 | } 275 | 276 | .task_id_actions .fa:hover, 277 | .task_tag_actions .fa:hover { 278 | color: blue; 279 | } 280 | 281 | .task_id:hover .task_id_actions, 282 | .task_tags:hover .task_tag_actions { 283 | display: flex; 284 | flex-direction: row; 285 | } 286 | 287 | td.workflow_index { 288 | width: 5em; 289 | text-align: left; 290 | } 291 | 292 | td.workflow_status { 293 | width: 20em; 294 | text-align: left; 295 | } 296 | 297 | td.task_timer { 298 | width: 15em; 299 | text-align: left !important; 300 | } 301 | 302 | td.task_timer pre { 303 | text-overflow: ellipsis; 304 | overflow: hidden; 305 | white-space: nowrap; 306 | } 307 | 308 | .workflow_table pre, 309 | .task_table pre { 310 | background: unset; 311 | } 312 | 313 | td.task_icon { 314 | font-size: 0.75em; 315 | } 316 | 317 | td.task_status { 318 | width: 15em; 319 | text-align: left; 320 | } 321 | 322 | table.workflow_table span { 323 | /* text-transform: uppercase; */ 324 | font-family: monospace; 325 | } 326 | 327 | table.task_table span { 328 | /* text-transform: uppercase; */ 329 | font-family: monospace; 330 | } 331 | 332 | table.workflow_table.pending pre, 333 | table.task_table.pending pre, 334 | table.task_table.submitted pre, 335 | table.task_table.missing pre { 336 | color: #9d9d9d; 337 | /* gray */ 338 | } 339 | 340 | table.workflow_table.running pre, 341 | table.task_table.running pre { 342 | color: #cdb62c; 343 | /* yellow */ 344 | } 345 | 346 | table.workflow_table.completed pre, 347 | table.task_table.completed pre { 348 | color: #39aa56; 349 | /* green */ 350 | } 351 | 352 | table.workflow_table.aborted pre, 353 | table.task_table.aborted pre { 354 | color: #FFA07A; 355 | /* salmon */ 356 | } 357 | 358 | table.workflow_table.failed pre, 359 | table.task_table.failed pre { 360 | color: #db4545; 361 | /* red */ 362 | } 363 | 364 | table.task_table { 365 | border: 0px; 366 | border-style: solid; 367 | } 368 | 369 | 370 | .bs-callout { 371 | padding: 20px; 372 | margin: 20px 0; 373 | border: 1px solid #eee; 374 | border-left-width: 5px; 375 | border-radius: 3px; 376 | } 377 | 378 | .bs-callout h4 { 379 | margin-top: 0 !important; 380 | margin-bottom: 5px; 381 | font-weight: 500; 382 | line-height: 1.1; 383 | display: block; 384 | margin-block-start: 1.33em; 385 | margin-block-end: 1.33em; 386 | margin-inline-start: 0px; 387 | margin-inline-end: 0px; 388 | } 389 | 390 | .bs-callout p:last-child { 391 | margin-bottom: 0; 392 | } 393 | 394 | .bs-callout code { 395 | border-radius: 3px; 396 | } 397 | 398 | .bs-callout+.bs-callout { 399 | margin-top: -5px; 400 | } 401 | 402 | .bs-callout-default { 403 | border-left-color: #777; 404 | } 405 | 406 | .bs-callout-default h4 { 407 | color: #777; 408 | } 409 | 410 | .bs-callout-primary { 411 | border-left-color: #428bca; 412 | } 413 | 414 | .bs-callout-primary h4 { 415 | color: #428bca; 416 | } 417 | 418 | .bs-callout-success { 419 | border-left-color: #5cb85c; 420 | } 421 | 422 | .bs-callout-success h4 { 423 | color: #5cb85c; 424 | } 425 | 426 | .bs-callout-danger { 427 | border-left-color: #d9534f; 428 | } 429 | 430 | .bs-callout-danger h4 { 431 | color: #d9534f; 432 | } 433 | 434 | .bs-callout-warning { 435 | border-left-color: #f0ad4e; 436 | } 437 | 438 | .bs-callout-warning h4 { 439 | color: #f0ad4e; 440 | } 441 | 442 | .bs-callout-info { 443 | border-left-color: #5bc0de; 444 | } 445 | 446 | .bs-callout-info h4 { 447 | color: #5bc0de; 448 | } -------------------------------------------------------------------------------- /style/index.js: -------------------------------------------------------------------------------- 1 | import './base.css'; 2 | -------------------------------------------------------------------------------- /style/sos_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 13 | 14 | 15 | 16 | 18 | 21 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) Bo Peng and the University of Texas MD Anderson Cancer Center 4 | # Distributed under the terms of the 3-clause BSD License. 5 | 6 | import json 7 | import os 8 | import sys 9 | import time 10 | from subprocess import Popen 11 | from urllib.parse import urljoin 12 | 13 | import pytest 14 | import requests 15 | from selenium import webdriver 16 | from selenium.webdriver import Chrome, Firefox, Remote 17 | from test_utils import Notebook 18 | from testpath.tempdir import TemporaryDirectory 19 | from webdriver_manager.chrome import ChromeDriverManager 20 | 21 | pjoin = os.path.join 22 | 23 | 24 | def _wait_for_server(proc, info_file_path): 25 | """Wait 30 seconds for the notebook server to start""" 26 | for i in range(300): 27 | if proc.poll() is not None: 28 | raise RuntimeError("Notebook server failed to start") 29 | if os.path.exists(info_file_path): 30 | try: 31 | with open(info_file_path) as f: 32 | return json.load(f) 33 | except ValueError: 34 | # If the server is halfway through writing the file, we may 35 | # get invalid JSON; it should be ready next iteration. 36 | pass 37 | time.sleep(0.1) 38 | raise RuntimeError("Didn't find %s in 30 seconds", info_file_path) 39 | 40 | 41 | @pytest.fixture(scope='session') 42 | def notebook_server(): 43 | info = {} 44 | temp_dir = TemporaryDirectory() 45 | td = temp_dir.name 46 | # do not use context manager because of https://github.com/vatlab/sos-notebook/issues/214 47 | if True: 48 | nbdir = info['nbdir'] = pjoin(td, 'notebooks') 49 | os.makedirs(pjoin(nbdir, u'sub ∂ir1', u'sub ∂ir 1a')) 50 | os.makedirs(pjoin(nbdir, u'sub ∂ir2', u'sub ∂ir 1b')) 51 | # print(nbdir) 52 | info['extra_env'] = { 53 | 'JUPYTER_CONFIG_DIR': pjoin(td, 'jupyter_config'), 54 | 'JUPYTER_RUNTIME_DIR': pjoin(td, 'jupyter_runtime'), 55 | 'IPYTHONDIR': pjoin(td, 'ipython'), 56 | } 57 | env = os.environ.copy() 58 | env.update(info['extra_env']) 59 | 60 | command = [ 61 | sys.executable, 62 | '-m', 63 | 'jupyterlab', 64 | '--no-browser', 65 | '--notebook-dir', 66 | nbdir, 67 | # run with a base URL that would be escaped, 68 | # to test that we don't double-escape URLs 69 | #'--NotebookApp.base_url=/a@b/', 70 | ] 71 | print("command=", command) 72 | proc = info['popen'] = Popen(command, cwd=nbdir, env=env) 73 | info_file_path = pjoin(td, 'jupyter_runtime', 74 | 'jpserver-%i.json' % proc.pid) 75 | info.update(_wait_for_server(proc, info_file_path)) 76 | 77 | print("Notebook server info:", info) 78 | yield info 79 | 80 | # manually try to clean up, which would fail under windows because 81 | # a permission error caused by iPython history.sqlite. 82 | try: 83 | temp_dir.cleanup() 84 | except: 85 | pass 86 | # Shut the server down 87 | requests.post( 88 | urljoin(info['url'], 'api/shutdown'), 89 | headers={'Authorization': 'token ' + info['token']}) 90 | 91 | 92 | def make_sauce_driver(): 93 | """This function helps travis create a driver on Sauce Labs. 94 | 95 | This function will err if used without specifying the variables expected 96 | in that context. 97 | """ 98 | 99 | username = os.environ["SAUCE_USERNAME"] 100 | access_key = os.environ["SAUCE_ACCESS_KEY"] 101 | capabilities = { 102 | "tunnel-identifier": os.environ["TRAVIS_JOB_NUMBER"], 103 | "build": os.environ["TRAVIS_BUILD_NUMBER"], 104 | "tags": [os.environ['TRAVIS_PYTHON_VERSION'], 'CI'], 105 | "platform": "Windows 10", 106 | "browserName": os.environ['JUPYTER_TEST_BROWSER'], 107 | "version": "latest", 108 | } 109 | if capabilities['browserName'] == 'firefox': 110 | # Attempt to work around issue where browser loses authentication 111 | capabilities['version'] = '57.0' 112 | hub_url = "%s:%s@localhost:4445" % (username, access_key) 113 | print("Connecting remote driver on Sauce Labs") 114 | driver = Remote( 115 | desired_capabilities=capabilities, 116 | command_executor="http://%s/wd/hub" % hub_url) 117 | return driver 118 | 119 | 120 | @pytest.fixture(scope='session') 121 | def selenium_driver(): 122 | 123 | if "JUPYTER_TEST_BROWSER" not in os.environ: 124 | os.environ["JUPYTER_TEST_BROWSER"] = 'chrome' 125 | 126 | if os.environ.get('SAUCE_USERNAME'): 127 | driver = make_sauce_driver() 128 | elif os.environ.get('JUPYTER_TEST_BROWSER') == 'live': 129 | driver = Chrome(ChromeDriverManager().install()) 130 | elif os.environ.get('JUPYTER_TEST_BROWSER') == 'chrome': 131 | chrome_options = webdriver.ChromeOptions() 132 | chrome_options.add_argument('--no-sandbox') 133 | chrome_options.add_argument('--window-size=1420,1080') 134 | chrome_options.add_argument('--headless') 135 | chrome_options.add_argument('--disable-gpu') 136 | driver = Chrome(ChromeDriverManager().install(), options=chrome_options) 137 | elif os.environ.get('JUPYTER_TEST_BROWSER') == 'firefox': 138 | driver = Firefox() 139 | else: 140 | raise ValueError( 141 | 'Invalid setting for JUPYTER_TEST_BROWSER. Valid options include live, chrome, and firefox' 142 | ) 143 | 144 | yield driver 145 | 146 | # Teardown 147 | driver.quit() 148 | 149 | 150 | @pytest.fixture(scope='module') 151 | def authenticated_browser(selenium_driver, notebook_server): 152 | selenium_driver.jupyter_server_info = notebook_server 153 | selenium_driver.get("{url}?token={token}".format(**notebook_server)) 154 | return selenium_driver 155 | 156 | 157 | @pytest.fixture(scope="class") 158 | def notebook(authenticated_browser): 159 | return Notebook.new_notebook( 160 | authenticated_browser, kernel_name='kernel-sos') 161 | -------------------------------------------------------------------------------- /test/test_frontend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) Bo Peng and the University of Texas MD Anderson Cancer Center 4 | # Distributed under the terms of the 3-clause BSD License. 5 | 6 | import time 7 | import unittest 8 | 9 | import pytest 10 | from ipykernel.tests.utils import execute, wait_for_idle 11 | from selenium.webdriver.common.keys import Keys 12 | # from sos_notebook.test_utils import flush_channels, sos_kernel, NotebookTest 13 | from test_utils import NotebookTest, flush_channels, sos_kernel 14 | 15 | 16 | class TestFrontEnd(NotebookTest): 17 | 18 | @pytest.mark.skip(reason="upstream bug") 19 | def test_run_in_console(self, notebook): 20 | idx = notebook.call("print(1)", kernel="SoS") 21 | time.sleep(5) 22 | notebook.execute_cell(idx, in_console=True) 23 | # the latest history cell 24 | assert "1" == notebook.get_cell_output(-1, in_console=True) 25 | 26 | #if the cell is non-SoS, the console should also change kernel 27 | idx = notebook.call("cat(123)", kernel="R") 28 | notebook.execute_cell(idx, in_console=True) 29 | # the latest history cell 30 | assert "123" == notebook.get_cell_output(-1, in_console=True) 31 | 32 | idx = notebook.call("print(12345)", kernel="SoS") 33 | notebook.execute_cell(idx, in_console=True) 34 | # the latest history cell 35 | assert "12345" == notebook.get_cell_output(-1, in_console=True) 36 | 37 | def test_run_directly_in_console(self, notebook): 38 | notebook.open_console() 39 | notebook.edit_prompt_cell('print("haha")', kernel='SoS', execute=True) 40 | assert "haha" == notebook.get_cell_output(-1, in_console=True) 41 | 42 | notebook.edit_prompt_cell('cat("haha2")', kernel="R", execute=True) 43 | assert "haha2" == notebook.get_cell_output(-1, in_console=True) 44 | 45 | def test_history_in_console(self, notebook): 46 | notebook.open_console() 47 | notebook.edit_prompt_cell("a = 1", execute=True) 48 | assert "" == notebook.get_prompt_content() 49 | notebook.edit_prompt_cell("b <- 2", kernel="R", execute=True) 50 | assert "" == notebook.get_prompt_content() 51 | # notebook.prompt_cell.send_keys(Keys.UP) 52 | notebook.send_keys_on_prompt_cell(Keys.UP) 53 | time.sleep(5) 54 | assert "b <- 2" == notebook.get_prompt_content() 55 | notebook.send_keys_on_prompt_cell(Keys.UP) 56 | time.sleep(5) 57 | # notebook.prompt_cell.send_keys(Keys.UP) 58 | assert "a = 1" == notebook.get_prompt_content() 59 | # FIXME: down keys does not work, perhaps because the cell is not focused and 60 | # the first step would be jumping to the end of the line 61 | notebook.send_keys_on_prompt_cell(Keys.DOWN) 62 | notebook.send_keys_on_prompt_cell(Keys.DOWN) 63 | # assert 'b <- 2' == notebook.get_prompt_content() 64 | 65 | def test_clear_history(self, notebook): 66 | notebook.open_console() 67 | notebook.edit_prompt_cell("a = 1", execute=True) 68 | notebook.edit_prompt_cell("b <- 2", kernel="R", execute=True) 69 | # use "clear" to clear all panel cells 70 | notebook.edit_prompt_cell("clear", kernel="SoS", execute=True) 71 | # we cannot wait for the completion of the cell because the cells 72 | # will be cleared 73 | # notebook.prompt_cell.send_keys(Keys.CONTROL, Keys.ENTER) 74 | assert not notebook.panel_cells 75 | 76 | def test_switch_kernel(self, notebook): 77 | kernels = notebook.get_kernel_list() 78 | assert "SoS" in kernels 79 | assert "R" in kernels 80 | backgroundColor = { 81 | "SoS": [0, 0, 0], 82 | "R": [220, 220, 218], 83 | "python3": [255, 217, 26], 84 | } 85 | 86 | # test change to R kernel by click 87 | notebook.select_kernel(index=0, kernel_name="R", by_click=True) 88 | # check background color for R kernel 89 | assert backgroundColor["R"], notebook.get_input_backgroundColor(0) 90 | 91 | # the cell keeps its color after evaluation 92 | notebook.edit_cell( 93 | index=0, 94 | content="""\ 95 | %preview -n rn 96 | rn <- rnorm(5) 97 | """, 98 | render=True, 99 | ) 100 | output = notebook.get_cell_output(0) 101 | assert "rn" in output and "num" in output 102 | assert backgroundColor["R"], notebook.get_output_backgroundColor(0) 103 | 104 | # test $get and shift to SoS kernel 105 | idx = notebook.call( 106 | """\ 107 | %get rn --from R 108 | len(rn) 109 | """, 110 | kernel="SoS", 111 | ) 112 | assert backgroundColor["SoS"], notebook.get_input_backgroundColor(idx) 113 | assert "5" in notebook.get_cell_output(idx) 114 | 115 | # switch to python3 kernel 116 | idx = notebook.call( 117 | """\ 118 | %use Python3 119 | """, 120 | kernel="SoS", 121 | ) 122 | assert backgroundColor["python3"] == notebook.get_input_backgroundColor( 123 | idx) 124 | 125 | notebook.append_cell("") 126 | assert backgroundColor["python3"] == notebook.get_input_backgroundColor( 127 | idx) 128 | 129 | 130 | 131 | if __name__ == "__main__": 132 | unittest.main() 133 | -------------------------------------------------------------------------------- /test/test_magics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) Bo Peng and the University of Texas MD Anderson Cancer Center 4 | # Distributed under the terms of the 3-clause BSD License. 5 | 6 | import os 7 | import sys 8 | import tempfile 9 | import time 10 | 11 | import pytest 12 | from test_utils import NotebookTest 13 | 14 | 15 | class TestMagics(NotebookTest): 16 | 17 | def test_magic_in_subkernel(self, notebook): 18 | """test %pwd in the python3 kernel (which is not a sos magic)""" 19 | assert len(notebook.check_output("%pwd", kernel="Python3")) > 0 20 | 21 | @pytest.mark.parametrize('magic', [ 22 | "cd", 23 | "convert", 24 | "dict", 25 | "get", 26 | "matplotlib", 27 | "preview", 28 | "put", 29 | "render", 30 | 'revisions', 31 | "run", 32 | "runfile", 33 | "save", 34 | "sandbox", 35 | "sessioninfo", 36 | "sosrun", 37 | "shutdown", 38 | "task", 39 | "use", 40 | "with", 41 | ]) 42 | def test_help_messages(self, notebook, magic): 43 | """test help functions of magics""" 44 | assert magic in notebook.check_output(f"%{magic} -h", kernel="SoS") 45 | 46 | def test_magic_capture(self, notebook): 47 | # test %capture 48 | # capture raw (default) 49 | notebook.call( 50 | """\ 51 | %capture 52 | cat('this is to stdout') 53 | """, 54 | kernel="R", 55 | ) 56 | output = notebook.check_output('__captured', kernel='SoS') 57 | assert 'stream' in output and 'stdout' in output and 'this is to stdout' in output 58 | # specify raw 59 | notebook.call( 60 | """\ 61 | %capture raw 62 | cat('this is to stdout') 63 | """, 64 | kernel="R", 65 | ) 66 | output = notebook.check_output('__captured', kernel='SoS') 67 | assert 'stream' in output and 'stdout' in output and 'this is to stdout' in output 68 | # 69 | # capture SoS execute_result (#220) 70 | notebook.call( 71 | """\ 72 | %capture raw 73 | 'this is to texts' 74 | """, 75 | kernel="SoS", 76 | ) 77 | output = notebook.check_output('__captured', kernel='SoS') 78 | assert 'execute_result' in output and 'text/plain' in output and 'this is to texts' in output 79 | # 80 | # capture to variable 81 | assert (notebook.check_output( 82 | """\ 83 | %capture stdout --to R_out 84 | cat('this is to stdout') 85 | """, 86 | kernel="R", 87 | ) == "this is to stdout") 88 | # 89 | notebook.call("%capture stdout --to R_out \n ", kernel="R") 90 | assert notebook.check_output("R_out", kernel="SoS") == "''" 91 | # 92 | notebook.call( 93 | """\ 94 | %capture text --to R_out 95 | paste('this is the return value') 96 | """, 97 | kernel="R", 98 | ) 99 | output = notebook.check_output("R_out", kernel="SoS") 100 | assert "this is the return value" in output 101 | # 102 | # capture as csv 103 | notebook.call( 104 | """\ 105 | %capture stdout --as csv --to res 106 | print('a,b\\nc,d') 107 | """, 108 | kernel="SoS", 109 | ) 110 | assert "a" in notebook.check_output("res", kernel="SoS") 111 | assert "DataFrame" in notebook.check_output("type(res)", kernel="SoS") 112 | # 113 | # capture as tsv 114 | notebook.call( 115 | """\ 116 | %capture stdout --as tsv --to res 117 | print('a\\tb\\nc\\td') 118 | """, 119 | kernel="SoS", 120 | ) 121 | assert "a" in notebook.check_output("res", kernel="SoS") 122 | assert "DataFrame" in notebook.check_output("type(res)", kernel="SoS") 123 | # 124 | # capture as json 125 | notebook.call( 126 | """\ 127 | %capture stdout --as json --to res 128 | print('[1,2,3]') 129 | """, 130 | kernel="SoS", 131 | ) 132 | assert "[1, 2, 3]" in notebook.check_output('res', kernel="SoS") 133 | # 134 | # test append to str 135 | notebook.call( 136 | """\ 137 | %capture stdout --to captured_text 138 | print('from sos') 139 | """, 140 | kernel="SoS", 141 | ) 142 | notebook.call( 143 | """\ 144 | %capture stdout --append captured_text 145 | cat('from R') 146 | """, 147 | kernel="R", 148 | ) 149 | output = notebook.check_output("captured_text", kernel="SoS") 150 | assert 'from sos' in output and 'from R' in output 151 | assert 'str' in notebook.check_output( 152 | "type(captured_text)", kernel="SoS") 153 | # test append to dataframe 154 | notebook.call( 155 | """\ 156 | %capture stdout --as tsv --to table 157 | print('a\\tb\\n11\\t22') 158 | """, 159 | kernel="SoS", 160 | ) 161 | notebook.call( 162 | """\ 163 | %capture stdout --as tsv --append table 164 | print('a\\tb\\n33\\t44') 165 | """, 166 | kernel="SoS", 167 | ) 168 | output = notebook.check_output("table", kernel="SoS") 169 | assert '11' in output and '22' in output and '33' in output and '44' in output 170 | assert 'DataFrame' in notebook.check_output("type(table)", kernel="SoS") 171 | 172 | def test_magic_cd(self, notebook): 173 | # magic cd that changes directory of all subfolders 174 | output1 = notebook.check_output( 175 | """\ 176 | import os 177 | print(os.getcwd()) 178 | """, 179 | kernel="Python3", 180 | ) 181 | notebook.call("%cd ..", kernel="SoS") 182 | output2 = notebook.check_output( 183 | """\ 184 | import os 185 | print(os.getcwd()) 186 | """, 187 | kernel="Python3", 188 | ) 189 | assert len(output1) > len(output2) and output1.startswith(output2) 190 | 191 | def test_magic_connectinfo(self, notebook): 192 | # test %capture 193 | assert "Connection file" in notebook.check_output( 194 | "%connectinfo", kernel="SoS") 195 | 196 | def test_magic_debug(self, notebook): 197 | assert "debug" in notebook.check_output( 198 | """\ 199 | %debug on 200 | %debug off 201 | """, 202 | kernel="SoS", 203 | expect_error=True, 204 | ) 205 | 206 | def test_magic_dict(self, notebook): 207 | # test %dict 208 | notebook.call( 209 | """\ 210 | R_out = 1 211 | ran = 5 212 | """, 213 | kernel="SoS", 214 | ) 215 | output = notebook.check_output( 216 | """\ 217 | %dict --keys 218 | """, 219 | kernel="SoS", 220 | ) 221 | assert "R_out" in output and "ran" in output 222 | # 223 | assert "r" in notebook.check_output("%dict ran", kernel="SoS") 224 | # 225 | assert "R_out" not in notebook.check_output( 226 | """\ 227 | %dict --reset 228 | %dict --keys 229 | """, 230 | kernel="SoS", 231 | ) 232 | 233 | def test_magic_expand(self, notebook): 234 | # test %expand 235 | notebook.call("par=100", kernel="SoS") 236 | assert "A parameter {par} greater than 50 is specified." == notebook.check_output( 237 | """\ 238 | cat('A parameter {par} greater than 50 is specified.'); 239 | """, 240 | kernel="R", 241 | ) 242 | assert "A parameter 100 greater than 50 is specified." == notebook.check_output( 243 | """\ 244 | %expand 245 | if ({par} > 50) {{ 246 | cat('A parameter {par} greater than 50 is specified.'); 247 | }} 248 | """, 249 | kernel="R", 250 | ) 251 | assert "A parameter 100 greater than 50 is specified." == notebook.check_output( 252 | """\ 253 | %expand ${ } 254 | if (${par} > 50) { 255 | cat('A parameter ${par} greater than 50 is specified.'); 256 | } 257 | """, 258 | kernel="R", 259 | ) 260 | assert "A parameter 100 greater than 50 is specified." == notebook.check_output( 261 | """\ 262 | %expand [ ] 263 | if ([par] > 50) { 264 | cat('A parameter [par] greater than 50 is specified.'); 265 | } 266 | """, 267 | kernel="R", 268 | ) 269 | 270 | def test_magic_get(self, notebook): 271 | # test %get 272 | notebook.call( 273 | """\ 274 | a = [1, 2, 3] 275 | b = [1, 2, '3'] 276 | """, 277 | kernel="SoS", 278 | ) 279 | assert "[1, 2, 3]" == notebook.check_output( 280 | """\ 281 | %get a 282 | a 283 | """, 284 | kernel="Python3", 285 | ) 286 | assert "List of 3" in notebook.check_output( 287 | """\ 288 | %get b 289 | str(b) 290 | R_var <- 'R variable' 291 | """, 292 | kernel="R", 293 | ) 294 | assert "R variable" in notebook.check_output( 295 | """\ 296 | %get --from R R_var 297 | R_var 298 | """, 299 | kernel="Python3", 300 | ) 301 | # 302 | # get with different variable names 303 | notebook.call( 304 | """\ 305 | a = 1025 306 | _b_a = 22 307 | """, 308 | kernel="SoS", 309 | ) 310 | assert "1025" == notebook.check_output( 311 | """\ 312 | %get a 313 | b <- 122 314 | c <- 555 315 | a 316 | """, 317 | kernel="R", 318 | ) 319 | # 320 | assert "22" in notebook.check_output( 321 | """\ 322 | %get _b_a 323 | .b_a 324 | """, 325 | kernel="R", 326 | expect_error=True, 327 | ) 328 | # 329 | # get from another kernel 330 | assert "555" in notebook.check_output( 331 | """\ 332 | %get c --from R 333 | c 334 | """, 335 | kernel="R", 336 | ) 337 | 338 | def test_magic_matplotlib(self, notebook): 339 | # test %capture 340 | pytest.importorskip("matplotlib") 341 | assert "data:image/png;base64" in notebook.check_output( 342 | """\ 343 | %matplotlib inline 344 | 345 | import matplotlib.pyplot as plt 346 | import numpy as np 347 | x = np.linspace(0, 10) 348 | plt.plot(x, np.sin(x), '--', linewidth=2) 349 | plt.show() 350 | """, 351 | kernel="SoS", 352 | selector="img", 353 | attribute="src", 354 | ) 355 | 356 | def test_magic_render(self, notebook): 357 | # test %put from subkernel to SoS Kernel 358 | output = notebook.check_output( 359 | '''\ 360 | %render 361 | """ 362 | # header 363 | 364 | * item1 365 | * item2 366 | """ 367 | ''', 368 | kernel="SoS", 369 | ) 370 | assert "header" in output and 'item1' in output and 'item2' in output 371 | assert '# header' not in output and '* item1' not in output and '* item2' not in output 372 | # render wrong type from subkernel 373 | output = notebook.check_output( 374 | '''\ 375 | %render text 376 | cat("\\n# header\\n* item1\\n* item2\\n") 377 | ''', 378 | kernel="R", 379 | ) 380 | assert "header" not in output and 'item1' not in output and 'item2' not in output 381 | # render correct type 382 | output = notebook.check_output( 383 | '''\ 384 | %render 385 | cat("\\n# header\\n* item1\\n* item2\\n") 386 | ''', 387 | kernel="R", 388 | ) 389 | assert "header" in output and 'item1' in output and 'item2' in output 390 | # 391 | # test render as other types 392 | output = notebook.check_output( 393 | '''\ 394 | %render --as Latex 395 | """ 396 | $$c = \\sqrt{a^2 + b^2}$$ 397 | """ 398 | ''', 399 | kernel="SoS") 400 | assert "c" in output and 'a' in output, output 401 | 402 | def test_magic_run(self, notebook): 403 | # test passing parameters and %run 404 | output = notebook.check_output( 405 | """\ 406 | %run --floatvar 1 --test_mode --INT_LIST 1 2 3 --infile a.txt 407 | VAR = 'This var is defined without global.' 408 | 409 | [global] 410 | GLOBAL_VAR='This var is defined with global.' 411 | 412 | [step_1] 413 | CELL_VAR='This var is defined in Cell.' 414 | parameter: floatvar=float 415 | parameter: stringvar='stringvar' 416 | print(VAR) 417 | print(GLOBAL_VAR) 418 | print(CELL_VAR) 419 | print(floatvar) 420 | print(stringvar) 421 | 422 | [step_2] 423 | parameter: test_mode=bool 424 | parameter: INT_LIST=[] 425 | parameter: infile = path 426 | parameter: b=1 427 | print(test_mode) 428 | print(INT_LIST) 429 | print(infile.name) 430 | python: expand=True 431 | print({b}) 432 | """, 433 | kernel="SoS", 434 | ) 435 | assert all(line in output.splitlines() for line in [ 436 | "This var is defined without global.", 437 | "This var is defined with global.", 438 | "This var is defined in Cell.", 439 | "1.0", 440 | "stringvar", 441 | "True", 442 | "['1', '2', '3']", 443 | "a.txt", 444 | "1", 445 | ]) 446 | 447 | def test_magic_runfile(self, notebook): 448 | # 449 | notebook.call( 450 | """\ 451 | %save check_run -f 452 | %run --var 1 453 | parameter: var=0 454 | python: expand=True 455 | print({var}) 456 | """, 457 | kernel="SoS", 458 | ) 459 | assert "21122" in notebook.check_output( 460 | "%runfile check_run --var=21122", kernel="SoS") 461 | 462 | @pytest.mark.skipif( 463 | sys.platform == "win32" or "TRAVIS" in os.environ, 464 | reason="Skip test because of no internet connection or in travis test", 465 | ) 466 | def test_magic_preview_dot(self, notebook): 467 | dotfile = os.path.join(os.path.expanduser('~'), 'a.dot') 468 | with open(dotfile, 'w') as dot: 469 | dot.write("""\ 470 | graph graphname { 471 | a -- b -- c; 472 | b -- d; 473 | } 474 | """) 475 | 476 | output = notebook.check_output( 477 | '%preview -n ~/a.dot', 478 | kernel="SoS", 479 | selector="img", 480 | ) 481 | assert "a.dot" in output and "data:image/png;base64" in output 482 | os.remove(dotfile) 483 | 484 | def test_magic_preview_in_R(self, notebook): 485 | assert "mtcars" in notebook.check_output( 486 | """\ 487 | %preview -n mtcars 488 | %use R 489 | """, 490 | kernel="R", 491 | ) 492 | 493 | def test_magic_preview_png(self, notebook): 494 | output = notebook.check_output( 495 | """\ 496 | %preview -n a.png 497 | R: 498 | png('a.png') 499 | plot(0) 500 | dev.off() 501 | """, 502 | kernel="SoS", 503 | selector="img", 504 | ) 505 | assert "a.png" in output and "data:image/png;base64" in output 506 | 507 | def test_magic_preview_jpg(self, notebook): 508 | output = notebook.check_output( 509 | """\ 510 | %preview -n a.jp* 511 | R: 512 | jpeg('a.jpg') 513 | plot(0) 514 | dev.off() 515 | """, 516 | kernel="SoS", 517 | selector="img", 518 | ) 519 | assert "a.jpg" in output and ("data:image/jpeg;base64" in output or 520 | "data:image/png;base64" in output) 521 | 522 | def test_magic_preview_pdf(self, notebook): 523 | output = notebook.check_output( 524 | """\ 525 | %preview -n a.pdf 526 | R: 527 | pdf('a.pdf') 528 | plot(0) 529 | dev.off() 530 | """, 531 | kernel="SoS", 532 | selector="embed", 533 | attribute="type", 534 | ) 535 | assert "a.pdf" in output and ( 536 | "application/x-google-chrome-pdf" in output or 537 | "application/pdf" in output) 538 | 539 | @pytest.mark.xfail( 540 | reason='Some system has imagemagick refusing to read PDF due to policy reasons.' 541 | ) 542 | def test_magic_preview_pdf_as_png(self, notebook): 543 | try: 544 | from wand.image import Image 545 | except ImportError: 546 | pytest.skip("Skip because imagemagick is not properly installed") 547 | # preview as png 548 | output = notebook.check_output( 549 | """\ 550 | %preview -n a.pdf -s png 551 | R: 552 | pdf('a.pdf') 553 | plot(0) 554 | dev.off() 555 | """, 556 | kernel="SoS", 557 | selector="img", 558 | ) 559 | assert "a.pdf" in output and "data:image/png;base64" in output 560 | 561 | def test_magic_preview_var(self, notebook): 562 | assert "> a: int" in notebook.check_output( 563 | """\ 564 | %preview -n a 565 | a=1 566 | """, 567 | kernel="SoS", 568 | ) 569 | 570 | def test_magic_preview_var_limit(self, notebook): 571 | output = notebook.check_output( 572 | """\ 573 | %preview var -n -l 5 574 | import numpy as np 575 | import pandas as pd 576 | var = pd.DataFrame( 577 | np.asmatrix([[i*10, i*10+1] for i in range(100)])) 578 | """, 579 | kernel="SoS", 580 | ) 581 | assert "var" in output and "41" in output and "80" not in output 582 | 583 | # def test_magic_preview_var_scatterplot(self, notebook): 584 | # output = notebook.check_output('''\ 585 | # %preview mtcars -n -s scatterplot mpg disp --by cyl 586 | # %get mtcars --from R 587 | # ''', kernel="SoS") 588 | 589 | # def test_magic_preview_var_scatterplot_tooltip(self, notebook): 590 | # output = notebook.check_output('''\ 591 | # %preview mtcars -n -s scatterplot _index disp hp mpg --tooltip wt qsec 592 | # %get mtcars --from R 593 | # ''', kernel="SoS") 594 | 595 | # def test_magic_preview_var_scatterplot_log(self, notebook): 596 | # output = notebook.check_output('''\ 597 | # %preview mtcars -n -s scatterplot disp hp --log xy --xlim 60 80 --ylim 40 300 598 | # %get mtcars --from R 599 | # ''', kernel="SoS") 600 | 601 | def test_magic_preview_csv(self, notebook): 602 | output = notebook.check_output( 603 | '''\ 604 | %preview -n a.csv 605 | with open('a.csv', 'w') as csv: 606 | csv.write("""\ 607 | a,b,c 608 | 1,2,3 609 | 4,5,6 610 | """) 611 | ''', 612 | kernel="SoS", 613 | ) 614 | assert "> a.csv" in output and " a b c " in output 615 | 616 | def test_magic_preview_txt(self, notebook): 617 | output = notebook.check_output( 618 | '''\ 619 | %preview -n a.txt 620 | with open('a.txt', 'w') as txt: 621 | txt.write("""\ 622 | hello 623 | world 624 | """) 625 | ''', 626 | kernel="SoS", 627 | ) 628 | assert "> a.txt" in output and "2 lines" in output 629 | 630 | def test_magic_preview_zip(self, notebook): 631 | output = notebook.check_output( 632 | """\ 633 | %preview -n a.zip 634 | import zipfile 635 | with open('a.csv', 'w') as tmp: 636 | tmp.write('blah') 637 | with zipfile.ZipFile('a.zip', 'w') as zfile: 638 | zfile.write('a.csv') 639 | """, 640 | kernel="SoS", 641 | ) 642 | import time 643 | time.sleep(20) 644 | assert "> a.zip" in output and "1 file" in output and "a.csv" in output 645 | 646 | def test_magic_preview_tar(self, notebook): 647 | output = notebook.check_output( 648 | """\ 649 | %preview -n a.tar 650 | import tarfile 651 | with open('a.csv', 'w') as tmp: 652 | tmp.write('blah') 653 | with tarfile.open('a.tar', 'w') as tar: 654 | tar.add('a.csv') 655 | """, 656 | kernel="SoS", 657 | ) 658 | assert "> a.tar" in output and "1 file" in output and "a.csv" in output 659 | 660 | def test_magic_preview_tar_gz(self, notebook): 661 | output = notebook.check_output( 662 | """\ 663 | %preview -n a.tar.gz 664 | import tarfile 665 | with open('a.csv', 'w') as tmp: 666 | tmp.write('blah') 667 | with tarfile.open('a.tar.gz', 'w:gz') as tar: 668 | tar.add('a.csv') 669 | """, 670 | kernel="SoS", 671 | ) 672 | assert "> a.tar.gz" in output and "1 file" in output and "a.csv" in output 673 | 674 | def test_magic_preview_gz(self, notebook): 675 | output = notebook.check_output( 676 | '''\ 677 | %preview -n a.gz 678 | import gzip 679 | 680 | with gzip.open('a.gz', 'w') as gz: 681 | gz.write(b""" 682 | Hello 683 | world 684 | """) 685 | ''', 686 | kernel="SoS", 687 | ) 688 | assert "> a.gz" in output and "Hello" in output and "world" in output 689 | 690 | def test_magic_preview_md(self, notebook): 691 | try: 692 | import markdown 693 | except ImportError: 694 | return 695 | output = notebook.check_output( 696 | '''\ 697 | %preview -n a.md 698 | with open('a.md', 'w') as md: 699 | md.write("""\ 700 | # title 701 | 702 | * item1 703 | * item2 704 | """) 705 | ''', 706 | kernel="SoS", 707 | ) 708 | assert "> a.md" in output and "title" in output and "item2" in output 709 | 710 | def test_magic_preview_html(self, notebook): 711 | output = notebook.check_output( 712 | '''\ 713 | %preview -n a.html 714 | with open('a.html', 'w') as dot: 715 | dot.write("""\ 716 | 717 | 718 | 719 | 720 |

My First Heading

721 | 722 |

My first paragraph.

723 | 724 | 725 | 726 | """) 727 | ''', 728 | kernel="SoS", 729 | ) 730 | assert ("> a.html" in output and "My First Heading" in output and 731 | "My first paragraph" in output) 732 | 733 | def test_magic_put(self, notebook): 734 | # test %put from subkernel to SoS Kernel 735 | notebook.call( 736 | """\ 737 | %put a b c R_var 738 | a <- c(1) 739 | b <- c(1, 2, 3) 740 | R_var <- 'R variable' 741 | """, 742 | kernel="R", 743 | ) 744 | 745 | assert "1" in notebook.check_output(content="a", kernel="SoS") 746 | 747 | assert "[1, 2, 3]" in notebook.check_output(content="b", kernel="SoS") 748 | 749 | assert "R variable" in notebook.check_output( 750 | content="R_var", kernel="SoS") 751 | 752 | # test %put from SoS to other kernel 753 | # 754 | notebook.call( 755 | """\ 756 | %put a1 b1 --to R 757 | a1 = 123 758 | b1 = 'this is python' 759 | """, 760 | kernel="SoS", 761 | ) 762 | assert "123" in notebook.check_output(content="cat(a1)", kernel="R") 763 | 764 | assert "this is python" in notebook.check_output( 765 | content="cat(b1)", kernel="R") 766 | # 767 | # test put variable with invalid names 768 | notebook.call( 769 | """\ 770 | %put .a.b 771 | .a.b <- 22""", 772 | kernel="R", 773 | expect_error=True, 774 | ) 775 | assert "22" == notebook.check_output("_a_b", kernel="SoS") 776 | 777 | # 778 | # test independence of variables 779 | notebook.call( 780 | """\ 781 | %put my_var --to R 782 | my_var = '124' 783 | """, 784 | kernel="SoS", 785 | ) 786 | assert "'124'" == notebook.check_output("my_var", kernel="R") 787 | 788 | notebook.call("my_var = 'something else'", kernel="R") 789 | assert "'124'" == notebook.check_output("my_var", kernel="SoS") 790 | 791 | def test_magic_sandbox(self, notebook): 792 | notebook.call( 793 | """\ 794 | %sandbox 795 | with open('test_blah.txt', 'w') as tb: 796 | tb.write('a') 797 | """, 798 | kernel="SoS", 799 | ) 800 | assert not os.path.isfile("test_blah.txt") 801 | 802 | def test_magic_save(self, notebook): 803 | tmp_file = os.path.join(os.path.expanduser("~"), "test_save.txt") 804 | if os.path.isfile(tmp_file): 805 | os.remove(tmp_file) 806 | notebook.call( 807 | """\ 808 | %save ~/test_save.txt 809 | a=1 810 | """, 811 | kernel="SoS", 812 | ) 813 | with open(tmp_file) as tt: 814 | assert tt.read() == "a=1\n" 815 | os.remove(tmp_file) 816 | 817 | def test_magic_sessioninfo(self, notebook): 818 | output = notebook.check_output( 819 | """\ 820 | %use Python3 821 | %use SoS 822 | %sessioninfo 823 | """, 824 | kernel="SoS", 825 | ) 826 | assert "SoS Version" in output and "Python3" in output 827 | # test the with option 828 | notebook.call( 829 | ''' 830 | sinfo = { 831 | 'str_section': 'rsync 3.2', 832 | 'list_section': [('v1', 'v2'), ('v3', b'v4')], 833 | 'dict_section': {'d1': 'd2', 'd3': b'd4'} 834 | } 835 | ''', 836 | kernel='SoS') 837 | output = notebook.check_output( 838 | """\ 839 | %use Python3 840 | %use SoS 841 | %sessioninfo --with sinfo 842 | """, 843 | kernel="SoS", 844 | ) 845 | assert "SoS Version" in output and "Python3" in output 846 | assert all( 847 | x in output for x in ('rsync 3.2', 'v1', 'v2', 'v3', 'v4', 'd1', 848 | 'd2', 'd3', 'd4')) 849 | 850 | @pytest.mark.skipif( 851 | sys.platform == "win32", 852 | reason="! magic does not support built-in command #203") 853 | def test_magic_shell(self, notebook): 854 | assert "haha" in notebook.check_output("!echo haha", kernel="SoS") 855 | 856 | @pytest.mark.skip( 857 | reason="Cannot figure out why the file sometimes does not exist") 858 | def test_magic_sossave(self, notebook): 859 | # 860 | notebook.save() 861 | 862 | tmp_file = os.path.join(tempfile.gettempdir(), "test_sossave.html") 863 | if os.path.isfile(tmp_file): 864 | os.remove(tmp_file) 865 | assert "Workflow saved to" in notebook.check_output( 866 | f"""\ 867 | %sossave {tmp_file} --force 868 | [10] 869 | print('kkk') 870 | """, 871 | kernel="SoS", 872 | ) 873 | with open(tmp_file) as tt: 874 | assert "kkk" in tt.read() 875 | 876 | def test_magic_use(self, notebook): 877 | idx = notebook.call( 878 | "%use R0 -l sos_r.kernel:sos_R -c #CCCCCC", kernel="SoS") 879 | assert [204, 204, 204] == notebook.get_input_backgroundColor(idx) 880 | 881 | idx = notebook.call( 882 | "%use R1 -l sos_r.kernel:sos_R -k ir -c #CCCCCC", kernel="SoS") 883 | assert [204, 204, 204] == notebook.get_input_backgroundColor(idx) 884 | 885 | notebook.call("%use R2 -k ir", kernel="SoS") 886 | notebook.call("a <- 1024", kernel="R2") 887 | assert "1024" == notebook.check_output("a", kernel="R2") 888 | 889 | notebook.call("%use R3 -k ir -l R", kernel="SoS") 890 | notebook.call("a <- 233", kernel="R3") 891 | assert "233" == notebook.check_output("a", kernel="R3") 892 | 893 | notebook.call("%use R2 -c red", kernel="R3") 894 | assert "1024" == notebook.check_output("a", kernel="R2") 895 | 896 | # def test_sos_vars(self, notebook): 897 | # # test automatic tranfer of sos variables 898 | # notebook.call("sosa = f'{3*8}'", kernel="Python3") 899 | # assert "24" in notebook.check_output("sosa", kernel="SoS") 900 | 901 | def test_magic_with(self, notebook): 902 | # test %with 903 | notebook.call("a = 3", kernel="SoS") 904 | notebook.call( 905 | """\ 906 | %with R -i a -o ran 907 | ran<-rnorm(a) 908 | """, 909 | kernel="SoS", 910 | ) 911 | assert len(notebook.check_output("ran", kernel="SoS")) > 0 912 | -------------------------------------------------------------------------------- /test/test_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) Bo Peng and the University of Texas MD Anderson Cancer Center 4 | # Distributed under the terms of the 3-clause BSD License. 5 | 6 | # 7 | # NOTE: for some namespace reason, this test can only be tested using 8 | # nose. 9 | 10 | import atexit 11 | import os 12 | import re 13 | import time 14 | # 15 | # 16 | from contextlib import contextmanager 17 | from queue import Empty 18 | from sys import platform 19 | from textwrap import dedent 20 | 21 | import pytest 22 | from ipykernel.tests import utils as test_utils 23 | from selenium.common.exceptions import NoSuchElementException 24 | from selenium.webdriver import ActionChains 25 | from selenium.webdriver.common.by import By 26 | from selenium.webdriver.common.keys import Keys 27 | from selenium.webdriver.remote.webelement import WebElement 28 | from selenium.webdriver.support import expected_conditions as EC 29 | from selenium.webdriver.support.ui import Select, WebDriverWait 30 | 31 | pjoin = os.path.join 32 | 33 | test_utils.TIMEOUT = 60 34 | 35 | KM = None 36 | KC = None 37 | 38 | 39 | @contextmanager 40 | def sos_kernel(): 41 | """Context manager for the global kernel instance 42 | Should be used for most kernel tests 43 | Returns 44 | ------- 45 | kernel_client: connected KernelClient instance 46 | """ 47 | yield start_sos_kernel() 48 | 49 | 50 | def flush_channels(kc=None): 51 | """flush any messages waiting on the queue""" 52 | 53 | if kc is None: 54 | kc = KC 55 | for channel in (kc.shell_channel, kc.iopub_channel): 56 | while True: 57 | try: 58 | channel.get_msg(block=True, timeout=0.1) 59 | except Empty: 60 | break 61 | # do not validate message because SoS has special sos_comm 62 | # else: 63 | # validate_message(msg) 64 | 65 | 66 | def start_sos_kernel(): 67 | """start the global kernel (if it isn't running) and return its client""" 68 | global KM, KC 69 | if KM is None: 70 | KM, KC = test_utils.start_new_kernel(kernel_name='sos') 71 | atexit.register(stop_sos_kernel) 72 | else: 73 | flush_channels(KC) 74 | return KC 75 | 76 | 77 | def stop_sos_kernel(): 78 | """Stop the global shared kernel instance, if it exists""" 79 | global KM, KC 80 | KC.stop_channels() 81 | KC = None 82 | if KM is None: 83 | return 84 | KM.shutdown_kernel(now=False) 85 | KM = None 86 | 87 | 88 | def get_result(iopub): 89 | """retrieve result from an execution""" 90 | result = None 91 | while True: 92 | msg = iopub.get_msg(block=True, timeout=1) 93 | msg_type = msg['msg_type'] 94 | content = msg['content'] 95 | if msg_type == 'status' and content['execution_state'] == 'idle': 96 | # idle message signals end of output 97 | break 98 | elif msg['msg_type'] == 'execute_result': 99 | result = content['data'] 100 | elif msg['msg_type'] == 'display_data': 101 | result = content['data'] 102 | else: 103 | # other output, ignored 104 | pass 105 | # text/plain can have fronzen dict, this is ok, 106 | from numpy import array, matrix, uint8 107 | 108 | # suppress pyflakes warning 109 | array 110 | matrix 111 | uint8 112 | 113 | # it can also have dict_keys, we will have to redefine it 114 | 115 | def dict_keys(args): 116 | return args 117 | 118 | if result is None: 119 | return None 120 | else: 121 | return eval(result['text/plain']) 122 | 123 | 124 | def get_display_data(iopub, data_type='text/plain'): 125 | """retrieve display_data from an execution from subkernel 126 | because subkernel (for example irkernel) does not return 127 | execution_result 128 | """ 129 | result = None 130 | while True: 131 | msg = iopub.get_msg(block=True, timeout=1) 132 | msg_type = msg['msg_type'] 133 | content = msg['content'] 134 | if msg_type == 'status' and content['execution_state'] == 'idle': 135 | # idle message signals end of output 136 | break 137 | elif msg['msg_type'] == 'display_data': 138 | if isinstance(data_type, str): 139 | if data_type in content['data']: 140 | result = content['data'][data_type] 141 | else: 142 | for dt in data_type: 143 | if dt in content['data']: 144 | result = content['data'][dt] 145 | # some early version of IRKernel still passes execute_result 146 | elif msg['msg_type'] == 'execute_result': 147 | result = content['data']['text/plain'] 148 | return result 149 | 150 | 151 | def clear_channels(iopub): 152 | """assemble stdout/err from an execution""" 153 | while True: 154 | msg = iopub.get_msg(block=True, timeout=1) 155 | msg_type = msg['msg_type'] 156 | content = msg['content'] 157 | if msg_type == 'status' and content['execution_state'] == 'idle': 158 | # idle message signals end of output 159 | break 160 | 161 | 162 | def get_std_output(iopub): 163 | '''Obtain stderr and remove some unnecessary warning from 164 | https://github.com/jupyter/jupyter_client/pull/201#issuecomment-314269710''' 165 | stdout, stderr = test_utils.assemble_output(iopub) 166 | return stdout, '\n'.join([ 167 | x for x in stderr.splitlines() if 'sticky' not in x and 168 | 'RuntimeWarning' not in x and 'communicator' not in x 169 | ]) 170 | 171 | 172 | def wait_for_selector(browser, 173 | selector, 174 | timeout=60, 175 | visible=False, 176 | single=False): 177 | wait = WebDriverWait(browser, timeout) 178 | if single: 179 | if visible: 180 | conditional = EC.visibility_of_element_located 181 | else: 182 | conditional = EC.presence_of_element_located 183 | else: 184 | if visible: 185 | conditional = EC.visibility_of_all_elements_located 186 | else: 187 | conditional = EC.presence_of_all_elements_located 188 | return wait.until(conditional((By.CSS_SELECTOR, selector))) 189 | 190 | 191 | def wait_for_tag(driver, 192 | tag, 193 | timeout=10, 194 | visible=False, 195 | single=False, 196 | wait_for_n=1): 197 | if wait_for_n > 1: 198 | return _wait_for_multiple(driver, By.TAG_NAME, tag, timeout, wait_for_n, 199 | visible) 200 | return _wait_for(driver, By.TAG_NAME, tag, timeout, visible, single) 201 | 202 | 203 | def _wait_for(driver, 204 | locator_type, 205 | locator, 206 | timeout=10, 207 | visible=False, 208 | single=False): 209 | """Waits `timeout` seconds for the specified condition to be met. Condition is 210 | met if any matching element is found. Returns located element(s) when found. 211 | Args: 212 | driver: Selenium web driver instance 213 | locator_type: type of locator (e.g. By.CSS_SELECTOR or By.TAG_NAME) 214 | locator: name of tag, class, etc. to wait for 215 | timeout: how long to wait for presence/visibility of element 216 | visible: if True, require that element is not only present, but visible 217 | single: if True, return a single element, otherwise return a list of matching 218 | elements 219 | """ 220 | wait = WebDriverWait(driver, timeout) 221 | if single: 222 | if visible: 223 | conditional = EC.visibility_of_element_located 224 | else: 225 | conditional = EC.presence_of_element_located 226 | else: 227 | if visible: 228 | conditional = EC.visibility_of_all_elements_located 229 | else: 230 | conditional = EC.presence_of_all_elements_located 231 | return wait.until(conditional((locator_type, locator))) 232 | 233 | 234 | def _wait_for_multiple(driver, 235 | locator_type, 236 | locator, 237 | timeout, 238 | wait_for_n, 239 | visible=False): 240 | """Waits until `wait_for_n` matching elements to be present (or visible). 241 | Returns located elements when found. 242 | Args: 243 | driver: Selenium web driver instance 244 | locator_type: type of locator (e.g. By.CSS_SELECTOR or By.TAG_NAME) 245 | locator: name of tag, class, etc. to wait for 246 | timeout: how long to wait for presence/visibility of element 247 | wait_for_n: wait until this number of matching elements are present/visible 248 | visible: if True, require that elements are not only present, but visible 249 | """ 250 | wait = WebDriverWait(driver, timeout) 251 | 252 | def multiple_found(driver): 253 | elements = driver.find_elements(locator_type, locator) 254 | if visible: 255 | elements = [e for e in elements if e.is_displayed()] 256 | if len(elements) < wait_for_n: 257 | return False 258 | return elements 259 | 260 | return wait.until(multiple_found) 261 | 262 | 263 | class CellTypeError(ValueError): 264 | 265 | def __init__(self, message=""): 266 | self.message = message 267 | 268 | 269 | promise_js = """ 270 | var done = arguments[arguments.length - 1]; 271 | %s.then( 272 | data => { done(["success", data]); }, 273 | error => { done(["error", error]); } 274 | ); 275 | """ 276 | 277 | 278 | def execute_promise(js, browser): 279 | state, data = browser.execute_async_script(promise_js % js) 280 | if state == 'success': 281 | return data 282 | raise Exception(data) 283 | 284 | 285 | class Notebook: 286 | 287 | def __init__(self, browser): 288 | self.browser = browser 289 | self._disable_autosave_and_onbeforeunload() 290 | # with open('./test/jquery-3.4.1.min.js', 'r') as jquery_js: 291 | # jquery=jquery_js.read() 292 | # self.browser.execute_script(jquery) 293 | wait_for_selector( 294 | browser, 295 | "div.jp-Notebook-cell", 296 | timeout=10, 297 | visible=False, 298 | single=True) 299 | # self.prompt_cell = list( 300 | # self.browser.find_elements_by_xpath( 301 | # "//*[@id='panel-wrapper']/div"))[-1] 302 | 303 | def __len__(self): 304 | return len(self.cells) 305 | 306 | def __getitem__(self, key): 307 | return self.cells[key] 308 | 309 | def __setitem__(self, key, item): 310 | if isinstance(key, int): 311 | self.edit_cell(index=key, content=item, render=False) 312 | # TODO: re-add slicing support, handle general python slicing behaviour 313 | # includes: overwriting the entire self.cells object if you do 314 | # self[:] = [] 315 | # elif isinstance(key, slice): 316 | # indices = (self.index(cell) for cell in self[key]) 317 | # for k, v in zip(indices, item): 318 | # self.edit_cell(index=k, content=v, render=False) 319 | 320 | def __iter__(self): 321 | return (cell for cell in self.cells) 322 | 323 | @property 324 | def body(self): 325 | # return self.browser.find_element_by_tag_name("body") 326 | return self.browser.find_element_by_xpath(".//*[@id='main']") 327 | 328 | @property 329 | def cells(self): 330 | """Gets all cells once they are visible. 331 | """ 332 | # For SOS note book, there are 2 extra cells, one is the selection box for kernel, the other is the preview panel 333 | 334 | return list( 335 | self.browser.find_elements_by_xpath( 336 | ".//div[contains(@class,'jp-Notebook-cell')]")) 337 | # ".//div[contains(@class,'jp-InputArea-editor')]")) 338 | 339 | @property 340 | def panel_cells(self): 341 | return list( 342 | self.browser.find_elements_by_css_selector("div .jp-Console-cell")) 343 | 344 | @property 345 | def current_index(self): 346 | return self.index(self.current_cell) 347 | 348 | @property 349 | def menu_buttons(self): 350 | return self.browser.find_elements_by_css_selector("div .p-MenuBar li") 351 | 352 | def index(self, cell): 353 | return self.cells.index(cell) 354 | 355 | def save(self, name=''): 356 | if name: 357 | self.browser.execute_script( 358 | f"Jupyter.notebook.set_notebook_name(arguments[0])", name) 359 | time.sleep(5) 360 | return execute_promise('Jupyter.notebook.save_notebook()', self.browser) 361 | 362 | # 363 | # operation 364 | # 365 | 366 | def append_cell(self, *values, cell_type="code"): 367 | for i, value in enumerate(values): 368 | if isinstance(value, str): 369 | self.add_cell(cell_type=cell_type, content=value) 370 | else: 371 | raise TypeError("Don't know how to add cell from %r" % value) 372 | 373 | def add_cell(self, index=-1, cell_type="code", content=""): 374 | self._focus_cell(index) 375 | # self._to_command_mode() 376 | 377 | addButton = self.browser.find_element_by_xpath( 378 | './/div[contains(@class,"jp-NotebookPanel-toolbar")]//button[contains(@title,"Insert a cell below")]' 379 | ) 380 | ActionChains(self.browser).move_to_element(addButton).click().perform() 381 | 382 | new_index = index + 1 if index >= 0 else index 383 | 384 | # self.current_cell=self.cells[new_index] 385 | self._focus_cell(new_index) 386 | if content: 387 | self.edit_cell(index=new_index, content=content) 388 | if cell_type != 'code': 389 | self._convert_cell_type(index=new_index, cell_type=cell_type) 390 | 391 | def select_kernel(self, index=0, kernel_name="SoS", by_click=True): 392 | self._focus_cell(index) 393 | kernel_selector = 'option[value={}]'.format(kernel_name) 394 | kernelList = self.current_cell.find_element_by_tag_name("select") 395 | kernel = wait_for_selector(kernelList, kernel_selector, single=True) 396 | if by_click: 397 | kernel.click() 398 | else: 399 | self.edit_cell( 400 | index=0, content="%use {}".format(kernel_name), render=True) 401 | 402 | def edit_cell(self, cell=None, index=0, content="", render=False): 403 | """Set the contents of a cell to *content*, by cell object or by index 404 | """ 405 | if cell is not None: 406 | index = self.index(cell) 407 | # print("begin edit cell",index,dedent(content)) 408 | # Select & delete anything already in the cell 409 | # ActionChains(self.browser).move_to_element(self.current_cell).send_keys(Keys.ENTER).perform() 410 | ActionChains(self.browser).send_keys_to_element(self.current_cell, 411 | Keys.DELETE).perform() 412 | 413 | ActionChains(self.browser).move_to_element(self.current_cell).send_keys( 414 | dedent(content)).perform() 415 | # print("change content",index) 416 | 417 | if render: 418 | self.execute_cell(self.current_index) 419 | 420 | # 421 | # Get info 422 | # 423 | def get_kernel_list(self): 424 | # kernelMenu = self.browser.find_element_by_id( 425 | # "menu-change-kernel-submenu") 426 | wait_for_selector(self.browser, 427 | 'div[title="Kernel Idle"]') 428 | kernelMenu = self.cells[0].find_element_by_class_name( 429 | "jp-CelllanguageDropDown") 430 | kernelEntries = kernelMenu.find_elements_by_tag_name("option") 431 | kernels = [] 432 | for kernelEntry in kernelEntries: 433 | kernels.append(kernelEntry.text) 434 | return kernels 435 | 436 | def get_input_backgroundColor(self, index=0, in_console=False): 437 | if in_console: 438 | rgba = self.current_cell.find_element_by_class_name( 439 | "jp-InputPrompt").value_of_css_property("background-color") 440 | else: 441 | self._focus_cell(index) 442 | rgba = self.current_cell.find_element_by_class_name( 443 | "jp-InputPrompt").value_of_css_property("background-color") 444 | 445 | r, g, b, a = map( 446 | int, 447 | re.search(r'rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+)', rgba).groups()) 448 | return [r, g, b] 449 | 450 | def get_output_backgroundColor(self, index=0): 451 | 452 | rgba = self.current_cell.find_element_by_class_name( 453 | "jp-OutputPrompt").value_of_css_property("background-color") 454 | r, g, b, a = map( 455 | int, 456 | re.search(r'rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+)', rgba).groups()) 457 | return [r, g, b] 458 | 459 | # 460 | # Execution of cells 461 | # 462 | 463 | def click_on_run_menu(self, index, selector): 464 | 465 | for button in self.menu_buttons: 466 | if button.text == "Run": 467 | if index != -1: 468 | self._focus_cell(index) 469 | ActionChains( 470 | self.browser).move_to_element(button).click().perform() 471 | # time.sleep(5) 472 | runButton = self.browser.find_element_by_css_selector(selector) 473 | ActionChains( 474 | self.browser).move_to_element(runButton).click().perform() 475 | 476 | def execute_cell(self, 477 | cell_or_index=None, 478 | expect_error=False, 479 | in_console=False): 480 | if not in_console: 481 | if isinstance(cell_or_index, int): 482 | index = cell_or_index 483 | elif isinstance(cell_or_index, WebElement): 484 | index = self.index(cell_or_index) 485 | else: 486 | raise TypeError( 487 | "execute_cell only accepts a WebElement or an int") 488 | self._focus_cell(index) 489 | runButton = self.browser.find_element_by_xpath( 490 | './/div[contains(@class,"jp-NotebookPanel-toolbar")]//button[contains(@title,"Run the selected cells and advance")]' 491 | ) 492 | ActionChains( 493 | self.browser).move_to_element(runButton).click().perform() 494 | # self.current_cell.send_keys(Keys.CONTROL, Keys.ENTER) 495 | # ActionChains(self.browser).move_to_element(self.current_cell).click().send_keys(Keys.CONTROL, Keys.ENTER).perform() 496 | self._wait_for_done(index, expect_error) 497 | else: 498 | print("run in console") 499 | self.click_on_run_menu( 500 | cell_or_index, 501 | 'div.p-MenuBar-menu [data-command="notebook:run-in-console"]') 502 | 503 | def call(self, content="", kernel="SoS", expect_error=False): 504 | ''' 505 | Append a codecell to the end of the notebook, with specified `content` and 506 | `kernel`, execute it, waits for the completion of execution, and raise an 507 | exception if there is any error (stderr message), unless `expect_error` is 508 | set to `True`. This function returns the index of the cell, which can be 509 | used to retrieve output. Note that the `content` will be automatically 510 | dedented. 511 | ''' 512 | # there will be at least a new cell from the new notebook. 513 | index = len(self.cells) 514 | self.add_cell( 515 | index=index - 1, cell_type="code", content=dedent(content)) 516 | self.select_kernel(index=index, kernel_name=kernel, by_click=True) 517 | self.execute_cell(cell_or_index=index, expect_error=expect_error) 518 | return index 519 | 520 | def check_output(self, 521 | content='', 522 | kernel="SoS", 523 | expect_error=False, 524 | selector=None, 525 | attribute='src'): 526 | ''' 527 | This function calls call and gets its output with get_cell_output. 528 | ''' 529 | return self.get_cell_output( 530 | self.call(content, kernel, expect_error), 531 | selector=selector, 532 | attribute=attribute) 533 | 534 | # 535 | # check output 536 | # 537 | 538 | def get_cell_output(self, 539 | index=0, 540 | in_console=False, 541 | selector=None, 542 | attribute='src'): 543 | outputs = "" 544 | if in_console: 545 | wait_for_selector(self.browser, "div .jp-Console-cell") 546 | outputs = self.browser.find_elements_by_css_selector( 547 | "div .jp-Console-cell .jp-Cell-outputWrapper" 548 | )[index].find_elements_by_css_selector("div .jp-OutputArea-output") 549 | else: 550 | outputs = self.cells[index].find_elements_by_css_selector( 551 | ".jp-OutputArea-output") 552 | output_text = "" 553 | has_error = False 554 | for output in outputs: 555 | if selector: 556 | try: 557 | # some div might not have img 558 | elem = output.find_element_by_css_selector(selector) 559 | output_text += elem.get_attribute(attribute) + '\n' 560 | except NoSuchElementException: 561 | pass 562 | 563 | output_text += output.text + "\n" 564 | # if "Out" in output_text: 565 | # output_text = "".join(output_text.split(":")[1:]) 566 | 567 | return output_text.strip() 568 | 569 | # 570 | # For console panel 571 | # 572 | def is_console_panel_open(self): 573 | try: 574 | self.browser.find_element_by_css_selector("div .jp-ConsolePanel") 575 | return True 576 | except NoSuchElementException: 577 | return False 578 | 579 | def open_console(self): 580 | if not self.is_console_panel_open(): 581 | ActionChains(self.browser).move_to_element( 582 | self.body).context_click().send_keys(Keys.DOWN).send_keys( 583 | Keys.DOWN).send_keys(Keys.DOWN).send_keys( 584 | Keys.DOWN).send_keys(Keys.DOWN).send_keys( 585 | Keys.DOWN).send_keys(Keys.DOWN).click().perform() 586 | 587 | def toggle_console_panel(self): 588 | if self.is_console_panel_open(): 589 | panelButton = self.browser.find_elements_by_css_selector( 590 | "div .p-TabBar-tabCloseIcon")[-1] 591 | ActionChains( 592 | self.browser).move_to_element(panelButton).click().perform() 593 | else: 594 | ActionChains(self.browser).move_to_element( 595 | self.body).context_click().send_keys(Keys.DOWN).send_keys( 596 | Keys.DOWN).send_keys(Keys.DOWN).send_keys( 597 | Keys.DOWN).send_keys(Keys.DOWN).send_keys( 598 | Keys.DOWN).send_keys(Keys.DOWN).click().perform() 599 | wait_for_selector(self.browser, "div .jp-ConsolePanel", timeout=120) 600 | 601 | def edit_prompt_cell(self, 602 | content, 603 | kernel='SoS', 604 | execute=False, 605 | expect_error=False): 606 | # # print("panel", self.prompt_cell.get_attribute("innerHTML")) 607 | # self.browser.execute_script("window.my_panel.cell.set_text(" + 608 | # repr(dedent(content)) + ")") 609 | 610 | # # the div is not clickable so I use send_key to get around it 611 | # self.prompt_cell.send_keys('\n') 612 | wait_for_selector(self.browser, "div.jp-CodeConsole-input") 613 | self.prompt_cell = self.browser.find_element_by_css_selector( 614 | "div.jp-CodeConsole-input") 615 | ActionChains(self.browser).move_to_element( 616 | self.prompt_cell).click().send_keys(content).perform() 617 | 618 | self.select_console_kernel(kernel) 619 | # self.prompt_cell.find_element_by_css_selector('.CodeMirror').click() 620 | 621 | if execute: 622 | ActionChains(self.browser).move_to_element( 623 | self.prompt_cell).click().perform() 624 | self.click_on_run_menu( 625 | -1, 'div.p-MenuBar-menu [data-command="runmenu:run"]') 626 | self._wait_for_done(-1, expect_error=expect_error) 627 | 628 | def send_keys_on_prompt_cell(self, *argv): 629 | ActionChains(self.browser).move_to_element( 630 | self.prompt_cell).click().send_keys(argv).perform() 631 | 632 | def get_prompt_content(self): 633 | # JS = 'return window.my_panel.cell.get_text();' 634 | # return self.browser.execute_script(JS) 635 | promptText = self.prompt_cell.find_element_by_class_name( 636 | "jp-InputArea-editor").text 637 | print(promptText) 638 | return promptText 639 | 640 | def select_console_kernel(self, kernel_name="SoS"): 641 | kernel_selector = 'option[value={}]'.format(kernel_name) 642 | kernelList = self.prompt_cell.find_element_by_tag_name("select") 643 | kernel = wait_for_selector(kernelList, kernel_selector, single=True) 644 | kernel.click() 645 | 646 | @classmethod 647 | def new_notebook(cls, browser, kernel_name='kernel-sos'): 648 | with new_window(browser, selector=".cell"): 649 | select_kernel(browser, kernel_name=kernel_name) 650 | return cls(browser) 651 | 652 | # 653 | # PRIVATE FUNCTIONS 654 | # 655 | 656 | def _disable_autosave_and_onbeforeunload(self): 657 | """Disable request to save before closing window and autosave. 658 | 659 | This is most easily done by using js directly. 660 | """ 661 | self.browser.execute_script("window.onbeforeunload = null;") 662 | # self.browser.execute_script("Jupyter.notebook.set_autosave_interval(0)") 663 | 664 | def _to_command_mode(self): 665 | """Changes us into command mode on currently focused cell 666 | 667 | """ 668 | # self.body.send_keys(Keys.ESCAPE) 669 | ActionChains(self.browser).move_to_element(self.body).send_keys( 670 | Keys.ESCAPE).perform() 671 | 672 | # self.browser.execute_script( 673 | # "return Jupyter.notebook.handle_command_mode(" 674 | # "Jupyter.notebook.get_cell(" 675 | # "Jupyter.notebook.get_edit_index()))") 676 | 677 | def _focus_cell(self, index=0): 678 | cell = self.cells[index] 679 | ActionChains(self.browser).move_to_element(cell).click().perform() 680 | self.current_cell = cell 681 | # print("focus cell",index) 682 | 683 | # print(self.current_cell) 684 | 685 | def _convert_cell_type(self, index=0, cell_type="code"): 686 | # TODO add check to see if it is already present 687 | self._focus_cell(index) 688 | cell = self.cells[index] 689 | if cell_type == "markdown": 690 | self.current_cell.send_keys("m") 691 | elif cell_type == "raw": 692 | self.current_cell.send_keys("r") 693 | elif cell_type == "code": 694 | self.current_cell.send_keys("y") 695 | else: 696 | raise CellTypeError( 697 | ("{} is not a valid cell type," 698 | "use 'code', 'markdown', or 'raw'").format(cell_type)) 699 | 700 | # self.wait_for_stale_cell(cell) 701 | self._focus_cell(index) 702 | return self.current_cell 703 | 704 | def _wait_for_done(self, index, expect_error=False): 705 | # 706 | # index < 0 means console panel 707 | while True: 708 | # main notebook 709 | if index >= 0: 710 | prompt = self.cells[index].find_element_by_css_selector( 711 | '.jp-InputArea-prompt').text 712 | else: 713 | if len(self.panel_cells) > 0: 714 | prompt = self.panel_cells[-1].find_element_by_css_selector( 715 | '.jp-InputArea-prompt').text 716 | else: 717 | prompt = "" 718 | if '*' not in prompt: 719 | break 720 | else: 721 | time.sleep(0.1) 722 | # check if there is output 723 | try: 724 | # no output? OK. 725 | # outputs = self.cells[index].find_elements_by_xpath(".//div[contains(@class,'jp-OutputArea-output')]") 726 | outputs = self.cells[index].find_elements_by_css_selector( 727 | ".jp-OutputArea-output") 728 | except NoSuchElementException: 729 | return 730 | # 731 | has_error = False 732 | for output in outputs: 733 | try: 734 | # errors = output.find_element_by_css_selector('.output_stderr') 735 | mime = output.get_attribute('data-mime-type') 736 | if mime == "application/vnd.jupyter.stderr": 737 | if expect_error: 738 | has_error = True 739 | else: 740 | raise ValueError( 741 | f'Cell produces error message: {output.text}. Use expect_error=True to suppress this error if needed.' 742 | ) 743 | except NoSuchElementException: 744 | # if no error, ok 745 | pass 746 | # 747 | if expect_error and not has_error: 748 | raise ValueError( 749 | 'Expect an error message from cell output, none found.') 750 | 751 | # def wait_for_output(self, index=0): 752 | # time.sleep(10) 753 | # return self.get_cell_output(index) 754 | 755 | # def set_cell_metadata(self, index, key, value): 756 | # JS = 'Jupyter.notebook.get_cell({}).metadata.{} = {}'.format( 757 | # index, key, value) 758 | # return self.browser.execute_script(JS) 759 | 760 | # def get_cell_type(self, index=0): 761 | # JS = 'return Jupyter.notebook.get_cell({}).cell_type'.format(index) 762 | # return self.browser.execute_script(JS) 763 | 764 | # def set_cell_input_prompt(self, index, prmpt_val): 765 | # JS = 'Jupyter.notebook.get_cell({}).set_input_prompt({})'.format( 766 | # index, prmpt_val) 767 | # self.browser.execute_script(JS) 768 | 769 | # def delete_cell(self, index): 770 | # self._focus_cell(index) 771 | # self._to_command_mode() 772 | # self.current_cell.send_keys('dd') 773 | 774 | # def add_markdown_cell(self, index=-1, content="", render=True): 775 | # self.add_cell(index, cell_type="markdown") 776 | # self.edit_cell(index=index, content=content, render=render) 777 | 778 | # def extend(self, values): 779 | # self.append_cell(*values) 780 | 781 | # def run_all(self): 782 | # for cell in self: 783 | # self.execute_cell(cell) 784 | 785 | # def trigger_keydown(self, keys): 786 | # trigger_keystrokes(self.body, keys) 787 | 788 | # def add_and_execute_cell(self, index=-1, cell_type="code", content=""): 789 | # self.add_cell(index=index, cell_type=cell_type, content=content) 790 | # self.execute_cell(index) 791 | 792 | # def add_and_execute_cell_in_kernel(self, index=-1, cell_type="code", content="", kernel="SoS"): 793 | # self.add_cell(index=index, cell_type=cell_type, content=content) 794 | # self.select_kernel(index=index+1, kernel_name=kernel, by_click=True) 795 | # self.execute_cell(cell_or_index=index+1) 796 | 797 | # def select_cell_range(self, initial_index=0, final_index=0): 798 | # self._focus_cell(initial_index) 799 | # self._to_command_mode() 800 | # for i in range(final_index - initial_index): 801 | # shift(self.browser, 'j') 802 | 803 | # def find_and_replace(self, index=0, find_txt='', replace_txt=''): 804 | # self._focus_cell(index) 805 | # self._to_command_mode() 806 | # self.body.send_keys('f') 807 | # wait_for_selector(self.browser, "#find-and-replace", single=True) 808 | # self.browser.find_element_by_id("findreplace_allcells_btn").click() 809 | # self.browser.find_element_by_id( 810 | # "findreplace_find_inp").send_keys(find_txt) 811 | # self.browser.find_element_by_id( 812 | # "findreplace_replace_inp").send_keys(replace_txt) 813 | # self.browser.find_element_by_id("findreplace_replaceall_btn").click() 814 | 815 | # def wait_for_stale_cell(self, cell): 816 | # """ This is needed to switch a cell's mode and refocus it, or to render it. 817 | 818 | # Warning: there is currently no way to do this when changing between 819 | # markdown and raw cells. 820 | # """ 821 | # wait = WebDriverWait(self.browser, 10) 822 | # element = wait.until(EC.staleness_of(cell)) 823 | 824 | # def get_cells_contents(self): 825 | # JS = 'return Jupyter.notebook.get_cells().map(function(c) {return c.get_text();})' 826 | # return self.browser.execute_script(JS) 827 | 828 | # def get_cell_contents(self, index=0, selector='div .CodeMirror-code'): 829 | # return self.cells[index].find_element_by_css_selector(selector).text 830 | 831 | 832 | # def select_kernel(browser, kernel_name='kernel-sos'): 833 | # """Clicks the "new" button and selects a kernel from the options. 834 | # """ 835 | # wait = WebDriverWait(browser, 10) 836 | # new_button = wait.until( 837 | # EC.element_to_be_clickable((By.ID, "new-dropdown-button"))) 838 | # new_button.click() 839 | # kernel_selector = '#{} a'.format(kernel_name) 840 | # kernel = wait_for_selector(browser, kernel_selector, single=True) 841 | # kernel.click() 842 | 843 | 844 | def select_kernel(browser, kernel_name='kernel-sos'): 845 | """Clicks the "new" button and selects a kernel from the options. 846 | """ 847 | wait = WebDriverWait(browser, 10) 848 | new_button = wait.until( 849 | # EC.element_to_be_clickable((By.XPATH, "//img[@ src='http://localhost:8888/kernelspecs/sos/logo-64x64.png']/.."))) 850 | EC.element_to_be_clickable(( 851 | By.XPATH, 852 | ".//*[@data-category='Notebook']//div[contains(string(),'SoS')]/..") 853 | )) 854 | # new_button.click() 855 | browser.execute_script("arguments[0].click();", new_button) 856 | 857 | # kernel_selector = '#{} a'.format(kernel_name) 858 | # kernel = wait_for_selector(browser, kernel_selector, single=True) 859 | # kernel.click() 860 | 861 | 862 | @contextmanager 863 | def new_window(browser, selector=None): 864 | """Contextmanager for switching to & waiting for a window created. 865 | 866 | This context manager gives you the ability to create a new window inside 867 | the created context and it will switch you to that new window. 868 | 869 | If you know a CSS selector that can be expected to appear on the window, 870 | then this utility can wait on that selector appearing on the page before 871 | releasing the context. 872 | 873 | Usage example: 874 | 875 | from notebook.tests.selenium.utils import new_window, Notebook 876 | 877 | ⋮ # something that creates a browser object 878 | 879 | with new_window(browser, selector=".cell"): 880 | select_kernel(browser, kernel_name=kernel_name) 881 | nb = Notebook(browser) 882 | 883 | """ 884 | initial_window_handles = browser.window_handles 885 | try: 886 | yield 887 | new_window_handle = next(window for window in browser.window_handles 888 | if window not in initial_window_handles) 889 | except StopIteration: 890 | return 891 | browser.switch_to.window(new_window_handle) 892 | if selector is not None: 893 | wait_for_selector(browser, selector) 894 | 895 | 896 | def shift(browser, k): 897 | """Send key combination Shift+(k)""" 898 | trigger_keystrokes(browser, "shift-%s" % k) 899 | 900 | 901 | def ctrl(browser, k): 902 | """Send key combination Ctrl+(k)""" 903 | trigger_keystrokes(browser, "control-%s" % k) 904 | 905 | 906 | def command(browser, k): 907 | trigger_keystrokes(browser, "command-%s" % k) 908 | 909 | 910 | def trigger_keystrokes(browser, *keys): 911 | """ Send the keys in sequence to the browser. 912 | Handles following key combinations 913 | 1. with modifiers eg. 'control-alt-a', 'shift-c' 914 | 2. just modifiers eg. 'alt', 'esc' 915 | 3. non-modifiers eg. 'abc' 916 | Modifiers : http://seleniumhq.github.io/selenium/docs/api/py/webdriver/selenium.webdriver.common.keys.html 917 | """ 918 | for each_key_combination in keys: 919 | keys = each_key_combination.split('-') 920 | if len(keys) > 1: # key has modifiers eg. control, alt, shift 921 | modifiers_keys = [getattr(Keys, x.upper()) for x in keys[:-1]] 922 | ac = ActionChains(browser) 923 | for i in modifiers_keys: 924 | ac = ac.key_down(i) 925 | ac.send_keys(keys[-1]) 926 | for i in modifiers_keys[::-1]: 927 | ac = ac.key_up(i) 928 | ac.perform() 929 | else: # single key stroke. Check if modifier eg. "up" 930 | browser.send_keys(getattr(Keys, keys[0].upper(), keys[0])) 931 | 932 | 933 | @pytest.mark.usefixtures("notebook") 934 | class NotebookTest: 935 | pass 936 | -------------------------------------------------------------------------------- /test/test_workflow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) Bo Peng and the University of Texas MD Anderson Cancer Center 4 | # Distributed under the terms of the 3-clause BSD License. 5 | 6 | 7 | 8 | from test_utils import NotebookTest 9 | 10 | 11 | class TestWorkflow(NotebookTest): 12 | 13 | def test_no_output(self, notebook): 14 | '''Test no output from workflow cell''' 15 | assert not notebook.check_output(''' 16 | [1] 17 | print('hellp world') 18 | ''', kernel='SoS') 19 | 20 | def test_task(self, notebook): 21 | '''Test the execution of tasks with -s force''' 22 | output = notebook.check_output('''\ 23 | %run -s force 24 | [10] 25 | input: for_each={'i': range(1)} 26 | task: queue='localhost' 27 | python: expand=True 28 | import time 29 | print("this is {i}") 30 | time.sleep({i}) 31 | 32 | [20] 33 | input: for_each={'i': range(2)} 34 | task: queue='localhost' 35 | python: expand=True 36 | import time 37 | print("this aa is {i}") 38 | time.sleep({i}) 39 | ''', kernel='SoS') 40 | assert "Ran for < 5 seconds" in output and 'completed' in output 41 | 42 | 43 | def test_background_mode(self, notebook): 44 | '''test executing sos workflows in background''' 45 | idx = notebook.call('''\ 46 | %run & 47 | import time 48 | for i in range(5): 49 | print('output {}'.format(i));time.sleep(1) 50 | ''', kernel='SoS') 51 | output = notebook.get_cell_output(idx) 52 | assert 'output 4' not in output 53 | import time 54 | time.sleep(10) 55 | output = notebook.get_cell_output(idx) 56 | assert 'output 4' in output 57 | 58 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "incremental": true, 8 | "jsx": "react", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noUnusedLocals": true, 13 | "preserveWatchOutput": true, 14 | "resolveJsonModule": true, 15 | "outDir": "lib", 16 | "rootDir": "src", 17 | "strict": false, 18 | "target": "ES2018" 19 | }, 20 | "include": ["src/*"] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "types": ["jest"] 5 | } 6 | } 7 | --------------------------------------------------------------------------------