From b219d5dc89ec6f61e72c74e40f338e6c132ea3df Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 5 Mar 2026 12:30:14 -0500 Subject: [PATCH 01/10] chore: add .github/.gitignore and js/.nvmrc Co-Authored-By: Claude Opus 4.6 --- .github/.gitignore | 1 + js/.nvmrc | 1 + 2 files changed, 2 insertions(+) create mode 100644 .github/.gitignore create mode 100644 js/.nvmrc diff --git a/.github/.gitignore b/.github/.gitignore new file mode 100644 index 00000000..2d19fc76 --- /dev/null +++ b/.github/.gitignore @@ -0,0 +1 @@ +*.html diff --git a/js/.nvmrc b/js/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/js/.nvmrc @@ -0,0 +1 @@ +22 From 9d50615a58f82265097cb5500774c763c3b95220 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 5 Mar 2026 12:32:59 -0500 Subject: [PATCH 02/10] chore: add Makefile mirroring shinychat conventions Co-Authored-By: Claude Opus 4.6 --- Makefile | 243 ++++++++++++++++++++++++++++++++++++ pkg-py/tests/test_output.py | 5 +- 2 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..828d787d --- /dev/null +++ b/Makefile @@ -0,0 +1,243 @@ +# Use qvm to manage quarto +QUARTO_VERSION ?= 1.7.31 +QUARTO_PATH = ~/.local/share/qvm/versions/v${QUARTO_VERSION}/bin/quarto +PATH_PKG_R := pkg-r +PATH_PKG_PY := pkg-py +PATH_PKG_JS := js + +.PHONY: install-quarto +install-quarto: + @echo "๐Ÿ”ต Installing quarto" + @if ! [ -z $(command -v qvm)]; then \ + @echo "Error: qvm is not installed. Please visit https://github.com/dpastoor/qvm/releases/ to install it." >&2 \ + exit 1; \ + fi + qvm install v${QUARTO_VERSION} + @echo "๐Ÿ”น Updating .vscode/settings.json" + @awk -v path="${QUARTO_PATH}" '/"quarto.path":/ {gsub(/"quarto.path": ".*"/, "\"quarto.path\": \"" path "\"")} 1' .vscode/settings.json > .vscode/settings.json.tmp && mv .vscode/settings.json.tmp .vscode/settings.json + @echo "๐Ÿ”น Updating .github/workflows/quartodoc.yaml" + @awk -v ver="${QUARTO_VERSION}" '/QUARTO_VERSION:/ {gsub(/QUARTO_VERSION: .*/, "QUARTO_VERSION: " ver)} 1' .github/workflows/quartodoc.yaml > .github/workflows/quartodoc.yaml.tmp && mv .github/workflows/quartodoc.yaml.tmp .github/workflows/quartodoc.yaml + + +.PHONY: docs +docs: r-docs-render py-docs-render ## [docs] Build the documentation + +.PHONY: docs-preview +docs-preview: ## [docs] Preview the documentation + @npx http-server docs -p 8080 + + +.PHONY: js-setup +js-setup: ## [js] Install JS dependencies + @echo "๐Ÿ†™ Setup JS dependencies" + cd $(PATH_PKG_JS) && npm install + +.PHONY: js-setup-ci +js-setup-ci: ## [js] Install JS dependencies as CI + @echo "๐Ÿ†™ Setup JS dependencies" + cd $(PATH_PKG_JS) && npm ci + +.PHONY: js-lint +js-lint: ## [js] Lint JS code + @echo "๐Ÿ“ Linting JS code" + cd $(PATH_PKG_JS) && npm run lint + +.PHONY: js-build +js-build: ## [js] Build JS code + @echo "๐Ÿงณ Building JS code" + cd $(PATH_PKG_JS) && npm run build + +.PHONY: js-build-watch +js-build-watch: ## [js] Build JS code in watch mode + @echo "๐Ÿงณ Building JS code in watch mode" + cd $(PATH_PKG_JS) && npm run watch + + +.PHONY: update-dist +update-dist: js-build r-update-dist py-update-dist ## Update shinyjson web assets in all packages + + +.PHONY: r-setup +r-setup: ## [r] Install R dependencies + @echo "๐Ÿ†™ Updating R dependencies" + cd $(PATH_PKG_R) && Rscript -e "pak::local_install_dev_deps()" + +.PHONY: r-check +r-check: r-check-format r-check-tests r-check-package ## [r] All R checks + +.PHONY: r-document +r-document: ## [r] Document package + @echo "๐Ÿ“œ Documenting R package" + cd $(PATH_PKG_R) && Rscript -e "devtools::document()" + +.PHONY: r-format +r-format: ## [r] Format R code + air format $(PATH_PKG_R)/ + +.PHONY: r-check-package +r-check-package: ## [r] Check package + @echo "" + @echo "๐Ÿ”„ Running R CMD Check" + cd $(PATH_PKG_R) && Rscript -e "devtools::check(document = FALSE)" + +.PHONY: r-check-tests +r-check-tests: ## [r] Check tests + @echo "" + @echo "๐Ÿงช Running R tests" + cd $(PATH_PKG_R) && Rscript -e "devtools::test()" + +.PHONY: r-check-format +r-check-format: ## [r] Check format + @echo "" + @echo "๐Ÿ“ Checking R format" + air format --check $(PATH_PKG_R)/ + +.PHONY: r-update-dist +r-update-dist: ## [r] Update shinyjson web assets + @echo "" + @echo "๐Ÿ”„ Updating shinyjson web assets" + if [ -d $(PATH_PKG_R)/inst/lib/shiny ]; then \ + rm -rf $(PATH_PKG_R)/inst/lib/shiny; \ + fi + mkdir -p $(PATH_PKG_R)/inst/lib/shiny + cp $(PATH_PKG_JS)/dist/shinyjson.js $(PATH_PKG_R)/inst/lib/shiny/ + cp $(PATH_PKG_JS)/dist/shinyjson.css $(PATH_PKG_R)/inst/lib/shiny/ + (git rev-parse HEAD) > "$(PATH_PKG_R)/inst/lib/shiny/GIT_VERSION" + +.PHONY: r-docs-render +r-docs-render: ## [r] Build R docs + @echo "๐Ÿ“– Rendering R docs with pkgdown" + cd $(PATH_PKG_R) && Rscript -e "pkgdown::build_site()" + +.PHONY: r-docs-preview +r-docs-preview: ## [r] Preview R docs + @echo "๐Ÿ“– Previewing R docs with pkgdown" + cd $(PATH_PKG_R) && Rscript -e "pkgdown::preview_site()" + + +.PHONY: py-setup +py-setup: ## [py] Setup python environment + uv sync --all-extras --all-groups --upgrade + +.PHONY: py-check +py-check: py-check-format py-check-types py-check-tests ## [py] Run python checks + +.PHONY: py-check-tox +py-check-tox: ## [py] Run python checks across versions with tox + @echo "" + @echo "๐Ÿ”„ Running tests and type checking with tox for Python 3.10--3.14" + uv run tox run-parallel + +.PHONY: py-check-tests +py-check-tests: ## [py] Run python tests + @echo "" + @echo "๐Ÿงช Running tests with pytest" + uv run playwright install + uv run pytest + +.PHONY: py-check-types +py-check-types: ## [py] Run python type checks + @echo "" + @echo "๐Ÿ“ Checking types with pyright" + uv run pyright + +.PHONY: py-check-format +py-check-format: + @echo "" + @echo "๐Ÿ“ Checking format with ruff" + uv run ruff check pkg-py --config pyproject.toml + +.PHONY: py-format +py-format: ## [py] Format python code + uv run ruff check --fix pkg-py --config pyproject.toml + uv run ruff format pkg-py --config pyproject.toml + +.PHONY: py-coverage +py-coverage: ## [py] Generate coverage report + @echo "๐Ÿ“” Generating coverage report" + uv run coverage run -m pytest + uv run coverage report + +.PHONY: py-coverage-report +py-coverage-report: py-coverage ## [py] Generate coverage report and open it in browser + uv run coverage html + @echo "" + @echo "๐Ÿ“ก Serving coverage report at http://localhost:8081/" + @npx http-server htmlcov --silent -p 8081 + +.PHONY: py-update-snaps +py-update-snaps: ## [py] Update python test snapshots + @echo "๐Ÿ“ธ Updating pytest snapshots" + uv run pytest --snapshot-update + +.PHONY: py-docs +py-docs: py-docs-api py-docs-render ## [py] Build python docs + +.PHONY: py-docs-render +py-docs-render: ## [py] Render python docs + @echo "๐Ÿ“– Rendering python docs with quarto" + @$(eval export IN_QUARTODOC=true) + ${QUARTO_PATH} render pkg-py/docs + +.PHONY: py-docs-preview +py-docs-preview: ## [py] Preview python docs + @echo "๐Ÿ“– Rendering python docs with quarto" + @$(eval export IN_QUARTODOC=true) + ${QUARTO_PATH} preview pkg-py/docs + +.PHONY: py-docs-api +py-docs-api: ## [py] Update python API docs + @echo "๐Ÿ“– Generating python docs with quartodoc" + @$(eval export IN_QUARTODOC=true) + cd pkg-py/docs && uv run quartodoc build + cd pkg-py/docs && uv run quartodoc interlinks + +.PHONY: py-docs-api-watch +py-docs-api-watch: ## [py] Update python docs + @echo "๐Ÿ“– Generating python docs with quartodoc" + @$(eval export IN_QUARTODOC=true) + uv run quartodoc build --config pkg-py/docs/_quarto.yml --watch + +.PHONY: py-docs-clean +py-docs-clean: ## [py] Clean python docs + @echo "๐Ÿงน Cleaning python docs" + rm -r pkg-py/docs/api + find pkg-py/docs/py -name '*.quarto_ipynb' -delete + +.PHONY: py-build +py-build: ## [py] Build python package + @echo "๐Ÿงณ Building python package" + @[ -d dist ] && rm -r dist || true + uv build + +.PHONY: py-update-dist +py-update-dist: ## [py] Update shinyjson web assets + @echo "" + @echo "๐Ÿ”„ Updating shinyjson web assets" + if [ -d $(PATH_PKG_PY)/src/shinyjson/www ]; then \ + rm -rf $(PATH_PKG_PY)/src/shinyjson/www; \ + fi + mkdir -p $(PATH_PKG_PY)/src/shinyjson/www + cp $(PATH_PKG_JS)/dist/shinyjson.js $(PATH_PKG_PY)/src/shinyjson/www/ + cp $(PATH_PKG_JS)/dist/shinyjson.css $(PATH_PKG_PY)/src/shinyjson/www/ + (git rev-parse HEAD) > "$(PATH_PKG_PY)/src/shinyjson/www/GIT_VERSION" + + +.PHONY: help +help: ## Show help messages for make targets + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; { \ + printf "\033[32m%-18s\033[0m", $$1; \ + if ($$2 ~ /^\[docs\]/) { \ + printf "\033[37m[docs]\033[0m%s\n", substr($$2, 7); \ + } else if ($$2 ~ /^\[py\]/) { \ + printf " \033[31m[py]\033[0m%s\n", substr($$2, 5); \ + } else if ($$2 ~ /^\[r\]/) { \ + printf " \033[34m[r]\033[0m%s\n", substr($$2, 4); \ + } else if ($$2 ~ /^\[js\]/) { \ + printf " \033[33m[js]\033[0m%s\n", substr($$2, 5); \ + } else { \ + printf " %s\n", $$2; \ + } \ + }' + +.DEFAULT_GOAL := help diff --git a/pkg-py/tests/test_output.py b/pkg-py/tests/test_output.py index d6e6aac7..0cfe67a5 100644 --- a/pkg-py/tests/test_output.py +++ b/pkg-py/tests/test_output.py @@ -1,5 +1,4 @@ from htmltools import HTMLDependency, Tag - from shinyjson._output import ui @@ -20,7 +19,9 @@ def test_ui_has_shinyjson_class(): def test_ui_accepts_extra_deps(): - dep = HTMLDependency("test", "1.0.0", source={"subdir": "/tmp"}, script={"src": "t.js"}) + dep = HTMLDependency( + "test", "1.0.0", source={"subdir": "/tmp"}, script={"src": "t.js"} + ) result = ui("my-output", extra_deps=[dep]) assert isinstance(result, Tag) From a773c2f8476a94536026584d38883e74bcd938bc Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 5 Mar 2026 12:34:24 -0500 Subject: [PATCH 03/10] fix: remove playwright install from py-check-tests Playwright is not a project dependency yet. The install step will be added back when e2e tests are introduced. Co-Authored-By: Claude Opus 4.6 --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 828d787d..afabc2b6 100644 --- a/Makefile +++ b/Makefile @@ -132,7 +132,6 @@ py-check-tox: ## [py] Run python checks across versions with tox py-check-tests: ## [py] Run python tests @echo "" @echo "๐Ÿงช Running tests with pytest" - uv run playwright install uv run pytest .PHONY: py-check-types From 311b80995e86540f36657f1b15b0711bc321d6cb Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 5 Mar 2026 12:35:12 -0500 Subject: [PATCH 04/10] ci: add py-check and verify-js-built workflows Co-Authored-By: Claude Opus 4.6 --- .github/workflows/py-check.yaml | 59 ++++++++++++++++++++++++++ .github/workflows/verify-js-built.yaml | 51 ++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 .github/workflows/py-check.yaml create mode 100644 .github/workflows/verify-js-built.yaml diff --git a/.github/workflows/py-check.yaml b/.github/workflows/py-check.yaml new file mode 100644 index 00000000..fac8424c --- /dev/null +++ b/.github/workflows/py-check.yaml @@ -0,0 +1,59 @@ +name: py-check.yaml + +on: + push: + branches: [main] + paths: + - "pkg-py/**/*" + - "pyproject.toml" + - ".github/workflows/py-check.yaml" + pull_request: + paths: + - "pkg-py/**/*" + - "pyproject.toml" + - ".github/workflows/py-check.yaml" + +permissions: + contents: read + +env: + UV_VERSION: "0.9.x" + +jobs: + py-test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + + steps: + - uses: actions/checkout@v4 + + - name: ๐Ÿš€ Install uv + uses: astral-sh/setup-uv@v6.1.0 + with: + version: ${{ env.UV_VERSION }} + + - name: ๐Ÿ Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: ๐Ÿ“ฆ Install the project + run: uv sync --python ${{ matrix.python-version }} --all-extras --all-groups + + - name: ๐Ÿ“œ Show uv.lock + run: cat uv.lock + + - name: ๐Ÿงช Check tests + run: make py-check-tests + + - name: ๐Ÿ“ Check types + run: make py-check-types + + - name: ๐Ÿ“ Check formatting + run: make py-check-format diff --git a/.github/workflows/verify-js-built.yaml b/.github/workflows/verify-js-built.yaml new file mode 100644 index 00000000..399a8ed7 --- /dev/null +++ b/.github/workflows/verify-js-built.yaml @@ -0,0 +1,51 @@ +name: verify-js-built.yaml + +on: + push: + branches: [main] + paths: + - "js/**/*" + - ".github/workflows/verify-js-built.yaml" + pull_request: + paths: + - "js/**/*" + - ".github/workflows/verify-js-built.yaml" + +jobs: + verify_js_built: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: ๐Ÿ”Ž Extract node version from .nvmrc + id: nvm + working-directory: js + run: | + echo "version=$(cat .nvmrc)" >> $GITHUB_OUTPUT + + - name: ๐ŸŸก Use Node.js + uses: actions/setup-node@v4 + with: + node-version: "${{ steps.nvm.outputs.version }}" + + - name: ๐Ÿ†™ Install dependencies + run: make js-setup-ci + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: ๐Ÿญ Build + run: make js-build + + - name: ๐Ÿง Check for uncommitted changes + run: | + if [[ `git status --porcelain` ]]; then + git diff + echo "Uncommitted changes found. Please commit any changes that result from 'make js-setup-ci js-build'." + exit 1 + else + echo "No uncommitted changes found." + fi + working-directory: js From ddef0c6962e5bbb255d58d7582a982c8b2218b56 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 5 Mar 2026 12:36:34 -0500 Subject: [PATCH 05/10] ci: add disabled workflows for R, release, and docs Co-Authored-By: Claude Opus 4.6 --- .github/workflows/R-CMD-check.yaml | 34 +++++++++++++++ .github/workflows/pkgdown.yaml | 69 +++++++++++++++++++++++++++++ .github/workflows/py-release.yaml | 60 +++++++++++++++++++++++++ .github/workflows/quartodoc.yaml | 70 ++++++++++++++++++++++++++++++ 4 files changed, 233 insertions(+) create mode 100644 .github/workflows/R-CMD-check.yaml create mode 100644 .github/workflows/pkgdown.yaml create mode 100644 .github/workflows/py-release.yaml create mode 100644 .github/workflows/quartodoc.yaml diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml new file mode 100644 index 00000000..393eae68 --- /dev/null +++ b/.github/workflows/R-CMD-check.yaml @@ -0,0 +1,34 @@ +# TODO: Enable when R package is implemented (has DESCRIPTION file). +# Uncomment the on: block below and remove the workflow_dispatch-only trigger. +# +# on: +# push: +# branches: [main] +# paths: +# - "pkg-r/**" +# - ".github/workflows/R-CMD-check.yaml" +# pull_request: +# paths: +# - "pkg-r/**" +# - ".github/workflows/R-CMD-check.yaml" + +name: R-CMD-check.yaml + +on: + workflow_dispatch: + +permissions: read-all + +jobs: + routine: + uses: rstudio/shiny-workflows/.github/workflows/routine.yaml@v1 + permissions: + contents: write + with: + working-directory: pkg-r + format-r-code: true + check-js: false + R-CMD-check: + uses: rstudio/shiny-workflows/.github/workflows/R-CMD-check.yaml@v1 + with: + working-directory: pkg-r diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml new file mode 100644 index 00000000..edbf83a7 --- /dev/null +++ b/.github/workflows/pkgdown.yaml @@ -0,0 +1,69 @@ +# TODO: Enable when R pkgdown is configured. +# Uncomment the on: block below and remove the workflow_dispatch-only trigger. +# +# on: +# push: +# branches: [main] +# paths: +# - "pkg-r/man/**/*" +# - "pkg-r/vignettes/**/*" +# - "pkg-r/pkgdown/**/*" +# - ".github/workflows/pkgdown.yaml" +# pull_request: +# paths: +# - "pkg-r/man/**/*" +# - "pkg-r/vignettes/**/*" +# - "pkg-r/pkgdown/**/*" +# - ".github/workflows/pkgdown.yaml" +# release: +# types: [published] +# workflow_dispatch: + +name: pkgdown.yaml + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + pkgdown: + runs-on: ubuntu-latest + concurrency: + group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + + # Only run on release events for tags starting with "r/v*" + if: github.event_name != 'release' || startsWith(github.ref, 'refs/tags/r/v') + + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::pkgdown, local::. + needs: website + working-directory: pkg-r + + - name: Build site + working-directory: pkg-r + shell: Rscript {0} + run: pkgdown::build_site() + + - name: Deploy to GitHub pages ๐Ÿš€ + if: github.event_name != 'pull_request' + uses: JamesIves/github-pages-deploy-action@v4.5.0 + with: + clean: false + branch: gh-pages + folder: docs diff --git a/.github/workflows/py-release.yaml b/.github/workflows/py-release.yaml new file mode 100644 index 00000000..b4b3fec8 --- /dev/null +++ b/.github/workflows/py-release.yaml @@ -0,0 +1,60 @@ +# TODO: Enable when PyPI project is created. +# Uncomment the on: block below and remove the workflow_dispatch-only trigger. +# Set up trusted publishing at: https://pypi.org/manage/project/shinyjson/settings/publishing/ +# +# on: +# release: +# types: [published] + +name: py-release.yaml + +on: + workflow_dispatch: + +env: + UV_VERSION: "0.9.x" + PYTHON_VERSION: 3.13 + +jobs: + py-release: + name: Build and release Python package + runs-on: ubuntu-latest + + # Python release tags start with "py/v*" + if: startsWith(github.ref, 'refs/tags/py/v') + + environment: + name: pypi + url: https://pypi.org/project/shinyjson/ + + permissions: + id-token: write + + steps: + - uses: actions/checkout@v4 + + - name: ๐Ÿš€ Install uv + uses: astral-sh/setup-uv@v6.1.0 + with: + version: ${{ env.UV_VERSION }} + + - name: ๐Ÿ Set up Python ${{ env.PYTHON_VERSION }} + run: uv python install ${{ env.PYTHON_VERSION }} + + - name: ๐Ÿ“ฆ Install the project + run: uv sync --python ${{ env.PYTHON_VERSION }} --all-extras --all-groups + + - name: ๐Ÿ“ Check types + run: make py-check-types + + - name: ๐Ÿ“ Check formatting + run: make py-check-format + + - name: ๐Ÿงช Check tests + run: make py-check-tests + + - name: ๐Ÿงณ Build package + run: uv build + + - name: ๐Ÿšข Publish release on PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/quartodoc.yaml b/.github/workflows/quartodoc.yaml new file mode 100644 index 00000000..79a13cbd --- /dev/null +++ b/.github/workflows/quartodoc.yaml @@ -0,0 +1,70 @@ +# TODO: Enable when quartodoc is configured in pkg-py/docs/. +# Uncomment the on: block below and remove the workflow_dispatch-only trigger. +# +# on: +# push: +# branches: +# - main +# paths: +# - "docs/**/*" +# - "pkg-py/**/*" +# - ".github/workflows/quartodoc.yaml" +# workflow_dispatch: + +name: quartodoc.yaml + +on: + workflow_dispatch: + +permissions: + contents: write + +env: + UV_VERSION: "0.9.x" + PYTHON_VERSION: 3.13 + QUARTO_VERSION: 1.7.31 + +jobs: + docs-publish: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: ๐Ÿ”ต Set up Quarto + uses: quarto-dev/quarto-actions/setup@v2 + with: + version: ${{ env.QUARTO_VERSION }} + + - name: ๐Ÿš€ Install uv + uses: astral-sh/setup-uv@v6.1.0 + with: + version: ${{ env.UV_VERSION }} + + - name: ๐Ÿ Set up Python ${{ env.PYTHON_VERSION }} + run: uv python install ${{ env.PYTHON_VERSION }} + + - name: ๐Ÿ“ฆ Install shinyjson and dependencies + run: uv sync --python ${{ env.PYTHON_VERSION }} --all-extras --all-groups + + - name: ๐Ÿ”Œ Activate venv + run: | + source .venv/bin/activate + echo "$VIRTUAL_ENV/bin" >> $GITHUB_PATH + echo "VIRTUAL_ENV=$VIRTUAL_ENV" >> $GITHUB_ENV + + - name: ๐Ÿญ Update Python docs + run: | + make py-docs-api + quarto render pkg-py/docs + + - name: ๐Ÿšข Deploy to GitHub pages + if: github.event_name != 'pull_request' + uses: JamesIves/github-pages-deploy-action@v4.5.0 + with: + clean: false + folder: docs + branch: gh-pages From de2d4188d4f71cc940898d0d6e1182c913103aa7 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 5 Mar 2026 12:38:56 -0500 Subject: [PATCH 06/10] fix: fix install-quarto shell syntax and add py-check-format to help - Use $$ for shell command substitution in Make recipe - Remove @ prefix inside continuation block - Add missing space before ] in test expression - Add ## comment to py-check-format so it appears in make help Co-Authored-By: Claude Opus 4.6 --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index afabc2b6..ff7d3b02 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,8 @@ PATH_PKG_JS := js .PHONY: install-quarto install-quarto: @echo "๐Ÿ”ต Installing quarto" - @if ! [ -z $(command -v qvm)]; then \ - @echo "Error: qvm is not installed. Please visit https://github.com/dpastoor/qvm/releases/ to install it." >&2 \ + @if [ -z "$$(command -v qvm)" ]; then \ + echo "Error: qvm is not installed. Please visit https://github.com/dpastoor/qvm/releases/ to install it." >&2; \ exit 1; \ fi qvm install v${QUARTO_VERSION} @@ -141,7 +141,7 @@ py-check-types: ## [py] Run python type checks uv run pyright .PHONY: py-check-format -py-check-format: +py-check-format: ## [py] Check python formatting @echo "" @echo "๐Ÿ“ Checking format with ruff" uv run ruff check pkg-py --config pyproject.toml From c0cdc99501b70a287db38c4e77268c0aa6e2fbb4 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 5 Mar 2026 12:39:26 -0500 Subject: [PATCH 07/10] chore: gitignore docs/plans/ Co-Authored-By: Claude Opus 4.6 --- .gitignore | 3 + docs/plans/2026-03-05-shinyjson-design.md | 114 --- .../2026-03-05-shinyjson-infrastructure.md | 891 ------------------ 3 files changed, 3 insertions(+), 1005 deletions(-) delete mode 100644 docs/plans/2026-03-05-shinyjson-design.md delete mode 100644 docs/plans/2026-03-05-shinyjson-infrastructure.md diff --git a/.gitignore b/.gitignore index ff6a6af8..2ca39ffe 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,9 @@ instance/ # Sphinx documentation docs/_build/ +# Design/plan documents +docs/plans/ + # PyBuilder .pybuilder/ target/ diff --git a/docs/plans/2026-03-05-shinyjson-design.md b/docs/plans/2026-03-05-shinyjson-design.md deleted file mode 100644 index 15a8863d..00000000 --- a/docs/plans/2026-03-05-shinyjson-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# shinyjson Design - -**Goal:** A minimal infrastructure package that lets other packages (e.g., `shinyshadcn`) build Shiny UI components powered by `vercel-labs/json-render`. - -**Architecture:** A monorepo with a single JS build (`js/`) whose output is copied into both a Python package (`pkg-py/`) and an R package (`pkg-r/`). The JS layer exposes a global registration API; downstream packages call it from their own JS bundle to register React components. The Python and R layers provide a render decorator + UI function following Shiny conventions. - -**Build order:** JS + repo infrastructure โ†’ Python package โ†’ R package. - ---- - -## Repo Structure - -``` -shinyjson/ -โ”œโ”€โ”€ js/ -โ”‚ โ”œโ”€โ”€ src/ -โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # window.shinyjson global + Shiny output binding -โ”‚ โ”‚ โ”œโ”€โ”€ registry.ts # registerComponents() + accumulated catalog/registry store -โ”‚ โ”‚ โ””โ”€โ”€ renderer.tsx # React component wrapping @json-render/react -โ”‚ โ”œโ”€โ”€ package.json -โ”‚ โ””โ”€โ”€ vite.config.ts # IIFE build โ†’ dist/shinyjson.{js,css} -โ”œโ”€โ”€ pkg-py/ -โ”‚ โ”œโ”€โ”€ src/shinyjson/ -โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”‚ โ”œโ”€โ”€ _render.py # @shinyjson.render (Renderer subclass) -โ”‚ โ”‚ โ”œโ”€โ”€ _output.py # shinyjson.ui() UI function -โ”‚ โ”‚ โ”œโ”€โ”€ _spec.py # shinyjson.Spec dataclass -โ”‚ โ”‚ โ””โ”€โ”€ www/ # js/dist/ copied here by `make update-dist` -โ”‚ โ”œโ”€โ”€ tests/ -โ”‚ โ””โ”€โ”€ pyproject.toml -โ”œโ”€โ”€ pkg-r/ -โ”‚ โ”œโ”€โ”€ R/ -โ”‚ โ””โ”€โ”€ inst/lib/shiny/ # js/dist/ copied here by `make update-dist` -โ”œโ”€โ”€ docs/ -โ”‚ โ””โ”€โ”€ plans/ -โ””โ”€โ”€ Makefile -``` - ---- - -## JS Layer - -- **Build:** Vite + TypeScript + React, output as a single IIFE bundle (`shinyjson.js`, `shinyjson.css`) -- **Global API:** - ```typescript - window.shinyjson.registerComponents(catalog: CatalogDef, registry: RegistryDef): void - ``` - Downstream packages call this once from their own JS bundle (loaded as a Shiny `HTMLDependency`). `shinyjson` accumulates all registrations before rendering. -- **Shiny output binding:** named `"shinyjson"` โ€” receives a JSON spec message from the server, mounts a React root in the container `
`, renders via `@json-render/react`'s `` using the accumulated registry. - ---- - -## Python Package API - -```python -# UI โ€” returns a
with the Shiny output binding + loads shinyjson.js/css -shinyjson.ui("my_id") - -# Server โ€” Renderer subclass; calls user fn, serializes Spec, sends to browser -@shinyjson.render -def my_id() -> shinyjson.Spec: - return shinyjson.Spec(root="card-1", elements={...}) -``` - -### `shinyjson.Spec` - -```python -@dataclass -class Spec: - root: str - elements: dict[str, Element] - - def to_dict(self) -> dict: ... -``` - -### Extension point - -`shinyjson.ui` accepts an `extra_deps` parameter for downstream packages to inject their own `HTMLDependency` (JS/CSS): - -```python -shinyjson.ui(id, extra_deps=[_shadcn_html_dep()]) -``` - -`shinyjson.render` can be subclassed; override `transform()` to convert downstream return types to `shinyjson.Spec`. - ---- - -## Extension Mechanism (how `shinyshadcn` is built on top) - -### JS (downstream package) - -```javascript -// shinyshadcn/src/index.ts โ€” loaded as HTMLDependency -window.shinyjson.registerComponents(catalog, registry); -``` - -### Python (downstream package) - -```python -# UI -def ui(id: str) -> Tag: - return shinyjson.ui(id, extra_deps=[_shadcn_html_dep()]) - -# Server -class render(shinyjson.render): - async def transform(self, value: ShadcnComponent) -> shinyjson.Spec: - return value.to_spec() -``` - -### What `shinyjson` explicitly does NOT provide - -- No components โ€” zero bundled UI elements -- No catalog โ€” downstream packages own their catalogs entirely -- No styling โ€” downstream packages own their CSS diff --git a/docs/plans/2026-03-05-shinyjson-infrastructure.md b/docs/plans/2026-03-05-shinyjson-infrastructure.md deleted file mode 100644 index 747492da..00000000 --- a/docs/plans/2026-03-05-shinyjson-infrastructure.md +++ /dev/null @@ -1,891 +0,0 @@ -# shinyjson Infrastructure Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Create the monorepo skeleton, JS IIFE bundle, and Python package for `shinyjson` โ€” the infrastructure layer that downstream packages (e.g. `shinyshadcn`) build Shiny JSON-render components on top of. - -**Architecture:** Phase 1 scaffolds the repo and builds a Vite IIFE bundle (`shinyjson.js`) that registers a Shiny output binding and exposes `window.shinyjson.registerComponents()`. Phase 2 creates the Python package with `shinyjson.ui()`, `@shinyjson.render`, and `shinyjson.Spec`, distributed with the JS assets via `HTMLDependency`. - -**Tech Stack:** TypeScript 5, React 18, `@json-render/core`, `@json-render/react`, Vite 5 (JS); Python 3.11+, `shiny>=1.2.0`, `htmltools`, `uv`, `pytest`, `pyright`, `ruff` (Python) - ---- - -## Phase 1: JS + Repo Infrastructure - ---- - -### Task 1: Create Directory Skeleton - -**Files:** -- Create: `js/src/` -- Create: `pkg-py/src/shinyjson/www/` -- Create: `pkg-py/tests/` -- Create: `pkg-r/R/` -- Create: `pkg-r/inst/lib/shiny/` - -**Step 1: Create all directories and placeholder files** - -```bash -mkdir -p js/src -mkdir -p pkg-py/src/shinyjson/www -mkdir -p pkg-py/tests -mkdir -p pkg-r/R -mkdir -p pkg-r/inst/lib/shiny - -# Placeholder Python package init (required before uv sync) -touch pkg-py/src/shinyjson/__init__.py -touch pkg-py/tests/__init__.py - -# Gitkeep for empty directories -touch pkg-py/src/shinyjson/www/.gitkeep -touch pkg-r/inst/lib/shiny/.gitkeep -``` - -**Step 2: Verify** - -```bash -find . -not -path './.git/*' -not -path './docs/*' -not -path './node_modules/*' | sort -``` - -Expected: All directories and placeholder files listed. - -**Step 3: Commit** - -```bash -git add pkg-py/ pkg-r/ js/ -git commit -m "chore: scaffold monorepo directory structure" -``` - ---- - -### Task 2: Root pyproject.toml + Python Environment - -**Files:** -- Create: `pyproject.toml` (at repo root) - -**Step 1: Create pyproject.toml** - -```toml -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "shinyjson" -version = "0.1.0" -description = "Shiny UI infrastructure for JSON-based component rendering" -requires-python = ">=3.11" -dependencies = [ - "shiny>=1.2.0", - "htmltools>=0.6.0", -] - -[tool.hatch.build.targets.wheel] -packages = ["pkg-py/src/shinyjson"] - -[dependency-groups] -dev = [ - "pytest>=8.0.0", - "pyright>=1.1.0", - "ruff>=0.6.0", - "pytest-cov>=5.0.0", -] - -[tool.pyright] -include = ["pkg-py/src/shinyjson"] -pythonVersion = "3.11" -typeCheckingMode = "basic" - -[tool.ruff] -line-length = 88 - -[tool.ruff.lint] -select = ["E", "F", "I"] -``` - -**Step 2: Install Python environment** - -```bash -uv sync --all-extras --all-groups -``` - -Expected: uv resolves dependencies and installs into `.venv/`. - -**Step 3: Verify install** - -```bash -uv run python -c "import shinyjson; print('ok')" -``` - -Expected: prints `ok`. - -**Step 4: Commit** - -```bash -git add pyproject.toml uv.lock -git commit -m "chore: add Python package configuration" -``` - ---- - -### Task 3: JS package.json + tsconfig - -**Files:** -- Create: `js/package.json` -- Create: `js/tsconfig.json` - -**Step 1: Verify the npm packages exist** - -```bash -npm view @json-render/core version -npm view @json-render/react version -``` - -Expected: Both print a version number. If either fails, check https://github.com/vercel-labs/json-render for the correct package names. - -**Step 2: Create js/package.json** - -```json -{ - "name": "@shinyjson/js", - "private": true, - "version": "0.1.0", - "type": "module", - "scripts": { - "build": "vite build", - "watch": "vite build --watch", - "lint": "tsc --noEmit" - }, - "dependencies": { - "@json-render/core": "latest", - "@json-render/react": "latest", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@vitejs/plugin-react": "^4.0.0", - "typescript": "^5.0.0", - "vite": "^5.0.0" - } -} -``` - -**Step 3: Create js/tsconfig.json** - -```json -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "strict": true - }, - "include": ["src"] -} -``` - -**Step 4: Install JS dependencies** - -```bash -cd js && npm install -``` - -Expected: `node_modules/` created, `package-lock.json` written. - -**Step 5: Commit** - -```bash -git add js/package.json js/tsconfig.json js/package-lock.json -git commit -m "chore: add JS package configuration and lockfile" -``` - ---- - -### Task 4: Vite Config + CSS Placeholder - -**Files:** -- Create: `js/vite.config.ts` -- Create: `js/src/shinyjson.css` - -**Step 1: Create js/vite.config.ts** - -```typescript -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -export default defineConfig({ - plugins: [react()], - build: { - lib: { - entry: "src/index.ts", - name: "shinyjson", - formats: ["iife"], - fileName: () => "shinyjson.js", - }, - outDir: "dist", - cssFileName: "shinyjson", - rollupOptions: { - // Bundle everything including React โ€” no externals - }, - }, -}); -``` - -**Step 2: Create js/src/shinyjson.css** - -```css -/* shinyjson base styles โ€” downstream packages provide component CSS */ -``` - -**Step 3: Commit** - -```bash -git add js/vite.config.ts js/src/shinyjson.css -git commit -m "chore: add Vite build configuration" -``` - ---- - -### Task 5: JS registry.ts - -**Files:** -- Create: `js/src/registry.ts` - -**Step 1: Create js/src/registry.ts** - -```typescript -import type { ComponentRegistry } from "@json-render/react"; - -// Accumulated component registry โ€” populated by downstream packages calling -// window.shinyjson.registerComponents() at page load. -let _registry: ComponentRegistry = {}; - -/** - * Register components from a downstream package (e.g. shinyshadcn). - * Called via window.shinyjson.registerComponents(catalog, registry). - * - * @param _catalog - Catalog definition (reserved for future validation) - * @param registry - Map of component name โ†’ React component - */ -function registerComponents( - _catalog: unknown, - registry: ComponentRegistry, -): void { - Object.assign(_registry, registry); -} - -/** - * Get the current accumulated registry for use by the renderer. - */ -function getRegistry(): ComponentRegistry { - return _registry; -} - -export { registerComponents, getRegistry }; -``` - -**Step 2: Type-check** - -```bash -cd js && npm run lint -``` - -Expected: No TypeScript errors. - ---- - -### Task 6: JS renderer.tsx - -**Files:** -- Create: `js/src/renderer.tsx` - -**Step 1: Create js/src/renderer.tsx** - -```tsx -import { Renderer } from "@json-render/react"; -import type { Spec } from "@json-render/core"; -import { getRegistry } from "./registry"; - -interface ShinyjsonRendererProps { - spec: Spec; -} - -/** - * React component that renders a json-render Spec using all registered components. - * The registry is read at render time, capturing all components registered via - * window.shinyjson.registerComponents() before this render was triggered. - */ -function ShinyjsonRenderer({ spec }: ShinyjsonRendererProps) { - return ; -} - -export { ShinyjsonRenderer }; -``` - -**Step 2: Type-check** - -```bash -cd js && npm run lint -``` - -Expected: No TypeScript errors. - ---- - -### Task 7: JS shiny.d.ts + index.ts - -**Files:** -- Create: `js/src/shiny.d.ts` -- Create: `js/src/index.ts` - -**Step 1: Create js/src/shiny.d.ts** - -Type declarations for Shiny's browser-global API. Shiny is loaded by the runtime before `shinyjson.js` โ€” these types are never bundled. - -```typescript -/** - * Type declarations for Shiny's global JavaScript API. - * Shiny is loaded by the browser before shinyjson.js runs. - */ -declare global { - const Shiny: { - OutputBinding: { - new (): ShinyOutputBinding; - prototype: ShinyOutputBinding; - }; - outputBindings: { - register(binding: ShinyOutputBinding, name: string): void; - }; - }; - - interface ShinyOutputBinding { - find(scope: Element): ArrayLike; - getId(el: Element): string; - renderValue(el: Element, data: unknown): void; - renderError(el: Element, err: { message: string; type?: string }): void; - clearError(el: Element): void; - showProgress(el: Element, show: boolean): void; - } -} - -export {}; -``` - -**Step 2: Create js/src/index.ts** - -```typescript -import React from "react"; -import { createRoot, type Root } from "react-dom/client"; -import type { Spec } from "@json-render/core"; -import type { ComponentRegistry } from "@json-render/react"; -import { registerComponents } from "./registry"; -import { ShinyjsonRenderer } from "./renderer"; -import "./shinyjson.css"; - -// Extend window with shinyjson's public global API -declare global { - interface Window { - shinyjson: { - registerComponents: (catalog: unknown, registry: ComponentRegistry) => void; - }; - } -} - -// Expose global API โ€” called by downstream packages at page load -window.shinyjson = { registerComponents }; - -// React root cache: one React root per output DOM element -const roots = new WeakMap(); - -function getOrCreateRoot(el: HTMLElement): Root { - if (!roots.has(el)) { - roots.set(el, createRoot(el)); - } - return roots.get(el)!; -} - -// Shiny output binding for .shinyjson-output elements -class ShinyjsonOutputBinding extends Shiny.OutputBinding { - find(scope: Element): NodeListOf { - return scope.querySelectorAll(".shinyjson-output"); - } - - renderValue(el: Element, data: Spec | null): void { - if (!data) return; - const root = getOrCreateRoot(el as HTMLElement); - root.render(React.createElement(ShinyjsonRenderer, { spec: data })); - } - - renderError(el: Element, err: { message: string }): void { - const root = getOrCreateRoot(el as HTMLElement); - root.render( - React.createElement( - "div", - { style: { color: "red", padding: "8px" } }, - err.message, - ), - ); - } -} - -// Register with Shiny โ€” Shiny is always loaded before this script -// because HTMLDependency ordering places Shiny's scripts first. -Shiny.outputBindings.register(new ShinyjsonOutputBinding(), "shinyjson.output"); -``` - -**Step 3: Type-check** - -```bash -cd js && npm run lint -``` - -Expected: No TypeScript errors. - ---- - -### Task 8: Build JS + Update Dist - -**Step 1: Build JS** - -```bash -cd js && npm run build -``` - -Expected: `js/dist/shinyjson.js` and `js/dist/shinyjson.css` created. The bundle will be large (~150KB+) because React is bundled. - -**Step 2: Verify build output** - -```bash -ls -lh js/dist/ -``` - -Expected: `shinyjson.js` and `shinyjson.css` present. - -**Step 3: Copy dist assets to Python package www/** - -```bash -cp -r js/dist/* pkg-py/src/shinyjson/www/ -git rev-parse HEAD > pkg-py/src/shinyjson/www/GIT_VERSION -``` - -**Step 4: Verify www/ contents** - -```bash -ls pkg-py/src/shinyjson/www/ -``` - -Expected: `shinyjson.js`, `shinyjson.css`, `GIT_VERSION`, `.gitkeep`. - -**Step 5: Commit** - -```bash -git add js/src/ js/dist/ pkg-py/src/shinyjson/www/ -git commit -m "feat: build JS IIFE bundle with Shiny output binding and global registry API" -``` - -Note: `js/dist/` is committed so the package can be published without requiring a JS build step by end users. - ---- - -## Phase 2: Python Package - ---- - -### Task 9: Python _spec.py (TDD) - -**Files:** -- Create: `pkg-py/tests/test_spec.py` -- Create: `pkg-py/src/shinyjson/_spec.py` - -**Step 1: Write the failing test** - -Create `pkg-py/tests/test_spec.py`: - -```python -from shinyjson._spec import Element, Spec - - -def test_element_to_dict_no_children(): - elem = Element(type="Card", props={"title": "Hello"}, children=[]) - assert elem.to_dict() == { - "type": "Card", - "props": {"title": "Hello"}, - "children": [], - } - - -def test_element_to_dict_with_children(): - elem = Element(type="Page", props={}, children=["card-1", "card-2"]) - assert elem.to_dict() == { - "type": "Page", - "props": {}, - "children": ["card-1", "card-2"], - } - - -def test_element_default_children(): - elem = Element(type="Metric", props={"value": 42}) - assert elem.to_dict()["children"] == [] - - -def test_spec_to_dict_single_element(): - spec = Spec( - root="card", - elements={"card": Element(type="Card", props={"title": "Hi"})}, - ) - assert spec.to_dict() == { - "root": "card", - "elements": { - "card": {"type": "Card", "props": {"title": "Hi"}, "children": []} - }, - } - - -def test_spec_to_dict_nested(): - spec = Spec( - root="page", - elements={ - "page": Element(type="Page", props={}, children=["card"]), - "card": Element(type="Card", props={"title": "Hi"}), - }, - ) - result = spec.to_dict() - assert result["root"] == "page" - assert result["elements"]["page"]["children"] == ["card"] - assert result["elements"]["card"]["type"] == "Card" -``` - -**Step 2: Run to verify it fails** - -```bash -uv run pytest pkg-py/tests/test_spec.py -v -``` - -Expected: `ImportError: cannot import name 'Element' from 'shinyjson._spec'` - -**Step 3: Write minimal implementation** - -Create `pkg-py/src/shinyjson/_spec.py`: - -```python -from dataclasses import dataclass, field -from typing import Any - - -@dataclass -class Element: - type: str - props: dict[str, Any] - children: list[str] = field(default_factory=list) - - def to_dict(self) -> dict[str, Any]: - return {"type": self.type, "props": self.props, "children": self.children} - - -@dataclass -class Spec: - root: str - elements: dict[str, Element] - - def to_dict(self) -> dict[str, Any]: - return { - "root": self.root, - "elements": { - key: elem.to_dict() for key, elem in self.elements.items() - }, - } -``` - -**Step 4: Run to verify it passes** - -```bash -uv run pytest pkg-py/tests/test_spec.py -v -``` - -Expected: 5 tests PASSED. - -**Step 5: Commit** - -```bash -git add pkg-py/src/shinyjson/_spec.py pkg-py/tests/test_spec.py -git commit -m "feat: add Spec and Element dataclasses" -``` - ---- - -### Task 10: Python _output.py (TDD) - -**Files:** -- Create: `pkg-py/tests/test_output.py` -- Create: `pkg-py/src/shinyjson/_output.py` - -**Step 1: Write the failing test** - -Create `pkg-py/tests/test_output.py`: - -```python -from htmltools import HTMLDependency, Tag - -from shinyjson._output import ui - - -def test_ui_returns_tag(): - result = ui("my-output") - assert isinstance(result, Tag) - - -def test_ui_has_correct_id(): - result = ui("my-output") - assert result.attrs.get("id") == "my-output" - - -def test_ui_has_shinyjson_class(): - result = ui("my-output") - classes = result.attrs.get("class", "") - assert "shinyjson-output" in classes - - -def test_ui_accepts_extra_deps(): - dep = HTMLDependency("test", "1.0.0", source="/tmp", script={"src": "t.js"}) - result = ui("my-output", extra_deps=[dep]) - assert isinstance(result, Tag) - - -def test_ui_no_extra_deps_by_default(): - result = ui("my-output") - assert isinstance(result, Tag) -``` - -**Step 2: Run to verify it fails** - -```bash -uv run pytest pkg-py/tests/test_output.py -v -``` - -Expected: `ImportError: cannot import name 'ui' from 'shinyjson._output'` - -**Step 3: Write minimal implementation** - -Create `pkg-py/src/shinyjson/_output.py`: - -```python -from pathlib import Path -from typing import Sequence - -from htmltools import HTMLDependency, Tag, div - - -def _dep() -> HTMLDependency: - return HTMLDependency( - name="shinyjson", - version="0.1.0", - source=str(Path(__file__).parent / "www"), - script={"src": "shinyjson.js"}, - stylesheet={"href": "shinyjson.css"}, - ) - - -def ui(id: str, extra_deps: Sequence[HTMLDependency] | None = None) -> Tag: - """Create a Shiny output placeholder for a shinyjson renderer. - - Args: - id: The output ID. Must match the server-side ``@shinyjson.render`` - function name. - extra_deps: Additional HTML dependencies to include. Used by downstream - packages to inject their own JS/CSS (e.g. ``shinyshadcn``). - - Returns: - A ``
`` tag that the shinyjson Shiny output binding renders into. - """ - return div( - id=id, - class_="shinyjson-output", - _dep(), - *(extra_deps or []), - ) -``` - -**Step 4: Run to verify it passes** - -```bash -uv run pytest pkg-py/tests/test_output.py -v -``` - -Expected: 5 tests PASSED. - -**Step 5: Commit** - -```bash -git add pkg-py/src/shinyjson/_output.py pkg-py/tests/test_output.py -git commit -m "feat: add shinyjson.ui() output placeholder function" -``` - ---- - -### Task 11: Python _render.py - -**Files:** -- Create: `pkg-py/src/shinyjson/_render.py` - -No TDD for this task โ€” testing a Shiny `Renderer` subclass requires a live Shiny session. The implementation is straightforward and verified by type-checking. - -**Step 1: Write implementation** - -Create `pkg-py/src/shinyjson/_render.py`: - -```python -from typing import Any - -from htmltools import Tag -from shiny.render.renderer import Renderer - -from ._output import ui -from ._spec import Spec - - -class render(Renderer[Spec]): - """Render a :class:`~shinyjson.Spec` as a reactive Shiny JSON output. - - Use this decorator on a server function that returns a - :class:`~shinyjson.Spec` instance. The spec is serialized and sent to the - browser, where the ``shinyjson`` Shiny output binding renders it using all - registered downstream components. - - Example:: - - @shinyjson.render - def my_output() -> shinyjson.Spec: - return shinyjson.Spec( - root="card", - elements={"card": shinyjson.Element(type="Card", props={"title": "Hi"})}, - ) - - Downstream packages subclass this to accept their own return types:: - - class render(shinyjson.render): - async def transform(self, value: MyComponent) -> Any: - return value.to_spec().to_dict() - """ - - async def transform(self, value: Spec) -> Any: - return value.to_dict() - - def auto_output_ui(self) -> Tag: - # Express mode: auto-generate the output container - return ui(self.output_id) -``` - -**Step 2: Type-check** - -```bash -uv run pyright pkg-py/src/shinyjson/_render.py -``` - -Expected: No errors. If `Renderer`'s `transform` signature causes type issues, add `# type: ignore[override]` on the `transform` method โ€” Shiny's internal types are not always fully exported. - -**Step 3: Commit** - -```bash -git add pkg-py/src/shinyjson/_render.py -git commit -m "feat: add shinyjson.render decorator" -``` - ---- - -### Task 12: Python __init__.py - -**Files:** -- Modify: `pkg-py/src/shinyjson/__init__.py` - -**Step 1: Replace the placeholder with the public API** - -```python -from ._output import ui -from ._render import render -from ._spec import Element, Spec - -__all__ = ["Element", "Spec", "render", "ui"] -``` - -**Step 2: Verify all exports are importable** - -```bash -uv run python -c " -import shinyjson -print(shinyjson.Spec) -print(shinyjson.Element) -print(shinyjson.render) -print(shinyjson.ui) -print('All exports OK') -" -``` - -Expected: Prints class/function names and `All exports OK`. - -**Step 3: Commit** - -```bash -git add pkg-py/src/shinyjson/__init__.py -git commit -m "feat: export public shinyjson API" -``` - ---- - -### Task 13: Full Python Checks - -Run all checks from the Makefile to ensure the package is clean. - -**Step 1: Run all tests** - -```bash -uv run pytest pkg-py/tests/ -v -``` - -Expected: All tests PASS (8 total: 5 spec + 5 output, minus any skipped). - -**Step 2: Run type checks** - -```bash -uv run pyright pkg-py/src/shinyjson -``` - -Expected: No errors. Document any known Shiny type stub gaps in a `# type: ignore` comment rather than working around them with `Any` everywhere. - -**Step 3: Run linter and formatter** - -```bash -uv run ruff check pkg-py/src/shinyjson -uv run ruff format --check pkg-py/src/shinyjson -``` - -If there are format issues: - -```bash -uv run ruff format pkg-py/src/shinyjson -uv run ruff check --fix pkg-py/src/shinyjson -``` - -**Step 4: Commit any fixes** - -```bash -git add pkg-py/src/shinyjson/ -git commit -m "chore: fix lint and format issues" -``` - -**Step 5: Verify full commit history** - -```bash -git log --oneline -``` - -Expected: ~10 commits showing the incremental progression from skeleton to full package. From a3946a5428c0b7fbf5c057faa1ef9192641e4643 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 5 Mar 2026 12:42:17 -0500 Subject: [PATCH 08/10] fix: add tox configuration for multi-version Python testing - Add tox and tox-uv as dev dependencies - Add [tool.tox] config testing across Python 3.11-3.14 - Fix version range in Makefile py-check-tox message Co-Authored-By: Claude Opus 4.6 --- Makefile | 2 +- pyproject.toml | 20 +++++++ uv.lock | 140 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ff7d3b02..237929ca 100644 --- a/Makefile +++ b/Makefile @@ -125,7 +125,7 @@ py-check: py-check-format py-check-types py-check-tests ## [py] Run python chec .PHONY: py-check-tox py-check-tox: ## [py] Run python checks across versions with tox @echo "" - @echo "๐Ÿ”„ Running tests and type checking with tox for Python 3.10--3.14" + @echo "๐Ÿ”„ Running tests and type checking with tox for Python 3.11--3.14" uv run tox run-parallel .PHONY: py-check-tests diff --git a/pyproject.toml b/pyproject.toml index 10ca9e65..fed57605 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,8 @@ dev = [ "pyright>=1.1.0", "ruff>=0.6.0", "pytest-cov>=5.0.0", + "tox>=4.0.0", + "tox-uv>=1.0.0", ] [tool.pyright] @@ -33,3 +35,21 @@ line-length = 88 [tool.ruff.lint] select = ["E", "F", "I"] + +[tool.tox] +legacy_tox_ini = """ +[tox] +env_list = type-py3{11,12,13,14}, py3{11,12,13,14} +isolated_build = True + +[testenv] +package = wheel +wheel_build_env = .pkg +commands = pytest + +[testenv:type] +deps = + pyright + pytest +commands = pyright +""" diff --git a/uv.lock b/uv.lock index 946f79a4..bf0ab87c 100644 --- a/uv.lock +++ b/uv.lock @@ -28,6 +28,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, ] +[[package]] +name = "cachetools" +version = "7.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/c7/342b33cc6877eebc6c9bb45cb9f78e170e575839699f6f3cc96050176431/cachetools-7.0.2.tar.gz", hash = "sha256:7e7f09a4ca8b791d8bb4864afc71e9c17e607a28e6839ca1a644253c97dbeae0", size = 36983, upload-time = "2026-03-02T19:45:16.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/04/4b6968e77c110f12da96fdbfcb39c6557c2e5e81bd7afcf8ed893d5bc588/cachetools-7.0.2-py3-none-any.whl", hash = "sha256:938dcad184827c5e94928c4fd5526e2b46692b7fb1ae94472da9131d0299343c", size = 13793, upload-time = "2026-03-02T19:45:15.495Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -153,6 +162,24 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -372,6 +399,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyproject-api" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330", size = 22785, upload-time = "2025-10-09T19:12:27.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09", size = 13218, upload-time = "2025-10-09T19:12:24.428Z" }, +] + [[package]] name = "pyright" version = "1.1.408" @@ -415,6 +454,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] +[[package]] +name = "python-discovery" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/bb/93a3e83bdf9322c7e21cafd092e56a4a17c4d8ef4277b6eb01af1a540a6f/python_discovery-1.1.0.tar.gz", hash = "sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268", size = 55674, upload-time = "2026-02-26T09:42:49.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/54/82a6e2ef37f0f23dccac604b9585bdcbd0698604feb64807dcb72853693e/python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b", size = 30687, upload-time = "2026-02-26T09:42:48.548Z" }, +] + [[package]] name = "python-multipart" version = "0.0.22" @@ -529,6 +581,8 @@ dev = [ { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, + { name = "tox" }, + { name = "tox-uv" }, ] [package.metadata] @@ -543,6 +597,8 @@ dev = [ { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-cov", specifier = ">=5.0.0" }, { name = "ruff", specifier = ">=0.6.0" }, + { name = "tox", specifier = ">=4.0.0" }, + { name = "tox-uv", specifier = ">=1.0.0" }, ] [[package]] @@ -612,6 +668,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] +[[package]] +name = "tox" +version = "4.47.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "colorama" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pluggy" }, + { name = "pyproject-api" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/62/182ae37b7472072086184e2246e01c6fb8399b87d76e0c79636774ee7117/tox-4.47.3.tar.gz", hash = "sha256:57643508d4c218ad312457a3b0ce3135c50fa1f9f1e4d40867683d880cad1c37", size = 254468, upload-time = "2026-03-04T02:39:01.842Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/3b/66b7b2cc1478b066e55eba27e2903db2f20bf363c14ac0e33c08df3364f8/tox-4.47.3-py3-none-any.whl", hash = "sha256:e447862a6821b421bbbfb8cbac071818c0a6884907a4c964d8322516d0b19b34", size = 202177, upload-time = "2026-03-04T02:39:00.482Z" }, +] + +[[package]] +name = "tox-uv" +version = "1.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tox-uv-bare" }, + { name = "uv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/51/9a6dd32e34a3ee200c7890497093875e2c0a0b08737bb897e5916c6575bc/tox_uv-1.33.1-py3-none-any.whl", hash = "sha256:0617caa6444097434cdef24477307ff3242021a44088df673ae08771d3657f79", size = 5364, upload-time = "2026-03-02T17:06:18.32Z" }, +] + +[[package]] +name = "tox-uv-bare" +version = "1.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tox" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/7b/5ce3aa477400c7791968037b3bf27a50a4e19160a111d9956d20e5ce6b06/tox_uv_bare-1.33.1.tar.gz", hash = "sha256:169185feb3cc8f321eb2a33c575c61dc6efd9bf6044b97636a7381261d29e85c", size = 27203, upload-time = "2026-03-02T17:06:21.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/8e/ae95104165f4e2da5d9d25d8c71c7c935227c3eeb88e0376dab48b787a1c/tox_uv_bare-1.33.1-py3-none-any.whl", hash = "sha256:e64fdcd607a0f66212ef9edb36a5a672f10b461fce2a8216dda3e93c45d4a3f9", size = 19718, upload-time = "2026-03-02T17:06:19.657Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -630,6 +730,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, ] +[[package]] +name = "uv" +version = "0.10.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/e7/600a90d4662dbd8414c1f6b709c8c79075d37d2044f72b94acbfaf29baad/uv-0.10.8.tar.gz", hash = "sha256:4b23242b5224c7eaea481ce6c6dbc210f0eafb447cf60211633980947cd23de4", size = 3936600, upload-time = "2026-03-03T21:35:22.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/6c/8ef256575242d5f3869c5a445ffd4363b91a89acb34a3e043bec2ad5a1be/uv-0.10.8-py3-none-linux_armv6l.whl", hash = "sha256:d214c82c7c14dd23f9aeb609d03070b8ea2b2f0cf249c9321cbbb5375a17e5df", size = 22461003, upload-time = "2026-03-03T21:35:20.093Z" }, + { url = "https://files.pythonhosted.org/packages/c9/fb/fd0656a92e6b9c4f92ddba7dcd76bd87469be500755125e06fea853dc212/uv-0.10.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1315c3901c5859aec2c5b4a17da4c5410d17f6890890f9f1a31f25aa0fa9ace", size = 21549446, upload-time = "2026-03-03T21:35:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/64/b9/1a4105df3afe7af99791f5b00fb037d85b2e3aaa1227e95878538d51ecf3/uv-0.10.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a253e5d2cae9e02654de31918b610dfc8f1f16a33f34046603757820bc45ee1b", size = 20222180, upload-time = "2026-03-03T21:35:46.984Z" }, + { url = "https://files.pythonhosted.org/packages/c5/72/6e98e0f8b3fe80cb881c36492dca6d932fbb05f956dfdccbdb8ebe4ceff4/uv-0.10.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:57a24e15fd9dd4a36bcec2ccbe4b26d2a172c109e954a8940f5e8a8b965dae74", size = 22064813, upload-time = "2026-03-03T21:35:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/737da8577f4b1799f7024f6cd98fffcac77076a1b078b277cffc84946e96/uv-0.10.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:675dc659195f9b9811ef5534eb3f16459fc88e109aefacbc91c07751b5b9715a", size = 22064861, upload-time = "2026-03-03T21:35:25.067Z" }, + { url = "https://files.pythonhosted.org/packages/7e/21/464ee3cd81f44345953cb26dd49870811f7647f3074f7651775cadb2158b/uv-0.10.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:18d2968b0a50111c2fc6b782f7c63ded4f461c44efab537f552cf565f9aaae25", size = 22054515, upload-time = "2026-03-03T21:35:44.572Z" }, + { url = "https://files.pythonhosted.org/packages/11/2c/1c592d7b843ffa999502116b0dc573732b40cb37061a4acc741dcdb181da/uv-0.10.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed3c7ebb6f757cddedb56dec3d7c745e5ea7310b11e12ae1c28f1e8172e7bbf", size = 23433992, upload-time = "2026-03-03T21:35:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e2/2b716f0613746138294598668bbe65295a8da3d8fa104a756dec6284bf3c/uv-0.10.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffaf115501e33be0d4f13cb5b7c2b46b031d4c679a6109e24a7edfb719c44c6c", size = 24257250, upload-time = "2026-03-03T21:35:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4d/0165e82cd1117cd6f8a7d9a2122c23cc091f7cf738aa4a2a54579420a08f/uv-0.10.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0209ee8cb573e113ff4a760360f28448f9ebcdcf9c91ca49e872821de5d2d054", size = 23338918, upload-time = "2026-03-03T21:35:33.795Z" }, + { url = "https://files.pythonhosted.org/packages/20/74/652129a25145732482bb0020602507f52d9a5ca0e1a40ddd6deb27402333/uv-0.10.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11dc790f732dc5fee61f0f6bd998fc2e9c200df1082245604ac091c32c23a523", size = 23259370, upload-time = "2026-03-03T21:35:39.478Z" }, + { url = "https://files.pythonhosted.org/packages/19/c5/6e5923d6c9e3b50dc8542647bea692b7c227a9489f59ddff4fdfb20d8459/uv-0.10.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e26f8c35684face38db814d452dd1a2181152dbf7f7b2de1f547e6ba0c378d67", size = 22174747, upload-time = "2026-03-03T21:35:42.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/cd/eee9e1883888327d07f51e7595ed5952e0bca2dc79d1c03b8a6e4309553e/uv-0.10.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:385add107d40c43dc00ca8c1a21ecf43101f846f8339eb7026bf6c9f6df7760d", size = 22893359, upload-time = "2026-03-03T21:35:30.802Z" }, + { url = "https://files.pythonhosted.org/packages/bf/36/407a22917e55ce5cc2e7af956e3b9d91648a96558858acef84e3c50d5ca8/uv-0.10.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:24e8eb28c4f05acb38e60fefe2a2b15f4283a3849ce580bf2a62aca0a13123b3", size = 22637451, upload-time = "2026-03-03T21:35:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/21/d5/dabef9914e1ff27ad95e4b1daf59cd97c80e26a44c04c2870bcca7c83fc0/uv-0.10.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:73a8c1a1fceac73cd983dcc0a64f4f94f5fd1e5428681a5a76132574264504fb", size = 23480991, upload-time = "2026-03-03T21:35:52.809Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c0/1a4a45a9246f087e9446d0d804a436f6ee0befeaef731b04d1b2802d9d8f/uv-0.10.8-py3-none-win32.whl", hash = "sha256:9f344fdb34938ce35e9211a1b866adfa0c7f043967652ed1431917514aeec062", size = 21579030, upload-time = "2026-03-03T21:35:28.176Z" }, + { url = "https://files.pythonhosted.org/packages/a4/2b/b29510efa1e6f409db105dbdafbd942ca3a2b638bef682ff2e5b9f6e4021/uv-0.10.8-py3-none-win_amd64.whl", hash = "sha256:1e63015284ed28c2112717256c328513215fb966a57c5870788eac2e8f949f28", size = 23944828, upload-time = "2026-03-03T21:36:00.763Z" }, + { url = "https://files.pythonhosted.org/packages/3f/9e/b5a11b0523171c0103c4fed54da76685a765ad4d3215e8220facfd24aed9/uv-0.10.8-py3-none-win_arm64.whl", hash = "sha256:a80284f46b6f2e0b3d03eb7c2d43e17139a4ec313e8b9f56a71efafc996804cb", size = 22322224, upload-time = "2026-03-03T21:35:14.148Z" }, +] + [[package]] name = "uvicorn" version = "0.41.0" @@ -643,6 +768,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, ] +[[package]] +name = "virtualenv" +version = "21.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, +] + [[package]] name = "watchfiles" version = "1.1.1" From 1e978d9a977cfd4c8fa576f78096c5c457cc2340 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 5 Mar 2026 12:45:55 -0500 Subject: [PATCH 09/10] feat: lower minimum Python to 3.10 No 3.11-specific features are used. The X | Y union syntax in _output.py and _render.py requires 3.10, which is the true minimum. Co-Authored-By: Claude Opus 4.6 --- Makefile | 2 +- pyproject.toml | 6 ++-- uv.lock | 78 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 237929ca..ff7d3b02 100644 --- a/Makefile +++ b/Makefile @@ -125,7 +125,7 @@ py-check: py-check-format py-check-types py-check-tests ## [py] Run python chec .PHONY: py-check-tox py-check-tox: ## [py] Run python checks across versions with tox @echo "" - @echo "๐Ÿ”„ Running tests and type checking with tox for Python 3.11--3.14" + @echo "๐Ÿ”„ Running tests and type checking with tox for Python 3.10--3.14" uv run tox run-parallel .PHONY: py-check-tests diff --git a/pyproject.toml b/pyproject.toml index fed57605..40de338d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "shinyjson" version = "0.1.0" description = "Shiny UI infrastructure for JSON-based component rendering" -requires-python = ">=3.11" +requires-python = ">=3.10" dependencies = [ "shiny>=1.2.0", "htmltools>=0.6.0", @@ -27,7 +27,7 @@ dev = [ [tool.pyright] include = ["pkg-py/src/shinyjson"] -pythonVersion = "3.11" +pythonVersion = "3.10" typeCheckingMode = "basic" [tool.ruff] @@ -39,7 +39,7 @@ select = ["E", "F", "I"] [tool.tox] legacy_tox_ini = """ [tox] -env_list = type-py3{11,12,13,14}, py3{11,12,13,14} +env_list = type-py3{10,11,12,13,14}, py3{10,11,12,13,14} isolated_build = True [testenv] diff --git a/uv.lock b/uv.lock index bf0ab87c..b99f066e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.11" +requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.12'", "python_full_version < '3.12'", @@ -11,6 +11,7 @@ name = "anyio" version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -23,6 +24,9 @@ wheels = [ name = "asgiref" version = "3.11.1" source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, @@ -64,6 +68,20 @@ version = "7.13.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, + { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, + { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, + { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, @@ -171,6 +189,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + [[package]] name = "filelock" version = "3.25.0" @@ -289,6 +319,19 @@ version = "3.11.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1a/a373746fa6d0e116dd9e54371a7b54622c44d12296d5d0f3ad5e3ff33490/orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174", size = 229140, upload-time = "2026-02-02T15:37:06.082Z" }, + { url = "https://files.pythonhosted.org/packages/52/a2/fa129e749d500f9b183e8a3446a193818a25f60261e9ce143ad61e975208/orjson-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63c6e6738d7c3470ad01601e23376aa511e50e1f3931395b9f9c722406d1a67", size = 128670, upload-time = "2026-02-02T15:37:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/1e82011cd1e0bd051ef9d35bed1aa7fb4ea1f0a055dc2c841b46b43a9ebd/orjson-3.11.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043d3006b7d32c7e233b8cfb1f01c651013ea079e08dcef7189a29abd8befe11", size = 123832, upload-time = "2026-02-02T15:37:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d8/a26b431ef962c7d55736674dddade876822f3e33223c1f47a36879350d04/orjson-3.11.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57036b27ac8a25d81112eb0cc9835cd4833c5b16e1467816adc0015f59e870dc", size = 129171, upload-time = "2026-02-02T15:37:11.112Z" }, + { url = "https://files.pythonhosted.org/packages/a7/19/f47819b84a580f490da260c3ee9ade214cf4cf78ac9ce8c1c758f80fdfc9/orjson-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:733ae23ada68b804b222c44affed76b39e30806d38660bf1eb200520d259cc16", size = 141967, upload-time = "2026-02-02T15:37:12.282Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cd/37ece39a0777ba077fdcdbe4cccae3be8ed00290c14bf8afdc548befc260/orjson-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fdfad2093bdd08245f2e204d977facd5f871c88c4a71230d5bcbd0e43bf6222", size = 130991, upload-time = "2026-02-02T15:37:13.465Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ed/f2b5d66aa9b6b5c02ff5f120efc7b38c7c4962b21e6be0f00fd99a5c348e/orjson-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cededd6738e1c153530793998e31c05086582b08315db48ab66649768f326baa", size = 133674, upload-time = "2026-02-02T15:37:14.694Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6e/baa83e68d1aa09fa8c3e5b2c087d01d0a0bd45256de719ed7bc22c07052d/orjson-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:14f440c7268c8f8633d1b3d443a434bd70cb15686117ea6beff8fdc8f5917a1e", size = 138722, upload-time = "2026-02-02T15:37:16.501Z" }, + { url = "https://files.pythonhosted.org/packages/0c/47/7f8ef4963b772cd56999b535e553f7eb5cd27e9dd6c049baee6f18bfa05d/orjson-3.11.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3a2479753bbb95b0ebcf7969f562cdb9668e6d12416a35b0dda79febf89cdea2", size = 409056, upload-time = "2026-02-02T15:37:17.895Z" }, + { url = "https://files.pythonhosted.org/packages/38/eb/2df104dd2244b3618f25325a656f85cc3277f74bbd91224752410a78f3c7/orjson-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:71924496986275a737f38e3f22b4e0878882b3f7a310d2ff4dc96e812789120c", size = 144196, upload-time = "2026-02-02T15:37:19.349Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2a/ee41de0aa3a6686598661eae2b4ebdff1340c65bfb17fcff8b87138aab21/orjson-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4a9eefdc70bf8bf9857f0290f973dec534ac84c35cd6a7f4083be43e7170a8f", size = 134979, upload-time = "2026-02-02T15:37:20.906Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/92fc5d3d402b87a8b28277a9ed35386218a6a5287c7fe5ee9b9f02c53fb2/orjson-3.11.7-cp310-cp310-win32.whl", hash = "sha256:ae9e0b37a834cef7ce8f99de6498f8fad4a2c0bf6bfc3d02abd8ed56aa15b2de", size = 127968, upload-time = "2026-02-02T15:37:23.178Z" }, + { url = "https://files.pythonhosted.org/packages/07/29/a576bf36d73d60df06904d3844a9df08e25d59eba64363aaf8ec2f9bff41/orjson-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:d772afdb22555f0c58cfc741bdae44180122b3616faa1ecadb595cd526e4c993", size = 125128, upload-time = "2026-02-02T15:37:24.329Z" }, { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, @@ -405,6 +448,7 @@ version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330", size = 22785, upload-time = "2025-10-09T19:12:27.21Z" } wheels = [ @@ -430,10 +474,12 @@ version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ @@ -680,6 +726,8 @@ dependencies = [ { name = "platformdirs" }, { name = "pluggy" }, { name = "pyproject-api" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/87/62/182ae37b7472072086184e2246e01c6fb8399b87d76e0c79636774ee7117/tox-4.47.3.tar.gz", hash = "sha256:57643508d4c218ad312457a3b0ce3135c50fa1f9f1e4d40867683d880cad1c37", size = 254468, upload-time = "2026-03-04T02:39:01.842Z" } @@ -705,6 +753,7 @@ version = "1.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tox" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b0/7b/5ce3aa477400c7791968037b3bf27a50a4e19160a111d9956d20e5ce6b06/tox_uv_bare-1.33.1.tar.gz", hash = "sha256:169185feb3cc8f321eb2a33c575c61dc6efd9bf6044b97636a7381261d29e85c", size = 27203, upload-time = "2026-03-02T17:06:21.118Z" } @@ -762,6 +811,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } wheels = [ @@ -777,6 +827,7 @@ dependencies = [ { name = "filelock" }, { name = "platformdirs" }, { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } wheels = [ @@ -792,6 +843,18 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, @@ -864,6 +927,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, @@ -885,6 +952,15 @@ version = "16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, From abd655e411862c0edf4aced54712ff1a845692a2 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 5 Mar 2026 12:50:25 -0500 Subject: [PATCH 10/10] ci: add conventional commits PR title linter Co-Authored-By: Claude Opus 4.6 --- .../verify-conventional-commits.yaml | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/verify-conventional-commits.yaml diff --git a/.github/workflows/verify-conventional-commits.yaml b/.github/workflows/verify-conventional-commits.yaml new file mode 100644 index 00000000..bfe3bd93 --- /dev/null +++ b/.github/workflows/verify-conventional-commits.yaml @@ -0,0 +1,48 @@ +name: Conventional Commits +on: + pull_request: + types: + - opened + - edited + - synchronize +jobs: + conventional-commits: + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + id: lint + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + build + chore + ci + docs + feat + fix + perf + refactor + release + style + test + - uses: marocchino/sticky-pull-request-comment@v2 + if: always() && (steps.lint.outputs.error_message != null) + with: + header: lint-error + message: | + Hey there! ๐Ÿ‘‹ + + We noticed that the title of your pull request doesn't follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. To ensure consistency, we kindly ask you to adjust the title accordingly. + +
Additional details + + ${{ steps.lint.outputs.error_message }} + +
+ + - if: ${{ steps.lint.outputs.error_message == null }} + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: lint-error + delete: true