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 c53dfd047..8ee5a8322 100644 --- a/.github/workflows/build-docs.yaml +++ b/.github/workflows/build-docs.yaml @@ -22,29 +22,14 @@ 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 run: | - cd docs - make site + make docs-site - name: Upload site artifact if: github.ref == 'refs/heads/main' @@ -52,15 +37,14 @@ jobs: with: path: "docs/_site" - deploy: if: github.ref == 'refs/heads/main' needs: build # Grant GITHUB_TOKEN the permissions required to make a Pages deployment permissions: - pages: write # to deploy to Pages - id-token: write # to verify the deployment originates from an appropriate source + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source # Deploy to the github-pages environment environment: diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index b0ac38ed0..8ebb3b9af 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -38,21 +38,11 @@ jobs: run: | make check-types - - name: Lint with flake8 + - name: Lint with ruff if: steps.install.outcome == 'success' && (success() || failure()) run: | make check-lint - - name: black - if: steps.install.outcome == 'success' && (success() || failure()) - run: | - make check-black - - - name: isort - if: steps.install.outcome == 'success' && (success() || failure()) - run: | - make check-isort - playwright-shiny: runs-on: ${{ matrix.os }} if: github.event_name != 'release' @@ -211,9 +201,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/.gitignore b/.gitignore index 6f46035ff..ecd7e58b4 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,8 @@ coverage.xml *.cover .hypothesis/ .pytest_cache/ +.ruff_cache/ + # Translations *.mo @@ -84,7 +86,7 @@ celerybeat-schedule .env # virtualenv -.venv +.venv*/ venv/ ENV/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0808f2b76..8bbbb4851 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,11 @@ +# For the rare case that you'd like to commit with no pre-commit hooks, you can use `--no-verify` in the commit call. +# ``` +# git commit --no-verify -m "MSG" +# ``` repos: - - repo: https://github.com/psf/black - rev: "24.2.0" + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.2 hooks: - - id: black - language_version: python3 + - id: ruff + args: [--fix] + - id: ruff-format diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..8e5d3f684 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "charliermarsh.ruff" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 07dbac4c2..5a060bb3b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,14 +25,13 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter", + "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true, "editor.tabSize": 4, "editor.codeActionsOnSave": { "source.organizeImports": "explicit" } }, - "isort.args": ["--profile", "black"], "editor.rulers": [88], "files.exclude": { "**/__pycache__": true, diff --git a/Makefile b/Makefile index 76c205aa3..c594ce2a5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,6 @@ -.PHONY: help clean% check% format% docs% lint test pyright playwright% install% testrail% coverage release +# Depend on `FORCE` to ensure the target is always run +FORCE: + .DEFAULT_GOAL := help define BROWSER_PYSCRIPT @@ -21,139 +23,294 @@ for line in sys.stdin: endef export PRINT_HELP_PYSCRIPT -BROWSER := python -c "$$BROWSER_PYSCRIPT" -help: - @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) +BROWSER := $(PYTHON) -c "$$BROWSER_PYSCRIPT" + +help: $(PYTHON) FORCE + @$(PYTHON) -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 +RUFF = $(SITE_PACKAGES)/ruff +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 +$(RSCONNECT): $(UV) ## install the main version of rsconnect till pypi version supports shiny express + @echo "-------- Installing rsconnect --------" + . $(PYBIN)/activate && \ + uv pip install "rsconnect-python @ git+https://github.com/rstudio/rsconnect-python.git" + + +# ----------------- +# Type stubs +# ----------------- + +typings/uvicorn: $(PYRIGHT) + @echo "-------- Creating stub for uvicorn --------" + . $(PYBIN)/activate && \ + pyright --createstub uvicorn + +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 + +typings/seaborn: $(PYRIGHT) + @echo "-------- Creating stub for seaborn --------" + . $(PYBIN)/activate && \ + pyright --createstub seaborn + +pyright-typings: typings/uvicorn typings/matplotlib/__init__.pyi typings/seaborn + +# ----------------- +# 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 -clean-build: ## remove build artifacts +clean-build: FORCE ## remove build artifacts rm -fr build/ rm -fr dist/ rm -fr .eggs/ find . -name '*.egg-info' -exec rm -fr {} + find . -name '*.egg' -exec rm -f {} + -clean-pyc: ## remove Python file artifacts +clean-pyc: FORCE ## remove Python file artifacts find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + find . -name '__pycache__' -exec rm -fr {} + -clean-test: ## remove test and coverage artifacts +clean-test: FORCE ## remove test and coverage artifacts rm -fr .tox/ rm -f .coverage rm -fr htmlcov/ rm -fr .pytest_cache rm -rf typings/ -typings/uvicorn: - pyright --createstub uvicorn - -typings/matplotlib/__init__.pyi: ## grab type stubs from GitHub - 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 - -typings/seaborn: - pyright --createstub seaborn - -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 -check-lint: - @echo "-------- Checking style with flake8 --------" - flake8 --show-source . -check-black: - @echo "-------- Checking code with black --------" - black --check . -check-isort: - @echo "-------- Sorting imports with isort --------" - isort --check-only --diff . -check-types: typings/uvicorn typings/matplotlib/__init__.pyi typings/seaborn +# ----------------- +# Check lint, test, and format of code +# ----------------- +check: check-lint check-types check-tests ## check code, style, types, and test (basic CI) +check-fix: format check-types check-tests ## check and format code, style, types, and test +check-lint: check-ruff ## check code lints and format + +check-ruff: $(RUFF) FORCE + @echo "-------- Running ruff lint and format checks --------" + @# Check imports in addition to code + # Check lints + . $(PYBIN)/activate && \ + ruff check . + # Check formatting + . $(PYBIN)/activate && \ + ruff format --check . +check-types: pyright-typings $(PYRIGHT) FORCE ## check types with pyright @echo "-------- Checking types with pyright --------" - pyright -check-tests: + . $(PYBIN)/activate && \ + pyright +check-tests: $(PYTEST) FORCE @echo "-------- Running tests with pytest --------" - python3 tests/pytest/asyncio_prevent.py - pytest + . $(PYBIN)/activate && \ + python tests/pytest/asyncio_prevent.py + . $(PYBIN)/activate && \ + pytest + -pyright: check-types ## check types with pyright -lint: check-lint ## check style with flake8 +pyright: check-types +lint: check-lint test: check-tests ## check tests quickly with the default Python -format: format-black format-isort ## format code with black and isort -format-black: - @echo "-------- Formatting code with black --------" - black . -format-isort: - @echo "-------- Sorting imports with isort --------" - isort . +# ----------------- +# Fix formatting of code +# ----------------- +format: format-ruff ## format code + +format-ruff: $(RUFF) FORCE + @echo "-------- Formatting code with ruff --------" + @# Reason for two commands: https://github.com/astral-sh/ruff/issues/8232 + @# Fix lints + . $(PYBIN)/activate && \ + ruff check --fix . + @# Fix formatting + . $(PYBIN)/activate && \ + ruff format . + +format-ruff-unsafe: $(RUFF) FORCE + @echo "-------- Formatting code with ruff (unsafe) --------" + . $(PYBIN)/activate && \ + ruff check --fix --unsafe-fixes . +# ----------------- +# 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-serve: $(PYBIN) FORCE ## docs: serve docs in browser + $(MAKE) docs-quartodoc + @echo "-------- Previewing docs in browser --------" + @cd docs && make serve + +docs-site: $(PYBIN) FORCE ## docs: render quarto site + $(MAKE) docs-quartodoc + @echo "-------- Previewing docs in browser --------" + @cd docs && make site -docs: ## docs: build docs with quartodoc +docs-quartodoc: $(PYBIN) FORCE ## docs: build quartodoc docs + $(MAKE) install-docs @echo "-------- Building docs with quartodoc --------" @cd docs && make quartodoc -docs-preview: ## docs: preview docs in browser - @echo "-------- Previewing docs in browser --------" - @cd docs && make serve + +# ----------------- +# Testing with playwright +# ----------------- # Default `SUB_FILE` to empty SUB_FILE:= -install-playwright: - playwright install --with-deps - -install-trcli: - which trcli || pip install trcli - -install-rsconnect: ## install the main version of rsconnect till pypi version supports shiny express - 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 -playwright-shiny: install-playwright ## end-to-end tests with playwright - pytest tests/playwright/shiny/$(SUB_FILE) +playwright-shiny: install-playwright $(PYTEST) FORCE ## end-to-end tests with playwright + . $(PYBIN)/activate && \ + pytest tests/playwright/shiny/$(SUB_FILE) -playwright-deploys: install-playwright install-rsconnect ## end-to-end tests on examples with playwright - pytest tests/playwright/deploys/$(SUB_FILE) +playwright-deploys: install-playwright $(RSCONNECT) $(PYTEST) FORCE ## end-to-end tests on examples with playwright + . $(PYBIN)/activate && \ + pytest tests/playwright/deploys/$(SUB_FILE) -playwright-examples: install-playwright ## end-to-end tests on examples with playwright - pytest tests/playwright/examples/$(SUB_FILE) +playwright-examples: install-playwright $(PYTEST) FORCE ## end-to-end tests on examples with playwright + . $(PYBIN)/activate && \ + pytest tests/playwright/examples/$(SUB_FILE) -playwright-debug: install-playwright ## All end-to-end tests, chrome only, headed - 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 + . $(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 -testrail-junit: install-playwright install-trcli ## end-to-end tests with playwright and generate junit report - pytest tests/playwright/shiny/$(SUB_FILE) --junitxml=report.xml - -coverage: ## check combined code coverage (must run e2e last) - pytest --cov-report term-missing --cov=shiny tests/pytest/ tests/playwright/shiny/$(SUB_FILE) - 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 ## end-to-end tests with playwright and generate junit report + . $(PYBIN)/activate && \ + pytest tests/playwright/shiny/$(SUB_FILE) --junitxml=report.xml + +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) && \ + 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: ## install dependencies - pip install -e ".[dev,test]" --upgrade - -# ## If caching is ever used, we could run: -# install-deps: ## install latest dependencies -# pip install --editable ".[dev,test]" --upgrade --upgrade-strategy eager diff --git a/README.md b/README.md index 23c3fb95f..dd2143ef3 100644 --- a/README.md +++ b/README.md @@ -58,3 +58,5 @@ pre-commit install # To disable: # pre-commit uninstall ``` + +If you absolutely need to skip the pre-commit hooks, you can use the [`--no-verify` flag when you commit](https://git-scm.com/docs/githooks#_pre_commit) (`git commit --no-verify -m "MSG"`). diff --git a/docs/Makefile b/docs/Makefile index 72b14314c..f705c4815 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,42 @@ 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 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) $(PYBIN) $(PYBIN): $(VENV) -help: - @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) +help: $(PYTHON) FORCE + @$(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 +# Install development version of htmltools +# Install development version of shinylive +deps: $(PYBIN) FORCE ## Install build dependencies + cd .. && $(MAKE) install-docs -deps: $(PYBIN) dev-htmltools dev-shinylive ## Install build dependencies - $(PYBIN)/pip install pip --upgrade - $(PYBIN)/pip install -e ..[doc] 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 + echo "$(PYBIN)/activate" . $(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 +68,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 +79,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 diff --git a/examples/airmass/location.py b/examples/airmass/location.py index 5a8cf926c..b644c19a5 100644 --- a/examples/airmass/location.py +++ b/examples/airmass/location.py @@ -58,9 +58,7 @@ def location_server( }, {maximumAge: Infinity, timeout: Infinity} ) - """.replace( - "#HERE#", module.resolve_id("here") - ) + """.replace("#HERE#", module.resolve_id("here")) ), selector="body", where="beforeEnd", diff --git a/examples/brownian/brownian_motion.py b/examples/brownian/brownian_motion.py index 92defcba5..d2f46a09a 100644 --- a/examples/brownian/brownian_motion.py +++ b/examples/brownian/brownian_motion.py @@ -5,14 +5,14 @@ # * `brownian_motion()` is a function that generates a random walk # * `brownian_widget()` uses restructured plotting code given in article -rs = np.random.RandomState() +rng = np.random.default_rng() # https://plotly.com/python/3d-line-plots/ def brownian_motion(T=1, N=100, mu=0.1, sigma=0.01, S0=20): dt = float(T) / N t = np.linspace(0, T, N) - W = rs.standard_normal(size=N) + W = rng.standard_normal(size=N) W = np.cumsum(W) * np.sqrt(dt) # standard brownian motion X = (mu - 0.5 * sigma**2) * t + sigma * W S = S0 * np.exp(X) # geometric brownian motion @@ -24,7 +24,7 @@ def brownian_data(n=100, mu=(0.0, 0.00), sigma=(0.1, 0.1), S0=(1.0, 1.0)): "x": brownian_motion(T=1, N=n, mu=mu[0], sigma=sigma[0], S0=S0[0]), "y": brownian_motion(T=1, N=n, mu=mu[1], sigma=sigma[1], S0=S0[1]), # "y": [i for i in range(n)], - "z": [i for i in range(n)], + "z": list(range(n)), } @@ -35,12 +35,12 @@ def brownian_widget(width=600, height=600): x=[], y=[], z=[], - marker=dict( - size=4, - color=[], - colorscale="Viridis", - ), - line=dict(color="darkblue", width=2), + marker={ + "size": 4, + "color": [], + "colorscale": "Viridis", + }, + line={"color": "darkblue", "width": 2}, ) ], layout={"showlegend": False, "width": width, "height": height}, diff --git a/examples/brownian/mediapipe.py b/examples/brownian/mediapipe.py index 1a2b6bd98..91f482b60 100644 --- a/examples/brownian/mediapipe.py +++ b/examples/brownian/mediapipe.py @@ -60,8 +60,8 @@ def rel_hand(start_pos: int, end_pos: int): normal_unit_vec = normal_vec / np.linalg.norm(normal_vec) def list_to_xyz(x): - x = list(map(lambda y: round(y, 2), x)) - return dict(x=x[0], y=x[1], z=x[2]) + x = [round(y, 2) for y in x] + return {"x": x[0], "y": x[1], "z": x[2]} # Invert, for some reason normal_unit_vec = normal_unit_vec * -1.0 @@ -82,8 +82,8 @@ def list_to_xyz(x): def xyz_mean(points): - return dict( - x=mean([p["x"] for p in points]), - y=mean([p["y"] for p in points]), - z=mean([p["z"] for p in points]), - ) + return { + "x": mean([p["x"] for p in points]), + "y": mean([p["y"] for p in points]), + "z": mean([p["z"] for p in points]), + } diff --git a/examples/brownian/shinymediapipe/__init__.py b/examples/brownian/shinymediapipe/__init__.py index 46e2f2c3b..cf2293072 100644 --- a/examples/brownian/shinymediapipe/__init__.py +++ b/examples/brownian/shinymediapipe/__init__.py @@ -1,4 +1,4 @@ -from ._hand import dependencies, input_hand, hand_options +from ._hand import dependencies, hand_options, input_hand __all__ = ( "dependencies", diff --git a/examples/cpuinfo/app.py b/examples/cpuinfo/app.py index 7cf934283..37d788b43 100644 --- a/examples/cpuinfo/app.py +++ b/examples/cpuinfo/app.py @@ -8,7 +8,7 @@ from math import ceil -import matplotlib +import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np import pandas as pd @@ -17,7 +17,7 @@ # The agg matplotlib backend seems to be a little more efficient than the default when # running on macOS, and also gives more consistent results across operating systems -matplotlib.use("agg") +mpl.use("agg") # max number of samples to retain MAX_SAMPLES = 1000 @@ -166,7 +166,7 @@ def plot(): ncols=ncols, squeeze=False, ) - for i in range(0, ncols * nrows): + for i in range(ncols * nrows): row = i // ncols col = i % ncols axes = axeses[row, col] diff --git a/examples/cpuinfo/fakepsutil.py b/examples/cpuinfo/fakepsutil.py index de42c8189..50a455646 100644 --- a/examples/cpuinfo/fakepsutil.py +++ b/examples/cpuinfo/fakepsutil.py @@ -7,12 +7,13 @@ def cpu_count(logical: bool = True): return 8 if logical else 4 -last_sample = np.random.uniform(0, 100, size=cpu_count(True)) +rng = np.random.default_rng() +last_sample = rng.uniform(0, 100, size=cpu_count(True)) def cpu_percent(percpu: bool = False): global last_sample - delta = np.random.normal(scale=10, size=len(last_sample)) + delta = rng.normal(scale=10, size=len(last_sample)) last_sample = (last_sample + delta).clip(0, 100) if percpu: return last_sample.tolist() diff --git a/examples/dataframe/app.py b/examples/dataframe/app.py index 99374627e..3568bbcb6 100644 --- a/examples/dataframe/app.py +++ b/examples/dataframe/app.py @@ -1,9 +1,13 @@ -import pandas as pd +from typing import TYPE_CHECKING + import seaborn as sns from shinyswatch.theme import darkly from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui +if TYPE_CHECKING: + import pandas as pd + def app_ui(req): dark = True if "dark" in req.query_params else None diff --git a/examples/express/accordion_app.py b/examples/express/accordion_app.py index fdd722c34..76e2505d2 100644 --- a/examples/express/accordion_app.py +++ b/examples/express/accordion_app.py @@ -17,6 +17,5 @@ def txt(): @render.plot def histogram(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) plt.hist(x, input.n(), density=True) diff --git a/examples/express/column_wrap_app.py b/examples/express/column_wrap_app.py index 5e5c44f06..b64660ea9 100644 --- a/examples/express/column_wrap_app.py +++ b/examples/express/column_wrap_app.py @@ -12,14 +12,12 @@ @render.plot def histogram(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) plt.hist(x, input.n(), density=True) with ui.card(): @render.plot def histogram2(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) plt.hist(x, input.n(), density=True, color="red") diff --git a/examples/express/nav_app.py b/examples/express/nav_app.py index 0a72627ff..9410b6085 100644 --- a/examples/express/nav_app.py +++ b/examples/express/nav_app.py @@ -13,8 +13,7 @@ @render.plot def histogram(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) plt.hist(x, input.n(), density=True) with ui.navset_card_underline(): @@ -25,6 +24,5 @@ def histogram(): @render.plot def histogram2(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) plt.hist(x, input.n2(), density=True) diff --git a/examples/express/plot_app.py b/examples/express/plot_app.py index 605fe6e62..9d31dddf5 100644 --- a/examples/express/plot_app.py +++ b/examples/express/plot_app.py @@ -9,6 +9,5 @@ @render.plot def histogram(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) plt.hist(x, input.n(), density=True) diff --git a/examples/express/shared_app.py b/examples/express/shared_app.py index 89c587a1c..10778565a 100644 --- a/examples/express/shared_app.py +++ b/examples/express/shared_app.py @@ -12,8 +12,7 @@ @render.plot def histogram(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) plt.hist(x, shared.rv(), density=True) diff --git a/examples/express/sidebar_app.py b/examples/express/sidebar_app.py index ac1e97844..b75714204 100644 --- a/examples/express/sidebar_app.py +++ b/examples/express/sidebar_app.py @@ -10,6 +10,5 @@ @render.plot def histogram(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) plt.hist(x, input.n(), density=True) diff --git a/examples/model-score/app.py b/examples/model-score/app.py index ef84aeb5e..e1f2cc815 100644 --- a/examples/model-score/app.py +++ b/examples/model-score/app.py @@ -16,6 +16,16 @@ THRESHOLD_LOW = 0.5 THRESHOLD_LOW_COLOR = "rgb(193, 0, 0)" + +def value_box_theme(score): + if score > THRESHOLD_MID: + return "text-success" + elif score < THRESHOLD_LOW: + return "bg-danger" + else: + return "text-warning" + + # Start a background thread that writes fake data to the SQLite database every second scoredata.begin() @@ -48,7 +58,7 @@ def df(): params=[150], ) # Convert timestamp to datetime object, which SQLite doesn't support natively - tbl["timestamp"] = pd.to_datetime(tbl["timestamp"], utc=True) + tbl["timestamp"] = pd.to_datetime(tbl["timestamp"], utc=True, format="ISO8601") # Create a short label for readability tbl["time"] = tbl["timestamp"].dt.strftime("%H:%M:%S") # Reverse order of rows @@ -71,10 +81,7 @@ def read_time_period(from_time, to_time): model_names = ["model_1", "model_2", "model_3", "model_4"] -model_colors = { - name: color - for name, color in zip(model_names, px.colors.qualitative.D3[0 : len(model_names)]) -} +model_colors = dict(zip(model_names, px.colors.qualitative.D3[0 : len(model_names)])) def app_ui(req): @@ -195,11 +202,7 @@ def value_boxes(): ui.value_box( model, ui.h2(score), - theme=( - "text-success" - if score > THRESHOLD_MID - else "text-warning" if score > THRESHOLD_LOW else "bg-danger" - ), + theme=value_box_theme(score), ) for model, score in scores_by_model.items() ], @@ -218,7 +221,7 @@ def plot_timeseries(): filtered_df(), x="time", y="score", - labels=dict(score="accuracy"), + labels={"score": "accuracy"}, color="model", color_discrete_map=model_colors, # The default for render_mode is "auto", which switches between @@ -232,13 +235,13 @@ def plot_timeseries(): fig.add_hline( THRESHOLD_LOW, line_dash="dash", - line=dict(color=THRESHOLD_LOW_COLOR, width=2), + line={"color": THRESHOLD_LOW_COLOR, "width": 2}, opacity=0.3, ) fig.add_hline( THRESHOLD_MID, line_dash="dash", - line=dict(color=THRESHOLD_MID_COLOR, width=2), + line={"color": THRESHOLD_MID_COLOR, "width": 2}, opacity=0.3, ) @@ -254,7 +257,7 @@ def plot_dist(): facet_row="model", nbins=20, x="score", - labels=dict(score="accuracy"), + labels={"score": "accuracy"}, color="model", color_discrete_map=model_colors, template="simple_white", @@ -263,13 +266,13 @@ def plot_dist(): fig.add_vline( THRESHOLD_LOW, line_dash="dash", - line=dict(color=THRESHOLD_LOW_COLOR, width=2), + line={"color": THRESHOLD_LOW_COLOR, "width": 2}, opacity=0.3, ) fig.add_vline( THRESHOLD_MID, line_dash="dash", - line=dict(color=THRESHOLD_MID_COLOR, width=2), + line={"color": THRESHOLD_MID_COLOR, "width": 2}, opacity=0.3, ) diff --git a/examples/static_plots/app.py b/examples/static_plots/app.py index b5c928169..f0dc0af5f 100644 --- a/examples/static_plots/app.py +++ b/examples/static_plots/app.py @@ -54,13 +54,14 @@ def server(input: Inputs, output: Outputs, session: Session): + rng = np.random.default_rng() + @reactive.calc def fake_data(): n = 5000 mean = [0, 0] - rng = np.random.RandomState(0) cov = [(input.var(), input.cov()), (input.cov(), 1 / input.var())] - return rng.multivariate_normal(mean, cov, n).T + return np.random.default_rng(seed=0).multivariate_normal(mean, cov, n).T @render.plot def seaborn(): @@ -98,7 +99,8 @@ def plotnine(): @render.plot def pandas(): ts = pd.Series( - np.random.randn(1000), index=pd.date_range("1/1/2000", periods=1000) + rng.standard_normal(1000), + index=pd.date_range("1/1/2000", periods=1000), ) ts = ts.cumsum() return ts.plot() diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 000000000..e40f6e361 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,84 @@ + +extend-exclude = ["docs", ".venv", "venv", "typings", "build", "_dev"] + +[lint] + +extend-ignore = [ + "E501", # E501: Line too long + "PT011", # PT011 `pytest.raises(ValueError)` is too broad + "PT022", # PT022 [*] No teardown in fixture + "B904", # B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling + # Conflicting lint rules: https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "E111", # * indentation-with-invalid-multiple (E111) + "E114", # * indentation-with-invalid-multiple-comment (E114) + "E117", # * over-indented (E117) + "D206", # * indent-with-spaces (D206) + "D300", # * triple-single-quotes (D300) + "Q000", # * bad-quotes-inline-string (Q000) + "Q001", # * bad-quotes-multiline-string (Q001) + "Q002", # * bad-quotes-docstring (Q002) + "Q003", # * avoidable-escaped-quote (Q003) + "COM812", # * missing-trailing-comma (COM812) + "COM819", # * prohibited-trailing-comma (COM819) + "ISC001", # * single-line-implicit-string-concatenation (ISC001) + "ISC002", # * multi-line-implicit-string-concatenation (ISC002) +] + + +# Rules to add https://docs.astral.sh/ruff/rules/ +# Default `select = ["E4", "E7", "E9", "F"]` + +extend-select = [ + # "C90", # Many false positives # C90; mccabe: https://docs.astral.sh/ruff/rules/complex-structure/ + # "DTZ", # Dates with timezones are different from dates without timezones # DTZ; flake8-datetimez: https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz + + "E", # E; pycodestyle: https://docs.astral.sh/ruff/rules/#pycodestyle-e-w + "F", # F; Pyflakes: https://docs.astral.sh/ruff/rules/#pyflakes-f + "I", # I; isort: https://docs.astral.sh/ruff/rules/#isort-i + "B", # B; flake8-bugbear: https://docs.astral.sh/ruff/rules/#flake8-bugbear-b + "Q", # Q; flake8-quotes: https://docs.astral.sh/ruff/rules/#flake8-quotes-q + "COM", # COM; Commas: https://docs.astral.sh/ruff/rules/#flake8-commas-com + "C4", # C4; flake8-comprehensions: https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 + "FA102", # FA102; flake8-future-annotations: https://docs.astral.sh/ruff/rules/#flake8-future-annotations-fa + "ISC", # ISC; flake8-implicit-str-concat: https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc + "ICN", # ICN; flake8-import-conventions: https://docs.astral.sh/ruff/rules/#flake8-import-conventions-icn + "PIE", # PIE; flake8-pie: https://docs.astral.sh/ruff/rules/#flake8-pie-pie + "PYI013", # PYI013; flake8-pyi Non-empty class body must not contain `...`: https://docs.astral.sh/ruff/rules/#flake8-pyi-pyi + "PYI030", # PYI030; flake8-pyi Multiple literal members in a union: https://docs.astral.sh/ruff/rules/#flake8-pyi-pyi + "PYI034", # PYI034; flake8-pyi `__new__` methods usually reutrn `Self`: https://docs.astral.sh/ruff/rules/#flake8-pyi-pyi + "PT", # PT; flake8-pytest-style: https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt + "SIM118", # SIM118; flake8-simplify Use `key {operator} dict`: https://docs.astral.sh/ruff/rules/#flake8-simplify-sim + "TCH", # TCH; flake8-type-checking: https://docs.astral.sh/ruff/rules/#flake8-type-checking-tch + # "FIX", # FIX; flake8-fixme: https://docs.astral.sh/ruff/rules/#flake8-fixme-fix + # "PGH", # PGH; pygrep-hooks: https://docs.astral.sh/ruff/rules/#pygrep-hooks-pgh + "NPY", # NPY; NumPy-specific rules: https://docs.astral.sh/ruff/rules/#numpy-specific-rules-npy + "RUF005", # RUF005; Ruff specific rules Consider {expression} instead of concatenation: https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf + "RUF100", # RUF100; Ruff specific rules Unused `noqa` directive https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf +] + +[lint.extend-per-file-ignores] +# I: isort; Do not reformat imports in `__init__.py` files. +"**/__init__.py" = ["I"] +# F403: 'from module import *' used; unable to detect undefined names +# Also ignore `F403` in all `__init__.py` files. +"shiny/__init__.py" = ["F403", "F405"] +# B018: Found useless expression. Either assign it to a variable or remove it. +# This check is incompatible with the `express` framework. +"**/app-express.py" = ["B018"] +"**/*express/**/*.py" = ["B018"] +# T20: no `print()` statements +"examples/**" = ["T20"] +"scripts/**" = ["T20"] +"shiny/_template_utils.py" = ["T20"] +"shiny/templates/**" = ["T20"] +"shiny/api-examples/**" = ["T20"] +"tests/**" = ["T20"] +# PT019: pytest +"tests/pytest/test_output_transformer.py" = ["PT019"] + + +[format] +docstring-code-format = true +line-ending = "lf" +indent-style = "space" +quote-style = "double" diff --git a/scripts/generate-imports.py b/scripts/generate-imports.py index a3bfc7f15..a11d836dd 100755 --- a/scripts/generate-imports.py +++ b/scripts/generate-imports.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +from __future__ import annotations import importlib import sys diff --git a/setup.cfg b/setup.cfg index 617a42393..c31fa9480 100644 --- a/setup.cfg +++ b/setup.cfg @@ -92,10 +92,7 @@ test = folium dev = - black>=24.0 - flake8>=6.0.0 - flake8-bugbear>=23.2.13 - isort>=5.10.1 + ruff pyright>=1.1.348 pre-commit>=2.15.0 wheel @@ -104,6 +101,7 @@ dev = pandas-stubs numpy shinyswatch>=0.2.4 + uv>=0.1.15 doc = jupyter jupyter_client < 8.0.0 @@ -122,26 +120,3 @@ shiny = py.typed [options.entry_points] console_scripts = shiny = shiny._main:main - -[flake8] -# E302: Expected 2 blank lines -# E501: Line too long -# F403: 'from module import *' used; unable to detect undefined names -# F405: Name may be undefined, or defined from star imports -# W503: Line break occurred before a binary operator -# E203: whitespace before ':' (see https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8) -# E701 multiple statements on one line (colon) -# E704: Multiple statements on one line (def) -ignore = E302, E501, F403, F405, W503, E203, E701, E704 -extend_exclude = docs, .venv, venv, typings, build, _dev - -[isort] -profile=black -skip= - __init__.py - typings/ - _dev/ - .venv - venv - .tox - build diff --git a/shiny/_app.py b/shiny/_app.py index 03e21aa89..23e122f32 100644 --- a/shiny/_app.py +++ b/shiny/_app.py @@ -39,9 +39,7 @@ # Default values for App options. LIB_PREFIX: str = "lib/" SANITIZE_ERRORS: bool = False -SANITIZE_ERROR_MSG: str = ( - "An error has occurred. Check your logs or contact the app author for clarification." -) +SANITIZE_ERROR_MSG: str = "An error has occurred. Check your logs or contact the app author for clarification." class App: @@ -96,9 +94,7 @@ def server(input: Inputs, output: Outputs, session: Session): may default to ``True`` in some production environments (e.g., Posit Connect). """ - sanitize_error_msg: str = ( - "An error has occurred. Check your logs or contact the app author for clarification." - ) + sanitize_error_msg: str = "An error has occurred. Check your logs or contact the app author for clarification." """ The message to show when an error occurs and ``SANITIZE_ERRORS=True``. """ @@ -512,7 +508,7 @@ def noop_server_fn(input: Inputs, output: Outputs, session: Session) -> None: def wrap_server_fn_with_output_session( - server: Callable[[Inputs], None] + server: Callable[[Inputs], None], ) -> Callable[[Inputs, Outputs, Session], None]: def _server(input: Inputs, output: Outputs, session: Session): # Only has 1 parameter, ignore output, session diff --git a/shiny/_autoreload.py b/shiny/_autoreload.py index b77fec6fe..07cb8c03e 100644 --- a/shiny/_autoreload.py +++ b/shiny/_autoreload.py @@ -76,7 +76,8 @@ async def _() -> None: } try: async with websockets.connect( - url, **options # pyright: ignore[reportArgumentType] + url, + **options, # pyright: ignore[reportArgumentType] ) as websocket: await websocket.send("reload_end") except websockets.exceptions.ConnectionClosed: @@ -110,9 +111,7 @@ def __init__( ws_url = autoreload_url() self.script = ( f""" -""".encode( - "ascii" - ) +""".encode("ascii") if ws_url else bytes() ) diff --git a/shiny/_connection.py b/shiny/_connection.py index f2e0323fe..411259eb3 100644 --- a/shiny/_connection.py +++ b/shiny/_connection.py @@ -137,5 +137,3 @@ def get_http_conn(self) -> HTTPConnection: class ConnectionClosed(Exception): """Raised when a Connection is closed from the other side.""" - - pass diff --git a/shiny/_custom_component_template_questions.py b/shiny/_custom_component_template_questions.py index 4ca5b3350..aa70d629b 100644 --- a/shiny/_custom_component_template_questions.py +++ b/shiny/_custom_component_template_questions.py @@ -61,12 +61,7 @@ def validate(self, document: Document): # Check for quotations - if ( - name.startswith('"') - or name.endswith('"') - or name.startswith("'") - or name.endswith("'") - ): + if name.startswith(('"', "'")) or name.endswith(('"', "'")): raise ValidationError( message="The name should be unquoted.", cursor_position=len(name), diff --git a/shiny/_docstring.py b/shiny/_docstring.py index 96029dfe4..5309e2500 100644 --- a/shiny/_docstring.py +++ b/shiny/_docstring.py @@ -45,7 +45,7 @@ def decorator(func: F) -> F: current.extend(["express", "core"]) else: current.append(mode) - setattr(func, "__no_example", current) # noqa: B010 + setattr(func, "__no_example", current) return func return decorator @@ -238,7 +238,7 @@ def __str__(self): return ( f"Could not find {type} example file named " - + f"{' or '.join(self.file_names)} in {self.dir}." + f"{' or '.join(self.file_names)} in {self.dir}." ) diff --git a/shiny/_error.py b/shiny/_error.py index 33f807dbc..bee5f6964 100644 --- a/shiny/_error.py +++ b/shiny/_error.py @@ -1,10 +1,12 @@ from __future__ import annotations -from typing import cast +from typing import TYPE_CHECKING, cast import starlette.exceptions as exceptions import starlette.responses as responses -from starlette.types import ASGIApp, Receive, Scope, Send + +if TYPE_CHECKING: + from starlette.types import ASGIApp, Receive, Scope, Send class ErrorMiddleware: diff --git a/shiny/_hostenv.py b/shiny/_hostenv.py index 89f1e0273..74876b770 100644 --- a/shiny/_hostenv.py +++ b/shiny/_hostenv.py @@ -1,6 +1,5 @@ from __future__ import annotations -import logging import os import re import typing @@ -9,6 +8,9 @@ from typing import Pattern from urllib.parse import ParseResult, urlparse +if typing.TYPE_CHECKING: + import logging + def is_workbench() -> bool: return bool(os.getenv("RS_SERVER_URL") and os.getenv("RS_SESSION_URL")) diff --git a/shiny/_main.py b/shiny/_main.py index bf61f6899..10f4653ba 100644 --- a/shiny/_main.py +++ b/shiny/_main.py @@ -8,9 +8,8 @@ import platform import re import sys -import types from pathlib import Path -from typing import Any, Optional +from typing import TYPE_CHECKING, Any, Optional import click import uvicorn @@ -24,6 +23,9 @@ from .express import is_express_app from .express._utils import escape_to_var_name +if TYPE_CHECKING: + import types + @click.group("main") def main() -> None: @@ -561,10 +563,10 @@ def create( shinylive export APPDIR DESTDIR """, - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ), + context_settings={ + "ignore_unknown_options": True, + "allow_extra_args": True, + }, ) def static() -> None: print( diff --git a/shiny/_typing_extensions.py b/shiny/_typing_extensions.py index 39cc1ac29..5047478a4 100644 --- a/shiny/_typing_extensions.py +++ b/shiny/_typing_extensions.py @@ -1,6 +1,6 @@ # # Within file flags to ignore unused imports -# flake8: noqa: F401 # pyright: reportUnusedImport=false +from __future__ import annotations __all__ = ( "Concatenate", diff --git a/shiny/_utils.py b/shiny/_utils.py index f9a180b28..89489050e 100644 --- a/shiny/_utils.py +++ b/shiny/_utils.py @@ -13,12 +13,23 @@ import sys import tempfile import warnings -from pathlib import Path -from types import ModuleType -from typing import Any, Awaitable, Callable, Generator, Optional, TypeVar, cast +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Generator, + Optional, + TypeVar, + cast, +) from ._typing_extensions import ParamSpec, TypeGuard +if TYPE_CHECKING: + from pathlib import Path + from types import ModuleType + CancelledError = asyncio.CancelledError T = TypeVar("T") @@ -182,7 +193,7 @@ def random_port( 10080, ] - unusable = set([x for x in unsafe_ports if x >= min and x <= max]) + unusable = {x for x in unsafe_ports if x >= min and x <= max} while n > 0: if (max - min + 1) <= len(unusable): break @@ -246,7 +257,7 @@ def private_seed() -> Generator[None, None, None]: def wrap_async( - fn: Callable[P, R] | Callable[P, Awaitable[R]] + fn: Callable[P, R] | Callable[P, Awaitable[R]], ) -> Callable[P, Awaitable[R]]: """ Given a synchronous function that returns R, return an async function that wraps the @@ -322,7 +333,7 @@ async def fn_async(*args: P.args, **kwargs: P.kwargs) -> R: # This function should generally be used in this code base instead of # `iscoroutinefunction()`. def is_async_callable( - obj: Callable[P, R] | Callable[P, Awaitable[R]] + obj: Callable[P, R] | Callable[P, Awaitable[R]], ) -> TypeGuard[Callable[P, Awaitable[R]]]: """ Determine if an object is an async function. @@ -413,7 +424,7 @@ def _step(fut: Optional["asyncio.Future[None]"] = None): assert fut.done() try: fut.result() - except BaseException as e: # noqa: B036 + except BaseException as e: exc = e if result_future.cancelled(): @@ -439,7 +450,7 @@ def _step(fut: Optional["asyncio.Future[None]"] = None): except (KeyboardInterrupt, SystemExit) as e: result_future.set_exception(e) raise - except BaseException as e: # noqa: B036 + except BaseException as e: result_future.set_exception(e) else: # If we get here, the coro didn't finish. Schedule it for completion. diff --git a/shiny/api-examples/data_frame/app-core.py b/shiny/api-examples/data_frame/app-core.py index a592ef238..0f27aa96f 100644 --- a/shiny/api-examples/data_frame/app-core.py +++ b/shiny/api-examples/data_frame/app-core.py @@ -1,4 +1,4 @@ -import pandas # noqa: F401 (this line needed for Shinylive to load plotly.express) +import pandas as pd # noqa: F401 (this line needed for Shinylive to load plotly.express) import plotly.express as px from shinywidgets import output_widget, render_widget diff --git a/shiny/api-examples/data_frame/app-express.py b/shiny/api-examples/data_frame/app-express.py index 24a5b47ba..98644c0d9 100644 --- a/shiny/api-examples/data_frame/app-express.py +++ b/shiny/api-examples/data_frame/app-express.py @@ -1,4 +1,4 @@ -import pandas # noqa: F401 (this line needed for Shinylive to load plotly.express) +import pandas as pd # noqa: F401 (this line needed for Shinylive to load plotly.express) import plotly.express as px from shinywidgets import render_widget diff --git a/shiny/api-examples/download/app-core.py b/shiny/api-examples/download/app-core.py index 74edfa82d..b57f20d0a 100644 --- a/shiny/api-examples/download/app-core.py +++ b/shiny/api-examples/download/app-core.py @@ -77,6 +77,8 @@ def make_example(id: str, label: str, title: str, desc: str, extra: Any = None): def server(input: Inputs, output: Outputs, session: Session): + rng = np.random.default_rng() + @render.download() def download1(): """ @@ -98,8 +100,8 @@ def download2(): """ print(input.num_points()) - x = np.random.uniform(size=input.num_points()) - y = np.random.uniform(size=input.num_points()) + x = rng.uniform(size=input.num_points()) + y = rng.uniform(size=input.num_points()) plt.figure() plt.scatter(x, y) plt.title(input.title()) @@ -108,7 +110,7 @@ def download2(): yield buf.getvalue() @render.download( - filename=lambda: f"新型-{date.today().isoformat()}-{np.random.randint(100, 999)}.csv" + filename=lambda: f"新型-{date.today().isoformat()}-{rng.randint(100, 999)}.csv" ) async def download3(): await asyncio.sleep(0.25) diff --git a/shiny/api-examples/download/app-express.py b/shiny/api-examples/download/app-express.py index 9eaa60647..a3298a81e 100644 --- a/shiny/api-examples/download/app-express.py +++ b/shiny/api-examples/download/app-express.py @@ -8,6 +8,8 @@ from shiny.express import render, ui +rng = np.random.default_rng() + ui.page_opts(title="Various download examples") with ui.accordion(open=True): @@ -41,8 +43,8 @@ def download2(): """ print(input.num_points()) - x = np.random.uniform(size=input.num_points()) - y = np.random.uniform(size=input.num_points()) + x = rng.uniform(size=input.num_points()) + y = rng.uniform(size=input.num_points()) plt.figure() plt.scatter(x, y) plt.title(input.title()) @@ -57,7 +59,7 @@ def download2(): @render.download( label="Download filename", - filename=lambda: f"新型-{date.today().isoformat()}-{np.random.randint(100, 999)}.csv", + filename=lambda: f"新型-{date.today().isoformat()}-{rng.randint(100, 999)}.csv", ) async def download3(): await asyncio.sleep(0.25) diff --git a/shiny/api-examples/input_action_button/app-core.py b/shiny/api-examples/input_action_button/app-core.py index 66f6abe13..b8dcbeb8b 100644 --- a/shiny/api-examples/input_action_button/app-core.py +++ b/shiny/api-examples/input_action_button/app-core.py @@ -16,8 +16,7 @@ def server(input: Inputs, output: Outputs, session: Session): # (not when the slider is changed) @reactive.event(input.go, ignore_none=False) def plot(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(input.n()) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(input.n()) fig, ax = plt.subplots() ax.hist(x, bins=30, density=True) return fig diff --git a/shiny/api-examples/input_action_button/app-express.py b/shiny/api-examples/input_action_button/app-express.py index 925ebc4d6..5936c2d86 100644 --- a/shiny/api-examples/input_action_button/app-express.py +++ b/shiny/api-examples/input_action_button/app-express.py @@ -13,8 +13,7 @@ # (not when the slider is changed) @reactive.event(input.go, ignore_none=False) def plot(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(input.n()) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(input.n()) fig, ax = plt.subplots() ax.hist(x, bins=30, density=True) return fig diff --git a/shiny/api-examples/input_action_link/app-core.py b/shiny/api-examples/input_action_link/app-core.py index 054ebacaa..a7bc48a80 100644 --- a/shiny/api-examples/input_action_link/app-core.py +++ b/shiny/api-examples/input_action_link/app-core.py @@ -16,8 +16,7 @@ def server(input: Inputs, output: Outputs, session: Session): # the slider is changed @reactive.event(input.go, ignore_none=False) def plot(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(input.n()) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(input.n()) fig, ax = plt.subplots() ax.hist(x, bins=30, density=True) return fig diff --git a/shiny/api-examples/input_action_link/app-express.py b/shiny/api-examples/input_action_link/app-express.py index 39939d756..0e5ac98b3 100644 --- a/shiny/api-examples/input_action_link/app-express.py +++ b/shiny/api-examples/input_action_link/app-express.py @@ -13,8 +13,7 @@ # the slider is changed @reactive.event(input.go, ignore_none=False) def plot(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(input.n()) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(input.n()) fig, ax = plt.subplots() ax.hist(x, bins=30, density=True) return fig diff --git a/shiny/api-examples/input_dark_mode/app-core.py b/shiny/api-examples/input_dark_mode/app-core.py index 30539ac30..dd6bf0f16 100644 --- a/shiny/api-examples/input_dark_mode/app-core.py +++ b/shiny/api-examples/input_dark_mode/app-core.py @@ -45,8 +45,7 @@ def _(): @render.plot(alt="A histogram") def plot() -> object: - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x, input.n(), density=True) diff --git a/shiny/api-examples/input_dark_mode/app-express.py b/shiny/api-examples/input_dark_mode/app-express.py index 063f215d8..f46b20aee 100644 --- a/shiny/api-examples/input_dark_mode/app-express.py +++ b/shiny/api-examples/input_dark_mode/app-express.py @@ -13,8 +13,7 @@ @render.plot(alt="A histogram") def plot() -> object: - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x, input.n(), density=True) diff --git a/shiny/api-examples/input_file/app-core.py b/shiny/api-examples/input_file/app-core.py index 4cd0d99ee..9331d5ba7 100644 --- a/shiny/api-examples/input_file/app-core.py +++ b/shiny/api-examples/input_file/app-core.py @@ -1,7 +1,13 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + import pandas as pd from shiny import App, Inputs, Outputs, Session, reactive, render, ui -from shiny.types import FileInfo + +if TYPE_CHECKING: + from shiny.types import FileInfo app_ui = ui.page_fluid( ui.input_file("file1", "Choose CSV File", accept=[".csv"], multiple=False), diff --git a/shiny/api-examples/input_file/app-express.py b/shiny/api-examples/input_file/app-express.py index 6a035a555..b930ad284 100644 --- a/shiny/api-examples/input_file/app-express.py +++ b/shiny/api-examples/input_file/app-express.py @@ -1,8 +1,14 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + import pandas as pd from shiny import reactive from shiny.express import input, render, ui -from shiny.types import FileInfo + +if TYPE_CHECKING: + from shiny.types import FileInfo ui.input_file("file1", "Choose CSV File", accept=[".csv"], multiple=False) ui.input_checkbox_group( diff --git a/shiny/api-examples/input_slider/app-core.py b/shiny/api-examples/input_slider/app-core.py index c8a238506..0c13e3e9e 100644 --- a/shiny/api-examples/input_slider/app-core.py +++ b/shiny/api-examples/input_slider/app-core.py @@ -12,8 +12,7 @@ def server(input: Inputs, output: Outputs, session: Session): @render.plot def distPlot(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x, input.obs(), density=True) diff --git a/shiny/api-examples/input_slider/app-express.py b/shiny/api-examples/input_slider/app-express.py index e6e701594..4af08f8c1 100644 --- a/shiny/api-examples/input_slider/app-express.py +++ b/shiny/api-examples/input_slider/app-express.py @@ -8,8 +8,7 @@ @render.plot def distPlot(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x, input.obs(), density=True) diff --git a/shiny/api-examples/isolate/app-core.py b/shiny/api-examples/isolate/app-core.py index f6a12559d..b80278463 100644 --- a/shiny/api-examples/isolate/app-core.py +++ b/shiny/api-examples/isolate/app-core.py @@ -18,8 +18,9 @@ def plot(): # ...but don't take a reactive dependency on the slider with reactive.isolate(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(input.n()) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal( + input.n() + ) fig, ax = plt.subplots() ax.hist(x, bins=30, density=True) diff --git a/shiny/api-examples/isolate/app-express.py b/shiny/api-examples/isolate/app-express.py index 52f8b9c38..192b01344 100644 --- a/shiny/api-examples/isolate/app-express.py +++ b/shiny/api-examples/isolate/app-express.py @@ -15,8 +15,7 @@ def plot(): # ...but don't take a reactive dependency on the slider with reactive.isolate(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(input.n()) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(input.n()) fig, ax = plt.subplots() ax.hist(x, bins=30, density=True) diff --git a/shiny/api-examples/layout_columns/app-core.py b/shiny/api-examples/layout_columns/app-core.py index 4691f902c..8654892cd 100644 --- a/shiny/api-examples/layout_columns/app-core.py +++ b/shiny/api-examples/layout_columns/app-core.py @@ -1,4 +1,12 @@ -from model_plots import * # model plots and cards +# model plots and cards +from model_plots import ( + card_acc, + card_feat, + card_loss, + plot_accuracy_over_time, + plot_feature_importance, + plot_loss_over_time, +) from shiny import App, Inputs, Outputs, Session, render, ui diff --git a/shiny/api-examples/layout_columns/model_plots.py b/shiny/api-examples/layout_columns/model_plots.py index 951204df9..c8b9f6cc5 100644 --- a/shiny/api-examples/layout_columns/model_plots.py +++ b/shiny/api-examples/layout_columns/model_plots.py @@ -3,10 +3,12 @@ from shiny import ui +rng = np.random.default_rng() + def plot_loss_over_time(): epochs = np.arange(1, 101) - loss = 1000 / np.sqrt(epochs) + np.random.rand(100) * 25 + loss = 1000 / np.sqrt(epochs) + rng.uniform(size=100) * 25 fig = plt.figure(figsize=(10, 6)) plt.plot(epochs, loss) @@ -17,7 +19,7 @@ def plot_loss_over_time(): def plot_accuracy_over_time(): epochs = np.arange(1, 101) - accuracy = np.sqrt(epochs) / 12 + np.random.rand(100) * 0.15 + accuracy = np.sqrt(epochs) / 12 + rng.uniform(size=100) * 0.15 accuracy = [np.min([np.max(accuracy[:i]), 1]) for i in range(1, 101)] fig = plt.figure(figsize=(10, 6)) @@ -29,7 +31,7 @@ def plot_accuracy_over_time(): def plot_feature_importance(): features = ["Product Category", "Price", "Brand", "Rating", "Number of Reviews"] - importance = np.random.rand(5) + importance = rng.uniform(size=5) fig = plt.figure(figsize=(10, 6)) plt.barh(features, importance) diff --git a/shiny/api-examples/layout_sidebar/app-core.py b/shiny/api-examples/layout_sidebar/app-core.py index 78e36e744..2a819cace 100644 --- a/shiny/api-examples/layout_sidebar/app-core.py +++ b/shiny/api-examples/layout_sidebar/app-core.py @@ -16,8 +16,7 @@ def server(input: Inputs, output: Outputs, session: Session): @render.plot(alt="A histogram") def plot() -> object: - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x, input.n(), density=True) diff --git a/shiny/api-examples/layout_sidebar/app-express.py b/shiny/api-examples/layout_sidebar/app-express.py index 10482a396..724afe970 100644 --- a/shiny/api-examples/layout_sidebar/app-express.py +++ b/shiny/api-examples/layout_sidebar/app-express.py @@ -9,8 +9,7 @@ @render.plot(alt="A histogram") def plot() -> object: - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x, input.n(), density=True) diff --git a/shiny/api-examples/nav_panel/app-core.py b/shiny/api-examples/nav_panel/app-core.py index db7650f65..4e35a38b6 100644 --- a/shiny/api-examples/nav_panel/app-core.py +++ b/shiny/api-examples/nav_panel/app-core.py @@ -61,7 +61,7 @@ def nav_controls(prefix: str) -> List[NavSetArg]: ui.navset_card_underline(*nav_controls("navset_card_underline()")), ui.h4("navset_pill_list()"), ui.navset_pill_list(*nav_controls("navset_pill_list()")), - ) + ), ) diff --git a/shiny/api-examples/notification_show/app-core.py b/shiny/api-examples/notification_show/app-core.py index 2f8e3a056..216bbe434 100644 --- a/shiny/api-examples/notification_show/app-core.py +++ b/shiny/api-examples/notification_show/app-core.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from shiny import App, Inputs, Outputs, Session, reactive, ui app_ui = ui.page_fluid( diff --git a/shiny/api-examples/notification_show/app-express.py b/shiny/api-examples/notification_show/app-express.py index d64a57431..e92050a85 100644 --- a/shiny/api-examples/notification_show/app-express.py +++ b/shiny/api-examples/notification_show/app-express.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from shiny import reactive from shiny.express import input, ui diff --git a/shiny/api-examples/output_image/app-core.py b/shiny/api-examples/output_image/app-core.py index 09452566a..b530f29be 100644 --- a/shiny/api-examples/output_image/app-core.py +++ b/shiny/api-examples/output_image/app-core.py @@ -1,5 +1,9 @@ +from typing import TYPE_CHECKING + from shiny import App, Inputs, Outputs, Session, render, ui -from shiny.types import ImgData + +if TYPE_CHECKING: + from shiny.types import ImgData app_ui = ui.page_fluid(ui.output_image("image")) diff --git a/shiny/api-examples/output_plot/app-core.py b/shiny/api-examples/output_plot/app-core.py index 339a1a6b7..1abd1b5b4 100644 --- a/shiny/api-examples/output_plot/app-core.py +++ b/shiny/api-examples/output_plot/app-core.py @@ -14,8 +14,7 @@ def server(input: Inputs, output: Outputs, session: Session): @render.plot def p(): - np.random.seed(19680801) - x_rand = 100 + 15 * np.random.randn(437) + x_rand = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x_rand, int(input.n()), density=True) return fig diff --git a/shiny/api-examples/output_plot/app-express.py b/shiny/api-examples/output_plot/app-express.py index f328cb6eb..39aea5fde 100644 --- a/shiny/api-examples/output_plot/app-express.py +++ b/shiny/api-examples/output_plot/app-express.py @@ -8,8 +8,7 @@ @render.plot def p(): - np.random.seed(19680801) - x_rand = 100 + 15 * np.random.randn(437) + x_rand = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x_rand, int(input.n()), density=True) return fig diff --git a/shiny/api-examples/output_table/app-core.py b/shiny/api-examples/output_table/app-core.py index 1dac0bab9..b23415044 100644 --- a/shiny/api-examples/output_table/app-core.py +++ b/shiny/api-examples/output_table/app-core.py @@ -53,7 +53,7 @@ def result(): } ) .set_table_styles( - [dict(selector="th", props=[("text-align", "right")])] + [{"selector": "th", "props": [("text-align", "right")]}] ) .highlight_min(color="silver") .highlight_max(color="yellow") diff --git a/shiny/api-examples/output_table/app-express.py b/shiny/api-examples/output_table/app-express.py index 12d8cb8aa..38cc6abd8 100644 --- a/shiny/api-examples/output_table/app-express.py +++ b/shiny/api-examples/output_table/app-express.py @@ -37,7 +37,7 @@ def result(): "qsec": "{0:0.2f}", } ) - .set_table_styles([dict(selector="th", props=[("text-align", "right")])]) + .set_table_styles([{"selector": "th", "props": [("text-align", "right")]}]) .highlight_min(color="silver") .highlight_max(color="yellow") ) diff --git a/shiny/api-examples/page_fixed/app-core.py b/shiny/api-examples/page_fixed/app-core.py index 80521e92c..42ae1e313 100644 --- a/shiny/api-examples/page_fixed/app-core.py +++ b/shiny/api-examples/page_fixed/app-core.py @@ -18,8 +18,7 @@ def server(input: Inputs, output: Outputs, session: Session): @render.plot(alt="A histogram") def plot() -> object: - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x, input.n(), density=True) diff --git a/shiny/api-examples/page_fixed/app-express.py b/shiny/api-examples/page_fixed/app-express.py index 91f751edd..b3f80d4db 100644 --- a/shiny/api-examples/page_fixed/app-express.py +++ b/shiny/api-examples/page_fixed/app-express.py @@ -12,8 +12,7 @@ @render.plot(alt="A histogram") def plot() -> object: - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x, input.n(), density=True) diff --git a/shiny/api-examples/page_fluid/app-core.py b/shiny/api-examples/page_fluid/app-core.py index a05d3618a..5e1888905 100644 --- a/shiny/api-examples/page_fluid/app-core.py +++ b/shiny/api-examples/page_fluid/app-core.py @@ -16,8 +16,7 @@ def server(input: Inputs, output: Outputs, session: Session): @render.plot(alt="A histogram") def plot() -> object: - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x, input.n(), density=True) diff --git a/shiny/api-examples/page_fluid/app-express.py b/shiny/api-examples/page_fluid/app-express.py index 587ec743a..86d522912 100644 --- a/shiny/api-examples/page_fluid/app-express.py +++ b/shiny/api-examples/page_fluid/app-express.py @@ -12,8 +12,7 @@ @render.plot(alt="A histogram") def plot() -> object: - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x, input.n(), density=True) diff --git a/shiny/api-examples/page_sidebar/app-core.py b/shiny/api-examples/page_sidebar/app-core.py index 8ce58f8b8..be30313ea 100644 --- a/shiny/api-examples/page_sidebar/app-core.py +++ b/shiny/api-examples/page_sidebar/app-core.py @@ -16,8 +16,7 @@ def server(input: Inputs, output: Outputs, session: Session): @render.plot(alt="A histogram") def plot() -> object: - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x, input.n(), density=True) diff --git a/shiny/api-examples/page_sidebar/app-express.py b/shiny/api-examples/page_sidebar/app-express.py index 4781f851c..12a2854a5 100644 --- a/shiny/api-examples/page_sidebar/app-express.py +++ b/shiny/api-examples/page_sidebar/app-express.py @@ -9,8 +9,7 @@ @render.plot(alt="A histogram") def plot() -> object: - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x, input.n(), density=True) diff --git a/shiny/api-examples/poll/app-core.py b/shiny/api-examples/poll/app-core.py index 6079a0be4..37ab86486 100644 --- a/shiny/api-examples/poll/app-core.py +++ b/shiny/api-examples/poll/app-core.py @@ -99,7 +99,7 @@ def stock_quotes() -> pd.DataFrame: """ ), ui.input_selectize( - "symbols", "Filter by symbol", [""] + SYMBOLS, multiple=True + "symbols", "Filter by symbol", ["", *SYMBOLS], multiple=True ), ui.output_data_frame("table"), fill=False, diff --git a/shiny/api-examples/poll/app-express.py b/shiny/api-examples/poll/app-express.py index df1f9c2cd..bba5b78ce 100644 --- a/shiny/api-examples/poll/app-express.py +++ b/shiny/api-examples/poll/app-express.py @@ -96,7 +96,7 @@ def stock_quotes() -> pd.DataFrame: case, an in-memory sqlite3) with the help of `shiny.reactive.poll`. """ ) - ui.input_selectize("symbols", "Filter by symbol", [""] + SYMBOLS, multiple=True) + ui.input_selectize("symbols", "Filter by symbol", ["", *SYMBOLS], multiple=True) @render.data_frame def table(): diff --git a/shiny/api-examples/remove_accordion_panel/app-core.py b/shiny/api-examples/remove_accordion_panel/app-core.py index fa51fd56d..fc473c077 100644 --- a/shiny/api-examples/remove_accordion_panel/app-core.py +++ b/shiny/api-examples/remove_accordion_panel/app-core.py @@ -27,7 +27,7 @@ def make_panel(letter: str) -> ui.AccordionPanel: def server(input: Inputs, output: Outputs, session: Session): # Copy the list for user - user_choices = [choice for choice in choices] + user_choices = choices.copy() @reactive.effect @reactive.event(input.remove_panel) diff --git a/shiny/api-examples/remove_accordion_panel/app-express.py b/shiny/api-examples/remove_accordion_panel/app-express.py index d01f643e6..ce2138077 100644 --- a/shiny/api-examples/remove_accordion_panel/app-express.py +++ b/shiny/api-examples/remove_accordion_panel/app-express.py @@ -20,19 +20,16 @@ f"Some narrative for section {letter}" -user_choices = [choice for choice in choices] - - @reactive.effect @reactive.event(input.remove_panel) def _(): - if len(user_choices) == 0: + if len(choices) == 0: ui.notification_show("No more panels to remove!") return - ui.remove_accordion_panel("acc", f"Section {user_choices.pop()}") + ui.remove_accordion_panel("acc", f"Section {choices.pop()}") label = "No more panels to remove!" - if len(user_choices) > 0: - label = f"Remove Section {user_choices[-1]}" + if len(choices) > 0: + label = f"Remove Section {choices[-1]}" ui.update_action_button("remove_panel", label=label) diff --git a/shiny/api-examples/render_image/app-core.py b/shiny/api-examples/render_image/app-core.py index 60d35d90a..a3c7d6732 100644 --- a/shiny/api-examples/render_image/app-core.py +++ b/shiny/api-examples/render_image/app-core.py @@ -1,5 +1,9 @@ +from typing import TYPE_CHECKING + from shiny import App, Inputs, Outputs, Session, render, ui -from shiny.types import ImgData + +if TYPE_CHECKING: + from shiny.types import ImgData app_ui = ui.page_fluid(ui.output_image("image")) diff --git a/shiny/api-examples/row/app-core.py b/shiny/api-examples/row/app-core.py index be69085a7..ee82d3e25 100644 --- a/shiny/api-examples/row/app-core.py +++ b/shiny/api-examples/row/app-core.py @@ -14,8 +14,7 @@ def server(input: Inputs, output: Outputs, session: Session): @render.plot(alt="A histogram") def plot() -> object: - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) + x = 100 + 15 * np.random.default_rng(seed=19680801).standard_normal(437) fig, ax = plt.subplots() ax.hist(x, input.n(), density=True) diff --git a/shiny/experimental/ui/_deprecated.py b/shiny/experimental/ui/_deprecated.py index e6c98b051..9356d712e 100644 --- a/shiny/experimental/ui/_deprecated.py +++ b/shiny/experimental/ui/_deprecated.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Literal, Optional, Sequence, TypeVar, overload +from typing import TYPE_CHECKING, Any, Literal, Optional, Sequence, TypeVar, overload from htmltools import ( MetadataNode, @@ -46,10 +46,6 @@ from ...ui._page import page_fillable as main_page_fillable from ...ui._page import page_navbar as main_page_navbar from ...ui._page import page_sidebar as main_page_sidebar -from ...ui._plot_output_opts import BrushOpts as MainBrushOpts -from ...ui._plot_output_opts import ClickOpts as MainClickOpts -from ...ui._plot_output_opts import DblClickOpts as MainDblClickOpts -from ...ui._plot_output_opts import HoverOpts as MainHoverOpts from ...ui._sidebar import DeprecatedPanelMain, DeprecatedPanelSidebar from ...ui._sidebar import Sidebar as MainSidebar from ...ui._sidebar import SidebarOpenSpec as MainSidebarOpenSpec @@ -72,6 +68,12 @@ from ...ui.fill._fill import is_fill_item as main_is_fill_item from ...ui.fill._fill import is_fillable_container as main_is_fillable_container +if TYPE_CHECKING: + from ...ui._plot_output_opts import BrushOpts as MainBrushOpts + from ...ui._plot_output_opts import ClickOpts as MainClickOpts + from ...ui._plot_output_opts import DblClickOpts as MainDblClickOpts + from ...ui._plot_output_opts import HoverOpts as MainHoverOpts + __all__ = ( # Input Switch "toggle_switch", @@ -727,8 +729,6 @@ class AccordionPanel(MainAccordionPanel): Deprecated. Please use `shiny.ui.AccordionPanel` instead. """ - ... - # Deprecated 2023-09-12 def accordion( diff --git a/shiny/express/_output.py b/shiny/express/_output.py index 375c35a70..6d89b1eb5 100644 --- a/shiny/express/_output.py +++ b/shiny/express/_output.py @@ -1,13 +1,16 @@ from __future__ import annotations -from contextlib import AbstractContextManager -from typing import Callable, TypeVar +from typing import TYPE_CHECKING, Callable, TypeVar from .._deprecated import warn_deprecated from .._typing_extensions import ParamSpec -from ..render.renderer import RendererT from .ui import hold +if TYPE_CHECKING: + from contextlib import AbstractContextManager + + from ..render.renderer import RendererT + __all__ = ("suspend_display",) P = ParamSpec("P") @@ -48,7 +51,7 @@ def wrapper(renderer: RendererT) -> RendererT: def suspend_display( - fn: Callable[P, R] | RendererT | None = None + fn: Callable[P, R] | RendererT | None = None, ) -> Callable[P, R] | RendererT | AbstractContextManager[None]: warn_deprecated( "`suspend_display` is deprecated. Please use `ui.hold` instead. " diff --git a/shiny/express/_recall_context.py b/shiny/express/_recall_context.py index f846de89c..337b0bb7e 100644 --- a/shiny/express/_recall_context.py +++ b/shiny/express/_recall_context.py @@ -2,13 +2,15 @@ import functools import sys -from types import TracebackType -from typing import Callable, Generic, Mapping, Optional, Type, TypeVar +from typing import TYPE_CHECKING, Callable, Generic, Mapping, Optional, Type, TypeVar from htmltools import MetadataNode, Tag, TagList, wrap_displayhook_handler from .._typing_extensions import ParamSpec +if TYPE_CHECKING: + from types import TracebackType + P = ParamSpec("P") R = TypeVar("R") U = TypeVar("U") @@ -24,7 +26,7 @@ def __init__( ): self.fn = fn if args is None: - args = tuple() + args = () if kwargs is None: kwargs = {} self.args: list[object] = list(args) @@ -81,7 +83,7 @@ def tagify(self) -> Tag | TagList | MetadataNode | str: def wrap_recall_context_manager( - fn: Callable[P, R] + fn: Callable[P, R], ) -> Callable[P, RecallContextManager[R]]: @functools.wraps(fn) def wrapped_fn(*args: P.args, **kwargs: P.kwargs) -> RecallContextManager[R]: diff --git a/shiny/express/_run.py b/shiny/express/_run.py index aae70a47d..04f2cd0dd 100644 --- a/shiny/express/_run.py +++ b/shiny/express/_run.py @@ -3,7 +3,7 @@ import ast import sys from pathlib import Path -from typing import Mapping, cast +from typing import Mapping, TYPE_CHECKING, cast from htmltools import Tag, TagList @@ -15,13 +15,15 @@ from ..types import MISSING, MISSING_TYPE from ._is_express import find_magic_comment_mode from ._mock_session import ExpressMockSession -from ._recall_context import RecallContextManager from .expressify_decorator._func_displayhook import _expressify_decorator_function_def from .expressify_decorator._node_transformers import ( DisplayFuncsTransformer, expressify_decorator_func_name, ) +if TYPE_CHECKING: + from ._recall_context import RecallContextManager + __all__ = ( "app_opts", "wrap_express_app", diff --git a/shiny/express/app.py b/shiny/express/app.py index 9612d0892..5e7bcea38 100644 --- a/shiny/express/app.py +++ b/shiny/express/app.py @@ -1,11 +1,14 @@ from __future__ import annotations from pathlib import Path +from typing import TYPE_CHECKING -from .._app import App from ._run import wrap_express_app from ._utils import unescape_from_var_name +if TYPE_CHECKING: + from .._app import App + # If someone requests shiny.express.app:_2f_path_2f_to_2f_app_2e_py, then we will call # wrap_express_app(Path("/path/to/app.py")) and return the result. diff --git a/shiny/express/expressify_decorator/_node_transformers.py b/shiny/express/expressify_decorator/_node_transformers.py index b80333f7e..478235d4e 100644 --- a/shiny/express/expressify_decorator/_node_transformers.py +++ b/shiny/express/expressify_decorator/_node_transformers.py @@ -1,11 +1,13 @@ from __future__ import annotations import ast -import types -from typing import Any, Callable, cast +from typing import TYPE_CHECKING, Any, Callable, cast from ._helpers import ast_matches_func +if TYPE_CHECKING: + import types + sys_alias = "__auto_displayhook_sys__" expressify_decorator_func_name = "_expressify_decorator_function_def" diff --git a/shiny/express/ui/_cm_components.py b/shiny/express/ui/_cm_components.py index f7ff8386c..f749a9104 100644 --- a/shiny/express/ui/_cm_components.py +++ b/shiny/express/ui/_cm_components.py @@ -1,22 +1,25 @@ "Context manager components for Shiny Express" +# ruff: noqa: C408 # https://docs.astral.sh/ruff/rules/unnecessary-literal-dict/ from __future__ import annotations -from typing import Literal, Optional - -from htmltools import Tag, TagAttrs, TagAttrValue, TagChild, TagFunction, TagList +from typing import TYPE_CHECKING, Literal, Optional from ... import ui from ..._docstring import add_example, no_example from ...types import MISSING, MISSING_TYPE -from ...ui._accordion import AccordionPanel -from ...ui._card import CardItem -from ...ui._layout_columns import BreakpointsUser -from ...ui._navs import NavMenu, NavPanel, NavSet, NavSetBar, NavSetCard -from ...ui._sidebar import SidebarOpenSpec, SidebarOpenValue -from ...ui.css import CssUnit from .._recall_context import RecallContextManager +if TYPE_CHECKING: + from htmltools import Tag, TagAttrs, TagAttrValue, TagChild, TagFunction, TagList + + from ...ui._accordion import AccordionPanel + from ...ui._card import CardItem + from ...ui._layout_columns import BreakpointsUser + from ...ui._navs import NavMenu, NavPanel, NavSet, NavSetBar, NavSetCard + from ...ui._sidebar import SidebarOpenSpec, SidebarOpenValue + from ...ui.css import CssUnit + __all__ = ( "sidebar", "layout_sidebar", diff --git a/shiny/express/ui/_hold.py b/shiny/express/ui/_hold.py index b6fe8d295..be5c820dd 100644 --- a/shiny/express/ui/_hold.py +++ b/shiny/express/ui/_hold.py @@ -1,14 +1,16 @@ from __future__ import annotations import sys -from types import TracebackType -from typing import Callable, Optional, Type, TypeVar +from typing import TYPE_CHECKING, Callable, Optional, Type, TypeVar from htmltools import wrap_displayhook_handler from ..._docstring import no_example from ..._typing_extensions import ParamSpec +if TYPE_CHECKING: + from types import TracebackType + __all__ = ("hold",) P = ParamSpec("P") @@ -46,7 +48,7 @@ def hold() -> HoldContextManager: class HoldContextManager: def __init__(self): - self.content: list[object] = list() + self.content: list[object] = [] def __enter__(self) -> list[object]: self.prev_displayhook = sys.displayhook diff --git a/shiny/express/ui/_page.py b/shiny/express/ui/_page.py index 738d25356..2f83bc0be 100644 --- a/shiny/express/ui/_page.py +++ b/shiny/express/ui/_page.py @@ -1,8 +1,6 @@ from __future__ import annotations -from typing import Callable - -from htmltools import Tag +from typing import TYPE_CHECKING, Callable from ... import ui from ..._docstring import no_example @@ -10,6 +8,9 @@ from .._recall_context import RecallContextManager from .._run import get_top_level_recall_context_manager +if TYPE_CHECKING: + from htmltools import Tag + __all__ = ("page_opts",) diff --git a/shiny/http_staticfiles.py b/shiny/http_staticfiles.py index db38f543c..257131050 100644 --- a/shiny/http_staticfiles.py +++ b/shiny/http_staticfiles.py @@ -17,8 +17,11 @@ ) import sys +from typing import TYPE_CHECKING -from starlette.background import BackgroundTask +if TYPE_CHECKING: + from starlette.background import BackgroundTask + from starlette.types import Receive, Scope, Send if "pyodide" not in sys.modules: # Running in native mode; use starlette StaticFiles @@ -39,7 +42,6 @@ from typing import Iterable, MutableMapping, Optional from starlette.responses import PlainTextResponse - from starlette.types import Receive, Scope, Send from . import _utils diff --git a/shiny/input_handler.py b/shiny/input_handler.py index 01d86067e..71130e49e 100644 --- a/shiny/input_handler.py +++ b/shiny/input_handler.py @@ -100,7 +100,6 @@ def _(value, name, session): def _( value: str | list[str], name: ResolvedId, session: Session ) -> date | None | tuple[date | None, date | None]: - if isinstance(value, str): return _safe_strptime_date(value) else: diff --git a/shiny/module.py b/shiny/module.py index c4001b3b9..26444be25 100644 --- a/shiny/module.py +++ b/shiny/module.py @@ -32,7 +32,7 @@ def wrapper(id: Id, *args: P.args, **kwargs: P.kwargs) -> R: @no_example() def server( - fn: Callable[Concatenate[Inputs, Outputs, Session, P], R] + fn: Callable[Concatenate[Inputs, Outputs, Session, P], R], ) -> Callable[Concatenate[str, P], R]: from .session import require_active_session, session_context diff --git a/shiny/plotutils.py b/shiny/plotutils.py index 85aebac8a..4d7eef4d0 100644 --- a/shiny/plotutils.py +++ b/shiny/plotutils.py @@ -10,12 +10,13 @@ from typing import TYPE_CHECKING, Literal, Optional, Union, cast from ._typing_extensions import TypedDict -from .types import BrushInfo, CoordInfo, CoordXY if TYPE_CHECKING: import numpy.typing as npt import pandas as pd + from .types import BrushInfo, CoordInfo, CoordXY + DataFrameColumn = Union[ "pd.Series[int]", "pd.Series[float]", @@ -227,7 +228,7 @@ def near_points( # For no current coordinfo if coordinfo is None: if add_dist: - new_df["dist"] = np.NaN + new_df["dist"] = np.nan if all_rows: new_df["selected_"] = False @@ -353,7 +354,8 @@ def to_float(x: DataFrameColumn) -> pd.Series[float]: return cast("pd.Series[float]", x.cat.codes + 1) # pyright: ignore elif ptypes.is_string_dtype(x): # pyright: ignore[reportUnknownMemberType] return cast( - "pd.Series[float]", x.astype("category").cat.codes + 1 # pyright: ignore + "pd.Series[float]", + x.astype("category").cat.codes + 1, # pyright: ignore ) elif ptypes.is_datetime64_any_dtype(x): # pyright: ignore[reportUnknownMemberType] # We need to convert the pandas datetimes, which are in nanoseconds since epoch, diff --git a/shiny/reactive/_extended_task.py b/shiny/reactive/_extended_task.py index d394cadb3..69d40a833 100644 --- a/shiny/reactive/_extended_task.py +++ b/shiny/reactive/_extended_task.py @@ -207,7 +207,7 @@ def extended_task(func: Callable[P, Awaitable[R]]) -> ExtendedTask[P, R]: ... @add_example() def extended_task( - func: Optional[Callable[P, Awaitable[R]]] = None + func: Optional[Callable[P, Awaitable[R]]] = None, ) -> ExtendedTask[P, R] | Callable[[Callable[P, Awaitable[R]]], ExtendedTask[P, R]]: """ Decorator to mark an async function as a slow computation. This will cause the diff --git a/shiny/reactive/_reactives.py b/shiny/reactive/_reactives.py index f82658d09..5c7b1498c 100644 --- a/shiny/reactive/_reactives.py +++ b/shiny/reactive/_reactives.py @@ -483,7 +483,7 @@ def __init__( if isinstance(fn, Renderer): raise TypeError( "`@reactive.effect` can not be combined with `@render.xx`.\n" - + "Please remove your call of `@reactive.effect`." + "Please remove your call of `@reactive.effect`." ) # The EffectAsync subclass will pass in an async function, but it tells the @@ -794,10 +794,10 @@ def event( ``@render.ui``, etc). """ - if any([not callable(arg) for arg in args]): + if any(not callable(arg) for arg in args): raise TypeError( "All objects passed to event decorator must be callable.\n" - + "If you are calling `@reactive.event(f())`, try calling `@reactive.event(f)` instead." + "If you are calling `@reactive.event(f())`, try calling `@reactive.event(f)` instead." ) if len(args) == 0: @@ -809,14 +809,14 @@ def decorator(user_fn: Callable[[], T]) -> Callable[[], T]: if not callable(user_fn): raise TypeError( "`@reactive.event()` must be applied to a function or Callable object.\n" - + "It should usually be applied before `@Calc`,` @Effect`, or `@render.xx` function.\n" - + "In other words, `@reactive.event()` goes below the other decorators." + "It should usually be applied before `@Calc`,` @Effect`, or `@render.xx` function.\n" + "In other words, `@reactive.event()` goes below the other decorators." ) if isinstance(user_fn, Calc_): raise TypeError( "`@reactive.event()` must be applied before `@reactive.calc`.\n" - + "In other words, `@reactive.calc` must be above `@reactive.event()`." + "In other words, `@reactive.calc` must be above `@reactive.event()`." ) # This is here instead of at the top of the .py file in order to avoid a @@ -828,7 +828,7 @@ def decorator(user_fn: Callable[[], T]) -> Callable[[], T]: # use case. For now we'll disallow it, for simplicity. raise TypeError( "`@reactive.event()` must be applied before `@render.xx` .\n" - + "In other words, `@render.xx` must be above `@reactive.event()`." + "In other words, `@render.xx` must be above `@reactive.event()`." ) initialized = False @@ -862,10 +862,10 @@ async def new_user_async_fn(): return new_user_async_fn # type: ignore - elif any([is_async_callable(arg) for arg in args]): + elif any(is_async_callable(arg) for arg in args): raise TypeError( - "When decorating a synchronous function with @reactive.event(), all" - + "arguments to @reactive.event() must be synchronous functions." + "When decorating a synchronous function with @reactive.event(), all " + "arguments to @reactive.event() must be synchronous functions." ) else: diff --git a/shiny/render/_coordmap.py b/shiny/render/_coordmap.py index e82fcb903..18baa25a2 100644 --- a/shiny/render/_coordmap.py +++ b/shiny/render/_coordmap.py @@ -5,17 +5,6 @@ from copy import deepcopy from typing import TYPE_CHECKING, Any, cast -from ..types import ( - Coordmap, - CoordmapDims, - CoordmapPanel, - CoordmapPanelDomain, - CoordmapPanelLog, - CoordmapPanelMapping, - CoordmapPanelRange, - PlotnineFigure, -) - if TYPE_CHECKING: import numpy as np import numpy.typing as npt @@ -23,6 +12,17 @@ from matplotlib.figure import Figure from matplotlib.gridspec import SubplotSpec + from ..types import ( + Coordmap, + CoordmapDims, + CoordmapPanel, + CoordmapPanelDomain, + CoordmapPanelLog, + CoordmapPanelMapping, + CoordmapPanelRange, + PlotnineFigure, + ) + def get_coordmap(fig: Figure) -> Coordmap | None: dims_ar = fig.get_size_inches() * fig.get_dpi() diff --git a/shiny/render/_dataframe.py b/shiny/render/_dataframe.py index 16c0509f3..1d770d138 100644 --- a/shiny/render/_dataframe.py +++ b/shiny/render/_dataframe.py @@ -4,8 +4,6 @@ import json from typing import TYPE_CHECKING, Any, Literal, Protocol, Union, cast, runtime_checkable -from htmltools import Tag - from .. import ui from .._docstring import add_example, no_example from ._dataframe_unsafe import serialize_numpy_dtypes @@ -13,6 +11,7 @@ if TYPE_CHECKING: import pandas as pd + from htmltools import Tag class AbstractTabularData(abc.ABC): @@ -96,15 +95,15 @@ def __init__( def to_payload(self) -> Jsonifiable: res = serialize_pandas_df(self.data) - res["options"] = dict( - width=self.width, - height=self.height, - summary=self.summary, - filters=self.filters, - row_selection_mode=self.row_selection_mode, - style="grid", - fill=self.height is None, - ) + res["options"] = { + "width": self.width, + "height": self.height, + "summary": self.summary, + "filters": self.filters, + "row_selection_mode": self.row_selection_mode, + "style": "grid", + "fill": self.height is None, + } return res @@ -164,9 +163,7 @@ def __init__( height: Union[str, float, None] = "500px", summary: Union[bool, str] = True, filters: bool = False, - row_selection_mode: Union[ - Literal["none"], Literal["single"], Literal["multiple"] - ] = "none", + row_selection_mode: Literal["none", "single", "multiple"] = "none", ): import pandas as pd @@ -186,14 +183,14 @@ def __init__( def to_payload(self) -> Jsonifiable: res = serialize_pandas_df(self.data) - res["options"] = dict( - width=self.width, - height=self.height, - summary=self.summary, - filters=self.filters, - row_selection_mode=self.row_selection_mode, - style="table", - ) + res["options"] = { + "width": self.width, + "height": self.height, + "summary": self.summary, + "filters": self.filters, + "row_selection_mode": self.row_selection_mode, + "style": "table", + } return res diff --git a/shiny/render/_express.py b/shiny/render/_express.py index cc7d65b60..a37fb2fd2 100644 --- a/shiny/render/_express.py +++ b/shiny/render/_express.py @@ -1,12 +1,11 @@ from __future__ import annotations import sys -from typing import Optional +from typing import TYPE_CHECKING, Optional from htmltools import Tag, TagAttrValue, TagFunction, TagList, wrap_displayhook_handler from .. import ui as _ui -from .._typing_extensions import Self from ..session._utils import require_active_session from ..types import MISSING, MISSING_TYPE from .renderer import AsyncValueFn, Renderer, ValueFn @@ -16,6 +15,9 @@ set_kwargs_value, ) +if TYPE_CHECKING: + from .._typing_extensions import Self + class express(Renderer[None]): """ diff --git a/shiny/render/_render.py b/shiny/render/_render.py index 9afda8c91..23cace29a 100644 --- a/shiny/render/_render.py +++ b/shiny/render/_render.py @@ -22,14 +22,15 @@ from htmltools import Tag, TagAttrValue, TagChild if TYPE_CHECKING: - from ..session._utils import RenderedDeps import pandas as pd + from .._typing_extensions import Self + from ..session._utils import RenderedDeps + from .. import _utils from .. import ui as _ui from .._docstring import add_example, no_example from .._namespaces import ResolvedId -from .._typing_extensions import Self from ..express._mock_session import ExpressMockSession from ..session import get_current_session, require_active_session from ..session._session import DownloadHandler, DownloadInfo @@ -383,8 +384,8 @@ def cast_result(result: ImgData | None) -> dict[str, Jsonifiable] | None: raise Exception( f"@render.plot doesn't know to render objects of type '{str(type(x))}'. " - + "Consider either requesting support for this type of plot object, and/or " - + " explictly saving the object to a (png) file and using @render.image." + "Consider either requesting support for this type of plot object, and/or " + " explictly saving the object to a (png) file and using @render.image." ) @@ -534,7 +535,7 @@ def __init__( # TODO: deal with kwargs collision with output_table async def transform(self, value: TableResult) -> dict[str, Jsonifiable]: - import pandas + import pandas as pd import pandas.io.formats.style html: str @@ -544,7 +545,7 @@ async def transform(self, value: TableResult) -> dict[str, Jsonifiable]: value.to_html(**self.kwargs), # pyright: ignore ) else: - if not isinstance(value, pandas.DataFrame): + if not isinstance(value, pd.DataFrame): if not isinstance(value, PandasCompatible): raise TypeError( "@render.table doesn't know how to render objects of type " diff --git a/shiny/render/_try_render_plot.py b/shiny/render/_try_render_plot.py index d2df11441..dff6e6d47 100644 --- a/shiny/render/_try_render_plot.py +++ b/shiny/render/_try_render_plot.py @@ -137,8 +137,7 @@ def try_render_matplotlib( return (False, None) try: - import matplotlib - import matplotlib.pyplot as plt # pyright: ignore[reportUnusedImport] # noqa: F401 + import matplotlib.pyplot as plt # pyright: ignore[reportUnusedImport] pixelratio = plot_size_info.pixelratio @@ -226,14 +225,12 @@ def try_render_matplotlib( return (True, res) finally: - import matplotlib.pyplot + import matplotlib.pyplot as plt - matplotlib.pyplot.close(fig) # pyright: ignore[reportUnknownMemberType] + plt.close(fig) # pyright: ignore[reportUnknownMemberType] -def get_matplotlib_figure( - x: object, allow_global: bool -) -> Figure | None: # pyright: ignore +def get_matplotlib_figure(x: object, allow_global: bool) -> Figure | None: # pyright: ignore import matplotlib.pyplot as plt from matplotlib.animation import Animation from matplotlib.artist import Artist @@ -260,8 +257,8 @@ def get_matplotlib_figure( if isinstance(x, Animation): raise RuntimeError( "Matplotlib's Animation class isn't supported by @render.plot. " - + "Consider explictly saving the animation to a file and " - + "then using @render.image instead to render it." + "Consider explictly saving the animation to a file and " + "then using @render.image instead to render it." ) # Libraries like pandas, xarray, etc have plot() methods that can return a wide diff --git a/shiny/render/renderer/__init__.py b/shiny/render/renderer/__init__.py index 057a08725..af692e879 100644 --- a/shiny/render/renderer/__init__.py +++ b/shiny/render/renderer/__init__.py @@ -1,4 +1,4 @@ -from ._renderer import ( # noqa: F401 +from ._renderer import ( Renderer, ValueFn, Jsonifiable, diff --git a/shiny/render/renderer/_renderer.py b/shiny/render/renderer/_renderer.py index ba4b28d88..022107c16 100644 --- a/shiny/render/renderer/_renderer.py +++ b/shiny/render/renderer/_renderer.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import ( + TYPE_CHECKING, Any, Awaitable, Callable, @@ -17,9 +18,11 @@ from htmltools import MetadataNode, Tag, TagList from ..._docstring import add_example -from ..._typing_extensions import Self from ..._utils import is_async_callable, wrap_async +if TYPE_CHECKING: + from ..._typing_extensions import Self + # TODO-barret-future: Double check docs are rendererd # Missing first paragraph from some classes: Example: TransformerMetadata. # No init method for TransformerParams. This is because the `DocClass` object does not @@ -137,7 +140,7 @@ class Renderer(Generic[IT]): # Q: Could we do this with typing without putting `P` in the Generic? # A: No. Even if we had a `P` in the Generic, the calling decorator would not have access to it. # Idea: Possibly use a chained method of `.ui_kwargs()`? https://github.com/posit-dev/py-shiny/issues/971 - _auto_output_ui_kwargs: dict[str, Any] = dict() + _auto_output_ui_kwargs: dict[str, Any] = {} __name__: str """ diff --git a/shiny/render/renderer/_utils.py b/shiny/render/renderer/_utils.py index 8bed1c415..74eedfe20 100644 --- a/shiny/render/renderer/_utils.py +++ b/shiny/render/renderer/_utils.py @@ -1,13 +1,15 @@ from __future__ import annotations -from typing import Any, Dict, cast +from typing import TYPE_CHECKING, Any, Dict, cast -from htmltools import TagFunction - -from ...session._utils import RenderedDeps from ...types import MISSING_TYPE, ImgData from ._renderer import Jsonifiable +if TYPE_CHECKING: + from htmltools import TagFunction + + from ...session._utils import RenderedDeps + JsonifiableDict = Dict[str, Jsonifiable] diff --git a/shiny/render/transformer/_transformer.py b/shiny/render/transformer/_transformer.py index 1476e1679..5a2a01e3f 100644 --- a/shiny/render/transformer/_transformer.py +++ b/shiny/render/transformer/_transformer.py @@ -33,10 +33,10 @@ ) from ..renderer import Jsonifiable, Renderer -from ..renderer._renderer import DefaultUIFn, DefaultUIFnResultOrNone if TYPE_CHECKING: from ...session import Session + from ..renderer._renderer import DefaultUIFn, DefaultUIFnResultOrNone from ..._deprecated import warn_deprecated from ..._docstring import add_example @@ -263,8 +263,8 @@ def __init__( self._default_ui = default_ui self._default_ui_passthrough_args = default_ui_passthrough_args - self._default_ui_args: tuple[object, ...] = tuple() - self._default_ui_kwargs: dict[str, object] = dict() + self._default_ui_args: tuple[object, ...] = () + self._default_ui_kwargs: dict[str, object] = {} # Allow for App authors to not require `@output` self._auto_register() diff --git a/shiny/session/__init__.py b/shiny/session/__init__.py index 8aecb2038..354014b66 100644 --- a/shiny/session/__init__.py +++ b/shiny/session/__init__.py @@ -3,7 +3,7 @@ """ from ._session import Session, Inputs, Outputs -from ._utils import ( # noqa: F401 +from ._utils import ( get_current_session, session_context as session_context, require_active_session, diff --git a/shiny/session/_session.py b/shiny/session/_session.py index 12bc3e578..fa0d954b9 100644 --- a/shiny/session/_session.py +++ b/shiny/session/_session.py @@ -190,8 +190,8 @@ def __init__( # query information about the request, like headers, cookies, etc. self.http_conn: HTTPConnection = conn.get_http_conn() - self.input: Inputs = Inputs(dict()) - self.output: Outputs = Outputs(self, self.ns, dict(), dict()) + self.input: Inputs = Inputs({}) + self.output: Outputs = Outputs(self, self.ns, {}, {}) self.user: str | None = None self.groups: list[str] | None = None @@ -677,7 +677,6 @@ def _send_message_sync(self, message: dict[str, object]) -> None: def _send_error_response(self, message_str: str) -> None: print("_send_error_response: " + message_str) - pass # ========================================================================== # Flush @@ -943,7 +942,8 @@ def download( def wrapper(fn: DownloadHandler): id_ = self.ns(id or fn.__name__) return self._parent.download( - id=id_, **kwargs # pyright: ignore[reportArgumentType] + id=id_, + **kwargs, # pyright: ignore[reportArgumentType] )(fn) return wrapper @@ -1063,7 +1063,7 @@ def set_renderer(renderer: RendererT) -> RendererT: if not isinstance(renderer, Renderer): raise TypeError( "`@output` must be applied to a `@render.xx` function.\n" - + "In other words, `@output` must be above `@render.xx`." + "In other words, `@output` must be above `@render.xx`." ) # Get the (possibly namespaced) output id diff --git a/shiny/types.py b/shiny/types.py index 89a22ac68..61551833e 100644 --- a/shiny/types.py +++ b/shiny/types.py @@ -14,12 +14,11 @@ from typing import TYPE_CHECKING, Any, BinaryIO, Literal, NamedTuple, Optional, Protocol -from htmltools import TagChild - from ._docstring import add_example from ._typing_extensions import NotRequired, TypedDict if TYPE_CHECKING: + from htmltools import TagChild from matplotlib.figure import Figure @@ -90,8 +89,6 @@ class SafeException(Exception): generate an error that is OK to be displayed to the user. """ - pass - @add_example() class SilentException(Exception): @@ -113,8 +110,6 @@ class SilentException(Exception): * :class:`~shiny.types.SilentCancelOutputException` """ - pass - @add_example() class SilentCancelOutputException(Exception): @@ -129,8 +124,6 @@ class SilentCancelOutputException(Exception): * :class:`~shiny.types.SilentException` """ - pass - class SilentOperationInProgressException(SilentException): # Throw a silent exception to indicate that an operation is in progress diff --git a/shiny/ui/_input_task_button.py b/shiny/ui/_input_task_button.py index 76229e915..bc1d60a0e 100644 --- a/shiny/ui/_input_task_button.py +++ b/shiny/ui/_input_task_button.py @@ -3,7 +3,7 @@ __all__ = ("input_task_button",) from functools import partial -from typing import Callable, Optional, TypeVar, cast, overload +from typing import TYPE_CHECKING, Callable, Optional, TypeVar, cast, overload from htmltools import HTML, Tag, TagAttrValue, TagChild, css, tags @@ -12,11 +12,13 @@ from .._docstring import add_example from .._namespaces import resolve_id from .._typing_extensions import ParamSpec -from ..reactive._extended_task import ExtendedTask from ..reactive._reactives import effect from ._html_deps_py_shiny import spin_dependency from ._html_deps_shinyverse import components_dependencies +if TYPE_CHECKING: + from ..reactive._extended_task import ExtendedTask + P = ParamSpec("P") R = TypeVar("R") diff --git a/shiny/ui/_input_update.py b/shiny/ui/_input_update.py index e7c8a7760..332a11a81 100644 --- a/shiny/ui/_input_update.py +++ b/shiny/ui/_input_update.py @@ -20,11 +20,9 @@ import json import re -from datetime import date from typing import TYPE_CHECKING, Literal, Mapping, Optional, cast, overload from htmltools import TagChild, TagList, tags -from starlette.requests import Request from starlette.responses import JSONResponse, Response from .._docstring import add_example, doc_format, no_example @@ -41,6 +39,10 @@ from ._utils import JSEval, _session_on_flush_send_msg, extract_js_keys if TYPE_CHECKING: + from datetime import date + + from starlette.requests import Request + from .. import Session @@ -751,10 +753,10 @@ def selectize_choices_json(request: Request) -> Response: if isinstance(search_fields[0], list): search_fields = search_fields[0] - if set(search_fields).difference(set(["label", "value", "optgroup"])): + if set(search_fields).difference({"label", "value", "optgroup"}): raise ValueError( "The selectize.js searchFields option must contain some combination of: " - + "'label', 'value', and 'optgroup'" + "'label', 'value', and 'optgroup'" ) # i.e. valueField (defaults to 'value') diff --git a/shiny/ui/_layout_columns.py b/shiny/ui/_layout_columns.py index d2de2d38e..e63de30bb 100644 --- a/shiny/ui/_layout_columns.py +++ b/shiny/ui/_layout_columns.py @@ -201,7 +201,8 @@ def validate_col_width( if len(list(y)) > n_kids: warn( - f"More column widths than children at breakpoint '{break_name}', extra widths will be ignored." + f"More column widths than children at breakpoint '{break_name}', extra widths will be ignored.", + stacklevel=2, ) return y @@ -262,9 +263,7 @@ def row_heights_attrs( # We use classes to activate CSS variables at the right breakpoints. Note: Mobile # row height is derived from xs or defaults to auto in the CSS, so we don't need the # class to activate it - classes = [ - f"bslib-grid--row-heights--{brk}" for brk in x_complete.keys() if brk != "xs" - ] + classes = [f"bslib-grid--row-heights--{brk}" for brk in x_complete if brk != "xs"] # Create CSS variables, treating numeric values as fractional units, passing strings css_vars: Dict[str, str] = {} diff --git a/shiny/ui/_markdown.py b/shiny/ui/_markdown.py index 3693f129d..18ddca3f8 100644 --- a/shiny/ui/_markdown.py +++ b/shiny/ui/_markdown.py @@ -52,7 +52,7 @@ def markdown( def default_md_renderer( - preset: Literal["commonmark", "gfm"] = "gfm" + preset: Literal["commonmark", "gfm"] = "gfm", ) -> Callable[[str], str]: try: from markdown_it.main import MarkdownIt diff --git a/shiny/ui/_modal.py b/shiny/ui/_modal.py index c24d164ab..57a11d2dd 100644 --- a/shiny/ui/_modal.py +++ b/shiny/ui/_modal.py @@ -7,15 +7,17 @@ "modal_remove", ) -from typing import Literal, Optional +from typing import TYPE_CHECKING, Literal, Optional from htmltools import HTML, Tag, TagAttrs, TagAttrValue, TagChild, div, tags from .._docstring import add_example from ..session import require_active_session -from ..session._session import Session from ..types import MISSING, MISSING_TYPE +if TYPE_CHECKING: + from ..session._session import Session + @add_example(ex_dir="../api-examples/modal") def modal_button(label: TagChild, icon: TagChild = None, **kwargs: TagAttrValue) -> Tag: @@ -129,16 +131,13 @@ def modal( ) # jQuery plugin doesn't work in Bootstrap 5, but vanilla JS doesn't work in Bootstrap 4 :sob: - js = "\n".join( - [ - "if (window.bootstrap && !window.bootstrap.Modal.VERSION.match(/^4\\. /)) {", - " var modal=new bootstrap.Modal(document.getElementById('shiny-modal'))", - " modal.show()", - "} else {", - " $('#shiny-modal').modal().focus()", - "}", - ] - ) + js = """\ +if (window.bootstrap && !window.bootstrap.Modal.VERSION.match(/^4\\. /)) { + var modal=new bootstrap.Modal(document.getElementById('shiny-modal')) + modal.show() +} else { + $('#shiny-modal').modal().focus() +}""" backdrop = None if easy_close else "static" keyboard = None if easy_close else "false" diff --git a/shiny/ui/_notification.py b/shiny/ui/_notification.py index 33593dce2..00804a1a1 100644 --- a/shiny/ui/_notification.py +++ b/shiny/ui/_notification.py @@ -4,13 +4,13 @@ from typing import TYPE_CHECKING, Any, Literal, Optional -from htmltools import TagChild - from .._docstring import add_example, no_example from .._utils import rand_hex from ..session import require_active_session if TYPE_CHECKING: + from htmltools import TagChild + from .. import Session diff --git a/shiny/ui/_output.py b/shiny/ui/_output.py index 2b21ae193..a509076dc 100644 --- a/shiny/ui/_output.py +++ b/shiny/ui/_output.py @@ -195,7 +195,7 @@ def output_image( height = f"{height}px" if isinstance(height, (float, int)) else height style = None if inline else css(width=width, height=height) - args: dict[str, str] = dict() + args: dict[str, str] = {} id_resolved = resolve_id(id) diff --git a/shiny/ui/_plot_output_opts.py b/shiny/ui/_plot_output_opts.py index d9c542b8b..c1e387d7d 100644 --- a/shiny/ui/_plot_output_opts.py +++ b/shiny/ui/_plot_output_opts.py @@ -44,7 +44,7 @@ def format_opt_names( opts: ClickOpts | DblClickOpts | HoverOpts | BrushOpts, prefix: str, ) -> dict[str, str]: - new_opts: dict[str, str] = dict() + new_opts: dict[str, str] = {} for key, value in opts.items(): new_key = f"data-{prefix}-" + re.sub("([A-Z])", "-\\1", key).lower() new_value = str(value) diff --git a/shiny/ui/_progress.py b/shiny/ui/_progress.py index 5f5ad764c..f18a682da 100644 --- a/shiny/ui/_progress.py +++ b/shiny/ui/_progress.py @@ -2,17 +2,18 @@ __all__ = ("Progress",) -from types import TracebackType from typing import TYPE_CHECKING, Optional, Type from warnings import warn from .._docstring import add_example from .._utils import rand_hex from ..session import require_active_session -from ..session._session import UpdateProgressMessage if TYPE_CHECKING: + from types import TracebackType + from .. import Session + from ..session._session import UpdateProgressMessage @add_example() @@ -95,7 +96,9 @@ def set( """ if self._closed: - warn("Attempting to set progress, but progress already closed.") + warn( + "Attempting to set progress, but progress already closed.", stacklevel=2 + ) return None self.value = value @@ -164,7 +167,10 @@ def close(self) -> None: Removes the progress panel. Future calls to set and close will be ignored. """ if self._closed: - warn("Attempting to close progress, but progress already closed.") + warn( + "Attempting to close progress, but progress already closed.", + stacklevel=2, + ) return None self._session._send_progress("close", {"id": self._id, "style": self._style}) diff --git a/shiny/ui/_sidebar.py b/shiny/ui/_sidebar.py index bff18727e..2baec5b6a 100644 --- a/shiny/ui/_sidebar.py +++ b/shiny/ui/_sidebar.py @@ -138,7 +138,7 @@ def _as_open( raise ValueError( f"""`open` must be one of {SidebarOpen._values_str()}, """ - + "or a dictionary with keys `desktop` and `mobile` using these values." + "or a dictionary with keys `desktop` and `mobile` using these values." ) @@ -321,9 +321,9 @@ def max_height_mobile(self) -> Optional[str]: if max_height_mobile is not None and self.open().mobile != "always": warnings.warn( "The `shiny.ui.sidebar(max_height_mobile=)` argument only applies to " - + "the sidebar when `open` is `'always'` on mobile, but " - + f"`open` is `'{self.open().mobile}'`. " - + "The `max_height_mobile` argument will be ignored.", + "the sidebar when `open` is `'always'` on mobile, but " + f"`open` is `'{self.open().mobile}'`. " + "The `max_height_mobile` argument will be ignored.", # `stacklevel=2`: Refers to the caller of `.max_height_mobile` property method stacklevel=2, ) diff --git a/shiny/ui/_tooltip.py b/shiny/ui/_tooltip.py index a8d197ee4..1a6615572 100644 --- a/shiny/ui/_tooltip.py +++ b/shiny/ui/_tooltip.py @@ -82,25 +82,30 @@ def tooltip( ```python icon_title = "About tooltips" + + def bs_info_icon(title: str): # Enhanced from https://rstudio.github.io/bsicons/ via `bsicons::bs_icon("info-circle", title = icon_title)` - return ui.HTML(f'') + return ui.HTML( + f'' + ) - ui.tooltip( - bs_info_icon(icon_title), - "Text shown in the tooltip." - ) + + ui.tooltip(bs_info_icon(icon_title), "Text shown in the tooltip.") ``` ```python icon_title = "About tooltips" + + def fa_info_circle(title: str): # Enhanced from https://rstudio.github.io/fontawesome/ via `fontawesome::fa("info-circle", a11y = "sem", title = icon_title)` - return ui.HTML(f'') - ui.tooltip( - fa_info_circle(icon_title), - "Text shown in the tooltip." - ) + return ui.HTML( + f'' + ) + + + ui.tooltip(fa_info_circle(icon_title), "Text shown in the tooltip.") ``` See Also diff --git a/shiny/ui/_utils.py b/shiny/ui/_utils.py index aa9d80a95..d526ad3ff 100644 --- a/shiny/ui/_utils.py +++ b/shiny/ui/_utils.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Dict, List, Optional, cast, overload +from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast, overload from htmltools import ( HTMLDependency, @@ -12,10 +12,12 @@ tags, ) -from .._typing_extensions import TypeGuard from ..session import Session, require_active_session from ..types import MISSING, MISSING_TYPE +if TYPE_CHECKING: + from .._typing_extensions import TypeGuard + def shiny_input_label(id: str, label: TagChild = None) -> Tag: cls = "control-label" + ("" if label else " shiny-label-null") diff --git a/shiny/ui/_valuebox.py b/shiny/ui/_valuebox.py index 913d6b422..1dc64bc31 100644 --- a/shiny/ui/_valuebox.py +++ b/shiny/ui/_valuebox.py @@ -290,7 +290,7 @@ def value_box_theme( """e.g. `"primary"`, `"danger"`, `"purple"`, etc.""" ) - if not (name.startswith("bg-") or name.startswith("text-")): + if not (name.startswith(("bg-", "text-"))): name = "bg-" + name return ValueBoxTheme(class_=name, bg=bg, fg=fg) diff --git a/shiny/ui/css/__init__.py b/shiny/ui/css/__init__.py index c708be9c7..6937cac55 100644 --- a/shiny/ui/css/__init__.py +++ b/shiny/ui/css/__init__.py @@ -1,4 +1,4 @@ -from ._css_unit import CssUnit, as_css_unit, as_css_padding +from ._css_unit import CssUnit, as_css_padding, as_css_unit __all__ = ( "CssUnit", diff --git a/tests/playwright/conftest.py b/tests/playwright/conftest.py index f51d25dc6..75d355867 100644 --- a/tests/playwright/conftest.py +++ b/tests/playwright/conftest.py @@ -10,9 +10,9 @@ from contextlib import contextmanager from pathlib import PurePath from time import sleep -from types import TracebackType from typing import ( IO, + TYPE_CHECKING, Any, Callable, Generator, @@ -39,7 +39,13 @@ "retry_with_timeout", ) -from playwright.sync_api import BrowserContext, Page + +if TYPE_CHECKING: + from types import TracebackType + + from playwright.sync_api import BrowserContext, Page + + from shiny._typing_extensions import Self # Make a single page fixture that can be used by all tests @@ -49,7 +55,7 @@ def session_page(browser: BrowserContext) -> Page: return browser.new_page() -@pytest.fixture(scope="function") +@pytest.fixture() # By going to `about:blank`, we _reset_ the page to a known state before each test. # It is not perfect, but it is faster than making a new page for each test. # This must be done before each test @@ -151,7 +157,7 @@ def close(self) -> None: sleep(0.5) self.proc.terminate() - def __enter__(self) -> ShinyAppProc: + def __enter__(self) -> Self: return self def __exit__( diff --git a/tests/playwright/controls.py b/tests/playwright/controls.py index 9a92db482..fde8d762d 100644 --- a/tests/playwright/controls.py +++ b/tests/playwright/controls.py @@ -3,7 +3,6 @@ from __future__ import annotations import json -import pathlib import re import sys import time @@ -18,12 +17,13 @@ # (Imports split over many import statements due to auto formatting) from shiny._typing_extensions import ( TypeGuard, # pyright: ignore[reportPrivateImportUsage] -) -from shiny._typing_extensions import ( assert_type, # pyright: ignore[reportPrivateImportUsage] ) from shiny.types import MISSING, MISSING_TYPE +if typing.TYPE_CHECKING: + import pathlib + """ Questions: * `_DateBase` is signaled as private, but `InputDateRange` will have two fields of `date_start` and `date_end`. Due to how the init selectors are created, they are not `InputDate` instances. Should we make `_DateBase` public? @@ -499,7 +499,6 @@ class InputPassword( # *, # width: Optional[str] = None, # placeholder: Optional[str] = None, - ... def __init__(self, page: Page, id: str) -> None: super().__init__( @@ -928,7 +927,9 @@ def set(self, value: bool, *, timeout: Timeout = None, **kwargs: object) -> None self.loc.wait_for(state="visible", timeout=timeout) self.loc.scroll_into_view_if_needed(timeout=timeout) self.loc.set_checked( - value, timeout=timeout, **kwargs # pyright: ignore[reportArgumentType] + value, + timeout=timeout, + **kwargs, # pyright: ignore[reportArgumentType] ) def toggle(self, *, timeout: Timeout = None, **kwargs: object) -> None: @@ -1685,7 +1686,7 @@ def slow_move(x: float, y: float, delay: float = sleep_time) -> None: values_found_txt = ", ".join([f'"{key}"' for key in key_arr]) raise ValueError( f"Could not find value '{value}' when moving slider from {error_msg_direction}\n" - + f"Values found:\n{values_found_txt}{trail_txt}" + f"Values found:\n{values_found_txt}{trail_txt}" ) def _grid_bb(self, *, timeout: Timeout = None) -> FloatRect: diff --git a/tests/playwright/deploys/express-page_sidebar/test_deploys_express_page_sidebar.py b/tests/playwright/deploys/express-page_sidebar/test_deploys_express_page_sidebar.py index 39a31c196..29a18b144 100644 --- a/tests/playwright/deploys/express-page_sidebar/test_deploys_express_page_sidebar.py +++ b/tests/playwright/deploys/express-page_sidebar/test_deploys_express_page_sidebar.py @@ -1,10 +1,6 @@ from controls import OutputTextVerbatim, Sidebar from playwright.sync_api import Page from utils.deploy_utils import create_deploys_app_url_fixture, skip_if_not_chrome -from utils.express_utils import compare_annotations - -from shiny import ui -from shiny.express import ui as xui app_url = create_deploys_app_url_fixture("express_page_sidebar") @@ -17,4 +13,3 @@ def test_express_page_sidebar(page: Page, app_url: str) -> None: sidebar.expect_text("SidebarTitle Sidebar Content") output_txt = OutputTextVerbatim(page, "txt") output_txt.expect_value("50") - compare_annotations(ui.sidebar, xui.sidebar) diff --git a/tests/playwright/deploys/plotly/app.py b/tests/playwright/deploys/plotly/app.py index bf90aa3f7..d36e1429f 100644 --- a/tests/playwright/deploys/plotly/app.py +++ b/tests/playwright/deploys/plotly/app.py @@ -1,5 +1,5 @@ # App altered from: https://github.com/rstudio/py-shiny/blob/main/shiny/api-examples/data_frame/app.py -import pandas # noqa: F401 (this line needed for Shinylive to load plotly.express) +import pandas as pd # noqa: F401 (this line needed for Shinylive to load plotly.express) import plotly.express as px import plotly.graph_objs as go from shinywidgets import output_widget, render_widget diff --git a/tests/playwright/examples/example_apps.py b/tests/playwright/examples/example_apps.py index 296c64f31..8b3ebc0a4 100644 --- a/tests/playwright/examples/example_apps.py +++ b/tests/playwright/examples/example_apps.py @@ -174,7 +174,7 @@ def on_console_msg(msg: ConsoleMessage) -> None: app_name = os.path.basename(os.path.dirname(ex_app_path)) short_app_path = f"{os.path.basename(os.path.dirname(os.path.dirname(ex_app_path)))}/{app_name}" - if short_app_path in app_hard_wait.keys(): + if short_app_path in app_hard_wait: # Apps are constantly invalidating and will not stabilize # Instead, wait for specific amount of time page.wait_for_timeout(app_hard_wait[short_app_path]) @@ -193,7 +193,7 @@ def on_console_msg(msg: ConsoleMessage) -> None: error_lines = [ line for line in error_lines - if not any([error_txt in line for error_txt in app_allow_external_errors]) + if not any(error_txt in line for error_txt in app_allow_external_errors) ] # Remove any app specific errors that are allowed @@ -208,11 +208,11 @@ def on_console_msg(msg: ConsoleMessage) -> None: app_allowable_errors = [app_allowable_errors] app_allowable_errors = ( # Remove ^INFO lines - ["INFO:"] + *["INFO:"], # Remove any known errors caused by external packages - + app_allow_external_errors + *app_allow_external_errors, # Remove any known errors allowed by the app - + app_allowable_errors + *app_allowable_errors, ) # If there is an array of allowable errors, remove them from errors. Ex: `PlotnineWarning` @@ -220,7 +220,7 @@ def on_console_msg(msg: ConsoleMessage) -> None: line for line in error_lines if len(line.strip()) > 0 - and not any([error_txt in line for error_txt in app_allowable_errors]) + and not any(error_txt in line for error_txt in app_allowable_errors) ] if len(error_lines) > 0: print("\nshort_app_path: " + short_app_path) @@ -237,10 +237,8 @@ def on_console_msg(msg: ConsoleMessage) -> None: line for line in console_errors if not any( - [ - error_txt in line - for error_txt in app_allow_js_errors[short_app_path] - ] + error_txt in line + for error_txt in app_allow_js_errors[short_app_path] ) ] assert len(console_errors) == 0, ( diff --git a/tests/playwright/shiny/TODO/navbar/app.py b/tests/playwright/shiny/TODO/navbar/app.py index d1a750dbc..eca2ba3f5 100644 --- a/tests/playwright/shiny/TODO/navbar/app.py +++ b/tests/playwright/shiny/TODO/navbar/app.py @@ -1,7 +1,11 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from shiny import App, ui -from shiny.types import NavSetArg + +if TYPE_CHECKING: + from shiny.types import NavSetArg my_sidebar = ui.sidebar("Sidebar content", open="open", title="Sidebar title") diff --git a/tests/playwright/shiny/bugs/0648-update-slider-datetime-value/test_update_slider_datetime_value.py b/tests/playwright/shiny/bugs/0648-update-slider-datetime-value/test_update_slider_datetime_value.py index b569d0de7..d8e398386 100644 --- a/tests/playwright/shiny/bugs/0648-update-slider-datetime-value/test_update_slider_datetime_value.py +++ b/tests/playwright/shiny/bugs/0648-update-slider-datetime-value/test_update_slider_datetime_value.py @@ -1,11 +1,13 @@ from __future__ import annotations -from typing import Optional +from typing import TYPE_CHECKING, Optional -from conftest import ShinyAppProc from controls import InputActionButton, InputSlider, OutputTextVerbatim from playwright.sync_api import Page, expect +if TYPE_CHECKING: + from conftest import ShinyAppProc + def test_slider_app(page: Page, local_app: ShinyAppProc) -> None: def check_case( diff --git a/tests/playwright/shiny/bugs/0666-sidebar/test_sidebar_colors.py b/tests/playwright/shiny/bugs/0666-sidebar/test_sidebar_colors.py index e376d16ed..0a72b89d0 100644 --- a/tests/playwright/shiny/bugs/0666-sidebar/test_sidebar_colors.py +++ b/tests/playwright/shiny/bugs/0666-sidebar/test_sidebar_colors.py @@ -1,10 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from colors import bg_color, fg_color -from conftest import ShinyAppProc from controls import Sidebar, _expect_class_value from playwright.sync_api import Page, expect +if TYPE_CHECKING: + from conftest import ShinyAppProc + def test_colors_are_rgb() -> None: assert bg_color.startswith("rgb(") diff --git a/tests/playwright/shiny/bugs/0676-row-selection/app.py b/tests/playwright/shiny/bugs/0676-row-selection/app.py index 1c8fee0fd..cea93bd41 100644 --- a/tests/playwright/shiny/bugs/0676-row-selection/app.py +++ b/tests/playwright/shiny/bugs/0676-row-selection/app.py @@ -7,11 +7,11 @@ from shiny import App, Inputs, Outputs, Session, render, ui df = pd.DataFrame( - dict( - id=["one", "two", "three"], - fname=["Alice", "Bob", "Charlie"], - lname=["Smith", "Jones", "Brown"], - ) + { + "id": ["one", "two", "three"], + "fname": ["Alice", "Bob", "Charlie"], + "lname": ["Smith", "Jones", "Brown"], + } ).set_index( # type: ignore "id", drop=False, diff --git a/tests/playwright/shiny/bugs/0676-row-selection/test_0676_row_selection.py b/tests/playwright/shiny/bugs/0676-row-selection/test_0676_row_selection.py index 9f83665ec..8362f3937 100644 --- a/tests/playwright/shiny/bugs/0676-row-selection/test_0676_row_selection.py +++ b/tests/playwright/shiny/bugs/0676-row-selection/test_0676_row_selection.py @@ -1,8 +1,12 @@ from __future__ import annotations -from conftest import ShinyAppProc +from typing import TYPE_CHECKING + from playwright.sync_api import Page, expect +if TYPE_CHECKING: + from conftest import ShinyAppProc + def test_row_selection(page: Page, local_app: ShinyAppProc) -> None: page.goto(local_app.url) diff --git a/tests/playwright/shiny/bugs/0696-resolve-id/app.py b/tests/playwright/shiny/bugs/0696-resolve-id/app.py index 1ed568c4e..8dd4c7b3e 100644 --- a/tests/playwright/shiny/bugs/0696-resolve-id/app.py +++ b/tests/playwright/shiny/bugs/0696-resolve-id/app.py @@ -17,7 +17,9 @@ from shiny import App, Inputs, Outputs, Session, module, reactive, render, ui from shiny.session import session_context -from shiny.types import ImgData + +if typing.TYPE_CHECKING: + from shiny.types import ImgData pandas_df = pd.DataFrame( { @@ -31,7 +33,7 @@ img_path = pathlib.Path(__file__).parent / "imgs" penguin_imgs = [str(img_path / img) for img in os.listdir(img_path)] assert len(penguin_imgs) > 0 -letters = [letter for letter in "abcdefghijklmnopqrstuvwxyz"][: len(penguin_imgs)] +letters = list("abcdefghijklmnopqrstuvwxyz")[: len(penguin_imgs)] input_keys = ( "input_action_button", @@ -229,7 +231,8 @@ def out_image() -> ImgData: def out_plot(): dt = [1, 2, 3, 4, 5] plt.bar( # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType] - dt[: ((n() + 1) % len(dt))], dt[: ((n() + 1) % len(dt))] # pyright: ignore + dt[: ((n() + 1) % len(dt))], + dt[: ((n() + 1) % len(dt))], # pyright: ignore ) @render.table diff --git a/tests/playwright/shiny/bugs/0696-resolve-id/check.py b/tests/playwright/shiny/bugs/0696-resolve-id/check.py index 60801f94e..0d2e7919e 100644 --- a/tests/playwright/shiny/bugs/0696-resolve-id/check.py +++ b/tests/playwright/shiny/bugs/0696-resolve-id/check.py @@ -48,7 +48,7 @@ class ModState(NamedTuple): navset_tab: str -blacklist = set(["awesome_component"]) +blacklist = {"awesome_component"} x_input_keys = ("x_" + key for key in x_input_keys) diff --git a/tests/playwright/shiny/bugs/0696-resolve-id/mod_state.py b/tests/playwright/shiny/bugs/0696-resolve-id/mod_state.py index 4a0ad4210..94fe831be 100644 --- a/tests/playwright/shiny/bugs/0696-resolve-id/mod_state.py +++ b/tests/playwright/shiny/bugs/0696-resolve-id/mod_state.py @@ -1,9 +1,12 @@ from __future__ import annotations import datetime +from typing import TYPE_CHECKING from controls import OutputTextVerbatim -from playwright.sync_api import Page + +if TYPE_CHECKING: + from playwright.sync_api import Page def expect_state( diff --git a/tests/playwright/shiny/bugs/0696-resolve-id/test_0696_resolve_id.py b/tests/playwright/shiny/bugs/0696-resolve-id/test_0696_resolve_id.py index be00085ae..f5cc68c66 100644 --- a/tests/playwright/shiny/bugs/0696-resolve-id/test_0696_resolve_id.py +++ b/tests/playwright/shiny/bugs/0696-resolve-id/test_0696_resolve_id.py @@ -4,9 +4,9 @@ import datetime import os from pathlib import Path +from typing import TYPE_CHECKING import pytest -from conftest import ShinyAppProc from controls import ( DownloadButton, DownloadLink, @@ -32,12 +32,15 @@ OutputTextVerbatim, OutputUi, ) -from examples.example_apps import reruns, reruns_delay from mod_state import expect_default_mod_state, expect_mod_state -from playwright.sync_api import Page +from examples.example_apps import reruns, reruns_delay from shiny._utils import guess_mime_type +if TYPE_CHECKING: + from conftest import ShinyAppProc + from playwright.sync_api import Page + img_path = Path(__file__).parent / "imgs" penguin_imgs = [str(img_path / img) for img in os.listdir(img_path)] diff --git a/tests/playwright/shiny/components/accordion/test_accordion.py b/tests/playwright/shiny/components/accordion/test_accordion.py index f47786728..556c15fd7 100644 --- a/tests/playwright/shiny/components/accordion/test_accordion.py +++ b/tests/playwright/shiny/components/accordion/test_accordion.py @@ -1,9 +1,10 @@ import pytest from conftest import ShinyAppProc from controls import Accordion, InputActionButton, OutputTextVerbatim -from examples.example_apps import reruns, reruns_delay from playwright.sync_api import Page +from examples.example_apps import reruns, reruns_delay + @pytest.mark.flaky(reruns=reruns, reruns_delay=reruns_delay) def test_accordion(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/components/data_frame/test_data_frame.py b/tests/playwright/shiny/components/data_frame/test_data_frame.py index bdb1b5863..6f710e73a 100644 --- a/tests/playwright/shiny/components/data_frame/test_data_frame.py +++ b/tests/playwright/shiny/components/data_frame/test_data_frame.py @@ -7,28 +7,29 @@ import pytest from conftest import ShinyAppProc, create_example_fixture, expect_to_change from controls import InputSelect, InputSwitch -from examples.example_apps import reruns, reruns_delay from playwright.sync_api import Locator, Page, expect +from examples.example_apps import reruns, reruns_delay + data_frame_app = create_example_fixture("dataframe") -@pytest.fixture +@pytest.fixture() def grid(page: Page) -> Locator: return page.locator("#grid") -@pytest.fixture +@pytest.fixture() def grid_container(page: Page, grid: Locator) -> Locator: return grid.locator("> div > div.shiny-data-grid") -@pytest.fixture +@pytest.fixture() def summary(page: Page, grid: Locator) -> Locator: return grid.locator("div.shiny-data-grid-summary") -@pytest.fixture +@pytest.fixture() def scroll_to_end(page: Page, grid_container: Locator) -> Callable[[], None]: def do(): grid_container.locator("tbody tr:first-child td:first-child").click() diff --git a/tests/playwright/shiny/components/layout_columns/test_layout_columns.py b/tests/playwright/shiny/components/layout_columns/test_layout_columns.py index 38f421c22..c3b0dd429 100644 --- a/tests/playwright/shiny/components/layout_columns/test_layout_columns.py +++ b/tests/playwright/shiny/components/layout_columns/test_layout_columns.py @@ -1,9 +1,11 @@ from __future__ import annotations -from typing import TypeVar +from typing import TYPE_CHECKING, TypeVar from conftest import ShinyAppProc, create_doc_example_core_fixture -from playwright.sync_api import Page + +if TYPE_CHECKING: + from playwright.sync_api import Page T = TypeVar("T") diff --git a/tests/playwright/shiny/components/nav/app.py b/tests/playwright/shiny/components/nav/app.py index ffe36f163..6b49b1dda 100644 --- a/tests/playwright/shiny/components/nav/app.py +++ b/tests/playwright/shiny/components/nav/app.py @@ -1,12 +1,14 @@ from __future__ import annotations -from typing import Any, Callable, List - -from htmltools import Tag +from typing import TYPE_CHECKING, Any, Callable, List from shiny import App, ui -from shiny.types import NavSetArg -from shiny.ui import Sidebar + +if TYPE_CHECKING: + from htmltools import Tag + + from shiny.types import NavSetArg + from shiny.ui import Sidebar # TODO-karan; Make test that uses sidebar / no sidebar (where possible) # TODO-karan; Make test that has/does not have a header & footer (where possible) diff --git a/tests/playwright/shiny/components/nav/test_nav.py b/tests/playwright/shiny/components/nav/test_nav.py index 546f81090..d6a80859d 100644 --- a/tests/playwright/shiny/components/nav/test_nav.py +++ b/tests/playwright/shiny/components/nav/test_nav.py @@ -1,9 +1,9 @@ from __future__ import annotations from dataclasses import dataclass +from typing import TYPE_CHECKING import pytest -from conftest import ShinyAppProc from controls import ( LayoutNavSetBar, LayoutNavSetCardPill, @@ -14,7 +14,10 @@ LayoutNavsetTab, LayoutNavSetUnderline, ) -from playwright.sync_api import Page + +if TYPE_CHECKING: + from conftest import ShinyAppProc + from playwright.sync_api import Page @pytest.mark.skip_browser("webkit") diff --git a/tests/playwright/shiny/implicit-register/app.py b/tests/playwright/shiny/implicit-register/app.py index eb88220a1..7bb6d7ecd 100644 --- a/tests/playwright/shiny/implicit-register/app.py +++ b/tests/playwright/shiny/implicit-register/app.py @@ -1,11 +1,11 @@ from shiny import App, Inputs, Outputs, Session, render, ui -scenarios = dict( - out1="The following output should be empty", - out2='The following output should have the word "One"', - out3='The following output should have the word "Two"', - out4='The following output should also have the word "Two"', -) +scenarios = { + "out1": "The following output should be empty", + "out2": 'The following output should have the word "One"', + "out3": 'The following output should have the word "Two"', + "out4": 'The following output should also have the word "Two"', +} app_ui = ui.page_fluid( [ diff --git a/tests/playwright/shiny/inputs/input_file/app.py b/tests/playwright/shiny/inputs/input_file/app.py index 4541df7c0..d825a0b75 100644 --- a/tests/playwright/shiny/inputs/input_file/app.py +++ b/tests/playwright/shiny/inputs/input_file/app.py @@ -1,9 +1,13 @@ +from __future__ import annotations + import typing import pandas as pd from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui -from shiny.types import FileInfo + +if typing.TYPE_CHECKING: + from shiny.types import FileInfo app_ui = ui.page_fluid( ui.input_file("file1", "Choose CSV File", accept=[".csv"], multiple=False), @@ -39,9 +43,7 @@ def summary(): # Get the row count, column count, and column names of the DataFrame row_count = df.shape[0] column_count = df.shape[1] - names: list[str] = ( - df.columns.tolist() - ) # pyright: ignore[reportUnknownMemberType] + names: list[str] = df.columns.tolist() # pyright: ignore[reportUnknownMemberType] column_names = ", ".join(str(name) for name in names) # Create a new DataFrame to display the information diff --git a/tests/playwright/shiny/inputs/input_file/test_input_file.py b/tests/playwright/shiny/inputs/input_file/test_input_file.py index 953c1ea35..be11c3120 100644 --- a/tests/playwright/shiny/inputs/input_file/test_input_file.py +++ b/tests/playwright/shiny/inputs/input_file/test_input_file.py @@ -1,9 +1,13 @@ from __future__ import annotations -from conftest import ShinyAppProc +from typing import TYPE_CHECKING + from controls import InputFile, OutputTable, OutputTextVerbatim from playwright.sync_api import FilePayload, Page, expect +if TYPE_CHECKING: + from conftest import ShinyAppProc + def test_input_file_kitchen(page: Page, local_app: ShinyAppProc) -> None: page.goto(local_app.url) diff --git a/tests/playwright/shiny/inputs/input_radio_checkbox_group/test_input_radio_checkbox_group_app.py b/tests/playwright/shiny/inputs/input_radio_checkbox_group/test_input_radio_checkbox_group_app.py index 24f5b265b..cafadac14 100644 --- a/tests/playwright/shiny/inputs/input_radio_checkbox_group/test_input_radio_checkbox_group_app.py +++ b/tests/playwright/shiny/inputs/input_radio_checkbox_group/test_input_radio_checkbox_group_app.py @@ -1,9 +1,14 @@ from __future__ import annotations -from conftest import ShinyAppProc +from typing import TYPE_CHECKING + +import pytest from controls import InputCheckboxGroup, InputRadioButtons, PatternOrStr from playwright.sync_api import Page, expect +if TYPE_CHECKING: + from conftest import ShinyAppProc + def test_input_checkbox_group_kitchen(page: Page, local_app: ShinyAppProc) -> None: page.goto(local_app.url) @@ -87,35 +92,32 @@ def test_locator_debugging(page: Page, local_app: ShinyAppProc) -> None: timeout = 100 # Non-existent div - try: - not_exist = InputRadioButtons(page, "does-not-exist") + not_exist = InputRadioButtons(page, "does-not-exist") + with pytest.raises(AssertionError) as e: not_exist.expect_choices(["a", "b", "c"], timeout=timeout) - except AssertionError as e: - assert "expected to have count '1'" in str(e) - assert "Actual value: 0" in str(e) + + assert "expected to have count '1'" in str(e.value) + assert "Actual value: 0" in str(e.value) check1 = InputCheckboxGroup(page, "check1") # Make sure it works check1.expect_choices(["red", "green", "blue"]) # Too many - try: + with pytest.raises(AssertionError) as e: check1.expect_choices(["red", "green", "blue", "test_value"], timeout=timeout) - except AssertionError as e: - assert "expected to have count '4'" in str(e) - assert "Actual value: 3" in str(e) + assert "expected to have count '4'" in str(e.value) + assert "Actual value: 3" in str(e.value) # Not enough - try: + with pytest.raises(AssertionError) as e: check1.expect_choices(["red", "green"], timeout=timeout) - except AssertionError as e: - assert "expected to have count '2'" in str(e) - assert "Actual value: 3" in str(e) + assert "expected to have count '2'" in str(e.value) + assert "Actual value: 3" in str(e.value) # Wrong value - try: + with pytest.raises(AssertionError) as e: check1.expect_choices(["red", "green", "test_value"], timeout=timeout) - except AssertionError as e: - assert "attribute 'test_value'" in str(e) - assert "Actual value: blue" in str(e) + assert "attribute 'test_value'" in str(e.value) + assert "Actual value: blue" in str(e.value) def test_locator_existance(page: Page, local_app: ShinyAppProc) -> None: @@ -124,12 +126,13 @@ def test_locator_existance(page: Page, local_app: ShinyAppProc) -> None: timeout = 100 # Non-existent div - try: - not_exist = InputCheckboxGroup(page, "does-not-exist") + not_exist = InputCheckboxGroup(page, "does-not-exist") + with pytest.raises(AssertionError) as e: not_exist.set(["green"], timeout=timeout) - except AssertionError as e: - assert "expected to have count '1'" in str(e) - assert "Actual value: 0" in str(e) + + print(str(e.value)) + assert "expected to have count '1'" in str(e.value) + assert "Actual value: 0" in str(e.value) check1 = InputCheckboxGroup(page, "check1") @@ -140,17 +143,16 @@ def test_locator_existance(page: Page, local_app: ShinyAppProc) -> None: check1.expect_selected(["green"]) # Different value - try: + with pytest.raises(AssertionError) as e: check1.set(["test_value"], timeout=timeout) - except AssertionError as e: - assert "expected to have count '1'" in str(e) - assert "Actual value: 0" in str(e) + assert "expected to have count '1'" in str(e.value) + assert "Actual value: 0" in str(e.value) # Extra value - try: + with pytest.raises(AssertionError) as e: check1.set(["blue", "test_value"], timeout=timeout) - except AssertionError as e: - assert "expected to have count '1'" in str(e) - assert "Actual value: 0" in str(e) + + assert "expected to have count '1'" in str(e.value) + assert "Actual value: 0" in str(e.value) check1.expect_selected(["green"]) diff --git a/tests/playwright/shiny/inputs/input_slider/test_input_slider_app.py b/tests/playwright/shiny/inputs/input_slider/test_input_slider_app.py index bb109e7f1..58968f982 100644 --- a/tests/playwright/shiny/inputs/input_slider/test_input_slider_app.py +++ b/tests/playwright/shiny/inputs/input_slider/test_input_slider_app.py @@ -1,6 +1,6 @@ -import re import time +import pytest from conftest import ShinyAppProc from controls import InputSlider, InputSliderRange, OutputTextVerbatim from playwright.sync_api import Page @@ -54,10 +54,8 @@ def test_slider_range(page: Page, local_app: ShinyAppProc) -> None: new_val = ("605", "840") s1.set(new_val, max_err_values=1000) - try: + with pytest.raises(ValueError, match="tuple entries cannot"): s1.expect_value((MISSING, MISSING)) # type: ignore - except ValueError as e: - assert re.search("tuple entries cannot", str(e)) s1.expect_value((new_val[0], MISSING)) s1.expect_value((MISSING, new_val[1])) s1.expect_value(new_val) diff --git a/tests/playwright/shiny/inputs/input_task_button/test_input_task_button.py b/tests/playwright/shiny/inputs/input_task_button/test_input_task_button.py index f90ce3d5b..767f1c5a2 100644 --- a/tests/playwright/shiny/inputs/input_task_button/test_input_task_button.py +++ b/tests/playwright/shiny/inputs/input_task_button/test_input_task_button.py @@ -1,10 +1,13 @@ from __future__ import annotations import time +from typing import TYPE_CHECKING -from conftest import ShinyAppProc from controls import InputNumeric, InputTaskButton, OutputText -from playwright.sync_api import Page + +if TYPE_CHECKING: + from conftest import ShinyAppProc + from playwright.sync_api import Page def click_extended_task_button( diff --git a/tests/playwright/shiny/inputs/input_task_button2/test_input_task_button2.py b/tests/playwright/shiny/inputs/input_task_button2/test_input_task_button2.py index 85da00177..77a6cc825 100644 --- a/tests/playwright/shiny/inputs/input_task_button2/test_input_task_button2.py +++ b/tests/playwright/shiny/inputs/input_task_button2/test_input_task_button2.py @@ -1,8 +1,12 @@ from __future__ import annotations -from conftest import ShinyAppProc +from typing import TYPE_CHECKING + from controls import InputTaskButton, OutputText -from playwright.sync_api import Page + +if TYPE_CHECKING: + from conftest import ShinyAppProc + from playwright.sync_api import Page def click_extended_task_button( diff --git a/tests/playwright/shiny/inputs/test_input_dark_mode.py b/tests/playwright/shiny/inputs/test_input_dark_mode.py index d5c793566..607e59c8e 100644 --- a/tests/playwright/shiny/inputs/test_input_dark_mode.py +++ b/tests/playwright/shiny/inputs/test_input_dark_mode.py @@ -1,8 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from conftest import ShinyAppProc, create_doc_example_core_fixture from controls import InputActionButton, InputDarkMode, LayoutNavSetBar -from playwright.sync_api import Page + +if TYPE_CHECKING: + from playwright.sync_api import Page app = create_doc_example_core_fixture("input_dark_mode") diff --git a/tests/playwright/shiny/inputs/test_input_slider.py b/tests/playwright/shiny/inputs/test_input_slider.py index 5da4cfaac..3bd655de6 100644 --- a/tests/playwright/shiny/inputs/test_input_slider.py +++ b/tests/playwright/shiny/inputs/test_input_slider.py @@ -1,3 +1,4 @@ +import pytest from conftest import ShinyAppProc, create_doc_example_core_fixture from controls import InputSlider, OutputTextVerbatim from playwright.sync_api import Page, expect @@ -49,13 +50,12 @@ def test_input_slider_kitchen(page: Page, slider_app: ShinyAppProc) -> None: # e # ), "Error message should contain the list of first 15 valid values" - try: + with pytest.raises(ValueError) as e: obs.set("not-a-number", timeout=800, max_err_values=4) - except ValueError as e: - values_found = '"10", "11", "12", "13", ...' - assert values_found in str( - e - ), "Error message should contain the list of first 4 valid values" + values_found = '"10", "11", "12", "13", ...' + assert values_found in str( + e + ), "Error message should contain the list of first 4 valid values" def test_input_slider_output(page: Page, template_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/inputs/test_input_switch.py b/tests/playwright/shiny/inputs/test_input_switch.py index 39034bf2f..0fc16a042 100644 --- a/tests/playwright/shiny/inputs/test_input_switch.py +++ b/tests/playwright/shiny/inputs/test_input_switch.py @@ -9,7 +9,6 @@ def test_input_switch_kitchen(page: Page, app: ShinyAppProc) -> None: page.goto(app.url) somevalue = InputSwitch(page, "somevalue") - somevalue.expect_label expect(somevalue.loc_label).to_have_text("Some value") somevalue.expect_label("Some value") diff --git a/tests/playwright/shiny/plot-sizing/app.py b/tests/playwright/shiny/plot-sizing/app.py index 646fe7fe4..83a0c0c93 100644 --- a/tests/playwright/shiny/plot-sizing/app.py +++ b/tests/playwright/shiny/plot-sizing/app.py @@ -111,7 +111,7 @@ def plot_with_mpl(fig_size: tuple[float, float] | None) -> object: return fig def plot_with_sns(fig_size: tuple[float, float] | None) -> object: - kwargs = dict() + kwargs = {} if fig_size: kwargs["height"] = fig_size[1] / dpi kwargs["aspect"] = fig_size[0] / fig_size[1] diff --git a/tests/playwright/shiny/session/flush/app.py b/tests/playwright/shiny/session/flush/app.py index a6591e31d..d4aef1026 100644 --- a/tests/playwright/shiny/session/flush/app.py +++ b/tests/playwright/shiny/session/flush/app.py @@ -91,8 +91,8 @@ def call_a( ): def _(): with reactive.isolate(): - all_vals.set(all_vals.get() + (f"a-{suffix}",)) - vals.set(vals.get() + (f"a-{suffix}",)) + all_vals.set((*all_vals.get(), f"a-{suffix}")) + vals.set((*vals.get(), f"a-{suffix}")) return _ @@ -102,12 +102,12 @@ def call_b( ): async def _(): with reactive.isolate(): - all_vals.set(all_vals.get() + (f"bx-{suffix}",)) - vals.set(vals.get() + (f"bx-{suffix}",)) + all_vals.set((*all_vals.get(), f"bx-{suffix}")) + vals.set((*vals.get(), f"bx-{suffix}")) await asyncio.sleep(0) with reactive.isolate(): - all_vals.set(all_vals.get() + (f"by-{suffix}",)) - vals.set(vals.get() + (f"by-{suffix}",)) + all_vals.set((*all_vals.get(), f"by-{suffix}")) + vals.set((*vals.get(), f"by-{suffix}")) return _ @@ -117,8 +117,8 @@ def call_c( ): def _(): with reactive.isolate(): - all_vals.set(all_vals.get() + (f"c-{suffix}",)) - vals.set(vals.get() + (f"c-{suffix}",)) + all_vals.set((*all_vals.get(), f"c-{suffix}")) + vals.set((*vals.get(), f"c-{suffix}")) return _ diff --git a/tests/playwright/utils/deploy_utils.py b/tests/playwright/utils/deploy_utils.py index 71378c9b0..e351ff18c 100644 --- a/tests/playwright/utils/deploy_utils.py +++ b/tests/playwright/utils/deploy_utils.py @@ -43,7 +43,7 @@ def skip_if_not_chrome(fn: CallableT) -> CallableT: def exception_swallower( - function: Callable[[str, str], str] + function: Callable[[str, str], str], ) -> Callable[[str, str], str]: def wrapper(app_name: str, app_dir: str) -> str: runtime_e: Exception | None = None diff --git a/tests/pytest/test_display_decorator.py b/tests/pytest/test_display_decorator.py index 63e014b28..469848002 100644 --- a/tests/pytest/test_display_decorator.py +++ b/tests/pytest/test_display_decorator.py @@ -1,5 +1,5 @@ # pyright: reportUnusedExpression=false -# flake8: noqa +# ruff: noqa: B018 from __future__ import annotations import contextlib diff --git a/tests/playwright/utils/express_utils.py b/tests/pytest/test_express_annotations.py similarity index 86% rename from tests/playwright/utils/express_utils.py rename to tests/pytest/test_express_annotations.py index 0a0399b88..704c5d410 100644 --- a/tests/playwright/utils/express_utils.py +++ b/tests/pytest/test_express_annotations.py @@ -29,3 +29,10 @@ def compare_annotations( assert ( ui_a[key] == layout_a[key] ), f"Type annotations for {key} in {core_fn} (Core) don't match {express_fn} (Express)" + + +def test_sidebar_annotations() -> None: + from shiny import ui + from shiny.express import ui as xui + + compare_annotations(ui.sidebar, xui.sidebar) diff --git a/tests/pytest/test_markdown.py b/tests/pytest/test_markdown.py index a5d912c25..2873672e3 100644 --- a/tests/pytest/test_markdown.py +++ b/tests/pytest/test_markdown.py @@ -14,9 +14,8 @@ def test_markdown(): '
a paragraph with a link: https://example.com
This is markdown and here is some code
:
print('Hello world!')\n
\n"
- )
+ ) == HTML(
+ "This is markdown and here is some code
:
print('Hello world!')\n
\n"
)
- assert (
- markdown(
- """
+ assert markdown(
+ """
# Hello World
This is **markdown** and here is some `code`:
print('Hello world!')
"""
- )
- == HTML(
- "This is markdown and here is some code
:
print('Hello world!')\n
\n"
- )
+ ) == HTML(
+ "This is markdown and here is some code
:
print('Hello world!')\n
\n"
)
diff --git a/tests/pytest/test_modules.py b/tests/pytest/test_modules.py
index 1b91fc7d7..a1522e35f 100644
--- a/tests/pytest/test_modules.py
+++ b/tests/pytest/test_modules.py
@@ -39,7 +39,7 @@ def test_module_ui():
assert get_id(y, 2) == "outer-out2"
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_session_scoping():
sessions: Dict[str, Union[Session, None, str]] = {}
diff --git a/tests/pytest/test_navs.py b/tests/pytest/test_navs.py
index b67bf15c1..b572db8e4 100644
--- a/tests/pytest/test_navs.py
+++ b/tests/pytest/test_navs.py
@@ -1,4 +1,4 @@
-"""Tests for """
+"""Tests for"""
import contextlib
import random
diff --git a/tests/pytest/test_output_transformer.py b/tests/pytest/test_output_transformer.py
index 0ae494b0c..a50dbe002 100644
--- a/tests/pytest/test_output_transformer.py
+++ b/tests/pytest/test_output_transformer.py
@@ -116,20 +116,16 @@ def test_renderer(
def test_output_transformer_pos_args():
- try:
+ with pytest.raises(TypeError, match="must have 2 positional parameters"):
@output_transformer # pyright: ignore[reportArgumentType]
async def TestTransformer(
_meta: TransformerMetadata,
): ...
- raise RuntimeError()
- except TypeError as e:
- assert "must have 2 positional parameters" in str(e)
-
def test_output_transformer_limits_positional_arg_count():
- try:
+ with pytest.raises(TypeError, match="more than 2 positional"):
@output_transformer
async def TestTransformer(
@@ -138,13 +134,9 @@ async def TestTransformer(
y: str,
): ...
- raise RuntimeError()
- except TypeError as e:
- assert "more than 2 positional" in str(e)
-
def test_output_transformer_does_not_allow_args():
- try:
+ with pytest.raises(TypeError, match="No variadic positional parameters"):
@output_transformer
async def TestTransformer(
@@ -153,14 +145,9 @@ async def TestTransformer(
*args: str,
): ...
- raise RuntimeError()
-
- except TypeError as e:
- assert "No variadic positional parameters" in str(e)
-
def test_output_transformer_kwargs_have_defaults():
- try:
+ with pytest.raises(TypeError, match="did not have a default value"):
@output_transformer
async def TestTransformer(
@@ -170,11 +157,6 @@ async def TestTransformer(
y: str,
): ...
- raise RuntimeError()
-
- except TypeError as e:
- assert "did not have a default value" in str(e)
-
def test_output_transformer_result_does_not_allow_args():
@output_transformer
@@ -187,17 +169,16 @@ async def TestTransformer(
def render_fn_sync(*args: str):
return " ".join(args)
- try:
+ with pytest.raises(
+ TypeError, match="Expected `params` to be of type `TransformerParams`"
+ ):
TestTransformer(
render_fn_sync,
"X", # pyright: ignore[reportArgumentType]
)
- raise RuntimeError()
- except TypeError as e:
- assert "Expected `params` to be of type `TransformerParams`" in str(e)
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_renderer_handler_or_transform_fn_can_be_async():
@output_transformer
async def AsyncTransformer(
diff --git a/tests/pytest/test_poll.py b/tests/pytest/test_poll.py
index 46b5b298c..60a79fb4a 100644
--- a/tests/pytest/test_poll.py
+++ b/tests/pytest/test_poll.py
@@ -52,7 +52,7 @@ async def __aexit__(
self._on_ended_callbacks.invoke()
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_poll():
async with OnEndedSessionCallbacks():
poll_invocations = 0
@@ -126,7 +126,7 @@ def _():
assert (poll_invocations, value_invocations) == (6, 4)
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_poll_errors():
async with OnEndedSessionCallbacks():
@@ -194,7 +194,7 @@ def _():
assert invocations == 3
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_file_reader():
tmpfile = tempfile.NamedTemporaryFile(delete=False)
try:
@@ -240,7 +240,7 @@ def read_file():
os.unlink(tmpfile.name)
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_file_reader_error():
async with OnEndedSessionCallbacks():
tmpfile1 = tempfile.NamedTemporaryFile(delete=False)
diff --git a/tests/pytest/test_reactives.py b/tests/pytest/test_reactives.py
index 4edaa4841..d2dd3d3aa 100644
--- a/tests/pytest/test_reactives.py
+++ b/tests/pytest/test_reactives.py
@@ -1,5 +1,7 @@
"""Tests for `shiny.reactive`."""
+from __future__ import annotations
+
import asyncio
from typing import List
@@ -14,7 +16,7 @@
from .mocktime import MockTime
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_flush_runs_newly_invalidated():
"""
Make sure that a flush will also run any calcs that were invalidated during the
@@ -43,7 +45,7 @@ def o1():
assert o1._exec_count == 1
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_flush_runs_newly_invalidated_async():
"""
Make sure that a flush will also run any calcs that were invalidated during the
@@ -75,7 +77,7 @@ async def o1():
# ======================================================================
# Setting Value to same value doesn't invalidate downstream
# ======================================================================
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_reactive_value_same_no_invalidate():
v = Value(1)
@@ -94,7 +96,7 @@ def o():
# ======================================================================
# Intializing reactive.Value to MISSING, and unsetting
# ======================================================================
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_reactive_value_unset():
v = Value[int]()
@@ -134,7 +136,7 @@ def o():
# ======================================================================
# reactive.Value.is_set() invalidates dependents only when set state changes
# ======================================================================
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_reactive_value_is_set():
v = Value[int]()
v_is_set: bool = False
@@ -177,7 +179,7 @@ def o():
# ======================================================================
# Recursive calls to calcs
# ======================================================================
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_recursive_calc():
v = Value(5)
@@ -199,7 +201,7 @@ def o():
assert v() == 0
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_recursive_async_calc():
v = Value(5)
@@ -226,7 +228,7 @@ async def o():
# ======================================================================
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_async_sequential():
x: Value[int] = Value(1)
results: list[int] = []
@@ -274,7 +276,7 @@ async def _():
# ======================================================================
# isolate()
# ======================================================================
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_isolate_basic_without_context():
# isolate() works with calc and Value; allows executing without a reactive context.
v = Value(1)
@@ -294,7 +296,7 @@ def get_r():
assert get_r() == 11
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_isolate_prevents_dependency():
v = Value(1)
@@ -331,7 +333,7 @@ def o():
# ======================================================================
# async isolate
# ======================================================================
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_isolate_async_basic_value():
async def f():
return 123
@@ -340,7 +342,7 @@ async def f():
assert await f() == 123
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_isolate_async_basic_without_context():
# async isolate works with calc and Value; allows executing without a reactive
# context.
@@ -358,7 +360,7 @@ async def get_r():
assert await get_r() == 11
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_isolate_async_prevents_dependency():
v = Value(1)
@@ -395,7 +397,7 @@ async def o():
# ======================================================================
# Priority for effects
# ======================================================================
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_effect_priority():
v = Value(1)
results: list[int] = []
@@ -446,7 +448,7 @@ def o4():
# Same as previous, but with async
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_async_effect_priority():
v = Value(1)
results: list[int] = []
@@ -499,7 +501,7 @@ async def o4():
# ======================================================================
# Destroying effects
# ======================================================================
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_effect_destroy():
v = Value(1)
results: list[int] = []
@@ -536,7 +538,7 @@ def o2():
# ======================================================================
# Error handling
# ======================================================================
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_error_handling():
vals: List[str] = []
@@ -583,7 +585,7 @@ def _():
assert vals == ["o1-1", "r", "o2"]
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_calc_error_rethrow():
# Make sure calcs re-throw errors.
vals: List[str] = []
@@ -622,7 +624,7 @@ def _():
# Invalidating dependents
# ======================================================================
# For https://github.com/posit-dev/py-shiny/issues/26
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_dependent_invalidation():
trigger = Value(0)
v = Value(0)
@@ -663,7 +665,7 @@ def r():
# ------------------------------------------------------------
# req() pauses execution in @effect() and @calc()
# ------------------------------------------------------------
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_req():
n_times = 0
@@ -714,7 +716,7 @@ def _():
assert val == 1
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_invalidate_later():
mock_time = MockTime()
with mock_time():
@@ -745,7 +747,7 @@ def obs1():
assert obs1._exec_count == 12
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_invalidate_later_invalidation():
mock_time = MockTime()
with mock_time():
@@ -773,7 +775,7 @@ def obs1():
assert obs1._exec_count == 2
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_mock_time():
mock_time = MockTime()
@@ -798,7 +800,7 @@ async def add_result_later(delay: float, msg: str):
# ------------------------------------------------------------
# @reactive.event() works as expected
# ------------------------------------------------------------
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_event_decorator():
n_times = 0
@@ -909,7 +911,7 @@ def _():
# ------------------------------------------------------------
# @event() works as expected with async
# ------------------------------------------------------------
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_event_async_decorator():
n_times = 0
@@ -1027,7 +1029,7 @@ async def _():
# ------------------------------------------------------------
# @event() handles silent exceptions in event function
# ------------------------------------------------------------
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_event_silent_exception():
n_times = 0
x = Value[bool]()
@@ -1057,7 +1059,7 @@ def _():
# ------------------------------------------------------------
# @event() handles silent exceptions in event function, async
# ------------------------------------------------------------
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_event_silent_exception_async():
n_times = 0
x = Value[bool]()
@@ -1093,7 +1095,7 @@ async def _():
# ------------------------------------------------------------
# @event() throws runtime errors if passed wrong type
# ------------------------------------------------------------
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_event_type_check():
with pytest.raises(TypeError):
# Should complain about missing argument to @event().
@@ -1146,7 +1148,7 @@ async def _(): ...
# ------------------------------------------------------------
# @output() throws runtime errors if passed wrong type
# ------------------------------------------------------------
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_output_type_check():
conn = MockConnection()
session = App(ui.TagList(), None)._create_session(conn)
@@ -1194,7 +1196,7 @@ def _(): ...
# ------------------------------------------------------------
# @effect()'s .suspend()/.resume() works as expected
# ------------------------------------------------------------
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_effect_pausing():
a = Value(float(1))
@@ -1276,7 +1278,7 @@ def _():
# ------------------------------------------------------------
# @effect()'s .suspend()/.resume() works as expected (with async)
# ------------------------------------------------------------
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_effect_async_pausing():
a = Value(float(1))
@@ -1355,7 +1357,7 @@ def _():
assert obsB._exec_count == 3
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_observer_async_suspended_resumed_observers_run_at_most_once():
a = Value(1)
diff --git a/tests/pytest/test_renderer.py b/tests/pytest/test_renderer.py
index 652461c7b..ab6ff9e7f 100644
--- a/tests/pytest/test_renderer.py
+++ b/tests/pytest/test_renderer.py
@@ -8,7 +8,7 @@
from shiny.render.renderer import Renderer, ValueFn
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_renderer_works():
# No args works
class test_renderer(Renderer[str]):
@@ -30,7 +30,7 @@ def txt_no_paren() -> str:
assert val == "Hello World! Hello World!"
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_renderer_works_with_args():
# No args works
class test_renderer_with_args(Renderer[str]):
diff --git a/tests/pytest/test_shinysession.py b/tests/pytest/test_shinysession.py
index 18017ba44..de3580f81 100644
--- a/tests/pytest/test_shinysession.py
+++ b/tests/pytest/test_shinysession.py
@@ -49,7 +49,7 @@ def test_input_nonexistent():
assert "y" not in input
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_input_nonexistent_deps():
# Make sure that `"x" in input` causes a reactive dependency to be created.
input = Inputs({})
diff --git a/tests/pytest/test_sidebar.py b/tests/pytest/test_sidebar.py
index f2f11605d..66c4597a6 100644
--- a/tests/pytest/test_sidebar.py
+++ b/tests/pytest/test_sidebar.py
@@ -1,12 +1,14 @@
from __future__ import annotations
-from typing import Literal
+from typing import TYPE_CHECKING, Literal
import pytest
from htmltools import Tag, TagAttrValue
from shiny import ui
-from shiny.ui._sidebar import SidebarOpenSpec, SidebarOpenValue
+
+if TYPE_CHECKING:
+ from shiny.ui._sidebar import SidebarOpenSpec, SidebarOpenValue
_s = ui.sidebar("Sidebar!")
_m = "Body"
@@ -25,38 +27,27 @@ def test_panel_main_and_panel_sidebar():
ui.layout_sidebar(_s)
ui.layout_sidebar(_s, None)
- try:
+ with pytest.raises(ValueError) as e:
ui.layout_sidebar(_s, _s)
- raise AssertionError("Should have raised ValueError")
- except ValueError as e:
- assert "multiple `sidebar()` objects" in str(e)
+ assert "multiple `sidebar()` objects" in str(e.value)
- try:
+ with pytest.raises(ValueError) as e:
ui.layout_sidebar(None, _ps) # pyright: ignore[reportArgumentType]
- raise AssertionError("Should have raised ValueError")
- except ValueError as e:
- assert "not being supplied with a `sidebar()` object." in str(e)
+ assert "not being supplied with a `sidebar()` object" in str(e.value)
- try:
+ with pytest.raises(ValueError) as e:
ui.layout_sidebar(_s, _pm)
- raise AssertionError("Should have raised ValueError")
- except ValueError as e:
- assert "is not being used with `panel_sidebar()`" in str(e)
+ assert "is not being used with `panel_sidebar()`" in str(e.value)
- try:
+ with pytest.raises(ValueError, match="not being supplied as the second argument"):
ui.layout_sidebar(_ps, None, _pm)
- raise AssertionError("Should have raised ValueError")
- except ValueError as e:
- assert "not being supplied as the second argument" in str(e)
- try:
+ with pytest.raises(ValueError) as e:
ui.layout_sidebar(_ps, _pm, None, "42")
- raise AssertionError("Should have raised ValueError")
- except ValueError as e:
- assert "Unexpected extra legacy `*args`" in str(e)
+ assert "Unexpected extra legacy `*args`" in str(e.value)
@pytest.mark.parametrize(
- "open_value, expected",
+ ("open_value", "expected"),
[
("closed", {"desktop": "closed", "mobile": "closed"}),
("open", {"desktop": "open", "mobile": "open"}),
diff --git a/tests/pytest/test_utils.py b/tests/pytest/test_utils.py
index ac99d4ab6..31a0d8a58 100644
--- a/tests/pytest/test_utils.py
+++ b/tests/pytest/test_utils.py
@@ -19,7 +19,9 @@ def test_randomness():
pub2 = random.randint(0, 100000000)
with private_seed():
priv2 = random.randint(0, 100000000)
- assert pub != priv and priv != pub2 and pub2 != priv2
+ assert pub != priv
+ assert priv != pub2
+ assert pub2 != priv2
# By setting the same seed, we should get the same randomness
random.seed(0)
@@ -93,7 +95,7 @@ def mutate_registrations():
assert cb4.exec_count == 1 # Registered during previous invoke(), was called
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_async_callbacks():
class AsyncMockCallback:
def __init__(self):
diff --git a/tests/pytest/test_utils_async.py b/tests/pytest/test_utils_async.py
index dd03f98d5..f91f7e5b2 100644
--- a/tests/pytest/test_utils_async.py
+++ b/tests/pytest/test_utils_async.py
@@ -1,5 +1,7 @@
"""Tests for `shiny.utils` async-related functions."""
+from __future__ import annotations
+
import asyncio
import contextvars
from typing import Iterator, List
@@ -142,7 +144,7 @@ async def inner():
asyncio.run(create_task_wrapper2())
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_coro_hybrid():
state = 0
@@ -165,7 +167,7 @@ async def test_task() -> int:
assert await fut == 100
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_coro_hybrid_throw():
async def test_task_throw():
raise ValueError("boom")
@@ -175,7 +177,7 @@ async def test_task_throw():
await fut
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_coro_hybrid_throw_later():
state = 0
@@ -191,7 +193,7 @@ async def test_task_throw_later():
await fut
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_coro_hybrid_cancel():
state = 0
@@ -208,7 +210,7 @@ async def test_task_cancel():
assert state == 1
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_coro_hybrid_self_cancel():
state = 0
@@ -231,7 +233,7 @@ async def test_task_cancel():
assert state == 1
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_coro_hybrid_self_cancel2():
state = 0
@@ -250,7 +252,7 @@ async def test_task_cancel():
assert state == 1
-@pytest.mark.asyncio
+@pytest.mark.asyncio()
async def test_coro_hybrid_context():
test = contextvars.ContextVar("test", default=False)