Add pytest coverage for CLI and rendering behavior

This commit is contained in:
2026-04-02 14:13:06 -07:00
parent f32da588fb
commit 695e5b6541
8 changed files with 1029 additions and 0 deletions

1
.gitignore vendored
View File

@@ -11,4 +11,5 @@ build/
.ruff_cache/
.mypy_cache/
.pytest_cache/
.coverage
.env

View File

@@ -36,8 +36,14 @@ Repository = "https://github.com/yunxiaoxu/rich-viewer"
dev = [
"ruff>=0.9",
"mypy>=1.14",
"pytest>=8.0",
"pytest-cov>=5.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--tb=short -q"
[build-system]
requires = ["uv_build>=0.11.2,<0.12.0"]
build-backend = "uv_build"

57
tests/conftest.py Normal file
View File

@@ -0,0 +1,57 @@
"""Shared test fixtures for rp."""
from __future__ import annotations
from collections.abc import Callable, Iterator
import io
import json
from pathlib import Path
import pytest
from rich.console import Console
from rp.render import RenderOptions
@pytest.fixture
def tmp_file(tmp_path: Path) -> Callable[..., Path]:
"""Factory fixture to create a temporary file with given content and suffix."""
def _make(content: str, suffix: str = ".txt", name: str | None = None) -> Path:
filename = name or f"test{suffix}"
p = tmp_path / filename
p.write_text(content, encoding="utf-8")
return p
return _make
@pytest.fixture
def capture_console() -> Iterator[Console]:
"""A Rich Console that captures output instead of printing to stdout."""
console = Console(file=io.StringIO(), width=120, force_terminal=True)
yield console
@pytest.fixture
def default_options() -> RenderOptions:
"""Default RenderOptions for testing."""
return RenderOptions()
@pytest.fixture
def sample_json() -> str:
"""Valid JSON string for testing."""
return json.dumps({"name": "test", "value": 42, "items": [1, 2, 3]}, indent=2)
@pytest.fixture
def sample_markdown() -> str:
"""Markdown content for testing."""
return "# Hello\n\nThis is **bold** and *italic*.\n\n- item 1\n- item 2\n"
@pytest.fixture
def sample_python() -> str:
"""Python code for testing."""
return 'def hello(name: str = "world") -> str:\n return f"Hello, {name}!"\n'

254
tests/test_cli.py Normal file
View File

@@ -0,0 +1,254 @@
"""Tests for the CLI entry point."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
from typer.testing import CliRunner
from rp import __version__
from rp.cli import app, main
runner = CliRunner()
class TestVersionFlag:
"""Tests for the version flags."""
def test_version_long_flag(self) -> None:
result = runner.invoke(app, ["--version"])
assert result.exit_code == 0
assert __version__ in result.output
def test_version_short_flag(self) -> None:
result = runner.invoke(app, ["-v"])
assert result.exit_code == 0
assert __version__ in result.output
def test_version_output_format(self) -> None:
result = runner.invoke(app, ["--version"])
assert result.exit_code == 0
assert "Rich Print (rp)" in result.output
assert "License: MIT" in result.output
class TestFileInput:
"""Tests for reading from a file argument."""
def test_render_python_file(self, tmp_file) -> None:
file = tmp_file("def hello(): pass\n", suffix=".py")
result = runner.invoke(app, [str(file), "--no-pager"])
assert result.exit_code == 0
assert result.output
def test_render_markdown_file(self, tmp_file) -> None:
file = tmp_file("# Hello\n\nWorld\n", suffix=".md")
result = runner.invoke(app, [str(file), "--no-pager"])
assert result.exit_code == 0
def test_render_json_file(self, tmp_file, sample_json: str) -> None:
file = tmp_file(sample_json, suffix=".json")
result = runner.invoke(app, [str(file), "--no-pager"])
assert result.exit_code == 0
def test_detect_type_called_with_file_path(self, tmp_file) -> None:
"""detect_type receives the file path string."""
file = tmp_file("x = 1\n", suffix=".py")
with patch("rp.cli.detect_type") as mock_detect_type:
mock_detect_type.return_value = "python"
with patch("rp.cli.render"):
runner.invoke(app, [str(file), "--no-pager"])
mock_detect_type.assert_called_once_with(
path=str(file),
content="x = 1\n",
explicit_type=None,
)
def test_render_called_with_correct_options(self, tmp_file) -> None:
"""Render receives the right theme, line_numbers, pager flags."""
file = tmp_file("x = 1\n", suffix=".py")
with patch("rp.cli.detect_type", return_value="python"):
with patch("rp.cli.render") as mock_render:
runner.invoke(
app,
[
str(file),
"--no-pager",
"--theme",
"github-dark",
"--line-numbers",
],
)
call_args = mock_render.call_args
assert call_args is not None
assert call_args[0][0] == "x = 1\n"
assert call_args[0][1] == "python"
options = call_args[0][3]
assert options.theme == "github-dark"
assert options.line_numbers is True
assert options.pager is False
def test_file_not_found(self) -> None:
result = runner.invoke(app, ["/nonexistent/file.py"])
assert result.exit_code == 1
assert "Error" in result.output
def test_directory_error(self, tmp_path: Path) -> None:
result = runner.invoke(app, [str(tmp_path)])
assert result.exit_code == 1
assert "directory" in result.output.lower() or "Error" in result.output
def test_empty_file(self, tmp_file) -> None:
file = tmp_file("", suffix=".txt")
result = runner.invoke(app, [str(file)])
assert result.exit_code == 0
assert "Empty" in result.output or "empty" in result.output.lower()
def test_binary_file_error(self, tmp_file) -> None:
file = tmp_file("placeholder", suffix=".bin")
with patch.object(
Path,
"read_text",
side_effect=UnicodeDecodeError("utf-8", b"x", 0, 1, "boom"),
):
result = runner.invoke(app, [str(file)])
assert result.exit_code == 1
assert "Cannot read binary file" in result.output
def test_os_error_when_reading_file(self, tmp_file) -> None:
file = tmp_file("placeholder", suffix=".txt")
with patch.object(Path, "read_text", side_effect=OSError("read failed")):
result = runner.invoke(app, [str(file)])
assert result.exit_code == 1
assert "read failed" in result.output
class TestStdinInput:
"""Tests for reading from stdin."""
def test_stdin_python(self) -> None:
result = runner.invoke(app, ["--no-pager"], input="x = 1\n")
assert result.exit_code == 0
def test_stdin_json(self, sample_json: str) -> None:
result = runner.invoke(app, ["--no-pager"], input=sample_json)
assert result.exit_code == 0
def test_stdin_empty(self) -> None:
result = runner.invoke(app, [], input="")
assert result.exit_code == 0
assert "Empty" in result.output or "empty" in result.output.lower()
def test_stdin_with_dash(self) -> None:
result = runner.invoke(app, ["-", "--no-pager"], input="hello\n")
assert result.exit_code == 0
def test_stdin_passes_none_path(self) -> None:
"""When reading from stdin, detect_type should receive path=None."""
with patch("rp.cli.detect_type") as mock_detect_type:
mock_detect_type.return_value = "text"
with patch("rp.cli.render"):
runner.invoke(app, ["--no-pager"], input="hello\n")
mock_detect_type.assert_called_once_with(
path=None,
content="hello\n",
explicit_type=None,
)
def test_stdin_shows_tty_prompt(self) -> None:
"""When stdin is a TTY and no input is piped, show the reading hint."""
with patch("rp.cli.sys.stdin.isatty", return_value=True):
with patch("rp.cli.sys.stdin.read", return_value="hello\n"):
with patch("rp.cli.render"):
with patch("rp.cli.Console.print") as mock_print:
main(pager=False)
mock_print.assert_any_call(
"[dim]Reading from stdin. Press Ctrl+D to end.[/dim]"
)
class TestTypeOverride:
"""Tests for the type override flags."""
def test_explicit_type(self, tmp_file) -> None:
file = tmp_file('{"key": "value"}\n', suffix=".txt")
result = runner.invoke(app, [str(file), "--type", "json", "--no-pager"])
assert result.exit_code == 0
def test_explicit_type_is_forwarded(self, tmp_file) -> None:
"""The --type flag value is passed as explicit_type to detect_type."""
file = tmp_file('{"key": "value"}\n', suffix=".txt")
with patch("rp.cli.detect_type") as mock_detect_type:
mock_detect_type.return_value = "json"
with patch("rp.cli.render"):
runner.invoke(app, [str(file), "--type", "json", "--no-pager"])
_, kwargs = mock_detect_type.call_args
assert kwargs["explicit_type"] == "json"
def test_explicit_type_short_flag(self, tmp_file) -> None:
file = tmp_file("def foo(): pass\n", suffix=".txt")
result = runner.invoke(app, [str(file), "-t", "python", "--no-pager"])
assert result.exit_code == 0
class TestThemeOption:
"""Tests for the theme option."""
def test_custom_theme(self, tmp_file) -> None:
file = tmp_file("x = 1\n", suffix=".py")
result = runner.invoke(app, [str(file), "--theme", "github-dark", "--no-pager"])
assert result.exit_code == 0
class TestLineNumbersOption:
"""Tests for the line number flags."""
def test_line_numbers_enabled(self, tmp_file) -> None:
file = tmp_file("x = 1\n", suffix=".py")
result = runner.invoke(app, [str(file), "--line-numbers", "--no-pager"])
assert result.exit_code == 0
def test_line_numbers_disabled(self, tmp_file) -> None:
file = tmp_file("x = 1\n", suffix=".py")
result = runner.invoke(app, [str(file), "--no-line-numbers", "--no-pager"])
assert result.exit_code == 0

142
tests/test_detect.py Normal file
View File

@@ -0,0 +1,142 @@
"""Tests for file type detection."""
from __future__ import annotations
from pathlib import Path
import pytest
from rp.detect import EXTENSION_MAP, _FILENAME_MAP, detect_type
class TestExplicitType:
"""Priority 1: explicit_type always wins."""
def test_explicit_overrides_everything(self) -> None:
assert detect_type(path="foo.py", content="...", explicit_type="json") == "json"
def test_explicit_with_none_path_and_content(self) -> None:
assert detect_type(explicit_type="rust") == "rust"
def test_explicit_empty_string(self) -> None:
# Empty string is falsy but not None, so it should still win.
assert detect_type(explicit_type="") == ""
class TestFilenameMap:
"""Priority 2: special filenames like Dockerfile."""
def test_dockerfile(self) -> None:
assert detect_type(path="Dockerfile") == "docker"
def test_dockerfile_with_path(self) -> None:
assert detect_type(path="/some/dir/Dockerfile") == "docker"
def test_makefile(self) -> None:
assert detect_type(path="Makefile") == "make"
def test_makefile_with_path(self) -> None:
assert detect_type(path="src/Makefile") == "make"
class TestExtensionMap:
"""Priority 3: file extension lookup."""
def test_python(self) -> None:
assert detect_type(path="script.py") == "python"
def test_python_case_insensitive(self) -> None:
assert detect_type(path="SCRIPT.PY") == "python"
def test_json_extension(self) -> None:
assert detect_type(path="data.json") == "json"
def test_markdown(self) -> None:
assert detect_type(path="README.md") == "markdown"
def test_rust(self) -> None:
assert detect_type(path="main.rs") == "rust"
def test_go(self) -> None:
assert detect_type(path="main.go") == "go"
def test_unknown_extension(self) -> None:
assert detect_type(path="file.xyz") == "text"
def test_no_extension(self) -> None:
assert detect_type(path="Makefile") == "make"
def test_double_extension(self) -> None:
assert detect_type(path="archive.tar.gz") == "text"
def test_hidden_file(self) -> None:
assert detect_type(path=".bashrc") == "text"
@pytest.mark.parametrize(
("suffix", "expected"),
sorted(EXTENSION_MAP.items()),
)
def test_all_known_extensions_detect(self, suffix: str, expected: str) -> None:
assert detect_type(path=f"example{suffix}") == expected
class TestContentJsonDetection:
"""Priority 4: detect JSON from content when path gives no clue."""
def test_json_object(self, sample_json: str) -> None:
assert detect_type(content=sample_json) == "json"
def test_json_array(self) -> None:
assert detect_type(content="[1, 2, 3]") == "json"
def test_json_with_whitespace(self) -> None:
assert detect_type(content=' \n {"a": 1} \n ') == "json"
def test_invalid_json_braces(self) -> None:
assert detect_type(content="{invalid json}") == "text"
def test_non_json_content(self) -> None:
assert detect_type(content="hello world") == "text"
def test_empty_content(self) -> None:
assert detect_type(content="") == "text"
def test_content_with_valid_path_takes_priority(self) -> None:
assert detect_type(path="script.py", content='{"a": 1}') == "python"
class TestFallback:
"""Priority 5: fallback to 'text'."""
def test_no_args(self) -> None:
assert detect_type() == "text"
def test_none_everything(self) -> None:
assert detect_type(path=None, content=None, explicit_type=None) == "text"
class TestExtensionMapCompleteness:
"""Verify EXTENSION_MAP entries are valid."""
def test_all_extensions_are_strings(self) -> None:
for ext, lexer in EXTENSION_MAP.items():
assert isinstance(ext, str), f"Key {ext!r} is not str"
assert isinstance(lexer, str), f"Value for {ext!r} is not str"
def test_all_extensions_start_with_dot(self) -> None:
for ext in EXTENSION_MAP:
assert ext.startswith("."), f"Extension {ext!r} doesn't start with '.'"
def test_all_filenames_in_filename_map(self) -> None:
for name in _FILENAME_MAP:
assert isinstance(name, str)
def test_filename_map_matches_path_name_lookup(self, tmp_file) -> None:
for name, expected in _FILENAME_MAP.items():
path = tmp_file("content", name=name)
assert detect_type(path=str(path)) == expected
def test_path_stringified_from_pathlib(tmp_file) -> None:
path = tmp_file('print("hi")\n', suffix=".py")
assert detect_type(path=str(Path(path))) == "python"

167
tests/test_pager.py Normal file
View File

@@ -0,0 +1,167 @@
"""Tests for pager integration."""
from __future__ import annotations
import signal
import subprocess
from unittest.mock import MagicMock, patch
from rp.pager import MousePager, _mouse_capable_less
class TestMouseCapableLess:
"""Tests for _mouse_capable_less()."""
def test_returns_path_when_less_supports_mouse(self) -> None:
with patch("rp.pager.shutil.which", return_value="/usr/bin/less"):
mock_result = MagicMock()
mock_result.stdout = "less 551 (POSIX regular expressions)"
mock_result.stderr = ""
with patch("rp.pager.subprocess.run", return_value=mock_result):
assert _mouse_capable_less() == "/usr/bin/less"
def test_returns_none_when_less_not_found(self) -> None:
with patch("rp.pager.shutil.which", return_value=None):
assert _mouse_capable_less() is None
def test_returns_none_when_less_too_old(self) -> None:
with patch("rp.pager.shutil.which", return_value="/usr/bin/less"):
mock_result = MagicMock()
mock_result.stdout = "less 444"
mock_result.stderr = ""
with patch("rp.pager.subprocess.run", return_value=mock_result):
assert _mouse_capable_less() is None
def test_returns_path_when_version_is_in_stderr(self) -> None:
with patch("rp.pager.shutil.which", return_value="/usr/bin/less"):
mock_result = MagicMock()
mock_result.stdout = ""
mock_result.stderr = "less 600"
with patch("rp.pager.subprocess.run", return_value=mock_result):
assert _mouse_capable_less() == "/usr/bin/less"
def test_returns_none_on_timeout(self) -> None:
with patch("rp.pager.shutil.which", return_value="/usr/bin/less"):
with patch(
"rp.pager.subprocess.run",
side_effect=subprocess.TimeoutExpired(cmd="less", timeout=5),
):
assert _mouse_capable_less() is None
def test_returns_none_on_os_error(self) -> None:
with patch("rp.pager.shutil.which", return_value="/usr/bin/less"):
with patch("rp.pager.subprocess.run", side_effect=OSError("nope")):
assert _mouse_capable_less() is None
def test_returns_none_when_version_not_parseable(self) -> None:
with patch("rp.pager.shutil.which", return_value="/usr/bin/less"):
mock_result = MagicMock()
mock_result.stdout = "some random output"
mock_result.stderr = ""
with patch("rp.pager.subprocess.run", return_value=mock_result):
assert _mouse_capable_less() is None
def test_boundary_version_543(self) -> None:
with patch("rp.pager.shutil.which", return_value="/usr/bin/less"):
mock_result = MagicMock()
mock_result.stdout = "less 543"
mock_result.stderr = ""
with patch("rp.pager.subprocess.run", return_value=mock_result):
assert _mouse_capable_less() == "/usr/bin/less"
def test_boundary_version_542(self) -> None:
with patch("rp.pager.shutil.which", return_value="/usr/bin/less"):
mock_result = MagicMock()
mock_result.stdout = "less 542"
mock_result.stderr = ""
with patch("rp.pager.subprocess.run", return_value=mock_result):
assert _mouse_capable_less() is None
class TestMousePager:
"""Tests for MousePager.show()."""
def test_falls_back_to_system_pager_when_no_less(self) -> None:
pager = MousePager()
with patch("rp.pager._mouse_capable_less", return_value=None):
with patch("rp.pager.SystemPager") as mock_system_pager:
mock_instance = MagicMock()
mock_system_pager.return_value = mock_instance
pager.show("content")
mock_instance.show.assert_called_once_with("content")
def test_uses_less_when_available(self) -> None:
pager = MousePager()
mock_result = MagicMock()
mock_result.returncode = 0
with patch("rp.pager._mouse_capable_less", return_value="/usr/bin/less"):
with patch("rp.pager.subprocess.run", return_value=mock_result) as mock_run:
pager.show("hello")
mock_run.assert_called_once_with(
["/usr/bin/less", "-R", "--mouse"],
input="hello",
text=True,
)
def test_sigint_does_not_trigger_fallback_for_negative_code(self) -> None:
pager = MousePager()
mock_result = MagicMock()
mock_result.returncode = -signal.SIGINT
with patch("rp.pager._mouse_capable_less", return_value="/usr/bin/less"):
with patch("rp.pager.subprocess.run", return_value=mock_result):
with patch("rp.pager.SystemPager") as mock_system_pager:
pager.show("content")
mock_system_pager.assert_not_called()
def test_sigint_does_not_trigger_fallback_for_128_plus_code(self) -> None:
pager = MousePager()
mock_result = MagicMock()
mock_result.returncode = 128 + signal.SIGINT
with patch("rp.pager._mouse_capable_less", return_value="/usr/bin/less"):
with patch("rp.pager.subprocess.run", return_value=mock_result):
with patch("rp.pager.SystemPager") as mock_system_pager:
pager.show("content")
mock_system_pager.assert_not_called()
def test_nonzero_exit_triggers_fallback(self) -> None:
pager = MousePager()
mock_result = MagicMock()
mock_result.returncode = 1
with patch("rp.pager._mouse_capable_less", return_value="/usr/bin/less"):
with patch("rp.pager.subprocess.run", return_value=mock_result):
with patch("rp.pager.SystemPager") as mock_system_pager:
mock_instance = MagicMock()
mock_system_pager.return_value = mock_instance
pager.show("content")
mock_instance.show.assert_called_once_with("content")
def test_oserror_triggers_fallback(self) -> None:
pager = MousePager()
with patch("rp.pager._mouse_capable_less", return_value="/usr/bin/less"):
with patch("rp.pager.subprocess.run", side_effect=OSError("fail")):
with patch("rp.pager.SystemPager") as mock_system_pager:
mock_instance = MagicMock()
mock_system_pager.return_value = mock_instance
pager.show("content")
mock_instance.show.assert_called_once_with("content")

257
tests/test_render.py Normal file
View File

@@ -0,0 +1,257 @@
"""Tests for the rendering module."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from rich.json import JSON as RichJSON
from rich.markdown import Markdown
from rich.padding import Padding
from rich.syntax import Syntax
from rp.pager import MousePager
from rp.render import RenderOptions, render
class TestRenderOptions:
"""RenderOptions dataclass defaults."""
def test_default_theme(self) -> None:
opts = RenderOptions()
assert opts.theme == "monokai"
def test_default_line_numbers_is_none(self) -> None:
opts = RenderOptions()
assert opts.line_numbers is None
def test_default_pager_is_none(self) -> None:
opts = RenderOptions()
assert opts.pager is None
def test_custom_values(self) -> None:
opts = RenderOptions(theme="github-dark", line_numbers=True, pager=False)
assert opts.theme == "github-dark"
assert opts.line_numbers is True
assert opts.pager is False
class TestRenderMarkdown:
"""Markdown rendering."""
@pytest.mark.parametrize("file_type", ["markdown", "md"])
def test_renders_markdown_types(
self,
capture_console,
sample_markdown: str,
file_type: str,
) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.Markdown", wraps=Markdown) as mock_markdown:
render(sample_markdown, file_type, capture_console, opts)
output = capture_console.file.getvalue()
assert len(output) > 0
mock_markdown.assert_called_once_with(sample_markdown)
class TestRenderJson:
"""JSON rendering."""
@pytest.mark.parametrize("content", ['{"a": 1}', "[1, 2, 3]"])
def test_renders_valid_json(
self,
capture_console,
content: str,
) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.RichJSON", wraps=RichJSON) as mock_json:
render(content, "json", capture_console, opts)
output = capture_console.file.getvalue()
assert len(output) > 0
mock_json.assert_called_once_with(content)
def test_invalid_json_falls_back_to_syntax(
self,
capture_console,
) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
render("{invalid json}", "json", capture_console, opts)
output = capture_console.file.getvalue()
assert len(output) > 0
mock_syntax.assert_called_once_with(
"{invalid json}",
"json",
theme="monokai",
line_numbers=False,
)
class TestRenderCode:
"""Code rendering with Syntax."""
def test_renders_python(self, capture_console, sample_python: str) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
render(sample_python, "python", capture_console, opts)
output = capture_console.file.getvalue()
assert len(output) > 0
mock_syntax.assert_called_once_with(
sample_python,
"python",
theme="monokai",
line_numbers=True,
)
def test_renders_unknown_type_as_text_fallback(self, capture_console) -> None:
opts = RenderOptions(pager=False)
def syntax_side_effect(*args, **kwargs):
lexer = args[1]
if lexer == "xyz_nonexistent_lexer":
raise Exception("unknown lexer")
return Syntax(*args, **kwargs)
with patch("rp.render.Syntax", side_effect=syntax_side_effect) as mock_syntax:
render("some content", "xyz_nonexistent_lexer", capture_console, opts)
output = capture_console.file.getvalue()
assert len(output) > 0
assert mock_syntax.call_args_list[0].args == (
"some content",
"xyz_nonexistent_lexer",
)
assert mock_syntax.call_args_list[0].kwargs == {
"theme": "monokai",
"line_numbers": True,
}
assert mock_syntax.call_args_list[1].args == ("some content", "text")
assert mock_syntax.call_args_list[1].kwargs == {
"theme": "monokai",
"line_numbers": True,
}
def test_renders_rust(self, capture_console) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
render("fn main() {}", "rust", capture_console, opts)
output = capture_console.file.getvalue()
assert len(output) > 0
mock_syntax.assert_called_once_with(
"fn main() {}",
"rust",
theme="monokai",
line_numbers=True,
)
class TestLineNumbers:
"""Line number auto-detection logic."""
@pytest.mark.parametrize(
("content", "file_type", "expected"),
[
("x = 1", "python", True),
("# Heading", "markdown", False),
("{}", "json", False),
],
)
def test_default_line_number_behavior(
self,
capture_console,
content: str,
file_type: str,
expected: bool,
) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
render(content, file_type, capture_console, opts)
if file_type == "python":
mock_syntax.assert_called_once()
assert mock_syntax.call_args.kwargs["line_numbers"] is expected
else:
assert len(capture_console.file.getvalue()) > 0
@pytest.mark.parametrize("line_numbers", [True, False])
def test_explicit_line_numbers(
self,
capture_console,
sample_python: str,
line_numbers: bool,
) -> None:
opts = RenderOptions(line_numbers=line_numbers, pager=False)
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
render(sample_python, "python", capture_console, opts)
mock_syntax.assert_called_once()
assert mock_syntax.call_args.kwargs["line_numbers"] is line_numbers
class TestPagerBehavior:
"""Pager auto-detection."""
def test_pager_disabled(self, capture_console, sample_python: str) -> None:
opts = RenderOptions(pager=False)
with patch.object(capture_console, "pager") as mock_pager:
render(sample_python, "python", capture_console, opts)
assert len(capture_console.file.getvalue()) > 0
mock_pager.assert_not_called()
def test_pager_auto_tty(self, capture_console, sample_python: str) -> None:
opts = RenderOptions(pager=None)
mock_context = MagicMock()
mock_context.__enter__.return_value = None
mock_context.__exit__.return_value = None
with patch("sys.stdout.isatty", return_value=True):
with patch.object(
capture_console, "pager", return_value=mock_context
) as mock_pager:
render(sample_python, "python", capture_console, opts)
mock_pager.assert_called_once()
assert mock_pager.call_args.kwargs["styles"] is True
assert isinstance(mock_pager.call_args.kwargs["pager"], MousePager)
def test_pager_auto_non_tty(self, capture_console, sample_python: str) -> None:
opts = RenderOptions(pager=None)
with patch("sys.stdout.isatty", return_value=False):
with patch.object(capture_console, "pager") as mock_pager:
render(sample_python, "python", capture_console, opts)
assert len(capture_console.file.getvalue()) > 0
mock_pager.assert_not_called()
class TestPadding:
"""Rendered output is wrapped in padding."""
def test_wraps_renderable_in_padding(
self,
capture_console,
sample_python: str,
) -> None:
opts = RenderOptions(pager=False)
with patch.object(capture_console, "print") as mock_print:
render(sample_python, "python", capture_console, opts)
printed = mock_print.call_args.args[0]
assert isinstance(printed, Padding)
assert printed.expand is True

145
uv.lock generated
View File

@@ -32,6 +32,99 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "coverage"
version = "7.13.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" },
{ url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" },
{ url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" },
{ url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" },
{ url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" },
{ url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" },
{ url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" },
{ url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" },
{ url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" },
{ url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" },
{ url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" },
{ url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" },
{ url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" },
{ url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" },
{ url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" },
{ url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" },
{ url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" },
{ url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" },
{ url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" },
{ url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" },
{ url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" },
{ url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" },
{ url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" },
{ url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" },
{ url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" },
{ url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" },
{ url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" },
{ url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" },
{ url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" },
{ url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" },
{ url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" },
{ url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" },
{ url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" },
{ url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" },
{ url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" },
{ url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" },
{ url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" },
{ url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" },
{ url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" },
{ url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" },
{ url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" },
{ url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" },
{ url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" },
{ url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" },
{ url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" },
{ url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" },
{ url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" },
{ url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" },
{ url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" },
{ url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" },
{ url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" },
{ url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" },
{ url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" },
{ url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" },
{ url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" },
{ url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" },
{ url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" },
{ url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" },
{ url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" },
{ url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" },
{ url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" },
{ url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" },
{ url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" },
{ url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" },
{ url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" },
{ url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" },
{ url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" },
{ url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" },
{ url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" },
{ url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" },
{ url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" },
{ url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" },
{ url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" },
{ url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "librt"
version = "0.8.1"
@@ -165,6 +258,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "packaging"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pathspec"
version = "1.0.4"
@@ -174,6 +276,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
@@ -183,6 +294,36 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
name = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "pytest-cov"
version = "7.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage" },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
]
[[package]]
name = "rich"
version = "14.3.3"
@@ -208,6 +349,8 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "mypy" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "ruff" },
]
@@ -220,6 +363,8 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "mypy", specifier = ">=1.14" },
{ name = "pytest", specifier = ">=8.0" },
{ name = "pytest-cov", specifier = ">=5.0" },
{ name = "ruff", specifier = ">=0.9" },
]