├── .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 | 
2 |
3 | [](https://jupyterlab-contrib.github.io/)
4 | [](https://github.com/jupyterlab-contrib/jupyterlab_code_formatter/actions/workflows/build.yml)
5 | [](https://mybinder.org/v2/gh/jupyterlab-contrib/jupyterlab_code_formatter/master?urlpath=lab)
6 | [](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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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\nHi
"
194 | expected = "%%html\nHi
"
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 |
--------------------------------------------------------------------------------