From c16a86ecf458c328ca401bef516895aa7ecbb958 Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 5 Jun 2024 01:23:59 +0900 Subject: [PATCH 01/15] build: project structure, ci, ruff lint, basedpyright, uv compile --- .github/workflows/apply-pip-compile.yml | 26 ++++ .github/workflows/check-pip-compile.yml | 45 ++++++ .github/workflows/deploy.yml | 93 ++++++++----- .github/workflows/lint.yml | 28 ++-- .github/workflows/styles.yml | 26 +--- .github/workflows/tests.yml | 4 +- .pre-commit-config.yaml | 11 -- .../.requirements.in.sha256 | 1 + .../.requirements_dev.in.sha256 | 1 + deps/aarch64-apple-darwin/requirements.txt | 66 +++++++++ .../aarch64-apple-darwin/requirements_dev.txt | 114 ++++++++++++++++ deps/requirements.in | 10 ++ deps/requirements_dev.in | 5 + .../.requirements.in.sha256 | 1 + .../.requirements_dev.in.sha256 | 1 + deps/x86_64-apple-darwin/requirements.txt | 66 +++++++++ deps/x86_64-apple-darwin/requirements_dev.txt | 114 ++++++++++++++++ .../.requirements.in.sha256 | 1 + .../.requirements_dev.in.sha256 | 1 + deps/x86_64-pc-windows-msvc/requirements.txt | 72 ++++++++++ .../requirements_dev.txt | 122 +++++++++++++++++ .../.requirements.in.sha256 | 1 + .../.requirements_dev.in.sha256 | 1 + .../x86_64-unknown-linux-gnu/requirements.txt | 66 +++++++++ .../requirements_dev.txt | 114 ++++++++++++++++ CHANGELOG.md => docs/CHANGELOG.md | 0 pyproject.toml | 122 ++++++++++++----- requirements.txt | 10 -- requirements_dev.txt | 6 - scripts/compile_requirements.sh | 129 ++++++++++++++++++ src/jupynium/events_control.py | 8 +- tox.ini | 5 +- 32 files changed, 1131 insertions(+), 139 deletions(-) create mode 100644 .github/workflows/apply-pip-compile.yml create mode 100644 .github/workflows/check-pip-compile.yml delete mode 100644 .pre-commit-config.yaml create mode 100644 deps/aarch64-apple-darwin/.requirements.in.sha256 create mode 100644 deps/aarch64-apple-darwin/.requirements_dev.in.sha256 create mode 100644 deps/aarch64-apple-darwin/requirements.txt create mode 100644 deps/aarch64-apple-darwin/requirements_dev.txt create mode 100644 deps/requirements.in create mode 100644 deps/requirements_dev.in create mode 100644 deps/x86_64-apple-darwin/.requirements.in.sha256 create mode 100644 deps/x86_64-apple-darwin/.requirements_dev.in.sha256 create mode 100644 deps/x86_64-apple-darwin/requirements.txt create mode 100644 deps/x86_64-apple-darwin/requirements_dev.txt create mode 100644 deps/x86_64-pc-windows-msvc/.requirements.in.sha256 create mode 100644 deps/x86_64-pc-windows-msvc/.requirements_dev.in.sha256 create mode 100644 deps/x86_64-pc-windows-msvc/requirements.txt create mode 100644 deps/x86_64-pc-windows-msvc/requirements_dev.txt create mode 100644 deps/x86_64-unknown-linux-gnu/.requirements.in.sha256 create mode 100644 deps/x86_64-unknown-linux-gnu/.requirements_dev.in.sha256 create mode 100644 deps/x86_64-unknown-linux-gnu/requirements.txt create mode 100644 deps/x86_64-unknown-linux-gnu/requirements_dev.txt rename CHANGELOG.md => docs/CHANGELOG.md (100%) delete mode 100644 requirements.txt delete mode 100644 requirements_dev.txt create mode 100644 scripts/compile_requirements.sh diff --git a/.github/workflows/apply-pip-compile.yml b/.github/workflows/apply-pip-compile.yml new file mode 100644 index 0000000..6e21652 --- /dev/null +++ b/.github/workflows/apply-pip-compile.yml @@ -0,0 +1,26 @@ +name: Apply pip compile (generate lockfiles) + +on: workflow_dispatch + +jobs: + apply-pip-compile: + name: Apply pip compile + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + - name: Install uv + run: | + pip3 install uv + - name: Run uv pip compile and push + run: | + set +e # Do not exit shell on failure + bash scripts/compile_requirements.sh + git config user.name github-actions[bot] + git config user.email github-actions[bot]@users.noreply.github.com + git add . + git commit -m "build: update requirements using uv pip compile [skip ci]" + git push diff --git a/.github/workflows/check-pip-compile.yml b/.github/workflows/check-pip-compile.yml new file mode 100644 index 0000000..a3e60ca --- /dev/null +++ b/.github/workflows/check-pip-compile.yml @@ -0,0 +1,45 @@ +name: Check pip compile sync + +on: [push, pull_request] + +jobs: + check-pip-compile: + name: Check pip compile + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + - name: Install uv + run: | + pip3 install uv + - name: Generate lockfile and print diff + run: | + set +e # Do not exit shell on failure + + out=$(bash scripts/compile_requirements.sh 2> _stderr.txt) + exit_code=$? + err=$(<_stderr.txt) + + if [[ -n "$out" ]]; then + # Display the raw output in the step + echo "${out}" + # Display the Markdown output in the job summary + { echo "\`\`\`"; echo "${out}"; echo "\`\`\`"; } >> "$GITHUB_STEP_SUMMARY" + fi + if [[ -n "$err" ]]; then + echo "${err}" + { echo "\`\`\`"; echo "${err}"; echo "\`\`\`"; } >> "$GITHUB_STEP_SUMMARY" + fi + + if [[ $exit_code -eq 0 ]]; then + # When the script fails, there are changes in requirements that are not compiled yet. + # Print the suggested changes. + { echo "\`\`\`diff"; git diff; echo "\`\`\`"; } >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + + # When the script fails, it means it does not have anything to compile. + exit 0 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d7696c1..bb241b2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,75 +1,94 @@ -name: Deploy +name: Deploy a new version on: - push: - tags: - - v[0-9]+.[0-9]+.[0-9]+ + workflow_dispatch: + inputs: + version_tag: + description: 'Version tag' + required: true + default: v0.1.0 + dry_run: + type: boolean + description: 'Dry run' + default: false jobs: deploy: runs-on: ubuntu-latest - environment: deploy + environment: mkdocs steps: - - name: Checkout to the branch of the tag + - uses: actions/checkout@v4 + - name: Push new version tag temporarily for changelog generation run: | - # checkout from GitHub CI without using actions/checkout - git clone https://oauth2:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git . - git fetch origin ${{ github.ref_name }} + git config user.name github-actions[bot] + git config user.email github-actions[bot]@users.noreply.github.com + git tag -a ${{ github.event.inputs.version_tag }} -m ${{ github.event.inputs.version_tag }} + git push --tags - # check if git tag is the last commit of some branch - TAG=${{ github.ref_name }} - git branch -a --contains $TAG - BRANCH=$(git branch -a --contains $TAG | grep -v HEAD | head -n 1 | sed 's/^* //' | sed 's/^ //') - echo "branch: $BRANCH" - LAST_COMMIT=$(git rev-parse $BRANCH) - echo "last commit hash: $LAST_COMMIT" - TAG_COMMIT=$(git rev-list -n 1 $TAG) - echo "tag commit hash: $TAG_COMMIT" + - name: (dry-run) Get CHANGELOG + if: ${{ github.event.inputs.dry_run }} + id: changelog-dry-run + uses: requarks/changelog-action@v1.10.2 + with: + includeInvalidCommits: true + excludeTypes: build,docs,style,other + token: ${{ github.token }} + tag: ${{ github.event.inputs.version_tag }} - if [[ "$LAST_COMMIT" != "$TAG_COMMIT" ]]; then - echo "ERROR: Tag $TAG is NOT the last commit of branch $BRANCH. Exiting.." - exit 1 - fi + - name: (dry-run) Display CHANGELOG + if: ${{ github.event.inputs.dry_run }} + run: | + echo '${{ steps.changelog-dry-run.outputs.changes }}' + echo '${{ steps.changelog-dry-run.outputs.changes }}' > "$GITHUB_STEP_SUMMARY" - git checkout "$BRANCH" + - name: (dry-run) Remove temporary version tag + if: ${{ github.event.inputs.dry_run }} + run: | + git tag -d ${{ github.event.inputs.version_tag }} + git push origin --delete ${{ github.event.inputs.version_tag }} - name: Update CHANGELOG + if: ${{ !github.event.inputs.dry_run }} id: changelog - uses: requarks/changelog-action@v1 + uses: requarks/changelog-action@v1.10.2 with: includeInvalidCommits: true - excludeTypes: build,docs,style + excludeTypes: build,docs,style,other token: ${{ github.token }} - tag: ${{ github.ref_name }} + tag: ${{ github.event.inputs.version_tag }} + changelogFilePath: docs/CHANGELOG.md - - name: Commit CHANGELOG.md + - name: Commit docs/CHANGELOG.md and update tag + if: ${{ !github.event.inputs.dry_run }} run: | - git config user.name github-actions - git config user.email github-actions@github.com - git tag -d ${{ github.ref_name }} - git push origin --delete ${{ github.ref_name }} - git add CHANGELOG.md - git commit -m "docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]" - git tag -a ${{ github.ref_name }} -m ${{ github.ref_name }} + git tag -d ${{ github.event.inputs.version_tag }} + git push origin --delete ${{ github.event.inputs.version_tag }} + git add docs/CHANGELOG.md + git commit -m "docs: update docs/CHANGELOG.md for ${{ github.event.inputs.version_tag }} [skip ci]" + git tag -a ${{ github.event.inputs.version_tag }} -m ${{ github.event.inputs.version_tag }} git push git push --tags - name: Create Release - uses: ncipollo/release-action@v1.12.0 + if: ${{ !github.event.inputs.dry_run }} + uses: ncipollo/release-action@v1.14.0 with: allowUpdates: true draft: false makeLatest: true - name: ${{ github.ref_name }} + name: ${{ github.event.inputs.version_tag }} + tag: ${{ github.event.inputs.version_tag }} body: ${{ steps.changelog.outputs.changes }} - token: ${{ github.token }} - name: Set up Python 3.11 + if: ${{ !github.event.inputs.dry_run }} uses: actions/setup-python@v4 with: python-version: 3.11 + - name: Build and upload to PyPI + if: ${{ !github.event.inputs.dry_run }} run: | python -m pip install --upgrade pip pip3 install build twine diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 73bd346..697f5d0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,14 +14,20 @@ jobs: python-version-file: pyproject.toml - name: Install ruff and requirements run: | - pip3 install -r <(cat requirements_dev.txt | grep '^ruff==') - - name: Run ruff + pip3 install -r <(grep '^ruff==' deps/x86_64-unknown-linux-gnu/requirements_dev.txt) + - name: Run ruff (code annotation) + run: | + set +e # Do not exit shell on ruff failure + + ruff check --output-format=github + exit 0 + - name: Run ruff (summary) run: | set +e # Do not exit shell on ruff failure nonzero_exit=0 files=$(find . -type f -name "*.py" | sort) - while read file; do + while read -r file; do out=$(ruff check --force-exclude "$file" 2> ruff_stderr.txt) exit_code=$? err=$(> $GITHUB_STEP_SUMMARY - echo "${out}" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + { echo "\`\`\`python"; echo "${out}"; echo "\`\`\`"; } >> "$GITHUB_STEP_SUMMARY" fi if [[ -n "$err" ]]; then echo "${err}" - echo "\`\`\`python" >> $GITHUB_STEP_SUMMARY - echo "${err}" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + { echo "\`\`\`python"; echo "${err}"; echo "\`\`\`"; } >> "$GITHUB_STEP_SUMMARY" fi out=$(ruff check --diff --force-exclude "$file" 2> ruff_stderr.txt) @@ -52,15 +54,11 @@ jobs: # Display the raw output in the step echo "${out}" # Display the Markdown output in the job summary - echo "\`\`\`diff" >> $GITHUB_STEP_SUMMARY - echo "${out}" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + { echo "\`\`\`python"; echo "${out}"; echo "\`\`\`"; } >> "$GITHUB_STEP_SUMMARY" fi if [[ -n "$err" ]]; then echo "${err}" - echo "\`\`\`python" >> $GITHUB_STEP_SUMMARY - echo "${err}" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + { echo "\`\`\`python"; echo "${err}"; echo "\`\`\`"; } >> "$GITHUB_STEP_SUMMARY" fi done <<< "$files" diff --git a/.github/workflows/styles.yml b/.github/workflows/styles.yml index 87b1812..3ad2fba 100644 --- a/.github/workflows/styles.yml +++ b/.github/workflows/styles.yml @@ -3,18 +3,6 @@ name: Style checking on: [push, pull_request] jobs: - stylua: - name: StyLua - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Lint with stylua - uses: JohnnyMorganz/stylua-action@v2 - with: - token: ${{ secrets.GITHUB_TOKEN }} - version: latest - args: --check . - ruff-format: name: ruff-format runs-on: ubuntu-latest @@ -26,7 +14,7 @@ jobs: python-version-file: pyproject.toml - name: Install ruff run: | - pip3 install -r <(cat requirements_dev.txt | grep '^ruff==') + pip3 install -r <(grep '^ruff==' deps/x86_64-unknown-linux-gnu/requirements_dev.txt) - name: Run ruff format run: | set +e # Do not exit shell on black failure @@ -39,10 +27,7 @@ jobs: echo "${err}" # Display the Markdown output in the job summary - echo "\`\`\`diff" >> $GITHUB_STEP_SUMMARY - echo "${out}" >> $GITHUB_STEP_SUMMARY - echo "${err}" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + { echo "\`\`\`diff"; echo "${out}"; echo "${err}"; echo "\`\`\`"; } >> "$GITHUB_STEP_SUMMARY" # Exit with the exit-code returned by ruff exit ${exit_code} @@ -58,7 +43,7 @@ jobs: python-version-file: pyproject.toml - name: Install ruff run: | - pip3 install -r <(cat requirements_dev.txt | grep '^ruff==') + pip3 install -r <(grep '^ruff==' deps/x86_64-unknown-linux-gnu/requirements_dev.txt) - name: Run ruff isort run: | set +e # Do not exit shell on app failure @@ -71,10 +56,7 @@ jobs: echo "${err}" # Display the Markdown output in the job summary - echo "\`\`\`diff" >> $GITHUB_STEP_SUMMARY - echo "${out}" >> $GITHUB_STEP_SUMMARY - echo "${err}" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + { echo "\`\`\`diff"; echo "${out}"; echo "${err}"; echo "\`\`\`"; } >> "$GITHUB_STEP_SUMMARY" # Exit with the exit-code returned by ruff exit ${exit_code} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 05ccdc3..3bf8317 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] nvim-tag: [stable] steps: @@ -21,7 +21,7 @@ jobs: run: | sudo apt-get update && sudo apt-get install libfuse2 sudo add-apt-repository universe - wget https://github.com/neovim/neovim/releases/download/${NVIM_TAG}/nvim.appimage + wget https://github.com/neovim/neovim/releases/download/"${NVIM_TAG}"/nvim.appimage chmod u+x nvim.appimage && sudo mv nvim.appimage /usr/local/bin/nvim # mkdir -p ~/.local/share/nvim/site/pack/tests/opt # ln -s $(pwd) ~/.local/share/nvim/site/pack/tests/opt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index e45836d..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,11 +0,0 @@ -repos: -- repo: https://github.com/JohnnyMorganz/StyLua - rev: v0.15.3 - hooks: - - id: stylua-github - -- repo: https://github.com/psf/black - rev: 22.12.0 - hooks: - - id: black - args: [--safe] diff --git a/deps/aarch64-apple-darwin/.requirements.in.sha256 b/deps/aarch64-apple-darwin/.requirements.in.sha256 new file mode 100644 index 0000000..2f7c133 --- /dev/null +++ b/deps/aarch64-apple-darwin/.requirements.in.sha256 @@ -0,0 +1 @@ +918b65d5e1c28c0eca34020abeb4851fe92201626cb5a1c4058e1a37d739a966 requirements.in diff --git a/deps/aarch64-apple-darwin/.requirements_dev.in.sha256 b/deps/aarch64-apple-darwin/.requirements_dev.in.sha256 new file mode 100644 index 0000000..53d17dd --- /dev/null +++ b/deps/aarch64-apple-darwin/.requirements_dev.in.sha256 @@ -0,0 +1 @@ +1e5b03eff652ffaf852c070d8ef8fe4fd361e74fbb1a915f9a59a2f3c3577725 requirements_dev.in diff --git a/deps/aarch64-apple-darwin/requirements.txt b/deps/aarch64-apple-darwin/requirements.txt new file mode 100644 index 0000000..dc31344 --- /dev/null +++ b/deps/aarch64-apple-darwin/requirements.txt @@ -0,0 +1,66 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.in -o aarch64-apple-darwin/requirements.txt --python-platform aarch64-apple-darwin +attrs==23.2.0 + # via + # outcome + # trio +certifi==2024.6.2 + # via selenium +coloredlogs==15.0.1 + # via -r requirements.in +exceptiongroup==1.2.1 + # via + # trio + # trio-websocket +gitdb==4.0.11 + # via gitpython +gitpython==3.1.43 + # via -r requirements.in +greenlet==3.0.3 + # via pynvim +h11==0.14.0 + # via wsproto +humanfriendly==10.0 + # via coloredlogs +idna==3.7 + # via trio +msgpack==1.0.8 + # via pynvim +outcome==1.3.0.post0 + # via trio +packaging==24.0 + # via -r requirements.in +persist-queue==0.8.1 + # via -r requirements.in +platformdirs==4.2.2 + # via -r requirements.in +psutil==5.9.8 + # via -r requirements.in +pynvim==0.5.0 + # via -r requirements.in +pysocks==1.7.1 + # via urllib3 +selenium==4.21.0 + # via -r requirements.in +setuptools==70.0.0 + # via -r requirements.in +smmap==5.0.1 + # via gitdb +sniffio==1.3.1 + # via trio +sortedcontainers==2.4.0 + # via trio +trio==0.25.1 + # via + # selenium + # trio-websocket +trio-websocket==0.11.1 + # via selenium +typing-extensions==4.12.1 + # via selenium +urllib3==2.2.1 + # via selenium +verboselogs==1.7 + # via -r requirements.in +wsproto==1.2.0 + # via trio-websocket diff --git a/deps/aarch64-apple-darwin/requirements_dev.txt b/deps/aarch64-apple-darwin/requirements_dev.txt new file mode 100644 index 0000000..9f1cadd --- /dev/null +++ b/deps/aarch64-apple-darwin/requirements_dev.txt @@ -0,0 +1,114 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements_dev.in -o aarch64-apple-darwin/requirements_dev.txt --python-platform aarch64-apple-darwin +attrs==23.2.0 + # via + # outcome + # trio +cachetools==5.3.3 + # via tox +certifi==2024.6.2 + # via selenium +chardet==5.2.0 + # via tox +colorama==0.4.6 + # via tox +coloredlogs==15.0.1 + # via -r requirements.in +coverage==7.5.3 + # via pytest-cov +distlib==0.3.8 + # via virtualenv +exceptiongroup==1.2.1 + # via + # pytest + # trio + # trio-websocket +filelock==3.14.0 + # via + # tox + # virtualenv +gitdb==4.0.11 + # via gitpython +gitpython==3.1.43 + # via -r requirements.in +greenlet==3.0.3 + # via pynvim +h11==0.14.0 + # via wsproto +humanfriendly==10.0 + # via coloredlogs +idna==3.7 + # via trio +iniconfig==2.0.0 + # via pytest +msgpack==1.0.8 + # via pynvim +outcome==1.3.0.post0 + # via trio +packaging==24.0 + # via + # -r requirements.in + # pyproject-api + # pytest + # tox +persist-queue==0.8.1 + # via -r requirements.in +platformdirs==4.2.2 + # via + # -r requirements.in + # tox + # virtualenv +pluggy==1.5.0 + # via + # pytest + # tox +psutil==5.9.8 + # via -r requirements.in +pynvim==0.5.0 + # via -r requirements.in +pyproject-api==1.6.1 + # via tox +pysocks==1.7.1 + # via urllib3 +pytest==8.2.2 + # via + # -r requirements_dev.in + # pytest-cov +pytest-cov==5.0.0 + # via -r requirements_dev.in +ruff==0.4.7 + # via -r requirements_dev.in +selenium==4.21.0 + # via -r requirements.in +setuptools==70.0.0 + # via -r requirements.in +smmap==5.0.1 + # via gitdb +sniffio==1.3.1 + # via trio +sortedcontainers==2.4.0 + # via trio +tomli==2.0.1 + # via + # coverage + # pyproject-api + # pytest + # tox +tox==4.15.0 + # via -r requirements_dev.in +trio==0.25.1 + # via + # selenium + # trio-websocket +trio-websocket==0.11.1 + # via selenium +typing-extensions==4.12.1 + # via selenium +urllib3==2.2.1 + # via selenium +verboselogs==1.7 + # via -r requirements.in +virtualenv==20.26.2 + # via tox +wsproto==1.2.0 + # via trio-websocket diff --git a/deps/requirements.in b/deps/requirements.in new file mode 100644 index 0000000..bbd4be7 --- /dev/null +++ b/deps/requirements.in @@ -0,0 +1,10 @@ +pynvim>=0.4.3 +coloredlogs>=15.0.0 +verboselogs>=1.7 +selenium>=4.7.2 +psutil>=5.9.4 +persist-queue>=0.8.0 +packaging>=22.0 +setuptools>=45.0 # for pkg_resources. Otherwise get LegacyVersion error +gitpython>=3.1.24 +platformdirs>=4.0.0 diff --git a/deps/requirements_dev.in b/deps/requirements_dev.in new file mode 100644 index 0000000..59b4bb0 --- /dev/null +++ b/deps/requirements_dev.in @@ -0,0 +1,5 @@ +-r requirements.in +ruff==0.4.7 +tox>=3.24.0 +pytest>=6.0.0 +pytest-cov>=2.0.0 diff --git a/deps/x86_64-apple-darwin/.requirements.in.sha256 b/deps/x86_64-apple-darwin/.requirements.in.sha256 new file mode 100644 index 0000000..2f7c133 --- /dev/null +++ b/deps/x86_64-apple-darwin/.requirements.in.sha256 @@ -0,0 +1 @@ +918b65d5e1c28c0eca34020abeb4851fe92201626cb5a1c4058e1a37d739a966 requirements.in diff --git a/deps/x86_64-apple-darwin/.requirements_dev.in.sha256 b/deps/x86_64-apple-darwin/.requirements_dev.in.sha256 new file mode 100644 index 0000000..53d17dd --- /dev/null +++ b/deps/x86_64-apple-darwin/.requirements_dev.in.sha256 @@ -0,0 +1 @@ +1e5b03eff652ffaf852c070d8ef8fe4fd361e74fbb1a915f9a59a2f3c3577725 requirements_dev.in diff --git a/deps/x86_64-apple-darwin/requirements.txt b/deps/x86_64-apple-darwin/requirements.txt new file mode 100644 index 0000000..c82690e --- /dev/null +++ b/deps/x86_64-apple-darwin/requirements.txt @@ -0,0 +1,66 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.in -o x86_64-apple-darwin/requirements.txt --python-platform x86_64-apple-darwin +attrs==23.2.0 + # via + # outcome + # trio +certifi==2024.6.2 + # via selenium +coloredlogs==15.0.1 + # via -r requirements.in +exceptiongroup==1.2.1 + # via + # trio + # trio-websocket +gitdb==4.0.11 + # via gitpython +gitpython==3.1.43 + # via -r requirements.in +greenlet==3.0.3 + # via pynvim +h11==0.14.0 + # via wsproto +humanfriendly==10.0 + # via coloredlogs +idna==3.7 + # via trio +msgpack==1.0.8 + # via pynvim +outcome==1.3.0.post0 + # via trio +packaging==24.0 + # via -r requirements.in +persist-queue==0.8.1 + # via -r requirements.in +platformdirs==4.2.2 + # via -r requirements.in +psutil==5.9.8 + # via -r requirements.in +pynvim==0.5.0 + # via -r requirements.in +pysocks==1.7.1 + # via urllib3 +selenium==4.21.0 + # via -r requirements.in +setuptools==70.0.0 + # via -r requirements.in +smmap==5.0.1 + # via gitdb +sniffio==1.3.1 + # via trio +sortedcontainers==2.4.0 + # via trio +trio==0.25.1 + # via + # selenium + # trio-websocket +trio-websocket==0.11.1 + # via selenium +typing-extensions==4.12.1 + # via selenium +urllib3==2.2.1 + # via selenium +verboselogs==1.7 + # via -r requirements.in +wsproto==1.2.0 + # via trio-websocket diff --git a/deps/x86_64-apple-darwin/requirements_dev.txt b/deps/x86_64-apple-darwin/requirements_dev.txt new file mode 100644 index 0000000..db1f91b --- /dev/null +++ b/deps/x86_64-apple-darwin/requirements_dev.txt @@ -0,0 +1,114 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements_dev.in -o x86_64-apple-darwin/requirements_dev.txt --python-platform x86_64-apple-darwin +attrs==23.2.0 + # via + # outcome + # trio +cachetools==5.3.3 + # via tox +certifi==2024.6.2 + # via selenium +chardet==5.2.0 + # via tox +colorama==0.4.6 + # via tox +coloredlogs==15.0.1 + # via -r requirements.in +coverage==7.5.3 + # via pytest-cov +distlib==0.3.8 + # via virtualenv +exceptiongroup==1.2.1 + # via + # pytest + # trio + # trio-websocket +filelock==3.14.0 + # via + # tox + # virtualenv +gitdb==4.0.11 + # via gitpython +gitpython==3.1.43 + # via -r requirements.in +greenlet==3.0.3 + # via pynvim +h11==0.14.0 + # via wsproto +humanfriendly==10.0 + # via coloredlogs +idna==3.7 + # via trio +iniconfig==2.0.0 + # via pytest +msgpack==1.0.8 + # via pynvim +outcome==1.3.0.post0 + # via trio +packaging==24.0 + # via + # -r requirements.in + # pyproject-api + # pytest + # tox +persist-queue==0.8.1 + # via -r requirements.in +platformdirs==4.2.2 + # via + # -r requirements.in + # tox + # virtualenv +pluggy==1.5.0 + # via + # pytest + # tox +psutil==5.9.8 + # via -r requirements.in +pynvim==0.5.0 + # via -r requirements.in +pyproject-api==1.6.1 + # via tox +pysocks==1.7.1 + # via urllib3 +pytest==8.2.2 + # via + # -r requirements_dev.in + # pytest-cov +pytest-cov==5.0.0 + # via -r requirements_dev.in +ruff==0.4.7 + # via -r requirements_dev.in +selenium==4.21.0 + # via -r requirements.in +setuptools==70.0.0 + # via -r requirements.in +smmap==5.0.1 + # via gitdb +sniffio==1.3.1 + # via trio +sortedcontainers==2.4.0 + # via trio +tomli==2.0.1 + # via + # coverage + # pyproject-api + # pytest + # tox +tox==4.15.0 + # via -r requirements_dev.in +trio==0.25.1 + # via + # selenium + # trio-websocket +trio-websocket==0.11.1 + # via selenium +typing-extensions==4.12.1 + # via selenium +urllib3==2.2.1 + # via selenium +verboselogs==1.7 + # via -r requirements.in +virtualenv==20.26.2 + # via tox +wsproto==1.2.0 + # via trio-websocket diff --git a/deps/x86_64-pc-windows-msvc/.requirements.in.sha256 b/deps/x86_64-pc-windows-msvc/.requirements.in.sha256 new file mode 100644 index 0000000..2f7c133 --- /dev/null +++ b/deps/x86_64-pc-windows-msvc/.requirements.in.sha256 @@ -0,0 +1 @@ +918b65d5e1c28c0eca34020abeb4851fe92201626cb5a1c4058e1a37d739a966 requirements.in diff --git a/deps/x86_64-pc-windows-msvc/.requirements_dev.in.sha256 b/deps/x86_64-pc-windows-msvc/.requirements_dev.in.sha256 new file mode 100644 index 0000000..53d17dd --- /dev/null +++ b/deps/x86_64-pc-windows-msvc/.requirements_dev.in.sha256 @@ -0,0 +1 @@ +1e5b03eff652ffaf852c070d8ef8fe4fd361e74fbb1a915f9a59a2f3c3577725 requirements_dev.in diff --git a/deps/x86_64-pc-windows-msvc/requirements.txt b/deps/x86_64-pc-windows-msvc/requirements.txt new file mode 100644 index 0000000..4735e3d --- /dev/null +++ b/deps/x86_64-pc-windows-msvc/requirements.txt @@ -0,0 +1,72 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.in -o x86_64-pc-windows-msvc/requirements.txt --python-platform x86_64-pc-windows-msvc +attrs==23.2.0 + # via + # outcome + # trio +certifi==2024.6.2 + # via selenium +cffi==1.16.0 + # via trio +coloredlogs==15.0.1 + # via -r requirements.in +exceptiongroup==1.2.1 + # via + # trio + # trio-websocket +gitdb==4.0.11 + # via gitpython +gitpython==3.1.43 + # via -r requirements.in +greenlet==3.0.3 + # via pynvim +h11==0.14.0 + # via wsproto +humanfriendly==10.0 + # via coloredlogs +idna==3.7 + # via trio +msgpack==1.0.8 + # via pynvim +outcome==1.3.0.post0 + # via trio +packaging==24.0 + # via -r requirements.in +persist-queue==0.8.1 + # via -r requirements.in +platformdirs==4.2.2 + # via -r requirements.in +psutil==5.9.8 + # via -r requirements.in +pycparser==2.22 + # via cffi +pynvim==0.5.0 + # via -r requirements.in +pyreadline3==3.4.1 + # via humanfriendly +pysocks==1.7.1 + # via urllib3 +selenium==4.21.0 + # via -r requirements.in +setuptools==70.0.0 + # via -r requirements.in +smmap==5.0.1 + # via gitdb +sniffio==1.3.1 + # via trio +sortedcontainers==2.4.0 + # via trio +trio==0.25.1 + # via + # selenium + # trio-websocket +trio-websocket==0.11.1 + # via selenium +typing-extensions==4.12.1 + # via selenium +urllib3==2.2.1 + # via selenium +verboselogs==1.7 + # via -r requirements.in +wsproto==1.2.0 + # via trio-websocket diff --git a/deps/x86_64-pc-windows-msvc/requirements_dev.txt b/deps/x86_64-pc-windows-msvc/requirements_dev.txt new file mode 100644 index 0000000..76b5375 --- /dev/null +++ b/deps/x86_64-pc-windows-msvc/requirements_dev.txt @@ -0,0 +1,122 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements_dev.in -o x86_64-pc-windows-msvc/requirements_dev.txt --python-platform x86_64-pc-windows-msvc +attrs==23.2.0 + # via + # outcome + # trio +cachetools==5.3.3 + # via tox +certifi==2024.6.2 + # via selenium +cffi==1.16.0 + # via trio +chardet==5.2.0 + # via tox +colorama==0.4.6 + # via + # pytest + # tox +coloredlogs==15.0.1 + # via -r requirements.in +coverage==7.5.3 + # via pytest-cov +distlib==0.3.8 + # via virtualenv +exceptiongroup==1.2.1 + # via + # pytest + # trio + # trio-websocket +filelock==3.14.0 + # via + # tox + # virtualenv +gitdb==4.0.11 + # via gitpython +gitpython==3.1.43 + # via -r requirements.in +greenlet==3.0.3 + # via pynvim +h11==0.14.0 + # via wsproto +humanfriendly==10.0 + # via coloredlogs +idna==3.7 + # via trio +iniconfig==2.0.0 + # via pytest +msgpack==1.0.8 + # via pynvim +outcome==1.3.0.post0 + # via trio +packaging==24.0 + # via + # -r requirements.in + # pyproject-api + # pytest + # tox +persist-queue==0.8.1 + # via -r requirements.in +platformdirs==4.2.2 + # via + # -r requirements.in + # tox + # virtualenv +pluggy==1.5.0 + # via + # pytest + # tox +psutil==5.9.8 + # via -r requirements.in +pycparser==2.22 + # via cffi +pynvim==0.5.0 + # via -r requirements.in +pyproject-api==1.6.1 + # via tox +pyreadline3==3.4.1 + # via humanfriendly +pysocks==1.7.1 + # via urllib3 +pytest==8.2.2 + # via + # -r requirements_dev.in + # pytest-cov +pytest-cov==5.0.0 + # via -r requirements_dev.in +ruff==0.4.7 + # via -r requirements_dev.in +selenium==4.21.0 + # via -r requirements.in +setuptools==70.0.0 + # via -r requirements.in +smmap==5.0.1 + # via gitdb +sniffio==1.3.1 + # via trio +sortedcontainers==2.4.0 + # via trio +tomli==2.0.1 + # via + # coverage + # pyproject-api + # pytest + # tox +tox==4.15.0 + # via -r requirements_dev.in +trio==0.25.1 + # via + # selenium + # trio-websocket +trio-websocket==0.11.1 + # via selenium +typing-extensions==4.12.1 + # via selenium +urllib3==2.2.1 + # via selenium +verboselogs==1.7 + # via -r requirements.in +virtualenv==20.26.2 + # via tox +wsproto==1.2.0 + # via trio-websocket diff --git a/deps/x86_64-unknown-linux-gnu/.requirements.in.sha256 b/deps/x86_64-unknown-linux-gnu/.requirements.in.sha256 new file mode 100644 index 0000000..2f7c133 --- /dev/null +++ b/deps/x86_64-unknown-linux-gnu/.requirements.in.sha256 @@ -0,0 +1 @@ +918b65d5e1c28c0eca34020abeb4851fe92201626cb5a1c4058e1a37d739a966 requirements.in diff --git a/deps/x86_64-unknown-linux-gnu/.requirements_dev.in.sha256 b/deps/x86_64-unknown-linux-gnu/.requirements_dev.in.sha256 new file mode 100644 index 0000000..53d17dd --- /dev/null +++ b/deps/x86_64-unknown-linux-gnu/.requirements_dev.in.sha256 @@ -0,0 +1 @@ +1e5b03eff652ffaf852c070d8ef8fe4fd361e74fbb1a915f9a59a2f3c3577725 requirements_dev.in diff --git a/deps/x86_64-unknown-linux-gnu/requirements.txt b/deps/x86_64-unknown-linux-gnu/requirements.txt new file mode 100644 index 0000000..7a09ea9 --- /dev/null +++ b/deps/x86_64-unknown-linux-gnu/requirements.txt @@ -0,0 +1,66 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.in -o x86_64-unknown-linux-gnu/requirements.txt --python-platform x86_64-unknown-linux-gnu +attrs==23.2.0 + # via + # outcome + # trio +certifi==2024.6.2 + # via selenium +coloredlogs==15.0.1 + # via -r requirements.in +exceptiongroup==1.2.1 + # via + # trio + # trio-websocket +gitdb==4.0.11 + # via gitpython +gitpython==3.1.43 + # via -r requirements.in +greenlet==3.0.3 + # via pynvim +h11==0.14.0 + # via wsproto +humanfriendly==10.0 + # via coloredlogs +idna==3.7 + # via trio +msgpack==1.0.8 + # via pynvim +outcome==1.3.0.post0 + # via trio +packaging==24.0 + # via -r requirements.in +persist-queue==0.8.1 + # via -r requirements.in +platformdirs==4.2.2 + # via -r requirements.in +psutil==5.9.8 + # via -r requirements.in +pynvim==0.5.0 + # via -r requirements.in +pysocks==1.7.1 + # via urllib3 +selenium==4.21.0 + # via -r requirements.in +setuptools==70.0.0 + # via -r requirements.in +smmap==5.0.1 + # via gitdb +sniffio==1.3.1 + # via trio +sortedcontainers==2.4.0 + # via trio +trio==0.25.1 + # via + # selenium + # trio-websocket +trio-websocket==0.11.1 + # via selenium +typing-extensions==4.12.1 + # via selenium +urllib3==2.2.1 + # via selenium +verboselogs==1.7 + # via -r requirements.in +wsproto==1.2.0 + # via trio-websocket diff --git a/deps/x86_64-unknown-linux-gnu/requirements_dev.txt b/deps/x86_64-unknown-linux-gnu/requirements_dev.txt new file mode 100644 index 0000000..663c964 --- /dev/null +++ b/deps/x86_64-unknown-linux-gnu/requirements_dev.txt @@ -0,0 +1,114 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements_dev.in -o x86_64-unknown-linux-gnu/requirements_dev.txt --python-platform x86_64-unknown-linux-gnu +attrs==23.2.0 + # via + # outcome + # trio +cachetools==5.3.3 + # via tox +certifi==2024.6.2 + # via selenium +chardet==5.2.0 + # via tox +colorama==0.4.6 + # via tox +coloredlogs==15.0.1 + # via -r requirements.in +coverage==7.5.3 + # via pytest-cov +distlib==0.3.8 + # via virtualenv +exceptiongroup==1.2.1 + # via + # pytest + # trio + # trio-websocket +filelock==3.14.0 + # via + # tox + # virtualenv +gitdb==4.0.11 + # via gitpython +gitpython==3.1.43 + # via -r requirements.in +greenlet==3.0.3 + # via pynvim +h11==0.14.0 + # via wsproto +humanfriendly==10.0 + # via coloredlogs +idna==3.7 + # via trio +iniconfig==2.0.0 + # via pytest +msgpack==1.0.8 + # via pynvim +outcome==1.3.0.post0 + # via trio +packaging==24.0 + # via + # -r requirements.in + # pyproject-api + # pytest + # tox +persist-queue==0.8.1 + # via -r requirements.in +platformdirs==4.2.2 + # via + # -r requirements.in + # tox + # virtualenv +pluggy==1.5.0 + # via + # pytest + # tox +psutil==5.9.8 + # via -r requirements.in +pynvim==0.5.0 + # via -r requirements.in +pyproject-api==1.6.1 + # via tox +pysocks==1.7.1 + # via urllib3 +pytest==8.2.2 + # via + # -r requirements_dev.in + # pytest-cov +pytest-cov==5.0.0 + # via -r requirements_dev.in +ruff==0.4.7 + # via -r requirements_dev.in +selenium==4.21.0 + # via -r requirements.in +setuptools==70.0.0 + # via -r requirements.in +smmap==5.0.1 + # via gitdb +sniffio==1.3.1 + # via trio +sortedcontainers==2.4.0 + # via trio +tomli==2.0.1 + # via + # coverage + # pyproject-api + # pytest + # tox +tox==4.15.0 + # via -r requirements_dev.in +trio==0.25.1 + # via + # selenium + # trio-websocket +trio-websocket==0.11.1 + # via selenium +typing-extensions==4.12.1 + # via selenium +urllib3==2.2.1 + # via selenium +verboselogs==1.7 + # via -r requirements.in +virtualenv==20.26.2 + # via tox +wsproto==1.2.0 + # via trio-websocket diff --git a/CHANGELOG.md b/docs/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to docs/CHANGELOG.md diff --git a/pyproject.toml b/pyproject.toml index e24dcc9..39e2269 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "jupynium" -dynamic = ["version"] +dynamic = ["version", "dependencies", "optional-dependencies"] description = "Neovim plugin that automates Jupyter Notebook editing/browsing using Selenium." authors = [ { name = "Kiyoon Kim" }, @@ -20,36 +20,16 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "License :: OSI Approved :: MIT License", ] keywords = ["neovim", "vim", "jupyter", "selenium", "jupyter-notebook", "nvim", "neovim-plugin", "nvim-plugin"] -dependencies = [ - "pynvim >= 0.4.3", - "coloredlogs >= 15.0.0", - "verboselogs >= 1.7", - "selenium >= 4.7.2", - "psutil >= 5.9.4", - "persist-queue >= 0.8.0", - "packaging >= 22.0", - "setuptools >= 45.0", # for pkg_resources. Otherwise get LegacyVersion error - "gitpython >= 3.1.24", - "platformdirs >= 4.0.0", -] -[project.optional-dependencies] -extra = [ - "notebook >= 6.4.5", -] -dev = [ - "ruff >= 0.4.2", - "pre-commit >= 2.21.0", -] -test = [ - "pytest >= 6.0", - "pytest-cov >= 2.0", - "importlib-metadata < 5.0.0; python_version < '3.8'", # flake8 dependency - "tox >= 3.24", -] +[tool.setuptools.dynamic] +dependencies = {file = ["deps/requirements.in"]} + +[tool.setuptools.packages.find] +where = ["src"] [project.urls] "Homepage" = "https://github.com/kiyoon/jupynium.nvim" @@ -59,9 +39,6 @@ jupynium = "jupynium.cmds.jupynium:main" ipynb2jupy = "jupynium.cmds.ipynb2jupy:main" ipynb2jupytext = "jupynium.cmds.ipynb2jupytext:main" -[tool.setuptools.packages.find] -where = ["src"] - [tool.setuptools_scm] write_to = "src/jupynium/_version.py" @@ -71,6 +48,12 @@ testpaths = [ "tests", ] +[tool.coverage.report] +omit = [ + "src/jupynium/_version.py", # CHANGE + # OPTIONALLY ADD MORE LATER +] + [tool.ruff] target-version = "py37" src = ["src"] # for ruff isort @@ -78,8 +61,85 @@ extend-exclude = [ "src/jupynium/_version.py", # CHANGE ] +[tool.ruff.lint] +# OPTIONALLY ADD MORE LATER +select = [ + # flake8 + "E", + "F", + "W", + "B", # Bugbear + "D", # Docstring + "D213", # Multi-line docstring summary should start at the second line (replace D212) + "N", # Naming + "C4", # flake8-comprehensions + "UP", # pyupgrade + "SIM", # simplify + "RUF", # ruff-specific + "RET501", # return + "RET502", # return + "RET503", # return + "PTH", # path + "NPY", # numpy + "PYI", # type stubs for pyright/pylance + "PT", # pytest + "PIE", # + "LOG", # logging + "COM818", # comma misplaced + "COM819", # comma + "DTZ", # datetime + "YTT", + "ASYNC", + + # Not important + "T10", # debug statements + "T20", # print statements +] + +ignore = [ + "E402", # Module level import not at top of file + "W293", # Blank line contains whitespace + "W291", # Trailing whitespace + "D10", # Missing docstring in public module / function / etc. + "D200", # One-line docstring should fit on one line with quotes + "D212", # Multi-line docstring summary should start at the first line + "D417", # require documentation for every function parameter. + "D401", # require an imperative mood for all docstrings. + "PTH123", # Path.open should be used instead of built-in open + "PT006", # Pytest parameterize style + "N812", # Lowercase `functional` imported as non-lowercase `F` (import torch.nn.functional as F) + "NPY002", # legacy numpy random + "UP017", # datetime.timezone.utc -> datetime.UTC + "SIM108", # use ternary operator instead of if-else +] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.pycodestyle] +# Black or ruff will enforce line length to be 88, except for docstrings and comments. +# We set it to 120 so we have more space for docstrings and comments. +max-line-length = 120 + [tool.ruff.lint.isort] -## Uncomment this if you want to use Python < 3.10 required-imports = [ "from __future__ import annotations", ] + +[tool.pyright] +include = ["src"] + +typeCheckingMode = "standard" +autoSearchPaths = true +useLibraryCodeForTypes = true +autoImportCompletions = true +diagnosticsMode = "openFilesOnly" + +reportUnusedImports = false +reportUnusedVariable = false +# reportUnusedClass = "warning" +# reportUnusedFunction = "warning" +reportUndefinedVariable = false # ruff handles this with F821 + +pythonVersion = "3.7" +pythonPlatform = "Linux" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 4a4032f..0000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -selenium==4.7.2 -coloredlogs==15.0.1 -verboselogs==1.7 -pynvim==0.4.3 -psutil==5.9.4 -persist-queue==0.8.0 -packaging==23.0 -setuptools==66.0 -gitpython==3.1.30 -platformdirs==4.0.0 diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 93af41a..0000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,6 +0,0 @@ -ruff==0.4.2 -tox==3.24.3 -pytest==6.2.5 -pytest-cov==2.12.1 -pre-commit==2.21.0 -importlib-metadata==4.13.0 diff --git a/scripts/compile_requirements.sh b/scripts/compile_requirements.sh new file mode 100644 index 0000000..9ef6498 --- /dev/null +++ b/scripts/compile_requirements.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash + +# This script compiles all requirements.in files to requirements.txt files +# This means that all dependencies are locked to a specific version +# Plus, it checks if the requirements.in file has changed since the last time it was compiled +# If not, it skips the file rather than recompiling it (which may change version unnecessarily often) + +if ! command -v uv &> /dev/null; then + echo "uv is not installed. Please run 'pip3 install --user uv'" >&2 + exit 1 +fi + +if ! command -v sha256sum &> /dev/null; then + echo "sha256sum is not installed." >&2 + echo "If you're on Mac, run 'brew install coreutils'" >&2 + exit 1 +fi + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +# NOTE: sha256sum will put the file path in the hash file. +# To simplify the directory (using relative paths), we change the working directory. +cd "$SCRIPT_DIR/../deps" || { echo "Failure"; exit 1; } + +TARGET_PLATFORMS=(x86_64-unknown-linux-gnu aarch64-apple-darwin x86_64-apple-darwin x86_64-pc-windows-msvc) +for platform in "${TARGET_PLATFORMS[@]}"; do + mkdir -p "$platform" +done + +shopt -s globstar + +function get_shafile() { + local file=$1 + local target_platform=$2 + # .requirements.in.sha256 + echo "$target_platform/.$file.sha256" +} + +function get_lockfile() { + local file=$1 + local target_platform=$2 + # requirements.txt + echo "$target_platform/${file%.in}.txt" +} + +function file_content_changed() { + # Check if the file has changed since the last time it was compiled, using the hash file. + # NOTE: returns 0 if the file has changed + local file=$1 + local target_platform=$2 + local shafile + shafile=$(get_shafile "$file" "$target_platform") + if [[ -f "$shafile" ]] && sha256sum -c "$shafile" &> /dev/null; then + return 1 + fi + return 0 +} + + +function deps_changed() { + # Check if the requirements*.in file has changed since the last time it was compiled, including its dependencies (-r another_requirements.in). + # + # When the requirements have dependencies on other requirements files, we need to check if those have changed as well + # e.g. requirements_dev.in has a dependency on requirements.in (-r requirements.in) + # Note that we also need to recursively check if the dependencies of the dependencies have changed. + # We need to recompile requirements_dev.txt if requirements.in has changed. + # NOTE: returns 0 if the deps have changed + local file=$1 + local target_platform=$2 + + if file_content_changed "$file" "$target_platform"; then + return 0 + fi + + + local file_deps + file_deps=$(grep -Eo -- '-r [^ ]+' "$file") + file_deps=${file_deps//"-r "/} # remove -r + for dep in $file_deps; do + echo "ℹī¸ $file depends on $dep" + dep=${dep#-r } # requirements.in + if deps_changed "$dep" "$target_platform"; then + return 0 + fi + done + return 1 +} + +num_files=0 +num_up_to_date=0 +files_changed=() + +# First, collect all files that need to be compiled. +# We don't compile them yet, because it will mess up the hash comparison. +for file in requirements*.in; do + for target_platform in "${TARGET_PLATFORMS[@]}"; do + # $file: requirements.in + ((num_files++)) + + lockfile=$(get_lockfile "$file" "$target_platform") + shafile=$(get_shafile "$file" "$target_platform") + # Process only changed files by comparing hash + if [[ -f "$lockfile" ]]; then + if ! deps_changed "$file" "$target_platform"; then + echo "⚡ Skipping $file due to no changes" + ((num_up_to_date++)) + continue + fi + fi + files_changed+=("$file") + done +done + +for file in "${files_changed[@]}"; do + for target_platform in "${TARGET_PLATFORMS[@]}"; do + lockfile=$(get_lockfile "$file" "$target_platform") + shafile=$(get_shafile "$file" "$target_platform") + echo "🔒 Generating lockfile $lockfile from $file" + uv pip compile "$file" -o "$lockfile" --python-platform "$target_platform" > /dev/null + sha256sum "$file" > "$shafile" # update hash + done +done + +# exit code 2 when all files are up to date +if [[ $num_files -eq $num_up_to_date ]]; then + echo "💖 All files are up to date!" + exit 2 +fi + diff --git a/src/jupynium/events_control.py b/src/jupynium/events_control.py index 68962dc..f4353dc 100644 --- a/src/jupynium/events_control.py +++ b/src/jupynium/events_control.py @@ -81,12 +81,12 @@ class OnLinesArgs: new_end_row: int # Optimisations - def is_chainable(self, other: "OnLinesArgs") -> bool: + def is_chainable(self, other: OnLinesArgs) -> bool: return ( self.start_row == other.start_row and self.new_end_row == other.old_end_row ) - def chain(self, other: "OnLinesArgs") -> "OnLinesArgs": + def chain(self, other: OnLinesArgs) -> OnLinesArgs: assert self.is_chainable(other) return OnLinesArgs( other.lines, @@ -111,7 +111,9 @@ class PrevLazyArgs: on_lines_args: OnLinesArgs | None = None update_selection_args: UpdateSelectionArgs | None = None - def process(self, nvim_info: NvimInfo, driver, bufnr) -> None: + def process( + self, nvim_info: NvimInfo, driver: selenium.webdriver, bufnr: int + ) -> None: if self.on_lines_args is not None: process_on_lines_event(nvim_info, driver, bufnr, self.on_lines_args) self.on_lines_args = None diff --git a/tox.ini b/tox.ini index 20196e6..05046ae 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.24.0 -envlist = python3.7, python3.8, python3.9, python3.10, python3.11 +envlist = python3.7, python3.8, python3.9, python3.10, python3.11, python3.12 isolated_build = true [gh-actions] @@ -10,11 +10,12 @@ python = 3.9: python3.9 3.10: python3.10 3.11: python3.11 + 3.11: python3.12 [testenv] setenv = PYTHONPATH = {toxinidir} deps = - -r{toxinidir}/requirements_dev.txt + -r{toxinidir}/deps/x86_64-unknown-linux-gnu/requirements_dev.txt commands = pytest --basetemp={envtmpdir} From c83e66b3cb9fd815e0bb87f38fc74beb72a08b44 Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 5 Jun 2024 01:32:41 +0900 Subject: [PATCH 02/15] build!: drop python3.7 support --- README.md | 2 +- pyproject.toml | 7 +++---- tox.ini | 3 +-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1a274fe..4373739 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The Jupynium server will receive events from Neovim, keep the copy of the buffer - Other browsers are not supported due to their limitation with Selenium (see [#49](https://github.com/kiyoon/jupynium.nvim/issues/49#issuecomment-1443304753)) - đŸĻŽ Mozilla geckodriver - May already be installed with Firefox. Check `geckodriver -V` -- 🐍 Python >= 3.7 +- 🐍 Python >= 3.8 - Supported Python installation methods include system-level and [Conda](https://docs.conda.io/en/latest/miniconda.html) - 📔 Jupyter Notebook >= 6.2 - Jupyter Lab is not supported diff --git a/pyproject.toml b/pyproject.toml index 39e2269..b794d8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,11 +11,10 @@ authors = [ ] readme = "README.md" license = { file="LICENSE" } -requires-python = ">=3.7" +requires-python = ">=3.8" classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -55,7 +54,7 @@ omit = [ ] [tool.ruff] -target-version = "py37" +target-version = "py38" src = ["src"] # for ruff isort extend-exclude = [ "src/jupynium/_version.py", # CHANGE @@ -141,5 +140,5 @@ reportUnusedVariable = false # reportUnusedFunction = "warning" reportUndefinedVariable = false # ruff handles this with F821 -pythonVersion = "3.7" +pythonVersion = "3.8" pythonPlatform = "Linux" diff --git a/tox.ini b/tox.ini index 05046ae..7c03fb4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,10 @@ [tox] minversion = 3.24.0 -envlist = python3.7, python3.8, python3.9, python3.10, python3.11, python3.12 +envlist = python3.8, python3.9, python3.10, python3.11, python3.12 isolated_build = true [gh-actions] python = - 3.7: python3.7 3.8: python3.8 3.9: python3.9 3.10: python3.10 From f49faa9d1d4d02c281d80937d5d1d2b01a298059 Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 5 Jun 2024 01:38:46 +0900 Subject: [PATCH 03/15] fix: python 3.8 build --- .github/workflows/tests.yml | 2 +- deps/aarch64-apple-darwin/requirements.txt | 2 +- deps/aarch64-apple-darwin/requirements_dev.txt | 2 +- deps/x86_64-apple-darwin/requirements.txt | 2 +- deps/x86_64-apple-darwin/requirements_dev.txt | 2 +- deps/x86_64-pc-windows-msvc/requirements.txt | 2 +- deps/x86_64-pc-windows-msvc/requirements_dev.txt | 2 +- deps/x86_64-unknown-linux-gnu/requirements.txt | 2 +- deps/x86_64-unknown-linux-gnu/requirements_dev.txt | 2 +- scripts/compile_requirements.sh | 6 ++++-- 10 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3bf8317..0944d9f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] nvim-tag: [stable] steps: diff --git a/deps/aarch64-apple-darwin/requirements.txt b/deps/aarch64-apple-darwin/requirements.txt index dc31344..394e6ef 100644 --- a/deps/aarch64-apple-darwin/requirements.txt +++ b/deps/aarch64-apple-darwin/requirements.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements.in -o aarch64-apple-darwin/requirements.txt --python-platform aarch64-apple-darwin +# uv pip compile requirements.in -o aarch64-apple-darwin/requirements.txt --python-platform aarch64-apple-darwin --python-version 3.8 attrs==23.2.0 # via # outcome diff --git a/deps/aarch64-apple-darwin/requirements_dev.txt b/deps/aarch64-apple-darwin/requirements_dev.txt index 9f1cadd..f6cc1de 100644 --- a/deps/aarch64-apple-darwin/requirements_dev.txt +++ b/deps/aarch64-apple-darwin/requirements_dev.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements_dev.in -o aarch64-apple-darwin/requirements_dev.txt --python-platform aarch64-apple-darwin +# uv pip compile requirements_dev.in -o aarch64-apple-darwin/requirements_dev.txt --python-platform aarch64-apple-darwin --python-version 3.8 attrs==23.2.0 # via # outcome diff --git a/deps/x86_64-apple-darwin/requirements.txt b/deps/x86_64-apple-darwin/requirements.txt index c82690e..48d0843 100644 --- a/deps/x86_64-apple-darwin/requirements.txt +++ b/deps/x86_64-apple-darwin/requirements.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements.in -o x86_64-apple-darwin/requirements.txt --python-platform x86_64-apple-darwin +# uv pip compile requirements.in -o x86_64-apple-darwin/requirements.txt --python-platform x86_64-apple-darwin --python-version 3.8 attrs==23.2.0 # via # outcome diff --git a/deps/x86_64-apple-darwin/requirements_dev.txt b/deps/x86_64-apple-darwin/requirements_dev.txt index db1f91b..50cc971 100644 --- a/deps/x86_64-apple-darwin/requirements_dev.txt +++ b/deps/x86_64-apple-darwin/requirements_dev.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements_dev.in -o x86_64-apple-darwin/requirements_dev.txt --python-platform x86_64-apple-darwin +# uv pip compile requirements_dev.in -o x86_64-apple-darwin/requirements_dev.txt --python-platform x86_64-apple-darwin --python-version 3.8 attrs==23.2.0 # via # outcome diff --git a/deps/x86_64-pc-windows-msvc/requirements.txt b/deps/x86_64-pc-windows-msvc/requirements.txt index 4735e3d..bc46043 100644 --- a/deps/x86_64-pc-windows-msvc/requirements.txt +++ b/deps/x86_64-pc-windows-msvc/requirements.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements.in -o x86_64-pc-windows-msvc/requirements.txt --python-platform x86_64-pc-windows-msvc +# uv pip compile requirements.in -o x86_64-pc-windows-msvc/requirements.txt --python-platform x86_64-pc-windows-msvc --python-version 3.8 attrs==23.2.0 # via # outcome diff --git a/deps/x86_64-pc-windows-msvc/requirements_dev.txt b/deps/x86_64-pc-windows-msvc/requirements_dev.txt index 76b5375..55f12be 100644 --- a/deps/x86_64-pc-windows-msvc/requirements_dev.txt +++ b/deps/x86_64-pc-windows-msvc/requirements_dev.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements_dev.in -o x86_64-pc-windows-msvc/requirements_dev.txt --python-platform x86_64-pc-windows-msvc +# uv pip compile requirements_dev.in -o x86_64-pc-windows-msvc/requirements_dev.txt --python-platform x86_64-pc-windows-msvc --python-version 3.8 attrs==23.2.0 # via # outcome diff --git a/deps/x86_64-unknown-linux-gnu/requirements.txt b/deps/x86_64-unknown-linux-gnu/requirements.txt index 7a09ea9..80c5916 100644 --- a/deps/x86_64-unknown-linux-gnu/requirements.txt +++ b/deps/x86_64-unknown-linux-gnu/requirements.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements.in -o x86_64-unknown-linux-gnu/requirements.txt --python-platform x86_64-unknown-linux-gnu +# uv pip compile requirements.in -o x86_64-unknown-linux-gnu/requirements.txt --python-platform x86_64-unknown-linux-gnu --python-version 3.8 attrs==23.2.0 # via # outcome diff --git a/deps/x86_64-unknown-linux-gnu/requirements_dev.txt b/deps/x86_64-unknown-linux-gnu/requirements_dev.txt index 663c964..320f171 100644 --- a/deps/x86_64-unknown-linux-gnu/requirements_dev.txt +++ b/deps/x86_64-unknown-linux-gnu/requirements_dev.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements_dev.in -o x86_64-unknown-linux-gnu/requirements_dev.txt --python-platform x86_64-unknown-linux-gnu +# uv pip compile requirements_dev.in -o x86_64-unknown-linux-gnu/requirements_dev.txt --python-platform x86_64-unknown-linux-gnu --python-version 3.8 attrs==23.2.0 # via # outcome diff --git a/scripts/compile_requirements.sh b/scripts/compile_requirements.sh index 9ef6498..b770cc6 100644 --- a/scripts/compile_requirements.sh +++ b/scripts/compile_requirements.sh @@ -5,6 +5,9 @@ # Plus, it checks if the requirements.in file has changed since the last time it was compiled # If not, it skips the file rather than recompiling it (which may change version unnecessarily often) +TARGET_PLATFORMS=(x86_64-unknown-linux-gnu aarch64-apple-darwin x86_64-apple-darwin x86_64-pc-windows-msvc) +PYTHON_VERSION=3.8 + if ! command -v uv &> /dev/null; then echo "uv is not installed. Please run 'pip3 install --user uv'" >&2 exit 1 @@ -22,7 +25,6 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) # To simplify the directory (using relative paths), we change the working directory. cd "$SCRIPT_DIR/../deps" || { echo "Failure"; exit 1; } -TARGET_PLATFORMS=(x86_64-unknown-linux-gnu aarch64-apple-darwin x86_64-apple-darwin x86_64-pc-windows-msvc) for platform in "${TARGET_PLATFORMS[@]}"; do mkdir -p "$platform" done @@ -116,7 +118,7 @@ for file in "${files_changed[@]}"; do lockfile=$(get_lockfile "$file" "$target_platform") shafile=$(get_shafile "$file" "$target_platform") echo "🔒 Generating lockfile $lockfile from $file" - uv pip compile "$file" -o "$lockfile" --python-platform "$target_platform" > /dev/null + uv pip compile "$file" -o "$lockfile" --python-platform "$target_platform" --python-version "$PYTHON_VERSION" > /dev/null sha256sum "$file" > "$shafile" # update hash done done From e4a0c2c7ff1687d6d7322916e8babf9355ddd664 Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 5 Jun 2024 02:19:10 +0900 Subject: [PATCH 04/15] refactor: ruff --- src/jupynium/buffer.py | 63 +++++++++++++---------- src/jupynium/cmds/ipynb2jupy.py | 5 +- src/jupynium/cmds/ipynb2jupytext.py | 5 +- src/jupynium/events_control.py | 57 +++++++++++--------- src/jupynium/ipynb.py | 29 ++++++----- src/jupynium/jupyter_notebook_selenium.py | 8 +-- src/jupynium/nvim.py | 28 +++++----- src/jupynium/process.py | 13 +++-- src/jupynium/pynvim_helpers.py | 2 +- src/jupynium/selenium_helpers.py | 13 +++-- 10 files changed, 129 insertions(+), 94 deletions(-) diff --git a/src/jupynium/buffer.py b/src/jupynium/buffer.py index ababd3b..73ba4ba 100644 --- a/src/jupynium/buffer.py +++ b/src/jupynium/buffer.py @@ -3,6 +3,7 @@ import logging from pkg_resources import resource_stream +from selenium.webdriver.remote.webdriver import WebDriver from .jupyter_notebook_selenium import insert_cell_at @@ -24,6 +25,7 @@ def _process_cell_type(cell_type: str) -> str: def _process_cell_types(cell_types: list[str]) -> list[str]: """ Return the cell types, e.g. ["markdown", "code", "header"]. + markdown (jupytext) is converted to markdown. """ return [_process_cell_type(cell_type) for cell_type in cell_types] @@ -31,21 +33,21 @@ def _process_cell_types(cell_types: list[str]) -> list[str]: class JupyniumBuffer: """ - This class mainly deals with the Nvim buffer and its cell information. + Deal with the Nvim buffer and its cell information. + This does have a functionality to sync with the Notebook. """ def __init__( self, buf: list[str] = [""], - header_cell_type="header", + header_cell_type: str = "header", ): """ - self.buf is a list of lines of the nvim buffer, + self.buf is a list of lines of the nvim buffer. Args: - header_cell_type (str, optional): Use only when partial update. - header_cell_separator (str, optional): Use only when partial update. + header_cell_type: Use only when partial update. """ self.buf = buf if self.buf == [""]: @@ -57,9 +59,10 @@ def __init__( else: self.full_analyse_buf(header_cell_type) - def full_analyse_buf(self, header_cell_type="header"): + def full_analyse_buf(self, header_cell_type: str = "header"): """ Main parser for the jupynium format (*.ju.*). + This function needs to support partial update. E.g. by looking at 1 line of change, it should be able to understand if: @@ -82,23 +85,15 @@ def full_analyse_buf(self, header_cell_type="header"): num_rows_per_cell = [] cell_types = [header_cell_type] for row, line in enumerate(self.buf): - if ( - line.startswith("# %%%") - or line.startswith('"""%%') - or line.startswith("'''%%") - ): + if line.startswith(("# %%%", '"""%%', "'''%%")): num_rows_per_cell.append(num_rows_this_cell) num_rows_this_cell = 1 cell_types.append("markdown") - elif line.startswith("# %% [md]") or line.startswith("# %% [markdown]"): + elif line.startswith(("# %% [md]", "# %% [markdown]")): num_rows_per_cell.append(num_rows_this_cell) num_rows_this_cell = 1 cell_types.append("markdown (jupytext)") - elif ( - line.strip() == "# %%" - or line.startswith('%%"""') - or line.startswith("%%'''") - ): + elif line.strip() == "# %%" or line.startswith(('%%"""', "%%'''")): num_rows_per_cell.append(num_rows_this_cell) num_rows_this_cell = 1 cell_types.append("code") @@ -109,7 +104,7 @@ def full_analyse_buf(self, header_cell_type="header"): self.num_rows_per_cell = num_rows_per_cell self.cell_types = cell_types - def _process_cell_text(self, cell_type, lines: list[str]): + def _process_cell_text(self, cell_type: str, lines: list[str]): """ Assuming that lines is just one cell's content, process it. """ @@ -133,12 +128,12 @@ def get_cells_text( ) -> list[str]: """ Get processed cell text. + In a code cell, remove comments for the magic commands. e.g. '# %time' -> '%time' In a markdown cell, remove the leading # from the lines or multiline string. e.g. '# # Markdown header' -> '# Markdown header' """ - if start_cell_idx == 0: start_row_offset = 0 else: @@ -176,7 +171,13 @@ def get_cell_text(self, cell_idx: int, strip: bool = True) -> str: return self.get_cells_text(cell_idx, cell_idx, strip=strip)[0] def process_on_lines( - self, driver, strip, lines, start_row, old_end_row, new_end_row + self, + driver: WebDriver, + strip: bool, + lines: list[str], + start_row: int, + old_end_row: int, + new_end_row: int, ): ( notebook_cell_operations, @@ -198,10 +199,7 @@ def process_on_lines( ) def _on_lines_update_buf(self, lines, start_row, old_end_row, new_end_row): - """ - Replace start_row:old_end_row to lines from self.buf - """ - + """Replace start_row:old_end_row to lines from self.buf.""" # Analyse how many cells are removed notebook_cell_delete_operations = [] notebook_cell_operations = [] @@ -313,7 +311,11 @@ def _on_lines_update_buf(self, lines, start_row, old_end_row, new_end_row): return notebook_cell_operations, modified_cell_idx_start, modified_cell_idx_end - def _apply_cell_operations(self, driver, notebook_cell_operations): + def _apply_cell_operations( + self, + driver: WebDriver, + notebook_cell_operations: list[tuple[str, int, list[str]]], + ): # Remove / create cells in Notebook for operation, cell_idx, cell_types in notebook_cell_operations: nb_cell_idx = cell_idx - 1 @@ -346,7 +348,7 @@ def _apply_cell_operations(self, driver, notebook_cell_operations): else: raise ValueError(f"Unknown cell type {cell_type}") - def get_cell_start_row(self, cell_idx): + def get_cell_start_row(self, cell_idx: int): return sum(self.num_rows_per_cell[:cell_idx]) def get_cell_index_from_row( @@ -397,8 +399,11 @@ def _partial_sync_to_notebook( self, driver, start_cell_idx, end_cell_idx, strip=True ): """ - Cell 1 in JupyniumBuffer is cell 0 in Notebook - Args are inclusive range in ju.py JupyniumBuffer + Given the range of cells to update, sync the JupyniumBuffer with the notebook. + + Note: + Cell 1 in JupyniumBuffer is cell 0 in Notebook. + Args are inclusive range in ju.py JupyniumBuffer """ assert start_cell_idx <= end_cell_idx < self.num_cells @@ -483,6 +488,8 @@ def num_cells(self): @property def num_cells_in_notebook(self): """ + Get the number of cells in the notebook. + If the buffer has 1 cell (no separator), it will be treated as markdown file. If the buffer has more than 1 cell, it will be treated as notebook. diff --git a/src/jupynium/cmds/ipynb2jupy.py b/src/jupynium/cmds/ipynb2jupy.py index 4198e86..1418f5e 100644 --- a/src/jupynium/cmds/ipynb2jupy.py +++ b/src/jupynium/cmds/ipynb2jupy.py @@ -1,4 +1,5 @@ #!/use/bin/env python3 +# ruff: noqa: T201 from __future__ import annotations import argparse @@ -54,7 +55,7 @@ def main(): os.makedirs(os.path.dirname(os.path.realpath(output_jupy_path)), exist_ok=True) if os.path.isfile(output_jupy_path) and not args.yes: - print("Do you want to overwrite {}?".format(output_jupy_path)) + print(f"Do you want to overwrite {output_jupy_path}?") answer = input("y/n: ") if answer != "y": print("Aborted") @@ -65,7 +66,7 @@ def main(): f.write(line) f.write("\n") - print('Converted "{}" to "{}"'.format(args.ipynb_path, output_jupy_path)) + print(f'Converted "{args.ipynb_path}" to "{output_jupy_path}"') if __name__ == "__main__": diff --git a/src/jupynium/cmds/ipynb2jupytext.py b/src/jupynium/cmds/ipynb2jupytext.py index 552a82f..3c9c5ae 100644 --- a/src/jupynium/cmds/ipynb2jupytext.py +++ b/src/jupynium/cmds/ipynb2jupytext.py @@ -1,4 +1,5 @@ #!/use/bin/env python3 +# ruff: noqa: T201 from __future__ import annotations import argparse @@ -56,7 +57,7 @@ def main(): os.makedirs(os.path.dirname(os.path.realpath(output_jupy_path)), exist_ok=True) if os.path.isfile(output_jupy_path) and not args.yes: - print("Do you want to overwrite {}?".format(output_jupy_path)) + print(f"Do you want to overwrite {output_jupy_path}?") answer = input("y/n: ") if answer != "y": print("Aborted") @@ -67,7 +68,7 @@ def main(): f.write(line) f.write("\n") - print('Converted "{}" to "{}"'.format(args.ipynb_path, output_jupy_path)) + print(f'Converted "{args.ipynb_path}" to "{output_jupy_path}"') if __name__ == "__main__": diff --git a/src/jupynium/events_control.py b/src/jupynium/events_control.py index f4353dc..6071312 100644 --- a/src/jupynium/events_control.py +++ b/src/jupynium/events_control.py @@ -5,6 +5,7 @@ import logging import os from dataclasses import dataclass +from os import PathLike from pkg_resources import resource_stream from selenium.common.exceptions import ( @@ -12,6 +13,7 @@ NoSuchElementException, ) from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver from . import selenium_helpers as sele from .buffer import JupyniumBuffer @@ -111,9 +113,7 @@ class PrevLazyArgs: on_lines_args: OnLinesArgs | None = None update_selection_args: UpdateSelectionArgs | None = None - def process( - self, nvim_info: NvimInfo, driver: selenium.webdriver, bufnr: int - ) -> None: + def process(self, nvim_info: NvimInfo, driver: WebDriver, bufnr: int) -> None: if self.on_lines_args is not None: process_on_lines_event(nvim_info, driver, bufnr, self.on_lines_args) self.on_lines_args = None @@ -234,7 +234,8 @@ def start_sync_with_filename( driver, ): """ - Start sync using a filename (not tab index) + Start sync using a filename (not tab index). + filename has to end with .ipynb """ driver.switch_to.window(nvim_info.home_window) @@ -277,7 +278,7 @@ def start_sync_with_filename( if ask: sync_input = nvim_info.nvim.eval( - """input("Press 'v' to sync from n[v]im, 'i' to load from [i]pynb and sync. (v/i/[c]ancel): ")""" # noqa: E501 + """input("Press 'v' to sync from n[v]im, 'i' to load from [i]pynb and sync. (v/i/[c]ancel): ")""" ) sync_input = str(sync_input).strip() else: @@ -329,7 +330,9 @@ def start_sync_with_filename( nvim_info.jupbufs[bufnr].full_sync_to_notebook(driver) -def choose_default_kernel(driver, page_type: str, buf_filetype, conda_or_venv_path): +def choose_default_kernel( + driver: WebDriver, page_type: str, buf_filetype: str, conda_or_venv_path: str | None +): """ Choose kernel based on buffer's filetype and conda env """ @@ -367,10 +370,10 @@ def match_with_path(env_path: str) -> str | None: return kernel_name except (KeyError, IndexError): pass - return + return None if len(valid_kernel_names) == 0: - return + return None elif len(valid_kernel_names) == 1: return valid_kernel_names[0] elif conda_or_venv_path is not None and conda_or_venv_path != "": @@ -398,19 +401,19 @@ def match_with_path(env_path: str) -> str | None: for valid_kernel_name in valid_kernel_names_old: if ( "conda_env_path" - not in kernel_specs[valid_kernel_name]["spec"]["metadata"].keys() + not in kernel_specs[valid_kernel_name]["spec"]["metadata"] ): valid_kernel_names.append(valid_kernel_name) if len(valid_kernel_names) == 0: - return + return None else: return valid_kernel_names[0] - return + return None -def process_request_event(nvim_info: NvimInfo, driver, event): +def process_request_event(nvim_info: NvimInfo, driver: WebDriver, event): """ Returns: status (bool) @@ -550,7 +553,7 @@ def skip_bloated(nvim_info: NvimInfo): nvim_info.nvim.vars["jupynium_message_bloated"] = False logger.info("Reloading all buffers from nvim") - for buf_id in nvim_info.jupbufs.keys(): + for buf_id in nvim_info.jupbufs: nvim_info.nvim.lua.Jupynium_grab_entire_buffer(buf_id) return True @@ -566,7 +569,7 @@ def lazy_on_lines_event( ): """ Lazy-process on_lines events. - ---- + Often, completion plugins like coc.nvim and nvim-cmp spams on_lines events. But they will have the same (bufnr, start_row, old_end_row, new_end_row) values. If the series of line changes are chainable, we can just process the last one. @@ -599,10 +602,9 @@ def process_on_lines_event( ) -# flake8: noqa: C901 def process_notification_event( nvim_info: NvimInfo, - driver, + driver: WebDriver, event, prev_lazy_args_per_buf: PrevLazyArgsPerBuf | None = None, ): @@ -753,14 +755,14 @@ def process_notification_event( # Code from jupyter-kernel.nvim has_experimental_types = ( - "metadata" in reply.keys() - and "_jupyter_types_experimental" in reply["metadata"].keys() + "metadata" in reply + and "_jupyter_types_experimental" in reply["metadata"] ) if has_experimental_types: replies = reply["metadata"]["_jupyter_types_experimental"] matches = [] for match in replies: - if "signature" in match.keys(): + if "signature" in match: matches.append( { "label": match.get("text", ""), @@ -829,7 +831,10 @@ def process_notification_event( def update_cell_selection( - nvim_info: NvimInfo, driver, bufnr, update_selection_args: UpdateSelectionArgs + nvim_info: NvimInfo, + driver: WebDriver, + bufnr: int, + update_selection_args: UpdateSelectionArgs, ): cursor_pos_row, visual_start_row = dataclasses.astuple(update_selection_args) @@ -904,13 +909,19 @@ def update_cell_selection( "jupynium_autoscroll_cell_top_margin_percent", 0 ) driver.execute_script( - "Jupyter.notebook.scroll_cell_percent(arguments[0], arguments[1], 0);", # noqa: E501 + "Jupyter.notebook.scroll_cell_percent(arguments[0], arguments[1], 0);", cell_index, top_margin_percent, ) -def download_ipynb(driver, nvim_info, bufnr, output_ipynb_path): +def download_ipynb( + driver: WebDriver, + nvim_info: NvimInfo, + bufnr: int, + output_ipynb_path: str | PathLike, +): + output_ipynb_path = str(output_ipynb_path) driver.switch_to.window(nvim_info.window_handles[bufnr]) with open(output_ipynb_path, "w") as f: @@ -928,7 +939,7 @@ def download_ipynb(driver, nvim_info, bufnr, output_ipynb_path): logger.info(f"Downloaded ipynb to {output_ipynb_path}") -def scroll_to_cell(driver, nvim_info, bufnr, cursor_pos_row): +def scroll_to_cell(driver: WebDriver, nvim_info: NvimInfo, bufnr: int, cursor_pos_row): # Which cell? cell_index, _, _ = nvim_info.jupbufs[bufnr].get_cell_index_from_row(cursor_pos_row) diff --git a/src/jupynium/ipynb.py b/src/jupynium/ipynb.py index 60e908b..48ba126 100644 --- a/src/jupynium/ipynb.py +++ b/src/jupynium/ipynb.py @@ -1,36 +1,39 @@ from __future__ import annotations import json +from collections.abc import Sequence +from os import PathLike -def load_ipynb(ipynb_path): - with open(ipynb_path, "r") as f: +def load_ipynb(ipynb_path: str | PathLike): + with open(ipynb_path) as f: ipynb = json.load(f) return ipynb -def read_ipynb_texts(ipynb, code_only=False): +def read_ipynb_texts(ipynb, code_only: bool = False): texts = [] cell_types = [] for cell in ipynb["cells"]: - if code_only: - if cell["cell_type"] != "code": - continue + if code_only and cell["cell_type"] != "code": + continue cell_types.append(cell["cell_type"]) texts.append("".join(cell["source"])) return cell_types, texts def ipynb_language(ipynb): - if "metadata" in ipynb: - if "kernelspec" in ipynb["metadata"]: - if "language" in ipynb["metadata"]["kernelspec"]: - return ipynb["metadata"]["kernelspec"]["language"] + if ( + "metadata" in ipynb + and "kernelspec" in ipynb["metadata"] + and "language" in ipynb["metadata"]["kernelspec"] + ): + return ipynb["metadata"]["kernelspec"]["language"] return None -def cells_to_jupy(cell_types, texts): +def cells_to_jupy(cell_types: list[str], texts: list[str]): cell_types_previous = ["code"] + cell_types[:-1] jupy: list[str] = [] @@ -57,7 +60,9 @@ def cells_to_jupy(cell_types, texts): return jupy -def cells_to_jupytext(cell_types, texts, python=True): +def cells_to_jupytext( + cell_types: Sequence[str], texts: Sequence[str], python: bool = True +): jupytext: list[str] = [] for cell_type, text in zip(cell_types, texts): diff --git a/src/jupynium/jupyter_notebook_selenium.py b/src/jupynium/jupyter_notebook_selenium.py index f1c98bd..ded2456 100644 --- a/src/jupynium/jupyter_notebook_selenium.py +++ b/src/jupynium/jupyter_notebook_selenium.py @@ -2,13 +2,15 @@ import logging +from selenium.webdriver.remote.webdriver import WebDriver + logger = logging.getLogger(__name__) -def insert_cell_at(driver, cell_type, cell_idx): +def insert_cell_at(driver: WebDriver, cell_type: str, cell_idx: int): """ - Instead of insert_cell_below or insert_cell_above, - it will select based on the given index. + Instead of insert_cell_below or insert_cell_above, it will select based on the given index. + If cell_idx == 0, insert above, otherwise insert below. """ assert cell_type in ["code", "markdown"] diff --git a/src/jupynium/nvim.py b/src/jupynium/nvim.py index bf2c12f..3eca5fd 100644 --- a/src/jupynium/nvim.py +++ b/src/jupynium/nvim.py @@ -1,9 +1,11 @@ from __future__ import annotations +import contextlib import logging from dataclasses import dataclass, field import pynvim +from selenium.webdriver.remote.webdriver import WebDriver from .buffer import JupyniumBuffer @@ -20,25 +22,27 @@ class NvimInfo: window_handles: dict[int, str] = field(default_factory=dict) # key = buffer ID auto_close_tab: bool = True - def attach_buffer(self, buf_id, content: list[str], window_handle): + def attach_buffer(self, buf_id: int, content: list[str], window_handle: str): if buf_id in self.jupbufs or buf_id in self.window_handles: logger.warning(f"Buffer {buf_id} is already attached") self.jupbufs[buf_id] = JupyniumBuffer(content) self.window_handles[buf_id] = window_handle - def detach_buffer(self, buf_id, driver): + def detach_buffer(self, buf_id: int, driver: WebDriver): if buf_id in self.jupbufs: del self.jupbufs[buf_id] if buf_id in self.window_handles: - if self.auto_close_tab: - if self.window_handles[buf_id] in driver.window_handles: - driver.switch_to.window(self.window_handles[buf_id]) - driver.close() - driver.switch_to.window(self.home_window) + if ( + self.auto_close_tab + and self.window_handles[buf_id] in driver.window_handles + ): + driver.switch_to.window(self.window_handles[buf_id]) + driver.close() + driver.switch_to.window(self.home_window) del self.window_handles[buf_id] - def check_window_alive_and_update(self, driver): + def check_window_alive_and_update(self, driver: WebDriver): detach_buffer_list = [] for buf_id, window in self.window_handles.items(): if window not in driver.window_handles: @@ -55,12 +59,10 @@ def check_window_alive_and_update(self, driver): for buf_id in detach_buffer_list: self.detach_buffer(buf_id, driver) - def close(self, driver): - try: - self.nvim.lua.Jupynium_reset_channel(async_=True) - except Exception: + def close(self, driver: WebDriver): + with contextlib.suppress(Exception): # Even if you fail it's not a big problem - pass + self.nvim.lua.Jupynium_reset_channel(async_=True) for buf_id in list(self.jupbufs.keys()): self.detach_buffer(buf_id, driver) diff --git a/src/jupynium/process.py b/src/jupynium/process.py index 2417731..816f1c4 100644 --- a/src/jupynium/process.py +++ b/src/jupynium/process.py @@ -1,22 +1,25 @@ # https://stackoverflow.com/questions/36799192/check-if-python-script-is-already-running from __future__ import annotations -from os import getpid -from os.path import exists +from os import PathLike, getpid +from pathlib import Path from psutil import Process, pid_exists from .definitions import jupynium_pid_path -def already_running_pid(name="jupynium", pid_path=jupynium_pid_path): +def already_running_pid( + name: str = "jupynium", pid_path: str | PathLike = jupynium_pid_path +): + pid_path = Path(pid_path) my_pid = getpid() - if exists(pid_path): + if pid_path.exists(): with open(pid_path) as f: pid = f.read() pid = int(pid) if pid.isnumeric() else None if pid is not None and pid_exists(pid): - if name in "".join(Process(my_pid).cmdline()) and name in "".join( + if name in "".join(Process(my_pid).cmdline()) and name in "".join( # noqa: SIM114 Process(pid).cmdline() ): return pid diff --git a/src/jupynium/pynvim_helpers.py b/src/jupynium/pynvim_helpers.py index 23cf7e7..e08261e 100644 --- a/src/jupynium/pynvim_helpers.py +++ b/src/jupynium/pynvim_helpers.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) -def attach_and_init(nvim_listen_addr): +def attach_and_init(nvim_listen_addr: str): logger.info("nvim addr: %s", nvim_listen_addr) for _ in range(30): try: diff --git a/src/jupynium/selenium_helpers.py b/src/jupynium/selenium_helpers.py index a3a1ec7..1e2b3b4 100644 --- a/src/jupynium/selenium_helpers.py +++ b/src/jupynium/selenium_helpers.py @@ -4,13 +4,14 @@ from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.wait import WebDriverWait logger = logging.getLogger(__name__) -def wait_until_notebook_loaded(driver, timeout=30): +def wait_until_notebook_loaded(driver: WebDriver, timeout: int = 30): """Wait until the Jupyter Notebook is loaded.""" try: WebDriverWait(driver, timeout).until( @@ -41,7 +42,7 @@ def wait_until_notebook_loaded(driver, timeout=30): driver.quit() -def wait_until_notebook_list_loaded(driver, timeout=10): +def wait_until_notebook_list_loaded(driver: WebDriver, timeout: int = 10): """Wait until the Jupyter Notebook home page (list of files) is loaded.""" try: WebDriverWait(driver, timeout).until( @@ -54,7 +55,7 @@ def wait_until_notebook_list_loaded(driver, timeout=10): driver.quit() -def wait_until_loaded(driver, timeout=10): +def wait_until_loaded(driver: WebDriver, timeout: int = 10): """Wait until the page is ready.""" try: WebDriverWait(driver, timeout).until( @@ -65,7 +66,9 @@ def wait_until_loaded(driver, timeout=10): driver.quit() -def wait_until_new_window(driver, current_handles, timeout=10): +def wait_until_new_window( + driver: WebDriver, current_handles: list[str], timeout: int = 10 +): """Wait until the page is ready.""" try: WebDriverWait(driver, timeout).until(EC.new_window_is_opened(current_handles)) @@ -74,7 +77,7 @@ def wait_until_new_window(driver, current_handles, timeout=10): driver.quit() -def is_browser_disconnected(driver): +def is_browser_disconnected(driver: WebDriver): """Check if the browser is disconnected.""" try: _ = driver.window_handles From ad052972b75b5e769beeef8eed05b49ad6083d3d Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 5 Jun 2024 02:38:58 +0900 Subject: [PATCH 05/15] refactor!: remove deprecated markdown cell. markdown (jupytext) are renamed to markdown --- lua/jupynium/cells.lua | 17 ++------ pyproject.toml | 1 - src/jupynium/buffer.py | 58 +++++++------------------- src/jupynium/cmds/ipynb2jupy.py | 73 --------------------------------- tests/test_buffer.py | 38 ++++++----------- 5 files changed, 32 insertions(+), 155 deletions(-) delete mode 100644 src/jupynium/cmds/ipynb2jupy.py diff --git a/lua/jupynium/cells.lua b/lua/jupynium/cells.lua index f880193..047e042 100644 --- a/lua/jupynium/cells.lua +++ b/lua/jupynium/cells.lua @@ -4,22 +4,16 @@ local M = {} --- Get the line type (cell separator, magic commands, empty, others) ---@param line string | number 1-indexed ----@return string "cell separator: markdown" | "cell separator: markdown (jupytext)" | "cell separator: code" | "magic commands" | "empty" | "others" +---@return string "cell separator: markdown" | "cell separator: code" | "magic commands" | "empty" | "others" function M.line_type(line) if type(line) == "number" then line = vim.api.nvim_buf_get_lines(0, line - 1, line, false)[1] end - if utils.string_begins_with(line, "# %%%") then + if vim.startswith(line, "# %% [md]") or vim.startswith(line, "# %% [markdown]") then return "cell separator: markdown" - elseif utils.string_begins_with(line, '"""%%') or utils.string_begins_with(line, "'''%%") then - return "cell separator: markdown (string)" - elseif utils.string_begins_with(line, "# %% [md]") or utils.string_begins_with(line, "# %% [markdown]") then - return "cell separator: markdown (jupytext)" elseif vim.fn.trim(line) == "# %%" then return "cell separator: code" - elseif utils.string_begins_with(line, '%%"""') or utils.string_begins_with(line, "%%'''") then - return "cell separator: code (string)" elseif utils.string_begins_with(line, "# ---") then return "metadata" elseif utils.string_begins_with(line, "# %") then @@ -57,13 +51,10 @@ function M.line_types_entire_buf(bufnr) local line_type = M.line_type(line) if line_type == "others" or line_type == "empty" then line_types[i] = "cell content: " .. current_cell_type - elseif line_type == "cell separator: markdown (jupytext)" then - current_cell_type = "markdown (jupytext)" - line_types[i] = line_type - elseif utils.string_begins_with(line_type, "cell separator: markdown") then + elseif vim.startswith(line_type, "cell separator: markdown") then current_cell_type = "markdown" line_types[i] = line_type - elseif utils.string_begins_with(line_type, "cell separator: code") then + elseif vim.startswith(line_type, "cell separator: code") then current_cell_type = "code" line_types[i] = line_type else diff --git a/pyproject.toml b/pyproject.toml index b794d8e..5cfeb79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,6 @@ where = ["src"] [project.scripts] jupynium = "jupynium.cmds.jupynium:main" -ipynb2jupy = "jupynium.cmds.ipynb2jupy:main" ipynb2jupytext = "jupynium.cmds.ipynb2jupytext:main" [tool.setuptools_scm] diff --git a/src/jupynium/buffer.py b/src/jupynium/buffer.py index 73ba4ba..c6cc46f 100644 --- a/src/jupynium/buffer.py +++ b/src/jupynium/buffer.py @@ -15,22 +15,6 @@ ) -def _process_cell_type(cell_type: str) -> str: - if cell_type == "markdown (jupytext)": - return "markdown" - - return cell_type - - -def _process_cell_types(cell_types: list[str]) -> list[str]: - """ - Return the cell types, e.g. ["markdown", "code", "header"]. - - markdown (jupytext) is converted to markdown. - """ - return [_process_cell_type(cell_type) for cell_type in cell_types] - - class JupyniumBuffer: """ Deal with the Nvim buffer and its cell information. @@ -85,15 +69,11 @@ def full_analyse_buf(self, header_cell_type: str = "header"): num_rows_per_cell = [] cell_types = [header_cell_type] for row, line in enumerate(self.buf): - if line.startswith(("# %%%", '"""%%', "'''%%")): + if line.startswith(("# %% [md]", "# %% [markdown]")): num_rows_per_cell.append(num_rows_this_cell) num_rows_this_cell = 1 cell_types.append("markdown") - elif line.startswith(("# %% [md]", "# %% [markdown]")): - num_rows_per_cell.append(num_rows_this_cell) - num_rows_this_cell = 1 - cell_types.append("markdown (jupytext)") - elif line.strip() == "# %%" or line.startswith(('%%"""', "%%'''")): + elif line.strip() == "# %%": num_rows_per_cell.append(num_rows_this_cell) num_rows_this_cell = 1 cell_types.append("code") @@ -112,7 +92,7 @@ def _process_cell_text(self, cell_type: str, lines: list[str]): return "\n".join( line[2:] if line.startswith("# %") else line for line in lines ) - elif cell_type == "markdown (jupytext)": + elif cell_type == "markdown": if len(lines) > 0 and lines[0] == '"""': return "\n".join(line for line in lines if not line.startswith('"""')) else: @@ -120,7 +100,7 @@ def _process_cell_text(self, cell_type: str, lines: list[str]): line[2:] if line.startswith("# ") else line for line in lines ) else: - # header, markdown + # header return "\n".join(lines) def get_cells_text( @@ -257,22 +237,18 @@ def _on_lines_update_buf(self, lines, start_row, old_end_row, new_end_row): ( "cell_type", cell_idx + 1, - _process_cell_types( - new_lines_buf.cell_types[ - 1 : 1 + len(notebook_cell_delete_operations) - ] - ), + new_lines_buf.cell_types[ + 1 : 1 + len(notebook_cell_delete_operations) + ], ) ] notebook_cell_operations.append( ( "insert", cell_idx + 1, - _process_cell_types( - new_lines_buf.cell_types[ - 1 + len(notebook_cell_delete_operations) : - ] - ), + new_lines_buf.cell_types[ + 1 + len(notebook_cell_delete_operations) : + ], ) ) else: @@ -280,7 +256,7 @@ def _on_lines_update_buf(self, lines, start_row, old_end_row, new_end_row): ( "cell_type", cell_idx + 1, - _process_cell_types(new_lines_buf.cell_types[1:]), + new_lines_buf.cell_types[1:], ) ] @@ -334,7 +310,7 @@ def _apply_cell_operations( f"Cell {nb_cell_idx + i} type change to {cell_type} " "from Notebook" ) - # "markdown" or "markdown (jupytext)" + # "markdown" if cell_type == "markdown": driver.execute_script( "Jupyter.notebook.cells_to_markdown([arguments[0]]);", @@ -369,7 +345,7 @@ def get_cell_index_from_row( int: cell index int: cell start row int: row index within the cell - """ # noqa: E501 + """ if num_rows_per_cell is None: num_rows_per_cell = self.num_rows_per_cell @@ -390,10 +366,7 @@ def _check_validity(self): assert len(self.buf) == sum(self.num_rows_per_cell) assert len(self.cell_types) == len(self.num_rows_per_cell) assert self.cell_types[0] == "header" - assert all( - x in ("code", "markdown", "markdown (jupytext)") - for x in self.cell_types[1:] - ) + assert all(x in ("code", "markdown") for x in self.cell_types[1:]) def _partial_sync_to_notebook( self, driver, start_cell_idx, end_cell_idx, strip=True @@ -439,8 +412,7 @@ def _partial_sync_to_notebook( for i, cell_type in enumerate( self.cell_types[start_cell_idx : end_cell_idx + 1] ) - if cell_type.startswith("markdown") - # "markdown" or "markdown (jupytext)" + if cell_type == "markdown" ] if len(code_cell_indices) > 0: diff --git a/src/jupynium/cmds/ipynb2jupy.py b/src/jupynium/cmds/ipynb2jupy.py deleted file mode 100644 index 1418f5e..0000000 --- a/src/jupynium/cmds/ipynb2jupy.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/use/bin/env python3 -# ruff: noqa: T201 -from __future__ import annotations - -import argparse -import os - -from ..ipynb import ipynb2jupy, load_ipynb - - -def get_parser(): - parser = argparse.ArgumentParser( - description="Convert ipynb to a jupynium file (.ju.py)." - "Deprecated: use ipynb2jupytext instead.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - parser.add_argument("ipynb_path", help="Path to ipynb file") - parser.add_argument( - "output_jupy_path", - nargs="?", - help="Path to output jupynium file. " - "If not specified, use file name of ipynb file or print to stdout (--stdout)", - ) - parser.add_argument( - "-y", "--yes", action="store_true", help="Do not ask for confirmation" - ) - parser.add_argument("-s", "--stdout", action="store_true", help="Print to stdout") - return parser - - -def check_args(args, parser): - if args.stdout and args.yes: - parser.error("Either one of --stdout or --yes can be specified") - - if args.output_jupy_path is not None and args.stdout: - parser.error("Either one of --stdout or output_jupy_path can be specified") - - -def main(): - parser = get_parser() - args = parser.parse_args() - check_args(args, parser) - - ipynb = load_ipynb(args.ipynb_path) - jupy = ipynb2jupy(ipynb) - - if args.stdout: - for line in jupy: - print(line) - else: - output_jupy_path = args.output_jupy_path - if output_jupy_path is None: - output_jupy_path = os.path.splitext(args.ipynb_path)[0] + ".ju.py" - - os.makedirs(os.path.dirname(os.path.realpath(output_jupy_path)), exist_ok=True) - - if os.path.isfile(output_jupy_path) and not args.yes: - print(f"Do you want to overwrite {output_jupy_path}?") - answer = input("y/n: ") - if answer != "y": - print("Aborted") - return - - with open(output_jupy_path, "w") as f: - for line in jupy: - f.write(line) - f.write("\n") - - print(f'Converted "{args.ipynb_path}" to "{output_jupy_path}"') - - -if __name__ == "__main__": - main() diff --git a/tests/test_buffer.py b/tests/test_buffer.py index 3ac4add..c688f79 100644 --- a/tests/test_buffer.py +++ b/tests/test_buffer.py @@ -13,8 +13,7 @@ def test_buffer_1(): def test_magic_command_1(): """ - Everything else except magic commands should be preserved after __init__() - or fully_analysed_buf() + Everything else except magic commands should be preserved after __init__() or fully_analysed_buf(). """ lines = ["a", "b", "c", "# %%", "# %time", "e", "f"] buffer = JupyniumBuffer(lines) @@ -23,25 +22,14 @@ def test_magic_command_1(): def test_buffer_markdown(): - buffer = JupyniumBuffer(["a", "b", "c", "# %%%", "d", "# %%", "f"]) - assert buffer.num_rows_per_cell == [3, 2, 2] - assert buffer.cell_types == ["header", "markdown", "code"] - - -def test_buffer_markdown_2(jupbuf1): - assert jupbuf1.num_rows_per_cell == [3, 2, 2] - assert jupbuf1.cell_types == ["header", "markdown", "code"] - - -def test_buffer_markdown_jupytext(): buffer = JupyniumBuffer(["a", "b", "c", "# %% [md]", "d", "# %%", "f"]) assert buffer.num_rows_per_cell == [3, 2, 2] - assert buffer.cell_types == ["header", "markdown (jupytext)", "code"] + assert buffer.cell_types == ["header", "markdown", "code"] md_cell_content = buffer.get_cell_text(1) assert md_cell_content == "d" -def test_buffer_markdown_jupytext_2(): +def test_buffer_markdown_2(): buffer = JupyniumBuffer( [ "a", @@ -56,7 +44,7 @@ def test_buffer_markdown_jupytext_2(): ] ) assert buffer.num_rows_per_cell == [3, 4, 2] - assert buffer.cell_types == ["header", "markdown (jupytext)", "code"] + assert buffer.cell_types == ["header", "markdown", "code"] header_cell_content = buffer.get_cell_text(0) md_cell_content = buffer.get_cell_text(1) @@ -64,7 +52,7 @@ def test_buffer_markdown_jupytext_2(): assert md_cell_content == "# header\ncontent\nnoescape" -def test_buffer_markdown_jupytext_3(): +def test_buffer_markdown_3(): buffer = JupyniumBuffer( [ "a", @@ -81,7 +69,7 @@ def test_buffer_markdown_jupytext_3(): ] ) assert buffer.num_rows_per_cell == [3, 6, 2] - assert buffer.cell_types == ["header", "markdown (jupytext)", "code"] + assert buffer.cell_types == ["header", "markdown", "code"] header_cell_content = buffer.get_cell_text(0) md_cell_content = buffer.get_cell_text(1, strip=True) @@ -89,7 +77,7 @@ def test_buffer_markdown_jupytext_3(): assert md_cell_content == "# # header\n# content\nnoescape" -def test_buffer_markdown_jupytext_inject(): +def test_buffer_markdown_inject(): buffer = JupyniumBuffer( [ "a", @@ -102,10 +90,10 @@ def test_buffer_markdown_jupytext_inject(): "# %%", "f", ], - "markdown (jupytext)", + "markdown", ) assert buffer.num_rows_per_cell == [3, 4, 2] - assert buffer.cell_types == ["markdown (jupytext)", "markdown (jupytext)", "code"] + assert buffer.cell_types == ["markdown", "markdown", "code"] header_cell_content = buffer.get_cell_text(0) md_cell_content = buffer.get_cell_text(1) @@ -113,7 +101,7 @@ def test_buffer_markdown_jupytext_inject(): assert md_cell_content == "# header\ncontent\nnoescape" -def test_buffer_markdown_jupytext_inject_2(): +def test_buffer_markdown_inject_2(): buffer = JupyniumBuffer( [ "a", @@ -129,7 +117,7 @@ def test_buffer_markdown_jupytext_inject_2(): "markdown", ) assert buffer.num_rows_per_cell == [3, 4, 2] - assert buffer.cell_types == ["markdown", "markdown (jupytext)", "code"] + assert buffer.cell_types == ["markdown", "markdown", "code"] header_cell_content = buffer.get_cell_text(0) md_cell_content = buffer.get_cell_text(1) @@ -137,7 +125,7 @@ def test_buffer_markdown_jupytext_inject_2(): assert md_cell_content == "# header\ncontent\nnoescape" -def test_buffer_markdown_jupytext_inject_3(): +def test_buffer_markdown_inject_3(): buffer = JupyniumBuffer( [ "a", @@ -153,7 +141,7 @@ def test_buffer_markdown_jupytext_inject_3(): "code", ) assert buffer.num_rows_per_cell == [3, 4, 2] - assert buffer.cell_types == ["code", "markdown (jupytext)", "code"] + assert buffer.cell_types == ["code", "markdown", "code"] header_cell_content = buffer.get_cell_text(0) md_cell_content = buffer.get_cell_text(1) From 24be71aab65d30dc6dc0632477913575765c2739 Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 5 Jun 2024 02:43:56 +0900 Subject: [PATCH 06/15] test: fix --- tests/test_buffer.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tests/test_buffer.py b/tests/test_buffer.py index c688f79..09077c0 100644 --- a/tests/test_buffer.py +++ b/tests/test_buffer.py @@ -102,30 +102,6 @@ def test_buffer_markdown_inject(): def test_buffer_markdown_inject_2(): - buffer = JupyniumBuffer( - [ - "a", - "# b", - "# # c", - "# %% [markdown]", - "# # header", - "# content", - "noescape", - "# %%", - "f", - ], - "markdown", - ) - assert buffer.num_rows_per_cell == [3, 4, 2] - assert buffer.cell_types == ["markdown", "markdown", "code"] - - header_cell_content = buffer.get_cell_text(0) - md_cell_content = buffer.get_cell_text(1) - assert header_cell_content == "a\n# b\n# # c" - assert md_cell_content == "# header\ncontent\nnoescape" - - -def test_buffer_markdown_inject_3(): buffer = JupyniumBuffer( [ "a", From 5d1636ac5406af75d63b6d33f9a3f07775cb345c Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 5 Jun 2024 02:51:34 +0900 Subject: [PATCH 07/15] test: fix --- tests/conftest.py | 2 +- tests/test_buffer.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ff41756..688e883 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ @pytest.fixture(scope="session") def jupbuf1(): - return JupyniumBuffer(["a", "b", "c", "'''%%%", "d", "%%'''", "f"]) + return JupyniumBuffer(["a", "b", "c", "# %% [markdown]", "# d", "# %%", "f"]) @pytest.fixture(scope="session") diff --git a/tests/test_buffer.py b/tests/test_buffer.py index 09077c0..57276cf 100644 --- a/tests/test_buffer.py +++ b/tests/test_buffer.py @@ -147,7 +147,7 @@ def test_check_validity(jupbuf1): @pytest.mark.xfail(raises=Exception) def test_check_invalid(): - buffer = JupyniumBuffer(["a", "b", "c", "'''%%%", "d", "%%'''", "f"]) + buffer = JupyniumBuffer(["a", "b", "c", "# %% [markdown]", "d", "# %%", "f"]) # manually modify the buffer buffer.buf.append("g") buffer._check_validity() @@ -167,8 +167,8 @@ def test_num_cells_2(): @pytest.mark.parametrize( "content,lines,start_row,old_end_row,new_end_row", [ - (["a", "b", "c", "# %%", "d", "e", "f"], ["# %%%", "g"], 3, 4, 5), - (["b", "c", "# %%", "d", "e", "f"], ["# %%%", "g"], 3, 4, 5), + (["a", "b", "c", "# %%", "d", "e", "f"], ["# %% [md]", "g"], 3, 4, 5), + (["b", "c", "# %%", "d", "e", "f"], ["# %% [markdown]", "g"], 3, 4, 5), (["b", "# %%", "d", "f", "f", "f", "f"], ["# %%"], 3, 4, 4), (["b", "# %%", "d", "f", "f", "f", "f"], ["# %%"], 3, 3, 4), (["b", "# %%", "d", "# %%", "f", "f", "f"], [""], 1, 4, 2), @@ -191,19 +191,19 @@ def test_on_lines_cellinfo(content, lines, start_row, old_end_row, new_end_row): [ ( ["a", "b", "c", "# %%", "d", "e", "f"], - ["# %%%", "g"], + ["# %% [md]", "g"], 3, 4, 5, - ["a", "b", "c", "# %%%", "g", "d", "e", "f"], + ["a", "b", "c", "# %% [md]", "g", "d", "e", "f"], ), ( ["b", "c", "# %%", "d", "e", "f"], - ["# %%%", "g"], + ["# %% [markdown]", "g"], 3, 4, 5, - ["b", "c", "# %%", "# %%%", "g", "e", "f"], + ["b", "c", "# %%", "# %% [markdown]", "g", "e", "f"], ), ( ["b", "# %%", "d", "f", "f", "f", "f"], From 6f1f59adcd278d6a1088854a319756cf4e0e10db Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 5 Jun 2024 10:51:45 +0900 Subject: [PATCH 08/15] feat: hide migration banner --- pyproject.toml | 26 +++++++++++++++++++++++++- src/jupynium/cmds/jupynium.py | 1 + tox.ini | 20 -------------------- 3 files changed, 26 insertions(+), 21 deletions(-) delete mode 100644 tox.ini diff --git a/pyproject.toml b/pyproject.toml index 5cfeb79..41f554a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,30 @@ omit = [ # OPTIONALLY ADD MORE LATER ] +[tool.tox] +legacy_tox_ini = """ + [tox] + minversion = 3.24.0 + envlist = python3.8, python3.9, python3.10, python3.11, python3.12 + isolated_build = true + + [gh-actions] + python = + 3.8: python3.8 + 3.9: python3.9 + 3.10: python3.10 + 3.11: python3.11 + 3.11: python3.12 + + [testenv] + setenv = + PYTHONPATH = {toxinidir} + deps = + -r{toxinidir}/deps/x86_64-unknown-linux-gnu/requirements_dev.txt + commands = + pytest --basetemp={envtmpdir} +""" + [tool.ruff] target-version = "py38" src = ["src"] # for ruff isort @@ -140,4 +164,4 @@ reportUnusedVariable = false reportUndefinedVariable = false # ruff handles this with F821 pythonVersion = "3.8" -pythonPlatform = "Linux" +# pythonPlatform = "Linux" diff --git a/src/jupynium/cmds/jupynium.py b/src/jupynium/cmds/jupynium.py index 44f18f5..b5b74e8 100644 --- a/src/jupynium/cmds/jupynium.py +++ b/src/jupynium/cmds/jupynium.py @@ -380,6 +380,7 @@ def fallback_open_notebook_server( "--no-browser", "--NotebookApp.token", notebook_token, + "--NotebookApp.show_banner=False", ] if notebook_dir is not None: diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 7c03fb4..0000000 --- a/tox.ini +++ /dev/null @@ -1,20 +0,0 @@ -[tox] -minversion = 3.24.0 -envlist = python3.8, python3.9, python3.10, python3.11, python3.12 -isolated_build = true - -[gh-actions] -python = - 3.8: python3.8 - 3.9: python3.9 - 3.10: python3.10 - 3.11: python3.11 - 3.11: python3.12 - -[testenv] -setenv = - PYTHONPATH = {toxinidir} -deps = - -r{toxinidir}/deps/x86_64-unknown-linux-gnu/requirements_dev.txt -commands = - pytest --basetemp={envtmpdir} From 1c3b1ac8637484230f17af1bde9fe976164dcfe9 Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 5 Jun 2024 16:25:00 +0900 Subject: [PATCH 09/15] feat: auto detect python version --- scripts/compile_requirements.sh | 3 ++- scripts/get_python_version.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 scripts/get_python_version.py diff --git a/scripts/compile_requirements.sh b/scripts/compile_requirements.sh index b770cc6..6677190 100644 --- a/scripts/compile_requirements.sh +++ b/scripts/compile_requirements.sh @@ -6,7 +6,6 @@ # If not, it skips the file rather than recompiling it (which may change version unnecessarily often) TARGET_PLATFORMS=(x86_64-unknown-linux-gnu aarch64-apple-darwin x86_64-apple-darwin x86_64-pc-windows-msvc) -PYTHON_VERSION=3.8 if ! command -v uv &> /dev/null; then echo "uv is not installed. Please run 'pip3 install --user uv'" >&2 @@ -25,6 +24,8 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) # To simplify the directory (using relative paths), we change the working directory. cd "$SCRIPT_DIR/../deps" || { echo "Failure"; exit 1; } +PYTHON_VERSION=$(python3 "$SCRIPT_DIR/get_python_version.py") + for platform in "${TARGET_PLATFORMS[@]}"; do mkdir -p "$platform" done diff --git a/scripts/get_python_version.py b/scripts/get_python_version.py new file mode 100644 index 0000000..78f238b --- /dev/null +++ b/scripts/get_python_version.py @@ -0,0 +1,31 @@ +""" +Get minimum python version from pyproject.toml. + +Note: + It only works if the format is like this: ">=3.11", ">=3.11,<3.12" +""" + +from pathlib import Path + +pyproject_toml_path = Path(__file__).parent.parent / "pyproject.toml" + +try: + import toml + + pyproject = toml.load(pyproject_toml_path) + version_range = pyproject["project"]["requires-python"] +except ImportError: + # alternatively, search for requires-python in pyproject.toml + with open(pyproject_toml_path) as f: + for line in f: + if line.startswith("requires-python"): + version_range = line.replace("requires-python", "").strip(" ='\"") + break + else: + raise ValueError("requires-python not found in pyproject.toml") + + +# get minimum python version +# it has a format like this: ">=3.6", ">=3.7,<3.8" +min_version = version_range.split(",")[0].replace(">=", "") +print(min_version) # noqa: T201 From ff5040b47416e7729684b8585a33a814d8340978 Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 5 Jun 2024 17:18:58 +0900 Subject: [PATCH 10/15] ci: apply styles --- .github/workflows/apply-styles.yml | 35 ++++++++++++++++++++++++++++++ scripts/get_python_version.py | 1 + 2 files changed, 36 insertions(+) create mode 100644 .github/workflows/apply-styles.yml diff --git a/.github/workflows/apply-styles.yml b/.github/workflows/apply-styles.yml new file mode 100644 index 0000000..c7f10f1 --- /dev/null +++ b/.github/workflows/apply-styles.yml @@ -0,0 +1,35 @@ +name: Apply ruff format, isort, and fixes + +on: + workflow_dispatch: + inputs: + ruff_select: + description: 'ruff select' + default: I,D20,D21,UP00,UP032,UP034 + ruff_ignore: + description: 'ruff ignore' + default: D212 + +jobs: + apply-ruff: + name: Apply ruff + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + - name: Install ruff + run: | + pip3 install -r <(grep '^ruff==' deps/x86_64-unknown-linux-gnu/requirements_dev.txt) + - name: Run ruff and push + run: | + set +e # Do not exit shell on ruff failure + ruff --select=${{ github.event.inputs.ruff_select }} --ignore=${{ github.event.inputs.ruff_ignore }} --fix --unsafe-fixes . + ruff format . + git config user.name github-actions[bot] + git config user.email github-actions[bot]@users.noreply.github.com + git add . + git commit -m "style: ruff format, isort, fixes [skip ci]" + git push diff --git a/scripts/get_python_version.py b/scripts/get_python_version.py index 78f238b..5867065 100644 --- a/scripts/get_python_version.py +++ b/scripts/get_python_version.py @@ -4,6 +4,7 @@ Note: It only works if the format is like this: ">=3.11", ">=3.11,<3.12" """ +from __future__ import annotations from pathlib import Path From 0f4645b5e7567559edd751ac91e63dc6085a7b08 Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 5 Jun 2024 17:19:53 +0900 Subject: [PATCH 11/15] style: ruff format --- scripts/get_python_version.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/get_python_version.py b/scripts/get_python_version.py index 5867065..26a2f33 100644 --- a/scripts/get_python_version.py +++ b/scripts/get_python_version.py @@ -4,6 +4,7 @@ Note: It only works if the format is like this: ">=3.11", ">=3.11,<3.12" """ + from __future__ import annotations from pathlib import Path From 8a8c9bb90eb5f8c7cb38b85f1f0227b58f810bd4 Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 5 Jun 2024 18:14:04 +0900 Subject: [PATCH 12/15] style: typing and ruff --- src/jupynium/cmds/jupynium.py | 76 ++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/src/jupynium/cmds/jupynium.py b/src/jupynium/cmds/jupynium.py index b5b74e8..053722b 100644 --- a/src/jupynium/cmds/jupynium.py +++ b/src/jupynium/cmds/jupynium.py @@ -1,4 +1,5 @@ #!/use/bin/env python3 +# ruff: noqa: T201 from __future__ import annotations import argparse @@ -12,7 +13,8 @@ import tempfile import time import traceback -from datetime import datetime +from datetime import datetime, timezone +from os import PathLike from pathlib import Path from urllib.parse import urlparse @@ -23,10 +25,12 @@ import verboselogs from git.exc import InvalidGitRepositoryError, NoSuchPathError from persistqueue.exceptions import Empty +from pynvim import Nvim from selenium import webdriver from selenium.common.exceptions import WebDriverException from selenium.webdriver.firefox.options import Options from selenium.webdriver.firefox.service import Service +from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.wait import WebDriverWait @@ -40,14 +44,16 @@ logger = verboselogs.VerboseLogger(__name__) -SOURCE_DIR = Path(os.path.dirname(os.path.abspath(__file__))) - def webdriver_firefox( - profiles_ini_path="~/.mozilla/firefox/profiles.ini", profile_name=None + profiles_ini_path: str | PathLike | None = "~/.mozilla/firefox/profiles.ini", + profile_name=None, ): """ + Get a Firefox webdriver with a specific profile. + profiles.ini path is used to remember the last session (password, etc.) + Args: profiles_ini_path: Path to profiles.ini profile_name: Profile name in profiles.ini. If None, use the default profile. @@ -157,7 +163,7 @@ def get_parser(): ) parser.add_argument( "--firefox_profiles_ini_path", - help="Path to firefox profiles.ini which will be used to remember the last session (password, etc.)\n" # noqa: E501 + help="Path to firefox profiles.ini which will be used to remember the last session (password, etc.)\n" "Example path:\n" "~/.mozilla/firefox/profiles.ini\n" "~/snap/firefox/common/.mozilla/firefox/profiles.ini", @@ -178,8 +184,8 @@ def get_parser(): parser.add_argument( "--notebook_dir", type=str, - help="When jupyter notebook has started using --jupyter_command, the root dir will be this.\n" # noqa: E501 - "If None, open at a git dir of nvim's buffer path and still navigate to the buffer dir.\n" # noqa: E501 + help="When jupyter notebook has started using --jupyter_command, the root dir will be this.\n" + "If None, open at a git dir of nvim's buffer path and still navigate to the buffer dir.\n" "(e.g. localhost:8888/nbclassic/tree/path/to/buffer)", ) parser.add_argument( @@ -232,6 +238,7 @@ def start_if_running_else_clear(args, q: persistqueue.UniqueQ): def number_of_windows_be_list(num_windows: list[int]): """ An expectation for the number of windows to be one of the listed values. + Slightly modified from EC.number_of_windows_to_be(num_windows). """ @@ -245,7 +252,7 @@ def attach_new_neovim( driver, new_args, nvims: dict[str, NvimInfo], - URL_to_home_windows: dict[str, str], + url_to_home_windows: dict[str, str], ): logger.info(f"New nvim wants to attach: {new_args}") if new_args.nvim_listen_addr in nvims: @@ -253,8 +260,8 @@ def attach_new_neovim( else: try: nvim = attach_and_init(new_args.nvim_listen_addr) - if new_args.notebook_URL in URL_to_home_windows.keys(): - home_window = URL_to_home_windows[new_args.notebook_URL] + if new_args.notebook_URL in url_to_home_windows: + home_window = url_to_home_windows[new_args.notebook_URL] else: prev_num_windows = len(driver.window_handles) driver.switch_to.new_window("tab") @@ -266,7 +273,7 @@ def attach_new_neovim( sele.wait_until_loaded(driver) home_window = driver.current_window_handle - URL_to_home_windows[new_args.notebook_URL] = home_window + url_to_home_windows[new_args.notebook_URL] = home_window nvim_info = NvimInfo( nvim, home_window, auto_close_tab=not new_args.no_auto_close_tab @@ -291,17 +298,17 @@ def generate_notebook_token(): return secrets.token_urlsafe(16) -def exception_no_notebook(notebook_URL, nvim): +def exception_no_notebook(notebook_url: str, nvim: Nvim | None): logger.exception( "Exception occurred. " - f"Are you sure you're running Jupyter Notebook at {notebook_URL}? " + f"Are you sure you're running Jupyter Notebook at {notebook_url}? " "Use --jupyter_command to specify the command to start Jupyter Notebook." ) if nvim is not None: nvim.lua.Jupynium_notify.error( [ "Can't connect to Jupyter Notebook.", - f"Are you sure you're running Jupyter Notebook at {notebook_URL}?", + f"Are you sure you're running Jupyter Notebook at {notebook_url}?", "Use jupyter_command to specify the command to start Jupyter Notebook.", ], ) @@ -321,9 +328,10 @@ def kill_child_processes(parent_pid, sig=signal.SIGTERM): psutil.wait_procs(children, timeout=3) -def kill_notebook_proc(notebook_proc): +def kill_notebook_proc(notebook_proc: subprocess.Popen | None): """ Kill the notebook process. + Used if we opened a Jupyter Notebook server using the --jupyter_command and when no server is running. """ @@ -347,11 +355,21 @@ def kill_notebook_proc(notebook_proc): def fallback_open_notebook_server( - notebook_port, notebook_url_path, jupyter_command, notebook_dir, nvim, driver + notebook_port: int, + notebook_url_path: str, + jupyter_command, + notebook_dir: str | PathLike | None, + nvim: Nvim | None, + driver: WebDriver, ): """ + After firefox failing to try to connect to Notebook, open the Notebook server and try again. + Args: - notebook_url_path (str): e.g. "/nbclassic" + notebook_url_path: e.g. "/nbclassic" + + Returns: + notebook_proc: subprocess.Popen object """ # Fallback: if the URL is localhost and if selenium can't connect, # open the Jupyter Notebook server and even start syncing. @@ -362,7 +380,7 @@ def fallback_open_notebook_server( if nvim is not None: # Root dir of the notebook is either the buffer's dir or the git dir. buffer_path = str(nvim.eval("expand('%:p')")) - buffer_dir = os.path.dirname(buffer_path) + buffer_dir = Path(buffer_path).parent try: repo = git.Repo(buffer_dir, search_parent_directories=True) notebook_dir = repo.working_tree_dir @@ -387,6 +405,7 @@ def fallback_open_notebook_server( # notebook_args += [f"--ServerApp.root_dir={root_dir}"] notebook_args += ["--NotebookApp.notebook_dir", notebook_dir] + notebook_proc = None try: # strip commands because we need to escape args with dashes. # e.g. --jupyter_command conda run ' --no-capture-output' ' -n' env_name jupyter @@ -405,6 +424,8 @@ def fallback_open_notebook_server( # Command doesn't exist exception_no_notebook(f"localhost:{notebook_port}{notebook_url_path}", nvim) + assert notebook_proc is not None + time.sleep(1) for _ in range(20): try: @@ -429,14 +450,13 @@ def fallback_open_notebook_server( return notebook_proc -# flake8: noqa: C901 def main(): # Initialise with NOTSET level and null device, and add stream handler separately. # This way, the root logging level is NOTSET (log all), # and we can customise each handler's behaviour. # If we set the level during the initialisation, it will affect to ALL streams, # so the file stream cannot be more verbose (lower level) than the console stream. - coloredlogs.install(fmt="", level=logging.NOTSET, stream=open(os.devnull, "w")) + coloredlogs.install(fmt="", level=logging.NOTSET, stream=open(os.devnull, "w")) # noqa: SIM115 console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) @@ -445,9 +465,9 @@ def main(): ) console_handler.setFormatter(console_format) - tmp_log_dir = os.path.join(tempfile.gettempdir(), "jupynium", "logs") - os.makedirs(tmp_log_dir, exist_ok=True) - log_path = os.path.join(tmp_log_dir, f"{datetime.now():%Y-%m-%d_%H-%M-%S}.log") + tmp_log_dir = Path(tempfile.gettempdir()) / "jupynium" / "logs" + tmp_log_dir.mkdir(parents=True, exist_ok=True) + log_path = tmp_log_dir / f"{datetime.now(tz=timezone.utc):%Y-%m-%d_%H-%M-%S}.log" f_handler = logging.FileHandler(log_path) f_handler.setLevel(logging.INFO) @@ -504,10 +524,10 @@ def main(): try: driver.get(args.notebook_URL) except WebDriverException: - notebook_URL = args.notebook_URL + notebook_url = args.notebook_URL if "://" not in args.notebook_URL: - notebook_URL = "http://" + notebook_URL - url = urlparse(notebook_URL) + notebook_url = "http://" + notebook_url + url = urlparse(notebook_url) if url.port is not None and url.hostname in ["localhost", "127.0.0.1"]: notebook_proc = fallback_open_notebook_server( url.port, @@ -536,7 +556,7 @@ def main(): home_window = driver.current_window_handle - URL_to_home_windows = {args.notebook_URL: home_window} + url_to_home_windows = {args.notebook_URL: home_window} if args.nvim_listen_addr is not None and nvim is not None: nvims = { args.nvim_listen_addr: NvimInfo( @@ -584,7 +604,7 @@ def main(): except Empty: pass else: - attach_new_neovim(driver, new_args, nvims, URL_to_home_windows) + attach_new_neovim(driver, new_args, nvims, url_to_home_windows) time.sleep(args.sleep_time_idle) except WebDriverException: From b8487a2d128ca5c733a96199502225a5e1d5809b Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 5 Jun 2024 18:22:29 +0900 Subject: [PATCH 13/15] chore: remove unnecessary luacheck config --- .luacheckrc | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 .luacheckrc diff --git a/.luacheckrc b/.luacheckrc deleted file mode 100644 index 35aedcd..0000000 --- a/.luacheckrc +++ /dev/null @@ -1,21 +0,0 @@ --- Rerun tests only if their modification time changed. -cache = true - --- Glorious list of warnings: https://luacheck.readthedocs.io/en/stable/warnings.html -ignore = { - "212", -- Unused argument, In the case of callback function, _arg_name is easier to understand than _, so this option is set to off. - "411", -- Redefining a local variable. - "412", -- Redefining an argument. - "422", -- Shadowing an argument - "431", -- Shadowing a variable - "122", -- Indirectly setting a readonly global -} - --- Global objects defined by the C code -read_globals = { - "vim", -} - -exclude_files = { - "src/**/*.lua", -} From 778ee2c6cf220f720a49b774061d86e419a08af7 Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Wed, 5 Jun 2024 18:52:12 +0900 Subject: [PATCH 14/15] refactor: typing --- src/jupynium/cmds/jupynium.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/jupynium/cmds/jupynium.py b/src/jupynium/cmds/jupynium.py index 053722b..8e2eb6e 100644 --- a/src/jupynium/cmds/jupynium.py +++ b/src/jupynium/cmds/jupynium.py @@ -13,6 +13,7 @@ import tempfile import time import traceback +from collections.abc import Sequence from datetime import datetime, timezone from os import PathLike from pathlib import Path @@ -47,7 +48,7 @@ def webdriver_firefox( profiles_ini_path: str | PathLike | None = "~/.mozilla/firefox/profiles.ini", - profile_name=None, + profile_name: str | None = None, ): """ Get a Firefox webdriver with a specific profile. @@ -178,7 +179,8 @@ def get_parser(): nargs="+", default=["jupyter"], help="Command to start Jupyter Notebook (but without notebook).\n" - "To use conda env, use `--jupyter_command conda run ' --no-capture-output' ' -n' base jupyter`. Notice the space before the dash.\n" # noqa: E501 + "To use conda env, use `--jupyter_command conda run ' --no-capture-output' ' -n' base jupyter`. " + "Notice the space before the dash.\n" "It is used only when the --notebook_URL is localhost, and is not running.", ) parser.add_argument( @@ -249,8 +251,8 @@ def _predicate(driver: webdriver.Firefox): def attach_new_neovim( - driver, - new_args, + driver: WebDriver, + new_args: argparse.Namespace, nvims: dict[str, NvimInfo], url_to_home_windows: dict[str, str], ): @@ -357,7 +359,7 @@ def kill_notebook_proc(notebook_proc: subprocess.Popen | None): def fallback_open_notebook_server( notebook_port: int, notebook_url_path: str, - jupyter_command, + jupyter_command: Sequence[str], notebook_dir: str | PathLike | None, nvim: Nvim | None, driver: WebDriver, @@ -600,7 +602,7 @@ def main(): # Check if a new newvim instance wants to attach to this server. try: - new_args = q.get(block=False) + new_args: argparse.Namespace = q.get(block=False) # type: ignore except Empty: pass else: From 7851f027872f61e8e463069e62f9a1eb118a8b53 Mon Sep 17 00:00:00 2001 From: Kiyoon Kim Date: Thu, 6 Jun 2024 21:04:14 +0900 Subject: [PATCH 15/15] refactor: ruff lint --- src/jupynium/buffer.py | 21 ++++++---- src/jupynium/cmds/ipynb2jupytext.py | 13 +++--- src/jupynium/cmds/jupynium.py | 2 +- src/jupynium/events_control.py | 61 +++++++++++++++++------------ src/jupynium/pynvim_helpers.py | 4 +- src/jupynium/rpc_messages.py | 1 + tests/conftest.py | 4 +- 7 files changed, 64 insertions(+), 42 deletions(-) diff --git a/src/jupynium/buffer.py b/src/jupynium/buffer.py index c6cc46f..bf68b0a 100644 --- a/src/jupynium/buffer.py +++ b/src/jupynium/buffer.py @@ -20,11 +20,12 @@ class JupyniumBuffer: Deal with the Nvim buffer and its cell information. This does have a functionality to sync with the Notebook. + It can also be part of the buffer, and anything above the first cell will be `header_cell_type`. """ def __init__( self, - buf: list[str] = [""], + buf: list[str] | None = None, header_cell_type: str = "header", ): """ @@ -33,14 +34,15 @@ def __init__( Args: header_cell_type: Use only when partial update. """ - self.buf = buf - if self.buf == [""]: + if buf is None: # each cell's row length. 0-th cell is not a cell, but it's the header. # You can put anything above and it won't be synced to Jupyter Notebook. + self.buf = [""] self.num_rows_per_cell: list[int] = [1] self.cell_types = ["header"] # 0-th cell is not a cell. else: + self.buf = buf self.full_analyse_buf(header_cell_type) def full_analyse_buf(self, header_cell_type: str = "header"): @@ -68,7 +70,7 @@ def full_analyse_buf(self, header_cell_type: str = "header"): num_rows_this_cell = 0 num_rows_per_cell = [] cell_types = [header_cell_type] - for row, line in enumerate(self.buf): + for _row, line in enumerate(self.buf): if line.startswith(("# %% [md]", "# %% [markdown]")): num_rows_per_cell.append(num_rows_this_cell) num_rows_this_cell = 1 @@ -178,7 +180,9 @@ def process_on_lines( driver, modified_cell_idx_start, modified_cell_idx_end, strip=strip ) - def _on_lines_update_buf(self, lines, start_row, old_end_row, new_end_row): + def _on_lines_update_buf( + self, lines: list[str], start_row: int, old_end_row: int, new_end_row: int + ): """Replace start_row:old_end_row to lines from self.buf.""" # Analyse how many cells are removed notebook_cell_delete_operations = [] @@ -369,7 +373,7 @@ def _check_validity(self): assert all(x in ("code", "markdown") for x in self.cell_types[1:]) def _partial_sync_to_notebook( - self, driver, start_cell_idx, end_cell_idx, strip=True + self, driver: WebDriver, start_cell_idx: int, end_cell_idx: int, strip=True ): """ Given the range of cells to update, sync the JupyniumBuffer with the notebook. @@ -437,7 +441,7 @@ def _partial_sync_to_notebook( *texts_per_cell, ) - def full_sync_to_notebook(self, driver, strip=True): + def full_sync_to_notebook(self, driver: WebDriver, strip: bool = True): # Full sync with notebook. # WARNING: syncing may result in data loss. num_cells = self.num_cells_in_notebook @@ -473,7 +477,8 @@ def num_cells_in_notebook(self): def num_rows(self): return len(self.buf) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + assert isinstance(other, JupyniumBuffer) return ( self.buf == other.buf and self.num_rows_per_cell == other.num_rows_per_cell diff --git a/src/jupynium/cmds/ipynb2jupytext.py b/src/jupynium/cmds/ipynb2jupytext.py index 3c9c5ae..b9beeb3 100644 --- a/src/jupynium/cmds/ipynb2jupytext.py +++ b/src/jupynium/cmds/ipynb2jupytext.py @@ -3,7 +3,7 @@ from __future__ import annotations import argparse -import os +from pathlib import Path from ..ipynb import ipynb2jupytext, load_ipynb @@ -50,13 +50,14 @@ def main(): for line in jupy: print(line) else: - output_jupy_path = args.output_jupy_path - if output_jupy_path is None: - output_jupy_path = os.path.splitext(args.ipynb_path)[0] + ".ju.py" + if args.output_jupy_path is None: + output_jupy_path = Path(args.ipynb_path).with_suffix(".ju.py") + else: + output_jupy_path = Path(args.output_jupy_path) - os.makedirs(os.path.dirname(os.path.realpath(output_jupy_path)), exist_ok=True) + output_jupy_path.parent.mkdir(parents=True, exist_ok=True) - if os.path.isfile(output_jupy_path) and not args.yes: + if output_jupy_path.is_file() and not args.yes: print(f"Do you want to overwrite {output_jupy_path}?") answer = input("y/n: ") if answer != "y": diff --git a/src/jupynium/cmds/jupynium.py b/src/jupynium/cmds/jupynium.py index 8e2eb6e..5453398 100644 --- a/src/jupynium/cmds/jupynium.py +++ b/src/jupynium/cmds/jupynium.py @@ -413,7 +413,7 @@ def fallback_open_notebook_server( # e.g. --jupyter_command conda run ' --no-capture-output' ' -n' env_name jupyter jupyter_command = [command.strip() for command in jupyter_command] - jupyter_command[0] = os.path.expanduser(jupyter_command[0]) + jupyter_command[0] = str(Path(jupyter_command[0]).expanduser()) jupyter_stdout = tempfile.NamedTemporaryFile() logger.info(f"Writing Jupyter Notebook server log to: {jupyter_stdout.name}") diff --git a/src/jupynium/events_control.py b/src/jupynium/events_control.py index 6071312..e7f4fd2 100644 --- a/src/jupynium/events_control.py +++ b/src/jupynium/events_control.py @@ -6,6 +6,8 @@ import os from dataclasses import dataclass from os import PathLike +from pathlib import Path +from typing import Any from pkg_resources import resource_stream from selenium.common.exceptions import ( @@ -130,7 +132,7 @@ class PrevLazyArgsPerBuf: data: dict[int, PrevLazyArgs] = dataclasses.field(default_factory=dict) - def process(self, bufnr: int, nvim_info: NvimInfo, driver) -> None: + def process(self, bufnr: int, nvim_info: NvimInfo, driver: WebDriver) -> None: if bufnr in self.data: self.data[bufnr].process(nvim_info, driver, bufnr) @@ -140,7 +142,11 @@ def process_all(self, nvim_info: NvimInfo, driver) -> None: self.data.clear() def lazy_on_lines_event( - self, nvim_info: NvimInfo, driver, bufnr: int, on_lines_args: OnLinesArgs + self, + nvim_info: NvimInfo, + driver: WebDriver, + bufnr: int, + on_lines_args: OnLinesArgs, ) -> None: if bufnr not in self.data: self.data[bufnr] = PrevLazyArgs() @@ -163,7 +169,7 @@ def overwrite_update_selection( self.data[bufnr].update_selection_args = update_selection_args -def process_events(nvim_info: NvimInfo, driver): +def process_events(nvim_info: NvimInfo, driver: WebDriver): """ Controls events for a single nvim, and a single cycle of events. @@ -241,6 +247,7 @@ def start_sync_with_filename( driver.switch_to.window(nvim_info.home_window) sele.wait_until_notebook_list_loaded(driver) + prev_windows = None if ipynb_filename == "": file_found = False else: @@ -265,9 +272,11 @@ def start_sync_with_filename( driver.execute_script("arguments[0].scrollIntoView();", notebook_elem) notebook_elem.click() file_found = True - sele.wait_until_new_window(driver, prev_windows) + sele.wait_until_new_window(driver, list(prev_windows)) break + assert prev_windows is not None + if file_found: new_window = set(driver.window_handles) - set(prev_windows) assert len(new_window) == 1 @@ -313,7 +322,7 @@ def start_sync_with_filename( new_btn.click() kernel_btn.click() - sele.wait_until_new_window(driver, prev_windows) + sele.wait_until_new_window(driver, list(prev_windows)) new_window = set(driver.window_handles) - prev_windows assert len(new_window) == 1 new_window = new_window.pop() @@ -333,9 +342,7 @@ def start_sync_with_filename( def choose_default_kernel( driver: WebDriver, page_type: str, buf_filetype: str, conda_or_venv_path: str | None ): - """ - Choose kernel based on buffer's filetype and conda env - """ + """Choose kernel based on buffer's filetype and conda env.""" if page_type == "notebook": kernel_specs = driver.execute_script( "return Jupyter.kernelselector.kernelspecs;" @@ -363,9 +370,9 @@ def match_with_path(env_path: str) -> str | None: """ for kernel_name in valid_kernel_names: try: - kernel_exec_path = kernel_specs[kernel_name]["spec"]["argv"][0] - exec_name = os.path.basename(kernel_exec_path) - env_exec_path = os.path.join(env_path, "bin", exec_name) + kernel_exec_path = Path(kernel_specs[kernel_name]["spec"]["argv"][0]) + exec_name = kernel_exec_path.name + env_exec_path = Path(env_path) / "bin" / exec_name if kernel_exec_path == env_exec_path: return kernel_name except (KeyError, IndexError): @@ -413,8 +420,13 @@ def match_with_path(env_path: str) -> str | None: return None -def process_request_event(nvim_info: NvimInfo, driver: WebDriver, event): +def process_request_event(nvim_info: NvimInfo, driver: WebDriver, event: list[Any]): """ + Process a request event, where an event can be request or notification. + + Request event requires a response. + Notification event doesn't require a response. + Returns: status (bool) request_event (rpcrequest event): to notify nvim after cleared up. @@ -667,9 +679,9 @@ def process_notification_event( ".ju." in buf_filepath and nvim_info.nvim.vars["jupynium_auto_download_ipynb"] ): - output_ipynb_path = os.path.splitext(buf_filepath)[0] - output_ipynb_path = os.path.splitext(output_ipynb_path)[0] - output_ipynb_path += ".ipynb" + # .ju.py -> .ipynb + output_ipynb_path = os.path.splitext(buf_filepath)[0] # noqa: PTH122 + output_ipynb_path = Path(output_ipynb_path).with_suffix(".ipynb") try: download_ipynb(driver, nvim_info, bufnr, output_ipynb_path) @@ -683,20 +695,21 @@ def process_notification_event( (buf_filepath, filename) = event_args assert buf_filepath != "" + buf_filepath = Path(buf_filepath) + filename = Path(filename) + if filename is not None and filename != "": - if os.path.isabs(filename): + if filename.is_absolute(): output_ipynb_path = filename else: - output_ipynb_path = os.path.join( - os.path.dirname(buf_filepath), filename - ) + output_ipynb_path = buf_filepath.parent / filename - if not output_ipynb_path.endswith(".ipynb"): - output_ipynb_path += ".ipynb" + if output_ipynb_path.suffix != ".ipynb": + output_ipynb_path = output_ipynb_path.with_suffix(".ipynb") else: - output_ipynb_path = os.path.splitext(buf_filepath)[0] - output_ipynb_path = os.path.splitext(output_ipynb_path)[0] - output_ipynb_path += ".ipynb" + # change suffix .ju.py -> .ipynb + output_ipynb_path = os.path.splitext(buf_filepath)[0] # noqa: PTH122 + output_ipynb_path = Path(output_ipynb_path).with_suffix(".ipynb") try: download_ipynb(driver, nvim_info, bufnr, output_ipynb_path) diff --git a/src/jupynium/pynvim_helpers.py b/src/jupynium/pynvim_helpers.py index e08261e..1532906 100644 --- a/src/jupynium/pynvim_helpers.py +++ b/src/jupynium/pynvim_helpers.py @@ -2,6 +2,7 @@ import logging import time +from os import PathLike import pynvim from pkg_resources import resource_stream @@ -9,7 +10,8 @@ logger = logging.getLogger(__name__) -def attach_and_init(nvim_listen_addr: str): +def attach_and_init(nvim_listen_addr: str | PathLike): + nvim_listen_addr = str(nvim_listen_addr) logger.info("nvim addr: %s", nvim_listen_addr) for _ in range(30): try: diff --git a/src/jupynium/rpc_messages.py b/src/jupynium/rpc_messages.py index a5fc90c..1489f89 100644 --- a/src/jupynium/rpc_messages.py +++ b/src/jupynium/rpc_messages.py @@ -29,6 +29,7 @@ def receive_message(nvim: Nvim): def receive_all_pending_messages(nvim: Nvim): """ It doesn't guarantee to grab all messages that are previously sent. + Maybe the last one or two may still be in process. """ events = [] diff --git a/tests/conftest.py b/tests/conftest.py index 688e883..f453a03 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,8 @@ from __future__ import annotations -import os import subprocess import tempfile +from pathlib import Path import pytest @@ -18,7 +18,7 @@ def jupbuf1(): @pytest.fixture(scope="session") def nvim_1(): with tempfile.TemporaryDirectory() as tmp: - path = os.path.join(tmp, "nvim") + path = str(Path(tmp) / "nvim") nvim_proc = subprocess.Popen( ["nvim", "--clean", "--headless", "--listen", path] )