├── .dockerignore ├── .github └── workflows │ ├── binder-on-pr.yml │ ├── build.yml │ ├── check-release.yml │ ├── prep-release.yml │ ├── publish-release.yml │ └── update-integration-tests.yml ├── .gitignore ├── .prettierignore ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASE.md ├── Taskfile.yml ├── babel.config.js ├── binder ├── environment.yml └── postBuild ├── conftest.py ├── dev.Dockerfile ├── docker-compose.dev.yml ├── docs ├── _static │ ├── format-all.gif │ ├── format-selected.gif │ ├── format-specific.gif │ └── settings.gif ├── changelog.md ├── conf.py ├── configuration.md ├── custom-formatter.md ├── dev.md ├── faq.md ├── getting-help.md ├── index.md ├── installation.md ├── jupyterhub.md ├── logo.png ├── usage.md └── your-support.md ├── install.json ├── jest.config.js ├── jupyter-config ├── nb-config │ └── jupyterlab_code_formatter.json └── server-config │ └── jupyterlab_code_formatter.json ├── jupyterlab_code_formatter ├── __init__.py ├── formatters.py ├── handlers.py └── tests │ ├── __init__.py │ ├── conftest.py │ ├── test_formatters.py │ └── test_handlers.py ├── package.json ├── pyproject.toml ├── requirements-dev.txt ├── requirements-readthedocs.txt ├── requirements-test.txt ├── requirements.txt ├── schema └── settings.json ├── scripts ├── build.sh ├── format.sh └── test.sh ├── setup.py ├── src ├── __tests__ │ └── jupyterlab_code_formatter.spec.ts ├── client.ts ├── constants.ts ├── formatter.ts └── index.ts ├── style ├── base.css ├── index.css └── index.js ├── test_snippets ├── some_python.py ├── test_notebook.ipynb ├── test_notebook_crablang.ipynb └── test_with_errors.ipynb ├── tsconfig.json ├── tsconfig.test.json ├── ui-tests ├── README.md ├── jupyter_server_test_config.py ├── package.json ├── playwright.config.js ├── tests │ └── jupyterlab_code_formatter.spec.ts └── yarn.lock └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.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: master 6 | pull_request: 7 | branches: '*' 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Base Setup 22 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 23 | 24 | - name: Install dependencies 25 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 26 | 27 | - name: Install R 28 | uses: r-lib/actions/setup-r@v2 29 | 30 | - name: Install R formatters 31 | run: Rscript -e 'install.packages(c("styler", "formatR"))' 32 | 33 | - name: Lint the extension 34 | run: | 35 | set -eux 36 | jlpm 37 | jlpm run lint:check 38 | 39 | - name: Test the extension 40 | run: | 41 | set -eux 42 | jlpm run test 43 | 44 | - name: Build the extension 45 | run: | 46 | set -eux 47 | python -m pip install .[test] 48 | 49 | pytest -vv -r ap --cov jupyterlab_code_formatter 50 | jupyter server extension list 51 | jupyter server extension list 2>&1 | grep -ie "jupyterlab_code_formatter.*OK" 52 | 53 | jupyter labextension list 54 | jupyter labextension list 2>&1 | grep -ie "jupyterlab_code_formatter.*OK" 55 | python -m jupyterlab.browser_check 56 | 57 | - name: Package the extension 58 | run: | 59 | set -eux 60 | 61 | pip install build 62 | python -m build 63 | pip uninstall -y "jupyterlab_code_formatter" jupyterlab 64 | 65 | - name: Upload extension packages 66 | uses: actions/upload-artifact@v4 67 | with: 68 | name: extension-artifacts 69 | path: dist/jupyterlab_code_formatter* 70 | if-no-files-found: error 71 | 72 | test_isolated: 73 | needs: build 74 | runs-on: ubuntu-latest 75 | 76 | steps: 77 | - name: Install Python 78 | uses: actions/setup-python@v5 79 | with: 80 | python-version: '3.9' 81 | architecture: 'x64' 82 | - uses: actions/download-artifact@v4 83 | with: 84 | name: extension-artifacts 85 | - name: Install and Test 86 | run: | 87 | set -eux 88 | # Remove NodeJS, twice to take care of system and locally installed node versions. 89 | sudo rm -rf $(which node) 90 | sudo rm -rf $(which node) 91 | 92 | pip install "jupyterlab>=4.0.0,<5" jupyterlab_code_formatter*.whl 93 | 94 | 95 | jupyter server extension list 96 | jupyter server extension list 2>&1 | grep -ie "jupyterlab_code_formatter.*OK" 97 | 98 | jupyter labextension list 99 | jupyter labextension list 2>&1 | grep -ie "jupyterlab_code_formatter.*OK" 100 | python -m jupyterlab.browser_check --no-chrome-test 101 | 102 | integration-tests: 103 | name: Integration tests 104 | needs: build 105 | runs-on: ubuntu-latest 106 | 107 | env: 108 | PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers 109 | 110 | steps: 111 | - name: Checkout 112 | uses: actions/checkout@v4 113 | 114 | - name: Base Setup 115 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 116 | 117 | - name: Download extension package 118 | uses: actions/download-artifact@v4 119 | with: 120 | name: extension-artifacts 121 | 122 | - name: Install the extension 123 | run: | 124 | set -eux 125 | python -m pip install "jupyterlab>=4.0.0,<5" jupyterlab_code_formatter*.whl 126 | 127 | - name: Install dependencies 128 | working-directory: ui-tests 129 | env: 130 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 131 | run: jlpm install 132 | 133 | - name: Set up browser cache 134 | uses: actions/cache@v4 135 | with: 136 | path: | 137 | ${{ github.workspace }}/pw-browsers 138 | key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} 139 | 140 | - name: Install browser 141 | run: jlpm playwright install chromium 142 | working-directory: ui-tests 143 | 144 | - name: Execute integration tests 145 | working-directory: ui-tests 146 | run: | 147 | jlpm playwright test 148 | 149 | - name: Upload Playwright Test report 150 | if: always() 151 | uses: actions/upload-artifact@v4 152 | with: 153 | name: jupyterlab_code_formatter-playwright-tests 154 | path: | 155 | ui-tests/test-results 156 | ui-tests/playwright-report 157 | 158 | check_links: 159 | name: Check Links 160 | runs-on: ubuntu-latest 161 | timeout-minutes: 15 162 | steps: 163 | - uses: actions/checkout@v4 164 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 165 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 166 | -------------------------------------------------------------------------------- /.github/workflows/check-release.yml: -------------------------------------------------------------------------------- 1 | name: Check Release 2 | on: 3 | push: 4 | branches: ["master"] 5 | pull_request: 6 | branches: ["*"] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | check_release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Base Setup 19 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 20 | - name: Install Dependencies 21 | run: | 22 | pip install -e . 23 | - name: Check Release 24 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 25 | with: 26 | 27 | token: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - name: Upload Distributions 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: jupyterlab_code_formatter-releaser-dist-${{ github.run_number }} 33 | path: .jupyter_releaser_checkout/dist 34 | -------------------------------------------------------------------------------- /.github/workflows/prep-release.yml: -------------------------------------------------------------------------------- 1 | name: "Step 1: Prep Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version_spec: 6 | description: "New Version Specifier" 7 | default: "next" 8 | required: false 9 | branch: 10 | description: "The branch to target" 11 | required: false 12 | post_version_spec: 13 | description: "Post Version Specifier" 14 | required: false 15 | # silent: 16 | # description: "Set a placeholder in the changelog and don't publish the release." 17 | # required: false 18 | # type: boolean 19 | since: 20 | description: "Use PRs with activity since this date or git reference" 21 | required: false 22 | since_last_stable: 23 | description: "Use PRs with activity since the last stable git tag" 24 | required: false 25 | type: boolean 26 | jobs: 27 | prep_release: 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: write 31 | steps: 32 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 33 | 34 | - name: Prep Release 35 | id: prep-release 36 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | version_spec: ${{ github.event.inputs.version_spec }} 40 | # silent: ${{ github.event.inputs.silent }} 41 | post_version_spec: ${{ github.event.inputs.post_version_spec }} 42 | branch: ${{ github.event.inputs.branch }} 43 | since: ${{ github.event.inputs.since }} 44 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 45 | 46 | - name: "** Next Step **" 47 | run: | 48 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" 49 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: "Step 2: Publish Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | branch: 6 | description: "The target branch" 7 | required: false 8 | release_url: 9 | description: "The URL of the draft GitHub release" 10 | required: false 11 | steps_to_skip: 12 | description: "Comma separated list of steps to skip" 13 | required: false 14 | 15 | jobs: 16 | publish_release: 17 | runs-on: ubuntu-latest 18 | environment: release 19 | permissions: 20 | id-token: write 21 | steps: 22 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 23 | 24 | - uses: actions/create-github-app-token@v1 25 | id: app-token 26 | with: 27 | app-id: ${{ vars.APP_ID }} 28 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 29 | 30 | - name: Populate Release 31 | id: populate-release 32 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 33 | with: 34 | token: ${{ steps.app-token.outputs.token }} 35 | branch: ${{ github.event.inputs.branch }} 36 | release_url: ${{ github.event.inputs.release_url }} 37 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }} 38 | 39 | - name: Finalize Release 40 | id: finalize-release 41 | env: 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2 44 | with: 45 | token: ${{ steps.app-token.outputs.token }} 46 | release_url: ${{ steps.populate-release.outputs.release_url }} 47 | 48 | - name: "** Next Step **" 49 | if: ${{ success() }} 50 | run: | 51 | echo "Verify the final release" 52 | echo ${{ steps.finalize-release.outputs.release_url }} 53 | 54 | - name: "** Failure Message **" 55 | if: ${{ failure() }} 56 | run: | 57 | echo "Failed to Publish the Draft Release Url:" 58 | echo ${{ steps.populate-release.outputs.release_url }} 59 | -------------------------------------------------------------------------------- /.github/workflows/update-integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Update Playwright Snapshots 2 | 3 | on: 4 | issue_comment: 5 | types: [created, edited] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | update-snapshots: 13 | if: > 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 | jupyterlab_code_formatter/labextension 11 | # Version file is handled by hatchling 12 | jupyterlab_code_formatter/_version.py 13 | 14 | # Integration tests 15 | ui-tests/test-results/ 16 | ui-tests/playwright-report/ 17 | 18 | # Created by https://www.gitignore.io/api/python 19 | # Edit at https://www.gitignore.io/?templates=python 20 | 21 | ### Python ### 22 | # Byte-compiled / optimized / DLL files 23 | __pycache__/ 24 | *.py[cod] 25 | *$py.class 26 | 27 | # C extensions 28 | *.so 29 | 30 | # Distribution / packaging 31 | .Python 32 | build/ 33 | develop-eggs/ 34 | dist/ 35 | downloads/ 36 | eggs/ 37 | .eggs/ 38 | lib/ 39 | lib64/ 40 | parts/ 41 | sdist/ 42 | var/ 43 | wheels/ 44 | pip-wheel-metadata/ 45 | share/python-wheels/ 46 | .installed.cfg 47 | *.egg 48 | MANIFEST 49 | 50 | # PyInstaller 51 | # Usually these files are written by a python script from a template 52 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 53 | *.manifest 54 | *.spec 55 | 56 | # Installer logs 57 | pip-log.txt 58 | pip-delete-this-directory.txt 59 | 60 | # Unit test / coverage reports 61 | htmlcov/ 62 | .tox/ 63 | .nox/ 64 | .coverage 65 | .coverage.* 66 | .cache 67 | nosetests.xml 68 | coverage/ 69 | coverage.xml 70 | *.cover 71 | .hypothesis/ 72 | .pytest_cache/ 73 | 74 | # Translations 75 | *.mo 76 | *.pot 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # celery beat schedule file 91 | celerybeat-schedule 92 | 93 | # SageMath parsed files 94 | *.sage.py 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # Mr Developer 104 | .mr.developer.cfg 105 | .project 106 | .pydevproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | # Pyre type checker 117 | .pyre/ 118 | 119 | # End of https://www.gitignore.io/api/python 120 | 121 | # OSX files 122 | .DS_Store 123 | 124 | .idea/ 125 | .yarn/ 126 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | !/package.json 6 | jupyterlab_code_formatter 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | ## 3.0.2 6 | 7 | ([Full Changelog](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/compare/v3.0.1...13234598efd659882e5a7862c8fe32b1f76828a1)) 8 | 9 | ### Bugs fixed 10 | 11 | - Update the method for determining kernel language [#354](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pull/354) ([@shreve](https://github.com/shreve)) 12 | 13 | ### Contributors to this release 14 | 15 | ([GitHub contributors page for this release](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/graphs/contributors?from=2024-08-07&to=2024-08-14&type=c)) 16 | 17 | [@shreve](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab_code_formatter+involves%3Ashreve+updated%3A2024-08-07..2024-08-14&type=Issues) 18 | 19 | 20 | 21 | ## 3.0.1 22 | 23 | ([Full Changelog](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/compare/v3.0.0...aad4b750c93ac9aa524a1cc18315706c7d464c29)) 24 | 25 | ### Bugs fixed 26 | 27 | - Improve safety of `RFormatter.importable` [#353](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pull/353) ([@shreve](https://github.com/shreve)) 28 | 29 | ### Maintenance and upkeep improvements 30 | 31 | - Use the new `publish-release` action [#355](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pull/355) ([@krassowski](https://github.com/krassowski)) 32 | - Add badges and fix some links [#351](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pull/351) ([@fcollonval](https://github.com/fcollonval)) 33 | 34 | ### Documentation improvements 35 | 36 | - Fix a typo in the documentation [#350](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pull/350) ([@panangam](https://github.com/panangam)) 37 | 38 | ### Contributors to this release 39 | 40 | ([GitHub contributors page for this release](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/graphs/contributors?from=2024-07-22&to=2024-08-07&type=c)) 41 | 42 | [@fcollonval](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab_code_formatter+involves%3Afcollonval+updated%3A2024-07-22..2024-08-07&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab_code_formatter+involves%3Agithub-actions+updated%3A2024-07-22..2024-08-07&type=Issues) | [@krassowski](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab_code_formatter+involves%3Akrassowski+updated%3A2024-07-22..2024-08-07&type=Issues) | [@panangam](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab_code_formatter+involves%3Apanangam+updated%3A2024-07-22..2024-08-07&type=Issues) | [@shreve](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab_code_formatter+involves%3Ashreve+updated%3A2024-07-22..2024-08-07&type=Issues) 43 | 44 | ## 3.0.0 45 | 46 | ([Full Changelog](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/compare/v2.1.0...07f0478cc39a27eb03053fc621a032eb0eaa8931)) 47 | 48 | ### Enhancements made 49 | 50 | - Add JupyterLab :: 4 classifier [#322](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pull/322) ([@graelo](https://github.com/graelo)) 51 | - Editor auto save [#318](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pull/318) ([@ryantam626](https://github.com/ryantam626)) 52 | - Add suppress error iff auto formatting on save config [#317](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pull/317) ([@ryantam626](https://github.com/ryantam626)) 53 | - Add go to cell option in failure dialog [#316](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pull/316) ([@ryantam626](https://github.com/ryantam626)) 54 | 55 | ### Bugs fixed 56 | 57 | - Fix environment variable leak for unused formatters [#338](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pull/338) ([@krassowski](https://github.com/krassowski)) 58 | - Suppress stderr in call to ruff and add ruff formatter [#333](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pull/333) ([@felix-cw](https://github.com/felix-cw)) 59 | - fix: restore support for python>=3.7,\<3.9 [#311](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pull/311) ([@pdhall99](https://github.com/pdhall99)) 60 | 61 | ### Maintenance and upkeep improvements 62 | 63 | - Fix workflows, build against JupyterLab 4, fix build TS 5 errors, lint [#346](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pull/346) ([@krassowski](https://github.com/krassowski)) 64 | 65 | ### Contributors to this release 66 | 67 | ([GitHub contributors page for this release](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/graphs/contributors?from=2023-05-08&to=2024-07-22&type=c)) 68 | 69 | [@felix-cw](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab_code_formatter+involves%3Afelix-cw+updated%3A2023-05-08..2024-07-22&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab_code_formatter+involves%3Agithub-actions+updated%3A2023-05-08..2024-07-22&type=Issues) | [@graelo](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab_code_formatter+involves%3Agraelo+updated%3A2023-05-08..2024-07-22&type=Issues) | [@krassowski](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab_code_formatter+involves%3Akrassowski+updated%3A2023-05-08..2024-07-22&type=Issues) | [@pdhall99](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab_code_formatter+involves%3Apdhall99+updated%3A2023-05-08..2024-07-22&type=Issues) | [@ryantam626](https://github.com/search?q=repo%3Ajupyterlab-contrib%2Fjupyterlab_code_formatter+involves%3Aryantam626+updated%3A2023-05-08..2024-07-22&type=Issues) 70 | 71 | ## 2.2.1 2023-05-21 72 | 73 | **General** 74 | 75 | - Iron out what's causing mismatch of labextension and package release process; 76 | 77 | **Server extension** 78 | 79 | - Actually support python>=3.7,<3.9 properly, courtesy of pdhall99; 80 | 81 | ## 2.2.0 Skipped 82 | 83 | ## 2.1.0 2023-05-08 84 | 85 | **Jupyterlab extension** 86 | 87 | - Support for JupyterLab 3.6+/4+ and Notebook v7, courtesy of mcrutch; 88 | - This will drop support for jupyterlab<=3.5! 89 | 90 | ## 2.0.0 2023-05-08 91 | 92 | **General** 93 | 94 | - Major refactor of repo, now based off the update jupyterlab extension cookiecutter; 95 | - Introduce an actually working dockerised dev env; 96 | 97 | **Server extension** 98 | 99 | - Add `ruff` support - courtesy of felix-cw; 100 | 101 | **Jupyterlab extension** 102 | 103 | - Add `ruff` support - courtesy of felix-cw; 104 | 105 | ## 1.6.1 2023-04-16 106 | 107 | **Server extension** 108 | 109 | - Use `importlib` instead of `pkg_resources` which is being deprecated; 110 | 111 | ## 1.6.0 2023-03-26 112 | 113 | **Server extension** 114 | 115 | - Swap importable check to something more performant - courtesy of krassowski; 116 | 117 | **Jupyterlab extension** 118 | 119 | - Add more isort configuration settings - courtesy of dcnadler; 120 | 121 | ## 1.5.3 2022-08-10 122 | 123 | **Server extension** 124 | 125 | - Remove implicit dependency of `which` binary - courtesy of KanchiShimono; 126 | 127 | ## 1.5.2 2022-08-06 128 | 129 | **Server extension** 130 | 131 | - Add `AStyle` support using `subprocess - courtesy of nthiery; 132 | 133 | ## 1.5.1 2022-07-24 134 | 135 | **General** 136 | 137 | - Add `rustfmt` support using `subprocess` - courtesy of andrelfpinto; 138 | - Add docs for adding a custom formatter; 139 | 140 | **Server extension** 141 | 142 | - Handle single/double leading question mark properly; 143 | - Re-add trailing new line if not in notebook; 144 | 145 | **Jupyterlab extension** 146 | 147 | - Suppress cell skipped error properly if configured to suppress; 148 | 149 | ## 1.5.0 2022-07-16 150 | 151 | **General** 152 | 153 | - Add `scalafmt` support using `subprocess` - courtesy of andrelfpinto; 154 | - Add Py10 to supported list - courtesy of haoxins; 155 | - Better error message - courtesy of ianhi; 156 | - Support more black configuration - courtesy of utkarshgupta137; 157 | 158 | **Server extension** 159 | 160 | - Make server extension work without `notebook` package, courtesy of KanchiShimono; 161 | 162 | **Jupyterlab extension** 163 | 164 | - Fix JSON schemas - courtesy of KanchiShimono; 165 | - Fix UI errors in configuration screen - courtesy of KanchiShimono; 166 | 167 | ## 1.4.11 2022-05-01 168 | 169 | **General** 170 | 171 | - Revamp documentation site; 172 | - Revamp development environment; 173 | 174 | **Server extension** 175 | 176 | - Escape Quarto's comment to exmept it from formatting - thanks rgaiacs for providing test case; 177 | - Add support for `blue` formatter; 178 | - Restore easily accessible formatter list API; 179 | - Escape run script command in JupyterLab; 180 | 181 | **Jupyterlab extension** 182 | 183 | - Add suppress formatter errors setting; 184 | 185 | ## 1.4.10 2021-04-02 186 | 187 | **Server extension** 188 | 189 | - Ignore more magic; 190 | 191 | **Jupyterlab extension** 192 | 193 | No change. 194 | 195 | ## 1.4.9 2021-04-02 196 | 197 | **Server extension** 198 | 199 | - Don't expect options to be always passed; 200 | 201 | **Jupyterlab extension** 202 | 203 | No change. 204 | 205 | ## 1.4.8 2021-04-02 206 | 207 | **Server extension** 208 | 209 | - Improve interactive help regex - again; 210 | 211 | **Jupyterlab extension** 212 | 213 | - Remove `include_trailing_comma` option for black, it's not an option to begin with. 214 | 215 | ## 1.4.7 2021-04-01 216 | 217 | **Server extension** 218 | 219 | - Improve interactive help regex; 220 | 221 | **Jupyterlab extension** 222 | 223 | No change. 224 | 225 | ## 1.4.6 2021-04-01 226 | 227 | **Server extension** 228 | 229 | - Improve interactive help regex; 230 | 231 | **Jupyterlab extension** 232 | 233 | - Support `noop`/`skip` in default formatters setting; 234 | 235 | ## 1.4.5 2021-03-14 236 | 237 | **Server extension** 238 | 239 | - Ignore interactive help lines while formatting; 240 | 241 | **Jupyterlab extension** 242 | 243 | No change. 244 | 245 | ## 1.4.4 2021-02-13 246 | 247 | **Server extension** 248 | 249 | - Handle incompatible magic language cellblock better; 250 | 251 | **Jupyterlab extension** 252 | 253 | - Auto format on save as an option - courtesy of simamumu; 254 | 255 | ## 1.4.3 2021-01-01 256 | 257 | **Server extension** 258 | 259 | - Attempt to address JupyterHub precarity - courtesy of SarunasAzna; 260 | 261 | **Jupyterlab extension** 262 | 263 | No changes. 264 | 265 | ## 1.4.2 2021-01-01 266 | 267 | **Server extension** 268 | 269 | - Attempt to auto enable server extension - courtesy of fcollonval; 270 | 271 | **Jupyterlab extension** 272 | 273 | No changes. 274 | 275 | ## 1.4.1 2021-01-01 276 | 277 | **Server extension** 278 | 279 | No changes. 280 | 281 | **Jupyterlab extension** 282 | 283 | No changes. 284 | 285 | **General** 286 | 287 | - Fix package publish procedure; 288 | 289 | ## 1.4.0 2021-01-01 290 | 291 | **Server extension** 292 | 293 | No changes. 294 | 295 | **Jupyterlab extension** 296 | 297 | - Minor fix for error messages; 298 | 299 | **General** 300 | 301 | - Project reorganisation, improve plugin packaging, massive thanks to ianhi for laying the groundwork; 302 | 303 | ## 1.3.8 2020-11-17 304 | 305 | **Server extension** 306 | 307 | No changes. 308 | 309 | **Jupyterlab extension** 310 | 311 | - Fix icon color in dark theme, courtesy of AllanChain; 312 | 313 | ## 1.3.7 2020-11-15 314 | 315 | **Server extension** 316 | 317 | - Handle shell commands in code cells; 318 | 319 | **Jupyterlab extension** 320 | 321 | No changes. 322 | 323 | ## 1.3.6 2020-08-08 324 | 325 | **Server extension** 326 | 327 | No changes. 328 | 329 | **Jupyterlab extension** 330 | 331 | - Fix isort schema spec for the following settings: 332 | - known_future_library 333 | - known_standard_library 334 | - known_third_party 335 | - known_first_party 336 | 337 | ## 1.3.5 2020-07-18 338 | 339 | **Server extension** 340 | 341 | No changes. 342 | 343 | **Jupyterlab extension** 344 | 345 | - Fix server URL lookup for JupyterLab 2.2.0+; 346 | 347 | ## 1.3.4 2020-07-11 348 | 349 | **Server extension** 350 | 351 | - Fix semicolon handling again; 352 | 353 | **Jupyterlab extension** 354 | 355 | No changes. 356 | 357 | ## 1.3.3 2020-07-10 358 | 359 | **Server extension** 360 | 361 | - Support isort 5 and also isort 4 at the same time, courtesy of dialvarezs; 362 | 363 | **Jupyterlab extension** 364 | 365 | No changes. 366 | 367 | ## 1.3.2 2020-07-08 368 | 369 | **Server extension** 370 | 371 | - Fix semicolon handling again; (This was mistakenly removed in 1.3.3 later on, and reintroduced later.) 372 | - Improve error message when formatter is not found; 373 | 374 | **Jupyterlab extension** 375 | 376 | No changes. 377 | 378 | ## 1.3.1 2020-05-08 379 | 380 | Same as 1.3.0. 381 | 382 | ## 1.3.0 2020-05-08 383 | 384 | **Server extension** 385 | 386 | - Move cell/file ending handling back to server extension; 387 | - Fix semicolon handling; 388 | 389 | **Jupyterlab extension** 390 | 391 | - Move cell/file ending handling back to server extension; 392 | - Fix erroneous detection of R default formatters; 393 | 394 | ## 1.2.5 2020-04-25 395 | 396 | **Server extension** 397 | 398 | - Ignore magic and trailing semicolon for R formatters; 399 | 400 | **Jupyterlab extension** 401 | 402 | No changes. 403 | 404 | ## 1.2.4 2020-04-18 405 | 406 | **Server extension** 407 | 408 | - Fix detect notebook type fallback - courtesy of devstein; 409 | 410 | **Jupyterlab extension** 411 | 412 | No changes. 413 | 414 | ## 1.2.3 2020-04-09 415 | 416 | **Server extension** 417 | 418 | No changes. 419 | 420 | **Jupyterlab extension** 421 | 422 | - Add detect notebook type fallback; 423 | - Make failure to determin default formatters more prominent; 424 | 425 | ## 1.2.2 2020-03-14 426 | 427 | **Server extension** 428 | 429 | No changes. 430 | 431 | **Jupyterlab extension** 432 | 433 | - Fix error reporting when blank code cell(s) exists; 434 | 435 | ## 1.2.1 2020-03-12 436 | 437 | **Server extension** 438 | 439 | - Add version API handler; 440 | 441 | **Jupyterlab extension** 442 | 443 | - Fully prohibit mismatched lab and server extension usage (accounting for either stale lab or server extension); 444 | - Use Jupyterlab dialogs for error reporting instead of console for clarity; 445 | - Support multiple default formatters to be ran in sequence; 446 | 447 | ## 1.2.0 2020-03-04 448 | 449 | **Server extension** 450 | 451 | No Changes 452 | 453 | **Jupyterlab extension** 454 | 455 | - Address Jupyter lab 2.0.0 breaing changes; 456 | 457 | ## 1.1.0 2020-02-08 458 | 459 | **Server extension** 460 | 461 | - Defer trailing newline removal to labextension; 462 | - Prohibit mismatched lab and server extension usage; 463 | 464 | **Jupyterlab extension** 465 | 466 | - Make tool bar format all button respect where it's clicked; 467 | - Delete trailing newline for notebook cells only; 468 | - Prohibit mismatched lab and server extension usage; 469 | 470 | ## 1.0.3 2019-12-07 471 | 472 | **Server extension** 473 | 474 | - Handle :code:`indent_by` and :code:`start_comments_with_one_space` for styler; 475 | - Unify magic and semicolon handling for Python formatters; 476 | 477 | **Jupyterlab extension** 478 | 479 | - Handle :code:`indent_by` and :code:`start_comments_with_one_space` for styler; 480 | 481 | **General** 482 | 483 | - Various fixes to docs; 484 | - Various fixes to Makefile; 485 | 486 | ## 1.0.2 2019-12-01 487 | 488 | **Server extension** 489 | 490 | - Fix optional :code:`rpy2` import crashing server extension; 491 | 492 | **Jupyterlab extension** 493 | 494 | No changes. 495 | 496 | ## 1.0.1 2019-12-01 497 | 498 | No change, simply fixing versioning error. 499 | 500 | ## 1.0.0 2019-12-01 501 | 502 | **Server extension** 503 | 504 | - Fix missing `rpy2` import error; 505 | - Add tests; 506 | 507 | **Jupyterlab extension** 508 | 509 | - Major refactoring; 510 | - Temporarily removed language filtering for command palette; 511 | - Tooltip format notebook changed to icon - thanks to mlucool; 512 | 513 | **General** 514 | 515 | - Project reorgnaisation; 516 | - Use nix for local development environment; 517 | - Documentation generation; 518 | 519 | ## 0.7.0 2019-11-02 520 | 521 | **Server extension** 522 | 523 | - Support more styler options; 524 | - Fix bad string comparsion of version strings; 525 | - Compile regex once only; 526 | 527 | **Jupyterlab extension** 528 | 529 | - Support more styler options; 530 | - Fix bad capitalisation of config schema; 531 | 532 | ## 0.6.1 2019-10-23 533 | 534 | **Server extension** 535 | 536 | - Retain semicolon after black's formatting action - courtesy of dfm; 537 | 538 | **Jupyterlab extension** 539 | 540 | No Change. 541 | 542 | ## 0.6.0 2019-10-16 543 | 544 | **Server extension** 545 | 546 | - Support formatting multiple code cell at the same time - courtesy of mlucool; 547 | - Return formatting error if they exists - courtesy of mlucool; 548 | 549 | **Jupyterlab extension** 550 | 551 | - Add `jupyterlab_code_foramtter:format` command and context menu button - courtesy of mlucool; 552 | - Add `jupyterlab_code_foramtter:format_all` command and command tools bar button - courtesy of mlucool; 553 | 554 | ## 0.5.2 2019-09-29 555 | 556 | **Server extension** 557 | 558 | - Trim trialing newline for autopep8; 559 | 560 | **Jupyterlab extension** 561 | 562 | No changes. 563 | 564 | ## 0.5.1 2019-09-09 565 | 566 | **Server extension** 567 | 568 | - Fix bug where presence of `rpy2` could cause plugin to be useless; 569 | 570 | **Jupyterlab extension** 571 | 572 | No changes. 573 | 574 | ## 0.5.0 2019-08-21 575 | 576 | **Server extension** 577 | 578 | - Support `styler` - Another R code formatter - courtesy of dev-wei; 579 | 580 | **Jupyterlab extension** 581 | 582 | - Support `styler` - Another R code formatter - courtesy of dev-wei; 583 | 584 | ## 0.4.0 2019-08-19 585 | 586 | **Server extension** 587 | 588 | - Support `formatr` - A R code formatter - courtesy of dev-wei; 589 | 590 | **Jupyterlab extension** 591 | 592 | - Support `formatr` - A R code formatter - courtesy of dev-wei; 593 | 594 | ## 0.3.0 2019-07-10 595 | 596 | **General** 597 | 598 | - Minor updates to README - courtesy of reza1615; 599 | 600 | **Server extension** 601 | 602 | No Change 603 | 604 | **Jupyterlab extension** 605 | 606 | - Support Jupyterlab ^1.0.0 - courtesy of gnestor; 607 | - Remove custom_style enum restriction - courtesy of CaselIT; 608 | - Add companion packages info; 609 | 610 | ## 0.2.3 2019-06-17 611 | 612 | Same as v0.2.2 - Re-publishing because I messed up the versioning. 613 | 614 | ## 0.2.2 2019-06-17 615 | 616 | **General** 617 | 618 | - Minor updates to README - courtesy of akashlakhera and mzakariaCERN; 619 | 620 | **Server extension** 621 | 622 | No Change 623 | 624 | **Jupyterlab extension** 625 | 626 | - Remove some excessive logging - courtesy of jtpio; 627 | - Make formatter commands visible for Python files and notebooks only - courtesy of jtpio; 628 | 629 | ## 0.2.1 2019-04-29 630 | 631 | **General** 632 | 633 | - Add Binder to README - courtesy of jtpio; 634 | - Add a test notebook for easier testing with Binder; 635 | 636 | **Server extension** 637 | 638 | - Add LICENSE in sdist - courtesy of xhochy; 639 | - Handle the exsistence of magic commands in codecell for Black - courtesy of Lif3line; 640 | 641 | **Jupyterlab extension** 642 | 643 | No Change 644 | 645 | ## 0.2.0 2019-03-24 646 | 647 | - Handle format_str interface change for black>=19.3b0; 648 | - Support Isort as a formatter; 649 | - Bugfixes - courtesy of gnestor; 650 | 651 | ## 0.1.8 2019-02-16 652 | 653 | - Minor fix for formatting files in code cells; 654 | 655 | ## 0.1.7 2019-02-16 656 | 657 | - Support formatting files in FileEditor - courtesy of rbedi; 658 | 659 | ## 0.1.6 2019-01-19 660 | 661 | - Expose autopep8 options - courtesy of timlod; 662 | 663 | ## 0.1.5 2018-12-01 664 | 665 | - Add commands to the main menu for better accessibility - courtesy of jtpio; 666 | 667 | ## 0.1.4 2018-10-10 668 | 669 | - Bump dependency ranges; 670 | 671 | ## 0.1.3 2018-08-24 672 | 673 | - Fix typo in command; 674 | 675 | ## 0.1.2 2018-08-24 676 | 677 | - Bump dependency ranges; 678 | 679 | ## 0.1.1 2018-08-18 680 | 681 | - Minor README update; 682 | 683 | ## 0.1.0 2018-08-18 684 | 685 | - Inital implementation; 686 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Ryan TAM 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](docs/logo.png) 2 | 3 | [![Extension status](https://img.shields.io/badge/status-ready-success 'ready to be used')](https://jupyterlab-contrib.github.io/) 4 | [![GitHub Action Status](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/actions/workflows/build.yml/badge.svg)](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/actions/workflows/build.yml) 5 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jupyterlab-contrib/jupyterlab_code_formatter/master?urlpath=lab) 6 | [![pypi-version](https://img.shields.io/pypi/v/jupyterlab-code-formatter.svg)](https://python.org/pypi/jupyterlab-code-formatter) 7 | 8 | _A JupyterLab plugin to facilitate invocation of code formatters._ 9 | 10 | --- 11 | 12 | Documentation: [Hosted on ReadTheDocs](https://jupyterlab-code-formatter.readthedocs.io/) 13 | 14 | --- 15 | 16 | ## Demo 17 | 18 | ![](docs/_static/format-all.gif) 19 | 20 | --- 21 | 22 | ## Quick Start 23 | 24 | I recommend you going to the [documentation site](https://jupyterlab-code-formatter.readthedocs.io/#quick-start), but this should work too. 25 | 26 | 1. **Install the package** 27 | 28 | ```bash 29 | pip install jupyterlab-code-formatter 30 | ``` 31 | 32 | 2. **Install some supported formatters** (isort+black are default for Python) 33 | 34 | ```bash 35 | # NOTE: Install black and isort, 36 | # JL code formatter is configured to invoke isort and black by default 37 | pip install black isort 38 | ``` 39 | 40 | 3. **Restart JupyterLab** 41 | 42 | This plugin includes a server plugin, restart JupyterLab if you have followed the above steps while it's running. 43 | 44 | 4. **Configure plugin** 45 | 46 | To configure which/how formatters are invoked, see [configuration](https://jupyterlab-code-formatter.readthedocs.io/configuration.html). 47 | 48 | --- 49 | 50 | ## Getting help 51 | 52 | If you don't use Discord then feel free to open a [GitHub issue](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/issues), do note I am a bit slower in responding in GitHub. 53 | 54 | --- 55 | 56 | ## Your Support 57 | 58 | I could really use your support in giving me a star on GitHub, recommending features or fixing bugs. 59 | 60 | - [Recommending features via GitHub Issues](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/issues) 61 | - [Submitting your PR on GitHub](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pulls) 62 | 63 | --- 64 | 65 | ## Contributors 66 | 67 | This extension was originally developed and maintained by [@ryantam626](https://github.com/ryantam626). 68 | Massive thanks to the below list of people who made past contributions to the project! 69 | 70 | 71 | 72 | 73 | 74 | ## License 75 | 76 | This project is licensed under the terms of the [MIT LICENSE](LICENSE) . 77 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a new release of jupyterlab_code_formatter 2 | 3 | The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). 4 | 5 | ## Manual release 6 | 7 | ### Python package 8 | 9 | This extension can be distributed as Python 10 | packages. All of the Python 11 | packaging instructions in the `pyproject.toml` file to wrap your extension in a 12 | Python package. Before generating a package, we first need to install `build`. 13 | 14 | ```bash 15 | pip install build twine hatch hatch-pip-deepfreeze 16 | ``` 17 | 18 | Bump the version using `hatch`. By default this will create a tag. 19 | See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. 20 | 21 | ```bash 22 | hatch version 23 | ``` 24 | 25 | To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: 26 | 27 | ```bash 28 | ./scripts/build.sh 29 | ``` 30 | 31 | Then to upload the package to PyPI, do: 32 | 33 | ```bash 34 | twine upload dist/* 35 | ``` 36 | 37 | ### NPM package 38 | 39 | To publish the frontend part of the extension as a NPM package, do: 40 | 41 | ```bash 42 | npm login 43 | npm publish --access public 44 | ``` 45 | 46 | ## Automated releases with the Jupyter Releaser 47 | 48 | The extension repository should already be compatible with the Jupyter Releaser. 49 | 50 | Check out the [workflow documentation](https://github.com/jupyter-server/jupyter_releaser#typical-workflow) for more information. 51 | 52 | Here is a summary of the steps to cut a new release: 53 | 54 | - Fork the [`jupyter-releaser` repo](https://github.com/jupyter-server/jupyter_releaser) 55 | - Add `ADMIN_GITHUB_TOKEN`, `PYPI_TOKEN` and `NPM_TOKEN` to the Github Secrets in the fork 56 | - Go to the Actions panel 57 | - Run the "Draft Changelog" workflow 58 | - Merge the Changelog PR 59 | - Run the "Draft Release" workflow 60 | - Run the "Publish Release" workflow 61 | 62 | ## Publishing to `conda-forge` 63 | 64 | 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 65 | 66 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. 67 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | vars: 4 | BASE_DIR: 5 | sh: git rev-parse --show-toplevel 6 | 7 | env: 8 | COMPOSE_DOCKER_CLI_BUILD: 1 9 | DOCKER_BUILDKIT: 1 10 | 11 | tasks: 12 | dev:build: 13 | desc: Build docker image for dev 14 | cmds: 15 | - BUILDKIT_PROGRESS=plain docker compose -f docker-compose.dev.yml build 16 | 17 | dev:up: 18 | desc: Spin up the docker compose stack for dev 19 | cmds: 20 | - docker compose -f docker-compose.dev.yml up -d 21 | - docker exec -it jupyterlab_code_formatter-dev-1 jlpm install 22 | - docker compose -f docker-compose.dev.yml up 23 | 24 | dev:down: 25 | desc: Shut down the docker compose stack for dev 26 | cmds: 27 | - docker compose -f docker-compose.dev.yml down 28 | 29 | dev:jlpm-watch: 30 | desc: Run `jlpm watch` task inside the dev container. 31 | cmds: 32 | - docker exec -it jupyterlab_code_formatter-dev-1 jlpm watch 33 | 34 | dev:jupyter-lab: 35 | desc: Run `jupyter lab` task inside the dev container. 36 | cmds: 37 | - docker exec -it jupyterlab_code_formatter-dev-1 jupyter lab --allow-root 38 | 39 | dev:shell: 40 | desc: Get a shell in dev container. 41 | cmds: 42 | - docker exec -it jupyterlab_code_formatter-dev-1 bash 43 | 44 | dev:format: 45 | desc: Run formatter in dev container 46 | cmds: 47 | - docker exec -it jupyterlab_code_formatter-dev-1 /plugin/scripts/format.sh 48 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@jupyterlab/testutils/lib/babel.config'); 2 | -------------------------------------------------------------------------------- /binder/environment.yml: -------------------------------------------------------------------------------- 1 | # a mybinder.org-ready environment for demoing jupyterlab_code_formatter 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 jupyterlab-code-formatter-demo 6 | # 7 | name: jupyterlab-code-formatter-demo 8 | 9 | channels: 10 | - conda-forge 11 | 12 | dependencies: 13 | # runtime dependencies 14 | - python >=3.8,<3.9.0a0 15 | - jupyterlab >=3,<4.0.0a0 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 jupyterlab_code_formatter 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 | sys.executable, 37 | "-m", 38 | "jupyter", 39 | "serverextension", 40 | "enable", 41 | "jupyterlab_code_formatter", 42 | ) 43 | _( 44 | sys.executable, 45 | "-m", 46 | "jupyter", 47 | "server", 48 | "extension", 49 | "enable", 50 | "jupyterlab_code_formatter", 51 | ) 52 | 53 | # verify the environment the extension didn't break anything 54 | _(sys.executable, "-m", "pip", "check") 55 | 56 | # list the extensions 57 | _("jupyter", "server", "extension", "list") 58 | 59 | # initially list installed extensions to determine if there are any surprises 60 | _("jupyter", "labextension", "list") 61 | 62 | # install black and isort 63 | _(sys.executable, "-m", "pip", "install", "black") 64 | _(sys.executable, "-m", "pip", "install", "isort") 65 | 66 | 67 | print("JupyterLab with jupyterlab_code_formatter is ready to run with:\n") 68 | print("\tjupyter lab\n") 69 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest_plugins = ("pytest_jupyter.jupyter_server", ) 4 | 5 | 6 | @pytest.fixture 7 | def jp_server_config(jp_server_config): 8 | return {"ServerApp": {"jpserver_extensions": {"jupyterlab_code_formatter": True}}} 9 | -------------------------------------------------------------------------------- /dev.Dockerfile: -------------------------------------------------------------------------------- 1 | # WARNING: This is only meant to be used in dev, you have been warned. 2 | FROM python:3.10.11-buster 3 | 4 | # Install common stuff and R 5 | RUN --mount=type=cache,target=/var/cache/apt \ 6 | apt update && \ 7 | apt install -y r-base curl jq gawk inotify-tools libgbm-dev libnss3 libasound2 cmake \ 8 | # Install random bullshit needed by browser libs 9 | gconf-service libasound2 libatk1.0-0 libc6 \ 10 | libcairo2 libcups2 libdbus-1-3 libexpat1 \ 11 | libfontconfig1 libgcc1 libgconf-2-4 \ 12 | libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 \ 13 | libnspr4 libpango-1.0-0 libpangocairo-1.0-0 \ 14 | libstdc++6 libx11-6 libx11-xcb1 libxcb1 \ 15 | libxcomposite1 libxcursor1 libxdamage1 libxext6 \ 16 | libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \ 17 | libxtst6 ca-certificates fonts-liberation \ 18 | libappindicator1 libnss3 lsb-release xdg-utils wget \ 19 | libdrm-dev libgbm-dev 20 | 21 | # Install NodeJS 22 | ENV NODE_VERSION=19.2.0 23 | RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash 24 | ENV NVM_DIR=/root/.nvm 25 | RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION} 26 | RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION} 27 | RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION} 28 | ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}" 29 | RUN npm install --global yarn 30 | 31 | # Install Rust 32 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y 33 | ENV PATH="/root/.cargo/bin:${PATH}" 34 | # TODO: Figrue out how to cache this. 35 | RUN cargo install evcxr_jupyter evcxr_repl 36 | 37 | ## Install R packages 38 | RUN R --vanilla -e 'install.packages("formatR", repos = "http://cran.us.r-project.org")' 39 | RUN R --vanilla -e 'install.packages("styler", repos = "http://cran.us.r-project.org")' 40 | 41 | # Install hatch for dependency management and build process 42 | RUN --mount=type=cache,target=/root/.cache/pip \ 43 | pip install hatch hatch-pip-deepfreeze 44 | 45 | # Make venv 46 | ENV VIRTUAL_ENV=/opt/venv 47 | RUN python3 -m venv $VIRTUAL_ENV 48 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 49 | 50 | # Copy all requirements.txt in 51 | COPY requirements* / 52 | RUN --mount=type=cache,target=/root/.cache/pip \ 53 | pip install -r /requirements.txt -r /requirements-dev.txt -r /requirements-test.txt 54 | 55 | # Install rust jupyter kernel 56 | RUN evcxr_jupyter --install 57 | 58 | # Copy repo into image... 59 | COPY . /plugin 60 | 61 | WORKDIR /plugin 62 | 63 | # Developement install of plugin 64 | RUN --mount=type=cache,target=/root/.cache/pip \ 65 | pip install -e ".[test]" 66 | RUN jupyter labextension develop . --overwrite 67 | RUN jupyter server extension enable jupyterlab_code_formatter 68 | RUN jlpm install 69 | RUN jlpm build 70 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | dev: 5 | image: jupyterlab-code-formatter-dev 6 | build: 7 | context: . 8 | dockerfile: dev.Dockerfile 9 | network_mode: host # Can't be bother to expose a bunch of ports. 10 | volumes: 11 | - ./:/plugin 12 | - node_modules:/plugin/node_modules 13 | entrypoint: sleep 99d 14 | 15 | volumes: 16 | node_modules: 17 | -------------------------------------------------------------------------------- /docs/_static/format-all.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab-contrib/jupyterlab_code_formatter/9d6e41e9f8842a0bec1f6bccfef75f11c9528058/docs/_static/format-all.gif -------------------------------------------------------------------------------- /docs/_static/format-selected.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab-contrib/jupyterlab_code_formatter/9d6e41e9f8842a0bec1f6bccfef75f11c9528058/docs/_static/format-selected.gif -------------------------------------------------------------------------------- /docs/_static/format-specific.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab-contrib/jupyterlab_code_formatter/9d6e41e9f8842a0bec1f6bccfef75f11c9528058/docs/_static/format-specific.gif -------------------------------------------------------------------------------- /docs/_static/settings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab-contrib/jupyterlab_code_formatter/9d6e41e9f8842a0bec1f6bccfef75f11c9528058/docs/_static/settings.gif -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | **Jupyterlab extension** 6 | 7 | - Add "Go to cell" option in dialog when formatting fails; 8 | - Add "suppressFormatterErrorsIFFAutoFormatOnSave" config; 9 | 10 | ## 2.2.1 2023-05-21 11 | 12 | **General** 13 | 14 | - Iron out what's causing mismatch of labextension and package release process; 15 | 16 | **Server extension** 17 | 18 | - Actually support python>=3.7,<3.9 properly, courtesy of pdhall99; 19 | 20 | ## 2.2.0 Skipped 21 | 22 | ## 2.1.0 2023-05-08 23 | 24 | **Jupyterlab extension** 25 | 26 | - Support for JupyterLab 3.6+/4+ and Notebook v7, courtesy of mcrutch; 27 | - This will drop support for jupyterlab<=3.5! 28 | 29 | ## 2.0.0 2023-05-08 30 | 31 | **General** 32 | 33 | - Major refactor of repo, now based off the update jupyterlab extension cookiecutter; 34 | - Introduce an actually working dockerised dev env; 35 | 36 | **Server extension** 37 | 38 | - Add `ruff` support - courtesy of felix-cw; 39 | 40 | **Jupyterlab extension** 41 | 42 | - Add `ruff` support - courtesy of felix-cw; 43 | 44 | ## 1.6.1 2023-04-16 45 | 46 | **Server extension** 47 | 48 | - Use `importlib` instead of `pkg_resources` which is being deprecated; 49 | 50 | ## 1.6.0 2023-03-26 51 | 52 | **Server extension** 53 | 54 | - Swap importable check to something more performant - courtesy of krassowski; 55 | 56 | **Jupyterlab extension** 57 | 58 | - Add more isort configuration settings - courtesy of dcnadler; 59 | 60 | ## 1.5.3 2022-08-10 61 | 62 | **Server extension** 63 | 64 | - Remove implicit dependency of `which` binary - courtesy of KanchiShimono; 65 | 66 | ## 1.5.2 2022-08-06 67 | 68 | **Server extension** 69 | 70 | - Add `AStyle` support using `subprocess - courtesy of nthiery; 71 | 72 | ## 1.5.1 2022-07-24 73 | 74 | **General** 75 | 76 | - Add `rustfmt` support using `subprocess` - courtesy of andrelfpinto; 77 | - Add docs for adding a custom formatter; 78 | 79 | **Server extension** 80 | 81 | - Handle single/double leading question mark properly; 82 | - Re-add trailing new line if not in notebook; 83 | 84 | **Jupyterlab extension** 85 | 86 | - Suppress cell skipped error properly if configured to suppress; 87 | 88 | ## 1.5.0 2022-07-16 89 | 90 | **General** 91 | 92 | - Add `scalafmt` support using `subprocess` - courtesy of andrelfpinto; 93 | - Add Py10 to supported list - courtesy of haoxins; 94 | - Better error message - courtesy of ianhi; 95 | - Support more black configuration - courtesy of utkarshgupta137; 96 | 97 | **Server extension** 98 | 99 | - Make server extension work without `notebook` package, courtesy of KanchiShimono; 100 | 101 | **Jupyterlab extension** 102 | 103 | - Fix JSON schemas - courtesy of KanchiShimono; 104 | - Fix UI errors in configuration screen - courtesy of KanchiShimono; 105 | 106 | ## 1.4.11 2022-05-01 107 | 108 | **General** 109 | 110 | - Revamp documentation site; 111 | - Revamp development environment; 112 | 113 | **Server extension** 114 | 115 | - Escape Quarto's comment to exmept it from formatting - thanks rgaiacs for providing test case; 116 | - Add support for `blue` formatter; 117 | - Restore easily accessible formatter list API; 118 | - Escape run script command in JupyterLab; 119 | 120 | **Jupyterlab extension** 121 | 122 | - Add suppress formatter errors setting; 123 | 124 | ## 1.4.10 2021-04-02 125 | 126 | **Server extension** 127 | 128 | - Ignore more magic; 129 | 130 | **Jupyterlab extension** 131 | 132 | No change. 133 | 134 | ## 1.4.9 2021-04-02 135 | 136 | **Server extension** 137 | 138 | - Don't expect options to be always passed; 139 | 140 | **Jupyterlab extension** 141 | 142 | No change. 143 | 144 | ## 1.4.8 2021-04-02 145 | 146 | **Server extension** 147 | 148 | - Improve interactive help regex - again; 149 | 150 | **Jupyterlab extension** 151 | 152 | - Remove `include_trailing_comma` option for black, it's not an option to begin with. 153 | 154 | ## 1.4.7 2021-04-01 155 | 156 | **Server extension** 157 | 158 | - Improve interactive help regex; 159 | 160 | **Jupyterlab extension** 161 | 162 | No change. 163 | 164 | ## 1.4.6 2021-04-01 165 | 166 | **Server extension** 167 | 168 | - Improve interactive help regex; 169 | 170 | **Jupyterlab extension** 171 | 172 | - Support `noop`/`skip` in default formatters setting; 173 | 174 | ## 1.4.5 2021-03-14 175 | 176 | **Server extension** 177 | 178 | - Ignore interactive help lines while formatting; 179 | 180 | **Jupyterlab extension** 181 | 182 | No change. 183 | 184 | ## 1.4.4 2021-02-13 185 | 186 | **Server extension** 187 | 188 | - Handle incompatible magic language cellblock better; 189 | 190 | **Jupyterlab extension** 191 | 192 | - Auto format on save as an option - courtesy of simamumu; 193 | 194 | ## 1.4.3 2021-01-01 195 | 196 | **Server extension** 197 | 198 | - Attempt to address JupyterHub precarity - courtesy of SarunasAzna; 199 | 200 | **Jupyterlab extension** 201 | 202 | No changes. 203 | 204 | ## 1.4.2 2021-01-01 205 | 206 | **Server extension** 207 | 208 | - Attempt to auto enable server extension - courtesy of fcollonval; 209 | 210 | **Jupyterlab extension** 211 | 212 | No changes. 213 | 214 | ## 1.4.1 2021-01-01 215 | 216 | **Server extension** 217 | 218 | No changes. 219 | 220 | **Jupyterlab extension** 221 | 222 | No changes. 223 | 224 | **General** 225 | 226 | - Fix package publish procedure; 227 | 228 | ## 1.4.0 2021-01-01 229 | 230 | **Server extension** 231 | 232 | No changes. 233 | 234 | **Jupyterlab extension** 235 | 236 | - Minor fix for error messages; 237 | 238 | **General** 239 | 240 | - Project reorganisation, improve plugin packaging, massive thanks to ianhi for laying the groundwork; 241 | 242 | ## 1.3.8 2020-11-17 243 | 244 | **Server extension** 245 | 246 | No changes. 247 | 248 | **Jupyterlab extension** 249 | 250 | - Fix icon color in dark theme, courtesy of AllanChain; 251 | 252 | ## 1.3.7 2020-11-15 253 | 254 | **Server extension** 255 | 256 | - Handle shell commands in code cells; 257 | 258 | **Jupyterlab extension** 259 | 260 | No changes. 261 | 262 | ## 1.3.6 2020-08-08 263 | 264 | **Server extension** 265 | 266 | No changes. 267 | 268 | **Jupyterlab extension** 269 | 270 | - Fix isort schema spec for the following settings: 271 | - known_future_library 272 | - known_standard_library 273 | - known_third_party 274 | - known_first_party 275 | 276 | ## 1.3.5 2020-07-18 277 | 278 | **Server extension** 279 | 280 | No changes. 281 | 282 | **Jupyterlab extension** 283 | 284 | - Fix server URL lookup for JupyterLab 2.2.0+; 285 | 286 | ## 1.3.4 2020-07-11 287 | 288 | **Server extension** 289 | 290 | - Fix semicolon handling again; 291 | 292 | **Jupyterlab extension** 293 | 294 | No changes. 295 | 296 | ## 1.3.3 2020-07-10 297 | 298 | **Server extension** 299 | 300 | - Support isort 5 and also isort 4 at the same time, courtesy of dialvarezs; 301 | 302 | **Jupyterlab extension** 303 | 304 | No changes. 305 | 306 | ## 1.3.2 2020-07-08 307 | 308 | **Server extension** 309 | 310 | - Fix semicolon handling again; (This was mistakenly removed in 1.3.3 later on, and reintroduced later.) 311 | - Improve error message when formatter is not found; 312 | 313 | **Jupyterlab extension** 314 | 315 | No changes. 316 | 317 | ## 1.3.1 2020-05-08 318 | 319 | Same as 1.3.0. 320 | 321 | ## 1.3.0 2020-05-08 322 | 323 | **Server extension** 324 | 325 | - Move cell/file ending handling back to server extension; 326 | - Fix semicolon handling; 327 | 328 | **Jupyterlab extension** 329 | 330 | - Move cell/file ending handling back to server extension; 331 | - Fix erroneous detection of R default formatters; 332 | 333 | ## 1.2.5 2020-04-25 334 | 335 | **Server extension** 336 | 337 | - Ignore magic and trailing semicolon for R formatters; 338 | 339 | **Jupyterlab extension** 340 | 341 | No changes. 342 | 343 | ## 1.2.4 2020-04-18 344 | 345 | **Server extension** 346 | 347 | - Fix detect notebook type fallback - courtesy of devstein; 348 | 349 | **Jupyterlab extension** 350 | 351 | No changes. 352 | 353 | ## 1.2.3 2020-04-09 354 | 355 | **Server extension** 356 | 357 | No changes. 358 | 359 | **Jupyterlab extension** 360 | 361 | - Add detect notebook type fallback; 362 | - Make failure to determin default formatters more prominent; 363 | 364 | ## 1.2.2 2020-03-14 365 | 366 | **Server extension** 367 | 368 | No changes. 369 | 370 | **Jupyterlab extension** 371 | 372 | - Fix error reporting when blank code cell(s) exists; 373 | 374 | ## 1.2.1 2020-03-12 375 | 376 | **Server extension** 377 | 378 | - Add version API handler; 379 | 380 | **Jupyterlab extension** 381 | 382 | - Fully prohibit mismatched lab and server extension usage (accounting for either stale lab or server extension); 383 | - Use Jupyterlab dialogs for error reporting instead of console for clarity; 384 | - Support multiple default formatters to be ran in sequence; 385 | 386 | ## 1.2.0 2020-03-04 387 | 388 | **Server extension** 389 | 390 | No Changes 391 | 392 | **Jupyterlab extension** 393 | 394 | - Address Jupyter lab 2.0.0 breaing changes; 395 | 396 | ## 1.1.0 2020-02-08 397 | 398 | **Server extension** 399 | 400 | - Defer trailing newline removal to labextension; 401 | - Prohibit mismatched lab and server extension usage; 402 | 403 | **Jupyterlab extension** 404 | 405 | - Make tool bar format all button respect where it's clicked; 406 | - Delete trailing newline for notebook cells only; 407 | - Prohibit mismatched lab and server extension usage; 408 | 409 | ## 1.0.3 2019-12-07 410 | 411 | **Server extension** 412 | 413 | - Handle :code:`indent_by` and :code:`start_comments_with_one_space` for styler; 414 | - Unify magic and semicolon handling for Python formatters; 415 | 416 | **Jupyterlab extension** 417 | 418 | - Handle :code:`indent_by` and :code:`start_comments_with_one_space` for styler; 419 | 420 | **General** 421 | 422 | - Various fixes to docs; 423 | - Various fixes to Makefile; 424 | 425 | ## 1.0.2 2019-12-01 426 | 427 | **Server extension** 428 | 429 | - Fix optional :code:`rpy2` import crashing server extension; 430 | 431 | **Jupyterlab extension** 432 | 433 | No changes. 434 | 435 | ## 1.0.1 2019-12-01 436 | 437 | No change, simply fixing versioning error. 438 | 439 | ## 1.0.0 2019-12-01 440 | 441 | **Server extension** 442 | 443 | - Fix missing `rpy2` import error; 444 | - Add tests; 445 | 446 | **Jupyterlab extension** 447 | 448 | - Major refactoring; 449 | - Temporarily removed language filtering for command palette; 450 | - Tooltip format notebook changed to icon - thanks to mlucool; 451 | 452 | **General** 453 | 454 | - Project reorgnaisation; 455 | - Use nix for local development environment; 456 | - Documentation generation; 457 | 458 | ## 0.7.0 2019-11-02 459 | 460 | **Server extension** 461 | 462 | - Support more styler options; 463 | - Fix bad string comparsion of version strings; 464 | - Compile regex once only; 465 | 466 | **Jupyterlab extension** 467 | 468 | - Support more styler options; 469 | - Fix bad capitalisation of config schema; 470 | 471 | ## 0.6.1 2019-10-23 472 | 473 | **Server extension** 474 | 475 | - Retain semicolon after black's formatting action - courtesy of dfm; 476 | 477 | **Jupyterlab extension** 478 | 479 | No Change. 480 | 481 | ## 0.6.0 2019-10-16 482 | 483 | **Server extension** 484 | 485 | - Support formatting multiple code cell at the same time - courtesy of mlucool; 486 | - Return formatting error if they exists - courtesy of mlucool; 487 | 488 | **Jupyterlab extension** 489 | 490 | - Add `jupyterlab_code_foramtter:format` command and context menu button - courtesy of mlucool; 491 | - Add `jupyterlab_code_foramtter:format_all` command and command tools bar button - courtesy of mlucool; 492 | 493 | ## 0.5.2 2019-09-29 494 | 495 | **Server extension** 496 | 497 | - Trim trialing newline for autopep8; 498 | 499 | **Jupyterlab extension** 500 | 501 | No changes. 502 | 503 | ## 0.5.1 2019-09-09 504 | 505 | **Server extension** 506 | 507 | - Fix bug where presence of `rpy2` could cause plugin to be useless; 508 | 509 | **Jupyterlab extension** 510 | 511 | No changes. 512 | 513 | ## 0.5.0 2019-08-21 514 | 515 | **Server extension** 516 | 517 | - Support `styler` - Another R code formatter - courtesy of dev-wei; 518 | 519 | **Jupyterlab extension** 520 | 521 | - Support `styler` - Another R code formatter - courtesy of dev-wei; 522 | 523 | ## 0.4.0 2019-08-19 524 | 525 | **Server extension** 526 | 527 | - Support `formatr` - A R code formatter - courtesy of dev-wei; 528 | 529 | **Jupyterlab extension** 530 | 531 | - Support `formatr` - A R code formatter - courtesy of dev-wei; 532 | 533 | ## 0.3.0 2019-07-10 534 | 535 | **General** 536 | 537 | - Minor updates to README - courtesy of reza1615; 538 | 539 | **Server extension** 540 | 541 | No Change 542 | 543 | **Jupyterlab extension** 544 | 545 | - Support Jupyterlab ^1.0.0 - courtesy of gnestor; 546 | - Remove custom_style enum restriction - courtesy of CaselIT; 547 | - Add companion packages info; 548 | 549 | ## 0.2.3 2019-06-17 550 | 551 | Same as v0.2.2 - Re-publishing because I messed up the versioning. 552 | 553 | ## 0.2.2 2019-06-17 554 | 555 | **General** 556 | 557 | - Minor updates to README - courtesy of akashlakhera and mzakariaCERN; 558 | 559 | **Server extension** 560 | 561 | No Change 562 | 563 | **Jupyterlab extension** 564 | 565 | - Remove some excessive logging - courtesy of jtpio; 566 | - Make formatter commands visible for Python files and notebooks only - courtesy of jtpio; 567 | 568 | ## 0.2.1 2019-04-29 569 | 570 | **General** 571 | 572 | - Add Binder to README - courtesy of jtpio; 573 | - Add a test notebook for easier testing with Binder; 574 | 575 | **Server extension** 576 | 577 | - Add LICENSE in sdist - courtesy of xhochy; 578 | - Handle the exsistence of magic commands in codecell for Black - courtesy of Lif3line; 579 | 580 | **Jupyterlab extension** 581 | 582 | No Change 583 | 584 | ## 0.2.0 2019-03-24 585 | 586 | - Handle format_str interface change for black>=19.3b0; 587 | - Support Isort as a formatter; 588 | - Bugfixes - courtesy of gnestor; 589 | 590 | ## 0.1.8 2019-02-16 591 | 592 | - Minor fix for formatting files in code cells; 593 | 594 | ## 0.1.7 2019-02-16 595 | 596 | - Support formatting files in FileEditor - courtesy of rbedi; 597 | 598 | ## 0.1.6 2019-01-19 599 | 600 | - Expose autopep8 options - courtesy of timlod; 601 | 602 | ## 0.1.5 2018-12-01 603 | 604 | - Add commands to the main menu for better accessibility - courtesy of jtpio; 605 | 606 | ## 0.1.4 2018-10-10 607 | 608 | - Bump dependency ranges; 609 | 610 | ## 0.1.3 2018-08-24 611 | 612 | - Fix typo in command; 613 | 614 | ## 0.1.2 2018-08-24 615 | 616 | - Bump dependency ranges; 617 | 618 | ## 0.1.1 2018-08-18 619 | 620 | - Minor README update; 621 | 622 | ## 0.1.0 2018-08-18 623 | 624 | - Inital implementation; 625 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | from sphinx.builders.html import StandaloneHTMLBuilder 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'Jupyterlab Code Formatter' 22 | copyright = '2023, Ryan Tam' 23 | author = 'Ryan Tam' 24 | 25 | # The full version, including alpha/beta/rc tags 26 | # TODO: Fix me 27 | release = '1.4.10' 28 | 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | 'myst_parser', 37 | 'sphinx_copybutton', 38 | 'sphinx.ext.intersphinx', 39 | 'sphinx_inline_tabs', 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # List of patterns, relative to source directory, that match files and 46 | # directories to ignore when looking for source files. 47 | # This pattern also affects html_static_path and html_extra_path. 48 | exclude_patterns = [] 49 | 50 | StandaloneHTMLBuilder.supported_image_types = [ 51 | 'image/svg+xml', 52 | 'image/gif', 53 | 'image/png', 54 | 'image/jpeg' 55 | ] 56 | 57 | source_suffix = { 58 | '.rst': 'restructuredtext', 59 | '.txt': 'markdown', 60 | '.md': 'markdown', 61 | } 62 | 63 | # -- Options for HTML output ------------------------------------------------- 64 | 65 | # The theme to use for HTML and HTML Help pages. See the documentation for 66 | # a list of builtin themes. 67 | # 68 | html_theme = 'furo' 69 | 70 | # Add any paths that contain custom static files (such as style sheets) here, 71 | # relative to this directory. They are copied after the builtin static files, 72 | # so a file named "default.css" will overwrite the builtin "default.css". 73 | html_static_path = ['_static'] 74 | 75 | master_doc = 'index' 76 | 77 | 78 | html_logo = "logo.png" 79 | html_static_path = ["_static"] 80 | html_theme_options = { 81 | "sidebar_hide_name": True, 82 | "navigation_with_keys": True, 83 | } 84 | 85 | myst_enable_extensions = [ 86 | "colon_fence", 87 | ] 88 | myst_heading_anchors = 3 89 | 90 | html_title = "JupyterLab Code Formatter" 91 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | For configuring this plugin, I highly recommend you turn use the JSON Settings Editor instead of the weird graphical settings editor. 4 | 5 | ![settings](_static/settings.gif) 6 | 7 | ## Format On Save 8 | 9 | In the settings page, include the following key value pair:- 10 | 11 | ```json 12 | { 13 | "formatOnSave": true 14 | } 15 | ``` 16 | 17 | :::{note} 18 | Settings can be found in "Settings" in the toolbar > "Advanced Settings Editor" > "Jupyterlab Code Formatter". 19 | ::: 20 | 21 | This invokes `jupyterlab_code_formatter:format_all` every time you save, which uses the [default formatters specified in settings](#changing-default-formatters). 22 | 23 | ## Keyboard Shortcuts 24 | 25 | To add a keyboard shortcut calling the JupyterLab commands registered by this plugin (documented [here](usage.md#preface)), add an entry in the Advanced Setting Edtior of JupyterLab (TDOO: How to get there.) like so:- 26 | 27 | ```json 28 | { 29 | "shortcuts": [ 30 | { 31 | "command": "jupyterlab_code_formatter:format", 32 | "keys": ["Ctrl K", "Ctrl M"], 33 | "selector": ".jp-Notebook.jp-mod-editMode" 34 | } 35 | ] 36 | } 37 | ``` 38 | 39 | The above example breaks down to 40 | 41 | - Under edit mode (detected through the selector); 42 | - Using the chord `Ctrl+K Ctrl+M`; 43 | - Invoke the `jupyterlab_code_formatter:format` command; 44 | 45 | ## Changing Default Formatter(s) 46 | 47 | The `jupyterlab_code_formatter:format` and `jupyterlab_code_formatter:format_all` JupyterLab commands will always invoke the formatter(s) specified in the settings. 48 | 49 | :::{note} 50 | Settings can be found in "Settings" in the toolbar > "Advanced Settings Editor" > "Jupyterlab Code Formatter". 51 | ::: 52 | 53 | To override the default settings, enter something like so in the "User Preferences" panel of the settings:- 54 | 55 | ```json 56 | { 57 | "preferences": { 58 | "default_formatter": { 59 | "python": "autopep8", 60 | "R": "styler" 61 | } 62 | } 63 | } 64 | ``` 65 | 66 | ## Changing Formatter Parameters 67 | 68 | Sometimes the stock default config of a code formatter doesn't suit your need, you can override the code formatter in the settings. 69 | 70 | :::{note} 71 | Settings can be found in "Settings" in the toolbar > "Advanced Settings Editor" > "Jupyterlab Code Formatter". 72 | ::: 73 | 74 | For example to override settings for the `autopep8` formatter, enter something like so in the "User Preferences" pnael of the settings:- 75 | 76 | ```json 77 | { 78 | "autopep8": { 79 | "max_line_length": 120, 80 | "ignore": ["E226", "E302", "E41"] 81 | } 82 | } 83 | ``` 84 | 85 | :::{warning} 86 | This plugin does not pick up file based configuration at the moment (e.g. setup.cfg, pyproject.yml, etc.) 87 | 88 | Ticket is already opened at [#167](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/issues/167). 89 | ::: 90 | 91 | :::{warning} 92 | This plugin might be out of sync with the list of possibilities of configuration option. 93 | 94 | See [settings.json](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/blob/master/schema/settings.json) for the JSON schema used, feel free to open a PR for updating it. 95 | ::: 96 | 97 | ## Chaining Formatters Invocation 98 | 99 | The `jupyterlab_code_formatter:format` and `jupyterlab_code_formatter:format_all` JupyterLab commands support invocation of multiple formatters one after the other via settings. 100 | 101 | :::{note} 102 | Settings can be found in "Settings" in the toolbar > "Advanced Settings Editor" > "Jupyterlab Code Formatter". 103 | ::: 104 | 105 | To do so, configure the default formatter to be an array of strings:- 106 | 107 | ```json 108 | { 109 | "preferences": { 110 | "default_formatter": { 111 | "python": ["isort", "black"], 112 | "R": ["styler", "formatR"] 113 | } 114 | } 115 | } 116 | ``` 117 | 118 | ## R Formatter Configuration Example 119 | 120 | R formatters are a little finicky to configure, the `list` construct in R is actually a JSON dictionary, to configure value of `math_token_spacing` and `reindention` of `styler`, do something like so:- 121 | 122 | ```json 123 | { 124 | "styler": { 125 | "math_token_spacing": { 126 | "zero": ["'^'"], 127 | "one": ["'+'", "'-'", "'*'", "'/'"] 128 | }, 129 | "reindention": { 130 | "regex_pattern": "^###", 131 | "indention": 0, 132 | "comments_only": true 133 | } 134 | } 135 | } 136 | ``` 137 | 138 | Once again this is done in the settings page. 139 | 140 | :::{note} 141 | Settings can be found in "Settings" in the toolbar > "Advanced Settings Editor" > "Jupyterlab Code Formatter". 142 | ::: 143 | -------------------------------------------------------------------------------- /docs/custom-formatter.md: -------------------------------------------------------------------------------- 1 | # Adding Custom Formatters 2 | 3 | To define a custom formatter, you can do so in the Jupyter notebook configuration (usually found `~/.jupyter/jupyter_notebook_config.py` or something along those lines), the following example adds a rather useless formatter as a example. 4 | 5 | ```python 6 | 7 | from jupyterlab_code_formatter.formatters import BaseFormatter, handle_line_ending_and_magic, SERVER_FORMATTERS 8 | 9 | class ExampleCustomFormatter(BaseFormatter): 10 | 11 | label = "Apply Example Custom Formatter" 12 | 13 | @property 14 | def importable(self) -> bool: 15 | return True 16 | 17 | @handle_line_ending_and_magic 18 | def format_code(self, code: str, notebook: bool, **options) -> str: 19 | return "42" 20 | 21 | SERVER_FORMATTERS["example"] = ExampleCustomFormatter() 22 | 23 | ``` 24 | 25 | When implementing your customer formatter using third party library, you will likely use `try... except` in the `importable` block instead of always returning `True`. 26 | 27 | Remember you are always welcomed to submit a pull request! 28 | -------------------------------------------------------------------------------- /docs/dev.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Prerequisites: 4 | 5 | - Install [task](https://taskfile.dev); 6 | - Install docker, with buildkit; 7 | 8 | 1. Spin up docker compose based dev env - `task dev:up` 9 | 2. Run `jlpm watch` inside dev container - `task dev:jlpm-watch` 10 | 3. In another terminal, run `jupyter lab` inside dev container - `task dev:jupyter-lab` 11 | 12 | This watches the source directory and run JupyterLab at the same time in different terminals to watch for changes in the 13 | extension's source and automatically rebuild the extension inside the dev docker container. 14 | 15 | 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). 16 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## Error When Writing Grammar Tables 4 | 5 | It is possible that black will fail when trying to create local cache directory, you can resovle this by creating the directory yourself (elevated permissions might be required):- 6 | 7 | ```bash 8 | python -c "import black; black.CACHE_DIR.mkdir(parents=True, exist_ok=True)" 9 | ``` 10 | 11 | For more information, see [issue #10](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/issues/10). 12 | 13 | ## JupyterLab Commands Not Showing Up 14 | 15 | Make sure you really have one of the formatters properly installed in the Jupyter Server's environment. 16 | 17 | The plugin is also configured to only show commands when a suitable notebook/script is opened, it's worth checking if you have opened one such notebook/script. 18 | 19 | ## 20 | -------------------------------------------------------------------------------- /docs/getting-help.md: -------------------------------------------------------------------------------- 1 | # Getting help 2 | 3 | I am most responsive on Discord, feel free to ping me in [Python Discord](https://discord.com/invite/python), the [#editors-ide](https://discord.com/channels/267624335836053506/813178633006350366) channel is a suitable place for that. 4 | 5 | If you don't use Discord then feel free to open a [GitHub issue](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/issues), do note I am a bit slower in responding in GitHub. 6 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ```{toctree} 2 | :hidden: 3 | 4 | installation.md 5 | usage.md 6 | configuration.md 7 | custom-formatter.md 8 | faq.md 9 | jupyterhub.md 10 | changelog.md 11 | dev.md 12 | getting-help.md 13 | your-support.md 14 | 15 | ``` 16 | 17 | # JupyterLab Code Formatter 18 | 19 | _A JupyterLab plugin to facilitate invocation of code formatters._ 20 | 21 | **Source Code**: [GitHub](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/). 22 | 23 | ## Demo 24 | 25 | ![format-all](_static/format-all.gif) 26 | 27 | ## Requirements 28 | 29 | - Python 3.7+ 30 | - JupyterLab >= 3.6.0 (if you are using JupyterLab>=3.0,<=3.5, pin this package to 2.0.0) 31 | - Any supported code formatters (you can also specify your own, see [custom formatter](custom-formatter.md)). 32 | 33 | :::{important} 34 | JupyterLab Code Formatter only provides an interface for invoking code formatters on Jupyter Server, and does not include any code formatter by default. 35 | ::: 36 | 37 | ## Quick Start 38 | 39 | [//]: # 'TODO: Add tab for common package managers' 40 | 41 | 1. **Install the package** 42 | 43 | ````{tab} Pip 44 | ```bash 45 | pip install jupyterlab-code-formatter 46 | ``` 47 | ```` 48 | 49 | ````{tab} Poetry 50 | ```bash 51 | poetry add jupyterlab-code-formatter 52 | ``` 53 | ```` 54 | 55 | ````{tab} Pipenv 56 | ```bash 57 | pipenv install jupyterlab-code-formatter 58 | ``` 59 | ```` 60 | 61 | 2. **Install some supported formatters** (isort+black are default for Python) 62 | 63 | ````{tab} Pip 64 | ```bash 65 | # NOTE: Install black and isort, 66 | # JL code formatter is configured to invoke isort and black by default 67 | pip install black isort 68 | ``` 69 | ```` 70 | 71 | ````{tab} Poetry 72 | ```bash 73 | # NOTE: Install black and isort, 74 | # JL code formatter is configured to invoke isort and black by default 75 | poetry add black isort 76 | ``` 77 | ```` 78 | 79 | ````{tab} Pipenv 80 | ```bash 81 | # NOTE: Install black and isort, 82 | # JL code formatter is configured to invoke isort and black by default 83 | pipenv install black isort 84 | ``` 85 | ```` 86 | 87 | 3. **Restart JupyterLab** 88 | 89 | This plugin includes a server plugin, as such you will need to restart JupyterLab if you have followed 90 | the above steps while it's running. 91 | 92 | 4. **Configure plugin** 93 | 94 | To configure which/how formatters are invoked, see [configuration](configuration.md). 95 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Detailed Installation Guide 2 | 3 | ## Python 4 | 5 | 1. **Install the package** 6 | 7 | ````{tab} Pip 8 | ```bash 9 | pip install jupyterlab-code-formatter 10 | ``` 11 | ```` 12 | 13 | ````{tab} Conda 14 | ```bash 15 | conda install -c conda-forge jupyterlab_code_formatter 16 | ``` 17 | ```` 18 | 19 | ````{tab} Poetry 20 | ```bash 21 | poetry add jupyterlab-code-formatter 22 | ``` 23 | ```` 24 | 25 | ````{tab} Pipenv 26 | ```bash 27 | pipenv install jupyterlab-code-formatter 28 | ``` 29 | ```` 30 | 31 | 2. **Install some supported formatters** 32 | 33 | Install any desired formatter from the below list 34 | 35 | - [black](https://github.com/psf/black); 36 | - [yapf](https://github.com/google/yapf); 37 | - [autopep8](https://github.com/peter-evans/autopep8); 38 | - [isort](https://github.com/PyCQA/isort); 39 | 40 | ````{tab} Pip 41 | ```bash 42 | # NOTE: You don't have to install all of them if you don't want to. 43 | pip install black 44 | pip install yapf 45 | pip install isort 46 | pip install autopep8 47 | ``` 48 | ```` 49 | 50 | ````{tab} Conda 51 | ```bash 52 | # NOTE: You don't have to install all of them if you don't want to. 53 | conda install -c conda-forge black 54 | conda install -c conda-forge yapf 55 | conda install -c conda-forge isort 56 | conda install -c conda-forge autopep8 57 | ``` 58 | ```` 59 | 60 | ````{tab} Poetry 61 | ```bash 62 | # NOTE: You don't have to install all of them if you don't want to. 63 | poetry add black 64 | poetry add yapf 65 | poetry add isort 66 | poetry add autopep8 67 | ``` 68 | ```` 69 | 70 | ````{tab} Pipenv 71 | ```bash 72 | # NOTE: You don't have to install all of them if you don't want to. 73 | pipenv install black 74 | pipenv install yapf 75 | pipenv install isort 76 | pipenv install autopep8 77 | ``` 78 | ```` 79 | 80 | 3. **Restart JupyterLab** 81 | 82 | This plugin includes a server plugin, restart JupyterLab if you have followed the above steps while it's running. 83 | 84 | 4. **Configure plugin** 85 | 86 | To configure which/how formatters are invoked, see [configuration](configuration.md). 87 | 88 | ## R 89 | 90 | 1. **Install Python -> R Bridge** 91 | 92 | ````{tab} Pip 93 | ```bash 94 | pip install rpy2 95 | ``` 96 | ```` 97 | 98 | ````{tab} Conda 99 | ```bash 100 | conda install -c conda-forge rpy2 101 | ``` 102 | ```` 103 | 104 | ````{tab} Poetry 105 | ```bash 106 | poetry add rpy2 107 | ``` 108 | ```` 109 | 110 | ````{tab} Pipenv 111 | ```bash 112 | pipenv install jupyterlab-code-formatter 113 | ``` 114 | ```` 115 | 116 | 2. **Install the package** 117 | 118 | ````{tab} Pip 119 | ```bash 120 | pip install jupyterlab-code-formatter 121 | ``` 122 | ```` 123 | 124 | ````{tab} Conda 125 | ```bash 126 | conda install -c conda-forge jupyterlab_code_formatter 127 | ``` 128 | ```` 129 | 130 | ````{tab} Poetry 131 | ```bash 132 | poetry add jupyterlab-code-formatter 133 | ``` 134 | ```` 135 | 136 | ````{tab} Pipenv 137 | ```bash 138 | pipenv install jupyterlab-code-formatter 139 | ``` 140 | ```` 141 | 142 | 3. **Install some supported formatters** 143 | Install any desired formatter from the below list. 144 | 145 | - [formatR](https://github.com/yihui/formatR/); 146 | - [styler](https://github.com/r-lib/styler); 147 | 148 | ```bash 149 | # NOTE: You don't have to install all of them if you don't want to. 150 | R --vanilla -e 'install.packages("formatR", repos = "http://cran.us.r-project.org")' 151 | R --vanilla -e 'install.packages("styler", repos = "http://cran.us.r-project.org")' 152 | ``` 153 | 154 | 4. **Restart JupyterLab** 155 | 156 | This plugin includes a server plugin, restart JupyterLab if you have followed the above steps while it's running. 157 | 158 | 5. **Configure plugin** 159 | 160 | To configure which/how formatters are invoked, see [configuration](configuration.md). 161 | -------------------------------------------------------------------------------- /docs/jupyterhub.md: -------------------------------------------------------------------------------- 1 | # JupyterHub 2 | 3 | [//]: # 'TODO: Double check this in another container' 4 | 5 | This plugin should work under a JupyterHub environment, however due to the difference in how JupyterHub starts the JupyterLab instance, one would need to enable the server extension with the `--sys-prefix` flag, that is: 6 | 7 | ```bash 8 | jupyter serverextension enable --py jupyterlab_code_formatter --sys-prefix 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab-contrib/jupyterlab_code_formatter/9d6e41e9f8842a0bec1f6bccfef75f11c9528058/docs/logo.png -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Preface 4 | 5 | This plugin registers JupyterLab commands when supported formatters are detected. 6 | 7 | Here is a non-exhaustive list of possibilities: 8 | 9 | - `jupyterlab_code_formatter:black` 10 | - `jupyterlab_code_formatter:isort` 11 | - `jupyterlab_code_formatter:yapf` 12 | - `jupyterlab_code_formatter:formatr` 13 | - `jupyterlab_code_formatter:styler` 14 | 15 | These commands invoke the specified code formatter in the current focused cell. 16 | 17 | To find out what formatters are available, you can query http://localhost:8888/jupyterlab_code_formatter/formatters (you might need to replace the port and address), the keys of formatter are shown there. 18 | 19 | --- 20 | 21 | In addition to the above commands, this plugin also adds two non-formatter-specific commands: 22 | 23 | - `jupyterlab_code_formatter:format` 24 | - `jupyterlab_code_formatter:format_all` 25 | 26 | These commands invoke the configured default code formatters, to configure the default code formatters see [here](configuration.md#changing-default-formatters). 27 | 28 | ## Invoke Default Code Formatter(s) 29 | 30 | Here are some examples showing how to invoke the default code formatter(s) via comand palette. 31 | 32 | ### For Focused Cell(s) 33 | 34 | Example using the context menu: 35 | 36 | ![format-selected](_static/format-selected.gif) 37 | 38 | You can also achieve this by invoking `jupyterlab_code_formatter:format`. 39 | 40 | ### For The Entire Document 41 | 42 | Example using the button on the toolbar: 43 | 44 | ![format-all](_static/format-all.gif) 45 | 46 | You can also achieve this by invoking `jupyterlab_code_formatter:format_all`. 47 | 48 | ## Invoke Specific Code Formatter 49 | 50 | Example using the command palette or menu bar: 51 | 52 | ![format-specific](_static/format-specific.gif) 53 | 54 | You can also achieve this by invoking `jupyterlab_code_formatter:black` for example, see possiblities in the [preface](#preface). 55 | -------------------------------------------------------------------------------- /docs/your-support.md: -------------------------------------------------------------------------------- 1 | # Your Support 2 | 3 | I could really use your support in giving me a star on GitHub, recommending features or fixing bugs! 4 | 5 | - [Recommending features via GitHub Issues](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/issues) 6 | - [Sumitting your PR on GitHub](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/pulls) 7 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "jupyterlab_code_formatter", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_code_formatter" 5 | } 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); 2 | 3 | const esModules = [ 4 | '@codemirror', 5 | '@jupyter/ydoc', 6 | '@jupyterlab/', 7 | 'lib0', 8 | 'nanoid', 9 | 'vscode-ws-jsonrpc', 10 | 'y-protocols', 11 | 'y-websocket', 12 | 'yjs' 13 | ].join('|'); 14 | 15 | const baseConfig = jestJupyterLab(__dirname); 16 | 17 | module.exports = { 18 | ...baseConfig, 19 | automock: false, 20 | collectCoverageFrom: [ 21 | 'src/**/*.{ts,tsx}', 22 | '!src/**/*.d.ts', 23 | '!src/**/.ipynb_checkpoints/*' 24 | ], 25 | coverageReporters: ['lcov', 'text'], 26 | testRegex: 'src/.*/.*.spec.ts[x]?$', 27 | transformIgnorePatterns: [`/node_modules/(?!${esModules}).+`] 28 | }; 29 | -------------------------------------------------------------------------------- /jupyter-config/nb-config/jupyterlab_code_formatter.json: -------------------------------------------------------------------------------- 1 | { 2 | "NotebookApp": { 3 | "nbserver_extensions": { 4 | "jupyterlab_code_formatter": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /jupyter-config/server-config/jupyterlab_code_formatter.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerApp": { 3 | "jpserver_extensions": { 4 | "jupyterlab_code_formatter": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /jupyterlab_code_formatter/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__ 2 | from .handlers import setup_handlers 3 | 4 | 5 | def _jupyter_labextension_paths(): 6 | return [{"src": "labextension", "dest": "jupyterlab_code_formatter"}] 7 | 8 | 9 | def _jupyter_server_extension_points(): 10 | return [{"module": "jupyterlab_code_formatter"}] 11 | 12 | 13 | def _load_jupyter_server_extension(server_app): 14 | """Registers the API handler to receive HTTP requests from the frontend extension. 15 | 16 | Parameters 17 | ---------- 18 | server_app: jupyterlab.labapp.LabApp 19 | JupyterLab application instance 20 | """ 21 | setup_handlers(server_app.web_app) 22 | name = "jupyterlab_code_formatter" 23 | server_app.log.info(f"Registered {name} server extension") 24 | 25 | 26 | # For backward compatibility with notebook server - useful for Binder/JupyterHub 27 | load_jupyter_server_extension = _load_jupyter_server_extension 28 | -------------------------------------------------------------------------------- /jupyterlab_code_formatter/formatters.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import copy 3 | import importlib 4 | import logging 5 | import re 6 | import shutil 7 | import subprocess 8 | import sys 9 | from functools import wraps 10 | from typing import List, Type 11 | 12 | if sys.version_info >= (3, 9): 13 | from functools import cache 14 | else: 15 | from functools import lru_cache 16 | 17 | cache = lru_cache(maxsize=None) 18 | 19 | from packaging import version 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | INCOMPATIBLE_MAGIC_LANGUAGES = [ 25 | "html", 26 | "js", 27 | "javascript", 28 | "latex", 29 | "perl", 30 | "markdown", 31 | "ruby", 32 | "script", 33 | "sh", 34 | "svg", 35 | "bash", 36 | "info", 37 | "cleanup", 38 | "delete", 39 | "configure", 40 | "logs", 41 | "sql", 42 | "local", 43 | "sparksql", 44 | ] 45 | 46 | 47 | class BaseFormatter(abc.ABC): 48 | @property 49 | @abc.abstractmethod 50 | def label(self) -> str: 51 | pass 52 | 53 | @property 54 | @abc.abstractmethod 55 | def importable(self) -> bool: 56 | pass 57 | 58 | @abc.abstractmethod 59 | def format_code(self, code: str, notebook: bool, **options) -> str: 60 | pass 61 | 62 | @property 63 | @cache 64 | def cached_importable(self) -> bool: 65 | return self.importable 66 | 67 | 68 | class BaseLineEscaper(abc.ABC): 69 | """A base class for defining how to escape certain sequence of text to avoid formatting.""" 70 | 71 | def __init__(self, code: str) -> None: 72 | self.code = code 73 | 74 | @property 75 | @abc.abstractmethod 76 | def langs(self) -> List[str]: 77 | pass 78 | 79 | @abc.abstractmethod 80 | def escape(self, line: str) -> str: 81 | pass 82 | 83 | @abc.abstractmethod 84 | def unescape(self, line: str) -> str: 85 | pass 86 | 87 | 88 | class MagicCommandEscaper(BaseLineEscaper): 89 | langs = ["python"] 90 | escaped_line_start = "# \x01 " 91 | unesacpe_start = len(escaped_line_start) 92 | 93 | def escape(self, line: str) -> str: 94 | if line.lstrip().startswith("%"): 95 | line = f"{self.escaped_line_start}{line}" 96 | return line 97 | 98 | def unescape(self, line: str) -> str: 99 | if line.lstrip().startswith(self.escaped_line_start): 100 | line = line[self.unesacpe_start :] 101 | return line 102 | 103 | 104 | class RunScriptEscaper(BaseLineEscaper): 105 | langs = ["python"] 106 | escaped_line_start = "# \x01 " 107 | unesacpe_start = len(escaped_line_start) 108 | 109 | def escape(self, line: str) -> str: 110 | if re.match(pattern=r"run\s+\w+", string=line.lstrip()): 111 | line = f"{self.escaped_line_start}{line}" 112 | return line 113 | 114 | def unescape(self, line: str) -> str: 115 | if line.lstrip().startswith(self.escaped_line_start): 116 | line = line[self.unesacpe_start :] 117 | return line 118 | 119 | 120 | class HelpEscaper(BaseLineEscaper): 121 | langs = ["python"] 122 | escaped_line_start = "# \x01 " 123 | unesacpe_start = len(escaped_line_start) 124 | 125 | def escape(self, line: str) -> str: 126 | lstripped = line.lstrip() 127 | if ( 128 | line.endswith("??") 129 | or line.endswith("?") 130 | or lstripped.startswith("?") 131 | or lstripped.startswith("??") 132 | ) and "#" not in line: 133 | line = f"{self.escaped_line_start}{line}" 134 | return line 135 | 136 | def unescape(self, line: str) -> str: 137 | if line.lstrip().startswith(self.escaped_line_start): 138 | line = line[self.unesacpe_start :] 139 | return line 140 | 141 | 142 | class CommandEscaper(BaseLineEscaper): 143 | langs = ["python"] 144 | escaped_line_start = "# \x01 " 145 | unesacpe_start = len(escaped_line_start) 146 | 147 | def escape(self, line: str) -> str: 148 | if line.lstrip().startswith("!"): 149 | line = f"{self.escaped_line_start}{line}" 150 | return line 151 | 152 | def unescape(self, line: str) -> str: 153 | if line.lstrip().startswith(self.escaped_line_start): 154 | line = line[self.unesacpe_start :] 155 | return line 156 | 157 | 158 | class QuartoCommentEscaper(BaseLineEscaper): 159 | langs = ["python"] 160 | escaped_line_start = "# \x01 " 161 | unesacpe_start = len(escaped_line_start) 162 | 163 | def escape(self, line: str) -> str: 164 | if line.lstrip().startswith("#| "): 165 | line = f"{self.escaped_line_start}{line}" 166 | return line 167 | 168 | def unescape(self, line: str) -> str: 169 | if line.lstrip().startswith(self.escaped_line_start): 170 | line = line[self.unesacpe_start :] 171 | return line 172 | 173 | 174 | ESCAPER_CLASSES: List[Type[BaseLineEscaper]] = [ 175 | MagicCommandEscaper, 176 | HelpEscaper, 177 | CommandEscaper, 178 | QuartoCommentEscaper, 179 | RunScriptEscaper, 180 | ] 181 | 182 | 183 | def handle_line_ending_and_magic(func): 184 | @wraps(func) 185 | def wrapped(self, code: str, notebook: bool, **options) -> str: 186 | if any(code.startswith(f"%{lang}") for lang in INCOMPATIBLE_MAGIC_LANGUAGES) or any( 187 | code.startswith(f"%%{lang}") for lang in INCOMPATIBLE_MAGIC_LANGUAGES 188 | ): 189 | logger.info("Non compatible magic language cell block detected, ignoring.") 190 | return code 191 | 192 | has_semicolon = code.strip().endswith(";") 193 | 194 | escapers = [escaper_cls(code) for escaper_cls in ESCAPER_CLASSES] 195 | 196 | lines = code.splitlines() 197 | for escaper in escapers: 198 | lines = map(escaper.escape, lines) 199 | code = "\n".join(lines) 200 | 201 | code = func(self, code, notebook, **options) 202 | 203 | lines = code.splitlines() 204 | lines.append("") 205 | 206 | for escaper in escapers: 207 | lines = map(escaper.unescape, lines) 208 | code = "\n".join(lines) 209 | 210 | if notebook: 211 | code = code.rstrip() 212 | 213 | if has_semicolon and notebook and not code.endswith(";"): 214 | code += ";" 215 | return code 216 | 217 | return wrapped 218 | 219 | 220 | BLUE_MONKEY_PATCHED = False 221 | 222 | 223 | def is_importable(pkg_name: str) -> bool: 224 | # find_spec will check for packages installed/uninstalled after JupyterLab started 225 | return importlib.util.find_spec(pkg_name) is not None 226 | 227 | 228 | def command_exist(name: str) -> bool: 229 | """Detect that command (executable) is installed""" 230 | return shutil.which(name) is not None 231 | 232 | 233 | def import_black(): 234 | global BLUE_MONKEY_PATCHED 235 | if BLUE_MONKEY_PATCHED: 236 | for module in list(sys.modules): 237 | if module.startswith("black."): 238 | importlib.reload(sys.modules[module]) 239 | 240 | import black 241 | 242 | black = importlib.reload(black) 243 | BLUE_MONKEY_PATCHED = False 244 | else: 245 | import black 246 | 247 | return black 248 | 249 | 250 | def import_blue(): 251 | """Import blue and perform monkey patch.""" 252 | global BLUE_MONKEY_PATCHED 253 | import blue 254 | 255 | if not BLUE_MONKEY_PATCHED: 256 | blue.monkey_patch_black(blue.Mode.synchronous) 257 | BLUE_MONKEY_PATCHED = True 258 | 259 | return blue 260 | 261 | 262 | class BlueFormatter(BaseFormatter): 263 | label = "Apply Blue Formatter" 264 | 265 | @property 266 | def importable(self) -> bool: 267 | return is_importable("blue") 268 | 269 | @staticmethod 270 | def handle_options(**options): 271 | blue = import_blue() 272 | 273 | return {"mode": blue.black.FileMode(**options)} 274 | 275 | @handle_line_ending_and_magic 276 | def format_code(self, code: str, notebook: bool, **options) -> str: 277 | blue = import_blue() 278 | 279 | code = blue.black.format_str(code, **self.handle_options(**options)) 280 | return code 281 | 282 | 283 | class BlackFormatter(BaseFormatter): 284 | label = "Apply Black Formatter" 285 | 286 | @property 287 | def importable(self) -> bool: 288 | return is_importable("black") 289 | 290 | @staticmethod 291 | def handle_options(**options): 292 | black = import_black() 293 | 294 | file_mode_change_version = version.parse("19.3b0") 295 | current_black_version = version.parse(black.__version__) 296 | if current_black_version >= file_mode_change_version: 297 | return {"mode": black.FileMode(**options)} 298 | else: 299 | return options 300 | 301 | @handle_line_ending_and_magic 302 | def format_code(self, code: str, notebook: bool, **options) -> str: 303 | black = import_black() 304 | 305 | code = black.format_str(code, **self.handle_options(**options)) 306 | return code 307 | 308 | 309 | class Autopep8Formatter(BaseFormatter): 310 | label = "Apply Autopep8 Formatter" 311 | 312 | @property 313 | def importable(self) -> bool: 314 | return is_importable("autopep8") 315 | 316 | @handle_line_ending_and_magic 317 | def format_code(self, code: str, notebook: bool, **options) -> str: 318 | from autopep8 import fix_code 319 | 320 | return fix_code(code, options=options) 321 | 322 | 323 | class YapfFormatter(BaseFormatter): 324 | label = "Apply YAPF Formatter" 325 | 326 | @property 327 | def importable(self) -> bool: 328 | return is_importable("yapf") 329 | 330 | @handle_line_ending_and_magic 331 | def format_code(self, code: str, notebook: bool, **options) -> str: 332 | from yapf.yapflib.yapf_api import FormatCode 333 | 334 | return FormatCode(code, **options)[0] 335 | 336 | 337 | class IsortFormatter(BaseFormatter): 338 | label = "Apply Isort Formatter" 339 | 340 | @property 341 | def importable(self) -> bool: 342 | return is_importable("isort") 343 | 344 | @handle_line_ending_and_magic 345 | def format_code(self, code: str, notebook: bool, **options) -> str: 346 | try: 347 | from isort import SortImports 348 | 349 | return SortImports(file_contents=code, **options).output 350 | except ImportError: 351 | import isort 352 | 353 | return isort.code(code=code, **options) 354 | 355 | 356 | class RFormatter(BaseFormatter): 357 | @property 358 | @abc.abstractmethod 359 | def package_name(self) -> str: 360 | pass 361 | 362 | @property 363 | def importable(self) -> bool: 364 | if not command_exist("Rscript"): 365 | return False 366 | 367 | package_location = subprocess.run( 368 | ["Rscript", "-e", f"cat(system.file(package='{self.package_name}'))"], 369 | capture_output=True, 370 | text=True, 371 | ) 372 | return package_location != "" 373 | 374 | 375 | class FormatRFormatter(RFormatter): 376 | label = "Apply FormatR Formatter" 377 | package_name = "formatR" 378 | 379 | @handle_line_ending_and_magic 380 | def format_code(self, code: str, notebook: bool, **options) -> str: 381 | import rpy2.robjects.packages as rpackages 382 | from rpy2.robjects import conversion, default_converter 383 | 384 | with conversion.localconverter(default_converter): 385 | format_r = rpackages.importr(self.package_name, robject_translations={".env": "env"}) 386 | formatted_code = format_r.tidy_source(text=code, output=False, **options) 387 | return "\n".join(formatted_code[0]) 388 | 389 | 390 | class StylerFormatter(RFormatter): 391 | label = "Apply Styler Formatter" 392 | package_name = "styler" 393 | 394 | @handle_line_ending_and_magic 395 | def format_code(self, code: str, notebook: bool, **options) -> str: 396 | import rpy2.robjects.packages as rpackages 397 | from rpy2.robjects import conversion, default_converter 398 | 399 | with conversion.localconverter(default_converter): 400 | styler_r = rpackages.importr(self.package_name) 401 | formatted_code = styler_r.style_text(code, **self._transform_options(styler_r, options)) 402 | return "\n".join(formatted_code) 403 | 404 | @staticmethod 405 | def _transform_options(styler_r, options): 406 | transformed_options = copy.deepcopy(options) 407 | import rpy2.robjects 408 | 409 | if "math_token_spacing" in transformed_options: 410 | if isinstance(options["math_token_spacing"], dict): 411 | transformed_options["math_token_spacing"] = rpy2.robjects.ListVector( 412 | options["math_token_spacing"] 413 | ) 414 | else: 415 | transformed_options["math_token_spacing"] = rpy2.robjects.ListVector( 416 | getattr(styler_r, options["math_token_spacing"])() 417 | ) 418 | 419 | if "reindention" in transformed_options: 420 | if isinstance(options["reindention"], dict): 421 | transformed_options["reindention"] = rpy2.robjects.ListVector(options["reindention"]) 422 | else: 423 | transformed_options["reindention"] = rpy2.robjects.ListVector( 424 | getattr(styler_r, options["reindention"])() 425 | ) 426 | return transformed_options 427 | 428 | 429 | class CommandLineFormatter(BaseFormatter): 430 | command: List[str] 431 | 432 | def __init__(self, command: List[str]): 433 | self.command = command 434 | 435 | @property 436 | def label(self) -> str: 437 | return f"Apply {self.command[0]} Formatter" 438 | 439 | @property 440 | def importable(self) -> bool: 441 | return command_exist(self.command[0]) 442 | 443 | @handle_line_ending_and_magic 444 | def format_code(self, code: str, notebook: bool, args: List[str] = [], **options) -> str: 445 | process = subprocess.run( 446 | self.command + args, 447 | input=code, 448 | stdout=subprocess.PIPE, 449 | stderr=subprocess.PIPE, 450 | universal_newlines=True, 451 | ) 452 | 453 | if process.stderr: 454 | logger.info(f"An error with {self.command[0]} has occurred:") 455 | logger.info(process.stderr) 456 | return code 457 | else: 458 | return process.stdout 459 | 460 | 461 | class RuffFixFormatter(CommandLineFormatter): 462 | ruff_args = ["check", "-eq", "--fix-only", "-"] 463 | 464 | @property 465 | def label(self) -> str: 466 | return "Apply ruff fix" 467 | 468 | def __init__(self): 469 | try: 470 | from ruff.__main__ import find_ruff_bin 471 | 472 | ruff_command = find_ruff_bin() 473 | except (ImportError, FileNotFoundError): 474 | ruff_command = "ruff" 475 | self.command = [ruff_command, *self.ruff_args] 476 | 477 | 478 | class RuffFormatFormatter(RuffFixFormatter): 479 | @property 480 | def label(self) -> str: 481 | return "Apply ruff formatter" 482 | 483 | ruff_args = ["format", "-q", "-"] 484 | 485 | 486 | SERVER_FORMATTERS = { 487 | "black": BlackFormatter(), 488 | "blue": BlueFormatter(), 489 | "autopep8": Autopep8Formatter(), 490 | "yapf": YapfFormatter(), 491 | "isort": IsortFormatter(), 492 | "ruff": RuffFixFormatter(), 493 | "ruffformat": RuffFormatFormatter(), 494 | "formatR": FormatRFormatter(), 495 | "styler": StylerFormatter(), 496 | "scalafmt": CommandLineFormatter(command=["scalafmt", "--stdin"]), 497 | "rustfmt": CommandLineFormatter(command=["rustfmt"]), 498 | "astyle": CommandLineFormatter(command=["astyle"]), 499 | } 500 | -------------------------------------------------------------------------------- /jupyterlab_code_formatter/handlers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import tornado 4 | from jupyter_server.base.handlers import APIHandler 5 | from jupyter_server.utils import url_path_join 6 | 7 | from jupyterlab_code_formatter.formatters import SERVER_FORMATTERS 8 | 9 | 10 | class FormattersAPIHandler(APIHandler): 11 | @tornado.web.authenticated 12 | def get(self) -> None: 13 | """Show what formatters are installed and available.""" 14 | use_cache = self.get_query_argument("cached", default=None) 15 | self.finish( 16 | json.dumps( 17 | { 18 | "formatters": { 19 | name: { 20 | "enabled": formatter.cached_importable if use_cache else formatter.importable, 21 | "label": formatter.label, 22 | } 23 | for name, formatter in SERVER_FORMATTERS.items() 24 | } 25 | } 26 | ) 27 | ) 28 | 29 | 30 | class FormatAPIHandler(APIHandler): 31 | @tornado.web.authenticated 32 | def post(self) -> None: 33 | data = json.loads(self.request.body.decode("utf-8")) 34 | formatter_instance = SERVER_FORMATTERS.get(data["formatter"]) 35 | use_cache = self.get_query_argument("cached", default=None) 36 | 37 | if formatter_instance is None or not ( 38 | formatter_instance.cached_importable if use_cache else formatter_instance.importable 39 | ): 40 | self.set_status(404, f"Formatter {data['formatter']} not found!") 41 | self.finish() 42 | else: 43 | notebook = data["notebook"] 44 | options = data.get("options", {}) 45 | formatted_code = [] 46 | for code in data["code"]: 47 | try: 48 | formatted_code.append({"code": formatter_instance.format_code(code, notebook, **options)}) 49 | except Exception as e: 50 | formatted_code.append({"error": str(e)}) 51 | self.finish(json.dumps({"code": formatted_code})) 52 | 53 | 54 | def setup_handlers(web_app): 55 | host_pattern = ".*$" 56 | 57 | base_url = web_app.settings["base_url"] 58 | 59 | web_app.add_handlers( 60 | host_pattern, 61 | [ 62 | ( 63 | url_path_join(base_url, "jupyterlab_code_formatter/formatters"), 64 | FormattersAPIHandler, 65 | ) 66 | ], 67 | ) 68 | 69 | web_app.add_handlers( 70 | host_pattern, 71 | [ 72 | ( 73 | url_path_join(base_url, "/jupyterlab_code_formatter/format"), 74 | FormatAPIHandler, 75 | ) 76 | ], 77 | ) 78 | -------------------------------------------------------------------------------- /jupyterlab_code_formatter/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Python unit tests for jupyterlab_code_formatter.""" 2 | -------------------------------------------------------------------------------- /jupyterlab_code_formatter/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | import typing as t 3 | 4 | import pytest 5 | from jupyter_server.serverapp import ServerApp 6 | from tornado.httpclient import HTTPResponse 7 | 8 | from jupyterlab_code_formatter import load_jupyter_server_extension 9 | 10 | 11 | @pytest.fixture(autouse=True) 12 | def jcf_serverapp(jp_serverapp: ServerApp) -> ServerApp: 13 | load_jupyter_server_extension(jp_serverapp) 14 | return jp_serverapp 15 | 16 | 17 | @pytest.fixture 18 | def request_format(jp_fetch): # type: ignore[no-untyped-def] 19 | def do_request( 20 | formatter: str, 21 | code: t.List[str], 22 | options: t.Dict[str, t.Any], 23 | headers: t.Optional[t.Dict[str, t.Any]] = None, 24 | **kwargs: t.Any, 25 | ) -> HTTPResponse: 26 | return jp_fetch( # type: ignore[no-any-return] 27 | "jupyterlab_code_formatter", 28 | "format", 29 | method="POST", 30 | body=json.dumps( 31 | { 32 | "code": code, 33 | "options": options, 34 | "notebook": True, 35 | "formatter": formatter, 36 | } 37 | ), 38 | headers=headers, 39 | **kwargs, 40 | ) 41 | 42 | return do_request 43 | 44 | 45 | @pytest.fixture 46 | def request_list_formatters(jp_fetch): # type: ignore[no-untyped-def] 47 | def do_request( 48 | headers: t.Optional[t.Dict[str, t.Any]] = None, 49 | **kwargs: t.Any, 50 | ) -> HTTPResponse: 51 | return jp_fetch( # type: ignore[no-any-return] 52 | "jupyterlab_code_formatter", 53 | "formatters", 54 | method="GET", 55 | headers=headers, 56 | **kwargs, 57 | ) 58 | 59 | return do_request 60 | -------------------------------------------------------------------------------- /jupyterlab_code_formatter/tests/test_formatters.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | from subprocess import run 5 | from unittest import mock 6 | 7 | import pytest 8 | 9 | from jupyterlab_code_formatter.formatters import SERVER_FORMATTERS 10 | 11 | 12 | def test_env_pollution_on_import(): 13 | # should not pollute environment on import 14 | code = "; ".join( 15 | [ 16 | "from jupyterlab_code_formatter import formatters", 17 | "import json", 18 | "import os", 19 | "assert formatters", 20 | "print(json.dumps(os.environ.copy()))", 21 | ] 22 | ) 23 | result = run([sys.executable, "-c", f"{code}"], capture_output=True, text=True, check=True, env={}) 24 | environ = json.loads(result.stdout) 25 | assert set(environ.keys()) - {"LC_CTYPE"} == set() 26 | 27 | 28 | @pytest.mark.parametrize("name", SERVER_FORMATTERS) 29 | def test_env_pollution_on_importable_check(name): 30 | formatter = SERVER_FORMATTERS[name] 31 | # should not pollute environment on `importable` check 32 | with mock.patch.dict(os.environ, {}, clear=True): 33 | # invoke the property getter 34 | is_importable = formatter.importable 35 | # the environment should have no extra keys 36 | assert set(os.environ.keys()) == set() 37 | if not is_importable: 38 | pytest.skip(f"{name} formatter was not importable, the test may yield false negatives") 39 | -------------------------------------------------------------------------------- /jupyterlab_code_formatter/tests/test_handlers.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | import typing as t 4 | if sys.version_info >= (3, 8): 5 | from importlib.metadata import version 6 | else: 7 | from importlib_metadata import version 8 | 9 | import pytest 10 | from jsonschema import validate 11 | from tornado.httpclient import HTTPResponse 12 | 13 | from jupyterlab_code_formatter.formatters import SERVER_FORMATTERS 14 | 15 | 16 | def _generate_list_formaters_entry_json_schema( 17 | formatter_name: str, 18 | ) -> t.Dict[str, t.Any]: 19 | return { 20 | "type": "object", 21 | "required": [formatter_name], 22 | "properties": { 23 | formatter_name: { 24 | "type": "object", 25 | "required": ["enabled", "label"], 26 | "properties": { 27 | "enabled": {"type": "boolean"}, 28 | "label": {"type": "string"}, 29 | }, 30 | } 31 | }, 32 | } 33 | 34 | 35 | EXPECTED_LIST_FORMATTERS_SCHEMA = { 36 | "type": "object", 37 | "required": ["formatters"], 38 | "properties": { 39 | "formatters": { 40 | formatter_name: _generate_list_formaters_entry_json_schema(formatter_name) 41 | for formatter_name in SERVER_FORMATTERS 42 | } 43 | }, 44 | } 45 | EXPECTED_FROMAT_SCHEMA = { 46 | "type": "object", 47 | "required": ["code"], 48 | "properties": { 49 | "code": { 50 | "type": "array", 51 | "items": { 52 | "type": "object", 53 | "oneOf": [ 54 | { 55 | "additionalProperties": False, 56 | "properties": {"code": {"type": "string"}}, 57 | }, 58 | { 59 | "additionalProperties": False, 60 | "properties": {"error": {"type": "string"}}, 61 | }, 62 | ], 63 | }, 64 | } 65 | }, 66 | } 67 | 68 | 69 | SIMPLE_VALID_PYTHON_CODE = "x= 22; e =1" 70 | 71 | 72 | def _check_http_code_and_schema( 73 | response: HTTPResponse, expected_code: int, expected_schema: t.Dict[str, t.Any] 74 | ) -> t.Dict[str, t.Any]: 75 | assert response.code == expected_code 76 | json_result: t.Dict[str, t.Any] = json.loads(response.body) 77 | validate(instance=json_result, schema=expected_schema) 78 | return json_result 79 | 80 | 81 | async def test_list_formatters(request_list_formatters): # type: ignore[no-untyped-def] 82 | """Check if the formatters list route works.""" 83 | response: HTTPResponse = await request_list_formatters() 84 | _check_http_code_and_schema( 85 | response=response, 86 | expected_code=200, 87 | expected_schema=EXPECTED_LIST_FORMATTERS_SCHEMA, 88 | ) 89 | 90 | 91 | async def test_404_on_unknown(request_format): # type: ignore[no-untyped-def] 92 | """Check that it 404 correctly if formatter name is bad.""" 93 | response: HTTPResponse = await request_format( 94 | formatter="UNKNOWN", 95 | code=[SIMPLE_VALID_PYTHON_CODE], 96 | options={}, 97 | raise_error=False, 98 | ) 99 | assert response.code == 404 100 | 101 | 102 | async def test_can_apply_python_formatter(request_format): # type: ignore[no-untyped-def] 103 | """Check that it can apply black with simple config.""" 104 | response: HTTPResponse = await request_format( 105 | formatter="black", 106 | code=[SIMPLE_VALID_PYTHON_CODE], 107 | options={"line_length": 88}, 108 | ) 109 | json_result = _check_http_code_and_schema( 110 | response=response, 111 | expected_code=200, 112 | expected_schema=EXPECTED_FROMAT_SCHEMA, 113 | ) 114 | assert json_result["code"][0]["code"] == "x = 22\ne = 1" 115 | 116 | 117 | async def test_can_use_black_config(request_format): # type: ignore[no-untyped-def] 118 | """Check that it can apply black with advanced config.""" 119 | given = "some_string='abc'" 120 | expected = "some_string = 'abc'" 121 | 122 | response: HTTPResponse = await request_format( 123 | formatter="black", 124 | code=[given], 125 | options={"line_length": 123, "string_normalization": False}, 126 | ) 127 | json_result = _check_http_code_and_schema( 128 | response=response, 129 | expected_code=200, 130 | expected_schema=EXPECTED_FROMAT_SCHEMA, 131 | ) 132 | assert json_result["code"][0]["code"] == expected 133 | 134 | 135 | async def test_return_error_if_any(request_format): # type: ignore[no-untyped-def] 136 | """Check that it returns the error if any.""" 137 | bad_python = "this_is_bad = 'hihi" 138 | 139 | response: HTTPResponse = await request_format( 140 | formatter="black", 141 | code=[bad_python], 142 | options={"line_length": 123, "string_normalization": False}, 143 | ) 144 | json_result = _check_http_code_and_schema( 145 | response=response, 146 | expected_code=200, 147 | expected_schema=EXPECTED_FROMAT_SCHEMA, 148 | ) 149 | assert json_result["code"][0]["error"] == "Cannot parse: 1:13: this_is_bad = 'hihi" 150 | 151 | 152 | @pytest.mark.parametrize("formatter", ("black", "yapf", "isort")) 153 | async def test_can_handle_magic(request_format, formatter): # type: ignore[no-untyped-def] 154 | """Check that it's fine to run formatters for code with magic.""" 155 | given = '%%timeit\nsome_string = "abc"' 156 | expected = '%%timeit\nsome_string = "abc"' 157 | 158 | response: HTTPResponse = await request_format( 159 | formatter=formatter, 160 | code=[given], 161 | options={}, 162 | ) 163 | json_result = _check_http_code_and_schema( 164 | response=response, 165 | expected_code=200, 166 | expected_schema=EXPECTED_FROMAT_SCHEMA, 167 | ) 168 | assert json_result["code"][0]["code"] == expected 169 | 170 | 171 | @pytest.mark.parametrize("formatter", ("black", "yapf", "isort")) 172 | async def test_can_handle_shell_cmd(request_format, formatter): # type: ignore[no-untyped-def] 173 | """Check that it's fine to run formatters for code with shell cmd.""" 174 | given = '%%timeit\nsome_string = "abc"\n!pwd' 175 | expected = '%%timeit\nsome_string = "abc"\n!pwd' 176 | 177 | response: HTTPResponse = await request_format( 178 | formatter=formatter, 179 | code=[given], 180 | options={}, 181 | ) 182 | json_result = _check_http_code_and_schema( 183 | response=response, 184 | expected_code=200, 185 | expected_schema=EXPECTED_FROMAT_SCHEMA, 186 | ) 187 | assert json_result["code"][0]["code"] == expected 188 | 189 | 190 | @pytest.mark.parametrize("formatter", ("black", "yapf", "isort")) 191 | async def test_can_handle_incompatible_magic_language(request_format, formatter): # type: ignore[no-untyped-def] 192 | """Check that it will ignore incompatible magic language cellblock.""" 193 | given = "%%html\n

Hi

" 194 | expected = "%%html\n

Hi

" 195 | 196 | response: HTTPResponse = await request_format( 197 | formatter=formatter, 198 | code=[given], 199 | options={}, 200 | ) 201 | json_result = _check_http_code_and_schema( 202 | response=response, 203 | expected_code=200, 204 | expected_schema=EXPECTED_FROMAT_SCHEMA, 205 | ) 206 | assert json_result["code"][0]["code"] == expected 207 | 208 | 209 | @pytest.mark.parametrize("formatter", ("black", "yapf", "isort")) 210 | async def test_can_handle_incompatible_magic_language_single(request_format, formatter): # type: ignore[no-untyped-def] 211 | """Check that it will ignore incompatible magic language cellblock with single %.""" 212 | given = "%html

Hi

" 213 | expected = "%html

Hi

" 214 | 215 | response: HTTPResponse = await request_format( 216 | formatter=formatter, 217 | code=[given], 218 | options={}, 219 | ) 220 | json_result = _check_http_code_and_schema( 221 | response=response, 222 | expected_code=200, 223 | expected_schema=EXPECTED_FROMAT_SCHEMA, 224 | ) 225 | assert json_result["code"][0]["code"] == expected 226 | 227 | 228 | async def test_can_ipython_help_signle(request_format): # type: ignore[no-untyped-def] 229 | """Check that it will ignore single question mark interactive help lines on the fly.""" 230 | given = " bruh?\nprint('test')\n#test?" 231 | expected = ' bruh?\nprint("test")\n# test?' 232 | 233 | response: HTTPResponse = await request_format( 234 | formatter="black", 235 | code=[given], 236 | options={}, 237 | ) 238 | json_result = _check_http_code_and_schema( 239 | response=response, 240 | expected_code=200, 241 | expected_schema=EXPECTED_FROMAT_SCHEMA, 242 | ) 243 | assert json_result["code"][0]["code"] == expected 244 | 245 | 246 | async def test_can_ipython_help_double(request_format): # type: ignore[no-untyped-def] 247 | """Check that it will ignore double question mark interactive help lines on the fly.""" 248 | given = " bruh??\nprint('test')\n#test?" 249 | expected = ' bruh??\nprint("test")\n# test?' 250 | 251 | response: HTTPResponse = await request_format( 252 | formatter="black", 253 | code=[given], 254 | options={}, 255 | ) 256 | json_result = _check_http_code_and_schema( 257 | response=response, 258 | expected_code=200, 259 | expected_schema=EXPECTED_FROMAT_SCHEMA, 260 | ) 261 | assert json_result["code"][0]["code"] == expected 262 | 263 | 264 | async def test_can_ipython_help_signle_leading(request_format): # type: ignore[no-untyped-def] 265 | """Check that it will ignore leading single question mark interactive help lines on the fly.""" 266 | given = " ?bruh\nprint('test')\n#test?" 267 | expected = ' ?bruh\nprint("test")\n# test?' 268 | 269 | response: HTTPResponse = await request_format( 270 | formatter="black", 271 | code=[given], 272 | options={}, 273 | ) 274 | json_result = _check_http_code_and_schema( 275 | response=response, 276 | expected_code=200, 277 | expected_schema=EXPECTED_FROMAT_SCHEMA, 278 | ) 279 | assert json_result["code"][0]["code"] == expected 280 | 281 | 282 | async def test_can_ipython_help_double_leading(request_format): # type: ignore[no-untyped-def] 283 | """Check that it will ignore leading double question mark interactive help lines on the fly.""" 284 | given = " ??bruh\nprint('test')\n#test?" 285 | expected = ' ??bruh\nprint("test")\n# test?' 286 | 287 | response: HTTPResponse = await request_format( 288 | formatter="black", 289 | code=[given], 290 | options={}, 291 | ) 292 | json_result = _check_http_code_and_schema( 293 | response=response, 294 | expected_code=200, 295 | expected_schema=EXPECTED_FROMAT_SCHEMA, 296 | ) 297 | assert json_result["code"][0]["code"] == expected 298 | 299 | 300 | async def test_will_ignore_quarto_comments(request_format): # type: ignore[no-untyped-def] 301 | """Check that it will ignore Quarto's comments at the top of a block.""" 302 | given = """#| eval: false 303 | 1 + 1""" 304 | 305 | response: HTTPResponse = await request_format( 306 | formatter="black", 307 | code=[given], 308 | options={}, 309 | ) 310 | json_result = _check_http_code_and_schema( 311 | response=response, 312 | expected_code=200, 313 | expected_schema=EXPECTED_FROMAT_SCHEMA, 314 | ) 315 | assert json_result["code"][0]["code"] == given 316 | 317 | 318 | async def test_will_ignore_run_command(request_format): # type: ignore[no-untyped-def] 319 | """Check that it will ignore run command.""" 320 | given = " run some_script.py" 321 | 322 | response: HTTPResponse = await request_format( 323 | formatter="black", 324 | code=[given], 325 | options={}, 326 | ) 327 | json_result = _check_http_code_and_schema( 328 | response=response, 329 | expected_code=200, 330 | expected_schema=EXPECTED_FROMAT_SCHEMA, 331 | ) 332 | assert json_result["code"][0]["code"] == given 333 | 334 | 335 | async def test_will_ignore_question_mark(request_format): # type: ignore[no-untyped-def] 336 | """Check that it will ignore single question mark in comments.""" 337 | given = """def f(): 338 | # bruh what? 339 | # again bruh? really 340 | # a ? b 341 | print('hi') 342 | x = '?'""" 343 | expected = """def f(): 344 | # bruh what? 345 | # again bruh? really 346 | # a ? b 347 | print("hi") 348 | x = "?\"""" 349 | 350 | response: HTTPResponse = await request_format( 351 | formatter="black", 352 | code=[given], 353 | options={}, 354 | ) 355 | json_result = _check_http_code_and_schema( 356 | response=response, 357 | expected_code=200, 358 | expected_schema=EXPECTED_FROMAT_SCHEMA, 359 | ) 360 | assert json_result["code"][0]["code"] == expected 361 | 362 | 363 | async def test_will_ignore_question_mark2(request_format): # type: ignore[no-untyped-def] 364 | """Check that it will ignore double question mark in comments.""" 365 | given = """def f(): 366 | # bruh what?? 367 | # again bruh?? really 368 | # a ? b ? c 369 | print('hi')""" 370 | expected = """def f(): 371 | # bruh what?? 372 | # again bruh?? really 373 | # a ? b ? c 374 | print("hi")""" 375 | 376 | response: HTTPResponse = await request_format( 377 | formatter="black", 378 | code=[given], 379 | options={}, 380 | ) 381 | json_result = _check_http_code_and_schema( 382 | response=response, 383 | expected_code=200, 384 | expected_schema=EXPECTED_FROMAT_SCHEMA, 385 | ) 386 | assert json_result["code"][0]["code"] == expected 387 | 388 | 389 | async def test_will_ignore_question_weird(request_format): # type: ignore[no-untyped-def] 390 | given = """wat 391 | wat??""" 392 | expected = """wat 393 | wat??""" 394 | 395 | response: HTTPResponse = await request_format( 396 | formatter="black", 397 | code=[given], 398 | options={}, 399 | ) 400 | json_result = _check_http_code_and_schema( 401 | response=response, 402 | expected_code=200, 403 | expected_schema=EXPECTED_FROMAT_SCHEMA, 404 | ) 405 | assert json_result["code"][0]["code"] == expected 406 | 407 | 408 | async def test_can_use_styler(request_format): # type: ignore[no-untyped-def] 409 | given = "a = 3; 2" 410 | expected = "a <- 3\n2" 411 | 412 | response: HTTPResponse = await request_format( 413 | formatter="styler", 414 | code=[given], 415 | options={"scope": "tokens"}, 416 | ) 417 | json_result = _check_http_code_and_schema( 418 | response=response, 419 | expected_code=200, 420 | expected_schema=EXPECTED_FROMAT_SCHEMA, 421 | ) 422 | assert json_result["code"][0]["code"] == expected 423 | 424 | 425 | async def test_can_use_styler2(request_format): # type: ignore[no-untyped-def] 426 | given = """data_frame( 427 | small = 2 , 428 | medium = 4,#comment without space 429 | large =6 430 | )""" 431 | expected = """data_frame( 432 | small = 2, 433 | medium = 4, # comment without space 434 | large = 6 435 | )""" 436 | 437 | response: HTTPResponse = await request_format( 438 | formatter="styler", 439 | code=[given], 440 | options={"strict": False}, 441 | ) 442 | json_result = _check_http_code_and_schema( 443 | response=response, 444 | expected_code=200, 445 | expected_schema=EXPECTED_FROMAT_SCHEMA, 446 | ) 447 | assert json_result["code"][0]["code"] == expected 448 | 449 | 450 | async def test_can_use_styler3(request_format): # type: ignore[no-untyped-def] 451 | given = "1++1/2*2^2" 452 | expected = "1 + +1/2*2^2" 453 | 454 | response: HTTPResponse = await request_format( 455 | formatter="styler", 456 | code=[given], 457 | options={ 458 | "math_token_spacing": { 459 | "one": ["'+'", "'-'"], 460 | "zero": ["'/'", "'*'", "'^'"], 461 | } 462 | }, 463 | ) 464 | json_result = _check_http_code_and_schema( 465 | response=response, 466 | expected_code=200, 467 | expected_schema=EXPECTED_FROMAT_SCHEMA, 468 | ) 469 | assert json_result["code"][0]["code"] == expected 470 | 471 | 472 | async def test_can_use_styler4(request_format): # type: ignore[no-untyped-def] 473 | given = """a <- function() { 474 | ### not to be indented 475 | # indent normally 476 | 33 477 | }""" 478 | expected = """a <- function() { 479 | ### not to be indented 480 | # indent normally 481 | 33 482 | }""" 483 | 484 | response: HTTPResponse = await request_format( 485 | formatter="styler", 486 | code=[given], 487 | options={ 488 | "reindention": { 489 | "regex_pattern": "^###", 490 | "indention": 0, 491 | "comments_only": True, 492 | } 493 | }, 494 | ) 495 | json_result = _check_http_code_and_schema( 496 | response=response, 497 | expected_code=200, 498 | expected_schema=EXPECTED_FROMAT_SCHEMA, 499 | ) 500 | assert json_result["code"][0]["code"] == expected 501 | 502 | 503 | async def test_can_use_styler5(request_format): # type: ignore[no-untyped-def] 504 | given = """call( 505 | # SHOULD BE ONE SPACE BEFORE 506 | 1,2) 507 | """ 508 | expected = """call( 509 | # SHOULD BE ONE SPACE BEFORE 510 | 1, 2 511 | )""" 512 | 513 | response: HTTPResponse = await request_format( 514 | formatter="styler", 515 | code=[given], 516 | options={"indent_by": 4, "start_comments_with_one_space": True}, 517 | ) 518 | json_result = _check_http_code_and_schema( 519 | response=response, 520 | expected_code=200, 521 | expected_schema=EXPECTED_FROMAT_SCHEMA, 522 | ) 523 | assert json_result["code"][0]["code"] == expected 524 | 525 | 526 | async def test_can_use_styler6(request_format): # type: ignore[no-untyped-def] 527 | given = "1+1-3" 528 | expected = "1 + 1 - 3" 529 | 530 | response: HTTPResponse = await request_format( 531 | formatter="styler", 532 | code=[given], 533 | options={ 534 | "math_token_spacing": "tidyverse_math_token_spacing", 535 | "reindention": "tidyverse_reindention", 536 | }, 537 | ) 538 | json_result = _check_http_code_and_schema( 539 | response=response, 540 | expected_code=200, 541 | expected_schema=EXPECTED_FROMAT_SCHEMA, 542 | ) 543 | assert json_result["code"][0]["code"] == expected 544 | 545 | 546 | @pytest.mark.skip(reason="rust toolchain doesn't seem to be picked up here for some reason.") 547 | async def test_can_rustfmt(request_format): # type: ignore[no-untyped-def] 548 | given = """// function to add two numbers 549 | fn add() { 550 | let a = 5; 551 | let b = 10; 552 | 553 | let sum = a + b; 554 | 555 | println!("Sum of a and b = {}", 556 | sum); 557 | } 558 | 559 | fn main() { 560 | // function call 561 | add(); 562 | }""" 563 | expected = """// function to add two numbers 564 | fn add() { 565 | let a = 5; 566 | let b = 10; 567 | 568 | let sum = a + b; 569 | 570 | println!("Sum of a and b = {}", sum); 571 | } 572 | 573 | fn main() { 574 | // function call 575 | add(); 576 | }""" 577 | 578 | response: HTTPResponse = await request_format( 579 | formatter="rustfmt", 580 | code=[given], 581 | options={}, 582 | ) 583 | json_result = _check_http_code_and_schema( 584 | response=response, 585 | expected_code=200, 586 | expected_schema=EXPECTED_FROMAT_SCHEMA, 587 | ) 588 | assert json_result["code"][0]["code"] == expected 589 | 590 | 591 | IMPORT_SORTING_EXAMPLE = ( 592 | "import numpy as np\nimport sys,os\nfrom enum import IntEnum\nfrom enum import auto" 593 | ) 594 | 595 | 596 | async def test_can_apply_ruff_formatter(request_format): # type: ignore[no-untyped-def] 597 | """Check that it can apply ruff with simple config.""" 598 | response: HTTPResponse = await request_format( 599 | formatter="ruffformat", 600 | code=[SIMPLE_VALID_PYTHON_CODE], 601 | options={}, 602 | ) 603 | json_result = _check_http_code_and_schema( 604 | response=response, 605 | expected_code=200, 606 | expected_schema=EXPECTED_FROMAT_SCHEMA, 607 | ) 608 | assert json_result["code"][0]["code"] == "x = 22\ne = 1" 609 | 610 | 611 | async def test_can_apply_ruff_import_fix(request_format): # type: ignore[no-untyped-def] 612 | """Check that it can organize imports with ruff.""" 613 | 614 | given = "import foo\nimport numpy as np\nimport sys,os\nfrom enum import IntEnum\nfrom enum import auto" 615 | expected = "import os\nimport sys\nfrom enum import IntEnum, auto\n\nimport numpy as np\n\nimport foo" 616 | response: HTTPResponse = await request_format( 617 | formatter="ruff", 618 | code=[given], 619 | options={ 620 | "args": [ 621 | "--select=I001", 622 | "--config", 623 | "lint.isort.known-first-party=['foo']", 624 | ] 625 | }, 626 | ) 627 | json_result = _check_http_code_and_schema( 628 | response=response, 629 | expected_code=200, 630 | expected_schema=EXPECTED_FROMAT_SCHEMA, 631 | ) 632 | assert json_result["code"][0]["code"] == expected 633 | 634 | 635 | async def test_can_apply_ruff_fix_unsafe(request_format): # type: ignore[no-untyped-def] 636 | """Check that it can apply unsafe fixes.""" 637 | 638 | given = """if arg != None: 639 | pass""" 640 | expected = """if arg is not None: 641 | pass""" 642 | response: HTTPResponse = await request_format( 643 | formatter="ruff", 644 | code=[given], 645 | options={"args": ["--select=E711", "--unsafe-fixes"]}, 646 | ) 647 | json_result = _check_http_code_and_schema( 648 | response=response, 649 | expected_code=200, 650 | expected_schema=EXPECTED_FROMAT_SCHEMA, 651 | ) 652 | assert json_result["code"][0]["code"] == expected 653 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyterlab_code_formatter", 3 | "version": "3.0.2", 4 | "description": " A JupyterLab plugin to facilitate invocation of code formatters.", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": " https://github.com/jupyterlab-contrib/jupyterlab_code_formatter", 11 | "bugs": { 12 | "url": " https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": { 16 | "name": "Ryan Tam", 17 | "email": "ryantam626@gmail.com" 18 | }, 19 | "files": [ 20 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 21 | "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", 22 | "schema/*.json" 23 | ], 24 | "main": "lib/index.js", 25 | "types": "lib/index.d.ts", 26 | "style": "style/index.css", 27 | "repository": { 28 | "type": "git", 29 | "url": " https://github.com/jupyterlab-contrib/jupyterlab_code_formatter.git" 30 | }, 31 | "scripts": { 32 | "build": "jlpm build:lib && jlpm build:labextension:dev", 33 | "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", 34 | "build:labextension": "jupyter labextension build .", 35 | "build:labextension:dev": "jupyter labextension build --development True .", 36 | "build:lib": "tsc --sourceMap", 37 | "build:lib:prod": "tsc", 38 | "clean": "jlpm clean:lib", 39 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 40 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 41 | "clean:labextension": "rimraf jupyterlab_code_formatter/labextension jupyterlab_code_formatter/_version.py", 42 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", 43 | "eslint": "jlpm eslint:check --fix", 44 | "eslint:check": "eslint . --cache --ext .ts,.tsx", 45 | "install:extension": "jlpm build", 46 | "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", 47 | "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", 48 | "prettier": "jlpm prettier:base --write --list-different", 49 | "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", 50 | "prettier:check": "jlpm prettier:base --check", 51 | "stylelint": "jlpm stylelint:check --fix", 52 | "stylelint:check": "stylelint --cache \"style/**/*.css\"", 53 | "test": "jest --coverage", 54 | "watch": "run-p watch:src watch:labextension", 55 | "watch:src": "tsc -w", 56 | "watch:labextension": "jupyter labextension watch ." 57 | }, 58 | "dependencies": { 59 | "@jupyterlab/application": "^4.0.0", 60 | "@jupyterlab/coreutils": "^6.0.0", 61 | "@jupyterlab/fileeditor": "^4.0.0", 62 | "@jupyterlab/mainmenu": "^4.0.0", 63 | "@jupyterlab/services": "^7.0.0", 64 | "@jupyterlab/settingregistry": "^4.0.0" 65 | }, 66 | "devDependencies": { 67 | "@jupyterlab/builder": "^4.2.3", 68 | "@jupyterlab/testutils": "^4.0.0", 69 | "@types/jest": "^29.2.0", 70 | "@types/json-schema": "^7.0.11", 71 | "@types/react": "^18.0.26", 72 | "@types/react-addons-linked-state-mixin": "^0.14.22", 73 | "@typescript-eslint/eslint-plugin": "^6.1.0", 74 | "@typescript-eslint/parser": "^6.1.0", 75 | "css-loader": "^6.7.1", 76 | "eslint": "^8.36.0", 77 | "eslint-config-prettier": "^8.8.0", 78 | "eslint-plugin-prettier": "^5.0.0", 79 | "jest": "^29.2.0", 80 | "mkdirp": "^1.0.3", 81 | "npm-run-all": "^4.1.5", 82 | "prettier": "^3.0.0", 83 | "rimraf": "^5.0.1", 84 | "source-map-loader": "^1.0.2", 85 | "style-loader": "^3.3.1", 86 | "stylelint": "^15.10.1", 87 | "stylelint-config-recommended": "^13.0.0", 88 | "stylelint-config-standard": "^34.0.0", 89 | "stylelint-csstree-validator": "^3.0.0", 90 | "stylelint-prettier": "^4.0.0", 91 | "typescript": "~5.0.2", 92 | "yjs": "^13.5.0" 93 | }, 94 | "sideEffects": [ 95 | "style/*.css", 96 | "style/index.js" 97 | ], 98 | "styleModule": "style/index.js", 99 | "publishConfig": { 100 | "access": "public" 101 | }, 102 | "jupyterlab": { 103 | "discovery": { 104 | "server": { 105 | "managers": [ 106 | "pip" 107 | ], 108 | "base": { 109 | "name": "jupyterlab_code_formatter" 110 | } 111 | } 112 | }, 113 | "extension": true, 114 | "outputDir": "jupyterlab_code_formatter/labextension", 115 | "schemaDir": "schema" 116 | }, 117 | "eslintIgnore": [ 118 | "node_modules", 119 | "dist", 120 | "coverage", 121 | "**/*.d.ts", 122 | "tests", 123 | "**/__tests__", 124 | "ui-tests" 125 | ], 126 | "eslintConfig": { 127 | "extends": [ 128 | "eslint:recommended", 129 | "plugin:@typescript-eslint/eslint-recommended", 130 | "plugin:@typescript-eslint/recommended", 131 | "plugin:prettier/recommended" 132 | ], 133 | "parser": "@typescript-eslint/parser", 134 | "parserOptions": { 135 | "project": "tsconfig.json", 136 | "sourceType": "module" 137 | }, 138 | "plugins": [ 139 | "@typescript-eslint" 140 | ], 141 | "rules": { 142 | "@typescript-eslint/naming-convention": [ 143 | "error", 144 | { 145 | "selector": "interface", 146 | "format": [ 147 | "PascalCase" 148 | ], 149 | "custom": { 150 | "regex": "^I[A-Z]", 151 | "match": true 152 | } 153 | } 154 | ], 155 | "@typescript-eslint/no-unused-vars": [ 156 | "warn", 157 | { 158 | "args": "none" 159 | } 160 | ], 161 | "@typescript-eslint/no-explicit-any": "off", 162 | "@typescript-eslint/no-namespace": "off", 163 | "@typescript-eslint/no-use-before-define": "off", 164 | "@typescript-eslint/quotes": [ 165 | "error", 166 | "single", 167 | { 168 | "avoidEscape": true, 169 | "allowTemplateLiterals": false 170 | } 171 | ], 172 | "curly": [ 173 | "error", 174 | "all" 175 | ], 176 | "eqeqeq": "error", 177 | "prefer-arrow-callback": "error" 178 | } 179 | }, 180 | "prettier": { 181 | "singleQuote": true, 182 | "trailingComma": "none", 183 | "arrowParens": "avoid", 184 | "endOfLine": "auto", 185 | "overrides": [ 186 | { 187 | "files": "package.json", 188 | "options": { 189 | "tabWidth": 4 190 | } 191 | } 192 | ] 193 | }, 194 | "stylelint": { 195 | "extends": [ 196 | "stylelint-config-recommended", 197 | "stylelint-config-standard", 198 | "stylelint-prettier/recommended" 199 | ], 200 | "plugins": [ 201 | "stylelint-csstree-validator" 202 | ], 203 | "rules": { 204 | "csstree/validator": true, 205 | "property-no-vendor-prefix": null, 206 | "selector-no-vendor-prefix": null, 207 | "value-no-vendor-prefix": null 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "jupyterlab_code_formatter" 7 | readme = "README.md" 8 | license = { file = "LICENSE" } 9 | requires-python = ">=3.7" 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.7", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | ] 25 | dependencies = [ 26 | "jupyter_server>=1.21,<3", 27 | "packaging", 28 | ] 29 | dynamic = ["version", "description", "authors", "urls", "keywords"] 30 | 31 | [project.optional-dependencies] 32 | dev = [ 33 | "black==22.1.0", 34 | "blue==0.9.1", 35 | "ruff", 36 | "isort", 37 | "yapf", 38 | "rpy2", 39 | "autopep8", 40 | "jupyterlab>=3.0.0", 41 | "click==8.0.2", 42 | 43 | ] 44 | test = [ 45 | "coverage", 46 | "pytest", 47 | "pytest-asyncio", 48 | "pytest-cov", 49 | "pytest-jupyter[server]>=0.6.0", 50 | "black==22.1.0", 51 | "blue==0.9.1", 52 | "isort", 53 | "yapf", 54 | "rpy2", 55 | "importlib_metadata; python_version<'3.8'", 56 | "ruff", 57 | ] 58 | 59 | [tool.hatch.version] 60 | source = "nodejs" 61 | 62 | [tool.hatch.metadata.hooks.nodejs] 63 | fields = ["description", "authors", "urls"] 64 | 65 | [tool.hatch.build.targets.sdist] 66 | artifacts = ["jupyterlab_code_formatter/labextension"] 67 | exclude = [".github", "binder"] 68 | 69 | [tool.hatch.build.targets.wheel.shared-data] 70 | "jupyterlab_code_formatter/labextension" = "share/jupyter/labextensions/jupyterlab_code_formatter" 71 | "install.json" = "share/jupyter/labextensions/jupyterlab_code_formatter/install.json" 72 | "jupyter-config/server-config" = "etc/jupyter/jupyter_server_config.d" 73 | "jupyter-config/nb-config" = "etc/jupyter/jupyter_notebook_config.d" 74 | 75 | [tool.hatch.build.hooks.version] 76 | path = "jupyterlab_code_formatter/_version.py" 77 | 78 | [tool.hatch.build.hooks.jupyter-builder] 79 | dependencies = ["hatch-jupyter-builder>=0.5"] 80 | build-function = "hatch_jupyter_builder.npm_builder" 81 | ensured-targets = [ 82 | "jupyterlab_code_formatter/labextension/static/style.js", 83 | "jupyterlab_code_formatter/labextension/package.json", 84 | ] 85 | skip-if-exists = ["jupyterlab_code_formatter/labextension/static/style.js"] 86 | 87 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 88 | build_cmd = "build:prod" 89 | npm = ["jlpm"] 90 | 91 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] 92 | build_cmd = "install:extension" 93 | npm = ["jlpm"] 94 | source_dir = "src" 95 | build_dir = "jupyterlab_code_formatter/labextension" 96 | 97 | [tool.jupyter-releaser.options] 98 | version_cmd = "hatch version" 99 | 100 | [tool.jupyter-releaser.hooks] 101 | before-build-npm = [ 102 | "python -m pip install 'jupyterlab>=4.0.0,<5'", 103 | "jlpm", 104 | "jlpm build:prod" 105 | ] 106 | before-build-python = ["jlpm clean:all"] 107 | 108 | [tool.check-wheel-contents] 109 | ignore = ["W002"] 110 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # frozen requirements generated by pip-deepfreeze 2 | aiofiles==22.1.0 3 | aiosqlite==0.19.0 4 | asttokens==2.2.1 5 | autopep8==1.6.0 6 | Babel==2.12.1 7 | backcall==0.2.0 8 | black==22.1.0 9 | blue==0.9.1 10 | certifi==2022.12.7 11 | charset-normalizer==3.1.0 12 | click==8.0.2 13 | comm==0.1.3 14 | debugpy==1.6.7 15 | decorator==5.1.1 16 | executing==1.2.0 17 | flake8==4.0.1 18 | ipykernel==6.22.0 19 | ipython==8.13.2 20 | ipython-genutils==0.2.0 21 | isort==5.12.0 22 | jedi==0.18.2 23 | json5==0.9.11 24 | jupyter-ydoc==0.2.4 25 | jupyter_server_fileid==0.9.0 26 | jupyter_server_ydoc==0.8.0 27 | jupyterlab==3.6.3 28 | jupyterlab_server==2.22.1 29 | matplotlib-inline==0.1.6 30 | mccabe==0.6.1 31 | mypy-extensions==1.0.0 32 | nbclassic==1.0.0 33 | nest-asyncio==1.5.6 34 | notebook==6.5.4 35 | notebook_shim==0.2.3 36 | parso==0.8.3 37 | pathspec==0.11.1 38 | pexpect==4.8.0 39 | pickleshare==0.7.5 40 | prompt-toolkit==3.0.38 41 | psutil==5.9.5 42 | pure-eval==0.2.2 43 | pycodestyle==2.8.0 44 | pyflakes==2.4.0 45 | pytz==2023.3 46 | pytz-deprecation-shim==0.1.0.post0 47 | requests==2.30.0 48 | rpy2==3.5.11 49 | ruff==0.0.265 50 | stack-data==0.6.2 51 | toml==0.10.2 52 | tomli==2.0.1 53 | tzdata==2023.3 54 | tzlocal==4.3 55 | urllib3==2.0.2 56 | wcwidth==0.2.6 57 | y-py==0.5.9 58 | yapf==0.33.0 59 | ypy-websocket==0.8.2 60 | -------------------------------------------------------------------------------- /requirements-readthedocs.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.13 2 | Babel==2.12.1 3 | certifi==2023.5.7 4 | charset-normalizer==3.1.0 5 | docutils==0.19 6 | furo==2023.3.27 7 | idna==3.4 8 | imagesize==1.4.1 9 | importlib-metadata==6.6.0 10 | Jinja2==3.1.2 11 | markdown-it-py==2.2.0 12 | MarkupSafe==2.1.2 13 | mdit-py-plugins==0.3.5 14 | mdurl==0.1.2 15 | myst-parser==1.0.0 16 | packaging==23.1 17 | Pygments==2.15.1 18 | pytz==2023.3 19 | PyYAML==6.0 20 | requests==2.30.0 21 | snowballstemmer==2.2.0 22 | sphinx-copybutton==0.5.2 23 | Sphinx==5.3.0 24 | sphinxcontrib-applehelp==1.0.2 25 | sphinxcontrib-devhelp==1.0.2 26 | sphinxcontrib-htmlhelp==2.0.0 27 | sphinxcontrib-jsmath==1.0.1 28 | sphinxcontrib-qthelp==1.0.3 29 | sphinxcontrib-serializinghtml==1.1.5 30 | sphinx_inline_tabs==2021.3.28b7 31 | typing_extensions==4.5.0 32 | urllib3==1.26.15 33 | zipp==3.15.0 34 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | # frozen requirements generated by pip-deepfreeze 2 | asttokens==2.2.1 3 | backcall==0.2.0 4 | black==22.1.0 5 | blue==0.9.1 6 | click==8.0.2 7 | comm==0.1.3 8 | coverage==7.2.5 9 | debugpy==1.6.7 10 | decorator==5.1.1 11 | exceptiongroup==1.1.1 12 | executing==1.2.0 13 | flake8==4.0.1 14 | iniconfig==2.0.0 15 | ipykernel==6.22.0 16 | ipython==8.13.2 17 | isort==5.12.0 18 | jedi==0.18.2 19 | matplotlib-inline==0.1.6 20 | mccabe==0.6.1 21 | mypy-extensions==1.0.0 22 | nest-asyncio==1.5.6 23 | parso==0.8.3 24 | pathspec==0.11.1 25 | pexpect==4.8.0 26 | pickleshare==0.7.5 27 | pluggy==1.0.0 28 | prompt-toolkit==3.0.38 29 | psutil==5.9.5 30 | pure-eval==0.2.2 31 | pycodestyle==2.8.0 32 | pyflakes==2.4.0 33 | pytest==7.3.1 34 | pytest-asyncio==0.21.0 35 | pytest-cov==4.0.0 36 | pytest-jupyter==0.7.0 37 | stack-data==0.6.2 38 | tomli==2.0.1 39 | wcwidth==0.2.6 40 | yapf==0.33.0 41 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # frozen requirements generated by pip-deepfreeze 2 | anyio==3.6.2 3 | argon2-cffi==21.3.0 4 | argon2-cffi-bindings==21.2.0 5 | arrow==1.2.3 6 | attrs==23.1.0 7 | beautifulsoup4==4.12.2 8 | bleach==6.0.0 9 | cffi==1.15.1 10 | defusedxml==0.7.1 11 | fastjsonschema==2.16.3 12 | fqdn==1.5.1 13 | idna==3.4 14 | isoduration==20.11.0 15 | Jinja2==3.1.2 16 | jsonpointer==2.3 17 | jsonschema==4.17.3 18 | jupyter-events==0.6.3 19 | jupyter_client==8.2.0 20 | jupyter_core==5.3.0 21 | jupyter_server==2.5.0 22 | jupyter_server_terminals==0.4.4 23 | jupyterlab-pygments==0.2.2 24 | MarkupSafe==2.1.2 25 | mistune==2.0.5 26 | nbclient==0.7.4 27 | nbconvert==7.3.1 28 | nbformat==5.8.0 29 | packaging==23.1 30 | pandocfilters==1.5.0 31 | platformdirs==3.5.0 32 | prometheus-client==0.16.0 33 | ptyprocess==0.7.0 34 | pycparser==2.21 35 | Pygments==2.15.1 36 | pyrsistent==0.19.3 37 | python-dateutil==2.8.2 38 | python-json-logger==2.0.7 39 | PyYAML==6.0 40 | pyzmq==25.0.2 41 | rfc3339-validator==0.1.4 42 | rfc3986-validator==0.1.1 43 | Send2Trash==1.8.2 44 | six==1.16.0 45 | sniffio==1.3.0 46 | soupsieve==2.4.1 47 | terminado==0.17.1 48 | tinycss2==1.2.1 49 | tornado==6.3.1 50 | traitlets==5.9.0 51 | uri-template==1.2.0 52 | webcolors==1.13 53 | webencodings==0.5.1 54 | websocket-client==1.5.1 55 | -------------------------------------------------------------------------------- /schema/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "jupyter.lab.setting-icon-class": "jp-EditIcon", 3 | "jupyter.lab.setting-icon-label": "Jupyterlab Code Formatter", 4 | "title": "Jupyterlab Code Formatter", 5 | "description": "Jupyterlab Code Formatter settings.", 6 | "definitions": { 7 | "preferences": { 8 | "properties": { 9 | "default_formatter": { 10 | "properties": { 11 | "python": { 12 | "anyOf": [ 13 | { "type": "string" }, 14 | { "type": "array", "items": { "type": "string" } } 15 | ] 16 | }, 17 | "R": { 18 | "anyOf": [ 19 | { "type": "string" }, 20 | { "type": "array", "items": { "type": "string" } } 21 | ] 22 | } 23 | }, 24 | "additionalProperties": true, 25 | "type": "object" 26 | } 27 | }, 28 | "additionalProperties": false, 29 | "type": "object" 30 | }, 31 | "black": { 32 | "properties": { 33 | "line_length": { 34 | "type": "number" 35 | }, 36 | "string_normalization": { 37 | "type": "boolean" 38 | }, 39 | "magic_trailing_comma": { 40 | "type": "boolean" 41 | }, 42 | "experimental_string_processing": { 43 | "type": "boolean" 44 | }, 45 | "preview": { 46 | "type": "boolean" 47 | } 48 | }, 49 | "additionalProperties": false, 50 | "type": "object" 51 | }, 52 | "isort": { 53 | "properties": { 54 | "profile": { 55 | "type": "string" 56 | }, 57 | "ensure_newline_before_comments": { 58 | "type": "boolean" 59 | }, 60 | "force_to_top": { 61 | "type": "string" 62 | }, 63 | "line_length": { 64 | "type": "number" 65 | }, 66 | "wrap_length": { 67 | "type": "number" 68 | }, 69 | "sections": { 70 | "type": "array", 71 | "items": { "type": "string" } 72 | }, 73 | "known_future_library": { 74 | "type": "array", 75 | "items": { "type": "string" } 76 | }, 77 | "known_standard_library": { 78 | "type": "array", 79 | "items": { "type": "string" } 80 | }, 81 | "known_third_party": { 82 | "type": "array", 83 | "items": { "type": "string" } 84 | }, 85 | "known_first_party": { 86 | "type": "array", 87 | "items": { "type": "string" } 88 | }, 89 | "multi_line_output": { 90 | "type": "number" 91 | }, 92 | "forced_separate": { 93 | "type": "string" 94 | }, 95 | "indent": { 96 | "type": "number" 97 | }, 98 | "length_sort": { 99 | "type": "boolean" 100 | }, 101 | "force_single_line": { 102 | "type": "boolean" 103 | }, 104 | "force_grid_wrap": { 105 | "type": "number" 106 | }, 107 | "default_section": { 108 | "type": "string" 109 | }, 110 | "import_heading_future": { 111 | "type": "string" 112 | }, 113 | "import_heading_stdlib": { 114 | "type": "string" 115 | }, 116 | "import_heading_thirdparty": { 117 | "type": "string" 118 | }, 119 | "import_heading_firstparty": { 120 | "type": "string" 121 | }, 122 | "import_heading_localfolder": { 123 | "type": "string" 124 | }, 125 | "balanced_wrapping": { 126 | "type": "boolean" 127 | }, 128 | "order_by_type": { 129 | "type": "boolean" 130 | }, 131 | "lines_after_imports": { 132 | "type": "number" 133 | }, 134 | "lines_between_types": { 135 | "type": "number" 136 | }, 137 | "combine_as_imports": { 138 | "type": "boolean" 139 | }, 140 | "combine_star": { 141 | "type": "boolean" 142 | }, 143 | "include_trailing_comma": { 144 | "type": "boolean" 145 | }, 146 | "use_parentheses": { 147 | "type": "boolean" 148 | }, 149 | "from_first": { 150 | "type": "boolean" 151 | }, 152 | "case_sensitive": { 153 | "type": "boolean" 154 | }, 155 | "force_alphabetical_sort": { 156 | "type": "boolean" 157 | } 158 | }, 159 | "patternProperties": { 160 | "^known_[a-z_]+": { 161 | "type": "array", 162 | "items": { "type": "string" } 163 | } 164 | }, 165 | "additionalProperties": false, 166 | "type": "object" 167 | }, 168 | "yapf": { 169 | "properties": { 170 | "style_config": { 171 | "type": "string" 172 | } 173 | }, 174 | "additionalProperties": false, 175 | "type": "object" 176 | }, 177 | "autopep8": { 178 | "properties": { 179 | "aggressive": { 180 | "type": "number" 181 | }, 182 | "max_line_length": { 183 | "type": "number" 184 | }, 185 | "ignore": { 186 | "type": "array", 187 | "items": { 188 | "type": "string" 189 | } 190 | }, 191 | "select": { 192 | "type": "array", 193 | "items": { 194 | "type": "string" 195 | } 196 | }, 197 | "experimental": { 198 | "type": "boolean" 199 | } 200 | }, 201 | "additionalProperties": false, 202 | "type": "object" 203 | }, 204 | "ruff": { 205 | "properties": { 206 | "args": { "type": "array", "items": { "type": "string" } } 207 | }, 208 | "additionalProperties": false, 209 | "type": "object" 210 | }, 211 | "ruffformat": { 212 | "properties": { 213 | "args": { "type": "array", "items": { "type": "string" } } 214 | }, 215 | "additionalProperties": false, 216 | "type": "object" 217 | }, 218 | "formatR": { 219 | "properties": { 220 | "comment": { 221 | "type": "boolean" 222 | }, 223 | "blank": { 224 | "type": "boolean" 225 | }, 226 | "arrow": { 227 | "type": "boolean" 228 | }, 229 | "brace_newline": { 230 | "type": "boolean" 231 | }, 232 | "indent": { 233 | "type": "number" 234 | }, 235 | "wrap": { 236 | "type": "boolean" 237 | }, 238 | "width_cutoff": { 239 | "type": "number" 240 | } 241 | }, 242 | "additionalProperties": false, 243 | "type": "object" 244 | }, 245 | "styler": { 246 | "properties": { 247 | "scope": { 248 | "type": "string" 249 | }, 250 | "strict": { 251 | "type": "boolean" 252 | }, 253 | "indent_by": { 254 | "type": "number" 255 | }, 256 | "start_comments_with_one_space": { 257 | "type": "boolean" 258 | }, 259 | "math_token_spacing": { 260 | "oneOf": [ 261 | { 262 | "properties": { 263 | "zero": { 264 | "oneOf": [ 265 | { "type": "string" }, 266 | { "type": "array", "items": { "type": "string" } } 267 | ] 268 | }, 269 | "one": { 270 | "oneOf": [ 271 | { "type": "string" }, 272 | { "type": "array", "items": { "type": "string" } } 273 | ] 274 | } 275 | }, 276 | "additionalProperties": false, 277 | "type": "object" 278 | }, 279 | { "type": "string" } 280 | ] 281 | }, 282 | "reindention": { 283 | "oneOf": [ 284 | { 285 | "properties": { 286 | "regex_pattern": { 287 | "type": "string" 288 | }, 289 | "indention": { 290 | "type": "number" 291 | }, 292 | "comments_only": { 293 | "type": "boolean" 294 | } 295 | }, 296 | "additionalProperties": false, 297 | "type": "object" 298 | }, 299 | { "type": "string" } 300 | ] 301 | } 302 | }, 303 | "additionalProperties": false, 304 | "type": "object" 305 | }, 306 | "astyle": { 307 | "properties": { 308 | "args": { "type": "array", "items": { "type": "string" } } 309 | }, 310 | "additionalProperties": false, 311 | "type": "object" 312 | }, 313 | "formatOnSave": { 314 | "additionalProperties": false, 315 | "type": "boolean" 316 | }, 317 | "cacheFormatters": { 318 | "type": "boolean" 319 | }, 320 | "suppressFormatterErrors": { 321 | "additionalProperties": false, 322 | "type": "boolean" 323 | }, 324 | "suppressFormatterErrorsIFFAutoFormatOnSave": { 325 | "additionalProperties": false, 326 | "type": "boolean" 327 | } 328 | }, 329 | "properties": { 330 | "preferences": { 331 | "title": "Code Formatter Preferences", 332 | "description": "Preferences for this plugin", 333 | "$ref": "#/definitions/preferences", 334 | "default": { 335 | "default_formatter": { 336 | "python": ["isort", "black"], 337 | "R": "formatR", 338 | "rust": "rustfmt", 339 | "c++11": "astyle" 340 | } 341 | } 342 | }, 343 | "black": { 344 | "title": "Black Config", 345 | "description": "Config to be passed into black's format_str function call.", 346 | "$ref": "#/definitions/black", 347 | "default": { 348 | "line_length": 88, 349 | "string_normalization": true 350 | } 351 | }, 352 | "yapf": { 353 | "title": "YAPF Config", 354 | "description": "Config to be passed into yapf's FormatCode function call.", 355 | "$ref": "#/definitions/yapf", 356 | "default": { 357 | "style_config": "google" 358 | } 359 | }, 360 | "autopep8": { 361 | "title": "Autopep8 Config", 362 | "description": "Config to be passed into autopep8's fix_code function call as the options dictionary.", 363 | "$ref": "#/definitions/autopep8", 364 | "default": {} 365 | }, 366 | "isort": { 367 | "title": "Isort Config", 368 | "description": "Config to be passed into isort's SortImports function call.", 369 | "$ref": "#/definitions/isort", 370 | "default": { 371 | "multi_line_output": 3, 372 | "include_trailing_comma": true, 373 | "force_grid_wrap": 0, 374 | "use_parentheses": true, 375 | "ensure_newline_before_comments": true, 376 | "line_length": 88 377 | } 378 | }, 379 | "formatR": { 380 | "title": "FormatR Config", 381 | "description": "Config to be passed into formatR's tidy_source function call.", 382 | "$ref": "#/definitions/formatR", 383 | "default": { 384 | "indent": 2, 385 | "arrow": true, 386 | "wrap": true, 387 | "width_cutoff": 150 388 | } 389 | }, 390 | "styler": { 391 | "title": "Styler Config", 392 | "description": "Config to be passed into styler's style_text function call.", 393 | "$ref": "#/definitions/styler", 394 | "default": {} 395 | }, 396 | "formatOnSave": { 397 | "title": "Auto format config", 398 | "description": "Auto format code when save the notebook.", 399 | "$ref": "#/definitions/formatOnSave", 400 | "default": false 401 | }, 402 | "cacheFormatters": { 403 | "title": "Cache formatters", 404 | "description": "Cache formatters on server for better performance (but will not detected newly installed/uninstalled formatters).", 405 | "$ref": "#/definitions/cacheFormatters", 406 | "default": false 407 | }, 408 | "astyle": { 409 | "title": "AStyle Config", 410 | "description": "Command line options to be passed to astyle.", 411 | "$ref": "#/definitions/astyle", 412 | "default": { 413 | "args": [] 414 | } 415 | }, 416 | "ruff": { 417 | "title": "Ruff Check Config", 418 | "description": "Command line options to be passed to ruff check. Default is to organise imports.", 419 | "$ref": "#/definitions/ruff", 420 | "default": { 421 | "args": ["--select=I001"] 422 | } 423 | }, 424 | "ruffformat": { 425 | "title": "Ruff Format Config", 426 | "description": "Command line options to be passed to ruff format.", 427 | "$ref": "#/definitions/ruffformat", 428 | "default": { 429 | "args": [] 430 | } 431 | }, 432 | "suppressFormatterErrors": { 433 | "title": "Suppress formatter errors", 434 | "description": "Whether to suppress all errors reported by formatter while formatting. Useful when you have format on save mode on.", 435 | "$ref": "#/definitions/suppressFormatterErrors", 436 | "default": false 437 | }, 438 | "suppressFormatterErrorsIFFAutoFormatOnSave": { 439 | "title": "Suppress formatter errors if and only if auto saving.", 440 | "description": "Whether to suppress all errors reported by formatter while formatting (if and only if auto saving). Useful when you have format on save mode on and still want to see error when manually formatting.", 441 | "$ref": "#/definitions/suppressFormatterErrorsIFFAutoFormatOnSave", 442 | "default": false 443 | } 444 | }, 445 | "additionalProperties": false, 446 | "type": "object" 447 | } 448 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | rm -rf dist/* 6 | rm -rf /plugin/jupyterlab_code_formatter/labextension 7 | hatch build 8 | -------------------------------------------------------------------------------- /scripts/format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | find /plugin/jupyterlab_code_formatter -name '*.py' | xargs isort --profile black 6 | find /plugin/jupyterlab_code_formatter -name '*.py' | xargs black --line-length 110 7 | yarn run prettier 8 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | pytest /plugin 6 | jupyter server extension list 7 | jupyter server extension list 2>&1 | grep -ie "jupyterlab_code_formatter.*OK" 8 | jupyter labextension list 9 | jupyter labextension list 2>&1 | grep -ie "jupyterlab_code_formatter.*OK" 10 | python -m jupyterlab.browser_check --allow-root 11 | 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | __import__('setuptools').setup() 2 | -------------------------------------------------------------------------------- /src/__tests__/jupyterlab_code_formatter.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests 3 | */ 4 | 5 | describe('jupyterlab_code_formatter', () => { 6 | it('should be tested', () => { 7 | expect(1 + 1).toEqual(2); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import { URLExt } from '@jupyterlab/coreutils'; 2 | import { ServerConnection } from '@jupyterlab/services'; 3 | import { Constants } from './constants'; 4 | 5 | class JupyterlabCodeFormatterClient { 6 | public request(path: string, method: string, body: any): Promise { 7 | const settings = ServerConnection.makeSettings(); 8 | const fullUrl = URLExt.join(settings.baseUrl, Constants.PLUGIN_NAME, path); 9 | return ServerConnection.makeRequest( 10 | fullUrl, 11 | { 12 | body, 13 | method 14 | }, 15 | settings 16 | ).then(response => { 17 | if (response.status !== 200) { 18 | return response.text().then(() => { 19 | throw new ServerConnection.ResponseError( 20 | response, 21 | response.statusText 22 | ); 23 | }); 24 | } 25 | return response.text(); 26 | }); 27 | } 28 | 29 | public getAvailableFormatters(cache: boolean) { 30 | return this.request('formatters' + (cache ? '?cached' : ''), 'GET', null); 31 | } 32 | } 33 | 34 | export default JupyterlabCodeFormatterClient; 35 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export namespace Constants { 2 | export const PLUGIN_NAME = 'jupyterlab_code_formatter'; 3 | export const FORMAT_COMMAND = `${PLUGIN_NAME}:format`; 4 | export const FORMAT_ALL_COMMAND = `${PLUGIN_NAME}:format_all`; 5 | // TODO: Extract this to style and import svg as string 6 | export const ICON_FORMAT_ALL_SVG = 7 | ''; 8 | export const ICON_FORMAT_ALL = 'fa fa-superpowers'; 9 | export const SETTINGS_SECTION = `${PLUGIN_NAME}:settings`; 10 | export const COMMAND_SECTION_NAME = 'Jupyterlab Code Formatter'; 11 | // TODO: Use package.json info 12 | export const PLUGIN_VERSION = '1.6.1'; 13 | } 14 | -------------------------------------------------------------------------------- /src/formatter.ts: -------------------------------------------------------------------------------- 1 | import { Cell, CodeCell } from '@jupyterlab/cells'; 2 | import { INotebookTracker, Notebook } from '@jupyterlab/notebook'; 3 | import JupyterlabCodeFormatterClient from './client'; 4 | import { IEditorTracker } from '@jupyterlab/fileeditor'; 5 | import { Widget } from '@lumino/widgets'; 6 | import { showErrorMessage, Dialog, showDialog } from '@jupyterlab/apputils'; 7 | 8 | type Context = { 9 | saving: boolean; 10 | }; 11 | 12 | class JupyterlabCodeFormatter { 13 | working = false; 14 | protected client: JupyterlabCodeFormatterClient; 15 | constructor(client: JupyterlabCodeFormatterClient) { 16 | this.client = client; 17 | } 18 | 19 | protected formatCode( 20 | code: string[], 21 | formatter: string, 22 | options: any, 23 | notebook: boolean, 24 | cache: boolean 25 | ) { 26 | return this.client 27 | .request( 28 | 'format' + (cache ? '?cached' : ''), 29 | 'POST', 30 | JSON.stringify({ 31 | code, 32 | notebook, 33 | formatter, 34 | options 35 | }) 36 | ) 37 | .then(resp => JSON.parse(resp)); 38 | } 39 | } 40 | 41 | export class JupyterlabNotebookCodeFormatter extends JupyterlabCodeFormatter { 42 | protected notebookTracker: INotebookTracker; 43 | 44 | constructor( 45 | client: JupyterlabCodeFormatterClient, 46 | notebookTracker: INotebookTracker 47 | ) { 48 | super(client); 49 | this.notebookTracker = notebookTracker; 50 | } 51 | 52 | public async formatAction(config: any, formatter?: string) { 53 | return this.formatCells(true, config, { saving: false }, formatter); 54 | } 55 | 56 | public async formatSelectedCodeCells( 57 | config: any, 58 | formatter?: string, 59 | notebook?: Notebook 60 | ) { 61 | return this.formatCells( 62 | true, 63 | config, 64 | { saving: false }, 65 | formatter, 66 | notebook 67 | ); 68 | } 69 | 70 | public async formatAllCodeCells( 71 | config: any, 72 | context: Context, 73 | formatter?: string, 74 | notebook?: Notebook 75 | ) { 76 | return this.formatCells(false, config, context, formatter, notebook); 77 | } 78 | 79 | private getCodeCells(selectedOnly = true, notebook?: Notebook): CodeCell[] { 80 | if (!this.notebookTracker.currentWidget) { 81 | return []; 82 | } 83 | const codeCells: CodeCell[] = []; 84 | notebook = notebook || this.notebookTracker.currentWidget.content; 85 | notebook.widgets.forEach((cell: Cell) => { 86 | if (cell.model.type === 'code') { 87 | if (!selectedOnly || (notebook).isSelectedOrActive(cell)) { 88 | codeCells.push(cell as CodeCell); 89 | } 90 | } 91 | }); 92 | return codeCells; 93 | } 94 | 95 | private getNotebookType(): string | null { 96 | // If there is no current notebook, there is nothing to do 97 | if (!this.notebookTracker.currentWidget) { 98 | return null; 99 | } 100 | 101 | // first, check the notebook's metadata for language info 102 | const metadata = 103 | this.notebookTracker.currentWidget.content.model?.sharedModel?.metadata; 104 | 105 | if (metadata) { 106 | // prefer kernelspec language 107 | if ( 108 | metadata.kernelspec && 109 | metadata.kernelspec.language && 110 | typeof metadata.kernelspec.language === 'string' 111 | ) { 112 | return metadata.kernelspec.language.toLowerCase(); 113 | } 114 | 115 | // otherwise, check language info code mirror mode 116 | if (metadata.language_info && metadata.language_info.codemirror_mode) { 117 | const mode = metadata.language_info.codemirror_mode; 118 | if (typeof mode === 'string') { 119 | return mode.toLowerCase(); 120 | } else if (typeof mode.name === 'string') { 121 | return mode.name.toLowerCase(); 122 | } 123 | } 124 | } 125 | 126 | // in the absence of metadata, look in the current session's kernel spec 127 | const sessionContext = this.notebookTracker.currentWidget.sessionContext; 128 | const kernelName = sessionContext?.session?.kernel?.name; 129 | if (kernelName) { 130 | const specs = sessionContext.specsManager.specs?.kernelspecs; 131 | if (specs && kernelName in specs) { 132 | return specs[kernelName]!.language; 133 | } 134 | } 135 | 136 | return null; 137 | } 138 | 139 | private getDefaultFormatters(config: any): Array { 140 | const notebookType = this.getNotebookType(); 141 | if (notebookType) { 142 | const defaultFormatter = 143 | config.preferences.default_formatter[notebookType]; 144 | if (defaultFormatter instanceof Array) { 145 | return defaultFormatter; 146 | } else if (defaultFormatter !== undefined) { 147 | return [defaultFormatter]; 148 | } 149 | } 150 | return []; 151 | } 152 | 153 | private async getFormattersToUse(config: any, formatter?: string) { 154 | const defaultFormatters = this.getDefaultFormatters(config); 155 | const formattersToUse = 156 | formatter !== undefined ? [formatter] : defaultFormatters; 157 | 158 | if (formattersToUse.length === 0) { 159 | await showErrorMessage( 160 | 'Jupyterlab Code Formatter Error', 161 | 'Unable to find default formatters to use, please file an issue on GitHub.' 162 | ); 163 | } 164 | 165 | return formattersToUse; 166 | } 167 | 168 | private async applyFormatters( 169 | selectedCells: CodeCell[], 170 | formattersToUse: string[], 171 | config: any, 172 | context: Context 173 | ) { 174 | for (const formatterToUse of formattersToUse) { 175 | if (formatterToUse === 'noop' || formatterToUse === 'skip') { 176 | continue; 177 | } 178 | const currentTexts = selectedCells.map( 179 | cell => cell.model.sharedModel.source 180 | ); 181 | const formattedTexts = await this.formatCode( 182 | currentTexts, 183 | formatterToUse, 184 | config[formatterToUse], 185 | true, 186 | config.cacheFormatters 187 | ); 188 | console.log( 189 | config.suppressFormatterErrorsIFFAutoFormatOnSave, 190 | context.saving 191 | ); 192 | 193 | const showErrors = 194 | !(config.suppressFormatterErrors ?? false) && 195 | !( 196 | (config.suppressFormatterErrorsIFFAutoFormatOnSave ?? false) && 197 | context.saving 198 | ); 199 | for (let i = 0; i < selectedCells.length; ++i) { 200 | const cell = selectedCells[i]; 201 | const currentText = currentTexts[i]; 202 | const formattedText = formattedTexts.code[i]; 203 | const cellValueHasNotChanged = 204 | cell.model.sharedModel.source === currentText; 205 | if (cellValueHasNotChanged) { 206 | if (formattedText.error) { 207 | if (showErrors) { 208 | const result = await showDialog({ 209 | title: 'Jupyterlab Code Formatter Error', 210 | body: formattedText.error, 211 | buttons: [ 212 | Dialog.createButton({ 213 | label: 'Go to cell', 214 | actions: ['revealError'] 215 | }), 216 | Dialog.okButton({ label: 'Dismiss' }) 217 | ] 218 | }); 219 | if (result.button.actions.indexOf('revealError') !== -1) { 220 | this.notebookTracker.currentWidget!.content.scrollToCell(cell); 221 | break; 222 | } 223 | } 224 | } else { 225 | cell.model.sharedModel.source = formattedText.code; 226 | } 227 | } else { 228 | if (showErrors) { 229 | await showErrorMessage( 230 | 'Jupyterlab Code Formatter Error', 231 | `Cell value changed since format request was sent, formatting for cell ${i} skipped.` 232 | ); 233 | } 234 | } 235 | } 236 | } 237 | } 238 | 239 | private async formatCells( 240 | selectedOnly: boolean, 241 | config: any, 242 | context: Context, 243 | formatter?: string, 244 | notebook?: Notebook 245 | ) { 246 | if (this.working) { 247 | return; 248 | } 249 | try { 250 | this.working = true; 251 | const selectedCells = this.getCodeCells(selectedOnly, notebook); 252 | if (selectedCells.length === 0) { 253 | this.working = false; 254 | return; 255 | } 256 | 257 | const formattersToUse = await this.getFormattersToUse(config, formatter); 258 | await this.applyFormatters( 259 | selectedCells, 260 | formattersToUse, 261 | config, 262 | context 263 | ); 264 | } catch (error) { 265 | await showErrorMessage('Jupyterlab Code Formatter Error', `${error}`); 266 | } 267 | this.working = false; 268 | } 269 | 270 | applicable(formatter: string, currentWidget: Widget) { 271 | const currentNotebookWidget = this.notebookTracker.currentWidget; 272 | // TODO: Handle showing just the correct formatter for the language later 273 | return currentNotebookWidget && currentWidget === currentNotebookWidget; 274 | } 275 | } 276 | 277 | export class JupyterlabFileEditorCodeFormatter extends JupyterlabCodeFormatter { 278 | protected editorTracker: IEditorTracker; 279 | 280 | constructor( 281 | client: JupyterlabCodeFormatterClient, 282 | editorTracker: IEditorTracker 283 | ) { 284 | super(client); 285 | this.editorTracker = editorTracker; 286 | } 287 | 288 | formatAction(config: any, formatter: string) { 289 | return this.formatEditor(config, { saving: false }, formatter); 290 | } 291 | 292 | public async formatEditor(config: any, context: Context, formatter?: string) { 293 | if (this.working) { 294 | return; 295 | } 296 | try { 297 | this.working = true; 298 | 299 | const formattersToUse = await this.getFormattersToUse(config, formatter); 300 | await this.applyFormatters(formattersToUse, config, context); 301 | } catch (error) { 302 | const msg = error instanceof Error ? error : `${error}`; 303 | await showErrorMessage('Jupyterlab Code Formatter Error', msg); 304 | } 305 | this.working = false; 306 | } 307 | 308 | private getEditorType() { 309 | if (!this.editorTracker.currentWidget) { 310 | return null; 311 | } 312 | 313 | const mimeType = this.editorTracker.currentWidget.content.model!.mimeType; 314 | 315 | const mimeTypes = new Map([ 316 | ['text/x-python', 'python'], 317 | ['application/x-rsrc', 'r'], 318 | ['application/x-scala', 'scala'], 319 | ['application/x-rustsrc', 'rust'], 320 | ['application/x-c++src', 'cpp'] // Not sure that this is right, whatever. 321 | // Add more MIME types and corresponding programming languages here 322 | ]); 323 | 324 | return mimeTypes.get(mimeType); 325 | } 326 | 327 | private getDefaultFormatters(config: any): Array { 328 | const editorType = this.getEditorType(); 329 | if (editorType) { 330 | const defaultFormatter = config.preferences.default_formatter[editorType]; 331 | if (defaultFormatter instanceof Array) { 332 | return defaultFormatter; 333 | } else if (defaultFormatter !== undefined) { 334 | return [defaultFormatter]; 335 | } 336 | } 337 | return []; 338 | } 339 | 340 | private async getFormattersToUse(config: any, formatter?: string) { 341 | const defaultFormatters = this.getDefaultFormatters(config); 342 | const formattersToUse = 343 | formatter !== undefined ? [formatter] : defaultFormatters; 344 | 345 | if (formattersToUse.length === 0) { 346 | await showErrorMessage( 347 | 'Jupyterlab Code Formatter Error', 348 | 'Unable to find default formatters to use, please file an issue on GitHub.' 349 | ); 350 | } 351 | 352 | return formattersToUse; 353 | } 354 | 355 | private async applyFormatters( 356 | formattersToUse: string[], 357 | config: any, 358 | context: Context 359 | ) { 360 | for (const formatterToUse of formattersToUse) { 361 | if (formatterToUse === 'noop' || formatterToUse === 'skip') { 362 | continue; 363 | } 364 | const showErrors = 365 | !(config.suppressFormatterErrors ?? false) && 366 | !( 367 | (config.suppressFormatterErrorsIFFAutoFormatOnSave ?? false) && 368 | context.saving 369 | ); 370 | 371 | const editorWidget = this.editorTracker.currentWidget; 372 | this.working = true; 373 | const editor = editorWidget!.content.editor; 374 | const code = editor.model.sharedModel.source; 375 | this.formatCode( 376 | [code], 377 | formatterToUse, 378 | config[formatterToUse], 379 | false, 380 | config.cacheFormatters 381 | ) 382 | .then(data => { 383 | if (data.code[0].error) { 384 | if (showErrors) { 385 | void showErrorMessage( 386 | 'Jupyterlab Code Formatter Error', 387 | data.code[0].error 388 | ); 389 | } 390 | this.working = false; 391 | return; 392 | } 393 | this.editorTracker.currentWidget!.content.editor.model.sharedModel.source = 394 | data.code[0].code; 395 | this.working = false; 396 | }) 397 | .catch(error => { 398 | const msg = error instanceof Error ? error : `${error}`; 399 | void showErrorMessage('Jupyterlab Code Formatter Error', msg); 400 | }); 401 | } 402 | } 403 | 404 | applicable(formatter: string, currentWidget: Widget) { 405 | const currentEditorWidget = this.editorTracker.currentWidget; 406 | // TODO: Handle showing just the correct formatter for the language later 407 | return currentEditorWidget && currentWidget === currentEditorWidget; 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentRegistry, 3 | DocumentWidget, 4 | DocumentModel 5 | } from '@jupyterlab/docregistry'; 6 | import { 7 | INotebookModel, 8 | INotebookTracker, 9 | NotebookPanel 10 | } from '@jupyterlab/notebook'; 11 | import { 12 | JupyterFrontEnd, 13 | JupyterFrontEndPlugin 14 | } from '@jupyterlab/application'; 15 | import { ICommandPalette, ToolbarButton } from '@jupyterlab/apputils'; 16 | import { ISettingRegistry } from '@jupyterlab/settingregistry'; 17 | import { IMainMenu } from '@jupyterlab/mainmenu'; 18 | import { IEditorTracker } from '@jupyterlab/fileeditor'; 19 | import JupyterlabCodeFormatterClient from './client'; 20 | import { 21 | JupyterlabFileEditorCodeFormatter, 22 | JupyterlabNotebookCodeFormatter 23 | } from './formatter'; 24 | import { DisposableDelegate, IDisposable } from '@lumino/disposable'; 25 | import { Constants } from './constants'; 26 | import { LabIcon } from '@jupyterlab/ui-components'; 27 | import { Widget } from '@lumino/widgets'; 28 | 29 | class JupyterLabCodeFormatter 30 | implements DocumentRegistry.IWidgetExtension 31 | { 32 | private app: JupyterFrontEnd; 33 | private readonly tracker: INotebookTracker; 34 | private palette: ICommandPalette; 35 | private settingRegistry: ISettingRegistry; 36 | private menu: IMainMenu; 37 | private config: any; 38 | private readonly editorTracker: IEditorTracker; 39 | private readonly client: JupyterlabCodeFormatterClient; 40 | private readonly notebookCodeFormatter: JupyterlabNotebookCodeFormatter; 41 | private readonly fileEditorCodeFormatter: JupyterlabFileEditorCodeFormatter; 42 | 43 | constructor( 44 | app: JupyterFrontEnd, 45 | tracker: INotebookTracker, 46 | palette: ICommandPalette, 47 | settingRegistry: ISettingRegistry, 48 | menu: IMainMenu, 49 | editorTracker: IEditorTracker 50 | ) { 51 | this.app = app; 52 | this.tracker = tracker; 53 | this.editorTracker = editorTracker; 54 | this.palette = palette; 55 | this.settingRegistry = settingRegistry; 56 | this.menu = menu; 57 | this.client = new JupyterlabCodeFormatterClient(); 58 | this.notebookCodeFormatter = new JupyterlabNotebookCodeFormatter( 59 | this.client, 60 | this.tracker 61 | ); 62 | this.fileEditorCodeFormatter = new JupyterlabFileEditorCodeFormatter( 63 | this.client, 64 | this.editorTracker 65 | ); 66 | 67 | this.setupSettings().then(() => { 68 | this.setupAllCommands(); 69 | this.setupContextMenu(); 70 | this.setupWidgetExtension(); 71 | }); 72 | } 73 | 74 | public createNew( 75 | nb: NotebookPanel, 76 | context: DocumentRegistry.IContext 77 | ): IDisposable { 78 | const button = new ToolbarButton({ 79 | tooltip: 'Format notebook', 80 | icon: new LabIcon({ 81 | name: Constants.FORMAT_ALL_COMMAND, 82 | svgstr: Constants.ICON_FORMAT_ALL_SVG 83 | }), 84 | onClick: async () => { 85 | await this.notebookCodeFormatter.formatAllCodeCells( 86 | this.config, 87 | { saving: false }, 88 | undefined, 89 | nb.content 90 | ); 91 | } 92 | }); 93 | nb.toolbar.insertAfter( 94 | 'cellType', 95 | this.app.commands.label(Constants.FORMAT_ALL_COMMAND), 96 | button 97 | ); 98 | 99 | context.saveState.connect(this.onSave, this); 100 | 101 | return new DisposableDelegate(() => { 102 | button.dispose(); 103 | }); 104 | } 105 | 106 | private async onSave( 107 | context: DocumentRegistry.IContext, 108 | state: DocumentRegistry.SaveState 109 | ) { 110 | if (state === 'started' && this.config.formatOnSave) { 111 | await context.sessionContext.ready; 112 | await this.notebookCodeFormatter.formatAllCodeCells( 113 | this.config, 114 | { saving: true }, 115 | undefined, 116 | undefined 117 | ); 118 | } 119 | } 120 | 121 | private createNewEditor( 122 | widget: DocumentWidget, 123 | context: DocumentRegistry.IContext 124 | ): IDisposable { 125 | // Connect to save(State) signal, to be able to detect document save event 126 | context.saveState.connect(this.onSaveEditor, this); 127 | // Return an empty disposable, because we don't create any object 128 | return new DisposableDelegate(() => {}); 129 | } 130 | 131 | private async onSaveEditor( 132 | context: DocumentRegistry.IContext, 133 | state: DocumentRegistry.SaveState 134 | ) { 135 | if (state === 'started' && this.config.formatOnSave) { 136 | this.fileEditorCodeFormatter.formatEditor( 137 | this.config, 138 | { saving: true }, 139 | undefined 140 | ); 141 | } 142 | } 143 | 144 | private setupWidgetExtension() { 145 | this.app.docRegistry.addWidgetExtension('Notebook', this); 146 | this.app.docRegistry.addWidgetExtension('editor', { 147 | createNew: ( 148 | widget: DocumentWidget, 149 | context: DocumentRegistry.IContext 150 | ): IDisposable => { 151 | return this.createNewEditor(widget, context); 152 | } 153 | }); 154 | } 155 | 156 | private setupContextMenu() { 157 | this.app.contextMenu.addItem({ 158 | command: Constants.FORMAT_COMMAND, 159 | selector: '.jp-Notebook' 160 | }); 161 | } 162 | 163 | private setupAllCommands() { 164 | this.client 165 | .getAvailableFormatters(this.config.cacheFormatters) 166 | .then(data => { 167 | const formatters = JSON.parse(data).formatters; 168 | const menuGroup: Array<{ command: string }> = []; 169 | Object.keys(formatters).forEach(formatter => { 170 | if (formatters[formatter].enabled) { 171 | const command = `${Constants.PLUGIN_NAME}:${formatter}`; 172 | this.setupCommand(formatter, formatters[formatter].label, command); 173 | menuGroup.push({ command }); 174 | } 175 | }); 176 | this.menu.editMenu.addGroup(menuGroup); 177 | }); 178 | 179 | this.app.commands.addCommand(Constants.FORMAT_COMMAND, { 180 | execute: async () => { 181 | await this.notebookCodeFormatter.formatSelectedCodeCells(this.config); 182 | }, 183 | // TODO: Add back isVisible 184 | label: 'Format cell' 185 | }); 186 | this.app.commands.addCommand(Constants.FORMAT_ALL_COMMAND, { 187 | execute: async () => { 188 | await this.notebookCodeFormatter.formatAllCodeCells(this.config, { 189 | saving: false 190 | }); 191 | }, 192 | iconClass: Constants.ICON_FORMAT_ALL, 193 | iconLabel: 'Format notebook' 194 | // TODO: Add back isVisible 195 | }); 196 | } 197 | 198 | private async setupSettings() { 199 | const settings = await this.settingRegistry.load( 200 | Constants.SETTINGS_SECTION 201 | ); 202 | const onSettingsUpdated = (jsettings: ISettingRegistry.ISettings) => { 203 | this.config = jsettings.composite; 204 | }; 205 | settings.changed.connect(onSettingsUpdated); 206 | onSettingsUpdated(settings); 207 | } 208 | 209 | private setupCommand(name: string, label: string, command: string) { 210 | this.app.commands.addCommand(command, { 211 | execute: async () => { 212 | for (const formatter of [ 213 | this.notebookCodeFormatter, 214 | this.fileEditorCodeFormatter 215 | ]) { 216 | if ( 217 | formatter.applicable(name, this.app.shell.currentWidget) 218 | ) { 219 | await formatter.formatAction(this.config, name); 220 | } 221 | } 222 | }, 223 | isVisible: () => { 224 | for (const formatter of [ 225 | this.notebookCodeFormatter, 226 | this.fileEditorCodeFormatter 227 | ]) { 228 | if ( 229 | formatter.applicable(name, this.app.shell.currentWidget) 230 | ) { 231 | return true; 232 | } 233 | } 234 | return false; 235 | }, 236 | label 237 | }); 238 | this.palette.addItem({ command, category: Constants.COMMAND_SECTION_NAME }); 239 | } 240 | } 241 | 242 | /** 243 | * Initialization data for the jupyterlab_code_formatter extension. 244 | */ 245 | const plugin: JupyterFrontEndPlugin = { 246 | id: Constants.PLUGIN_NAME, 247 | autoStart: true, 248 | requires: [ 249 | ICommandPalette, 250 | INotebookTracker, 251 | ISettingRegistry, 252 | IMainMenu, 253 | IEditorTracker 254 | ], 255 | activate: ( 256 | app: JupyterFrontEnd, 257 | palette: ICommandPalette, 258 | tracker: INotebookTracker, 259 | settingRegistry: ISettingRegistry, 260 | menu: IMainMenu, 261 | editorTracker: IEditorTracker 262 | ) => { 263 | new JupyterLabCodeFormatter( 264 | app, 265 | tracker, 266 | palette, 267 | settingRegistry, 268 | menu, 269 | editorTracker 270 | ); 271 | console.log('JupyterLab extension jupyterlab_code_formatter is activated!'); 272 | } 273 | }; 274 | 275 | export default plugin; 276 | -------------------------------------------------------------------------------- /style/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | See the JupyterLab Developer Guide for useful CSS Patterns: 3 | 4 | https://jupyterlab.readthedocs.io/en/stable/developer/css.html 5 | */ 6 | -------------------------------------------------------------------------------- /style/index.css: -------------------------------------------------------------------------------- 1 | @import url('base.css'); 2 | -------------------------------------------------------------------------------- /style/index.js: -------------------------------------------------------------------------------- 1 | import './base.css'; 2 | -------------------------------------------------------------------------------- /test_snippets/some_python.py: -------------------------------------------------------------------------------- 1 | # this doesn't make sense as all lul 2 | import requests # TROLOLOLOL 3 | 4 | headers = { 5 | 'Referer': 'https://www.transtats.bts.gov/DL_SelectFields.asp?Table_ID=236&DB_Short_Name=On-Time', 6 | 'Origin': 'https://www.transtats.bts.gov', 7 | 'Content-Type': 'application/x-www-form-urlencoded', 8 | } 9 | 10 | params = ( 11 | ('Table_ID', '236'), 12 | ('Has_Group', '3'), ('Is_Zipped', '0'), 13 | ) 14 | 15 | with open('modern-1-url.txt', encoding='utf-8') as f: 16 | data = f.read().strip() 17 | 18 | os.makedirs('data', exist_ok=True) 19 | 20 | 21 | import pandas as pd 22 | 23 | 24 | 25 | 26 | 27 | 28 | def read(fp): 29 | df = (pd.read_csv(fp) 30 | .rename(columns=str.lower) .drop('unnamed: 36', axis=1) .pipe(extract_city_name) .pipe(time_to_datetime, ['dep_time', 'arr_time', 'crs_arr_time', 'crs_dep_time']) 31 | .assign(fl_date=lambda x: pd.to_datetime(x['fl_date']), 32 | dest=lambda x: pd.Categorical(x['dest']), 33 | origin=lambda x: pd.Categorical(x['origin']), tail_num=lambda x: pd.Categorical(x['tail_num']), unique_carrier=lambda x: pd.Categorical(x['unique_carrier']), 34 | cancellation_code=lambda x: pd.Categorical(x['cancellation_code']))) 35 | return df 36 | 37 | 38 | def extract_city_name(df:pd.DataFrame) -> pd.DataFrame: 39 | ''' 40 | Chicago, IL -> Chicago for origin_city_name and dest_city_name 41 | ''' 42 | cols = ['origin_city_name', 'dest_city_name'] 43 | city = df[cols].apply(lambda x: x.str.extract("(.*), \w{2}", expand=False)) 44 | df = df.copy() 45 | df[['origin_city_name', 'dest_city_name']] = city 46 | return df 47 | -------------------------------------------------------------------------------- /test_snippets/test_notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# this doesn't make sense as all lul\n", 10 | "import requests # TROLOLOLOL\n", 11 | "import abc\n", 12 | "\n", 13 | "headers = {\n", 14 | " 'Referer': 'https://www.transtats.bts.gov/DL_SelectFields.asp?Table_ID=236&DB_Short_Name=On-Time',\n", 15 | " 'Origin': 'https://www.transtats.bts.gov',\n", 16 | " 'Content-Type': 'application/x-www-form-urlencoded',\n", 17 | "}\n", 18 | "\n", 19 | "params = (\n", 20 | " ('Table_ID', '236'),\n", 21 | " ('Has_Group', '3'), ('Is_Zipped', '0'),\n", 22 | ")\n", 23 | "\n", 24 | "with open('modern-1-url.txt', encoding='utf-8') as f:\n", 25 | " data = f.read().strip()\n", 26 | "\n", 27 | "os.makedirs('data', exist_ok=True)\n", 28 | "\n", 29 | "\n", 30 | "import pandas as pd\n", 31 | "\n", 32 | "\n", 33 | "\n", 34 | "\n", 35 | "\n", 36 | "\n", 37 | "def read(fp):\n", 38 | " df = (pd.read_csv(fp)\n", 39 | " .rename(columns=str.lower) .drop('unnamed: 36', axis=1) .pipe(extract_city_name) .pipe(time_to_datetime, ['dep_time', 'arr_time', 'crs_arr_time', 'crs_dep_time'])\n", 40 | " .assign(fl_date=lambda x: pd.to_datetime(x['fl_date']),\n", 41 | " dest=lambda x: pd.Categorical(x['dest']),\n", 42 | " origin=lambda x: pd.Categorical(x['origin']), tail_num=lambda x: pd.Categorical(x['tail_num']), unique_carrier=lambda x: pd.Categorical(x['unique_carrier']),\n", 43 | " cancellation_code=lambda x: pd.Categorical(x['cancellation_code'])))\n", 44 | " return df\n", 45 | "\n", 46 | "\n", 47 | "def extract_city_name(df:pd.DataFrame) -> pd.DataFrame:\n", 48 | " '''\n", 49 | " Chicago, IL -> Chicago for origin_city_name and dest_city_name\n", 50 | " '''\n", 51 | " cols = ['origin_city_name', 'dest_city_name']\n", 52 | " city = df[cols].apply(lambda x: x.str.extract(\"(.*), \\w{2}\", expand=False))\n", 53 | " df = df.copy()\n", 54 | " df[['origin_city_name', 'dest_city_name']] = city\n", 55 | " return df\n" 56 | ] 57 | } 58 | ], 59 | "metadata": { 60 | "kernelspec": { 61 | "display_name": "Python 3 (ipykernel)", 62 | "language": "python", 63 | "name": "python3" 64 | }, 65 | "language_info": { 66 | "codemirror_mode": { 67 | "name": "ipython", 68 | "version": 3 69 | }, 70 | "file_extension": ".py", 71 | "mimetype": "text/x-python", 72 | "name": "python", 73 | "nbconvert_exporter": "python", 74 | "pygments_lexer": "ipython3", 75 | "version": "3.10.8" 76 | } 77 | }, 78 | "nbformat": 4, 79 | "nbformat_minor": 4 80 | } 81 | -------------------------------------------------------------------------------- /test_snippets/test_notebook_crablang.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "3637bfb4-a089-40dd-82ba-9fa9f3d323e7", 7 | "metadata": { 8 | "tags": [] 9 | }, 10 | "outputs": [], 11 | "source": [ 12 | "// function to add two numbers\n", 13 | "fn add() {\n", 14 | " let a = 5;\n", 15 | " let b = 10;\n", 16 | "\n", 17 | " let sum = a + b;\n", 18 | "\n", 19 | " println!(\"Sum of a and b = {}\", \n", 20 | " sum);\n", 21 | "}\n", 22 | "\n", 23 | "fn main() {\n", 24 | " // function call\n", 25 | " add();\n", 26 | "}" 27 | ] 28 | } 29 | ], 30 | "metadata": { 31 | "kernelspec": { 32 | "display_name": "Rust", 33 | "language": "rust", 34 | "name": "rust" 35 | }, 36 | "language_info": { 37 | "codemirror_mode": "rust", 38 | "file_extension": ".rs", 39 | "mimetype": "text/rust", 40 | "name": "Rust", 41 | "pygment_lexer": "rust", 42 | "version": "" 43 | } 44 | }, 45 | "nbformat": 4, 46 | "nbformat_minor": 5 47 | } 48 | -------------------------------------------------------------------------------- /test_snippets/test_with_errors.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "95cc4d6e-9fa2-493c-af98-83b1b88df0c6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "ddd =" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "id": "4e6159ec-e287-4a22-b76e-9fc5f012a6d4", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "hello = 42" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "id": "41a699a0-0480-4d36-ae81-d06a6cc074b4", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "fff =" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "id": "804c67fe-6c33-4590-9616-57db0497b1eb", 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "id": "411369a5-160e-472f-befa-25c82b836a61", 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "hello = 42" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "id": "2c20f1d6-7b8b-4083-819d-d0daef945d23", 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "fff2 =" 59 | ] 60 | } 61 | ], 62 | "metadata": { 63 | "kernelspec": { 64 | "display_name": "Python 3 (ipykernel)", 65 | "language": "python", 66 | "name": "python3" 67 | }, 68 | "language_info": { 69 | "codemirror_mode": { 70 | "name": "ipython", 71 | "version": 3 72 | }, 73 | "file_extension": ".py", 74 | "mimetype": "text/x-python", 75 | "name": "python", 76 | "nbconvert_exporter": "python", 77 | "pygments_lexer": "ipython3", 78 | "version": "3.10.11" 79 | } 80 | }, 81 | "nbformat": 4, 82 | "nbformat_minor": 5 83 | } 84 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "incremental": true, 8 | "jsx": "react", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "preserveWatchOutput": true, 15 | "resolveJsonModule": true, 16 | "outDir": "lib", 17 | "rootDir": "src", 18 | "strict": true, 19 | "strictNullChecks": true, 20 | "target": "ES2018" 21 | }, 22 | "include": ["src/*"] 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "types": ["jest"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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": "jupyterlab_code_formatter-ui-tests", 3 | "version": "1.0.0", 4 | "description": "JupyterLab jupyterlab_code_formatter 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/jupyterlab_code_formatter.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jupyterlab/galata'; 2 | 3 | /** 4 | * Don't load JupyterLab webpage before running the tests. 5 | * This is required to ensure we capture all log messages. 6 | */ 7 | test.use({ autoGoto: false }); 8 | 9 | test('should emit an activation console message', async ({ page }) => { 10 | const logs: string[] = []; 11 | 12 | page.on('console', message => { 13 | logs.push(message.text()); 14 | }); 15 | 16 | await page.goto(); 17 | 18 | expect( 19 | logs.filter( 20 | s => s === 'JupyterLab extension jupyterlab_code_formatter is activated!' 21 | ) 22 | ).toHaveLength(1); 23 | }); 24 | --------------------------------------------------------------------------------