diff --git a/.github/py-shiny/setup/action.yaml b/.github/py-shiny/setup/action.yaml index e96f99619..63b1e86e2 100644 --- a/.github/py-shiny/setup/action.yaml +++ b/.github/py-shiny/setup/action.yaml @@ -17,15 +17,10 @@ runs: # cache-dependency-path: | # setup.cfg - - name: Upgrade pip - shell: bash - run: python -m pip install --upgrade pip - - name: Install dependencies shell: bash run: | - pip install https://github.com/rstudio/py-htmltools/tarball/main - make install-deps + make install-ci - name: Install shell: bash diff --git a/.github/workflows/build-docs.yaml b/.github/workflows/build-docs.yaml index 7cc001a61..5929db954 100644 --- a/.github/workflows/build-docs.yaml +++ b/.github/workflows/build-docs.yaml @@ -23,30 +23,15 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Upgrade pip - run: python -m pip install --upgrade pip - - name: Install Quarto uses: quarto-dev/quarto-actions/setup@v2 with: version: 1.3.340 - - name: Install dependencies - run: | - cd docs - make ../venv - make deps - - - name: Run quartodoc - run: | - cd docs - make quartodoc - - name: Build site if: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'docs') }} run: | - cd docs - make site + make docs-site - name: Upload site artifact if: github.ref == 'refs/heads/main' diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index ddc857c25..a75b7c46d 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -220,9 +220,7 @@ jobs: python-version: "3.10" - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install https://github.com/rstudio/py-htmltools/tarball/main - make install-deps + make install-ci make install - name: "Build Package" run: | diff --git a/Makefile b/Makefile index 0f183acfa..7f0d5d636 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # Prerequisites of .PHONY are always interpreted as literal target names, never as patterns (even if they contain ‘%’ characters). # # .PHONY: help clean% check% format% docs% lint test pyright playwright% install% testrail% coverage release js-* # Using `FORCE` as prerequisite to _force_ the target to always run; https://www.gnu.org/software/make/manual/make.html#index-FORCE -FORCE: ; +FORCE: .DEFAULT_GOAL := help @@ -26,11 +26,150 @@ for line in sys.stdin: endef export PRINT_HELP_PYSCRIPT -BROWSER := python -c "$$BROWSER_PYSCRIPT" + +BROWSER := $(PYTHON) -c "$$BROWSER_PYSCRIPT" help: FORCE - @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + @python3 -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + + +# Gather python3 version; Ex: `3.11` +PYTHON_VERSION := $(shell python3 -c "from platform import python_version; print(python_version().rsplit('.', 1)[0])") + +# ----------------- +# File paths for common executables / paths +# ----------------- + +VENV = .venv +# VENV = .venv-$(PYTHON_VERSION) +PYBIN = $(VENV)/bin +PIP = $(PYBIN)/pip +PYTHON = $(PYBIN)/python +SITE_PACKAGES=$(VENV)/lib/python$(PYTHON_VERSION)/site-packages + +# ----------------- +# Core virtual environment and installing +# ----------------- + +# Any targets that depend on $(VENV) or $(PYBIN) will cause the venv to be +# created. To use the venv, python scripts should run with the prefix $(PYBIN), +# as in `$(PYBIN)/pip`. +$(VENV): + @echo "-------- Making virtual environment: $(VENV) --------" + python3 -m venv $(VENV) + $(PYBIN)/pip install --upgrade pip + +$(PYBIN): $(VENV) +$(PYTHON): $(PYBIN) +$(PIP): $(PYBIN) + +UV = $(SITE_PACKAGES)/uv +$(UV): + $(MAKE) $(PYBIN) + @echo "-------- Installing uv --------" + . $(PYBIN)/activate && \ + pip install uv + + +# ----------------- +# Python package executables +# ----------------- +# Use `FOO=$(PYBIN)/foo` to define the path to a package's executable +# Depend on `$(FOO)` to ensure the package is installed, +# but use `$(PYBIN)/foo` to actually run the package's executable +BLACK = $(SITE_PACKAGES)/black +ISORT = $(SITE_PACKAGES)/isort +FLAKE8 = $(SITE_PACKAGES)/flake8 +PYTEST = $(SITE_PACKAGES)/pytest +COVERAGE = $(SITE_PACKAGES)/coverage +PYRIGHT = $(SITE_PACKAGES)/pyright +PLAYWRIGHT = $(SITE_PACKAGES)/playwright +$(RUFF) $(PYTEST) $(COVERAGE) $(PYRIGHT) $(PLAYWRIGHT): + @$(MAKE) install-deps + + +# ----------------- +# Helper packages not defined in `setup.cfg` +# ----------------- + +TRCLI = $(SITE_PACKAGES)/trcli +$(TRCLI): $(UV) + @echo "-------- Installing trcli --------" + . $(PYBIN)/activate && \ + uv pip install trcli + +TWINE = $(SITE_PACKAGES)/twine +$(TWINE): $(UV) + @echo "-------- Installing twine --------" + . $(PYBIN)/activate && \ + uv pip install twine + +RSCONNECT = $(SITE_PACKAGES)/rsconnect +# Install the main version of rsconnect till pypi version supports shiny express +$(RSCONNECT): $(UV) + @echo "-------- Installing rsconnect --------" + . $(PYBIN)/activate && \ + uv pip install "rsconnect-python @ git+https://github.com/rstudio/rsconnect-python.git" + + +# ----------------- +# Type stubs +# ----------------- + +typings/appdirs: $(PYRIGHT) + @echo "-------- Creating stub for appdirs --------" + . $(PYBIN)/activate && \ + pyright --createstub appdirs +typings/folium: $(PYRIGHT) + @echo "-------- Creating stub for folium --------" + . $(PYBIN)/activate && \ + pyright --createstub folium +typings/uvicorn: $(PYRIGHT) + @echo "-------- Creating stub for uvicorn --------" + . $(PYBIN)/activate && \ + pyright --createstub uvicorn +typings/seaborn: $(PYRIGHT) + @echo "-------- Creating stub for seaborn --------" + . $(PYBIN)/activate && \ + pyright --createstub seaborn + +typings/matplotlib/__init__.pyi: ## grab type stubs from GitHub + @echo "-------- Creating stub for matplotlib --------" + mkdir -p typings + git clone --depth 1 https://github.com/microsoft/python-type-stubs typings/python-type-stubs + mv typings/python-type-stubs/stubs/matplotlib typings/ + rm -rf typings/python-type-stubs + +pyright-typings: typings/appdirs typings/folium typings/uvicorn typings/seaborn typings/matplotlib/__init__.pyi + +# ----------------- +# Install +# ----------------- +## install the package to the active Python's site-packages +# Note that instead of --force-reinstall, we uninstall and then install, because +# --force-reinstall also reinstalls all deps. And if we also used --no-deps, then the +# deps wouldn't be installed the first time. +install: dist $(PIP) FORCE + . $(PYBIN)/activate && \ + pip uninstall -y shiny && \ + pip install dist/shiny*.whl + +install-deps: $(UV) FORCE ## install dependencies + . $(PYBIN)/activate && \ + uv pip install -e ".[dev,test]" --refresh + +install-ci: $(UV) FORCE ## install dependencies for CI + . $(PYBIN)/activate && \ + uv pip install -e ".[dev,test]" --refresh \ + "htmltools @ git+https://github.com/posit-dev/py-htmltools.git" + +# ## If caching is ever used, we could run: +# install-deps: ## install latest dependencies +# pip install --editable ".[dev,test]" --upgrade --upgrade-strategy eager +# ----------------- +# Clean files +# ----------------- clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts # Remove build artifacts @@ -56,27 +195,10 @@ clean-test: FORCE rm -fr .pytest_cache rm -rf typings/ -typings/appdirs: - @echo "Creating appdirs stubs" - pyright --createstub appdirs -typings/folium: - @echo "Creating folium stubs" - pyright --createstub folium -typings/uvicorn: - @echo "Creating uvicorn stubs" - pyright --createstub uvicorn -typings/seaborn: - @echo "Creating seaborn stubs" - pyright --createstub seaborn -typings/matplotlib/__init__.pyi: - @echo "Creating matplotlib stubs" - mkdir -p typings - git clone --depth 1 https://github.com/microsoft/python-type-stubs typings/python-type-stubs - mv typings/python-type-stubs/stubs/matplotlib typings/ - rm -rf typings/python-type-stubs - -pyright-typings: typings/appdirs typings/folium typings/uvicorn typings/seaborn typings/matplotlib/__init__.pyi +# ----------------- +# Check lint, test, and format of code +# ----------------- check: check-format check-lint check-types check-tests ## check code, style, types, and test (basic CI) check-fix: format check-lint check-types check-tests ## check and format code, style, types, and test check-format: check-black check-isort @@ -84,22 +206,27 @@ check-lint: check-flake8 check-types: check-pyright check-tests: check-pytest -check-flake8: FORCE +check-flake8: $(FLAKE8) FORCE @echo "-------- Checking style with flake8 ---------" - flake8 --show-source . -check-black: FORCE + . $(PYBIN)/activate && \ + flake8 --show-source . +check-black: $(BLACK) FORCE @echo "-------- Checking code with black -----------" - black --check . -check-isort: FORCE + . $(PYBIN)/activate && \ + black --check . +check-isort: $(ISORT) FORCE @echo "-------- Sorting imports with isort ---------" - isort --check-only --diff . -check-pyright: pyright-typings + . $(PYBIN)/activate && \ + isort --check-only --diff . +check-pyright: $(PYRIGHT) pyright-typings @echo "-------- Checking types with pyright --------" - pyright -check-pytest: FORCE + . $(PYBIN)/activate && \ + pyright +check-pytest: $(PYTEST) FORCE @echo "-------- Running tests with pytest ----------" - python3 tests/pytest/asyncio_prevent.py - pytest + . $(PYBIN)/activate && \ + python tests/pytest/asyncio_prevent.py && \ + pytest # Check types with pyright pyright: check-types @@ -115,15 +242,36 @@ format-isort: FORCE @echo "-------- Sorting imports with isort --------" isort . -docs: FORCE ## docs: build docs with quartodoc - @echo "-------- Building docs with quartodoc ------" - @cd docs && make quartodoc +# ----------------- +# Documentation +# ----------------- +# Install docs deps; Used in `./docs/Makefile` +install-docs: $(UV) FORCE + . $(PYBIN)/activate && \ + uv pip install -e ".[dev,test,doc]" \ + "htmltools @ git+https://github.com/posit-dev/py-htmltools.git" \ + "shinylive @ git+https://github.com/posit-dev/py-shinylive.git" + +docs: docs-serve FORCE ## docs: build and serve docs in browser -docs-preview: FORCE ## docs: preview docs in browser +docs-serve: $(PYBIN) FORCE ## docs: build and serve docs in browser + $(MAKE) docs-quartodoc @echo "-------- Previewing docs in browser --------" @cd docs && make serve +docs-site: $(PYBIN) FORCE ## docs: build docs site + $(MAKE) docs-quartodoc + @echo "-------- Previewing docs in browser --------" + @cd docs && make site + +docs-quartodoc: $(PYBIN) FORCE ## docs: build quartodoc docs + $(MAKE) install-docs + @echo "-------- Building docs with quartodoc --------" + @cd docs && make quartodoc +# ----------------- +# JS assets +# ----------------- install-npm: FORCE $(if $(shell which npm), @echo -n, $(error Please install node.js and npm first. See https://nodejs.org/en/download/ for instructions.)) js/node_modules: install-npm @@ -142,65 +290,59 @@ clean-js: FORCE @echo "-------- Removing js/node_modules ----------" rm -rf js/node_modules + +# ----------------- +# Testing with playwright +# ----------------- + # Default `SUB_FILE` to empty SUB_FILE:= PYTEST_BROWSERS:= --browser webkit --browser firefox --browser chromium -install-playwright: FORCE - playwright install --with-deps - -install-trcli: FORCE - $(if $(shell which trcli), @echo -n, $(shell pip install trcli)) -# Installs the main version of rsconnect till pypi version supports shiny express -install-rsconnect: FORCE - pip install git+https://github.com/rstudio/rsconnect-python.git#egg=rsconnect-python +install-playwright: $(PLAYWRIGHT) FORCE + @echo "-------- Installing playwright browsers --------" + @. $(PYBIN)/activate && \ + playwright install --with-deps -# end-to-end tests with playwright; (SUB_FILE="" within tests/playwright/shiny/) -playwright-shiny: install-playwright - pytest tests/playwright/shiny/$(SUB_FILE) $(PYTEST_BROWSERS) +playwright-shiny: install-playwright $(PYTEST) FORCE ## end-to-end tests with playwright; (SUB_FILE="" within tests/playwright/shiny/) + . $(PYBIN)/activate && \ + pytest tests/playwright/shiny/$(SUB_FILE) $(PYTEST_BROWSERS) -# end-to-end tests on deployed apps with playwright; (SUB_FILE="" within tests/playwright/deploys/) -playwright-deploys: install-playwright install-rsconnect - pytest tests/playwright/deploys/$(SUB_FILE) $(PYTEST_BROWSERS) +playwright-deploys: install-playwright $(RSCONNECT) $(PYTEST) FORCE ## end-to-end tests on deployed apps with playwright; (SUB_FILE="" within tests/playwright/deploys/) + . $(PYBIN)/activate && \ + pytest tests/playwright/deploys/$(SUB_FILE) $(PYTEST_BROWSERS) -# end-to-end tests on all py-shiny examples with playwright; (SUB_FILE="" within tests/playwright/examples/) -playwright-examples: install-playwright - pytest tests/playwright/examples/$(SUB_FILE) $(PYTEST_BROWSERS) +playwright-examples: install-playwright $(PYTEST) FORCE ## end-to-end tests on all py-shiny examples with playwright; (SUB_FILE="" within tests/playwright/examples/) + . $(PYBIN)/activate && \ + pytest tests/playwright/examples/$(SUB_FILE) $(PYTEST_BROWSERS) -playwright-debug: install-playwright ## All end-to-end tests, chrome only, headed; (SUB_FILE="" within tests/playwright/) - pytest -c tests/playwright/playwright-pytest.ini tests/playwright/$(SUB_FILE) +playwright-debug: install-playwright $(PYTEST) FORCE ## All end-to-end tests, chrome only, headed; (SUB_FILE="" within tests/playwright/) + . $(PYBIN)/activate && \ + pytest -c tests/playwright/playwright-pytest.ini tests/playwright/$(SUB_FILE) -playwright-show-trace: ## Show trace of failed tests +playwright-show-trace: FORCE ## Show trace of failed tests npx playwright show-trace test-results/*/trace.zip # end-to-end tests with playwright and generate junit report -testrail-junit: install-playwright install-trcli - pytest tests/playwright/shiny/$(SUB_FILE) --junitxml=report.xml $(PYTEST_BROWSERS) - -coverage: FORCE ## check combined code coverage (must run e2e last) - pytest --cov-report term-missing --cov=shiny tests/pytest/ tests/playwright/shiny/$(SUB_FILE) $(PYTEST_BROWSERS) - coverage html - $(BROWSER) htmlcov/index.html - -release: dist ## package and upload a release - twine upload dist/* - -dist: clean ## builds source and wheel package - python3 setup.py sdist - python3 setup.py bdist_wheel +testrail-junit: install-playwright $(TRCLI) $(PYTEST) FORCE + . $(PYBIN)/activate && \ + pytest tests/playwright/shiny/$(SUB_FILE) --junitxml=report.xml $(PYTEST_BROWSERS) + +coverage: $(PYTEST) $(COVERAGE) FORCE ## check combined code coverage (must run e2e last) + . $(PYBIN)/activate && \ + pytest --cov-report term-missing --cov=shiny tests/pytest/ tests/playwright/shiny/$(SUB_FILE) $(PYTEST_BROWSERS) && \ + coverage html && \ + $(BROWSER) htmlcov/index.html + +# ----------------- +# Release +# ----------------- +release: $(TWINE) dist FORCE ## package and upload a release + . $(PYBIN)/activate && \ + twine upload dist/* + +dist: clean $(PYTHON) FORCE ## builds source and wheel package + . $(PYBIN)/activate && \ + python setup.py sdist && \ + python setup.py bdist_wheel ls -l dist - -## install the package to the active Python's site-packages -# Note that instead of --force-reinstall, we uninstall and then install, because -# --force-reinstall also reinstalls all deps. And if we also used --no-deps, then the -# deps wouldn't be installed the first time. -install: dist - pip uninstall -y shiny - python3 -m pip install dist/shiny*.whl - -install-deps: FORCE ## install dependencies - pip install -e ".[dev,test]" --upgrade - -# ## If caching is ever used, we could run: -# install-deps: FORCE ## install latest dependencies -# pip install --editable ".[dev,test]" --upgrade --upgrade-strategy eager diff --git a/docs/Makefile b/docs/Makefile index 72b14314c..ccd47dc8c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,5 +1,6 @@ .PHONY: help Makefile .DEFAULT_GOAL := help +FORCE: define BROWSER_PYSCRIPT import os, webbrowser, sys @@ -21,43 +22,40 @@ for line in sys.stdin: endef export PRINT_HELP_PYSCRIPT -BROWSER := python -c "$$BROWSER_PYSCRIPT" +BROWSER := $(PYTHON) -c "$$BROWSER_PYSCRIPT" # Use venv from parent -VENV = ../venv +VENV = ../.venv +PARENT_PYBIN_TARGET=.venv/bin PYBIN = $(VENV)/bin +PIP = $(PYBIN)/pip +PYTHON = $(PYBIN)/python + # Any targets that depend on $(VENV) or $(PYBIN) will cause the venv to be # created. To use the venv, python scripts should run with the prefix $(PYBIN), # as in `$(PYBIN)/pip`. $(VENV): - python3 -m venv $(VENV) + cd .. && $(MAKE) $(PARENT_PYBIN_TARGET) $(PYBIN): $(VENV) -help: - @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) - -dev-htmltools: $(PYBIN) ## Install development version of htmltools - $(PYBIN)/pip install https://github.com/posit-dev/py-htmltools/tarball/main - -dev-shinylive: $(PYBIN) ## Install development version of shinylive - $(PYBIN)/pip install https://github.com/posit-dev/py-shinylive/tarball/main +help: FORCE + @python3 -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) -deps: $(PYBIN) dev-htmltools dev-shinylive ## Install build dependencies - $(PYBIN)/pip install pip --upgrade - $(PYBIN)/pip install -e ..[doc] +deps: $(PYBIN) FORCE ## Install build dependencies + cd .. && $(MAKE) install-docs quartodoc: quartodoc_build_core quartodoc_build_express quartodoc_post ## Build quartodocs for express and core ## Build interlinks for API docs -quartodoc_interlinks: $(PYBIN) +quartodoc_interlinks: $(PYBIN) FORCE . $(PYBIN)/activate \ && quartodoc interlinks ## Build core API docs -quartodoc_build_core: $(PYBIN) quartodoc_interlinks +quartodoc_build_core: $(PYBIN) quartodoc_interlinks FORCE $(eval export SHINY_ADD_EXAMPLES=true) $(eval export IN_QUARTODOC=true) $(eval export SHINY_MODE=core) @@ -68,7 +66,7 @@ quartodoc_build_core: $(PYBIN) quartodoc_interlinks && echo "::endgroup::" ## Build express API docs -quartodoc_build_express: $(PYBIN) quartodoc_interlinks +quartodoc_build_express: $(PYBIN) quartodoc_interlinks FORCE $(eval export SHINY_ADD_EXAMPLES=true) $(eval export IN_QUARTODOC=true) $(eval export SHINY_MODE=express) @@ -79,17 +77,17 @@ quartodoc_build_express: $(PYBIN) quartodoc_interlinks && echo "::endgroup::" ## Clean up after quartodoc build -quartodoc_post: $(PYBIN) +quartodoc_post: $(PYBIN) FORCE . $(PYBIN)/activate \ && python _combine_objects_json.py -site: ## Build website +site: $(PYBIN) FORCE ## Build website . $(PYBIN)/activate \ && quarto render -serve: ## Build website and serve +serve: $(PYBIN) FORCE ## Build website and serve . $(PYBIN)/activate \ && quarto preview --port 8080 -clean: ## Clean build artifacts +clean: FORCE ## Clean build artifacts rm -rf _inv api _site .quarto