Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
876e9ea
Add unit and pytest tests, coverage config, and Makefile targets
karangattu Jan 21, 2026
2ce8333
Merge branch 'main' into add-test-code-coverage
karangattu Jan 21, 2026
2f48f12
Add comprehensive unit tests for UI components
karangattu Jan 22, 2026
3bffa5e
Add comprehensive pytest tests for UI and core modules
karangattu Jan 22, 2026
6a83d20
Add comprehensive unit tests for shiny UI and utils
karangattu Jan 22, 2026
d05d797
Add comprehensive pytest tests for UI and bookmark modules
karangattu Jan 22, 2026
1b29800
Refactor tests to use TemporaryDirectory for files
karangattu Jan 22, 2026
d9c226e
Add comprehensive tests for module exports and types
karangattu Jan 22, 2026
71284cc
Add full coverage tests for shiny UI and render modules
karangattu Jan 22, 2026
f0f04c1
Add pytest tests for shiny UI and HTTP modules
karangattu Jan 22, 2026
9e25a40
Refactor multi-line string arguments for consistency
karangattu Jan 22, 2026
8c0a3e6
Reorder imports in test files for consistency
karangattu Jan 22, 2026
3bc190e
Add type annotations and improve test robustness
karangattu Jan 22, 2026
939806a
Add type annotations and improve test type safety
karangattu Jan 22, 2026
5d291f3
Replace run_until_complete with asyncio.run in tests
karangattu Jan 22, 2026
063b120
black formatting
karangattu Jan 22, 2026
75cc4e3
Add tests for bookmark, main create, and run modules
karangattu Jan 22, 2026
a3c1c34
Add comprehensive pytest test coverage for core modules
karangattu Jan 22, 2026
5f16691
Update tests for async functions and class naming
karangattu Jan 22, 2026
ca1f8a3
Reformat test code for readability and consistency
karangattu Jan 22, 2026
f0d49b8
Refactor tests for stricter typing and improved clarity
karangattu Jan 22, 2026
f4cb254
Improve coverage config and add session flush test
karangattu Jan 22, 2026
552cd85
Enable parallel test execution with pytest-xdist
karangattu Jan 22, 2026
921c289
Add and refactor pytest tests for input and session modules
karangattu Jan 22, 2026
597a99d
Refactor and expand UI-related test coverage
karangattu Jan 22, 2026
5597104
Add and update UI component tests for Shiny
karangattu Jan 22, 2026
e437362
Ignore test_theme.py in pytest and add test results
karangattu Jan 22, 2026
ace416d
Add coverage exclusion pragmas to type-only code
karangattu Jan 22, 2026
7f2f646
Add comprehensive tests for shiny.ui and shiny.render modules
karangattu Jan 22, 2026
702eabb
Simplify pytest addopts by removing ignored tests
karangattu Jan 22, 2026
2539bbd
Improve module import tests for UI components
karangattu Jan 22, 2026
0efd5c3
Improve CI setup and test workflow
karangattu Jan 22, 2026
746a07a
Merge branch 'main' into add-test-code-coverage
karangattu Jan 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 6 additions & 3 deletions .github/py-shiny/setup/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ runs:
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
cache: 'pip'

- name: Upgrade `pip`
shell: bash
run: |
python -m pip install --upgrade pip

# https://github.com/astral-sh/uv/blob/main/docs/guides/integration/github.md
- name: Install `uv`
shell: bash
run: |
pip install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"

# https://github.com/astral-sh/uv/blob/main/docs/guides/integration/github.md#using-uv-pip
- name: Allow uv to use the system Python by default
Expand Down
22 changes: 19 additions & 3 deletions .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:
jobs:
check:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 30-minute timeout seems quite generous given the PR description states execution time increased to 5 minutes. Consider setting a more appropriate timeout (e.g., 10-15 minutes) to catch runaway tests earlier while still providing adequate buffer.

Suggested change
timeout-minutes: 30
timeout-minutes: 15

Copilot uses AI. Check for mistakes.
strategy:
matrix:
# "3.10" must be a string; otherwise it is interpreted as 3.1.
Expand All @@ -39,10 +40,25 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Run unit tests
if: steps.install.outcome == 'success' && (success() || failure())
- name: Run unit tests with coverage
if: steps.install.outcome == 'success' && (success() || failure()) && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
run: |
python -m pytest tests/pytest/ --cov=shiny --cov-report=xml -q

- name: Run unit tests without coverage
if: steps.install.outcome == 'success' && (success() || failure()) && (matrix.os != 'ubuntu-latest' || matrix.python-version != '3.12')
run: |
make check-tests
python -m pytest tests/pytest/ -q

- name: Upload coverage reports to Codecov
if: steps.install.outcome == 'success' && (success() || failure()) && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
uses: codecov/codecov-action@v5
with:
files: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
verbose: true

- name: Type check
if: steps.install.outcome == 'success' && (success() || failure())
Expand Down
24 changes: 20 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ clean-js: FORCE
SUB_FILE:=
PYTEST_BROWSERS:= --browser webkit --browser firefox --browser chromium
PYTEST_DEPLOYS_BROWSERS:= --browser chromium
PYTEST_XDIST?= -n auto


# Full test path to playwright tests
Expand Down Expand Up @@ -222,10 +223,25 @@ playwright-examples: FORCE
playwright-ai: FORCE
$(MAKE) playwright TEST_FILE="$(AI_TEST_FILE)"

coverage: FORCE ## check combined code coverage (must run e2e last)
pytest --cov-report term-missing --cov=shiny tests/pytest/ $(SHINY_TEST_FILE) $(PYTEST_BROWSERS)
coverage html
$(BROWSER) htmlcov/index.html
coverage: FORCE ## check unit test coverage (HTML + term)
$(MAKE) coverage-unit

coverage-unit: FORCE ## check unit test coverage only (HTML + term)
pytest tests/pytest/ $(PYTEST_XDIST) --cov=shiny --cov-report=term-missing --cov-report=html
coverage combine
@echo "Coverage report: htmlcov/index.html"

coverage-check: FORCE ## check coverage meets minimum threshold
pytest tests/pytest/ --cov=shiny --cov-fail-under=25

# CI coverage report: generates both HTML and term reports for CI environments
coverage-ci: FORCE ## generate unit test coverage reports for CI (HTML + term)
@echo "-------- Running tests with coverage --------"
pytest tests/pytest/ $(PYTEST_XDIST) --cov=shiny --cov-report=html --cov-report=term
coverage combine
@echo "Coverage HTML report: htmlcov/index.html"
@echo "-------- Coverage Report Summary --------"
coverage report

release: dist ## package and upload a release
twine upload dist/*
Expand Down
53 changes: 53 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,56 @@ skip = [
# Note: This setting can not be done via CLI and must be set within a config
ignore_errors = true
exclude = ["shiny/api-examples", "shiny/templates"]

[tool.coverage.run]
source = ["shiny"]
branch = true
parallel = true
concurrency = ["multiprocessing"]
omit = [
"shiny/api-examples/*",
"shiny/templates/*",
"shiny/www/*",
"shiny/_version.py",
]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"if typing.TYPE_CHECKING:",
"@overload",
"@typing.overload",
"\\.\\.\\.", # Ellipsis in stub files
# Exclude import statements
"^import ",
"^from .* import ",
# Exclude TypedDict and Protocol definitions (type-only code)
"class .*\\(TypedDict\\):",
"class .*\\(.*TypedDict.*\\):",
"class .*\\(Protocol\\):",
"class .*\\(.*Protocol.*\\):",
# Exclude type aliases and TypeVar
"^[A-Z][a-zA-Z0-9_]* = ",
".* = TypeVar\\(",
".* = Union\\[",
# Exclude __all__ definitions
"^__all__ = ",
# Exclude sentinel/marker class definitions
"^class [A-Z_]+:",
"^ pass$",
# Exclude docstrings (triple-quoted strings)
'"""',
"'''",
]
# Note: show_missing is intentionally not set to true by default.
# Using `--cov-report=term-missing` with the full shiny source causes
# coverage report generation to be extremely slow. Use HTML reports
# or targeted term-missing reports on specific modules instead.
precision = 2
fail_under = 25

[tool.coverage.html]
directory = "htmlcov"
5 changes: 5 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ testpaths=tests/pytest/
; Note: Browsers are set within `./Makefile`
addopts = --strict-markers --durations=6 --durations-min=5.0 --numprocesses auto
verbosity_test_cases=2

[coverage:run]
# Create separate coverage data files for each parallel worker
parallel = true
concurrency = multiprocessing
20 changes: 11 additions & 9 deletions shiny/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,20 @@ class MISSING_TYPE:
pass


MISSING: MISSING_TYPE = MISSING_TYPE()
DEPRECATED: MISSING_TYPE = MISSING_TYPE() # A MISSING that communicates deprecation
MISSING: MISSING_TYPE = MISSING_TYPE() # pragma: no cover
DEPRECATED: MISSING_TYPE = (
MISSING_TYPE()
) # A MISSING that communicates deprecation # pragma: no cover
Comment on lines +48 to +50
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The # pragma: no cover comment on line 50 appears at the end of the multi-line statement, which may not properly exclude line 49 from coverage. Consider adding the pragma comment to line 48 as well, or restructure to a single line assignment.

Suggested change
DEPRECATED: MISSING_TYPE = (
MISSING_TYPE()
) # A MISSING that communicates deprecation # pragma: no cover
DEPRECATED: MISSING_TYPE = MISSING_TYPE() # A MISSING that communicates deprecation # pragma: no cover

Copilot uses AI. Check for mistakes.

ListOrTuple = Union[List[T], Tuple[T, ...]]
ListOrTuple = Union[List[T], Tuple[T, ...]] # pragma: no cover


# Information about a single file, with a structure like:
# {'name': 'mtcars.csv', 'size': 1303, 'type': 'text/csv', 'datapath: '/...../mtcars.csv'}
# The incoming data doesn't include 'datapath'; that field is added by the
# FileUploadOperation class.
@add_example(ex_dir="./api-examples/input_file")
class FileInfo(TypedDict):
@add_example(ex_dir="./api-examples/input_file") # pragma: no cover
class FileInfo(TypedDict): # pragma: no cover
Comment on lines +59 to +60
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The # pragma: no cover comments are redundant on both the decorator and the class definition. The exclusion pattern in pyproject.toml already excludes class .*\(TypedDict\): lines. Remove the duplicate pragma comments to reduce clutter.

Suggested change
@add_example(ex_dir="./api-examples/input_file") # pragma: no cover
class FileInfo(TypedDict): # pragma: no cover
@add_example(ex_dir="./api-examples/input_file")
class FileInfo(TypedDict):

Copilot uses AI. Check for mistakes.
"""
Class for information about a file upload.

Expand All @@ -74,8 +76,8 @@ class FileInfo(TypedDict):
"""The path to the file on the server."""


@add_example(ex_dir="./api-examples/output_image")
class ImgData(TypedDict):
@add_example(ex_dir="./api-examples/output_image") # pragma: no cover
class ImgData(TypedDict): # pragma: no cover
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above - these # pragma: no cover comments are redundant given the TypedDict exclusion pattern in pyproject.toml configuration.

Suggested change
class ImgData(TypedDict): # pragma: no cover
class ImgData(TypedDict):

Copilot uses AI. Check for mistakes.
"""
Return type for :class:`~shiny.render.image`.

Expand Down Expand Up @@ -185,8 +187,8 @@ def __init__(self, message: str, sanitize: bool = True, close: bool = False):
self.close = close


class ActionButtonValue(int):
pass
class ActionButtonValue(int): # pragma: no cover
pass # pragma: no cover
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The # pragma: no cover comment on line 191 is redundant. The exclusion pattern ^ pass$ in pyproject.toml already excludes standalone pass statements. Only the class definition line needs the pragma comment.

Suggested change
pass # pragma: no cover
pass

Copilot uses AI. Check for mistakes.


class NavSetArg(Protocol):
Expand Down
100 changes: 100 additions & 0 deletions tests/pytest/test_accordion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Tests for accordion UI components."""

from shiny.ui import (
accordion,
accordion_panel,
)


class TestAccordionPanel:
"""Tests for accordion_panel function."""

def test_basic_accordion_panel(self):
"""Test creating a basic accordion panel."""
panel = accordion_panel("Panel Title", "Panel content")
assert panel is not None
assert panel._data_value == "Panel Title"

def test_accordion_panel_with_value(self):
"""Test accordion panel with explicit value."""
panel = accordion_panel("Display Title", "Content", value="panel_id")
assert panel._data_value == "panel_id"

def test_accordion_panel_with_icon(self):
"""Test accordion panel with icon."""
from htmltools import tags

icon = tags.i(class_="fa fa-cog")
panel = accordion_panel("Settings", "Settings content", icon=icon)
assert panel is not None
assert panel._icon is not None


class TestAccordion:
"""Tests for accordion function."""

def test_basic_accordion(self):
"""Test creating a basic accordion."""
acc = accordion(
accordion_panel("Panel 1", "Content 1"),
accordion_panel("Panel 2", "Content 2"),
)
html = str(acc)

assert "accordion" in html
assert "Panel 1" in html
assert "Panel 2" in html

def test_accordion_with_id(self):
"""Test accordion with id."""
acc = accordion(accordion_panel("Panel 1", "Content 1"), id="my_accordion")
html = str(acc)

assert "my_accordion" in html

def test_accordion_open_panels(self):
"""Test accordion with specific panels open."""
acc = accordion(
accordion_panel("Panel 1", "Content 1"),
accordion_panel("Panel 2", "Content 2"),
open="Panel 1",
)
html = str(acc)

assert "Panel 1" in html

def test_accordion_open_all(self):
"""Test accordion with all panels open."""
acc = accordion(
accordion_panel("Panel 1", "Content 1"),
accordion_panel("Panel 2", "Content 2"),
open=True,
)
html = str(acc)

assert "Panel 1" in html

def test_accordion_multiple(self):
"""Test accordion that allows multiple panels open."""
acc = accordion(
accordion_panel("Panel 1", "Content 1"),
accordion_panel("Panel 2", "Content 2"),
multiple=True,
)
html = str(acc)

assert "accordion" in html

def test_accordion_width(self):
"""Test accordion with explicit width."""
acc = accordion(accordion_panel("Panel 1", "Content 1"), width="400px")
html = str(acc)

assert "400px" in html

def test_accordion_height(self):
"""Test accordion with explicit height."""
acc = accordion(accordion_panel("Panel 1", "Content 1"), height="300px")
html = str(acc)

assert "300px" in html
80 changes: 80 additions & 0 deletions tests/pytest/test_accordion_full.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Tests for shiny/ui/_accordion.py module."""

from shiny.ui._accordion import AccordionPanel, accordion, accordion_panel


class TestAccordion:
"""Tests for accordion function."""

def test_accordion_is_callable(self):
"""Test accordion is callable."""
assert callable(accordion)

def test_accordion_returns_tag(self):
"""Test accordion returns a Tag."""
from htmltools import Tag

result = accordion(
accordion_panel("Panel 1", "Content 1"),
accordion_panel("Panel 2", "Content 2"),
)
assert isinstance(result, Tag)

def test_accordion_with_id(self):
"""Test accordion with id parameter."""
from htmltools import Tag

result = accordion(
accordion_panel("Panel 1", "Content 1"),
id="my_accordion",
)
assert isinstance(result, Tag)


class TestAccordionPanel:
"""Tests for accordion_panel function."""

def test_accordion_panel_is_callable(self):
"""Test accordion_panel is callable."""
assert callable(accordion_panel)

def test_accordion_panel_returns_accordion_panel(self):
"""Test accordion_panel returns an AccordionPanel object."""
result = accordion_panel("Panel Title", "Content")
assert isinstance(result, AccordionPanel)


class TestAccordionPanelClass:
"""Tests for AccordionPanel class."""

def test_accordion_panel_class_exists(self):
"""Test AccordionPanel class exists."""
assert AccordionPanel is not None


class TestAccordionExported:
"""Tests for accordion functions export."""

def test_accordion_in_ui(self):
"""Test accordion is in ui module."""
from shiny import ui

assert hasattr(ui, "accordion")

def test_accordion_panel_in_ui(self):
"""Test accordion_panel is in ui module."""
from shiny import ui

assert hasattr(ui, "accordion_panel")

def test_update_accordion_in_ui(self):
"""Test update_accordion is in ui module."""
from shiny import ui

assert hasattr(ui, "update_accordion")

def test_update_accordion_panel_in_ui(self):
"""Test update_accordion_panel is in ui module."""
from shiny import ui

assert hasattr(ui, "update_accordion_panel")
Loading
Loading