From 596dc662dfb25787ab725c99b7d3acffe8159f79 Mon Sep 17 00:00:00 2001 From: "Maarten A. Breddels" Date: Fri, 26 Jul 2024 16:35:08 +0200 Subject: [PATCH] chore: initial commit --- .bumpversion.cfg | 34 ++++ .github/workflows/test.yaml | 330 +++++++++++++++++++++++++++++++++ .gitignore | 16 ++ .pre-commit-config.yaml | 22 +++ LICENSE.txt | 9 + README.md | 21 +++ docs/index.md | 168 +++++++++++++++++ mkdocs.yml | 18 ++ pyproject.toml | 158 ++++++++++++++++ release.sh | 25 +++ src/mkdocs_pycafe/__about__.py | 1 + src/mkdocs_pycafe/__init__.py | 85 +++++++++ tests/__init__.py | 3 + 13 files changed, 890 insertions(+) create mode 100644 .bumpversion.cfg create mode 100644 .github/workflows/test.yaml create mode 100755 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 docs/index.md create mode 100644 mkdocs.yml create mode 100644 pyproject.toml create mode 100755 release.sh create mode 100644 src/mkdocs_pycafe/__about__.py create mode 100644 src/mkdocs_pycafe/__init__.py create mode 100644 tests/__init__.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..9e9c7db --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,34 @@ +[bumpversion] +current_version = 1.36.0 +commit = True +tag = True +parse = (?P\d+)(\.(?P\d+))(\.(?P\d+))((?P.)(?P\d+))? +serialize = + {major}.{minor}.{patch}{release}{build} + {major}.{minor}.{patch} + +[bumpversion:part:release] +optional_value = g +first_value = g +values = + a + b + g + +[bumpversion:file:solara/__init__.py] + +[bumpversion:file:packages/solara-assets/solara_assets/__init__.py] + +[bumpversion:file:packages/solara-enterprise/solara_enterprise/__init__.py] + +[bumpversion:file:packages/solara-enterprise/pyproject.toml] + +[bumpversion:file:packages/solara-server/pyproject.toml] + +[bumpversion:file:packages/solara-meta/pyproject.toml] + +[bumpversion:file:packages/pytest-ipywidgets/pyproject.toml] + +[bumpversion:file:solara/server/static/solara_bootstrap.py] + +[bumpversion:file:release.md] diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..82bd7fb --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,330 @@ +name: Test + +on: + push: + branches: + - main + tags: + - v* + pull_request: + workflow_dispatch: + schedule: + - cron: '0 6 * * *' # at 06:00 UTC every day + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ !(github.ref == 'refs/heads/main') }} + + +defaults: + run: + shell: bash {0} + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install build tools + run: pip install hatch + + - name: Build mkdocs-pycafe + run: hatch build + + - name: Upload Test artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: mkdocs-pycafe-builds-${{ github.run_number }} + path: | + dist + README.md + + code-quality: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [3.8, "3.9"] + env: + LOCK_FILE_LOCATION: .ci-package-locks/code-quality/python${{ matrix.python-version }}.txt + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Prepare + id: prepare + run: | + if [ -f ${{ env.LOCK_FILE_LOCATION }} ]; then + echo "LOCKS_EXIST=true" >> "$GITHUB_OUTPUT" + else + echo "LOCKS_EXIST=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install without locking versions + if: github.event_name == 'schedule' || steps.prepare.outputs.LOCKS_EXIST == 'false' + id: install_no_lock + run: | + mkdir -p .ci-package-locks/code-quality + pip install pre-commit + pip freeze --exclude mkdocs-pycafe > ${{ env.LOCK_FILE_LOCATION }} + git diff --quiet || echo "HAS_DIFF=true" >> "$GITHUB_OUTPUT" + + - name: Install + if: github.event_name != 'schedule' && steps.prepare.outputs.LOCKS_EXIST == 'true' + run: pip install -r ${{ env.LOCK_FILE_LOCATION }} + + - name: Install pre-commit + run: pre-commit install + + - name: Run pre-commit + run: pre-commit run --all-files + + - name: Upload CI package locks + if: steps.install_no_lock.outputs.HAS_DIFF == 'true' || steps.prepare.outputs.LOCKS_EXIST == 'false' + uses: actions/upload-artifact@v4 + with: + name: ci-package-locks-code-quality-python${{ matrix.python-version }} + path: ./**/${{ env.LOCK_FILE_LOCATION }} + + test-install: + needs: [build] + runs-on: ${{ matrix.os }}-${{(matrix.os == 'ubuntu' && matrix.python == '3.6') && '20.04' || (matrix.os == 'macos' && matrix.python == '3.6') && '13' || 'latest' }} + strategy: + fail-fast: false + matrix: + os: [ubuntu, macos, windows] + python: ["3.6", "3.12"] + + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: mkdocs-pycafe-builds-${{ github.run_number }} + + - name: Debug + run: ls -R dist + + - name: Install mkdocs-pycafe-ui + run: + pip install dist/*.whl + + - name: Test import + run: python -c "import mkdocs_pycafe" + + integration-test: + needs: [build] + timeout-minutes: 15 + runs-on: ${{ matrix.os }}-latest + strategy: + fail-fast: false + matrix: + # just ubuntu give enough confidence + os: [ubuntu] + # just 1 version, it's heavy + python-version: [3.8] + env: + LOCK_FILE_LOCATION: .ci-package-locks/integration/os${{ matrix.os }}-python${{ matrix.python-version }}-ipywidgets${{ matrix.ipywidgets_major }}.txt + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + + - uses: actions/download-artifact@v4 + with: + name: mkdocs-pycafe-builds-${{ github.run_number }} + + - name: Prepare + id: prepare + run: | + mkdir test-results + if [ -f ${{ env.LOCK_FILE_LOCATION }} ]; then + echo "LOCKS_EXIST=true" >> "$GITHUB_OUTPUT" + else + echo "LOCKS_EXIST=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install without locking versions + if: github.event_name == 'schedule' || steps.prepare.outputs.LOCKS_EXIST == 'false' + id: install_no_lock + run: | + mkdir -p .ci-package-locks/integration + pip install `echo dist/*.whl`[all] + pip freeze --exclude mkdocs-pycafe > ${{ env.LOCK_FILE_LOCATION }} + git diff --quiet || echo "HAS_DIFF=true" >> "$GITHUB_OUTPUT" + + - name: Install + if: github.event_name != 'schedule' && steps.prepare.outputs.LOCKS_EXIST == 'true' + run: | + pip install -r ${{ env.LOCK_FILE_LOCATION }} + pip install `echo dist/*.whl`[all] + + - name: Install playwright + run: playwright install + + - name: test + if: github.event_name != 'schedule' || steps.install_no_lock.outputs.HAS_DIFF == 'true' + run: pytest tests/integration --timeout=360 --video=retain-on-failure --output=test-results -vv -s --log-cli-level=warning + + - name: Upload Test artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-integration-os${{ matrix.os }}-python${{ matrix.python-version }}-ipywidgets${{ matrix.ipywidgets_major }} + path: test-results + + - name: Upload CI package locks + if: steps.install_no_lock.outputs.HAS_DIFF == 'true' || steps.prepare.outputs.LOCKS_EXIST == 'false' + uses: actions/upload-artifact@v4 + with: + name: ci-package-locks-integration-os${{ matrix.os }}-python${{ matrix.python-version }}-ipywidgets${{ matrix.ipywidgets_major }} + path: ./**/${{ env.LOCK_FILE_LOCATION }} + + unit-test: + needs: [build] + runs-on: ${{ matrix.os }}-${{(matrix.os == 'ubuntu' && matrix.python == '3.6') && '20.04' || (matrix.os == 'macos' && matrix.python == '3.6') && '13' || 'latest' }} + strategy: + fail-fast: false + matrix: + os: [ubuntu, macos, windows] + python: [3.6, 3.12] + env: + LOCK_FILE_LOCATION: .ci-package-locks/unit/os${{ matrix.os }}-python${{ matrix.python }}-ipywidgets${{ matrix.ipywidgets }}.txt + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + cache: "pip" + + - uses: actions/download-artifact@v4 + with: + name: mkdocs-pycafe-builds-${{ github.run_number }} + + - name: Prepare + id: prepare + run: | + if [ -f ${{ env.LOCK_FILE_LOCATION }} ]; then + echo "LOCKS_EXIST=true" >> "$GITHUB_OUTPUT" + else + echo "LOCKS_EXIST=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install without locking versions + id: install_no_lock + if: github.event_name == 'schedule' || steps.prepare.outputs.LOCKS_EXIST == 'false' + run: | + mkdir -p .ci-package-locks/unit + pip install `echo dist/*.whl`[all] + pip freeze --exclude mkdocs-pycafe > ${{ env.LOCK_FILE_LOCATION }} + git diff --quiet || echo "HAS_DIFF=true" >> "$GITHUB_OUTPUT" + + - name: Install + if: github.event_name != 'schedule' && steps.prepare.outputs.LOCKS_EXIST == 'true' + run: | + pip install -r ${{ env.LOCK_FILE_LOCATION }} + pip install `echo dist/*.whl`[all] + + - name: test + if: github.event_name != 'schedule' || steps.install_no_lock.outputs.HAS_DIFF == 'true' + run: pytest tests/unit --doctest-modules --timeout=60 + + - name: Upload CI package locks + if: steps.install_no_lock.outputs.HAS_DIFF == 'true' || steps.prepare.outputs.LOCKS_EXIST == 'false' + uses: actions/upload-artifact@v4 + with: + name: ci-package-locks-unit-os${{ matrix.os }}-python${{ matrix.python }}-ipywidgets${{ matrix.ipywidgets }} + path: ./**/${{ env.LOCK_FILE_LOCATION }} + + update-ci-package-locks: + needs: [build, code-quality, integration-test, unit-test] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.head_ref || github.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name || github.event.repository.full_name }} + + - uses: actions/download-artifact@v4 + with: + pattern: ci-package-locks-* + merge-multiple: true + + - name: Prepare + id: prepare + # We check if lock files have changed. This should only be the case if we are either running on a schedule + # or if some lock files did not exist yet. + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + + git add -N .ci-package-locks + git diff --quiet || echo "HAS_DIFF=true" >> "$GITHUB_OUTPUT" + + - name: Update CI package locks + if: steps.prepare.outputs.HAS_DIFF == 'true' + run: | + git add .ci-package-locks + git commit -m "Update CI package locks" + git push + + release: + needs: [build, code-quality, test-install, integration-test, unit-test] + runs-on: ubuntu-latest + permissions: + id-token: write # this permission is mandatory for trusted publishing + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - uses: actions/download-artifact@v4 + with: + name: mkdocs-pycafe-builds-${{ github.run_number }} + + - name: Install build tools + run: pip install hatch + + - name: Install mkdocs-pycafe + run: pip install dist/*.whl + + - name: Test import mkdocs-pycafe + run: python -c "import mkdocs_pycafe" + + - name: Publish package distributions to PyPI + if: startsWith(github.event.ref, 'refs/tags/v') + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..7e729c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.egg-info/ +.eggs/ +.ipynb_checkpoints/ +dist/ +build/ +*.py[cod] +**/node_modules/ + +# Compiled javascript +ipyvue/labextension/ +ipyvue/nbextension/ +js/lib +js/jupyter-vue-*.tgz + +# coverage +.coverage diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..05f939a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + exclude: .bumpversion.cfg + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + files: ^.*\.(py|md|yaml|js|ts|ipynb)$ + args: [] + additional_dependencies: + - tomli + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: "v0.3.4" + hooks: + - id: ruff + stages: [commit] + - id: ruff-format + stages: [commit] diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1644167 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2024-present Maarten A. Breddels + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3162fb3 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# mkdocs-pycafe + +[![PyPI - Version](https://img.shields.io/pypi/v/mkdocs-pycafe.svg)](https://pypi.org/project/mkdocs-pycafe) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/mkdocs-pycafe.svg)](https://pypi.org/project/mkdocs-pycafe) + +----- + +**Table of Contents** + +- [Installation](#installation) +- [License](#license) + +## Installation + +```console +pip install mkdocs-pycafe +``` + +## License + +`mkdocs-pycafe` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..33c58d1 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,168 @@ +# PyCafe plugin for MkDocs + +## Introduction + +Did you ever want your code snippets in your documentation to be interactive? Let you users edit them and see what the result is? + +This plugin allow you to create links to, or embed PyCafe projects in your MkDocs documentation from code blocks. + +## Installation + +```bash +pip install mkdocs mkdocs-material mkdocs-pycafe +``` + +*(Assuming you use mkdocs-material.)* + +For documentation on mkdocs, visit [mkdocs.org](https://www.mkdocs.org), for documentation of MkDocs Material, visit [squidfunk.github.io/mkdocs-material](https://squidfunk.github.io/mkdocs-material). + +## Configuration + +Make sure you configure mkdocs to use PyCafe's formatter for Python codeblocks, and enable the snippets plugin if you want to use the scissor syntax (`--8<--`). + +```yaml +site_name: PyCafe plugin for MkDocs +site_url: https://mkdocs.py.cafe +theme: + name: material +markdown_extensions: + - pymdownx.superfences: + custom_fences: + - name: python + class: 'highlight' + validator: !!python/name:mkdocs_pycafe.validator + format: !!python/object/apply:mkdocs_pycafe.formatter + kwds: + type: solara + requirements: | + altair + anywidget + - pymdownx.snippets: + url_download: true +``` + +In this case, we added default requirements, and a default PyCafe type of `solara`. You can also specify the type in the code block itself by providing `pycafe-type` and `requirements` attributes. + +In the examples below, we mostly use the `extra-requirements` attribute to specify additional requirements, instead of overwriting the default requirements. + + +## Usage + +### Code block with a link to PyCafe + +To insert a link to PyCafe below you code blocks, add the `pycafe-link` option, and specify the `extra-requirements`if needed. + +```` +```{.python pycafe-link extra-requirements="vega_datasets"} +from vega_datasets import data +import altair as alt + +cars = data.cars() + +chart = alt.Chart(cars).mark_circle().encode( + x='Horsepower', + y='Miles_per_Gallon', + color='Origin', +) + +# assign a widget to page so solara knows what to render +page = alt.JupyterChart(chart) +``` +```` + +Which will render as: + +```{.python pycafe-link extra-requirements="vega_datasets"} +from vega_datasets import data +import altair as alt + +cars = data.cars() + +chart = alt.Chart(cars).mark_circle().encode( + x='Horsepower', + y='Miles_per_Gallon', + color='Origin', +) + +# assign a widget to page so solara knows what to render +page = alt.JupyterChart(chart) +``` + + +### Code block with an embedded app + +If want the app to be embedded, instead of providing a linl, you can use the pycafe-embed flag. + +```` + +```{.python pycafe-embed extra-requirements="vega_datasets" pycafe-embed-style="border: 1px solid #e6e6e6; border-radius: 8px;" pycafe-embed-width="100%" pycafe-embed-height="400px"} +from vega_datasets import data +import altair as alt + +cars = data.cars() + +chart = alt.Chart(cars).mark_circle().encode( + x='Horsepower', + y='Miles_per_Gallon', + color='Origin', +) + +# assign a widget to page so solara knows what to render +page = alt.JupyterChart(chart) +``` +```` + +Which will render as: + +```{.python pycafe-embed extra-requirements="vega_datasets" pycafe-embed-style="border: 1px solid #e6e6e6; border-radius: 8px;" pycafe-embed-width="100%" pycafe-embed-height="400px"} +from vega_datasets import data +import altair as alt + +cars = data.cars() + +chart = alt.Chart(cars).mark_circle().encode( + x='Horsepower', + y='Miles_per_Gallon', + color='Origin', +) + +# assign a widget to page so solara knows what to render +page = alt.JupyterChart(chart) +``` + + +### Code block with code from PyCafe + +If you enabled the snippets plugin, you can include code from PyCafe directly using the `--8<--` syntax. + +```` +```python +;--8<-- "https://py.cafe/files/maartenbreddels/altair-car-performance-comparison/app.py" +``` +```` + +Which will render as: + +```python +--8<-- "https://py.cafe/files/maartenbreddels/altair-car-performance-comparison/app.py" +``` + +Although this can be combined with the above PyCafe link and embed options, it is more flexible to directly use +html to include a link to, or embed a PyCafe project. + +A link can be made using Markdown or HTML +``` +[Custom link to PyCafe project](https://py.cafe/maartenbreddels/altair-car-performance-comparison) +Custom link to PyCafe project +``` + +Which will render as: Custom link to PyCafe project + +To embed a PyCafe project, use the following HTML code (also available via the Share dialog on the PyCafe website): +```html + +``` +Which will render as: + + + diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..50da2b7 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,18 @@ +site_name: PyCafe plugin for MkDocs +site_url: https://mkdocs.py.cafe +theme: + name: material +markdown_extensions: + - pymdownx.superfences: + custom_fences: + - name: python + class: 'highlight' + validator: !!python/name:mkdocs_pycafe.validator + format: !!python/object/apply:mkdocs_pycafe.formatter + kwds: + type: solara + requirements: | + altair + anywidget + - pymdownx.snippets: + url_download: true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..64ffdb5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,158 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "mkdocs-pycafe" +dynamic = ["version"] +description = 'Mkdocs plugins to link code to py.cafe' +readme = "README.md" +requires-python = ">=3.6" +license = "MIT" +keywords = [] +authors = [ + { name = "Maarten A. Breddels", email = "maartenbreddels@gmail.com" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [] + +[project.urls] +Documentation = "https://github.com/py-cafe/mkdocs-pycafe#readme" +Issues = "https://github.com/py-cafe/mkdocs-pycafe/issues" +Source = "hhttps://github.com/py-cafe/mkdocs-pycafe" + +[tool.hatch.version] +path = "src/mkdocs_pycafe/__about__.py" + +[tool.hatch.envs.default] +dependencies = [ + "coverage[toml]>=6.5", + "pytest", +] +[tool.hatch.envs.default.scripts] +test = "pytest {args:tests}" +test-cov = "coverage run -m pytest {args:tests}" +cov-report = [ + "- coverage combine", + "coverage report", +] +cov = [ + "test-cov", + "cov-report", +] + +[[tool.hatch.envs.all.matrix]] +python = ["3.7", "3.8", "3.9", "3.10", "3.11"] + +[tool.hatch.envs.lint] +detached = true +dependencies = [ + "black>=23.1.0", + "mypy>=1.0.0", + "ruff>=0.0.243", +] +[tool.hatch.envs.lint.scripts] +typing = "mypy --install-types --non-interactive {args:src/mkdocs_pycafe tests}" +style = [ + "ruff {args:.}", + "black --check --diff {args:.}", +] +fmt = [ + "black {args:.}", + "ruff --fix {args:.}", + "style", +] +all = [ + "style", + "typing", +] + +[tool.black] +target-version = ["py37"] +line-length = 160 +skip-string-normalization = true + +[tool.ruff] +target-version = "py37" +line-length = 160 +lint.select = [ + "A", + "ARG", + "B", + "C", + "DTZ", + "E", + "EM", + "F", + "FBT", + "I", + "ICN", + "ISC", + "N", + "PLC", + "PLE", + "PLR", + "PLW", + "Q", + "RUF", + "S", + "T", + "TID", + "UP", + "W", + "YTT", +] +lint.ignore = [ + # Allow non-abstract empty methods in abstract base classes + "B027", + # Allow boolean positional values in function calls, like `dict.get(... True)` + "FBT003", + # Ignore checks for possible passwords + "S105", "S106", "S107", + # Ignore complexity + "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", +] +lint.unfixable = [ + # Don't touch unused imports + "F401", +] + +[tool.ruff.lint.isort] +known-first-party = ["mkdocs-pycafe"] + +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.lint.per-file-ignores] +# Tests can use magic values, assertions, and relative imports +"tests/**/*" = ["PLR2004", "S101", "TID252"] + +[tool.coverage.run] +source_pkgs = ["mkdocs-pycafe", "tests"] +branch = true +parallel = true +omit = [ + "src/mkdocs-pycafe/__about__.py", +] + +[tool.coverage.paths] +mkdocs-pycafe = ["src/mkdocs-pycafe", "*/mkdocs-pycafe/src/mkdocs-pycafe"] +tests = ["tests", "*/mkdocs-pycafe/tests"] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..4234c11 --- /dev/null +++ b/release.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e -o pipefail +# usage: ./release minor -n +# (git diff --quiet master @widgetti/solara-vuetify-app@10.0.3 -- packages/solara-vuetify-app) || {\ +# echo -e "\033[31m There are unreleased changes to the solara-vuetify-app package.\n Please release the javascript package before Solara by running \n\n \ +# \033[0m (cd packages/solara-vuetify-app && ./release.sh -n)\n"; \ +# exit 1;} +(git diff --quiet master @widgetti/solara-vuetify3-app@5.0.2 -- packages/solara-vuetify3-app) || {\ + echo -e "\033[31m There are unreleased changes to the solara-vuetify3-app package.\n Please release the javascript package before Solara by running \n\n \ + \033[0m (cd packages/solara-vuetify3-app && ./release.sh -n)\n"; \ + exit 1;} +(git diff --quiet master @widgetti/solara-vuetify3-app@5.0.2 -- packages/solara-widget-manager) || {\ + echo -e "\033[31m There are unreleased changes to the solara-widget-manager package.\n Please release the javascript package before Solara by running \n\n \ + \033[0m (cd packages/solara-vuetify-app && ./release.sh -n) && \ + (cd packages/solara-vuetify3-app && ./release.sh -n)\n"; \ + exit 1;} +(git diff --quiet master @widgetti/solara-vuetify3-app@5.0.2 -- packages/solara-widget-manager8) || {\ + echo -e "\033[31m There are unreleased changes to the solara-widget-manager8 package.\n Please release the javascript package before Solara by running \n\n \ + \033[0m (cd packages/solara-vuetify-app && ./release.sh -n) && \ + (cd packages/solara-vuetify3-app && ./release.sh -n)\n"; \ + exit 1;} + +version=$(bump2version --dry-run --list $* | grep new_version | sed -r s,"^.*=",,) +echo Version tag v$version +bumpversion $* --verbose && git push upstream master v$version diff --git a/src/mkdocs_pycafe/__about__.py b/src/mkdocs_pycafe/__about__.py new file mode 100644 index 0000000..f102a9c --- /dev/null +++ b/src/mkdocs_pycafe/__about__.py @@ -0,0 +1 @@ +__version__ = "0.0.1" diff --git a/src/mkdocs_pycafe/__init__.py b/src/mkdocs_pycafe/__init__.py new file mode 100644 index 0000000..bccc5cf --- /dev/null +++ b/src/mkdocs_pycafe/__init__.py @@ -0,0 +1,85 @@ +import base64 +import gzip +import json +from functools import partial +from urllib.parse import quote_plus, urlencode + +base_url = "https://py.cafe" + + +def validator(language, inputs, options, attrs, md): + valid_flags = {"pycafe-link", "pycafe-embed"} + valid_inputs = {"requirements", "extra-requirements", "pycafe-type"} + + for k, v in inputs.items(): + if k in valid_inputs: + options[k] = v + continue + elif k in valid_flags: + options[k] = True + continue + attrs[k] = v + md.preprocessors["fenced_code_block"].extension.superfences[0]["validator"](language, inputs, options, attrs, md) + return True + + +def _formatter(src="", language="", class_name=None, options=None, md="", requirements="", pycafe_type="solara", **kwargs): + from pymdownx.superfences import SuperFencesException + + options = options or {} + pycafe_link = options.get("pycafe-link", False) + pycafe_embed = options.get("pycafe-embed", False) + pycafe_embed_height = options.get("pycafe-embed-height", "400px") + pycafe_embed_width = options.get("pycafe-embed-width", "100%") + pycafe_embed_style = options.get("pycafe-embed-style", "border: 1px solid #e6e6e6; border-radius: 8px;") + pycafe_embed_theme = options.get("pycafe-embed-theme", "light") + pycafe_type = options.get("pycafe-type", pycafe_type) + requirements = "\n".join(options.get("requirements", "").split(",")) or requirements + extra_requirements = "\n".join(options.get("extra-requirements", "").split(",")) + requirements = requirements.rstrip() + "\n" + extra_requirements + + el = md.preprocessors["fenced_code_block"].extension.superfences[0]["formatter"]( + src=src, class_name=class_name, language=language, md=md, options=options, **kwargs + ) + try: + if pycafe_link: + url = pycafe_edit_url(code=src, requirements=requirements, app_type=pycafe_type) + text = "Run and edit above code in py.cafe" + target = "_blank" + el = el + f"""{text}""" + if pycafe_embed: + url = pycafe_embed_url(code=src, requirements=requirements, app_type=pycafe_type, theme=pycafe_embed_theme) + # e.g. + el = el + f"""""" + except Exception as e: + raise SuperFencesException from e + + return el + + +def pycafe_query(code: str, requirements: str): + json_object = {"code": code} + if requirements: + json_object["requirements"] = requirements + json_text = json.dumps(json_object) + # gzip -> base64 + compressed_json_text = gzip.compress(json_text.encode("utf8")) + base64_text = base64.b64encode(compressed_json_text).decode("utf8") + query = urlencode({"c": base64_text}, quote_via=quote_plus) + return query + + +def pycafe_edit_url(*, code: str, requirements, app_type): + query = pycafe_query(code, requirements) + return f"{base_url}/snippet/{app_type}/v1#{query}" + + +def pycafe_embed_url(*, code: str, requirements, app_type, theme="light"): + query = pycafe_query(code, requirements) + return f"{base_url}/embed?apptype={app_type}&theme={theme}&linkToApp=false#{query}" + + +def formatter(requirements="", type="solara"): # noqa: A002 + """Create a formatter with default requirements and type.""" + return partial(_formatter, pycafe_type=type, requirements=requirements) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..1edd7db --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2024-present Maarten A. Breddels +# +# SPDX-License-Identifier: MIT