├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ └── check-release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── RELEASE.md ├── frz-jupyterlab-variableinspector ├── __init__.py └── _version.py ├── install.json ├── package-lock.json ├── package.json ├── pyproject.toml ├── setup.py ├── src ├── LICENCE.md ├── handler.ts ├── index.ts ├── inspectorscripts.ts ├── kernelconnector.ts ├── manager.ts └── variableinspector.ts ├── style ├── base.css ├── index.css └── index.js ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | **/*.d.ts 5 | tests 6 | 7 | **/__tests__ 8 | ui-tests 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/eslint-recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:prettier/recommended' 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | project: 'tsconfig.json', 11 | sourceType: 'module' 12 | }, 13 | plugins: ['@typescript-eslint'], 14 | rules: { 15 | '@typescript-eslint/naming-convention': [ 16 | 'error', 17 | { 18 | selector: 'interface', 19 | format: ['PascalCase'], 20 | custom: { 21 | regex: '^I[A-Z]', 22 | match: true 23 | } 24 | } 25 | ], 26 | '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], 27 | '@typescript-eslint/no-explicit-any': 'off', 28 | '@typescript-eslint/no-namespace': 'off', 29 | '@typescript-eslint/no-use-before-define': 'off', 30 | '@typescript-eslint/quotes': [ 31 | 'error', 32 | 'single', 33 | { avoidEscape: true, allowTemplateLiterals: false } 34 | ], 35 | curly: ['error', 'all'], 36 | eqeqeq: 'error', 37 | 'prefer-arrow-callback': 'error' 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: '*' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Base Setup 18 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 19 | 20 | - name: Install dependencies 21 | run: python -m pip install -U jupyterlab~=3.1 check-manifest 22 | 23 | - name: Lint the extension 24 | run: | 25 | set -eux 26 | jlpm 27 | jlpm run lint:check 28 | 29 | - name: Build the extension 30 | run: | 31 | set -eux 32 | python -m pip install .[test] 33 | 34 | jupyter labextension list 35 | jupyter labextension list 2>&1 | grep -ie "jupyterlab-variableinspector.*OK" 36 | python -m jupyterlab.browser_check 37 | 38 | - name: Package the extension 39 | run: | 40 | set -eux 41 | check-manifest -v 42 | 43 | pip install build 44 | python -m build 45 | pip uninstall -y "frz-jupyterlab-variableinspector" jupyterlab 46 | 47 | - name: Upload extension packages 48 | uses: actions/upload-artifact@v2 49 | with: 50 | name: extension-artifacts 51 | path: dist/frz-jupyterlab-variableinspector* 52 | if-no-files-found: error 53 | 54 | test_isolated: 55 | needs: build 56 | runs-on: ubuntu-latest 57 | 58 | steps: 59 | - name: Checkout 60 | uses: actions/checkout@v2 61 | - name: Install Python 62 | uses: actions/setup-python@v2 63 | with: 64 | python-version: '3.9' 65 | architecture: 'x64' 66 | - uses: actions/download-artifact@v2 67 | with: 68 | name: extension-artifacts 69 | - name: Install and Test 70 | run: | 71 | set -eux 72 | # Remove NodeJS, twice to take care of system and locally installed node versions. 73 | sudo rm -rf $(which node) 74 | sudo rm -rf $(which node) 75 | 76 | pip install "jupyterlab~=3.1" frz-jupyterlab-variableinspector*.whl 77 | 78 | 79 | jupyter labextension list 80 | jupyter labextension list 2>&1 | grep -ie "jupyterlab-variableinspector.*OK" 81 | python -m jupyterlab.browser_check --no-chrome-test 82 | 83 | -------------------------------------------------------------------------------- /.github/workflows/check-release.yml: -------------------------------------------------------------------------------- 1 | name: Check Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | check_release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | - name: Install Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: 3.9 23 | architecture: 'x64' 24 | - name: Install node 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: '14.x' 28 | 29 | 30 | - name: Get pip cache dir 31 | id: pip-cache 32 | run: | 33 | echo "::set-output name=dir::$(pip cache dir)" 34 | - name: Cache pip 35 | uses: actions/cache@v1 36 | with: 37 | path: ${{ steps.pip-cache.outputs.dir }} 38 | key: ${{ runner.os }}-pip-${{ hashFiles('package.json') }} 39 | restore-keys: | 40 | ${{ runner.os }}-pip- 41 | - name: Cache checked links 42 | uses: actions/cache@v2 43 | with: 44 | path: ~/.cache/pytest-link-check 45 | key: ${{ runner.os }}-linkcheck-${{ hashFiles('**/.md') }}-md-links 46 | restore-keys: | 47 | ${{ runner.os }}-linkcheck- 48 | - name: Upgrade packaging dependencies 49 | run: | 50 | pip install --upgrade pip setuptools wheel jupyter-packaging~=0.10 --user 51 | - name: Install Dependencies 52 | run: | 53 | pip install . 54 | - name: Check Release 55 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v1 56 | with: 57 | token: ${{ secrets.GITHUB_TOKEN }} 58 | 59 | - name: Upload Distributions 60 | uses: actions/upload-artifact@v2 61 | with: 62 | name: frz-jupyterlab-variableinspector-releaser-dist-${{ github.run_number }} 63 | path: .jupyter_releaser_checkout/dist 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | .eslintcache 5 | .stylelintcache 6 | *.egg-info/ 7 | .ipynb_checkpoints 8 | *.tsbuildinfo 9 | frz-jupyterlab-variableinspector/labextension 10 | 11 | # Integration tests 12 | ui-tests/test-results/ 13 | ui-tests/playwright-report/ 14 | 15 | # Created by https://www.gitignore.io/api/python 16 | # Edit at https://www.gitignore.io/?templates=python 17 | 18 | ### Python ### 19 | # Byte-compiled / optimized / DLL files 20 | __pycache__/ 21 | *.py[cod] 22 | *$py.class 23 | 24 | # C extensions 25 | *.so 26 | 27 | # Distribution / packaging 28 | .Python 29 | build/ 30 | develop-eggs/ 31 | dist/ 32 | downloads/ 33 | eggs/ 34 | .eggs/ 35 | lib/ 36 | lib64/ 37 | parts/ 38 | sdist/ 39 | var/ 40 | wheels/ 41 | pip-wheel-metadata/ 42 | share/python-wheels/ 43 | .installed.cfg 44 | *.egg 45 | MANIFEST 46 | 47 | # PyInstaller 48 | # Usually these files are written by a python script from a template 49 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 50 | *.manifest 51 | *.spec 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | 57 | # Unit test / coverage reports 58 | htmlcov/ 59 | .tox/ 60 | .nox/ 61 | .coverage 62 | .coverage.* 63 | .cache 64 | nosetests.xml 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 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | frz-jupyterlab-variableinspector 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "arrowParens": "avoid", 5 | "endOfLine": "auto" 6 | } 7 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended", 4 | "stylelint-config-standard", 5 | "stylelint-prettier/recommended" 6 | ], 7 | "rules": { 8 | "property-no-vendor-prefix": null, 9 | "selector-no-vendor-prefix": null, 10 | "value-no-vendor-prefix": null 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Feiran Zhang 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include *.md 3 | include pyproject.toml 4 | 5 | include package.json 6 | include install.json 7 | include ts*.json 8 | include yarn.lock 9 | 10 | graft frz-jupyterlab-variableinspector/labextension 11 | 12 | # Javascript files 13 | graft src 14 | graft style 15 | prune **/node_modules 16 | prune lib 17 | prune binder 18 | 19 | # Patterns to exclude from any directory 20 | global-exclude *~ 21 | global-exclude *.pyc 22 | global-exclude *.pyo 23 | global-exclude .git 24 | global-exclude .ipynb_checkpoints 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # frz-jupyterlab-variableinspector 2 | 3 | Forked from https://github.com/lckr/jupyterlab-variableInspector. 4 | 5 | Customized Variable Inspector extension for JupyterLab, better support for R (especially for big objects, also showing the .Last.value). 6 | 7 | If you define a variable named "jlvi_brief", variable contents will be skipped, which dramatically improve the speed when there are too many variables. 8 | 9 | 10 | ## Requirements 11 | 12 | - JupyterLab >= 3.0 13 | 14 | ## Install 15 | 16 | To install the extension, execute: 17 | 18 | ```bash 19 | pip install frz-jupyterlab-variableinspector 20 | ``` 21 | 22 | ## Uninstall 23 | 24 | To remove the extension, execute: 25 | 26 | ```bash 27 | pip uninstall frz-jupyterlab-variableinspector 28 | ``` 29 | 30 | ## Contributing 31 | 32 | ### Development install 33 | 34 | Note: You will need NodeJS to build the extension package. 35 | 36 | The `jlpm` command is JupyterLab's pinned version of 37 | [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use 38 | `yarn` or `npm` in lieu of `jlpm` below. 39 | 40 | ```bash 41 | # Clone the repo to your local environment 42 | # Change directory to the frz-jupyterlab-variableinspector directory 43 | # Install package in development mode 44 | pip install -e . 45 | # Link your development version of the extension with JupyterLab 46 | jupyter labextension develop . --overwrite 47 | # Rebuild extension Typescript source after making changes 48 | jlpm build 49 | ``` 50 | 51 | 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. 52 | 53 | ```bash 54 | # Watch the source directory in one terminal, automatically rebuilding when needed 55 | jlpm watch 56 | # Run JupyterLab in another terminal 57 | jupyter lab 58 | ``` 59 | 60 | 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). 61 | 62 | 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: 63 | 64 | ```bash 65 | jupyter lab build --minimize=False 66 | ``` 67 | 68 | ### Development uninstall 69 | 70 | ```bash 71 | pip uninstall frz-jupyterlab-variableinspector 72 | ``` 73 | 74 | In development mode, you will also need to remove the symlink created by `jupyter labextension develop` 75 | command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` 76 | folder is located. Then you can remove the symlink named `jupyterlab-variableinspector` within that folder. 77 | 78 | ### Packaging the extension 79 | 80 | See [RELEASE](RELEASE.md) 81 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a new release of frz-jupyterlab-variableinspector 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 10 | packages. All of the Python 11 | packaging instructions in the `pyproject.toml` file to wrap your extension in a 12 | Python package. Before generating a package, we first need to install `build`. 13 | 14 | ```bash 15 | pip install build twine 16 | ``` 17 | 18 | To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: 19 | 20 | ```bash 21 | python -m build 22 | ``` 23 | 24 | > `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. 25 | 26 | Then to upload the package to PyPI, do: 27 | 28 | ```bash 29 | twine upload dist/* 30 | ``` 31 | 32 | ### NPM package 33 | 34 | To publish the frontend part of the extension as a NPM package, do: 35 | 36 | ```bash 37 | npm login 38 | npm publish --access public 39 | ``` 40 | 41 | ## Automated releases with the Jupyter Releaser 42 | 43 | The extension repository should already be compatible with the Jupyter Releaser. 44 | 45 | Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. 46 | 47 | Here is a summary of the steps to cut a new release: 48 | 49 | - Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) 50 | - Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork 51 | - Go to the Actions panel 52 | - Run the "Draft Changelog" workflow 53 | - Merge the Changelog PR 54 | - Run the "Draft Release" workflow 55 | - Run the "Publish Release" workflow 56 | 57 | ## Publishing to `conda-forge` 58 | 59 | 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 60 | 61 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. 62 | -------------------------------------------------------------------------------- /frz-jupyterlab-variableinspector/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from ._version import __version__ 5 | 6 | 7 | HERE = Path(__file__).parent.resolve() 8 | 9 | 10 | with (HERE / "labextension" / "package.json").open() as fid: 11 | data = json.load(fid) 12 | 13 | 14 | def _jupyter_labextension_paths(): 15 | return [{ 16 | "src": "labextension", 17 | "dest": data["name"] 18 | }] 19 | 20 | -------------------------------------------------------------------------------- /frz-jupyterlab-variableinspector/_version.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | __all__ = ["__version__"] 5 | 6 | def _fetchVersion(): 7 | HERE = Path(__file__).parent.resolve() 8 | 9 | for settings in HERE.rglob("package.json"): 10 | try: 11 | with settings.open() as f: 12 | version = json.load(f)["version"] 13 | return ( 14 | version.replace("-alpha.", "a") 15 | .replace("-beta.", "b") 16 | .replace("-rc.", "rc") 17 | ) 18 | except FileNotFoundError: 19 | pass 20 | 21 | raise FileNotFoundError(f"Could not find package.json under dir {HERE!s}") 22 | 23 | __version__ = _fetchVersion() 24 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "frz-jupyterlab-variableinspector", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package frz-jupyterlab-variableinspector" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feiranzhang/jupyterlab-variableinspector", 3 | "version": "0.1.4", 4 | "description": "Customized Variable Inspector extension for JupyterLab.", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension", 9 | "variable inspector" 10 | ], 11 | "homepage": "https://github.com/zhangfeiran/jupyterlab-variableInspector", 12 | "bugs": { 13 | "url": "https://github.com/zhangfeiran/jupyterlab-variableInspector/issues" 14 | }, 15 | "license": "BSD-3-Clause", 16 | "author": { 17 | "name": "Feiran Zhang", 18 | "email": "feiranzhang@outlook.com" 19 | }, 20 | "files": [ 21 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 22 | "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" 23 | ], 24 | "main": "lib/index.js", 25 | "types": "lib/index.d.ts", 26 | "style": "style/index.css", 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/zhangfeiran/jupyterlab-variableInspector.git" 30 | }, 31 | "scripts": { 32 | "build": "jlpm build:lib && jlpm build:labextension:dev", 33 | "build:prod": "jlpm clean && jlpm build:lib && jlpm build:labextension", 34 | "build:labextension": "jupyter labextension build .", 35 | "build:labextension:dev": "jupyter labextension build --development True .", 36 | "build:lib": "tsc", 37 | "clean": "jlpm clean:lib", 38 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 39 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 40 | "clean:labextension": "rimraf frz-jupyterlab-variableinspector/labextension", 41 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", 42 | "eslint": "jlpm eslint:check --fix", 43 | "eslint:check": "eslint . --cache --ext .ts,.tsx", 44 | "install:extension": "jlpm build", 45 | "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", 46 | "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", 47 | "prettier": "jlpm prettier:base --write --list-different", 48 | "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", 49 | "prettier:check": "jlpm prettier:base --check", 50 | "stylelint": "jlpm stylelint:check --fix", 51 | "stylelint:check": "stylelint --cache \"style/**/*.css\"", 52 | "watch": "run-p watch:src watch:labextension", 53 | "watch:src": "tsc -w", 54 | "watch:labextension": "jupyter labextension watch ." 55 | }, 56 | "dependencies": { 57 | "@jupyterlab/application": "^3.0.16", 58 | "@jupyterlab/apputils": "^3.0.16", 59 | "@jupyterlab/console": "^3.0.16", 60 | "@jupyterlab/coreutils": "^5.0.7", 61 | "@jupyterlab/csvviewer": "^3.0.16", 62 | "@jupyterlab/notebook": "^3.0.16", 63 | "@jupyterlab/services": "^6.0.10", 64 | "@jupyterlab/ui-components": "^3.0.16", 65 | "@lumino/datagrid": "^0.32.0" 66 | }, 67 | "devDependencies": { 68 | "@jupyterlab/builder": "^3.0.16", 69 | "@typescript-eslint/eslint-plugin": "^4.8.1", 70 | "@typescript-eslint/parser": "^4.8.1", 71 | "eslint": "^7.14.0", 72 | "eslint-config-prettier": "^6.15.0", 73 | "eslint-plugin-prettier": "^3.1.4", 74 | "npm-run-all": "^4.1.5", 75 | "prettier": "^2.1.1", 76 | "rimraf": "^3.0.2", 77 | "stylelint": "^14.3.0", 78 | "stylelint-config-prettier": "^9.0.3", 79 | "stylelint-config-recommended": "^6.0.0", 80 | "stylelint-config-standard": "~24.0.0", 81 | "stylelint-prettier": "^2.0.0", 82 | "typescript": "~4.1.3" 83 | }, 84 | "sideEffects": [ 85 | "style/*.css", 86 | "style/index.js" 87 | ], 88 | "styleModule": "style/index.js", 89 | "publishConfig": { 90 | "access": "public" 91 | }, 92 | "jupyterlab": { 93 | "extension": true, 94 | "outputDir": "frz-jupyterlab-variableinspector/labextension" 95 | }, 96 | "jupyter-releaser": { 97 | "hooks": { 98 | "before-build-npm": [ 99 | "python -m pip install jupyterlab~=3.1", 100 | "jlpm" 101 | ], 102 | "before-build-python": [ 103 | "jlpm clean:all" 104 | ] 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] 3 | build-backend = "jupyter_packaging.build_api" 4 | 5 | [tool.jupyter-packaging.options] 6 | skip-if-exists = ["frz-jupyterlab-variableinspector/labextension/static/style.js"] 7 | ensured-targets = ["frz-jupyterlab-variableinspector/labextension/static/style.js", "frz-jupyterlab-variableinspector/labextension/package.json"] 8 | 9 | [tool.jupyter-packaging.builder] 10 | factory = "jupyter_packaging.npm_builder" 11 | 12 | [tool.jupyter-packaging.build-args] 13 | build_cmd = "build:prod" 14 | npm = ["jlpm"] 15 | 16 | [tool.check-manifest] 17 | ignore = ["frz-jupyterlab-variableinspector/labextension/**", "yarn.lock", ".*", "package-lock.json"] 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | frz-jupyterlab-variableinspector setup 3 | """ 4 | import json 5 | import sys 6 | from pathlib import Path 7 | 8 | import setuptools 9 | 10 | HERE = Path(__file__).parent.resolve() 11 | 12 | # Get the package info from package.json 13 | pkg_json = json.loads((HERE / "package.json").read_bytes()) 14 | 15 | # The name of the project 16 | name = "frz-jupyterlab-variableinspector" 17 | 18 | lab_path = (HERE / pkg_json["jupyterlab"]["outputDir"]) 19 | 20 | # Representative files that should exist after a successful build 21 | ensured_targets = [ 22 | str(lab_path / "package.json"), 23 | str(lab_path / "static/style.js") 24 | ] 25 | 26 | labext_name = pkg_json["name"] 27 | 28 | data_files_spec = [ 29 | ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), 30 | ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), 31 | ] 32 | 33 | long_description = (HERE / "README.md").read_text() 34 | 35 | version = ( 36 | pkg_json["version"] 37 | .replace("-alpha.", "a") 38 | .replace("-beta.", "b") 39 | .replace("-rc.", "rc") 40 | ) 41 | 42 | setup_args = dict( 43 | name=name, 44 | version=version, 45 | url=pkg_json["homepage"], 46 | author=pkg_json["author"]["name"], 47 | author_email=pkg_json["author"]["email"], 48 | description=pkg_json["description"], 49 | license=pkg_json["license"], 50 | license_file="LICENSE", 51 | long_description=long_description, 52 | long_description_content_type="text/markdown", 53 | packages=setuptools.find_packages(), 54 | zip_safe=False, 55 | include_package_data=True, 56 | python_requires=">=3.7", 57 | platforms="Linux, Mac OS X, Windows", 58 | keywords=["Jupyter", "JupyterLab", "JupyterLab3"], 59 | classifiers=[ 60 | "License :: OSI Approved :: BSD License", 61 | "Programming Language :: Python", 62 | "Programming Language :: Python :: 3", 63 | "Programming Language :: Python :: 3.7", 64 | "Programming Language :: Python :: 3.8", 65 | "Programming Language :: Python :: 3.9", 66 | "Programming Language :: Python :: 3.10", 67 | "Framework :: Jupyter", 68 | "Framework :: Jupyter :: JupyterLab", 69 | "Framework :: Jupyter :: JupyterLab :: 3", 70 | "Framework :: Jupyter :: JupyterLab :: Extensions", 71 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 72 | ], 73 | ) 74 | 75 | try: 76 | from jupyter_packaging import ( 77 | wrap_installers, 78 | npm_builder, 79 | get_data_files 80 | ) 81 | post_develop = npm_builder( 82 | build_cmd="install:extension", source_dir="src", build_dir=lab_path 83 | ) 84 | setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) 85 | setup_args["data_files"] = get_data_files(data_files_spec) 86 | except ImportError as e: 87 | import logging 88 | logging.basicConfig(format="%(levelname)s: %(message)s") 89 | logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") 90 | if not ("--name" in sys.argv or "--version" in sys.argv): 91 | raise e 92 | 93 | if __name__ == "__main__": 94 | setuptools.setup(**setup_args) 95 | -------------------------------------------------------------------------------- /src/LICENCE.md: -------------------------------------------------------------------------------- 1 | ==================================== 2 | The IPython-contrib licensing terms 3 | ==================================== 4 | 5 | IPython-contrib is licensed under the terms of the Modified BSD License (also 6 | known as New or Revised or 3-Clause BSD), as follows: 7 | 8 | - Copyright (c) 2013-2015, IPython-contrib Developers 9 | 10 | All rights reserved. 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modification, are permitted provided that the following conditions are met: 14 | 15 | Redistributions of source code must retain the above copyright notice, this 16 | list of conditions and the following disclaimer. 17 | 18 | Redistributions in binary form must reproduce the above copyright notice, this 19 | list of conditions and the following disclaimer in the documentation and/or 20 | other materials provided with the distribution. 21 | 22 | Neither the name of the IPython-contrib Developers nor the names of its 23 | contributors may be used to endorse or promote products derived from this 24 | software without specific prior written permission. 25 | 26 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 27 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 28 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 29 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 30 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 31 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 32 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 33 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 34 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | -------------------------------------------------------------------------------- /src/handler.ts: -------------------------------------------------------------------------------- 1 | ///* eslint-disable @typescript-eslint/camelcase */ 2 | import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; 3 | 4 | import { IDisposable } from '@lumino/disposable'; 5 | 6 | import { IVariableInspector } from './variableinspector'; 7 | 8 | import { KernelConnector } from './kernelconnector'; 9 | 10 | import { ISessionContext } from '@jupyterlab/apputils'; 11 | 12 | import { KernelMessage, Kernel } from '@jupyterlab/services'; 13 | 14 | import { Signal, ISignal } from '@lumino/signaling'; 15 | 16 | import { IExecuteResult } from '@jupyterlab/nbformat'; 17 | 18 | import { JSONModel, DataModel } from '@lumino/datagrid'; 19 | import { 20 | IExecuteInputMsg, 21 | IExecuteReplyMsg, 22 | IExecuteRequestMsg, 23 | } from '@jupyterlab/services/lib/kernel/messages'; 24 | 25 | /** 26 | * An object that handles code inspection. 27 | */ 28 | export class VariableInspectionHandler 29 | implements IDisposable, IVariableInspector.IInspectable { 30 | private _connector: KernelConnector; 31 | private _rendermime: IRenderMimeRegistry; 32 | private _initScript: string; 33 | private _queryCommand: string; 34 | private _matrixQueryCommand: string; 35 | private _widgetQueryCommand: string; 36 | private _deleteCommand: string; 37 | private _disposed = new Signal(this); 38 | private _inspected = new Signal< 39 | this, 40 | IVariableInspector.IVariableInspectorUpdate 41 | >(this); 42 | private _isDisposed = false; 43 | private _ready: Promise; 44 | private _id: string; 45 | 46 | constructor(options: VariableInspectionHandler.IOptions) { 47 | this._id = options.id; 48 | this._connector = options.connector; 49 | this._rendermime = options.rendermime; 50 | this._queryCommand = options.queryCommand; 51 | this._matrixQueryCommand = options.matrixQueryCommand; 52 | this._widgetQueryCommand = options.widgetQueryCommand; 53 | this._deleteCommand = options.deleteCommand; 54 | this._initScript = options.initScript; 55 | 56 | this._ready = this._connector.ready.then(() => { 57 | this._initOnKernel().then((msg: KernelMessage.IExecuteReplyMsg) => { 58 | this._connector.iopubMessage.connect(this._queryCall); 59 | return; 60 | }); 61 | }); 62 | 63 | this._connector.kernelRestarted.connect( 64 | (sender, kernelReady: Promise) => { 65 | const title: IVariableInspector.IVariableTitle = { 66 | contextName: 'Restarting kernel... ', 67 | }; 68 | this._inspected.emit({ 69 | title: title, 70 | payload: [], 71 | } as IVariableInspector.IVariableInspectorUpdate); 72 | 73 | this._ready = kernelReady.then(() => { 74 | this._initOnKernel().then((msg: KernelMessage.IExecuteReplyMsg) => { 75 | this._connector.iopubMessage.connect(this._queryCall); 76 | this.performInspection(); 77 | }); 78 | }); 79 | } 80 | ); 81 | } 82 | 83 | get id(): string { 84 | return this._id; 85 | } 86 | 87 | get rendermime(): IRenderMimeRegistry { 88 | return this._rendermime; 89 | } 90 | 91 | /** 92 | * A signal emitted when the handler is disposed. 93 | */ 94 | get disposed(): ISignal { 95 | return this._disposed; 96 | } 97 | 98 | get isDisposed(): boolean { 99 | return this._isDisposed; 100 | } 101 | 102 | get ready(): Promise { 103 | return this._ready; 104 | } 105 | 106 | /** 107 | * A signal emitted when an inspector value is generated. 108 | */ 109 | get inspected(): ISignal< 110 | VariableInspectionHandler, 111 | IVariableInspector.IVariableInspectorUpdate 112 | > { 113 | return this._inspected; 114 | } 115 | 116 | /** 117 | * Performs an inspection by sending an execute request with the query command to the kernel. 118 | */ 119 | public performInspection(): void { 120 | const content: KernelMessage.IExecuteRequestMsg['content'] = { 121 | code: this._queryCommand, 122 | stop_on_error: false, 123 | store_history: false, 124 | }; 125 | this._connector.fetch(content, this._handleQueryResponse); 126 | } 127 | 128 | /** 129 | * Performs an inspection of a Jupyter Widget 130 | */ 131 | public performWidgetInspection( 132 | varName: string 133 | ): Kernel.IShellFuture { 134 | const request: KernelMessage.IExecuteRequestMsg['content'] = { 135 | code: this._widgetQueryCommand + '(' + varName + ')', 136 | stop_on_error: false, 137 | store_history: false, 138 | }; 139 | return this._connector.execute(request); 140 | } 141 | 142 | /** 143 | * Performs an inspection of the specified matrix. 144 | */ 145 | public performMatrixInspection( 146 | varName: string, 147 | maxRows = 100000 148 | ): Promise { 149 | const request: KernelMessage.IExecuteRequestMsg['content'] = { 150 | code: this._matrixQueryCommand + '(' + varName + ', ' + maxRows + ')', 151 | stop_on_error: false, 152 | store_history: false, 153 | }; 154 | const con = this._connector; 155 | return new Promise((resolve, reject) => { 156 | con.fetch(request, (response: KernelMessage.IIOPubMessage) => { 157 | const msgType = response.header.msg_type; 158 | switch (msgType) { 159 | case 'execute_result': { 160 | const payload = response.content as IExecuteResult; 161 | let content: string = payload.data['text/plain'] as string; 162 | content = content.replace(/^'|'$/g, ''); 163 | content = content.replace(/\\"/g, '"'); 164 | content = content.replace(/\\'/g, "\\\\'"); 165 | 166 | const modelOptions = JSON.parse(content) as JSONModel.IOptions; 167 | const jsonModel = new JSONModel(modelOptions); 168 | resolve(jsonModel); 169 | break; 170 | } 171 | case 'error': 172 | console.log(response); 173 | reject("Kernel error on 'matrixQuery' call!"); 174 | break; 175 | default: 176 | break; 177 | } 178 | }); 179 | }); 180 | } 181 | 182 | /** 183 | * Send a kernel request to delete a variable from the global environment 184 | */ 185 | public performDelete(varName: string): void { 186 | const content: KernelMessage.IExecuteRequestMsg['content'] = { 187 | code: this._deleteCommand + "('" + varName + "')", 188 | stop_on_error: false, 189 | store_history: false, 190 | }; 191 | 192 | this._connector.fetch(content, this._handleQueryResponse); 193 | } 194 | 195 | /* 196 | * Disposes the kernel connector. 197 | */ 198 | dispose(): void { 199 | if (this.isDisposed) { 200 | return; 201 | } 202 | this._isDisposed = true; 203 | this._disposed.emit(void 0); 204 | Signal.clearData(this); 205 | } 206 | 207 | /** 208 | * Initializes the kernel by running the set up script located at _initScriptPath. 209 | */ 210 | private _initOnKernel(): Promise { 211 | const content: KernelMessage.IExecuteRequestMsg['content'] = { 212 | code: this._initScript, 213 | stop_on_error: false, 214 | silent: true, 215 | }; 216 | 217 | return this._connector.fetch(content, () => { 218 | //no op 219 | }); 220 | } 221 | 222 | /* 223 | * Handle query response. Emit new signal containing the IVariableInspector.IInspectorUpdate object. 224 | * (TODO: query resp. could be forwarded to panel directly) 225 | */ 226 | private _handleQueryResponse = ( 227 | response: KernelMessage.IIOPubMessage 228 | ): void => { 229 | const msgType = response.header.msg_type; 230 | switch (msgType) { 231 | case 'execute_result': { 232 | const payload = response.content as IExecuteResult; 233 | let content: string = payload.data['text/plain'] as string; 234 | if (content.slice(0, 1) === "'" || content.slice(0, 1) === '"') { 235 | content = content.slice(1, -1); 236 | content = content.replace(/\\"/g, '"').replace(/\\'/g, "'"); 237 | } 238 | 239 | const update = JSON.parse(content) as IVariableInspector.IVariable[]; 240 | const title = { 241 | contextName: '', 242 | kernelName: this._connector.kernelName || '', 243 | }; 244 | 245 | this._inspected.emit({ title: title, payload: update }); 246 | break; 247 | } 248 | case 'display_data': { 249 | const payloadDisplay = response.content as IExecuteResult; 250 | let contentDisplay: string = payloadDisplay.data[ 251 | 'text/plain' 252 | ] as string; 253 | if ( 254 | contentDisplay.slice(0, 1) === "'" || 255 | contentDisplay.slice(0, 1) === '"' 256 | ) { 257 | contentDisplay = contentDisplay.slice(1, -1); 258 | contentDisplay = contentDisplay 259 | .replace(/\\"/g, '"') 260 | .replace(/\\'/g, "'"); 261 | } 262 | 263 | const updateDisplay = JSON.parse( 264 | contentDisplay 265 | ) as IVariableInspector.IVariable[]; 266 | 267 | const titleDisplay = { 268 | contextName: '', 269 | kernelName: this._connector.kernelName || '', 270 | }; 271 | 272 | this._inspected.emit({ title: titleDisplay, payload: updateDisplay }); 273 | break; 274 | } 275 | default: 276 | break; 277 | } 278 | }; 279 | 280 | /* 281 | * Invokes a inspection if the signal emitted from specified session is an 'execute_input' msg. 282 | */ 283 | private _queryCall = ( 284 | sess: ISessionContext, 285 | msg: KernelMessage.IMessage 286 | ): void => { 287 | const msgType = msg.header.msg_type; 288 | switch (msgType) { 289 | case 'execute_input': { 290 | const code = (msg as IExecuteInputMsg).content.code; 291 | if ( 292 | !(code === this._queryCommand) && 293 | !(code === this._matrixQueryCommand) && 294 | !code.startsWith(this._widgetQueryCommand) 295 | ) { 296 | this.performInspection(); 297 | } 298 | break; 299 | } 300 | default: 301 | break; 302 | } 303 | }; 304 | } 305 | 306 | /** 307 | * A name space for inspection handler statics. 308 | */ 309 | export namespace VariableInspectionHandler { 310 | /** 311 | * The instantiation options for an inspection handler. 312 | */ 313 | export interface IOptions { 314 | connector: KernelConnector; 315 | rendermime?: IRenderMimeRegistry; 316 | queryCommand: string; 317 | matrixQueryCommand: string; 318 | widgetQueryCommand: string; 319 | deleteCommand: string; 320 | initScript: string; 321 | id: string; 322 | } 323 | } 324 | 325 | export class DummyHandler 326 | implements IDisposable, IVariableInspector.IInspectable { 327 | private _isDisposed = false; 328 | private _disposed = new Signal(this); 329 | private _inspected = new Signal< 330 | this, 331 | IVariableInspector.IVariableInspectorUpdate 332 | >(this); 333 | private _connector: KernelConnector; 334 | private _rendermime: IRenderMimeRegistry = null; 335 | 336 | constructor(connector: KernelConnector) { 337 | this._connector = connector; 338 | } 339 | 340 | get disposed(): ISignal { 341 | return this._disposed; 342 | } 343 | 344 | get isDisposed(): boolean { 345 | return this._isDisposed; 346 | } 347 | 348 | get inspected(): ISignal< 349 | DummyHandler, 350 | IVariableInspector.IVariableInspectorUpdate 351 | > { 352 | return this._inspected; 353 | } 354 | 355 | get rendermime(): IRenderMimeRegistry { 356 | return this._rendermime; 357 | } 358 | 359 | dispose(): void { 360 | if (this.isDisposed) { 361 | return; 362 | } 363 | this._isDisposed = true; 364 | this._disposed.emit(void 0); 365 | Signal.clearData(this); 366 | } 367 | 368 | public performInspection(): void { 369 | const title = { 370 | contextName: '. Language currently not supported. ', 371 | kernelName: this._connector.kernelName || '', 372 | } as IVariableInspector.IVariableTitle; 373 | this._inspected.emit({ 374 | title: title, 375 | payload: [], 376 | } as IVariableInspector.IVariableInspectorUpdate); 377 | } 378 | 379 | public performMatrixInspection( 380 | varName: string, 381 | maxRows: number 382 | ): Promise { 383 | return new Promise((resolve, reject) => { 384 | reject('Cannot inspect matrices w/ the DummyHandler!'); 385 | }); 386 | } 387 | 388 | public performWidgetInspection( 389 | varName: string 390 | ): Kernel.IShellFuture< 391 | KernelMessage.IExecuteRequestMsg, 392 | KernelMessage.IExecuteReplyMsg 393 | > { 394 | const request: KernelMessage.IExecuteRequestMsg['content'] = { 395 | code: '', 396 | stop_on_error: false, 397 | store_history: false, 398 | }; 399 | return this._connector.execute(request); 400 | } 401 | 402 | public performDelete(varName: string): void { 403 | //noop 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IVariableInspector, 3 | VariableInspectorPanel, 4 | } from './variableinspector'; 5 | 6 | import { KernelConnector } from './kernelconnector'; 7 | 8 | import { VariableInspectionHandler, DummyHandler } from './handler'; 9 | 10 | import { VariableInspectorManager, IVariableInspectorManager } from './manager'; 11 | 12 | import { Languages } from './inspectorscripts'; 13 | 14 | import { ICommandPalette, WidgetTracker } from '@jupyterlab/apputils'; 15 | 16 | import { 17 | ILabShell, 18 | ILayoutRestorer, 19 | JupyterFrontEnd, 20 | JupyterFrontEndPlugin, 21 | } from '@jupyterlab/application'; 22 | 23 | import { IConsoleTracker } from '@jupyterlab/console'; 24 | 25 | import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; 26 | 27 | import { listIcon } from '@jupyterlab/ui-components'; 28 | 29 | namespace CommandIDs { 30 | export const open = 'variableinspector:open'; 31 | } 32 | 33 | /** 34 | * A service providing variable introspection. 35 | */ 36 | const variableinspector: JupyterFrontEndPlugin = { 37 | id: 'jupyterlab_variableinspector', 38 | requires: [ICommandPalette, ILayoutRestorer, ILabShell], 39 | provides: IVariableInspectorManager, 40 | autoStart: true, 41 | activate: ( 42 | app: JupyterFrontEnd, 43 | palette: ICommandPalette, 44 | restorer: ILayoutRestorer, 45 | labShell: ILabShell 46 | ): IVariableInspectorManager => { 47 | const manager = new VariableInspectorManager(); 48 | const category = 'Variable Inspector'; 49 | const command = CommandIDs.open; 50 | const label = 'Open Variable Inspector'; 51 | const namespace = 'variableinspector'; 52 | const tracker = new WidgetTracker({ namespace }); 53 | 54 | /** 55 | * Create and track a new inspector. 56 | */ 57 | function newPanel(): VariableInspectorPanel { 58 | const panel = new VariableInspectorPanel(); 59 | 60 | panel.id = 'jp-variableinspector'; 61 | panel.title.label = 'Variable Inspector'; 62 | panel.title.icon = listIcon; 63 | panel.title.closable = true; 64 | panel.disposed.connect(() => { 65 | if (manager.panel === panel) { 66 | manager.panel = null; 67 | } 68 | }); 69 | 70 | //Track the inspector panel 71 | tracker.add(panel); 72 | 73 | return panel; 74 | } 75 | 76 | // Enable state restoration 77 | restorer.restore(tracker, { 78 | command, 79 | args: () => null, 80 | name: () => 'variableinspector', 81 | }); 82 | 83 | // Add command to palette 84 | app.commands.addCommand(command, { 85 | label, 86 | execute: () => { 87 | if (!manager.panel || manager.panel.isDisposed) { 88 | manager.panel = newPanel(); 89 | } 90 | if (!manager.panel.isAttached) { 91 | labShell.add(manager.panel, 'main'); 92 | } 93 | if (manager.source) { 94 | manager.source.performInspection(); 95 | } 96 | labShell.activateById(manager.panel.id); 97 | }, 98 | }); 99 | palette.addItem({ command, category }); 100 | 101 | console.log( 102 | 'JupyterLab extension jupyterlab_variableinspector is activated!' 103 | ); 104 | return manager; 105 | }, 106 | }; 107 | 108 | /** 109 | * An extension that registers consoles for variable inspection. 110 | */ 111 | const consoles: JupyterFrontEndPlugin = { 112 | id: 'jupyterlab-variableinspector:consoles', 113 | requires: [IVariableInspectorManager, IConsoleTracker, ILabShell], 114 | autoStart: true, 115 | activate: ( 116 | app: JupyterFrontEnd, 117 | manager: IVariableInspectorManager, 118 | consoles: IConsoleTracker, 119 | labShell: ILabShell 120 | ): void => { 121 | const handlers: { 122 | [id: string]: Promise; 123 | } = {}; 124 | 125 | /** 126 | * Subscribes to the creation of new consoles. If a new notebook is created, build a new handler for the consoles. 127 | * Adds a promise for a instanced handler to the 'handlers' collection. 128 | */ 129 | consoles.widgetAdded.connect((sender, consolePanel) => { 130 | if (manager.hasHandler(consolePanel.sessionContext.path)) { 131 | handlers[consolePanel.id] = new Promise((resolve, reject) => { 132 | resolve(manager.getHandler(consolePanel.sessionContext.path)); 133 | }); 134 | } else { 135 | handlers[consolePanel.id] = new Promise((resolve, reject) => { 136 | const session = consolePanel.sessionContext; 137 | 138 | // Create connector and init w script if it exists for kernel type. 139 | const connector = new KernelConnector({ session }); 140 | const scripts: Promise = connector.ready.then( 141 | () => { 142 | return connector.kernelLanguage.then((lang) => { 143 | return Languages.getScript(lang); 144 | }); 145 | } 146 | ); 147 | 148 | scripts.then((result: Languages.LanguageModel) => { 149 | const initScript = result.initScript; 150 | const queryCommand = result.queryCommand; 151 | const matrixQueryCommand = result.matrixQueryCommand; 152 | const widgetQueryCommand = result.widgetQueryCommand; 153 | const deleteCommand = result.deleteCommand; 154 | 155 | const options: VariableInspectionHandler.IOptions = { 156 | queryCommand: queryCommand, 157 | matrixQueryCommand: matrixQueryCommand, 158 | widgetQueryCommand, 159 | deleteCommand: deleteCommand, 160 | connector: connector, 161 | initScript: initScript, 162 | id: session.path, //Using the sessions path as an identifier for now. 163 | }; 164 | const handler = new VariableInspectionHandler(options); 165 | manager.addHandler(handler); 166 | consolePanel.disposed.connect(() => { 167 | delete handlers[consolePanel.id]; 168 | handler.dispose(); 169 | }); 170 | 171 | handler.ready.then(() => { 172 | resolve(handler); 173 | }); 174 | }); 175 | 176 | //Otherwise log error message. 177 | scripts.catch((result: string) => { 178 | console.log(result); 179 | const handler = new DummyHandler(connector); 180 | consolePanel.disposed.connect(() => { 181 | delete handlers[consolePanel.id]; 182 | handler.dispose(); 183 | }); 184 | 185 | resolve(handler); 186 | }); 187 | }); 188 | } 189 | }); 190 | 191 | /** 192 | * If focus window changes, checks whether new focus widget is a console. 193 | * In that case, retrieves the handler associated to the console after it has been 194 | * initialized and updates the manager with it. 195 | */ 196 | labShell.currentChanged.connect((sender, args) => { 197 | const widget = args.newValue; 198 | if (!widget || !consoles.has(widget)) { 199 | return; 200 | } 201 | const future = handlers[widget.id]; 202 | future.then((source: IVariableInspector.IInspectable) => { 203 | if (source) { 204 | manager.source = source; 205 | manager.source.performInspection(); 206 | } 207 | }); 208 | }); 209 | 210 | app.contextMenu.addItem({ 211 | command: CommandIDs.open, 212 | selector: '.jp-CodeConsole', 213 | }); 214 | }, 215 | }; 216 | 217 | /** 218 | * An extension that registers notebooks for variable inspection. 219 | */ 220 | const notebooks: JupyterFrontEndPlugin = { 221 | id: 'jupyterlab-variableinspector:notebooks', 222 | requires: [IVariableInspectorManager, INotebookTracker, ILabShell], 223 | autoStart: true, 224 | activate: ( 225 | app: JupyterFrontEnd, 226 | manager: IVariableInspectorManager, 227 | notebooks: INotebookTracker, 228 | labShell: ILabShell 229 | ): void => { 230 | const handlers: { [id: string]: Promise } = {}; 231 | 232 | /** 233 | * Subscribes to the creation of new notebooks. If a new notebook is created, build a new handler for the notebook. 234 | * Adds a promise for a instanced handler to the 'handlers' collection. 235 | */ 236 | notebooks.widgetAdded.connect((sender, nbPanel: NotebookPanel) => { 237 | //A promise that resolves after the initialization of the handler is done. 238 | handlers[nbPanel.id] = new Promise((resolve, reject) => { 239 | const session = nbPanel.sessionContext; 240 | const connector = new KernelConnector({ session }); 241 | const rendermime = nbPanel.content.rendermime; 242 | 243 | const scripts: Promise = connector.ready.then( 244 | () => { 245 | return connector.kernelLanguage.then((lang) => { 246 | return Languages.getScript(lang); 247 | }); 248 | } 249 | ); 250 | 251 | scripts.then((result: Languages.LanguageModel) => { 252 | const initScript = result.initScript; 253 | const queryCommand = result.queryCommand; 254 | const matrixQueryCommand = result.matrixQueryCommand; 255 | const widgetQueryCommand = result.widgetQueryCommand; 256 | const deleteCommand = result.deleteCommand; 257 | 258 | const options: VariableInspectionHandler.IOptions = { 259 | queryCommand: queryCommand, 260 | matrixQueryCommand: matrixQueryCommand, 261 | widgetQueryCommand, 262 | deleteCommand: deleteCommand, 263 | connector: connector, 264 | rendermime, 265 | initScript: initScript, 266 | id: session.path, //Using the sessions path as an identifier for now. 267 | }; 268 | const handler = new VariableInspectionHandler(options); 269 | manager.addHandler(handler); 270 | nbPanel.disposed.connect(() => { 271 | delete handlers[nbPanel.id]; 272 | handler.dispose(); 273 | }); 274 | 275 | handler.ready.then(() => { 276 | resolve(handler); 277 | }); 278 | }); 279 | 280 | //Otherwise log error message. 281 | scripts.catch((result: string) => { 282 | reject(result); 283 | }); 284 | }); 285 | }); 286 | 287 | /** 288 | * If focus window changes, checks whether new focus widget is a notebook. 289 | * In that case, retrieves the handler associated to the notebook after it has been 290 | * initialized and updates the manager with it. 291 | */ 292 | labShell.currentChanged.connect((sender, args) => { 293 | const widget = args.newValue; 294 | if (!widget || !notebooks.has(widget)) { 295 | return; 296 | } 297 | const future = handlers[widget.id]; 298 | future.then((source: VariableInspectionHandler) => { 299 | if (source) { 300 | manager.source = source; 301 | manager.source.performInspection(); 302 | } 303 | }); 304 | }); 305 | 306 | app.contextMenu.addItem({ 307 | command: CommandIDs.open, 308 | selector: '.jp-Notebook', 309 | }); 310 | }, 311 | }; 312 | 313 | /** 314 | * Export the plugins as default. 315 | */ 316 | const plugins: JupyterFrontEndPlugin[] = [ 317 | variableinspector, 318 | consoles, 319 | notebooks, 320 | ]; 321 | export default plugins; 322 | -------------------------------------------------------------------------------- /src/inspectorscripts.ts: -------------------------------------------------------------------------------- 1 | export namespace Languages { 2 | export type LanguageModel = { 3 | initScript: string; 4 | queryCommand: string; 5 | matrixQueryCommand: string; 6 | widgetQueryCommand: string; 7 | deleteCommand: string; 8 | }; 9 | } 10 | 11 | export abstract class Languages { 12 | /** 13 | * Init and query script for supported languages. 14 | */ 15 | 16 | static py_script = `import json 17 | import sys 18 | from IPython import get_ipython 19 | from IPython.core.magics.namespace import NamespaceMagics 20 | 21 | 22 | _jupyterlab_variableinspector_nms = NamespaceMagics() 23 | _jupyterlab_variableinspector_Jupyter = get_ipython() 24 | _jupyterlab_variableinspector_nms.shell = _jupyterlab_variableinspector_Jupyter.kernel.shell 25 | 26 | __np = None 27 | __pd = None 28 | __pyspark = None 29 | __tf = None 30 | __K = None 31 | __torch = None 32 | __ipywidgets = None 33 | 34 | 35 | def _check_imported(): 36 | global __np, __pd, __pyspark, __tf, __K, __torch, __ipywidgets 37 | 38 | if 'numpy' in sys.modules: 39 | # don't really need the try 40 | import numpy as __np 41 | 42 | if 'pandas' in sys.modules: 43 | import pandas as __pd 44 | 45 | if 'pyspark' in sys.modules: 46 | import pyspark as __pyspark 47 | 48 | if 'tensorflow' in sys.modules or 'keras' in sys.modules: 49 | import tensorflow as __tf 50 | 51 | try: 52 | import keras.backend as __K 53 | except ImportError: 54 | try: 55 | import tensorflow.keras.backend as __K 56 | except ImportError: 57 | __K = None 58 | 59 | if 'torch' in sys.modules: 60 | import torch as __torch 61 | 62 | if 'ipywidgets' in sys.modules: 63 | import ipywidgets as __ipywidgets 64 | 65 | 66 | def _jupyterlab_variableinspector_getsizeof(x): 67 | if type(x).__name__ in ['ndarray', 'Series']: 68 | return x.nbytes 69 | elif __pyspark and isinstance(x, __pyspark.sql.DataFrame): 70 | return "?" 71 | elif __tf and isinstance(x, __tf.Variable): 72 | return "?" 73 | elif __torch and isinstance(x, __torch.Tensor): 74 | return x.element_size() * x.nelement() 75 | elif __pd and type(x).__name__ == 'DataFrame': 76 | return x.memory_usage().sum() 77 | else: 78 | return sys.getsizeof(x) 79 | 80 | 81 | def _jupyterlab_variableinspector_getshapeof(x): 82 | if __pd and isinstance(x, __pd.DataFrame): 83 | return "%d rows x %d cols" % x.shape 84 | if __pd and isinstance(x, __pd.Series): 85 | return "%d rows" % x.shape 86 | if __np and isinstance(x, __np.ndarray): 87 | shape = " x ".join([str(i) for i in x.shape]) 88 | return "%s" % shape 89 | if __pyspark and isinstance(x, __pyspark.sql.DataFrame): 90 | return "? rows x %d cols" % len(x.columns) 91 | if __tf and isinstance(x, __tf.Variable): 92 | shape = " x ".join([str(int(i)) for i in x.shape]) 93 | return "%s" % shape 94 | if __tf and isinstance(x, __tf.Tensor): 95 | shape = " x ".join([str(int(i)) for i in x.shape]) 96 | return "%s" % shape 97 | if __torch and isinstance(x, __torch.Tensor): 98 | shape = " x ".join([str(int(i)) for i in x.shape]) 99 | return "%s" % shape 100 | if isinstance(x, list): 101 | return "%s" % len(x) 102 | if isinstance(x, dict): 103 | return "%s keys" % len(x) 104 | return '' 105 | 106 | 107 | def _jupyterlab_variableinspector_getcontentof(x): 108 | # returns content in a friendly way for python variables 109 | # pandas and numpy 110 | if __pd and isinstance(x, __pd.DataFrame): 111 | colnames = ', '.join(x.columns.map(str)) 112 | content = "Columns: %s" % colnames 113 | elif __pd and isinstance(x, __pd.Series): 114 | content = str(x.values).replace(" ", ", ")[1:-1] 115 | content = content.replace("\\n", "") 116 | elif __np and isinstance(x, __np.ndarray): 117 | content = x.__repr__() 118 | elif __torch and isinstance(x, __torch.Tensor): 119 | if x.nelement() < 1048576: 120 | content = x.__repr__() 121 | else: 122 | content = 'too big' 123 | else: 124 | content = str(x) 125 | 126 | if len(content) > 50: 127 | return content[:50] + " ..." 128 | else: 129 | return content 130 | 131 | 132 | def _jupyterlab_variableinspector_is_matrix(x): 133 | # True if type(x).__name__ in ["DataFrame", "ndarray", "Series"] else False 134 | if __pd and isinstance(x, __pd.DataFrame): 135 | return True 136 | if __pd and isinstance(x, __pd.Series): 137 | return True 138 | if __np and isinstance(x, __np.ndarray) and len(x.shape) <= 2: 139 | return True 140 | if __pyspark and isinstance(x, __pyspark.sql.DataFrame): 141 | return True 142 | if __tf and isinstance(x, __tf.Variable) and len(x.shape) <= 2: 143 | return True 144 | if __tf and isinstance(x, __tf.Tensor) and len(x.shape) <= 2: 145 | return True 146 | if __torch and isinstance(x, __torch.Tensor) and len(x.shape) <= 2: 147 | return True 148 | if isinstance(x, list): 149 | return True 150 | return False 151 | 152 | 153 | def _jupyterlab_variableinspector_is_widget(x): 154 | return __ipywidgets and issubclass(x, __ipywidgets.DOMWidget) 155 | 156 | 157 | def _jupyterlab_variableinspector_dict_list(): 158 | _check_imported() 159 | def keep_cond(v): 160 | try: 161 | obj = eval(v) 162 | if isinstance(obj, (str, list, dict)): 163 | return True 164 | if __tf and isinstance(obj, __tf.Variable): 165 | return True 166 | if __torch and isinstance(obj, __torch.Tensor): 167 | return True 168 | if __pd and __pd is not None and ( 169 | isinstance(obj, __pd.core.frame.DataFrame) 170 | or isinstance(obj, __pd.core.series.Series)): 171 | return True 172 | if v in ['__np', '__pd', '__pyspark', '__tf', '__K', '__torch', '__ipywidgets']: 173 | return obj is not None 174 | if str(obj)[0] == "<": 175 | return False 176 | if str(obj).startswith("_Feature"): 177 | # removes tf/keras objects 178 | return False 179 | return True 180 | except: 181 | return False 182 | values = _jupyterlab_variableinspector_nms.who_ls() 183 | if 'jlvi_brief' in values: 184 | vardic = [ 185 | { 186 | 'varName': _v, 187 | 'varType': type(eval(_v)).__name__, 188 | 'varSize': '0', 189 | 'varShape': str(_jupyterlab_variableinspector_getshapeof(eval(_v))), 190 | 'varContent': '', 191 | 'isMatrix': False, 192 | 'isWidget': _jupyterlab_variableinspector_is_widget(type(eval(_v))) 193 | } 194 | for _v in values if keep_cond(_v) 195 | ] 196 | else: 197 | vardic = [ 198 | { 199 | 'varName': _v, 200 | 'varType': type(eval(_v)).__name__, 201 | 'varSize': '0', 202 | 'varShape': str(_jupyterlab_variableinspector_getshapeof(eval(_v))), 203 | 'varContent': str(_jupyterlab_variableinspector_getcontentof(eval(_v))), 204 | 'isMatrix': False, 205 | 'isWidget': _jupyterlab_variableinspector_is_widget(type(eval(_v))) 206 | } 207 | for _v in values if keep_cond(_v) 208 | ] 209 | return json.dumps(vardic, ensure_ascii=False) 210 | 211 | 212 | def _jupyterlab_variableinspector_getmatrixcontent(x, max_rows=10000): 213 | # to do: add something to handle this in the future 214 | threshold = max_rows 215 | 216 | if __pd and __pyspark and isinstance(x, __pyspark.sql.DataFrame): 217 | df = x.limit(threshold).toPandas() 218 | return _jupyterlab_variableinspector_getmatrixcontent(df.copy()) 219 | elif __np and __pd and type(x).__name__ == "DataFrame": 220 | if threshold is not None: 221 | x = x.head(threshold) 222 | x.columns = x.columns.map(str) 223 | return x.to_json(orient="table", default_handler=_jupyterlab_variableinspector_default, force_ascii=False) 224 | elif __np and __pd and type(x).__name__ == "Series": 225 | if threshold is not None: 226 | x = x.head(threshold) 227 | return x.to_json(orient="table", default_handler=_jupyterlab_variableinspector_default, force_ascii=False) 228 | elif __np and __pd and type(x).__name__ == "ndarray": 229 | df = __pd.DataFrame(x) 230 | return _jupyterlab_variableinspector_getmatrixcontent(df) 231 | elif __tf and (isinstance(x, __tf.Variable) or isinstance(x, __tf.Tensor)): 232 | df = __K.get_value(x) 233 | return _jupyterlab_variableinspector_getmatrixcontent(df) 234 | elif __torch and isinstance(x, __torch.Tensor): 235 | df = x.cpu().numpy() 236 | return _jupyterlab_variableinspector_getmatrixcontent(df) 237 | elif isinstance(x, list): 238 | s = __pd.Series(x) 239 | return _jupyterlab_variableinspector_getmatrixcontent(s) 240 | 241 | 242 | def _jupyterlab_variableinspector_displaywidget(widget): 243 | display(widget) 244 | 245 | 246 | def _jupyterlab_variableinspector_default(o): 247 | if isinstance(o, __np.number): return int(o) 248 | raise TypeError 249 | 250 | 251 | def _jupyterlab_variableinspector_deletevariable(x): 252 | exec("del %s" % x, globals()) 253 | `; 254 | 255 | static r_script = ` 256 | library(repr) 257 | 258 | .Last.value = '' 259 | 260 | .ls.objects = function (pos = 1, pattern, order.by, decreasing = FALSE, head = FALSE, n = 5) 261 | { 262 | napply <- function(names, fn) sapply(names, function(x) fn(get(x, pos = pos))) 263 | names <- ls(pos = pos, pattern = pattern) 264 | 265 | .Last.value <<- base::.Last.value 266 | 267 | if (length(names) == 0) { 268 | return(jsonlite::toJSON(data.frame())) 269 | } 270 | 271 | names = c('.Last.value',names) 272 | 273 | obj.class <- napply(names, function(x) paste(as.character(class(x)),collapse=rawToChar(as.raw(c(92,110))))) 274 | obj.mode <- napply(names, mode) 275 | obj.type <- ifelse(is.na(obj.class), obj.mode, obj.class) 276 | 277 | obj.dim <- t(napply(names, function(x) as.numeric(dim(x))[1:2])) 278 | has_no_dim <- is.na(obj.dim)[1:length(names)] 279 | obj.dim[has_no_dim, 1] <- napply(names, length)[has_no_dim] 280 | 281 | obj.size = rep(0,length(names)) 282 | 283 | obj.content = rep("NA", length(names)) 284 | if (!"jlvi_brief" %in% names) { 285 | obj.content <- napply(names, function(x) { 286 | a = capture.output(try({str(x, max.level = 1, list.len = 3)})) 287 | b = lapply(a[1:min(length(a), 4)], function(x) { 288 | paste(substring(x, 1, 30), ifelse(nchar(x) > 30, "...", "")) 289 | }) 290 | paste(b, collapse = rawToChar(as.raw(c(92, 110)))) 291 | }) 292 | } 293 | 294 | is_function <- (obj.type == "function") 295 | if (!"jlvi_brief" %in% names) { 296 | obj.content[is_function] <- napply(names[is_function], function(x) { 297 | a = strsplit(repr_text(x), rawToChar(as.raw(c(92, 110))))[[1]] 298 | if (length(a) >= 4) 299 | a[[4]] = "..." 300 | b = lapply(a[1:min(length(a), 4)], function(x) { 301 | paste(substring(x, 1, 30), ifelse(nchar(x) > 30, "...", "")) 302 | }) 303 | paste(b, collapse = rawToChar(as.raw(c(92, 110)))) 304 | }) 305 | } 306 | 307 | obj.content <- unlist(obj.content, use.names = FALSE) 308 | 309 | out <- data.frame(obj.type, obj.size, obj.dim) 310 | names(out) <- c("varType", "varSize", "Rows", "Columns") 311 | out$varShape <- paste(out$Rows, " x ", out$Columns) 312 | out$varContent <- obj.content 313 | out$isMatrix <- FALSE 314 | out$varName <- row.names(out) 315 | out <- out[, !(names(out) %in% c("Rows", "Columns"))] 316 | rownames(out) <- NULL 317 | # print(out) 318 | if (!missing(order.by)) 319 | out <- out[order(out[[order.by]], decreasing = decreasing), 320 | ] 321 | if (head) 322 | out <- head(out, n) 323 | jsonlite::toJSON(out) 324 | } 325 | 326 | .deleteVariable <- function(x) { 327 | remove(list=c(x), envir=.GlobalEnv) 328 | } 329 | `; 330 | 331 | static scripts: { [index: string]: Languages.LanguageModel } = { 332 | python3: { 333 | initScript: Languages.py_script, 334 | queryCommand: '_jupyterlab_variableinspector_dict_list()', 335 | matrixQueryCommand: '_jupyterlab_variableinspector_getmatrixcontent', 336 | widgetQueryCommand: '_jupyterlab_variableinspector_displaywidget', 337 | deleteCommand: '_jupyterlab_variableinspector_deletevariable', 338 | }, 339 | python2: { 340 | initScript: Languages.py_script, 341 | queryCommand: '_jupyterlab_variableinspector_dict_list()', 342 | matrixQueryCommand: '_jupyterlab_variableinspector_getmatrixcontent', 343 | widgetQueryCommand: '_jupyterlab_variableinspector_displaywidget', 344 | deleteCommand: '_jupyterlab_variableinspector_deletevariable', 345 | }, 346 | python: { 347 | initScript: Languages.py_script, 348 | queryCommand: '_jupyterlab_variableinspector_dict_list()', 349 | matrixQueryCommand: '_jupyterlab_variableinspector_getmatrixcontent', 350 | widgetQueryCommand: '_jupyterlab_variableinspector_displaywidget', 351 | deleteCommand: '_jupyterlab_variableinspector_deletevariable', 352 | }, 353 | R: { 354 | initScript: Languages.r_script, 355 | queryCommand: '.ls.objects()', 356 | matrixQueryCommand: '.ls.objects', 357 | widgetQueryCommand: 'TODO', 358 | deleteCommand: '.deleteVariable', 359 | }, 360 | scala: { 361 | initScript: 362 | '_root_.almond.api.JupyterAPIHolder.value.VariableInspector.init()', 363 | queryCommand: 364 | '_root_.almond.api.JupyterAPIHolder.value.VariableInspector.dictList()', 365 | matrixQueryCommand: '', // TODO 366 | widgetQueryCommand: '', // TODO 367 | deleteCommand: '', // TODO 368 | }, 369 | }; 370 | 371 | public static getScript(lang: string): Promise { 372 | return new Promise((resolve, reject) => { 373 | if (lang in Languages.scripts) { 374 | resolve(Languages.scripts[lang]); 375 | } else { 376 | reject('Language ' + lang + ' not supported yet!'); 377 | } 378 | }); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/kernelconnector.ts: -------------------------------------------------------------------------------- 1 | import { ISessionContext } from '@jupyterlab/apputils'; 2 | 3 | import { KernelMessage } from '@jupyterlab/services'; 4 | import { IShellFuture } from '@jupyterlab/services/lib/kernel/kernel'; 5 | import { 6 | IExecuteReplyMsg, 7 | IExecuteRequestMsg, 8 | } from '@jupyterlab/services/lib/kernel/messages'; 9 | 10 | import { ISignal, Signal } from '@lumino/signaling'; 11 | 12 | /** 13 | * Connector class that handles execute request to a kernel 14 | */ 15 | export class KernelConnector { 16 | private _session: ISessionContext; 17 | private _kernelRestarted = new Signal>(this); 18 | 19 | constructor(options: KernelConnector.IOptions) { 20 | this._session = options.session; 21 | this._session.statusChanged.connect( 22 | (sender: ISessionContext, newStatus: KernelMessage.Status) => { 23 | switch (newStatus) { 24 | case 'restarting': 25 | case 'autorestarting': 26 | this._kernelRestarted.emit(this._session.ready); 27 | break; 28 | default: 29 | break; 30 | } 31 | } 32 | ); 33 | } 34 | 35 | get kernelRestarted(): ISignal> { 36 | return this._kernelRestarted; 37 | } 38 | 39 | get kernelLanguage(): Promise { 40 | return this._session.session.kernel.info.then((infoReply) => { 41 | return infoReply.language_info.name; 42 | }); 43 | } 44 | 45 | get kernelName(): string { 46 | return this._session.kernelDisplayName; 47 | } 48 | 49 | /** 50 | * A Promise that is fulfilled when the session associated w/ the connector is ready. 51 | */ 52 | get ready(): Promise { 53 | return this._session.ready; 54 | } 55 | 56 | /** 57 | * A signal emitted for iopub messages of the kernel associated with the kernel. 58 | */ 59 | get iopubMessage(): ISignal { 60 | return this._session.iopubMessage; 61 | } 62 | 63 | /** 64 | * Executes the given request on the kernel associated with the connector. 65 | * @param content: IExecuteRequestMsg to forward to the kernel. 66 | * @param ioCallback: Callable to forward IOPub messages of the kernel to. 67 | * @returns Promise 68 | */ 69 | fetch( 70 | content: KernelMessage.IExecuteRequestMsg['content'], 71 | ioCallback: (msg: KernelMessage.IIOPubMessage) => any 72 | ): Promise { 73 | const kernel = this._session.session.kernel; 74 | if (!kernel) { 75 | return Promise.reject( 76 | new Error('Require kernel to perform variable inspection!') 77 | ); 78 | } 79 | 80 | const future = kernel.requestExecute(content); 81 | 82 | future.onIOPub = (msg: KernelMessage.IIOPubMessage): void => { 83 | ioCallback(msg); 84 | }; 85 | return future.done as Promise; 86 | } 87 | 88 | execute( 89 | content: KernelMessage.IExecuteRequestMsg['content'] 90 | ): IShellFuture { 91 | return this._session.session.kernel.requestExecute(content); 92 | } 93 | } 94 | 95 | export namespace KernelConnector { 96 | export interface IOptions { 97 | session: ISessionContext; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/manager.ts: -------------------------------------------------------------------------------- 1 | import { 2 | VariableInspectorPanel, 3 | IVariableInspector, 4 | } from './variableinspector'; 5 | 6 | import { Token } from '@lumino/coreutils'; 7 | 8 | import { VariableInspectionHandler } from './handler'; 9 | 10 | export const IVariableInspectorManager = new Token( 11 | 'jupyterlab_extension/variableinspector:IVariableInspectorManager' 12 | ); 13 | 14 | export interface IVariableInspectorManager { 15 | source: IVariableInspector.IInspectable | null; 16 | hasHandler(id: string): boolean; 17 | getHandler(id: string): VariableInspectionHandler; 18 | addHandler(handler: VariableInspectionHandler): void; 19 | } 20 | 21 | /** 22 | * A class that manages variable inspector widget instances and offers persistent 23 | * `IVariableInspector` instance that other plugins can communicate with. 24 | */ 25 | export class VariableInspectorManager implements IVariableInspectorManager { 26 | private _source: IVariableInspector.IInspectable = null; 27 | private _panel: VariableInspectorPanel = null; 28 | private _handlers: { [id: string]: VariableInspectionHandler } = {}; 29 | 30 | public hasHandler(id: string): boolean { 31 | if (this._handlers[id]) { 32 | return true; 33 | } else { 34 | return false; 35 | } 36 | } 37 | 38 | public getHandler(id: string): VariableInspectionHandler { 39 | return this._handlers[id]; 40 | } 41 | 42 | public addHandler(handler: VariableInspectionHandler): void { 43 | this._handlers[handler.id] = handler; 44 | } 45 | 46 | /** 47 | * The current inspector panel. 48 | */ 49 | get panel(): VariableInspectorPanel { 50 | return this._panel; 51 | } 52 | 53 | set panel(panel: VariableInspectorPanel) { 54 | if (this.panel === panel) { 55 | return; 56 | } 57 | this._panel = panel; 58 | 59 | if (panel && !panel.source) { 60 | panel.source = this._source; 61 | } 62 | } 63 | 64 | /** 65 | * The source of events the inspector panel listens for. 66 | */ 67 | get source(): IVariableInspector.IInspectable { 68 | return this._source; 69 | } 70 | 71 | set source(source: IVariableInspector.IInspectable) { 72 | if (this._source === source) { 73 | return; 74 | } 75 | 76 | // remove subscriptions 77 | if (this._source) { 78 | this._source.disposed.disconnect(this._onSourceDisposed, this); 79 | } 80 | 81 | this._source = source; 82 | 83 | if (this._panel && !this._panel.isDisposed) { 84 | this._panel.source = this._source; 85 | } 86 | // Subscribe to new source 87 | if (this._source) { 88 | this._source.disposed.connect(this._onSourceDisposed, this); 89 | } 90 | } 91 | 92 | private _onSourceDisposed(): void { 93 | this._source = null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/variableinspector.ts: -------------------------------------------------------------------------------- 1 | import { OutputAreaModel, SimplifiedOutputArea } from '@jupyterlab/outputarea'; 2 | 3 | import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; 4 | 5 | import { Kernel, KernelMessage } from '@jupyterlab/services'; 6 | 7 | import { ISignal } from '@lumino/signaling'; 8 | 9 | import { Token } from '@lumino/coreutils'; 10 | 11 | import { DockLayout, Widget } from '@lumino/widgets'; 12 | 13 | import { DataGrid, DataModel } from '@lumino/datagrid'; 14 | 15 | import { closeIcon, searchIcon } from '@jupyterlab/ui-components'; 16 | 17 | import '../style/index.css'; 18 | 19 | const TITLE_CLASS = 'jp-VarInspector-title'; 20 | const PANEL_CLASS = 'jp-VarInspector'; 21 | const TABLE_CLASS = 'jp-VarInspector-table'; 22 | const TABLE_BODY_CLASS = 'jp-VarInspector-content'; 23 | 24 | /** 25 | * The inspector panel token. 26 | */ 27 | export const IVariableInspector = new Token( 28 | 'jupyterlab_extension/variableinspector:IVariableInspector' 29 | ); 30 | 31 | /** 32 | * An interface for an inspector. 33 | */ 34 | export interface IVariableInspector { 35 | source: IVariableInspector.IInspectable | null; 36 | } 37 | 38 | /** 39 | * A namespace for inspector interfaces. 40 | */ 41 | export namespace IVariableInspector { 42 | export interface IInspectable { 43 | disposed: ISignal; 44 | inspected: ISignal; 45 | rendermime: IRenderMimeRegistry; 46 | performInspection(): void; 47 | performMatrixInspection( 48 | varName: string, 49 | maxRows?: number 50 | ): Promise; 51 | performWidgetInspection( 52 | varName: string 53 | ): Kernel.IShellFuture< 54 | KernelMessage.IExecuteRequestMsg, 55 | KernelMessage.IExecuteReplyMsg 56 | >; 57 | performDelete(varName: string): void; 58 | } 59 | 60 | export interface IVariableInspectorUpdate { 61 | title: IVariableTitle; 62 | payload: Array; 63 | } 64 | 65 | export interface IVariable { 66 | varName: string; 67 | varSize: string; 68 | varShape: string; 69 | varContent: string; 70 | varType: string; 71 | isMatrix: boolean; 72 | isWidget: boolean; 73 | } 74 | export interface IVariableTitle { 75 | kernelName?: string; 76 | contextName?: string; //Context currently reserved for special information. 77 | } 78 | } 79 | 80 | /** 81 | * A panel that renders the variables 82 | */ 83 | export class VariableInspectorPanel 84 | extends Widget 85 | implements IVariableInspector { 86 | private _source: IVariableInspector.IInspectable | null = null; 87 | private _table: HTMLTableElement; 88 | private _title: HTMLElement; 89 | 90 | constructor() { 91 | super(); 92 | this.addClass(PANEL_CLASS); 93 | this._title = Private.createTitle(); 94 | this._title.className = TITLE_CLASS; 95 | this._table = Private.createTable(); 96 | this._table.className = TABLE_CLASS; 97 | this.node.appendChild(this._title as HTMLElement); 98 | this.node.appendChild(this._table as HTMLElement); 99 | } 100 | 101 | get source(): IVariableInspector.IInspectable | null { 102 | return this._source; 103 | } 104 | 105 | set source(source: IVariableInspector.IInspectable | null) { 106 | if (this._source === source) { 107 | // this._source.performInspection(); 108 | return; 109 | } 110 | //Remove old subscriptions 111 | if (this._source) { 112 | this._source.inspected.disconnect(this.onInspectorUpdate, this); 113 | this._source.disposed.disconnect(this.onSourceDisposed, this); 114 | } 115 | this._source = source; 116 | //Subscribe to new object 117 | if (this._source) { 118 | this._source.inspected.connect(this.onInspectorUpdate, this); 119 | this._source.disposed.connect(this.onSourceDisposed, this); 120 | this._source.performInspection(); 121 | } 122 | } 123 | 124 | /** 125 | * Dispose resources 126 | */ 127 | dispose(): void { 128 | if (this.isDisposed) { 129 | return; 130 | } 131 | this.source = null; 132 | super.dispose(); 133 | } 134 | 135 | protected onInspectorUpdate( 136 | sender: any, 137 | allArgs: IVariableInspector.IVariableInspectorUpdate 138 | ): void { 139 | if (!this.isAttached) { 140 | return; 141 | } 142 | 143 | const title = allArgs.title; 144 | const args = allArgs.payload; 145 | 146 | if (title.contextName) { 147 | this._title.innerHTML = title.contextName; 148 | } else { 149 | this._title.innerHTML = 150 | " Inspecting '" + title.kernelName + "' " + title.contextName; 151 | } 152 | 153 | //Render new variable state 154 | let row: HTMLTableRowElement; 155 | this._table.deleteTFoot(); 156 | this._table.createTFoot(); 157 | this._table.tFoot.className = TABLE_BODY_CLASS; 158 | for (let index = 0; index < args.length; index++) { 159 | const item = args[index]; 160 | console.log(item); 161 | 162 | const name = item.varName; 163 | const varType = item.varType; 164 | 165 | row = this._table.tFoot.insertRow(); 166 | 167 | // Add delete icon and onclick event 168 | let cell = row.insertCell(0); 169 | cell.title = 'Delete Variable'; 170 | cell.className = 'jp-VarInspector-deleteButton'; 171 | const ico = closeIcon.element(); 172 | ico.onclick = (ev: MouseEvent): any => { 173 | this.source.performDelete(name); 174 | }; 175 | cell.append(ico); 176 | 177 | // Add onclick event for inspection 178 | cell = row.insertCell(1); 179 | if (item.isMatrix) { 180 | cell.title = 'View Contents'; 181 | cell.className = 'jp-VarInspector-inspectButton'; 182 | const ico = searchIcon.element(); 183 | ico.onclick = (ev: MouseEvent): any => { 184 | console.log('Click on ' + name); 185 | this._source 186 | .performMatrixInspection(name) 187 | .then((model: DataModel) => { 188 | this._showMatrix(model, name, varType); 189 | }); 190 | }; 191 | cell.append(ico); 192 | } else { 193 | cell.innerHTML = ''; 194 | } 195 | 196 | // cell = row.insertCell(2); 197 | cell.className = 'jp-VarInspector-varName'; 198 | cell.innerHTML = name; 199 | 200 | // Add remaining cells 201 | cell = row.insertCell(2); 202 | cell.innerHTML = varType.replace(/\\n/g, "
");; 203 | 204 | // cell = row.insertCell(4); 205 | // cell.innerHTML = item.varSize; 206 | cell = row.insertCell(3); 207 | cell.innerHTML = item.varShape; 208 | cell = row.insertCell(4); 209 | 210 | const rendermime = this._source.rendermime; 211 | if (item.isWidget && rendermime) { 212 | const model = new OutputAreaModel({ trusted: true }); 213 | const output = new SimplifiedOutputArea({ model, rendermime }); 214 | output.future = this._source.performWidgetInspection(item.varName); 215 | Widget.attach(output, cell); 216 | } else { 217 | cell.innerHTML = Private.escapeHtml(item.varContent).replace( 218 | /\\n/g, 219 | '
' 220 | ); 221 | } 222 | } 223 | } 224 | 225 | /** 226 | * Handle source disposed signals. 227 | */ 228 | protected onSourceDisposed(sender: any, args: void): void { 229 | this.source = null; 230 | } 231 | 232 | private _showMatrix( 233 | dataModel: DataModel, 234 | name: string, 235 | varType: string 236 | ): void { 237 | const datagrid = new DataGrid({ 238 | defaultSizes: { 239 | rowHeight: 32, 240 | columnWidth: 128, 241 | rowHeaderWidth: 64, 242 | columnHeaderHeight: 32, 243 | }, 244 | }); 245 | 246 | datagrid.dataModel = dataModel; 247 | datagrid.title.label = varType + ': ' + name; 248 | datagrid.title.closable = true; 249 | const lout: DockLayout = this.parent.layout as DockLayout; 250 | lout.addWidget(datagrid, { mode: 'split-right' }); 251 | //todo activate/focus matrix widget 252 | } 253 | } 254 | 255 | namespace Private { 256 | const entityMap = new Map( 257 | Object.entries({ 258 | '&': '&', 259 | '<': '<', 260 | '>': '>', 261 | '"': '"', 262 | "'": ''', 263 | '/': '/', 264 | }) 265 | ); 266 | 267 | export function escapeHtml(source: string): string { 268 | return String(source).replace(/[&<>"'/]/g, (s: string) => entityMap.get(s)); 269 | } 270 | 271 | export function createTable(): HTMLTableElement { 272 | const table = document.createElement('table'); 273 | table.createTHead(); 274 | const hrow = table.tHead.insertRow(0) as HTMLTableRowElement; 275 | 276 | const cell1 = hrow.insertCell(0); 277 | cell1.innerHTML = ''; 278 | // const cell2 = hrow.insertCell(1); 279 | // cell2.innerHTML = ''; 280 | const cell3 = hrow.insertCell(1); 281 | cell3.innerHTML = 'Name'; 282 | const cell4 = hrow.insertCell(2); 283 | cell4.innerHTML = 'Type'; 284 | // const cell5 = hrow.insertCell(4); 285 | // cell5.innerHTML = 'Size'; 286 | const cell6 = hrow.insertCell(3); 287 | cell6.innerHTML = 'Shape'; 288 | const cell7 = hrow.insertCell(4); 289 | cell7.innerHTML = 'Content'; 290 | return table; 291 | } 292 | 293 | export function createTitle(header = ''): HTMLParagraphElement { 294 | const title = document.createElement('p'); 295 | title.innerHTML = header; 296 | return title; 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /style/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | See the JupyterLab Developer Guide for useful CSS Patterns: 3 | 4 | https://jupyterlab.readthedocs.io/en/stable/developer/css.html 5 | */ 6 | -------------------------------------------------------------------------------- /style/index.css: -------------------------------------------------------------------------------- 1 | 2 | @import url('base.css'); 3 | 4 | .jp-VarInspector { 5 | flex-direction: column; 6 | overflow: auto; 7 | font-size: var(--jp-ui-font-size1); 8 | } 9 | 10 | .jp-VarInspector-table { 11 | border-collapse: collapse; 12 | margin: auto; 13 | width: 100%; 14 | color: var(--jp-content-font-color1); 15 | } 16 | 17 | .jp-VarInspector-table td, 18 | .jp-VarInspector-table thead { 19 | border: 1px solid; 20 | border-color: var(--jp-layout-color2); 21 | padding: 8px; 22 | } 23 | 24 | .jp-VarInspector-table tr:nth-child(even) { 25 | background-color: var(--jp-layout-color1); 26 | } 27 | 28 | .jp-VarInspector-content tr:hover { 29 | background-color: var(--jp-layout-color2); 30 | } 31 | 32 | .jp-VarInspector-table thead { 33 | font-size: var(--jp-ui-font-size0); 34 | text-align: center; 35 | background-color: var(--jp-layout-color2); 36 | color: var(--jp-ui-font-color1); 37 | font-weight: 600; 38 | letter-spacing: 1px; 39 | text-transform: uppercase; 40 | } 41 | 42 | .jp-VarInspector-title { 43 | font-size: var(--jp-ui-font-size1); 44 | color: var(--jp-content-font-color1); 45 | text-align: left; 46 | padding-left: 10px; 47 | } 48 | 49 | .jp-VarInspector-deleteButton { 50 | text-align: center; 51 | width: 1em; 52 | } 53 | 54 | .jp-VarInspector-deleteButton:hover { 55 | text-shadow: 0 0 0 red; 56 | background-color: var(--jp-layout-color3); 57 | } 58 | 59 | .jp-VarInspector-inspectButton { 60 | text-align: center; 61 | width: 1em; 62 | } 63 | 64 | 65 | .jp-VarInspector-varName { 66 | font-weight: 600; 67 | } 68 | 69 | -------------------------------------------------------------------------------- /style/index.js: -------------------------------------------------------------------------------- 1 | import './base.css'; 2 | -------------------------------------------------------------------------------- /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 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "preserveWatchOutput": true, 15 | "resolveJsonModule": true, 16 | "outDir": "lib", 17 | "rootDir": "src", 18 | "strict": true, 19 | "strictNullChecks": false, 20 | "target": "es2017", 21 | "types": [] 22 | }, 23 | "include": ["src/*"] 24 | } 25 | --------------------------------------------------------------------------------