├── .copier-answers.yml ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── binder-on-pr.yml │ ├── build.yml │ ├── check-release.yml │ ├── enforce-label.yml │ ├── prep-release.yml │ ├── publish-release.yml │ └── update-integration-tests.yml ├── .gitignore ├── .prettierignore ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASE.md ├── binder ├── environment.yml └── postBuild ├── early_demo.gif ├── install.json ├── ipywidgets.png ├── lckr_jupyterlab_variableinspector └── __init__.py ├── package.json ├── pyproject.toml ├── schema └── jupyterlab-variableInspector-settings.json ├── setup.py ├── src ├── LICENCE.md ├── handler.ts ├── index.ts ├── inspectorscripts.ts ├── kernelconnector.ts ├── manager.ts ├── tokens.ts └── variableinspector.ts ├── style ├── base.css ├── index.css └── index.js ├── tsconfig.json ├── ui-tests ├── README.md ├── jupyter_server_test_config.py ├── package.json ├── playwright.config.js ├── tests │ └── lckr_jupyterlab_variableinspector.spec.ts └── yarn.lock └── yarn.lock /.copier-answers.yml: -------------------------------------------------------------------------------- 1 | # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY 2 | _commit: v4.2.3 3 | _src_path: https://github.com/jupyterlab/extension-template 4 | author_email: '' 5 | author_name: lckr 6 | has_binder: true 7 | has_settings: false 8 | kind: frontend 9 | labextension_name: '@lckr/jupyterlab_variableinspector' 10 | project_short_description: Variable inspector extension for JupyterLab 11 | python_name: lckr_jupyterlab_variableinspector 12 | repository: https://github.com/jupyterlab-contrib/jupyterlab-variableInspector 13 | test: true 14 | 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Your Environment(please complete the following information):** 27 | 28 | - OS (**important**): [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version of this extension you are using 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/workflows/binder-on-pr.yml: -------------------------------------------------------------------------------- 1 | name: Binder Badge 2 | on: 3 | pull_request_target: 4 | types: [opened] 5 | 6 | jobs: 7 | binder: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | pull-requests: write 11 | steps: 12 | - uses: jupyterlab/maintainer-tools/.github/actions/binder-link@v1 13 | with: 14 | github_token: ${{ secrets.github_token }} 15 | -------------------------------------------------------------------------------- /.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@v3 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>=4.0.0,<5" 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 . 33 | 34 | jupyter labextension list 35 | jupyter labextension list 2>&1 | grep -ie "@lckr/jupyterlab_variableinspector.*OK" 36 | python -m jupyterlab.browser_check 37 | 38 | - name: Package the extension 39 | run: | 40 | set -eux 41 | 42 | pip install build 43 | python -m build 44 | pip uninstall -y "lckr_jupyterlab_variableinspector" jupyterlab 45 | 46 | - name: Upload extension packages 47 | uses: actions/upload-artifact@v3 48 | with: 49 | name: extension-artifacts 50 | path: dist/lckr_jupyterlab_variableinspector* 51 | if-no-files-found: error 52 | 53 | test_isolated: 54 | needs: build 55 | runs-on: ubuntu-latest 56 | 57 | steps: 58 | - name: Install Python 59 | uses: actions/setup-python@v4 60 | with: 61 | python-version: '3.9' 62 | architecture: 'x64' 63 | - uses: actions/download-artifact@v3 64 | with: 65 | name: extension-artifacts 66 | - name: Install and Test 67 | run: | 68 | set -eux 69 | # Remove NodeJS, twice to take care of system and locally installed node versions. 70 | sudo rm -rf $(which node) 71 | sudo rm -rf $(which node) 72 | 73 | pip install "jupyterlab>=4.0.0,<5" lckr_jupyterlab_variableinspector*.whl 74 | 75 | 76 | jupyter labextension list 77 | jupyter labextension list 2>&1 | grep -ie "@lckr/jupyterlab_variableinspector.*OK" 78 | python -m jupyterlab.browser_check --no-browser-test 79 | 80 | integration-tests: 81 | name: Integration tests 82 | needs: build 83 | runs-on: ubuntu-latest 84 | 85 | env: 86 | PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers 87 | 88 | steps: 89 | - name: Checkout 90 | uses: actions/checkout@v3 91 | 92 | - name: Base Setup 93 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 94 | 95 | - name: Download extension package 96 | uses: actions/download-artifact@v3 97 | with: 98 | name: extension-artifacts 99 | 100 | - name: Install the extension 101 | run: | 102 | set -eux 103 | python -m pip install "jupyterlab>=4.0.0,<5" lckr_jupyterlab_variableinspector*.whl 104 | 105 | - name: Install dependencies 106 | working-directory: ui-tests 107 | env: 108 | YARN_ENABLE_IMMUTABLE_INSTALLS: 0 109 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 110 | run: jlpm install 111 | 112 | - name: Set up browser cache 113 | uses: actions/cache@v3 114 | with: 115 | path: | 116 | ${{ github.workspace }}/pw-browsers 117 | key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} 118 | 119 | - name: Install browser 120 | run: jlpm playwright install chromium 121 | working-directory: ui-tests 122 | 123 | - name: Execute integration tests 124 | working-directory: ui-tests 125 | run: | 126 | jlpm playwright test 127 | 128 | - name: Upload Playwright Test report 129 | if: always() 130 | uses: actions/upload-artifact@v3 131 | with: 132 | name: lckr_jupyterlab_variableinspector-playwright-tests 133 | path: | 134 | ui-tests/test-results 135 | ui-tests/playwright-report 136 | 137 | check_links: 138 | name: Check Links 139 | runs-on: ubuntu-latest 140 | timeout-minutes: 15 141 | steps: 142 | - uses: actions/checkout@v3 143 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 144 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 145 | -------------------------------------------------------------------------------- /.github/workflows/check-release.yml: -------------------------------------------------------------------------------- 1 | name: Check Release 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | branches: ["*"] 7 | 8 | jobs: 9 | check_release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Base Setup 15 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 16 | - name: Check Release 17 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 18 | with: 19 | 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | - name: Upload Distributions 23 | uses: actions/upload-artifact@v3 24 | with: 25 | name: lckr_jupyterlab_variableinspector-releaser-dist-${{ github.run_number }} 26 | path: .jupyter_releaser_checkout/dist 27 | -------------------------------------------------------------------------------- /.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 | since: 16 | description: "Use PRs with activity since this date or git reference" 17 | required: false 18 | since_last_stable: 19 | description: "Use PRs with activity since the last stable git tag" 20 | required: false 21 | type: boolean 22 | jobs: 23 | prep_release: 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: write 27 | steps: 28 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 29 | 30 | - name: Prep Release 31 | id: prep-release 32 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 33 | with: 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | version_spec: ${{ github.event.inputs.version_spec }} 36 | post_version_spec: ${{ github.event.inputs.post_version_spec }} 37 | target: ${{ github.event.inputs.target }} 38 | branch: ${{ github.event.inputs.branch }} 39 | since: ${{ github.event.inputs.since }} 40 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 41 | 42 | - name: "** Next Step **" 43 | run: | 44 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" 45 | -------------------------------------------------------------------------------- /.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: > 14 | ( 15 | github.event.issue.author_association == 'OWNER' || 16 | github.event.issue.author_association == 'COLLABORATOR' || 17 | github.event.issue.author_association == 'MEMBER' 18 | ) && github.event.issue.pull_request && contains(github.event.comment.body, 'please update snapshots') 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: React to the triggering comment 23 | run: | 24 | gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions --raw-field 'content=+1' 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | with: 31 | token: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: Get PR Info 34 | id: pr 35 | env: 36 | PR_NUMBER: ${{ github.event.issue.number }} 37 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | GH_REPO: ${{ github.repository }} 39 | COMMENT_AT: ${{ github.event.comment.created_at }} 40 | run: | 41 | pr="$(gh api /repos/${GH_REPO}/pulls/${PR_NUMBER})" 42 | head_sha="$(echo "$pr" | jq -r .head.sha)" 43 | pushed_at="$(echo "$pr" | jq -r .pushed_at)" 44 | 45 | if [[ $(date -d "$pushed_at" +%s) -gt $(date -d "$COMMENT_AT" +%s) ]]; then 46 | echo "Updating is not allowed because the PR was pushed to (at $pushed_at) after the triggering comment was issued (at $COMMENT_AT)" 47 | exit 1 48 | fi 49 | 50 | echo "head_sha=$head_sha" >> $GITHUB_OUTPUT 51 | 52 | - name: Checkout the branch from the PR that triggered the job 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | run: gh pr checkout ${{ github.event.issue.number }} 56 | 57 | - name: Validate the fetched branch HEAD revision 58 | env: 59 | EXPECTED_SHA: ${{ steps.pr.outputs.head_sha }} 60 | run: | 61 | actual_sha="$(git rev-parse HEAD)" 62 | 63 | if [[ "$actual_sha" != "$EXPECTED_SHA" ]]; then 64 | echo "The HEAD of the checked out branch ($actual_sha) differs from the HEAD commit available at the time when trigger comment was submitted ($EXPECTED_SHA)" 65 | exit 1 66 | fi 67 | 68 | - name: Base Setup 69 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 70 | 71 | - name: Install dependencies 72 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 73 | 74 | - name: Install extension 75 | run: | 76 | set -eux 77 | jlpm 78 | python -m pip install . 79 | 80 | - uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@v1 81 | with: 82 | github_token: ${{ secrets.GITHUB_TOKEN }} 83 | # Playwright knows how to start JupyterLab server 84 | start_server_script: 'null' 85 | test_folder: ui-tests 86 | npm_client: jlpm 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.log 5 | .eslintcache 6 | .stylelintcache 7 | *.egg-info/ 8 | .ipynb_checkpoints 9 | *.tsbuildinfo 10 | lckr_jupyterlab_variableinspector/labextension 11 | .vscode 12 | # Version file is handled by hatchling 13 | lckr_jupyterlab_variableinspector/_version.py 14 | 15 | # Integration tests 16 | ui-tests/test-results/ 17 | ui-tests/playwright-report/ 18 | 19 | # Created by https://www.gitignore.io/api/python 20 | # Edit at https://www.gitignore.io/?templates=python 21 | 22 | ### Python ### 23 | # Byte-compiled / optimized / DLL files 24 | __pycache__/ 25 | *.py[cod] 26 | *$py.class 27 | 28 | # C extensions 29 | *.so 30 | 31 | # Distribution / packaging 32 | .Python 33 | build/ 34 | develop-eggs/ 35 | dist/ 36 | downloads/ 37 | eggs/ 38 | .eggs/ 39 | lib/ 40 | lib64/ 41 | parts/ 42 | sdist/ 43 | var/ 44 | wheels/ 45 | pip-wheel-metadata/ 46 | share/python-wheels/ 47 | .installed.cfg 48 | *.egg 49 | MANIFEST 50 | 51 | # PyInstaller 52 | # Usually these files are written by a python script from a template 53 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 54 | *.manifest 55 | *.spec 56 | 57 | # Installer logs 58 | pip-log.txt 59 | pip-delete-this-directory.txt 60 | 61 | # Unit test / coverage reports 62 | htmlcov/ 63 | .tox/ 64 | .nox/ 65 | .coverage 66 | .coverage.* 67 | .cache 68 | nosetests.xml 69 | coverage/ 70 | coverage.xml 71 | *.cover 72 | .hypothesis/ 73 | .pytest_cache/ 74 | 75 | # Translations 76 | *.mo 77 | *.pot 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # celery beat schedule file 92 | celerybeat-schedule 93 | 94 | # SageMath parsed files 95 | *.sage.py 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # Mr Developer 105 | .mr.developer.cfg 106 | .project 107 | .pydevproject 108 | 109 | # mkdocs documentation 110 | /site 111 | 112 | # mypy 113 | .mypy_cache/ 114 | .dmypy.json 115 | dmypy.json 116 | 117 | # Pyre type checker 118 | .pyre/ 119 | 120 | # End of https://www.gitignore.io/api/python 121 | 122 | # OSX files 123 | .DS_Store 124 | 125 | # virtualenv 126 | venv 127 | 128 | # Yarn cache 129 | .yarn/ 130 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | !/package.json 6 | lckr_jupyterlab_variableinspector 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | ## 3.2.4 6 | 7 | ([Full Changelog](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/compare/v3.2.3...dd7e6954cc20cc2273a635d9acf803efb2e34595)) 8 | 9 | ### Enhancements made 10 | 11 | - Lazily import modules like numpy/pandas/keras [#320](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/320) ([@martinRenou](https://github.com/martinRenou)) 12 | - Improve pandas dataframe inspection [#319](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/319) ([@martinRenou](https://github.com/martinRenou)) 13 | 14 | ### Contributors to this release 15 | 16 | ([GitHub contributors page for this release](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/graphs/contributors?from=2024-09-03&to=2024-09-05&type=c)) 17 | 18 | [@github-actions](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Agithub-actions+updated%3A2024-09-03..2024-09-05&type=Issues) | [@martinRenou](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3AmartinRenou+updated%3A2024-09-03..2024-09-05&type=Issues) 19 | 20 | 21 | 22 | ## 3.2.3 23 | 24 | ([Full Changelog](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/compare/v3.2.2...3f3853d9ba99eb2f3c2e9d6e045a708ebddc9353)) 25 | 26 | ### Enhancements made 27 | 28 | - Some more performance improvements [#313](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/313) ([@martinRenou](https://github.com/martinRenou)) 29 | 30 | ### Maintenance and upkeep improvements 31 | 32 | - Bump webpack from 5.89.0 to 5.94.0 [#316](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/316) ([@dependabot](https://github.com/dependabot)) 33 | - Bump micromatch from 4.0.5 to 4.0.8 [#315](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/315) ([@dependabot](https://github.com/dependabot)) 34 | 35 | ### Contributors to this release 36 | 37 | ([GitHub contributors page for this release](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/graphs/contributors?from=2024-08-30&to=2024-09-03&type=c)) 38 | 39 | [@dependabot](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Adependabot+updated%3A2024-08-30..2024-09-03&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Agithub-actions+updated%3A2024-08-30..2024-09-03&type=Issues) | [@martinRenou](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3AmartinRenou+updated%3A2024-08-30..2024-09-03&type=Issues) 40 | 41 | ## 3.2.2 42 | 43 | ([Full Changelog](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/compare/v3.2.1...aecd6baa8ac0af81bb21a221c6b156ab84e161ae)) 44 | 45 | ### Enhancements made 46 | 47 | - Small speedup improvement on the inspection logic in Python [#311](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/311) ([@martinRenou](https://github.com/martinRenou)) 48 | - Lazy inspection [#309](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/309) ([@martinRenou](https://github.com/martinRenou)) 49 | 50 | ### Maintenance and upkeep improvements 51 | 52 | - Update release workflows [#314](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/314) ([@krassowski](https://github.com/krassowski)) 53 | - CI: Fix visual regression tests [#310](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/310) ([@martinRenou](https://github.com/martinRenou)) 54 | - Updated integration tests workflow [#306](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/306) ([@krassowski](https://github.com/krassowski)) 55 | - Bump ws from 8.14.2 to 8.17.1 in /ui-tests [#305](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/305) ([@dependabot](https://github.com/dependabot)) 56 | - Set `packageManager` in package.json [#304](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/304) ([@fcollonval](https://github.com/fcollonval)) 57 | - Bump braces from 3.0.2 to 3.0.3 [#303](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/303) ([@dependabot](https://github.com/dependabot)) 58 | - Bump tar from 6.2.0 to 6.2.1 in /ui-tests [#301](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/301) ([@dependabot](https://github.com/dependabot)) 59 | - Bump ip from 2.0.0 to 2.0.1 in /ui-tests [#299](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/299) ([@dependabot](https://github.com/dependabot)) 60 | 61 | ### Contributors to this release 62 | 63 | ([GitHub contributors page for this release](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/graphs/contributors?from=2024-01-23&to=2024-08-30&type=c)) 64 | 65 | [@dependabot](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Adependabot+updated%3A2024-01-23..2024-08-30&type=Issues) | [@fcollonval](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Afcollonval+updated%3A2024-01-23..2024-08-30&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Agithub-actions+updated%3A2024-01-23..2024-08-30&type=Issues) | [@krassowski](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Akrassowski+updated%3A2024-01-23..2024-08-30&type=Issues) | [@martinRenou](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3AmartinRenou+updated%3A2024-01-23..2024-08-30&type=Issues) 66 | 67 | ## 3.2.1 68 | 69 | ([Full Changelog](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/compare/v3.2.0...e200a2bf9bdb0cdd9a12dd7885cab532a1dacef3)) 70 | 71 | ### Maintenance and upkeep improvements 72 | 73 | - Set toolkit packages as singleton shared assets [#298](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/298) ([@fcollonval](https://github.com/fcollonval)) 74 | 75 | ### Contributors to this release 76 | 77 | ([GitHub contributors page for this release](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/graphs/contributors?from=2024-01-23&to=2024-01-23&type=c)) 78 | 79 | [@fcollonval](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Afcollonval+updated%3A2024-01-23..2024-01-23&type=Issues) 80 | 81 | ## 3.2.0 82 | 83 | ([Full Changelog](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/compare/v3.1.0...e71150e553c55db1815b015409d98dc1ad609730)) 84 | 85 | ### Enhancements made 86 | 87 | - Use UI toolkit for variable table [#291](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/291) ([@kentarolim10](https://github.com/kentarolim10)) 88 | - Add option to filter out variables [#290](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/290) ([@kentarolim10](https://github.com/kentarolim10)) 89 | 90 | ### Bugs fixed 91 | 92 | - Fix some styling issues [#297](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/297) ([@fcollonval](https://github.com/fcollonval)) 93 | - Update the inspector when the kernel changes [#296](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/296) ([@fcollonval](https://github.com/fcollonval)) 94 | 95 | ### Contributors to this release 96 | 97 | ([GitHub contributors page for this release](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/graphs/contributors?from=2023-11-04&to=2024-01-22&type=c)) 98 | 99 | [@fcollonval](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Afcollonval+updated%3A2023-11-04..2024-01-22&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Agithub-actions+updated%3A2023-11-04..2024-01-22&type=Issues) | [@kentarolim10](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Akentarolim10+updated%3A2023-11-04..2024-01-22&type=Issues) 100 | 101 | ## 3.1.0 102 | 103 | ([Full Changelog](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/compare/v3.0.7...13134a81cc4c78b40f52e8b7a8b2b2f4a0b42a4b)) 104 | 105 | ### Bugs fixed 106 | 107 | - Fix README [#289](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/289) ([@fcollonval](https://github.com/fcollonval)) 108 | 109 | ### Maintenance and upkeep improvements 110 | 111 | - JupyterLab4 compatibility draft: Dependency updates and typescript code changes [#270](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/270) ([@ericsnekbytes](https://github.com/ericsnekbytes)) 112 | 113 | ### Documentation improvements 114 | 115 | - Fix README [#289](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/pull/289) ([@fcollonval](https://github.com/fcollonval)) 116 | 117 | ### Contributors to this release 118 | 119 | ([GitHub contributors page for this release](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/graphs/contributors?from=2021-04-10&to=2023-11-04&type=c)) 120 | 121 | [@ericsnekbytes](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Aericsnekbytes+updated%3A2021-04-10..2023-11-04&type=Issues) | [@fcollonval](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Afcollonval+updated%3A2021-04-10..2023-11-04&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Agithub-actions+updated%3A2021-04-10..2023-11-04&type=Issues) | [@lckr](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab-variableInspector+involves%3Alckr+updated%3A2021-04-10..2023-11-04&type=Issues) 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, lckr All rights reserved. 4 | Copyright (c) 2023, JupyterLab contrib Developers Team 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 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * 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 | * 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_variableinspector 2 | 3 | [![Extension status](https://img.shields.io/badge/status-ready-success 'ready to be used')](https://jupyterlab-contrib.github.io/) 4 | ![PyPi_Version](https://img.shields.io/pypi/v/lckr-jupyterlab-variableinspector) 5 | ![Build](https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/workflows/Build/badge.svg) 6 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jupyterlab-contrib/jupyterlab-variableInspector.git/master?urlpath=lab) 7 | 8 | Jupyterlab extension that shows currently used variables and their values. 9 | Contributions in any form are welcome! 10 | 11 | ## Features 12 | 13 | ![Demogif](early_demo.gif) 14 | 15 | - Allows inspection of variables for both consoles and notebooks. 16 | - Allows inspection of matrices in a datagrid-viewer. This might not work for large matrices. 17 | - Allows an inline and interactive inspection of Jupyter Widgets. 18 | 19 | ### Supported Languages 20 | 21 | - This extension is currently targets `python` as a main language but also supports the following languages with different levels of feature completeness 22 | - `R` 23 | - `scala` via the [almond kernel](https://github.com/almond-sh/almond) 24 | 25 | ### How it Works 26 | 27 | In order to allow variable inspection, all content that is displayed first need to be sent from the kernel to the front end. 28 | Therefore, opening large data frames with the datagrid viewer can dramatically increase your occupied memory and _significantly slow down_ your browser. 29 | Use at your own risk. 30 | 31 | ## Requirements 32 | 33 | - JupyterLab >= 3.0 34 | 35 | ### Requirements for `python` functionality 36 | 37 | - `pandas` and `numpy` are required to enable matrix inspection. 38 | - `pyspark` for spark support. 39 | - `tensorflow` and `keras` to allow inspection of tf objects. 40 | - `torch` for PyTorch support. 41 | 42 | ### Requirements for `R` functionality 43 | 44 | - The `repr` library. 45 | 46 | ### Requirements for `ipywidgets` functionality 47 | 48 | The variable inspector can also display Jupyter interactive widgets: 49 | 50 | ![ipywidgets](./ipywidgets.png) 51 | 52 | The requirements for this functionality are: 53 | 54 | - `ipywidgets`, which can be installed with `pip install ipywidgets`. 55 | 56 | ## Install 57 | 58 | To install the extension, execute: 59 | 60 | ```bash 61 | pip install lckr_jupyterlab_variableinspector 62 | ``` 63 | 64 | ## Uninstall 65 | 66 | To remove the extension, execute: 67 | 68 | ```bash 69 | pip uninstall lckr_jupyterlab_variableinspector 70 | ``` 71 | 72 | ## Contributing 73 | 74 | ### Development install 75 | 76 | Note: You will need NodeJS to build the extension package. 77 | 78 | The `jlpm` command is JupyterLab's pinned version of 79 | [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use 80 | `yarn` or `npm` in lieu of `jlpm` below. 81 | 82 | ```bash 83 | # Clone the repo to your local environment 84 | # Change directory to the lckr_jupyterlab_variableinspector directory 85 | # Install package in development mode 86 | pip install -e "." 87 | # Link your development version of the extension with JupyterLab 88 | jupyter labextension develop . --overwrite 89 | # Rebuild extension Typescript source after making changes 90 | jlpm build 91 | ``` 92 | 93 | 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. 94 | 95 | ```bash 96 | # Watch the source directory in one terminal, automatically rebuilding when needed 97 | jlpm watch 98 | # Run JupyterLab in another terminal 99 | jupyter lab 100 | ``` 101 | 102 | 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). 103 | 104 | 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: 105 | 106 | ```bash 107 | jupyter lab build --minimize=False 108 | ``` 109 | 110 | ### Development uninstall 111 | 112 | ```bash 113 | pip uninstall lckr_jupyterlab_variableinspector 114 | ``` 115 | 116 | In development mode, you will also need to remove the symlink created by `jupyter labextension develop` 117 | command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` 118 | folder is located. Then you can remove the symlink named `@lckr/jupyterlab_variableinspector` within that folder. 119 | 120 | ### Testing the extension 121 | 122 | #### Frontend tests 123 | 124 | This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. 125 | 126 | To execute them, execute: 127 | 128 | ```sh 129 | jlpm 130 | jlpm test 131 | ``` 132 | 133 | #### Integration tests 134 | 135 | This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests). 136 | More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. 137 | 138 | More information are provided within the [ui-tests](./ui-tests/README.md) README. 139 | 140 | ### Packaging the extension 141 | 142 | See [RELEASE](RELEASE.md) 143 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a new release of lckr_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 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. 62 | 63 | Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. 64 | 65 | Here is a summary of the steps to cut a new release: 66 | 67 | - Add tokens to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository: 68 | - `ADMIN_GITHUB_TOKEN` (with "public_repo" and "repo:status" permissions); see the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) 69 | - `NPM_TOKEN` (with "automation" permission); see the [documentation](https://docs.npmjs.com/creating-and-viewing-access-tokens) 70 | - Set up PyPI 71 | 72 |
Using PyPI trusted publisher (modern way) 73 | 74 | - Set up your PyPI project by [adding a trusted publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/) 75 | - The _workflow name_ is `publish-release.yml` and the _environment_ should be left blank. 76 | - Ensure the publish release job as `permissions`: `id-token : write` (see the [documentation](https://docs.pypi.org/trusted-publishers/using-a-publisher/)) 77 | 78 |
79 | 80 |
Using PyPI token (legacy way) 81 | 82 | - If the repo generates PyPI release(s), create a scoped PyPI [token](https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/#saving-credentials-on-github). We recommend using a scoped token for security reasons. 83 | 84 | - You can store the token as `PYPI_TOKEN` in your fork's `Secrets`. 85 | 86 | - Advanced usage: if you are releasing multiple repos, you can create a secret named `PYPI_TOKEN_MAP` instead of `PYPI_TOKEN` that is formatted as follows: 87 | 88 | ```text 89 | owner1/repo1,token1 90 | owner2/repo2,token2 91 | ``` 92 | 93 | If you have multiple Python packages in the same repository, you can point to them as follows: 94 | 95 | ```text 96 | owner1/repo1/path/to/package1,token1 97 | owner1/repo1/path/to/package2,token2 98 | ``` 99 | 100 |
101 | 102 | - Go to the Actions panel 103 | - Run the "Step 1: Prep Release" workflow 104 | - Check the draft changelog 105 | - Run the "Step 2: Publish Release" workflow 106 | 107 | ## Publishing to `conda-forge` 108 | 109 | 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 110 | 111 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. 112 | -------------------------------------------------------------------------------- /binder/environment.yml: -------------------------------------------------------------------------------- 1 | # a mybinder.org-ready environment for demoing lckr_jupyterlab_variableinspector 2 | # this environment may also be used locally on Linux/MacOS/Windows, e.g. 3 | # 4 | # conda env update --file binder/environment.yml 5 | # conda activate lckr-jupyterlab-variableinspector-demo 6 | # 7 | name: lckr-jupyterlab-variableinspector-demo 8 | 9 | channels: 10 | - conda-forge 11 | 12 | dependencies: 13 | # runtime dependencies 14 | - python >=3.10,<3.11.0a0 15 | - jupyterlab >=4.0.0,<5 16 | # labextension build dependencies 17 | - nodejs >=18,<19 18 | - pip 19 | - wheel 20 | # additional packages for demos 21 | - ipywidgets 22 | -------------------------------------------------------------------------------- /binder/postBuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ perform a development install of lckr_jupyterlab_variableinspector 3 | 4 | On Binder, this will run _after_ the environment has been fully created from 5 | the environment.yml in this directory. 6 | 7 | This script should also run locally on Linux/MacOS/Windows: 8 | 9 | python3 binder/postBuild 10 | """ 11 | import subprocess 12 | import sys 13 | from pathlib import Path 14 | 15 | 16 | ROOT = Path.cwd() 17 | 18 | def _(*args, **kwargs): 19 | """ Run a command, echoing the args 20 | 21 | fails hard if something goes wrong 22 | """ 23 | print("\n\t", " ".join(args), "\n") 24 | return_code = subprocess.call(args, **kwargs) 25 | if return_code != 0: 26 | print("\nERROR", return_code, " ".join(args)) 27 | sys.exit(return_code) 28 | 29 | # verify the environment is self-consistent before even starting 30 | _(sys.executable, "-m", "pip", "check") 31 | 32 | # install the labextension 33 | _(sys.executable, "-m", "pip", "install", "-e", ".") 34 | _(sys.executable, "-m", "jupyter", "labextension", "develop", "--overwrite", ".") 35 | 36 | # verify the environment the extension didn't break anything 37 | _(sys.executable, "-m", "pip", "check") 38 | 39 | # list the extensions 40 | _("jupyter", "server", "extension", "list") 41 | 42 | # initially list installed extensions to determine if there are any surprises 43 | _("jupyter", "labextension", "list") 44 | 45 | 46 | print("JupyterLab with lckr_jupyterlab_variableinspector is ready to run with:\n") 47 | print("\tjupyter lab\n") 48 | -------------------------------------------------------------------------------- /early_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab-contrib/jupyterlab-variableInspector/1c11ea3d826a8a52311a2b0453548a54dc6ef503/early_demo.gif -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "lckr_jupyterlab_variableinspector", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package lckr_jupyterlab_variableinspector" 5 | } 6 | -------------------------------------------------------------------------------- /ipywidgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab-contrib/jupyterlab-variableInspector/1c11ea3d826a8a52311a2b0453548a54dc6ef503/ipywidgets.png -------------------------------------------------------------------------------- /lckr_jupyterlab_variableinspector/__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 'lckr_jupyterlab_variableinspector' outside a proper installation.") 9 | __version__ = "dev" 10 | 11 | 12 | def _jupyter_labextension_paths(): 13 | return [{ 14 | "src": "labextension", 15 | "dest": "@lckr/jupyterlab_variableinspector" 16 | }] 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lckr/jupyterlab_variableinspector", 3 | "version": "3.2.4", 4 | "description": "Variable inspector extension for JupyterLab", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension", 9 | "variable inspector" 10 | ], 11 | "homepage": "https://github.com/jupyterlab-contrib/jupyterlab-variableInspector", 12 | "bugs": { 13 | "url": "https://github.com/jupyterlab-contrib/jupyterlab-variableInspector/issues" 14 | }, 15 | "license": "BSD-3-Clause", 16 | "author": "lckr", 17 | "files": [ 18 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 19 | "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}", 20 | "style/index.js" 21 | ], 22 | "main": "lib/index.js", 23 | "types": "lib/index.d.ts", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/jupyterlab-contrib/jupyterlab-variableInspector.git" 27 | }, 28 | "scripts": { 29 | "build": "jlpm build:lib && jlpm build:labextension:dev", 30 | "build:labextension": "jupyter labextension build .", 31 | "build:labextension:dev": "jupyter labextension build --development True .", 32 | "build:lib": "tsc --sourceMap", 33 | "build:lib:prod": "tsc", 34 | "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", 35 | "clean": "jlpm clean:lib", 36 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", 37 | "clean:labextension": "rimraf lckr_jupyterlab_variableinspector/labextension lckr_jupyterlab_variableinspector/_version.py", 38 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 39 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 40 | "eslint": "jlpm eslint:check --fix", 41 | "eslint:check": "eslint . --cache --ext .ts,.tsx", 42 | "install:extension": "jlpm build", 43 | "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", 44 | "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", 45 | "prettier": "jlpm prettier:base --write --list-different", 46 | "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", 47 | "prettier:check": "jlpm prettier:base --check", 48 | "stylelint": "jlpm stylelint:check --fix", 49 | "stylelint:check": "stylelint --cache \"style/**/*.css\"", 50 | "watch": "run-p watch:src watch:labextension", 51 | "watch:labextension": "jupyter labextension watch .", 52 | "watch:src": "tsc -w --sourceMap" 53 | }, 54 | "packageManager": "yarn@3.5.0", 55 | "dependencies": { 56 | "@jupyter/web-components": "^0.15.0", 57 | "@jupyterlab/application": "^4.0.0", 58 | "@jupyterlab/apputils": "^4.0.0", 59 | "@jupyterlab/console": "^4.0.0", 60 | "@jupyterlab/coreutils": "^6.0.0", 61 | "@jupyterlab/notebook": "^4.0.0", 62 | "@jupyterlab/outputarea": "^4.0.0", 63 | "@jupyterlab/rendermime": "^4.0.0", 64 | "@jupyterlab/services": "^7.0.0", 65 | "@jupyterlab/ui-components": "^4.0.0", 66 | "@lumino/coreutils": "^2.0.0", 67 | "@lumino/datagrid": "^2.0.0", 68 | "@lumino/disposable": "^2.0.0", 69 | "@lumino/signaling": "^2.0.0", 70 | "@lumino/widgets": "^2.0.0", 71 | "react": "^18.2.0", 72 | "wildcard-match": "^5.1.2" 73 | }, 74 | "devDependencies": { 75 | "@jupyterlab/builder": "^4.0.0", 76 | "@types/json-schema": "^7.0.11", 77 | "@types/react": "^18.0.26", 78 | "@types/react-addons-linked-state-mixin": "^0.14.22", 79 | "@typescript-eslint/eslint-plugin": "^6.1.0", 80 | "@typescript-eslint/parser": "^6.1.0", 81 | "css-loader": "^6.7.1", 82 | "eslint": "^8.36.0", 83 | "eslint-config-prettier": "^8.8.0", 84 | "eslint-plugin-prettier": "^5.0.0", 85 | "npm-run-all": "^4.1.5", 86 | "prettier": "^3.0.0", 87 | "rimraf": "^5.0.1", 88 | "source-map-loader": "^1.0.2", 89 | "style-loader": "^3.3.1", 90 | "stylelint": "^15.10.1", 91 | "stylelint-config-recommended": "^13.0.0", 92 | "stylelint-config-standard": "^34.0.0", 93 | "stylelint-csstree-validator": "^3.0.0", 94 | "stylelint-prettier": "^4.0.0", 95 | "typescript": "~5.0.2", 96 | "yjs": "^13.5.40" 97 | }, 98 | "jupyterlab": { 99 | "schemaDir": "schema", 100 | "extension": true, 101 | "outputDir": "lckr_jupyterlab_variableinspector/labextension", 102 | "sharedPackages": { 103 | "@jupyter/web-components": { 104 | "bundled": true, 105 | "singleton": true 106 | }, 107 | "@microsoft/fast-components": { 108 | "bundled": true, 109 | "singleton": true 110 | }, 111 | "@microsoft/fast-element": { 112 | "bundled": true, 113 | "singleton": true 114 | }, 115 | "@microsoft/fast-foundation": { 116 | "bundled": true, 117 | "singleton": true 118 | } 119 | } 120 | }, 121 | "styleModule": "style/index.js", 122 | "eslintIgnore": [ 123 | "node_modules", 124 | "dist", 125 | "coverage", 126 | "**/*.d.ts", 127 | "tests", 128 | "**/__tests__", 129 | "ui-tests" 130 | ], 131 | "eslintConfig": { 132 | "extends": [ 133 | "eslint:recommended", 134 | "plugin:@typescript-eslint/eslint-recommended", 135 | "plugin:@typescript-eslint/recommended", 136 | "plugin:prettier/recommended" 137 | ], 138 | "parser": "@typescript-eslint/parser", 139 | "parserOptions": { 140 | "project": "tsconfig.json", 141 | "sourceType": "module" 142 | }, 143 | "plugins": [ 144 | "@typescript-eslint" 145 | ], 146 | "rules": { 147 | "@typescript-eslint/naming-convention": [ 148 | "error", 149 | { 150 | "selector": "interface", 151 | "format": [ 152 | "PascalCase" 153 | ], 154 | "custom": { 155 | "regex": "^I[A-Z]", 156 | "match": true 157 | } 158 | } 159 | ], 160 | "@typescript-eslint/no-unused-vars": [ 161 | "warn", 162 | { 163 | "args": "none" 164 | } 165 | ], 166 | "@typescript-eslint/no-explicit-any": "off", 167 | "@typescript-eslint/no-namespace": "off", 168 | "@typescript-eslint/no-use-before-define": "off", 169 | "@typescript-eslint/quotes": [ 170 | "error", 171 | "single", 172 | { 173 | "avoidEscape": true, 174 | "allowTemplateLiterals": false 175 | } 176 | ], 177 | "curly": [ 178 | "error", 179 | "all" 180 | ], 181 | "eqeqeq": "error", 182 | "prefer-arrow-callback": "error" 183 | } 184 | }, 185 | "prettier": { 186 | "singleQuote": true, 187 | "trailingComma": "none", 188 | "arrowParens": "avoid", 189 | "endOfLine": "auto", 190 | "overrides": [ 191 | { 192 | "files": "package.json", 193 | "options": { 194 | "tabWidth": 4 195 | } 196 | } 197 | ] 198 | }, 199 | "stylelint": { 200 | "extends": [ 201 | "stylelint-config-recommended", 202 | "stylelint-config-standard", 203 | "stylelint-prettier/recommended" 204 | ], 205 | "plugins": [ 206 | "stylelint-csstree-validator" 207 | ], 208 | "rules": { 209 | "csstree/validator": true, 210 | "property-no-vendor-prefix": null, 211 | "selector-class-pattern": null, 212 | "selector-no-vendor-prefix": null, 213 | "value-no-vendor-prefix": null 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /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 = "lckr_jupyterlab_variableinspector" 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 | ] 27 | dynamic = ["version", "description", "authors", "urls", "keywords"] 28 | 29 | [tool.hatch.version] 30 | source = "nodejs" 31 | 32 | [tool.hatch.metadata.hooks.nodejs] 33 | fields = ["description", "authors", "urls"] 34 | 35 | [tool.hatch.build.targets.sdist] 36 | artifacts = ["lckr_jupyterlab_variableinspector/labextension"] 37 | exclude = [".github", "binder"] 38 | 39 | [tool.hatch.build.targets.wheel.shared-data] 40 | "lckr_jupyterlab_variableinspector/labextension" = "share/jupyter/labextensions/@lckr/jupyterlab_variableinspector" 41 | "install.json" = "share/jupyter/labextensions/@lckr/jupyterlab_variableinspector/install.json" 42 | 43 | [tool.hatch.build.hooks.version] 44 | path = "lckr_jupyterlab_variableinspector/_version.py" 45 | 46 | [tool.hatch.build.hooks.jupyter-builder] 47 | dependencies = ["hatch-jupyter-builder>=0.5"] 48 | build-function = "hatch_jupyter_builder.npm_builder" 49 | ensured-targets = [ 50 | "lckr_jupyterlab_variableinspector/labextension/static/style.js", 51 | "lckr_jupyterlab_variableinspector/labextension/package.json", 52 | ] 53 | skip-if-exists = ["lckr_jupyterlab_variableinspector/labextension/static/style.js"] 54 | 55 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 56 | build_cmd = "build:prod" 57 | npm = ["jlpm"] 58 | 59 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] 60 | build_cmd = "install:extension" 61 | npm = ["jlpm"] 62 | source_dir = "src" 63 | build_dir = "lckr_jupyterlab_variableinspector/labextension" 64 | 65 | [tool.jupyter-releaser.options] 66 | version_cmd = "hatch version" 67 | 68 | [tool.jupyter-releaser.hooks] 69 | before-build-npm = [ 70 | "python -m pip install 'jupyterlab>=4.0.0,<5'", 71 | "jlpm", 72 | "jlpm build:prod" 73 | ] 74 | before-build-python = ["jlpm clean:all"] 75 | 76 | [tool.check-wheel-contents] 77 | ignore = ["W002"] 78 | -------------------------------------------------------------------------------- /schema/jupyterlab-variableInspector-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "jupyter.lab.setting-icon": "ui-components:list", 3 | "title": "Variable Inspector", 4 | "description": "Settings for the jupyterlab-variableInspector extension.", 5 | "type": "object", 6 | "properties": { 7 | "maxItems": { 8 | "type": "number", 9 | "minimum": 0, 10 | "title": "Maximum number of items", 11 | "description": "The maximum number of items to show in lists/dicts etc", 12 | "default": 10 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | lckr_jupyterlab_variableinspector setup 3 | """ 4 | 5 | __import__("setuptools").setup() 6 | -------------------------------------------------------------------------------- /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 | import { ISessionContext } from '@jupyterlab/apputils'; 2 | 3 | import { ISettingRegistry } from '@jupyterlab/settingregistry'; 4 | 5 | import { IExecuteResult } from '@jupyterlab/nbformat'; 6 | 7 | import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; 8 | 9 | import { KernelMessage, Kernel } from '@jupyterlab/services'; 10 | 11 | import { 12 | IExecuteInputMsg, 13 | IExecuteReplyMsg, 14 | IExecuteRequestMsg 15 | } from '@jupyterlab/services/lib/kernel/messages'; 16 | 17 | import { Signal, ISignal } from '@lumino/signaling'; 18 | 19 | import { JSONModel, DataModel } from '@lumino/datagrid'; 20 | 21 | import { IVariableInspector } from './tokens'; 22 | 23 | import { KernelConnector } from './kernelconnector'; 24 | 25 | abstract class AbstractHandler implements IVariableInspector.IInspectable { 26 | private _isDisposed = false; 27 | private _disposed = new Signal(this); 28 | protected _inspected = new Signal< 29 | IVariableInspector.IInspectable, 30 | IVariableInspector.IVariableInspectorUpdate 31 | >(this); 32 | protected _connector: KernelConnector; 33 | protected _rendermime: IRenderMimeRegistry | null = null; 34 | private _enabled: boolean; 35 | 36 | constructor(connector: KernelConnector) { 37 | this._connector = connector; 38 | this._enabled = false; 39 | } 40 | 41 | get enabled(): boolean { 42 | return this._enabled; 43 | } 44 | 45 | set enabled(value: boolean) { 46 | this._enabled = value; 47 | } 48 | 49 | get disposed(): ISignal { 50 | return this._disposed; 51 | } 52 | 53 | get isDisposed(): boolean { 54 | return this._isDisposed; 55 | } 56 | 57 | get inspected(): ISignal< 58 | IVariableInspector.IInspectable, 59 | IVariableInspector.IVariableInspectorUpdate 60 | > { 61 | return this._inspected; 62 | } 63 | 64 | get rendermime(): IRenderMimeRegistry | null { 65 | return this._rendermime; 66 | } 67 | 68 | abstract performInspection(): void; 69 | 70 | abstract performMatrixInspection( 71 | varName: string, 72 | maxRows: number 73 | ): Promise; 74 | 75 | abstract performWidgetInspection( 76 | varName: string 77 | ): Kernel.IShellFuture< 78 | KernelMessage.IExecuteRequestMsg, 79 | KernelMessage.IExecuteReplyMsg 80 | >; 81 | 82 | dispose(): void { 83 | if (this.isDisposed) { 84 | return; 85 | } 86 | this._isDisposed = true; 87 | this._disposed.emit(); 88 | Signal.clearData(this); 89 | } 90 | 91 | performDelete(varName: string): void { 92 | //noop 93 | } 94 | } 95 | 96 | /** 97 | * An object that handles code inspection. 98 | */ 99 | export class VariableInspectionHandler extends AbstractHandler { 100 | private _initScript: string; 101 | private _queryCommand: string; 102 | private _matrixQueryCommand: string; 103 | private _widgetQueryCommand: string; 104 | private _deleteCommand: string; 105 | private _changeSettingsCommand: 106 | | ((settings: IVariableInspector.ISettings) => string) 107 | | undefined; 108 | private _ready: Promise; 109 | private _id: string; 110 | private _setting: ISettingRegistry.ISettings; 111 | 112 | constructor(options: VariableInspectionHandler.IOptions) { 113 | super(options.connector); 114 | this._id = options.id; 115 | this._rendermime = options.rendermime ?? null; 116 | this._queryCommand = options.queryCommand; 117 | this._matrixQueryCommand = options.matrixQueryCommand; 118 | this._widgetQueryCommand = options.widgetQueryCommand; 119 | this._changeSettingsCommand = options.changeSettingsCommand; 120 | this._deleteCommand = options.deleteCommand; 121 | this._initScript = options.initScript; 122 | this._setting = options.setting; 123 | 124 | this._ready = this._connector.ready.then(() => { 125 | this._initOnKernel().then((msg: KernelMessage.IExecuteReplyMsg) => { 126 | this.performSettingsChange(); 127 | this._connector.iopubMessage.connect(this._queryCall); 128 | return; 129 | }); 130 | }); 131 | 132 | const onKernelReset = (sender: unknown, kernelReady: Promise) => { 133 | const title: IVariableInspector.IVariableTitle = { 134 | contextName: 'Waiting for kernel... ' 135 | }; 136 | this._inspected.emit({ 137 | title: title, 138 | payload: [] 139 | } as IVariableInspector.IVariableInspectorUpdate); 140 | 141 | this._ready = kernelReady.then(() => { 142 | this._initOnKernel().then((msg: KernelMessage.IExecuteReplyMsg) => { 143 | this.performSettingsChange(); 144 | this._connector.iopubMessage.connect(this._queryCall); 145 | this.performInspection(); 146 | }); 147 | }); 148 | }; 149 | 150 | this._setting.changed.connect(async () => { 151 | await this._ready; 152 | 153 | this.performSettingsChange(); 154 | this.performInspection(); 155 | }); 156 | 157 | this._connector.kernelRestarted.connect(onKernelReset); 158 | this._connector.kernelChanged.connect(onKernelReset); 159 | } 160 | 161 | get id(): string { 162 | return this._id; 163 | } 164 | 165 | get ready(): Promise { 166 | return this._ready; 167 | } 168 | 169 | /** 170 | * Performs an inspection by sending an execute request with the query command to the kernel. 171 | */ 172 | performInspection(): void { 173 | if (!this.enabled) { 174 | return; 175 | } 176 | 177 | const content: KernelMessage.IExecuteRequestMsg['content'] = { 178 | code: this._queryCommand, 179 | stop_on_error: false, 180 | store_history: false 181 | }; 182 | this._connector.fetch(content, this._handleQueryResponse); 183 | } 184 | 185 | /** 186 | * Performs an inspection of a Jupyter Widget 187 | */ 188 | performWidgetInspection( 189 | varName: string 190 | ): Kernel.IShellFuture { 191 | const request: KernelMessage.IExecuteRequestMsg['content'] = { 192 | code: this._widgetQueryCommand + '(' + varName + ')', 193 | stop_on_error: false, 194 | store_history: false 195 | }; 196 | return this._connector.execute(request); 197 | } 198 | 199 | /** 200 | * Performs an inspection of the specified matrix. 201 | */ 202 | performMatrixInspection( 203 | varName: string, 204 | maxRows = 100000 205 | ): Promise { 206 | const request: KernelMessage.IExecuteRequestMsg['content'] = { 207 | code: this._matrixQueryCommand + '(' + varName + ', ' + maxRows + ')', 208 | stop_on_error: false, 209 | store_history: false 210 | }; 211 | const con = this._connector; 212 | return new Promise((resolve, reject) => { 213 | con.fetch(request, (response: KernelMessage.IIOPubMessage) => { 214 | const msgType = response.header.msg_type; 215 | switch (msgType) { 216 | case 'execute_result': { 217 | const payload = response.content as IExecuteResult; 218 | let content: string = payload.data['text/plain'] as string; 219 | content = content.replace(/^'|'$/g, ''); 220 | content = content.replace(/\\"/g, '"'); 221 | content = content.replace(/\\'/g, "\\\\'"); 222 | 223 | const modelOptions = JSON.parse(content) as JSONModel.IOptions; 224 | const jsonModel = new JSONModel(modelOptions); 225 | resolve(jsonModel); 226 | break; 227 | } 228 | case 'error': 229 | console.log(response); 230 | reject("Kernel error on 'matrixQuery' call!"); 231 | break; 232 | default: 233 | break; 234 | } 235 | }); 236 | }); 237 | } 238 | 239 | /** 240 | * Send a kernel request to delete a variable from the global environment 241 | */ 242 | performDelete(varName: string): void { 243 | const content: KernelMessage.IExecuteRequestMsg['content'] = { 244 | code: this._deleteCommand + "('" + varName + "')", 245 | stop_on_error: false, 246 | store_history: false 247 | }; 248 | 249 | this._connector.fetch(content, this._handleQueryResponse); 250 | } 251 | 252 | /** 253 | * Send a kernel request to change settings 254 | */ 255 | performSettingsChange(): void { 256 | if (!this._changeSettingsCommand) { 257 | return; 258 | } 259 | 260 | const settings: IVariableInspector.ISettings = { 261 | maxItems: this._setting.get('maxItems').composite as number 262 | }; 263 | 264 | const content: KernelMessage.IExecuteRequestMsg['content'] = { 265 | code: this._changeSettingsCommand(settings), 266 | stop_on_error: false, 267 | store_history: false 268 | }; 269 | 270 | this._connector.fetch(content, this._handleQueryResponse); 271 | } 272 | 273 | /** 274 | * Initializes the kernel by running the set up script located at _initScriptPath. 275 | */ 276 | private _initOnKernel(): Promise { 277 | const content: KernelMessage.IExecuteRequestMsg['content'] = { 278 | code: this._initScript, 279 | stop_on_error: false, 280 | silent: true 281 | }; 282 | 283 | return this._connector.fetch(content, () => { 284 | //no op 285 | }); 286 | } 287 | 288 | /* 289 | * Handle query response. Emit new signal containing the IVariableInspector.IInspectorUpdate object. 290 | * (TODO: query resp. could be forwarded to panel directly) 291 | */ 292 | private _handleQueryResponse = ( 293 | response: KernelMessage.IIOPubMessage 294 | ): void => { 295 | const msgType = response.header.msg_type; 296 | switch (msgType) { 297 | case 'execute_result': { 298 | const payload = response.content as IExecuteResult; 299 | let content: string = payload.data['text/plain'] as string; 300 | if (content.slice(0, 1) === "'" || content.slice(0, 1) === '"') { 301 | content = content.slice(1, -1); 302 | content = content.replace(/\\"/g, '"').replace(/\\'/g, "'"); 303 | } 304 | 305 | const update = JSON.parse(content) as IVariableInspector.IVariable[]; 306 | const title = { 307 | contextName: '', 308 | kernelName: this._connector.kernelName || '' 309 | }; 310 | 311 | this._inspected.emit({ title: title, payload: update }); 312 | break; 313 | } 314 | case 'display_data': { 315 | const payloadDisplay = response.content as IExecuteResult; 316 | let contentDisplay: string = payloadDisplay.data[ 317 | 'text/plain' 318 | ] as string; 319 | if ( 320 | contentDisplay.slice(0, 1) === "'" || 321 | contentDisplay.slice(0, 1) === '"' 322 | ) { 323 | contentDisplay = contentDisplay.slice(1, -1); 324 | contentDisplay = contentDisplay 325 | .replace(/\\"/g, '"') 326 | .replace(/\\'/g, "'"); 327 | } 328 | 329 | const updateDisplay = JSON.parse( 330 | contentDisplay 331 | ) as IVariableInspector.IVariable[]; 332 | 333 | const titleDisplay = { 334 | contextName: '', 335 | kernelName: this._connector.kernelName || '' 336 | }; 337 | 338 | this._inspected.emit({ title: titleDisplay, payload: updateDisplay }); 339 | break; 340 | } 341 | default: 342 | break; 343 | } 344 | }; 345 | 346 | /* 347 | * Invokes a inspection if the signal emitted from specified session is an 'execute_input' msg. 348 | */ 349 | private _queryCall = ( 350 | sess: ISessionContext, 351 | msg: KernelMessage.IMessage 352 | ): void => { 353 | const msgType = msg.header.msg_type; 354 | switch (msgType) { 355 | case 'execute_input': { 356 | const code = (msg as IExecuteInputMsg).content.code; 357 | if ( 358 | !(code === this._queryCommand) && 359 | !(code === this._matrixQueryCommand) && 360 | !code.startsWith(this._widgetQueryCommand) 361 | ) { 362 | this.performInspection(); 363 | } 364 | break; 365 | } 366 | default: 367 | break; 368 | } 369 | }; 370 | } 371 | 372 | /** 373 | * A name space for inspection handler statics. 374 | */ 375 | export namespace VariableInspectionHandler { 376 | /** 377 | * The instantiation options for an inspection handler. 378 | */ 379 | export interface IOptions { 380 | connector: KernelConnector; 381 | rendermime?: IRenderMimeRegistry; 382 | queryCommand: string; 383 | matrixQueryCommand: string; 384 | widgetQueryCommand: string; 385 | changeSettingsCommand?(settings: IVariableInspector.ISettings): string; 386 | deleteCommand: string; 387 | initScript: string; 388 | id: string; 389 | setting: ISettingRegistry.ISettings; 390 | } 391 | } 392 | 393 | export class DummyHandler extends AbstractHandler { 394 | constructor(connector: KernelConnector) { 395 | super(connector); 396 | } 397 | 398 | performInspection(): void { 399 | const title: IVariableInspector.IVariableTitle = { 400 | contextName: '. Language currently not supported. ', 401 | kernelName: this._connector.kernelName || '' 402 | }; 403 | this._inspected.emit({ 404 | title: title, 405 | payload: [] 406 | } as IVariableInspector.IVariableInspectorUpdate); 407 | } 408 | 409 | performMatrixInspection( 410 | varName: string, 411 | maxRows: number 412 | ): Promise { 413 | return new Promise((resolve, reject) => { 414 | reject('Cannot inspect matrices w/ the DummyHandler!'); 415 | }); 416 | } 417 | 418 | performWidgetInspection( 419 | varName: string 420 | ): Kernel.IShellFuture< 421 | KernelMessage.IExecuteRequestMsg, 422 | KernelMessage.IExecuteReplyMsg 423 | > { 424 | const request: KernelMessage.IExecuteRequestMsg['content'] = { 425 | code: '', 426 | stop_on_error: false, 427 | store_history: false 428 | }; 429 | return this._connector.execute(request); 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ICommandPalette, WidgetTracker } from '@jupyterlab/apputils'; 2 | 3 | import { 4 | ILabShell, 5 | ILayoutRestorer, 6 | JupyterFrontEnd, 7 | JupyterFrontEndPlugin 8 | } from '@jupyterlab/application'; 9 | 10 | import { IConsoleTracker } from '@jupyterlab/console'; 11 | 12 | import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; 13 | 14 | import { ISettingRegistry } from '@jupyterlab/settingregistry'; 15 | 16 | import { listIcon } from '@jupyterlab/ui-components'; 17 | 18 | import { DummyHandler, VariableInspectionHandler } from './handler'; 19 | 20 | import { Languages } from './inspectorscripts'; 21 | 22 | import { KernelConnector } from './kernelconnector'; 23 | 24 | import { VariableInspectorManager } from './manager'; 25 | 26 | import { VariableInspectorPanel } from './variableinspector'; 27 | 28 | import { IVariableInspector, IVariableInspectorManager } from './tokens'; 29 | import { addJupyterLabThemeChangeListener } from '@jupyter/web-components'; 30 | addJupyterLabThemeChangeListener(); 31 | namespace CommandIDs { 32 | export const open = 'variableinspector:open'; 33 | } 34 | 35 | const SETTINGS_ID = 36 | '@lckr/jupyterlab_variableinspector:jupyterlab-variableInspector-settings'; 37 | 38 | /** 39 | * A service providing variable introspection. 40 | */ 41 | const variableinspector: JupyterFrontEndPlugin = { 42 | id: '@lckr/jupyterlab_variableinspector', 43 | requires: [ICommandPalette, ILayoutRestorer, ILabShell], 44 | provides: IVariableInspectorManager, 45 | autoStart: true, 46 | activate: ( 47 | app: JupyterFrontEnd, 48 | palette: ICommandPalette, 49 | restorer: ILayoutRestorer, 50 | labShell: ILabShell 51 | ): IVariableInspectorManager => { 52 | const manager = new VariableInspectorManager(); 53 | const category = 'Variable Inspector'; 54 | const command = CommandIDs.open; 55 | const label = 'Open Variable Inspector'; 56 | const namespace = 'variableinspector'; 57 | const tracker = new WidgetTracker({ namespace }); 58 | 59 | /** 60 | * Create and track a new inspector. 61 | */ 62 | function newPanel(): VariableInspectorPanel { 63 | const panel = new VariableInspectorPanel(); 64 | 65 | panel.id = 'jp-variableinspector'; 66 | panel.title.label = 'Variable Inspector'; 67 | panel.title.icon = listIcon; 68 | panel.title.closable = true; 69 | panel.disposed.connect(() => { 70 | if (manager.panel === panel) { 71 | manager.panel = null; 72 | } 73 | }); 74 | 75 | //Track the inspector panel 76 | tracker.add(panel); 77 | 78 | return panel; 79 | } 80 | 81 | // Enable state restoration 82 | restorer.restore(tracker, { 83 | command, 84 | args: () => ({}), 85 | name: () => 'variableinspector' 86 | }); 87 | 88 | // Add command to palette 89 | app.commands.addCommand(command, { 90 | label, 91 | execute: () => { 92 | if (!manager.panel || manager.panel.isDisposed) { 93 | manager.panel = newPanel(); 94 | } 95 | if (!manager.panel.isAttached) { 96 | labShell.add(manager.panel, 'main'); 97 | } 98 | if (manager.source) { 99 | manager.source.performInspection(); 100 | } 101 | labShell.activateById(manager.panel.id); 102 | } 103 | }); 104 | palette.addItem({ command, category }); 105 | 106 | console.log( 107 | 'JupyterLab extension @lckr/jupyterlab_variableinspector is activated!' 108 | ); 109 | return manager; 110 | } 111 | }; 112 | 113 | /** 114 | * An extension that registers consoles for variable inspection. 115 | */ 116 | const consoles: JupyterFrontEndPlugin = { 117 | id: '@lckr/jupyterlab-variableinspector:consoles', 118 | requires: [ 119 | IVariableInspectorManager, 120 | IConsoleTracker, 121 | ILabShell, 122 | ISettingRegistry 123 | ], 124 | autoStart: true, 125 | activate: async ( 126 | app: JupyterFrontEnd, 127 | manager: IVariableInspectorManager, 128 | consoles: IConsoleTracker, 129 | labShell: ILabShell, 130 | settings: ISettingRegistry 131 | ): Promise => { 132 | const handlers: { 133 | [id: string]: Promise; 134 | } = {}; 135 | const setting = await settings.load(SETTINGS_ID); 136 | 137 | /** 138 | * Subscribes to the creation of new consoles. If a new notebook is created, build a new handler for the consoles. 139 | * Adds a promise for a instanced handler to the 'handlers' collection. 140 | */ 141 | consoles.widgetAdded.connect((sender, consolePanel) => { 142 | if (manager.hasHandler(consolePanel.sessionContext.path)) { 143 | handlers[consolePanel.id] = new Promise((resolve, reject) => { 144 | resolve(manager.getHandler(consolePanel.sessionContext.path)); 145 | }); 146 | } else { 147 | handlers[consolePanel.id] = new Promise((resolve, reject) => { 148 | const session = consolePanel.sessionContext; 149 | 150 | // Create connector and init w script if it exists for kernel type. 151 | const connector = new KernelConnector({ session }); 152 | const scripts: Promise = 153 | connector.ready.then(() => { 154 | return connector.kernelLanguage.then(lang => { 155 | return Languages.getScript(lang); 156 | }); 157 | }); 158 | 159 | scripts.then((result: Languages.LanguageModel) => { 160 | const initScript = result.initScript; 161 | const queryCommand = result.queryCommand; 162 | const matrixQueryCommand = result.matrixQueryCommand; 163 | const widgetQueryCommand = result.widgetQueryCommand; 164 | const deleteCommand = result.deleteCommand; 165 | const changeSettingsCommand = result.changeSettingsCommand; 166 | 167 | const options: VariableInspectionHandler.IOptions = { 168 | queryCommand, 169 | matrixQueryCommand, 170 | widgetQueryCommand, 171 | deleteCommand, 172 | connector, 173 | initScript, 174 | changeSettingsCommand, 175 | id: session.path, //Using the sessions path as an identifier for now. 176 | setting 177 | }; 178 | const handler = new VariableInspectionHandler(options); 179 | manager.addHandler(handler); 180 | consolePanel.disposed.connect(() => { 181 | delete handlers[consolePanel.id]; 182 | handler.dispose(); 183 | }); 184 | 185 | handler.ready.then(() => { 186 | resolve(handler); 187 | }); 188 | }); 189 | 190 | //Otherwise log error message. 191 | scripts.catch((result: string) => { 192 | console.log(result); 193 | const handler = new DummyHandler(connector); 194 | consolePanel.disposed.connect(() => { 195 | delete handlers[consolePanel.id]; 196 | handler.dispose(); 197 | }); 198 | 199 | resolve(handler); 200 | }); 201 | }); 202 | } 203 | 204 | setSource(labShell); 205 | }); 206 | 207 | const setSource = (sender: ILabShell, args?: ILabShell.IChangedArgs) => { 208 | const widget = args?.newValue ?? sender.currentWidget; 209 | if (!widget || !consoles.has(widget)) { 210 | return; 211 | } 212 | const future = handlers[widget.id]; 213 | future.then((source: IVariableInspector.IInspectable) => { 214 | if (source) { 215 | manager.source = source; 216 | manager.source.performInspection(); 217 | } 218 | }); 219 | }; 220 | /** 221 | * If focus window changes, checks whether new focus widget is a console. 222 | * In that case, retrieves the handler associated to the console after it has been 223 | * initialized and updates the manager with it. 224 | */ 225 | setSource(labShell); 226 | labShell.currentChanged.connect(setSource); 227 | 228 | app.contextMenu.addItem({ 229 | command: CommandIDs.open, 230 | selector: '.jp-CodeConsole' 231 | }); 232 | } 233 | }; 234 | 235 | /** 236 | * An extension that registers notebooks for variable inspection. 237 | */ 238 | const notebooks: JupyterFrontEndPlugin = { 239 | id: '@lckr/jupyterlab-variableinspector:notebooks', 240 | requires: [ 241 | IVariableInspectorManager, 242 | INotebookTracker, 243 | ILabShell, 244 | ISettingRegistry 245 | ], 246 | autoStart: true, 247 | activate: async ( 248 | app: JupyterFrontEnd, 249 | manager: IVariableInspectorManager, 250 | notebooks: INotebookTracker, 251 | labShell: ILabShell, 252 | settings: ISettingRegistry 253 | ): Promise => { 254 | const handlers: { [id: string]: Promise } = {}; 255 | const setting = await settings.load(SETTINGS_ID); 256 | 257 | /** 258 | * Subscribes to the creation of new notebooks. If a new notebook is created, build a new handler for the notebook. 259 | * Adds a promise for a instanced handler to the 'handlers' collection. 260 | */ 261 | notebooks.widgetAdded.connect((sender, nbPanel: NotebookPanel) => { 262 | //A promise that resolves after the initialization of the handler is done. 263 | handlers[nbPanel.id] = new Promise((resolve, reject) => { 264 | const session = nbPanel.sessionContext; 265 | const connector = new KernelConnector({ session }); 266 | const rendermime = nbPanel.content.rendermime; 267 | 268 | const scripts: Promise = connector.ready.then( 269 | async () => { 270 | const lang = await connector.kernelLanguage; 271 | return Languages.getScript(lang); 272 | } 273 | ); 274 | 275 | scripts.then((result: Languages.LanguageModel) => { 276 | const initScript = result.initScript; 277 | const queryCommand = result.queryCommand; 278 | const matrixQueryCommand = result.matrixQueryCommand; 279 | const widgetQueryCommand = result.widgetQueryCommand; 280 | const deleteCommand = result.deleteCommand; 281 | const changeSettingsCommand = result.changeSettingsCommand; 282 | 283 | const options: VariableInspectionHandler.IOptions = { 284 | queryCommand, 285 | matrixQueryCommand, 286 | widgetQueryCommand, 287 | deleteCommand, 288 | connector, 289 | rendermime, 290 | initScript, 291 | changeSettingsCommand, 292 | id: session.path, //Using the sessions path as an identifier for now. 293 | setting 294 | }; 295 | const handler = new VariableInspectionHandler(options); 296 | manager.addHandler(handler); 297 | nbPanel.disposed.connect(() => { 298 | delete handlers[nbPanel.id]; 299 | handler.dispose(); 300 | }); 301 | 302 | handler.ready.then(() => { 303 | resolve(handler); 304 | }); 305 | }); 306 | 307 | //Otherwise log error message. 308 | scripts.catch((result: string) => { 309 | reject(result); 310 | }); 311 | }); 312 | 313 | setSource(labShell); 314 | }); 315 | 316 | const setSource = (sender: ILabShell, args?: ILabShell.IChangedArgs) => { 317 | const widget = args?.newValue ?? sender.currentWidget; 318 | if (!widget || !notebooks.has(widget) || widget.isDisposed) { 319 | return; 320 | } 321 | const future = handlers[widget.id]; 322 | future?.then((source: VariableInspectionHandler) => { 323 | if (source) { 324 | manager.source = source; 325 | manager.source.performInspection(); 326 | } 327 | }); 328 | }; 329 | /** 330 | * If focus window changes, checks whether new focus widget is a notebook. 331 | * In that case, retrieves the handler associated to the notebook after it has been 332 | * initialized and updates the manager with it. 333 | */ 334 | setSource(labShell); 335 | labShell.currentChanged.connect(setSource); 336 | 337 | app.contextMenu.addItem({ 338 | command: CommandIDs.open, 339 | selector: '.jp-Notebook' 340 | }); 341 | } 342 | }; 343 | 344 | /** 345 | * Export the plugins as default. 346 | */ 347 | const plugins: JupyterFrontEndPlugin[] = [ 348 | variableinspector, 349 | consoles, 350 | notebooks 351 | ]; 352 | export default plugins; 353 | -------------------------------------------------------------------------------- /src/inspectorscripts.ts: -------------------------------------------------------------------------------- 1 | import { IVariableInspector } from './tokens'; 2 | 3 | export namespace Languages { 4 | export type LanguageModel = { 5 | initScript: string; 6 | queryCommand: string; 7 | matrixQueryCommand: string; 8 | widgetQueryCommand: string; 9 | deleteCommand: string; 10 | changeSettingsCommand?: (settings: IVariableInspector.ISettings) => string; 11 | }; 12 | } 13 | 14 | export abstract class Languages { 15 | /** 16 | * Init and query script for supported languages. 17 | */ 18 | 19 | static py_script = `import json 20 | import sys 21 | from importlib import __import__ 22 | from itertools import islice 23 | import collections 24 | from IPython import get_ipython 25 | from IPython.core.magics.namespace import NamespaceMagics 26 | 27 | 28 | _jupyterlab_variableinspector_nms = NamespaceMagics() 29 | _jupyterlab_variableinspector_Jupyter = get_ipython() 30 | _jupyterlab_variableinspector_nms.shell = _jupyterlab_variableinspector_Jupyter.kernel.shell 31 | 32 | _jupyterlab_variableinspector_maxitems = 10 33 | 34 | __np = None 35 | __pd = None 36 | __pyspark = None 37 | __tf = None 38 | __K = None 39 | __torch = None 40 | __ipywidgets = None 41 | __xr = None 42 | 43 | 44 | def _attempt_import(module): 45 | try: 46 | # Only "import" if it was already imported 47 | if module in sys.modules: 48 | return __import__(module) 49 | except ImportError: 50 | return None 51 | 52 | 53 | def _check_imported(): 54 | global __np, __pd, __pyspark, __tf, __K, __torch, __ipywidgets, __xr 55 | 56 | __np = _attempt_import('numpy') 57 | __pd = _attempt_import('pandas') 58 | __pyspark = _attempt_import('pyspark') 59 | __tf = _attempt_import('tensorflow') 60 | __K = _attempt_import('keras.backend') or _attempt_import('tensorflow.keras.backend') 61 | __torch = _attempt_import('torch') 62 | __ipywidgets = _attempt_import('ipywidgets') 63 | __xr = _attempt_import('xarray') 64 | 65 | 66 | def _jupyterlab_variableinspector_changesettings(maxitems, **kwargs): 67 | global _jupyterlab_variableinspector_maxitems 68 | 69 | _jupyterlab_variableinspector_maxitems = maxitems 70 | 71 | 72 | def _jupyterlab_variableinspector_getsizeof(x): 73 | if type(x).__name__ in ['ndarray', 'Series']: 74 | return x.nbytes 75 | elif __pyspark and isinstance(x, __pyspark.sql.DataFrame): 76 | return "?" 77 | elif __tf and isinstance(x, __tf.Variable): 78 | return "?" 79 | elif __torch and isinstance(x, __torch.Tensor): 80 | return x.element_size() * x.nelement() 81 | elif __pd and type(x).__name__ == 'DataFrame': 82 | # DO NOT CALL df.memory_usage() for big dataframes as this can be very costly 83 | # to the point of making the kernel unresponsive or crashing it 84 | if len(x.columns) < 10_000: 85 | return x.memory_usage().sum() 86 | else: 87 | return "?" 88 | else: 89 | return sys.getsizeof(x) 90 | 91 | 92 | def _jupyterlab_variableinspector_getshapeof(x): 93 | if __pd and isinstance(x, __pd.DataFrame): 94 | return "%d rows x %d cols" % x.shape 95 | if __pd and isinstance(x, __pd.Series): 96 | return "%d rows" % x.shape 97 | if __np and isinstance(x, __np.ndarray): 98 | shape = " x ".join([str(i) for i in x.shape]) 99 | return "%s" % shape 100 | if __pyspark and isinstance(x, __pyspark.sql.DataFrame): 101 | return "? rows x %d cols" % len(x.columns) 102 | if __tf and isinstance(x, __tf.Variable): 103 | shape = " x ".join([str(int(i)) for i in x.shape]) 104 | return "%s" % shape 105 | if __tf and isinstance(x, __tf.Tensor): 106 | shape = " x ".join([str(int(i)) for i in x.shape]) 107 | return "%s" % shape 108 | if __torch and isinstance(x, __torch.Tensor): 109 | shape = " x ".join([str(int(i)) for i in x.shape]) 110 | return "%s" % shape 111 | if __xr and isinstance(x, __xr.DataArray): 112 | shape = " x ".join([str(int(i)) for i in x.shape]) 113 | return "%s" % shape 114 | if isinstance(x, list): 115 | return "%s" % len(x) 116 | if isinstance(x, dict): 117 | return "%s keys" % len(x) 118 | return None 119 | 120 | 121 | def _jupyterlab_variableinspector_getcontentof(x): 122 | # returns content in a friendly way for python variables 123 | # pandas and numpy 124 | if isinstance(x, (bool, str, int, float, type(None))): 125 | content = str(x) 126 | elif isinstance(x, (list, tuple)): 127 | if len(x) <= _jupyterlab_variableinspector_maxitems: 128 | content = str(x) 129 | else: 130 | content = "[" 131 | for i in range(_jupyterlab_variableinspector_maxitems): 132 | content += f"{x[i]}, " 133 | content += "...]" 134 | elif isinstance(x, collections.abc.Mapping): 135 | if len(x.keys()) <= _jupyterlab_variableinspector_maxitems: 136 | content = str(x) 137 | else: 138 | first_ten_keys = list(islice(x.keys(), _jupyterlab_variableinspector_maxitems)) 139 | content = "{" 140 | for idx, key in enumerate(first_ten_keys): 141 | if idx > 0: 142 | content += ", " 143 | content += f'"{key}": {x[key]}' 144 | content += ", ...}" 145 | elif __pd and isinstance(x, __pd.DataFrame): 146 | if len(x.columns) <= _jupyterlab_variableinspector_maxitems: 147 | colnames = ', '.join(x.columns.map(str)) 148 | content = "Columns: %s" % colnames 149 | else: 150 | content = "Columns: " 151 | for idx in range(_jupyterlab_variableinspector_maxitems): 152 | if idx > 0: 153 | content += ", " 154 | content += str(x.columns[idx]) 155 | content += ", ..." 156 | return content 157 | elif __pd and isinstance(x, __pd.Series): 158 | content = str(x.values).replace(" ", ", ")[1:-1] 159 | content = content.replace("\\n", "") 160 | elif __np and isinstance(x, __np.ndarray): 161 | content = x.__repr__() 162 | elif __xr and isinstance(x, __xr.DataArray): 163 | content = x.values.__repr__() 164 | else: 165 | content = str(x) 166 | 167 | if len(content) > 150: 168 | return content[:150] + " ..." 169 | else: 170 | return content 171 | 172 | 173 | def _jupyterlab_variableinspector_is_matrix(x): 174 | # True if type(x).__name__ in ["DataFrame", "ndarray", "Series"] else False 175 | if __pd and isinstance(x, __pd.DataFrame): 176 | return True 177 | if __pd and isinstance(x, __pd.Series): 178 | return True 179 | if __np and isinstance(x, __np.ndarray) and len(x.shape) <= 2: 180 | return True 181 | if __pyspark and isinstance(x, __pyspark.sql.DataFrame): 182 | return True 183 | if __tf and isinstance(x, __tf.Variable) and len(x.shape) <= 2: 184 | return True 185 | if __tf and isinstance(x, __tf.Tensor) and len(x.shape) <= 2: 186 | return True 187 | if __torch and isinstance(x, __torch.Tensor) and len(x.shape) <= 2: 188 | return True 189 | if __xr and isinstance(x, __xr.DataArray) and len(x.shape) <= 2: 190 | return True 191 | if isinstance(x, list): 192 | return True 193 | return False 194 | 195 | 196 | def _jupyterlab_variableinspector_is_widget(x): 197 | return __ipywidgets and issubclass(x, __ipywidgets.DOMWidget) 198 | 199 | 200 | def _jupyterlab_variableinspector_dict_list(): 201 | _check_imported() 202 | def keep_cond(v): 203 | try: 204 | obj = eval(v) 205 | if isinstance(obj, (bool, str, list, tuple, collections.abc.Mapping, int, float, type(None))): 206 | return True 207 | if __tf and isinstance(obj, __tf.Variable): 208 | return True 209 | if __pd and __pd is not None and ( 210 | isinstance(obj, __pd.core.frame.DataFrame) 211 | or isinstance(obj, __pd.core.series.Series)): 212 | return True 213 | if __xr and __xr is not None and isinstance(obj, __xr.DataArray): 214 | return True 215 | if str(obj)[0] == "<": 216 | return False 217 | if v in ['__np', '__pd', '__pyspark', '__tf', '__K', '__torch', '__ipywidgets', '__xr']: 218 | return obj is not None 219 | if str(obj).startswith("_Feature"): 220 | # removes tf/keras objects 221 | return False 222 | return True 223 | except: 224 | return False 225 | values = _jupyterlab_variableinspector_nms.who_ls() 226 | vardic = [ 227 | { 228 | 'varName': _v, 229 | 'varType': type(eval(_v)).__name__, 230 | 'varSize': str(_jupyterlab_variableinspector_getsizeof(eval(_v))), 231 | 'varShape': str(_jupyterlab_variableinspector_getshapeof(eval(_v))) if _jupyterlab_variableinspector_getshapeof(eval(_v)) else '', 232 | 'varContent': str(_jupyterlab_variableinspector_getcontentof(eval(_v))), 233 | 'isMatrix': _jupyterlab_variableinspector_is_matrix(eval(_v)), 234 | 'isWidget': _jupyterlab_variableinspector_is_widget(type(eval(_v))) 235 | } 236 | for _v in values if keep_cond(_v) 237 | ] 238 | return json.dumps(vardic, ensure_ascii=False) 239 | 240 | 241 | def _jupyterlab_variableinspector_getmatrixcontent(x, max_rows=10000): 242 | # to do: add something to handle this in the future 243 | threshold = max_rows 244 | 245 | if __pd and __pyspark and isinstance(x, __pyspark.sql.DataFrame): 246 | df = x.limit(threshold).toPandas() 247 | return _jupyterlab_variableinspector_getmatrixcontent(df.copy()) 248 | elif __np and __pd and type(x).__name__ == "DataFrame": 249 | if threshold is not None: 250 | x = x.head(threshold) 251 | x.columns = x.columns.map(str) 252 | return x.to_json(orient="table", default_handler=_jupyterlab_variableinspector_default, force_ascii=False) 253 | elif __np and __pd and type(x).__name__ == "Series": 254 | if threshold is not None: 255 | x = x.head(threshold) 256 | return x.to_json(orient="table", default_handler=_jupyterlab_variableinspector_default, force_ascii=False) 257 | elif __np and __pd and type(x).__name__ == "ndarray": 258 | df = __pd.DataFrame(x) 259 | return _jupyterlab_variableinspector_getmatrixcontent(df) 260 | elif __tf and (isinstance(x, __tf.Variable) or isinstance(x, __tf.Tensor)): 261 | df = __K.get_value(x) 262 | return _jupyterlab_variableinspector_getmatrixcontent(df) 263 | elif __torch and isinstance(x, __torch.Tensor): 264 | df = x.cpu().numpy() 265 | return _jupyterlab_variableinspector_getmatrixcontent(df) 266 | elif __xr and isinstance(x, __xr.DataArray): 267 | df = x.to_numpy() 268 | return _jupyterlab_variableinspector_getmatrixcontent(df) 269 | elif isinstance(x, list): 270 | s = __pd.Series(x) 271 | return _jupyterlab_variableinspector_getmatrixcontent(s) 272 | 273 | 274 | def _jupyterlab_variableinspector_displaywidget(widget): 275 | display(widget) 276 | 277 | 278 | def _jupyterlab_variableinspector_default(o): 279 | if isinstance(o, __np.number): return int(o) 280 | raise TypeError 281 | 282 | 283 | def _jupyterlab_variableinspector_deletevariable(x): 284 | exec("del %s" % x, globals()) 285 | `; 286 | 287 | static r_script = `library(repr) 288 | 289 | .ls.objects = function (pos = 1, pattern, order.by, decreasing = FALSE, head = FALSE, 290 | n = 5) 291 | { 292 | napply <- function(names, fn) sapply(names, function(x) fn(get(x, 293 | pos = pos))) 294 | names <- ls(pos = pos, pattern = pattern) 295 | if (length(names) == 0) { 296 | return(jsonlite::toJSON(data.frame())) 297 | } 298 | obj.class <- napply(names, function(x) as.character(class(x))[1]) 299 | obj.mode <- napply(names, mode) 300 | obj.type <- ifelse(is.na(obj.class), obj.mode, obj.class) 301 | obj.size <- napply(names, object.size) 302 | obj.dim <- t(napply(names, function(x) as.numeric(dim(x))[1:2])) 303 | obj.content <- rep("NA", length(names)) 304 | has_no_dim <- is.na(obj.dim)[1:length(names)] 305 | obj.dim[has_no_dim, 1] <- napply(names, length)[has_no_dim] 306 | vec <- (obj.type != "function") 307 | obj.content[vec] <- napply(names[vec], function(x) toString(x, width = 154)[1]) 308 | 309 | obj.rownames <- napply(names, rownames) 310 | has_rownames <- obj.rownames != "NULL" 311 | obj.rownames <- sapply(obj.rownames[has_rownames], function(x) paste(x, 312 | collapse=", ")) 313 | obj.rownames.short <- sapply(obj.rownames, function(x) paste(substr(x, 1, 150), "....")) 314 | obj.rownames <- ifelse(nchar(obj.rownames) > 154, obj.rownames.short, obj.rownames) 315 | obj.rownames <- sapply(obj.rownames, function(x) paste("Row names: ",x)) 316 | obj.content[has_rownames] <- obj.rownames 317 | 318 | 319 | obj.colnames <- napply(names, colnames) 320 | has_colnames <- obj.colnames != "NULL" 321 | obj.colnames <- sapply(obj.colnames[has_colnames], function(x) paste(x, 322 | collapse = ", ")) 323 | obj.colnames.short <- sapply(obj.colnames, function(x) paste(substr(x, 324 | 1, 150), "....")) 325 | obj.colnames <- ifelse(nchar(obj.colnames) > 154, obj.colnames.short, 326 | obj.colnames) 327 | obj.colnames <- sapply(obj.colnames, function(x) paste("Column names: ",x)) 328 | 329 | obj.content[has_colnames] <- obj.colnames 330 | 331 | is_function <- (obj.type == "function") 332 | obj.content[is_function] <- napply(names[is_function], function(x) paste(strsplit(repr_text(x),")")[[1]][1],")",sep="")) 333 | obj.content <- unlist(obj.content, use.names = FALSE) 334 | 335 | 336 | out <- data.frame(obj.type, obj.size, obj.dim) 337 | names(out) <- c("varType", "varSize", "Rows", "Columns") 338 | out$varShape <- paste(out$Rows, " x ", out$Columns) 339 | out$varContent <- obj.content 340 | out$isMatrix <- FALSE 341 | out$varName <- row.names(out) 342 | out <- out[, !(names(out) %in% c("Rows", "Columns"))] 343 | rownames(out) <- NULL 344 | print(out) 345 | if (!missing(order.by)) 346 | out <- out[order(out[[order.by]], decreasing = decreasing), 347 | ] 348 | if (head) 349 | out <- head(out, n) 350 | jsonlite::toJSON(out) 351 | } 352 | 353 | .deleteVariable <- function(x) { 354 | remove(list=c(x), envir=.GlobalEnv) 355 | } 356 | `; 357 | 358 | static scripts: { [index: string]: Languages.LanguageModel } = { 359 | python3: { 360 | initScript: Languages.py_script, 361 | queryCommand: '_jupyterlab_variableinspector_dict_list()', 362 | matrixQueryCommand: '_jupyterlab_variableinspector_getmatrixcontent', 363 | widgetQueryCommand: '_jupyterlab_variableinspector_displaywidget', 364 | deleteCommand: '_jupyterlab_variableinspector_deletevariable', 365 | changeSettingsCommand: (settings: IVariableInspector.ISettings) => 366 | `_jupyterlab_variableinspector_changesettings(maxitems=${settings.maxItems})` 367 | }, 368 | python2: { 369 | initScript: Languages.py_script, 370 | queryCommand: '_jupyterlab_variableinspector_dict_list()', 371 | matrixQueryCommand: '_jupyterlab_variableinspector_getmatrixcontent', 372 | widgetQueryCommand: '_jupyterlab_variableinspector_displaywidget', 373 | deleteCommand: '_jupyterlab_variableinspector_deletevariable', 374 | changeSettingsCommand: (settings: IVariableInspector.ISettings) => 375 | `_jupyterlab_variableinspector_changesettings(maxitems=${settings.maxItems})` 376 | }, 377 | python: { 378 | initScript: Languages.py_script, 379 | queryCommand: '_jupyterlab_variableinspector_dict_list()', 380 | matrixQueryCommand: '_jupyterlab_variableinspector_getmatrixcontent', 381 | widgetQueryCommand: '_jupyterlab_variableinspector_displaywidget', 382 | deleteCommand: '_jupyterlab_variableinspector_deletevariable', 383 | changeSettingsCommand: (settings: IVariableInspector.ISettings) => 384 | `_jupyterlab_variableinspector_changesettings(maxitems=${settings.maxItems})` 385 | }, 386 | R: { 387 | initScript: Languages.r_script, 388 | queryCommand: '.ls.objects()', 389 | matrixQueryCommand: '.ls.objects', 390 | widgetQueryCommand: 'TODO', 391 | deleteCommand: '.deleteVariable' 392 | }, 393 | scala: { 394 | initScript: 395 | '_root_.almond.api.JupyterAPIHolder.value.VariableInspector.init()', 396 | queryCommand: 397 | '_root_.almond.api.JupyterAPIHolder.value.VariableInspector.dictList()', 398 | matrixQueryCommand: '', // TODO 399 | widgetQueryCommand: '', // TODO 400 | deleteCommand: '' // TODO 401 | } 402 | }; 403 | 404 | static getScript(lang: string): Promise { 405 | return new Promise((resolve, reject) => { 406 | if (lang in Languages.scripts) { 407 | resolve(Languages.scripts[lang]); 408 | } else { 409 | reject('Language ' + lang + ' not supported yet!'); 410 | } 411 | }); 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /src/kernelconnector.ts: -------------------------------------------------------------------------------- 1 | import { ISessionContext } from '@jupyterlab/apputils'; 2 | 3 | import { KernelMessage } from '@jupyterlab/services'; 4 | 5 | import { IShellFuture } from '@jupyterlab/services/lib/kernel/kernel'; 6 | 7 | import { 8 | IExecuteReplyMsg, 9 | IExecuteRequestMsg 10 | } from '@jupyterlab/services/lib/kernel/messages'; 11 | 12 | import { ISignal, Signal } from '@lumino/signaling'; 13 | 14 | /** 15 | * Connector class that handles execute request to a kernel 16 | */ 17 | export class KernelConnector { 18 | private _session: ISessionContext; 19 | private _kernelChanged = new Signal>(this); 20 | private _kernelRestarted = new Signal>(this); 21 | 22 | constructor(options: KernelConnector.IOptions) { 23 | this._session = options.session; 24 | this._session.statusChanged.connect( 25 | (sender: ISessionContext, newStatus: KernelMessage.Status) => { 26 | switch (newStatus) { 27 | case 'restarting': 28 | case 'autorestarting': 29 | this._kernelRestarted.emit(this._session.ready); 30 | break; 31 | default: 32 | break; 33 | } 34 | } 35 | ); 36 | this._session.kernelChanged.connect(() => 37 | this._kernelChanged.emit(this._session.ready) 38 | ); 39 | } 40 | 41 | get kernelChanged(): ISignal> { 42 | return this._kernelChanged; 43 | } 44 | 45 | get kernelRestarted(): ISignal> { 46 | return this._kernelRestarted; 47 | } 48 | 49 | get kernelLanguage(): Promise { 50 | if (!this._session.session?.kernel) { 51 | return Promise.resolve(''); 52 | } 53 | 54 | return this._session.session.kernel.info.then(infoReply => { 55 | return infoReply.language_info.name; 56 | }); 57 | } 58 | 59 | get kernelName(): string { 60 | return this._session.kernelDisplayName; 61 | } 62 | 63 | /** 64 | * A Promise that is fulfilled when the session associated w/ the connector is ready. 65 | */ 66 | get ready(): Promise { 67 | return this._session.ready; 68 | } 69 | 70 | /** 71 | * A signal emitted for iopub messages of the kernel associated with the kernel. 72 | */ 73 | get iopubMessage(): ISignal { 74 | return this._session.iopubMessage; 75 | } 76 | 77 | /** 78 | * Executes the given request on the kernel associated with the connector. 79 | * @param content: IExecuteRequestMsg to forward to the kernel. 80 | * @param ioCallback: Callable to forward IOPub messages of the kernel to. 81 | * @returns Promise 82 | */ 83 | fetch( 84 | content: KernelMessage.IExecuteRequestMsg['content'], 85 | ioCallback: (msg: KernelMessage.IIOPubMessage) => any 86 | ): Promise { 87 | const kernel = this._session.session?.kernel; 88 | if (!kernel) { 89 | return Promise.reject( 90 | new Error('Require kernel to perform variable inspection!') 91 | ); 92 | } 93 | 94 | const future = kernel.requestExecute(content); 95 | 96 | future.onIOPub = (msg: KernelMessage.IIOPubMessage): void => { 97 | ioCallback(msg); 98 | }; 99 | return future.done as Promise; 100 | } 101 | 102 | execute( 103 | content: KernelMessage.IExecuteRequestMsg['content'] 104 | ): IShellFuture { 105 | if (!this._session.session?.kernel) { 106 | throw new Error('No session available.'); 107 | } 108 | return this._session.session.kernel.requestExecute(content); 109 | } 110 | } 111 | 112 | export namespace KernelConnector { 113 | export interface IOptions { 114 | session: ISessionContext; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/manager.ts: -------------------------------------------------------------------------------- 1 | import { VariableInspectionHandler } from './handler'; 2 | 3 | import { VariableInspectorPanel } from './variableinspector'; 4 | 5 | import { IVariableInspector, IVariableInspectorManager } from './tokens'; 6 | 7 | /** 8 | * A class that manages variable inspector widget instances and offers persistent 9 | * `IVariableInspector` instance that other plugins can communicate with. 10 | */ 11 | export class VariableInspectorManager implements IVariableInspectorManager { 12 | private _source: IVariableInspector.IInspectable | null = null; 13 | private _panel: VariableInspectorPanel | null = null; 14 | private _handlers: { [id: string]: VariableInspectionHandler } = {}; 15 | 16 | hasHandler(id: string): boolean { 17 | if (this._handlers[id]) { 18 | return true; 19 | } else { 20 | return false; 21 | } 22 | } 23 | 24 | getHandler(id: string): VariableInspectionHandler { 25 | return this._handlers[id]; 26 | } 27 | 28 | addHandler(handler: VariableInspectionHandler): void { 29 | this._handlers[handler.id] = handler; 30 | } 31 | 32 | /** 33 | * The current inspector panel. 34 | */ 35 | get panel(): VariableInspectorPanel | null { 36 | return this._panel; 37 | } 38 | 39 | set panel(panel: VariableInspectorPanel | null) { 40 | if (this.panel === panel) { 41 | return; 42 | } 43 | this._panel = panel; 44 | 45 | if (panel && !panel.source) { 46 | panel.source = this._source; 47 | } 48 | } 49 | 50 | /** 51 | * The source of events the inspector panel listens for. 52 | */ 53 | get source(): IVariableInspector.IInspectable | null { 54 | return this._source; 55 | } 56 | 57 | set source(source: IVariableInspector.IInspectable | null) { 58 | if (this._source === source) { 59 | return; 60 | } 61 | 62 | // remove subscriptions 63 | if (this._source) { 64 | this._source.disposed.disconnect(this._onSourceDisposed, this); 65 | } 66 | 67 | this._source = source; 68 | 69 | if (this._panel && !this._panel.isDisposed) { 70 | this._panel.source = this._source; 71 | } 72 | // Subscribe to new source 73 | if (this._source) { 74 | this._source.disposed.connect(this._onSourceDisposed, this); 75 | } 76 | } 77 | 78 | private _onSourceDisposed(): void { 79 | this._source = null; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/tokens.ts: -------------------------------------------------------------------------------- 1 | import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; 2 | import { Kernel, KernelMessage } from '@jupyterlab/services'; 3 | import { Token } from '@lumino/coreutils'; 4 | import { DataModel } from '@lumino/datagrid'; 5 | import { IObservableDisposable } from '@lumino/disposable'; 6 | import { ISignal } from '@lumino/signaling'; 7 | import type { VariableInspectionHandler } from './handler'; 8 | 9 | export const IVariableInspectorManager = new Token( 10 | 'jupyterlab_extension/variableinspector:IVariableInspectorManager' 11 | ); 12 | 13 | export interface IVariableInspectorManager { 14 | source: IVariableInspector.IInspectable | null; 15 | hasHandler(id: string): boolean; 16 | getHandler(id: string): VariableInspectionHandler; 17 | addHandler(handler: VariableInspectionHandler): void; 18 | } 19 | 20 | /** 21 | * The inspector panel token. 22 | */ 23 | export const IVariableInspector = new Token( 24 | 'jupyterlab_extension/variableinspector:IVariableInspector' 25 | ); 26 | 27 | /** 28 | * An interface for an inspector. 29 | */ 30 | export interface IVariableInspector { 31 | source: IVariableInspector.IInspectable | null; 32 | } 33 | 34 | /** 35 | * A namespace for inspector interfaces. 36 | */ 37 | export namespace IVariableInspector { 38 | export interface IInspectable extends IObservableDisposable { 39 | inspected: ISignal; 40 | rendermime: IRenderMimeRegistry | null; 41 | enabled: boolean; 42 | performInspection(): void; 43 | performMatrixInspection( 44 | varName: string, 45 | maxRows?: number 46 | ): Promise; 47 | performWidgetInspection( 48 | varName: string 49 | ): Kernel.IShellFuture< 50 | KernelMessage.IExecuteRequestMsg, 51 | KernelMessage.IExecuteReplyMsg 52 | >; 53 | performDelete(varName: string): void; 54 | } 55 | 56 | export interface ISettings { 57 | maxItems: number; 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 | 75 | export interface IVariableTitle { 76 | kernelName?: string; 77 | contextName?: string; //Context currently reserved for special information. 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/variableinspector.ts: -------------------------------------------------------------------------------- 1 | import { OutputAreaModel, SimplifiedOutputArea } from '@jupyterlab/outputarea'; 2 | 3 | import { closeIcon, searchIcon } from '@jupyterlab/ui-components'; 4 | 5 | import { DataGrid, DataModel } from '@lumino/datagrid'; 6 | 7 | import { DockLayout, Widget } from '@lumino/widgets'; 8 | 9 | import { IVariableInspector } from './tokens'; 10 | 11 | import { 12 | DataGrid as WebDataGrid, 13 | DataGridRow, 14 | DataGridCell, 15 | provideJupyterDesignSystem, 16 | Select, 17 | Option, 18 | TextField, 19 | Button, 20 | jpDataGrid, 21 | jpDataGridRow, 22 | jpDataGridCell, 23 | jpTextField, 24 | jpOption, 25 | jpSelect, 26 | jpButton 27 | } from '@jupyter/web-components'; 28 | provideJupyterDesignSystem().register( 29 | jpDataGrid(), 30 | jpDataGridRow(), 31 | jpDataGridCell(), 32 | jpTextField(), 33 | jpOption(), 34 | jpSelect(), 35 | jpButton() 36 | ); 37 | 38 | import wildcardMatch from 'wildcard-match'; 39 | import { Message } from '@lumino/messaging'; 40 | 41 | const TITLE_CLASS = 'jp-VarInspector-title'; 42 | const PANEL_CLASS = 'jp-VarInspector'; 43 | const TABLE_CLASS = 'jp-VarInspector-table'; 44 | const TABLE_ROW_CLASS = 'jp-VarInspector-table-row'; 45 | const TABLE_ROW_HIDDEN_CLASS = 'jp-VarInspector-table-row-hidden'; 46 | const TABLE_TYPE_CLASS = 'jp-VarInspector-type'; 47 | const TABLE_NAME_CLASS = 'jp-VarInspector-varName'; 48 | const FILTER_TYPE_CLASS = 'filter-type'; 49 | const FILTER_INPUT_CLASS = 'filter-input'; 50 | const FILTER_BUTTON_CLASS = 'filter-button'; 51 | const FILTER_LIST_CLASS = 'filter-list'; 52 | const FILTERED_BUTTON_CLASS = 'filtered-variable-button'; 53 | 54 | type FILTER_TYPES = 'type' | 'name'; 55 | 56 | /** 57 | * A panel that renders the variables 58 | */ 59 | export class VariableInspectorPanel 60 | extends Widget 61 | implements IVariableInspector 62 | { 63 | private _source: IVariableInspector.IInspectable | null = null; 64 | private _table: WebDataGrid; 65 | private _filteredTable: HTMLDivElement; 66 | private _title: HTMLElement; 67 | private _filtered: { type: Array; name: Array }; 68 | 69 | constructor() { 70 | super(); 71 | this.addClass(PANEL_CLASS); 72 | this._title = Private.createTitle(); 73 | this._title.className = TITLE_CLASS; 74 | this._table = Private.createTable(); 75 | this._table.className = TABLE_CLASS; 76 | this._filteredTable = Private.createFilterTable(); 77 | this.node.appendChild(this._title as HTMLElement); 78 | this.node.appendChild(this._filteredTable as HTMLElement); 79 | this.node.appendChild(this._table as HTMLElement); 80 | this._filtered = { type: [], name: [] }; 81 | this.intializeFilteredTable(); 82 | } 83 | 84 | //Sets up the filter table so when the filter button is pressed, a new filter is created 85 | protected intializeFilteredTable() { 86 | const filterType = this._filteredTable.querySelector( 87 | '.' + FILTER_TYPE_CLASS 88 | ) as Select; 89 | const filterInput = this._filteredTable.querySelector( 90 | '.' + FILTER_INPUT_CLASS 91 | ) as TextField; 92 | const filterButton = this._filteredTable.querySelector( 93 | '.' + FILTER_BUTTON_CLASS 94 | ) as Button; 95 | filterButton.addEventListener('click', () => { 96 | this.onFilterChange( 97 | filterType.value as FILTER_TYPES, 98 | filterInput.value, 99 | true 100 | ); 101 | }); 102 | } 103 | 104 | // Checks if string is in the filtered array 105 | protected stringInFilter(string: string, filterType: FILTER_TYPES) { 106 | // console.log(this._filtered[filterType]); 107 | for (let i = 0; i < this._filtered[filterType].length; i++) { 108 | const isMatch = wildcardMatch(this._filtered[filterType][i]); 109 | if (isMatch(string)) { 110 | return true; 111 | } 112 | } 113 | return false; 114 | } 115 | /* 116 | Either adds a new filter or removes a previously existing filter based 117 | Params: 118 | filterType: By what type the varName is filtering on 119 | varName: The name of the variable we are trying to filter out 120 | isAdding: If we are adding a new filter or removing a previous filter 121 | */ 122 | 123 | protected onFilterChange( 124 | filterType: FILTER_TYPES, 125 | varName: string, 126 | isAdding: boolean 127 | ) { 128 | if (varName === '') { 129 | return; 130 | } 131 | if (isAdding) { 132 | if (this._filtered[filterType].includes(varName)) { 133 | return; 134 | } 135 | this._filtered[filterType].push(varName); 136 | const filterList = this._filteredTable.querySelector( 137 | '.' + FILTER_LIST_CLASS 138 | ) as HTMLUListElement; 139 | const newFilteredButton = Private.createFilteredButton( 140 | varName, 141 | filterType 142 | ); 143 | newFilteredButton.addEventListener('click', () => { 144 | const filterText = newFilteredButton.querySelector( 145 | '.filtered-variable-button-text' 146 | ) as HTMLDivElement; 147 | this.onFilterChange(filterType, filterText.innerHTML, false); 148 | this.addFilteredOutRows(); 149 | newFilteredButton.remove(); 150 | }); 151 | filterList.appendChild(newFilteredButton); 152 | this.filterOutTable(); 153 | } else { 154 | this._filtered[filterType] = this._filtered[filterType].filter( 155 | filter => filter !== varName 156 | ); 157 | } 158 | } 159 | 160 | /* 161 | Goes through each filtered out row and checks if they should still be filtered 162 | If not, the row becomes visible again 163 | */ 164 | protected addFilteredOutRows() { 165 | const rows = this._table.querySelectorAll( 166 | '.' + TABLE_ROW_HIDDEN_CLASS 167 | ) as NodeListOf; 168 | for (let i = 0; i < rows.length; i++) { 169 | const rowName = rows[i].querySelector( 170 | '.' + TABLE_NAME_CLASS 171 | ) as DataGridCell; 172 | const rowType = rows[i].querySelector( 173 | '.' + TABLE_TYPE_CLASS 174 | ) as DataGridCell; 175 | if ( 176 | !this.stringInFilter(rowName.innerHTML, 'name') && 177 | !this._filtered['type'].includes(rowType.innerHTML) 178 | ) { 179 | rows[i].className = TABLE_ROW_CLASS; 180 | } 181 | } 182 | } 183 | 184 | /* 185 | Goes through each row and checks if the row should be filtered out 186 | A row is filtered out if it matches any of the values in the _filtered object 187 | */ 188 | protected filterOutTable() { 189 | const rows = this._table.querySelectorAll( 190 | '.' + TABLE_ROW_CLASS 191 | ) as NodeListOf; 192 | for (let i = 0; i < rows.length; i++) { 193 | const rowName = rows[i].querySelector( 194 | '.' + TABLE_NAME_CLASS 195 | ) as DataGridCell; 196 | const rowType = rows[i].querySelector( 197 | '.' + TABLE_TYPE_CLASS 198 | ) as DataGridCell; 199 | if ( 200 | this.stringInFilter(rowName.innerHTML, 'name') || 201 | this._filtered['type'].includes(rowType.innerHTML) 202 | ) { 203 | rows[i].className = TABLE_ROW_HIDDEN_CLASS; 204 | } 205 | } 206 | } 207 | 208 | /* 209 | Goes through each row and if it finds a variable with name 'name', then it deletes it 210 | */ 211 | protected removeRow(name: string) { 212 | const rows = this._table.querySelectorAll( 213 | '.' + TABLE_ROW_CLASS 214 | ) as NodeListOf; 215 | for (let i = 0; i < rows.length; i++) { 216 | const cell = rows[i].querySelector( 217 | '.' + TABLE_NAME_CLASS 218 | ) as DataGridCell; 219 | if (cell.innerHTML === name) { 220 | rows[i].remove(); 221 | return; 222 | } 223 | } 224 | } 225 | 226 | get source(): IVariableInspector.IInspectable | null { 227 | return this._source; 228 | } 229 | 230 | set source(source: IVariableInspector.IInspectable | null) { 231 | if (this._source === source) { 232 | // this._source.performInspection(); 233 | return; 234 | } 235 | //Remove old subscriptions 236 | if (this._source) { 237 | this._source.enabled = false; 238 | this._source.inspected.disconnect(this.onInspectorUpdate, this); 239 | this._source.disposed.disconnect(this.onSourceDisposed, this); 240 | } 241 | this._source = source; 242 | //Subscribe to new object 243 | if (this._source) { 244 | this._source.enabled = true; 245 | this._source.inspected.connect(this.onInspectorUpdate, this); 246 | this._source.disposed.connect(this.onSourceDisposed, this); 247 | this._source.performInspection(); 248 | } 249 | } 250 | 251 | /** 252 | * Dispose resources 253 | */ 254 | dispose(): void { 255 | if (this.isDisposed) { 256 | return; 257 | } 258 | if (this.source) { 259 | this.source.enabled = false; 260 | } 261 | this.source = null; 262 | super.dispose(); 263 | } 264 | 265 | protected onCloseRequest(msg: Message): void { 266 | super.onCloseRequest(msg); 267 | if (this._source) { 268 | this._source.enabled = false; 269 | } 270 | } 271 | 272 | protected onAfterShow(msg: Message): void { 273 | super.onAfterShow(msg); 274 | if (this._source) { 275 | this._source.enabled = true; 276 | this._source.performInspection(); 277 | } 278 | } 279 | 280 | protected onInspectorUpdate( 281 | sender: any, 282 | allArgs: IVariableInspector.IVariableInspectorUpdate 283 | ): void { 284 | if (!this.isAttached) { 285 | return; 286 | } 287 | 288 | const title = allArgs.title; 289 | const args = allArgs.payload; 290 | 291 | if (title.contextName) { 292 | this._title.innerHTML = title.contextName; 293 | } else { 294 | this._title.innerHTML = 295 | " Inspecting '" + title.kernelName + "' " + title.contextName; 296 | } 297 | 298 | this._table.innerHTML = ''; 299 | const headerRow = document.createElement('jp-data-grid-row') as DataGridRow; 300 | headerRow.className = 'sticky-header'; 301 | const columns = [' ', ' ', 'NAME', 'TYPE', 'SIZE', 'SHAPE', 'CONTENT']; 302 | for (let i = 0; i < columns.length; i++) { 303 | const headerCell = document.createElement( 304 | 'jp-data-grid-cell' 305 | ) as DataGridCell; 306 | headerCell.className = 'column-header'; 307 | headerCell.textContent = columns[i]; 308 | headerCell.gridColumn = (i + 1).toString(); 309 | headerRow.appendChild(headerCell); 310 | } 311 | this._table.appendChild(headerRow); 312 | 313 | //Render new variable state 314 | for (let index = 0; index < args.length; index++) { 315 | const item = args[index]; 316 | const name = item.varName; 317 | const varType = item.varType; 318 | 319 | const row = document.createElement('jp-data-grid-row') as DataGridRow; 320 | row.className = TABLE_ROW_CLASS; 321 | if (this._filtered['type'].includes(varType)) { 322 | row.className = TABLE_ROW_HIDDEN_CLASS; 323 | } else if (this.stringInFilter(name, 'name')) { 324 | row.className = TABLE_ROW_HIDDEN_CLASS; 325 | } 326 | 327 | // Add delete icon and onclick event 328 | let cell = document.createElement('jp-data-grid-cell') as DataGridCell; 329 | cell.title = 'Delete Variable'; 330 | cell.className = 'jp-VarInspector-deleteButton'; 331 | cell.gridColumn = '1'; 332 | const closeButton = document.createElement('jp-button') as Button; 333 | closeButton.appearance = 'stealth'; 334 | const ico = closeIcon.element(); 335 | ico.className = 'icon-button'; 336 | ico.onclick = (ev: MouseEvent): any => { 337 | this.removeRow(name); 338 | }; 339 | closeButton.append(ico); 340 | cell.append(closeButton); 341 | row.appendChild(cell); 342 | 343 | // Add onclick event for inspection 344 | cell = document.createElement('jp-data-grid-cell') as DataGridCell; 345 | if (item.isMatrix) { 346 | cell.title = 'View Contents'; 347 | cell.className = 'jp-VarInspector-inspectButton'; 348 | const searchButton = document.createElement('jp-button') as Button; 349 | searchButton.appearance = 'stealth'; 350 | const ico = searchIcon.element(); 351 | ico.className = 'icon-button'; 352 | ico.onclick = (ev: MouseEvent): any => { 353 | this._source 354 | ?.performMatrixInspection(item.varName) 355 | .then((model: DataModel) => { 356 | this._showMatrix(model, item.varName, item.varType); 357 | }); 358 | }; 359 | searchButton.append(ico); 360 | cell.append(searchButton); 361 | } else { 362 | cell.innerHTML = ''; 363 | } 364 | cell.gridColumn = '2'; 365 | row.appendChild(cell); 366 | 367 | cell = document.createElement('jp-data-grid-cell') as DataGridCell; 368 | cell.className = TABLE_NAME_CLASS; 369 | cell.innerHTML = name; 370 | cell.gridColumn = '3'; 371 | row.appendChild(cell); 372 | 373 | // Add remaining cells 374 | cell = document.createElement('jp-data-grid-cell') as DataGridCell; 375 | cell.innerHTML = varType; 376 | cell.className = TABLE_TYPE_CLASS; 377 | cell.gridColumn = '4'; 378 | row.appendChild(cell); 379 | cell = document.createElement('jp-data-grid-cell') as DataGridCell; 380 | cell.innerHTML = item.varSize; 381 | cell.gridColumn = '5'; 382 | row.appendChild(cell); 383 | cell = document.createElement('jp-data-grid-cell') as DataGridCell; 384 | cell.innerHTML = item.varShape; 385 | cell.gridColumn = '6'; 386 | row.appendChild(cell); 387 | 388 | cell = document.createElement('jp-data-grid-cell') as DataGridCell; 389 | const rendermime = this._source?.rendermime; 390 | if (item.isWidget && rendermime) { 391 | const model = new OutputAreaModel({ trusted: true }); 392 | const output = new SimplifiedOutputArea({ model, rendermime }); 393 | output.future = this._source!.performWidgetInspection(item.varName); 394 | Widget.attach(output, cell); 395 | } else { 396 | cell.innerHTML = Private.escapeHtml(item.varContent).replace( 397 | /\\n/g, 398 | '
' 399 | ); 400 | } 401 | cell.gridColumn = '7'; 402 | row.appendChild(cell); 403 | this._table.appendChild(row); 404 | } 405 | } 406 | 407 | /** 408 | * Handle source disposed signals. 409 | */ 410 | protected onSourceDisposed(sender: any, args: void): void { 411 | this.source = null; 412 | } 413 | 414 | private _showMatrix( 415 | dataModel: DataModel, 416 | name: string, 417 | varType: string 418 | ): void { 419 | const datagrid = new DataGrid({ 420 | defaultSizes: { 421 | rowHeight: 32, 422 | columnWidth: 128, 423 | rowHeaderWidth: 64, 424 | columnHeaderHeight: 32 425 | } 426 | }); 427 | 428 | datagrid.dataModel = dataModel; 429 | datagrid.title.label = varType + ': ' + name; 430 | datagrid.title.closable = true; 431 | const lout: DockLayout = this.parent!.layout as DockLayout; 432 | lout.addWidget(datagrid, { mode: 'split-right' }); 433 | //todo activate/focus matrix widget 434 | } 435 | } 436 | 437 | namespace Private { 438 | const entityMap = new Map( 439 | Object.entries({ 440 | '&': '&', 441 | '<': '<', 442 | '>': '>', 443 | '"': '"', 444 | "'": ''', 445 | '/': '/' 446 | }) 447 | ); 448 | 449 | export function escapeHtml(source: string): string { 450 | return String(source).replace( 451 | /[&<>"'/]/g, 452 | (s: string) => entityMap.get(s)! 453 | ); 454 | } 455 | 456 | export function createTable(): WebDataGrid { 457 | const table = document.createElement('jp-data-grid') as WebDataGrid; 458 | table.generateHeader = 'sticky'; 459 | table.gridTemplateColumns = '1fr 1fr 6fr 4fr 4fr 5fr 16fr'; 460 | return table; 461 | } 462 | 463 | export function createTitle(header = ''): HTMLParagraphElement { 464 | const title = document.createElement('p'); 465 | title.innerHTML = header; 466 | return title; 467 | } 468 | 469 | export function createFilterTable(): HTMLDivElement { 470 | const container = document.createElement('div'); 471 | container.className = 'filter-container'; 472 | const filterType = document.createElement('jp-select') as Select; 473 | filterType.className = FILTER_TYPE_CLASS; 474 | filterType.selectedIndex = 0; 475 | const varTypeOption = document.createElement('jp-option') as Option; 476 | varTypeOption.value = 'type'; 477 | varTypeOption.innerHTML = 'Type'; 478 | const nameOption = document.createElement('jp-option') as Option; 479 | nameOption.value = 'name'; 480 | nameOption.innerHTML = 'Name'; 481 | filterType.appendChild(varTypeOption); 482 | filterType.appendChild(nameOption); 483 | const searchContainer = document.createElement('div'); 484 | searchContainer.className = 'filter-search-container'; 485 | const input = document.createElement('jp-text-field') as TextField; 486 | input.setAttribute('type', 'text'); 487 | input.setAttribute('placeholder', 'Filter out variable'); 488 | input.className = FILTER_INPUT_CLASS; 489 | const filterButton = document.createElement('jp-button') as Button; 490 | filterButton.textContent = 'Filter'; 491 | filterButton.className = FILTER_BUTTON_CLASS; 492 | filterButton.appearance = 'accent'; 493 | const list = document.createElement('ul'); 494 | list.className = FILTER_LIST_CLASS; 495 | 496 | searchContainer.appendChild(filterType); 497 | searchContainer.appendChild(input); 498 | searchContainer.appendChild(filterButton); 499 | container.appendChild(searchContainer); 500 | container.appendChild(list); 501 | return container; 502 | } 503 | 504 | //Creates a button with given filter information displayed on the button 505 | export function createFilteredButton( 506 | filterName: string, 507 | filterType: FILTER_TYPES 508 | ): Button { 509 | const filteredButton = document.createElement('jp-button') as Button; 510 | filteredButton.value = filterType; 511 | filteredButton.title = filterType; 512 | filteredButton.className = FILTERED_BUTTON_CLASS; 513 | const filterButtonContent = document.createElement('div'); 514 | filterButtonContent.className = 'filter-button-content'; 515 | const buttonText = document.createElement('div'); 516 | buttonText.className = 'filtered-variable-button-text'; 517 | buttonText.innerHTML = filterName; 518 | closeIcon.element({ 519 | container: filterButtonContent 520 | }); 521 | filterButtonContent.insertAdjacentElement('afterbegin', buttonText); 522 | filteredButton.appendChild(filterButtonContent); 523 | filteredButton.className = FILTERED_BUTTON_CLASS; 524 | return filteredButton; 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /style/base.css: -------------------------------------------------------------------------------- 1 | .jp-VarInspector { 2 | flex-direction: column; 3 | overflow: auto; 4 | font-size: var(--jp-ui-font-size1); 5 | } 6 | 7 | .jp-VarInspector-table { 8 | font-family: monospace; 9 | border-collapse: collapse; 10 | margin: auto; 11 | width: 100%; 12 | color: var(--jp-content-font-color1); 13 | padding: 0 4px; 14 | } 15 | 16 | .jp-VarInspector-table td, 17 | .jp-VarInspector-table thead { 18 | border: 1px solid; 19 | border-color: var(--jp-layout-color2); 20 | padding: 8px; 21 | } 22 | 23 | .jp-VarInspector-table tr:nth-child(even) { 24 | background-color: var(--jp-layout-color1); 25 | } 26 | 27 | .jp-VarInspector-content tr:hover { 28 | background-color: var(--jp-layout-color2); 29 | } 30 | 31 | .jp-VarInspector-table thead { 32 | font-size: var(--jp-ui-font-size0); 33 | text-align: center; 34 | background-color: var(--jp-layout-color2); 35 | color: var(--jp-ui-font-color1); 36 | font-family: sans-serif; 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: 4px 10px; 47 | } 48 | 49 | .filter-container { 50 | display: flex; 51 | flex-direction: column; 52 | } 53 | 54 | .filter-search-container { 55 | display: flex; 56 | align-items: center; 57 | padding: 0 1rem; 58 | } 59 | 60 | .filter-input { 61 | width: 20rem !important; 62 | margin-left: 1rem; 63 | } 64 | 65 | .type-button { 66 | color: var(--jp-ui-font-color0); 67 | } 68 | 69 | .name-button { 70 | color: var(--jp-ui-font-color1); 71 | } 72 | 73 | .filter-list { 74 | display: flex; 75 | } 76 | 77 | .jp-VarInspector-table-row-hidden { 78 | display: none; 79 | } 80 | 81 | .jp-VarInspector-deleteButton { 82 | display: flex; 83 | justify-content: space-around; 84 | width: 1em; 85 | } 86 | 87 | .jp-VarInspector-inspectButton { 88 | display: flex; 89 | justify-content: space-around; 90 | width: 1em; 91 | } 92 | 93 | .jp-VarInspector-varName { 94 | font-weight: 600; 95 | } 96 | 97 | .filter-button-content { 98 | display: flex; 99 | align-items: center; 100 | gap: 0.5rem; 101 | } 102 | 103 | .icon-button { 104 | cursor: pointer; 105 | } 106 | -------------------------------------------------------------------------------- /style/index.css: -------------------------------------------------------------------------------- 1 | @import url('base.css'); 2 | -------------------------------------------------------------------------------- /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 | "sourceMap": true, 19 | "strict": true, 20 | "strictNullChecks": true, 21 | "target": "ES2018" 22 | }, 23 | "include": ["src/*"] 24 | } 25 | -------------------------------------------------------------------------------- /ui-tests/README.md: -------------------------------------------------------------------------------- 1 | # Integration Testing 2 | 3 | This folder contains the integration tests of the extension. 4 | 5 | They are defined using [Playwright](https://playwright.dev/docs/intro) test runner 6 | and [Galata](https://github.com/jupyterlab/jupyterlab/tree/main/galata) helper. 7 | 8 | The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). 9 | 10 | The JupyterLab server configuration to use for the integration test is defined 11 | in [jupyter_server_test_config.py](./jupyter_server_test_config.py). 12 | 13 | The default configuration will produce video for failing tests and an HTML report. 14 | 15 | > There is a new experimental UI mode that you may fall in love with; see [that video](https://www.youtube.com/watch?v=jF0yA-JLQW0). 16 | 17 | ## Run the tests 18 | 19 | > All commands are assumed to be executed from the root directory 20 | 21 | To run the tests, you need to: 22 | 23 | 1. Compile the extension: 24 | 25 | ```sh 26 | jlpm install 27 | jlpm build:prod 28 | ``` 29 | 30 | > Check the extension is installed in JupyterLab. 31 | 32 | 2. Install test dependencies (needed only once): 33 | 34 | ```sh 35 | cd ./ui-tests 36 | jlpm install 37 | jlpm playwright install 38 | cd .. 39 | ``` 40 | 41 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: 42 | 43 | ```sh 44 | cd ./ui-tests 45 | jlpm playwright test 46 | ``` 47 | 48 | Test results will be shown in the terminal. In case of any test failures, the test report 49 | will be opened in your browser at the end of the tests execution; see 50 | [Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) 51 | for configuring that behavior. 52 | 53 | ## Update the tests snapshots 54 | 55 | > All commands are assumed to be executed from the root directory 56 | 57 | If you are comparing snapshots to validate your tests, you may need to update 58 | the reference snapshots stored in the repository. To do that, you need to: 59 | 60 | 1. Compile the extension: 61 | 62 | ```sh 63 | jlpm install 64 | jlpm build:prod 65 | ``` 66 | 67 | > Check the extension is installed in JupyterLab. 68 | 69 | 2. Install test dependencies (needed only once): 70 | 71 | ```sh 72 | cd ./ui-tests 73 | jlpm install 74 | jlpm playwright install 75 | cd .. 76 | ``` 77 | 78 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) command: 79 | 80 | ```sh 81 | cd ./ui-tests 82 | jlpm playwright test -u 83 | ``` 84 | 85 | > Some discrepancy may occurs between the snapshots generated on your computer and 86 | > the one generated on the CI. To ease updating the snapshots on a PR, you can 87 | > type `please update playwright snapshots` to trigger the update by a bot on the CI. 88 | > Once the bot has computed new snapshots, it will commit them to the PR branch. 89 | 90 | ## Create tests 91 | 92 | > All commands are assumed to be executed from the root directory 93 | 94 | To create tests, the easiest way is to use the code generator tool of playwright: 95 | 96 | 1. Compile the extension: 97 | 98 | ```sh 99 | jlpm install 100 | jlpm build:prod 101 | ``` 102 | 103 | > Check the extension is installed in JupyterLab. 104 | 105 | 2. Install test dependencies (needed only once): 106 | 107 | ```sh 108 | cd ./ui-tests 109 | jlpm install 110 | jlpm playwright install 111 | cd .. 112 | ``` 113 | 114 | 3. Start the server: 115 | 116 | ```sh 117 | cd ./ui-tests 118 | jlpm start 119 | ``` 120 | 121 | 4. Execute the [Playwright code generator](https://playwright.dev/docs/codegen) in **another terminal**: 122 | 123 | ```sh 124 | cd ./ui-tests 125 | jlpm playwright codegen localhost:8888 126 | ``` 127 | 128 | ## Debug tests 129 | 130 | > All commands are assumed to be executed from the root directory 131 | 132 | To debug tests, a good way is to use the inspector tool of playwright: 133 | 134 | 1. Compile the extension: 135 | 136 | ```sh 137 | jlpm install 138 | jlpm build:prod 139 | ``` 140 | 141 | > Check the extension is installed in JupyterLab. 142 | 143 | 2. Install test dependencies (needed only once): 144 | 145 | ```sh 146 | cd ./ui-tests 147 | jlpm install 148 | jlpm playwright install 149 | cd .. 150 | ``` 151 | 152 | 3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): 153 | 154 | ```sh 155 | cd ./ui-tests 156 | jlpm playwright test --debug 157 | ``` 158 | 159 | ## Upgrade Playwright and the browsers 160 | 161 | To update the web browser versions, you must update the package `@playwright/test`: 162 | 163 | ```sh 164 | cd ./ui-tests 165 | jlpm up "@playwright/test" 166 | jlpm playwright install 167 | ``` 168 | -------------------------------------------------------------------------------- /ui-tests/jupyter_server_test_config.py: -------------------------------------------------------------------------------- 1 | """Server configuration for integration tests. 2 | 3 | !! Never use this configuration in production because it 4 | opens the server to the world and provide access to JupyterLab 5 | JavaScript objects through the global window variable. 6 | """ 7 | from jupyterlab.galata import configure_jupyter_server 8 | 9 | configure_jupyter_server(c) 10 | 11 | # Uncomment to set server log level to debug level 12 | # c.ServerApp.log_level = "DEBUG" 13 | -------------------------------------------------------------------------------- /ui-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lckr/jupyterlab_variableinspector-ui-tests", 3 | "version": "1.0.0", 4 | "description": "JupyterLab @lckr/jupyterlab_variableinspector Integration Tests", 5 | "private": true, 6 | "scripts": { 7 | "start": "jupyter lab --config jupyter_server_test_config.py", 8 | "test": "jlpm playwright test", 9 | "test:update": "jlpm playwright test --update-snapshots" 10 | }, 11 | "devDependencies": { 12 | "@jupyterlab/galata": "^5.0.5", 13 | "@playwright/test": "^1.37.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ui-tests/playwright.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration for Playwright using default from @jupyterlab/galata 3 | */ 4 | const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); 5 | 6 | module.exports = { 7 | ...baseConfig, 8 | webServer: { 9 | command: 'jlpm start', 10 | url: 'http://localhost:8888/lab', 11 | timeout: 120 * 1000, 12 | reuseExistingServer: !process.env.CI 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /ui-tests/tests/lckr_jupyterlab_variableinspector.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jupyterlab/galata'; 2 | 3 | test('test', async ({ page }) => { 4 | await page.getByText('Python 3 (ipykernel)').first().click(); 5 | await page.getByText('Python 3 (ipykernel) | Idle').waitFor(); 6 | await page.getByLabel('notebook content').getByRole('textbox').fill('a = 1'); 7 | await page.keyboard.press('Shift+Enter'); 8 | await page.getByRole('textbox').nth(1).fill('b = "hello"'); 9 | await page.keyboard.press('Control+Enter'); 10 | 11 | await page.getByRole('tabpanel').click({ 12 | button: 'right' 13 | }); 14 | await page.getByRole('menu').getByText('Open Variable Inspector').click(); 15 | 16 | // const rows = await page.locator('.jp-VarInspector-table-row'); 17 | 18 | const firstRow = await page.locator('.jp-VarInspector-table-row').first(); 19 | await expect 20 | .soft(firstRow.locator('.jp-VarInspector-varName')) 21 | .toHaveText(/a/); 22 | await expect 23 | .soft(firstRow.locator('.jp-VarInspector-type')) 24 | .toHaveText(/int/); 25 | await expect 26 | .soft(firstRow.locator('jp-data-grid-cell').nth(4)) 27 | .toHaveText(/\d\d/); 28 | await expect 29 | .soft(firstRow.locator('jp-data-grid-cell').last()) 30 | .toHaveText(/1/); 31 | const secondRow = await page.locator('.jp-VarInspector-table-row').last(); 32 | await expect 33 | .soft(secondRow.locator('.jp-VarInspector-varName')) 34 | .toHaveText(/b/); 35 | await expect 36 | .soft(secondRow.locator('.jp-VarInspector-type')) 37 | .toHaveText(/str/); 38 | await expect 39 | .soft(secondRow.locator('jp-data-grid-cell').nth(4)) 40 | .toHaveText(/\d\d/); 41 | await expect 42 | .soft(secondRow.locator('jp-data-grid-cell').last()) 43 | .toHaveText(/hello/); 44 | }); 45 | 46 | test('variable filter', async ({ page }) => { 47 | await page.getByText('Python 3 (ipykernel)').first().click(); 48 | await page.getByText('Python 3 (ipykernel) | Idle').waitFor(); 49 | await page.getByLabel('notebook content').getByRole('textbox').fill('a1 = 1'); 50 | await page.keyboard.press('Shift+Enter'); 51 | await page.getByRole('textbox').nth(1).fill('b1 = "hello"'); 52 | await page.keyboard.press('Control+Enter'); 53 | 54 | await page.getByRole('tabpanel').click({ 55 | button: 'right' 56 | }); 57 | await page.getByRole('menu').getByText('Open Variable Inspector').click(); 58 | 59 | //Filter out rows with int type 60 | await page.locator('.filter-input').pressSequentially('int'); 61 | await page.locator('.filter-button').click(); 62 | 63 | //expect.soft only to have one row with name b and type str 64 | await expect 65 | .soft(await page.locator('.jp-VarInspector-table-row').count()) 66 | .toEqual(1); 67 | const bRow = await page.locator('.jp-VarInspector-table-row').first(); 68 | await expect.soft(bRow.locator('.jp-VarInspector-varName')).toHaveText(/b1/); 69 | await expect.soft(bRow.locator('.jp-VarInspector-type')).toHaveText(/str/); 70 | await expect 71 | .soft(bRow.locator('jp-data-grid-cell').nth(4)) 72 | .toHaveText(/\d\d/); 73 | await expect 74 | .soft(bRow.locator('jp-data-grid-cell').last()) 75 | .toHaveText(/hello/); 76 | 77 | // Remove filter 78 | await page.locator('.filtered-variable-button').click(); 79 | 80 | //Filter out all variables with 1 in the name 81 | await page.evaluate('document.querySelector(".filter-type").value="name"'); 82 | await page.locator('.filter-input').pressSequentially('*1'); 83 | await page.locator('.filter-button').click(); 84 | 85 | //expect.softs no rows except for header 86 | await expect 87 | .soft(await page.locator('.jp-VarInspector-table-row').count()) 88 | .toEqual(0); 89 | 90 | //Remove the filter 91 | await page.locator('.filtered-variable-button').click(); 92 | await expect 93 | .soft(await page.locator('.jp-VarInspector-table-row').count()) 94 | .toEqual(2); 95 | 96 | //Filter out variables name b1 97 | await page.locator('.filter-input').pressSequentially('b1'); 98 | await page.locator('.filter-button').click(); 99 | 100 | //expect.soft one row with name a1 and type int 101 | await expect 102 | .soft(await page.locator('.jp-VarInspector-table-row').count()) 103 | .toEqual(1); 104 | const aRow = await page.locator('.jp-VarInspector-table-row').first(); 105 | await expect.soft(aRow.locator('.jp-VarInspector-varName')).toHaveText(/a1/); 106 | await expect.soft(aRow.locator('.jp-VarInspector-type')).toHaveText(/int/); 107 | await expect 108 | .soft(aRow.locator('jp-data-grid-cell').nth(4)) 109 | .toHaveText(/\d\d/); 110 | await expect.soft(aRow.locator('jp-data-grid-cell').last()).toHaveText(/1/); 111 | }); 112 | --------------------------------------------------------------------------------