24 Commits
v0.2.0 ... main

Author SHA1 Message Date
ee67bcb768 Merge pull request 'Fix code wrapping for syntax-highlighted output' (#3) from hotfix/code-wrap into main
All checks were successful
Release / checks (3.12) (push) Successful in -26s
Release / checks (3.13) (push) Successful in -24s
Release / checks (3.14) (push) Successful in -22s
Release / release (push) Successful in -11s
CI / lint (push) Successful in -27s
CI / typecheck (push) Successful in -23s
CI / test (3.12) (push) Successful in -27s
CI / test (3.13) (push) Successful in -26s
CI / test (3.14) (push) Successful in -26s
2026-04-04 15:06:49 -07:00
868656ea12 Bump version to 0.3.1
All checks were successful
CI / lint (pull_request) Successful in -25s
CI / typecheck (pull_request) Successful in -22s
CI / test (3.12) (pull_request) Successful in -24s
CI / test (3.13) (pull_request) Successful in -23s
CI / test (3.14) (pull_request) Successful in -26s
2026-04-04 14:57:45 -07:00
7df3acde9b fix: enable word wrapping for syntax-highlighted code output
Long code lines were clipped at the terminal width, making them
unreadable. Pass word_wrap=True to all Syntax() calls so lines wrap
instead of being cut off.
2026-04-04 14:47:21 -07:00
579df72869 Merge pull request 'Bump version to 0.3.0' (#2) from dev into main
All checks were successful
CI / lint (push) Successful in -28s
CI / test (3.12) (push) Successful in -24s
CI / test (3.13) (push) Successful in -23s
CI / test (3.14) (push) Successful in -26s
CI / typecheck (push) Successful in -25s
Release / checks (3.12) (push) Successful in -24s
Release / checks (3.13) (push) Successful in -21s
Release / checks (3.14) (push) Successful in -21s
Release / release (push) Successful in -1s
2026-04-04 07:00:54 -07:00
537238f56f Bump version to 0.3.0
All checks were successful
CI / lint (push) Successful in -26s
CI / typecheck (push) Successful in -26s
CI / test (3.14) (push) Successful in -24s
CI / lint (pull_request) Successful in -28s
CI / typecheck (pull_request) Successful in -23s
CI / test (3.12) (pull_request) Successful in -25s
CI / test (3.13) (pull_request) Successful in -23s
CI / test (3.14) (pull_request) Successful in -24s
CI / test (3.12) (push) Successful in -25s
CI / test (3.13) (push) Successful in -25s
2026-04-04 06:58:19 -07:00
598450d9e2 Merge pull request 'Merge dev into main' (#1) from dev into main
All checks were successful
CI / lint (push) Successful in -28s
CI / typecheck (push) Successful in -24s
CI / test (3.12) (push) Successful in -24s
CI / test (3.13) (push) Successful in -23s
CI / test (3.14) (push) Successful in -24s
2026-04-04 06:42:41 -07:00
0f93d3a095 Add watch mode for live file monitoring
All checks were successful
CI / lint (push) Successful in -26s
CI / typecheck (push) Successful in -23s
CI / test (3.12) (push) Successful in -25s
CI / test (3.13) (push) Successful in -24s
CI / test (3.14) (push) Successful in -24s
CI / lint (pull_request) Successful in -28s
CI / test (3.14) (pull_request) Successful in -24s
CI / typecheck (pull_request) Successful in -24s
CI / test (3.12) (pull_request) Successful in -25s
CI / test (3.13) (pull_request) Successful in -24s
Add --watch flag that polls a file and redraws the terminal when its
content changes. Useful for monitoring log files, configuration files,
or any file that changes over time.

Features:
- Polls file every 1 second for changes
- Disables pager (pager would block the watch loop)
- File-only: rejects stdin or - input
- Error recovery: retries when file is missing or temporarily unreadable
- State comparison: only redraws when content actually changes
2026-04-04 05:23:28 -07:00
d94cf2df30 Run CI on dev pushes
All checks were successful
CI / lint (push) Successful in -20s
CI / typecheck (push) Successful in -19s
CI / test (3.12) (push) Successful in -21s
CI / test (3.13) (push) Successful in -18s
CI / test (3.14) (push) Successful in -21s
2026-04-03 19:11:59 -07:00
93c777cf6a Add opt-in debug logging across the CLI pipeline
Thread RP_DEBUG-driven logging through input, detection, rendering, and pager decisions so CLI behavior is inspectable without polluting stdout. Add focused coverage for debug wiring, env parsing, and stderr-only output.
2026-04-03 19:09:24 -07:00
37502c3083 Add script detection, env shebang parser, and opt-in content guessing 2026-04-03 13:55:54 -07:00
63102473da Add Git workflow and update docs 2026-04-03 03:32:00 -07:00
1ac724603a Ignore OpenCode state directory
All checks were successful
CI / lint (push) Successful in 13s
CI / test (3.12) (push) Successful in -32s
CI / test (3.14) (push) Successful in -30s
CI / typecheck (push) Successful in -30s
CI / test (3.13) (push) Successful in -31s
2026-04-03 00:43:10 -07:00
907b911e95 Add release workflow and expand Python support matrix 2026-04-03 00:43:02 -07:00
383447d050 Add GitHub Actions CI workflow 2026-04-02 21:23:38 -07:00
695e5b6541 Add pytest coverage for CLI and rendering behavior 2026-04-02 14:34:46 -07:00
f32da588fb Bump version to 0.2.3 2026-04-01 23:59:55 -07:00
0e7f30d6c3 Add padding around rendered output 2026-04-01 23:59:34 -07:00
65ff34dd2c Bump version to 0.2.2 2026-04-01 23:51:52 -07:00
217feab7de Improve JSON detection: strip both ends, check matching brackets, use full content 2026-04-01 23:51:41 -07:00
c3a71c2ff9 Bump version to 0.2.1 2026-04-01 23:12:21 -07:00
32a0471910 Fix less mouse pager fallback handling 2026-04-01 22:53:44 -07:00
32a8a1d4ad Improve documentation and modernize type hints 2026-04-01 21:06:34 -07:00
5935140620 Fix mypy type errors in render.py by annotating renderable variable 2026-04-01 18:57:49 -07:00
5b336f2ce0 Add LICENSE, fix PEP 639 classifier warning, enhance --version output, add dev dependencies
- Create MIT LICENSE file
- Remove deprecated License classifier, add license-files to pyproject.toml
- Add __license__ constant to __init__.py
- Show full name, copyright, and license in rp --version
- Add ruff and mypy as dev dependencies
2026-04-01 18:40:03 -07:00
20 changed files with 2677 additions and 86 deletions

64
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: CI
on:
push:
branches: [main, dev]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Install dependencies
run: uv sync --locked
- name: Run ruff check
run: uv run ruff check src/ tests/
- name: Run ruff format check
run: uv run ruff format --check src/ tests/
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Install dependencies
run: uv sync --locked
- name: Run mypy
run: uv run mypy src/
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- name: Install dependencies
run: uv sync --locked
- name: Run tests with coverage
run: uv run pytest --cov=rp --cov-report=term-missing

124
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,124 @@
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
checks:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- name: Install dependencies
run: uv sync --locked
- name: Run release checks
run: |
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/
uv run mypy src/
uv run pytest --tb=short -q
release:
runs-on: ubuntu-latest
needs: checks
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: true
- name: Install dependencies
run: uv sync --locked
- name: Validate release version
run: |
package_version="$(grep '^version = ' pyproject.toml | cut -d '"' -f 2)"
module_version="$(grep '^__version__ = ' src/rp/__init__.py | cut -d '"' -f 2)"
expected_tag="v${package_version}"
if [ "$package_version" != "$module_version" ]; then
printf 'Version mismatch: pyproject.toml=%s src/rp/__init__.py=%s\n' "$package_version" "$module_version"
exit 1
fi
if [ "${{ github.ref_name }}" != "$expected_tag" ]; then
printf 'Tag %s does not match package version %s\n' "${{ github.ref_name }}" "$expected_tag"
exit 1
fi
- name: Build release artifacts
run: |
rm -rf dist
uv build
cd dist
sha256sum *.tar.gz *.whl > SHA256SUMS
- name: Generate release notes
run: |
tag="${{ github.ref_name }}"
release_target="${tag}^"
previous_tag="$(git tag --list 'v*' --sort=-version:refname | awk -v tag="$tag" '$0 == tag { found = 1; next } found { print; exit }')"
if ! git rev-parse --verify "$release_target" >/dev/null 2>&1; then
release_target="$tag"
fi
{
printf '# %s\n\n' "$tag"
if [ -n "$previous_tag" ]; then
printf '## Changes since %s\n\n' "$previous_tag"
git log --no-merges --pretty='- %s (%h)' "${previous_tag}..${release_target}"
else
printf '## Changes\n\n'
git log --no-merges --pretty='- %s (%h)' "$release_target"
fi
} > release-notes.md
- name: Create GitHub release
if: ${{ github.server_url == 'https://github.com' }}
uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
tag_name: ${{ github.ref_name }}
body_path: release-notes.md
files: |
dist/*.tar.gz
dist/*.whl
dist/SHA256SUMS
- name: Create Gitea release
if: ${{ github.server_url == 'https://gitea.yunxiao.xyz' }}
uses: akkuman/gitea-release-action@v1
env:
NODE_OPTIONS: --experimental-fetch
with:
server_url: https://gitea.yunxiao.xyz
token: ${{ secrets.RELEASE_TOKEN_GITEA }}
name: ${{ github.ref_name }}
tag_name: ${{ github.ref_name }}
body_path: release-notes.md
files: |
dist/*.tar.gz
dist/*.whl
dist/SHA256SUMS

2
.gitignore vendored
View File

@@ -11,4 +11,6 @@ build/
.ruff_cache/
.mypy_cache/
.pytest_cache/
.coverage
.env
.opencode/state

102
AGENTS.md
View File

@@ -24,10 +24,13 @@ uv run rp <file> # Rich-print a file
uv run rp --help # Show help
uv run rp --version # Show version
echo '{"a":1}' | uv run rp # Read from stdin
cat patch.txt | uv run rp --guess-content # Enable heuristic content guessing
```
### Testing
This project does not yet have automated tests. When adding tests:
Run tests with pytest:
```bash
# Run all tests
uv run pytest
@@ -35,8 +38,11 @@ uv run pytest
# Run a single test file
uv run pytest tests/test_detect.py
# Run a single test class
uv run pytest tests/test_detect.py::TestExplicitType
# Run a single test function
uv run pytest tests/test_detect.py::test_detect_json_from_content
uv run pytest tests/test_detect.py::TestExplicitType::test_explicit_overrides_everything
# Run with verbose output
uv run pytest -v
@@ -58,7 +64,7 @@ from __future__ import annotations
import sys
from pathlib import Path
from typing import Annotated, Optional
from typing import Annotated
import typer
from rich.console import Console
@@ -87,6 +93,7 @@ def detect_type(
path: str | None = None,
content: str | None = None,
explicit_type: str | None = None,
guess_content: bool = False,
) -> str:
...
```
@@ -147,38 +154,95 @@ When adding new file types:
2. Use Pygments lexer name as the value
3. For special filenames (e.g., `Dockerfile`), add to `_FILENAME_MAP`
Detection stays conservative by default: explicit type, filename map, extension map,
shebang, and JSON detection all run before falling back to `text`. Heuristic
Pygments content guessing is opt-in via `--guess-content` and should remain a
late fallback.
### Rendering
- `markdown` and `md` → use `rich.markdown.Markdown`
- `json` → use `rich.json.JSON` with fallback to `Syntax`
- All other types → use `rich.syntax.Syntax`
### Pager Behavior
The pager prefers `less -R --mouse` when available (version 543+) for mouse scrolling. Falls back to Rich's `SystemPager` otherwise.
### Version Sync
Version metadata must stay in sync between `pyproject.toml` and `src/rp/__init__.py`.
## Project Structure
```
rich-viewer/
├── pyproject.toml # Project config, dependencies, entry point
├── uv.lock # Lockfile (commit this)
rich-print/
├── pyproject.toml # Project config, dependencies, entry point
├── uv.lock # Lockfile (commit this)
├── .gitignore
├── README.md
├── AGENTS.md # This file
── src/
└── rp/
├── __init__.py # Version metadata
├── cli.py # Typer CLI entry point
├── detect.py # File type detection
├── render.py # Rendering logic
── py.typed # PEP 561 marker
├── AGENTS.md # This file
── src/
└── rp/
├── __init__.py # Version metadata
├── cli.py # Typer CLI entry point
├── detect.py # File type detection
├── render.py # Rendering logic
── pager.py # Pager integration
│ └── py.typed # PEP 561 marker
└── tests/
├── conftest.py
├── test_cli.py
├── test_detect.py
├── test_render.py
└── test_pager.py
```
## Dependencies
- `rich>=13.0` — Terminal rendering (Markdown, JSON, Syntax)
- `pygments>=2.13.0` — Lexer detection and syntax lexers
- `typer>=0.12` — CLI framework
Both are runtime dependencies. No dev dependencies are currently specified.
Runtime dependencies. Dev dependencies: `ruff>=0.9`, `mypy>=1.14`, `pytest>=8.0`, `pytest-cov>=5.0`, `types-pygments>=2.19`.
## Git Conventions
- Write clear, imperative commit messages
- Reference issues/PRs when applicable
- Keep commits focused (one logical change per commit)
## Git Workflow
### Branches
- **Long-lived branches**
- `main` — stable release branch; only updated via merges from `dev`
- `dev` — integration branch for day-to-day development
- **Short-lived branches**
- `feature/<topic>` — new features
- `fix/<topic>` — bug fixes
- `docs/<topic>` — documentation changes
- `chore/<topic>` — maintenance, tooling, CI
- `refactor/<topic>` — refactoring without behavior change
- `test/<topic>` — adding or improving tests
### Branching Rules
- Start normal work from the latest `dev`
- Keep branches short-lived and focused on one logical change
- Do not commit directly to `main` or `dev` unless explicitly requested
- Delete the short-lived branch after it is merged
### Pull Requests
- Open normal PRs into `dev`, not `main`
- Use **squash merge** for PRs into `dev`
- Ensure CI passes before merging
- Keep PRs focused; split unrelated changes into separate PRs
### Release Flow
- Merge `dev` into `main` when changes are ready to release
- Tag releases from `main` with version numbers (e.g., `v1.2.3`)
- If a hotfix is made directly against `main`, merge it back to `dev`
### Agent Expectations
- Unless the user says otherwise, assume new work should target `dev`
- If a branch must be created, follow the naming rules above
- Do not create or push branches automatically unless the user asks
- Do not commit changes unless the user explicitly asks
## Notes for Agents
- This is a small, focused CLI tool — prefer simplicity over abstraction

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Yunxiao Xu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

120
README.md
View File

@@ -1,3 +1,123 @@
# rp
Rich print files — markdown, JSON, code, and more — beautifully in your terminal.
## Features
- **Syntax highlighting** for 500+ languages via Pygments
- **Markdown rendering** with headings, code blocks, tables, and links
- **JSON formatting** with syntax highlighting
- **Pager integration** with mouse scrolling support
- **Stdin support** for piping content
- **Watch mode** for live file redraws
## Installation
### Install globally with uv
```bash
uv tool install .
```
### Run directly with uv
```bash
uv run rp <file>
```
## Usage
### Basic file input
```bash
rp README.md
rp config.json
rp script.py
```
### Read from stdin
```bash
cat data.json | rp
echo '{"status": "ok"}' | rp
curl -s https://api.example.com | rp
```
### Force file type
```bash
rp --type python script.txt
rp -t json data.txt
```
### Guess from content
```bash
cat patch.txt | rp --guess-content
rp --guess-content snippet.txt
```
### Pager control
```bash
rp --pager README.md # Force pager
rp --no-pager file.py # Disable pager
```
The pager prefers `less -R --mouse` for mouse scrolling when available, otherwise falls back to the system pager.
### Watch a file
```bash
rp --watch README.md
rp --watch --type json data.txt
```
`--watch` is file-only. It does not support stdin or `-`, disables the pager, redraws the screen when the file changes, and keeps retrying if the file becomes missing or temporarily unreadable.
### Show version
```bash
rp --version
```
## Development
### Setup
```bash
uv sync
```
### Testing
```bash
uv run pytest # Run all tests
uv run pytest -v # Verbose output
uv run pytest tests/test_detect.py # Single file
```
### Linting & Type Checking
```bash
uv run ruff check src/
uv run ruff format src/
uv run mypy src/
```
### Build
```bash
uv build
```
## Workflow
- `main` — stable release branch
- `dev` — integration branch for day-to-day work
- Short-lived branches: `feature/<topic>`, `fix/<topic>`, `docs/<topic>`
- Open PRs into `dev` (squash merge); merge `dev` to `main` for releases
## License
MIT

View File

@@ -1,9 +1,10 @@
[project]
name = "rp"
version = "0.2.0"
version = "0.3.1"
description = "Rich print files — markdown, JSON, code, and more — beautifully in your terminal"
readme = "README.md"
license = "MIT"
license-files = ["LICENSE"]
authors = [
{ name = "Yunxiao Xu", email = "xuyunxiao2001@gmail.com" }
]
@@ -12,15 +13,16 @@ classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Utilities",
"Typing :: Typed",
]
requires-python = ">=3.12"
dependencies = [
"pygments>=2.13.0",
"rich>=13.0",
"typer>=0.12",
]
@@ -29,8 +31,21 @@ dependencies = [
rp = "rp.cli:app"
[project.urls]
Homepage = "https://github.com/yunxiaoxu/rich-viewer"
Repository = "https://github.com/yunxiaoxu/rich-viewer"
Homepage = "https://gitea.yunxiao.xyz/YunxiaoXu/rich-print"
Repository = "https://gitea.yunxiao.xyz/YunxiaoXu/rich-print.git"
[dependency-groups]
dev = [
"ruff>=0.9",
"mypy>=1.14",
"pytest>=8.0",
"pytest-cov>=5.0",
"types-pygments>=2.19.0.20260402",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--tb=short -q"
[build-system]
requires = ["uv_build>=0.11.2,<0.12.0"]

View File

@@ -1,4 +1,7 @@
"""rp — Rich print files beautifully in your terminal."""
"""Project metadata for rp."""
__version__ = "0.2.0"
from __future__ import annotations
__version__ = "0.3.1"
__author__ = "Yunxiao Xu"
__license__ = "MIT"

View File

@@ -1,18 +1,23 @@
"""CLI entry point for rp — rich print files beautifully."""
"""CLI entry point for rp."""
from __future__ import annotations
from collections.abc import Callable
import sys
import time
from pathlib import Path
from typing import Annotated, Optional
from typing import Annotated
import typer
from rich.console import Console
from rp import __version__
from rp import __author__, __license__, __version__
from rp.debug import DebugLogger, env_debug_enabled, log_debug, make_debug_logger
from rp.detect import detect_type
from rp.render import RenderOptions, render
_WATCH_POLL_INTERVAL = 1.0
app = typer.Typer(
name="rp",
help="Rich print files — markdown, JSON, code, and more — beautifully in your terminal.",
@@ -22,20 +27,124 @@ app = typer.Typer(
def version_callback(value: bool) -> None:
"""Print version information and exit.
Args:
value: Flag value from typer callback.
"""
if value:
console = Console()
console.print(f"rp {__version__}")
console.print(f"Rich Print (rp) {__version__}")
console.print(f"Copyright (c) 2026 {__author__}")
console.print(f"License: {__license__}")
raise typer.Exit()
def _read_file_content(file: Path) -> str:
"""Read a UTF-8 text file for rendering."""
if not file.exists():
raise FileNotFoundError(f"File not found: {file}")
if file.is_dir():
raise IsADirectoryError(f"Is a directory: {file}")
try:
return file.read_text(encoding="utf-8")
except UnicodeDecodeError as exc:
raise ValueError(f"Cannot read binary file: {file}") from exc
def _render_content(
*,
console: Console,
debug: DebugLogger | None,
content: str,
file_path: str | None,
file_type: str | None,
guess_content: bool,
theme: str,
line_numbers: bool | None,
pager: bool | None,
) -> None:
"""Detect type and render content with the standard pipeline."""
detected_type = detect_type(
path=file_path,
content=content,
explicit_type=file_type,
guess_content=guess_content,
debug=debug,
)
options = RenderOptions(
theme=theme,
line_numbers=line_numbers,
pager=pager,
debug=debug,
)
render(content, detected_type, console, options)
def _watch_file(
*,
file: Path,
console: Console,
debug: DebugLogger | None,
file_type: str | None,
guess_content: bool,
theme: str,
line_numbers: bool | None,
sleep: Callable[[float], None] | None = None,
) -> None:
"""Poll a file and redraw when its visible state changes."""
if sleep is None:
sleep = time.sleep
previous_state: tuple[str, str] | None = None
while True:
try:
content = _read_file_content(file)
except (
FileNotFoundError,
IsADirectoryError,
PermissionError,
OSError,
ValueError,
) as exc:
state = ("error", str(exc))
if state != previous_state:
log_debug(debug, f"watch: detected error state {state[1]!r}")
console.clear(home=True)
console.print(f"[red]Error:[/red] {state[1]}")
previous_state = state
else:
state = ("content", content)
if state != previous_state:
log_debug(debug, f"watch: rendering update for {str(file)!r}")
console.clear(home=True)
_render_content(
console=console,
debug=debug,
content=content,
file_path=str(file),
file_type=file_type,
guess_content=guess_content,
theme=theme,
line_numbers=line_numbers,
pager=False,
)
previous_state = state
sleep(_WATCH_POLL_INTERVAL)
@app.command()
def main(
file: Annotated[
Optional[Path],
Path | None,
typer.Argument(help="File to rich-print. Use '-' or omit to read from stdin."),
] = None,
file_type: Annotated[
Optional[str],
str | None,
typer.Option(
"--type", "-t", help="Force file type (e.g. json, python, markdown)."
),
@@ -45,21 +154,35 @@ def main(
typer.Option("--theme", help="Pygments color theme."),
] = "monokai",
line_numbers: Annotated[
Optional[bool],
bool | None,
typer.Option(
"--line-numbers/--no-line-numbers",
help="Show line numbers. Default: auto.",
),
] = None,
pager: Annotated[
Optional[bool],
bool | None,
typer.Option(
"--pager/--no-pager",
help="Use pager for output. Default: auto-detect.",
),
] = None,
watch: Annotated[
bool,
typer.Option(
"--watch/--no-watch",
help="Watch a file for changes and redraw when it updates.",
),
] = False,
guess_content: Annotated[
bool,
typer.Option(
"--guess-content/--no-guess-content",
help="Use heuristic content-based type detection as a fallback.",
),
] = False,
version: Annotated[
Optional[bool],
bool | None,
typer.Option(
"--version",
"-v",
@@ -69,27 +192,55 @@ def main(
),
] = None,
) -> None:
"""Rich print files beautifully in your terminal."""
"""Render a file or stdin input in the terminal.
Args:
file: File to rich-print. Use '-' or omit to read from stdin.
file_type: Force file type (e.g. json, python, markdown).
theme: Pygments color theme.
line_numbers: Show line numbers. Default: auto.
pager: Use pager for output. Default: auto-detect.
watch: Watch a file for changes and redraw when it updates.
guess_content: Use heuristic content-based type detection as a fallback.
version: Show version and exit.
"""
console = Console()
debug = make_debug_logger(Console(stderr=True), env_debug_enabled())
if watch and (file is None or str(file) == "-"):
console.print("[red]Error:[/red] --watch only supports file input.")
raise typer.Exit(code=1)
if file is None or str(file) == "-":
log_debug(debug, "input: reading from stdin")
if sys.stdin.isatty():
console.print("[dim]Reading from stdin. Press Ctrl+D to end.[/dim]")
content = sys.stdin.read()
file_path = None
else:
if not file.exists():
console.print(f"[red]Error:[/red] File not found: {file}")
raise typer.Exit(code=1)
if file.is_dir():
console.print(f"[red]Error:[/red] Is a directory: {file}")
raise typer.Exit(code=1)
log_debug(debug, f"input: reading file {str(file)!r}")
if watch:
log_debug(debug, f"watch: polling {str(file)!r}")
_watch_file(
file=file,
console=console,
debug=debug,
file_type=file_type,
guess_content=guess_content,
theme=theme,
line_numbers=line_numbers,
)
raise typer.Exit()
try:
content = file.read_text(encoding="utf-8")
except UnicodeDecodeError:
console.print(f"[red]Error:[/red] Cannot read binary file: {file}")
raise typer.Exit(code=1)
except (PermissionError, OSError) as e:
content = _read_file_content(file)
except (
FileNotFoundError,
IsADirectoryError,
PermissionError,
OSError,
ValueError,
) as e:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(code=1)
file_path = str(file)
@@ -98,14 +249,17 @@ def main(
console.print("[dim]Empty input, nothing to display.[/dim]")
raise typer.Exit()
file_type = detect_type(path=file_path, content=content, explicit_type=file_type)
options = RenderOptions(
_render_content(
console=console,
debug=debug,
content=content,
file_path=file_path,
file_type=file_type,
guess_content=guess_content,
theme=theme,
line_numbers=line_numbers,
pager=pager,
)
render(content, file_type, console, options)
if __name__ == "__main__":

40
src/rp/debug.py Normal file
View File

@@ -0,0 +1,40 @@
"""Debug logging helpers for rp."""
from __future__ import annotations
import os
from collections.abc import Callable, Mapping
from rich.console import Console
from rich.text import Text
DEBUG_ENV_VAR = "RP_DEBUG"
_FALSE_DEBUG_VALUES = {"", "0", "false", "no", "off"}
type DebugLogger = Callable[[str], None]
def env_debug_enabled(env: Mapping[str, str] | None = None) -> bool:
"""Return whether debug logging is enabled via ``RP_DEBUG``."""
source = os.environ if env is None else env
value = source.get(DEBUG_ENV_VAR)
if value is None:
return False
return value.strip().lower() not in _FALSE_DEBUG_VALUES
def make_debug_logger(console: Console, enabled: bool) -> DebugLogger | None:
"""Return a console-backed debug logger when debug mode is enabled."""
if not enabled:
return None
def _log(message: str) -> None:
console.print(Text.assemble(("[rp debug] ", "dim"), message))
return _log
def log_debug(debug: DebugLogger | None, message: str) -> None:
"""Emit a debug line when a debug logger is configured."""
if debug is not None:
debug(message)

View File

@@ -3,8 +3,15 @@
from __future__ import annotations
import json
import shlex
from numbers import Real
from pathlib import Path
from pygments.lexers import guess_lexer
from pygments.util import ClassNotFound
from rp.debug import DebugLogger, log_debug
EXTENSION_MAP: dict[str, str] = {
".md": "markdown",
".json": "json",
@@ -52,33 +59,242 @@ EXTENSION_MAP: dict[str, str] = {
_FILENAME_MAP: dict[str, str] = {
"Dockerfile": "docker",
"Makefile": "make",
".bashrc": "bash",
".zshrc": "bash",
".profile": "bash",
}
_SHEBANG_MAP: dict[str, str] = {
"python": "python",
"python3": "python",
"python2": "python",
"sh": "bash",
"bash": "bash",
"zsh": "bash",
}
_ENV_OPTIONS_WITH_ARG = {
"-C",
"-u",
"--argv0",
"--block-signal",
"--chdir",
"--default-signal",
"--ignore-signal",
"--unset",
}
_ENV_LONG_OPTIONS_WITH_ARG = {
opt.lstrip("-") for opt in _ENV_OPTIONS_WITH_ARG if opt.startswith("--")
}
def _is_env_assignment(part: str) -> bool:
"""Return whether a token looks like an env-style variable assignment."""
name, separator, _ = part.partition("=")
return (
bool(separator)
and bool(name)
and (name[0].isalpha() or name[0] == "_")
and all(char.isalnum() or char == "_" for char in name[1:])
)
def _detect_env_program(parts: list[str]) -> str | None:
"""Return the actual program name from an env-based shebang."""
remaining = parts[1:]
while remaining:
part = remaining.pop(0)
if part.startswith("--split-string="):
try:
remaining = shlex.split(part.partition("=")[2]) + remaining
except ValueError:
return None
continue
if part in {"-S", "--split-string"}:
if not remaining:
return None
try:
remaining = shlex.split(remaining.pop(0)) + remaining
except ValueError:
return None
continue
if part.startswith("-S"):
try:
remaining = shlex.split(part[2:]) + remaining
except ValueError:
return None
continue
if part == "--":
if not remaining:
return None
return Path(remaining[0]).name
if part in _ENV_OPTIONS_WITH_ARG:
if not remaining:
return None
remaining.pop(0)
continue
if any(part.startswith(f"{option}=") for option in _ENV_LONG_OPTIONS_WITH_ARG):
continue
if part.startswith("-"):
continue
if _is_env_assignment(part):
continue
return Path(part).name
return None
def _detect_shebang(content: str) -> str | None:
"""Return a lexer from the first shebang line when recognized."""
lines = content.splitlines()
first_line = lines[0] if lines else content
if not first_line.startswith("#!"):
return None
command = first_line[2:].strip()
if not command:
return None
try:
parts = shlex.split(command)
except ValueError:
return None
if not parts:
return None
program = Path(parts[0]).name
if program == "env":
env_program = _detect_env_program(parts)
if env_program is None:
return None
program = env_program
return _SHEBANG_MAP.get(program)
def _detect_pygments_content(
content: str,
debug: DebugLogger | None = None,
) -> str | None:
"""Return a lexer guessed from content when Pygments is confident."""
try:
lexer = guess_lexer(content)
except ClassNotFound:
log_debug(debug, "type: Pygments could not guess a lexer from content")
return None
aliases: list[str] = getattr(lexer, "aliases", [])
alias = aliases[0] if aliases else None
analyse_text = getattr(lexer, "analyse_text", None)
score_value = analyse_text(content) if callable(analyse_text) else 0.0
score = float(score_value) if isinstance(score_value, Real) else 0.0
guessed = alias if alias is not None else lexer.__class__.__name__
log_debug(debug, f"type: content guess -> lexer={guessed!r}, score={score:.3f}")
if alias is None:
log_debug(debug, "type: rejected content guess because the lexer has no alias")
return None
if alias == "text":
log_debug(
debug,
"type: rejected content guess because the lexer resolved to plain text",
)
return None
if score < 0.5:
log_debug(
debug,
"type: rejected content guess because the score is below 0.500",
)
return None
log_debug(debug, f"type: inferred {alias!r} from heuristic content guessing")
return alias
def detect_type(
path: str | None = None,
content: str | None = None,
explicit_type: str | None = None,
guess_content: bool = False,
debug: DebugLogger | None = None,
) -> str:
"""Detect the file type for syntax highlighting.
Detection priority:
1. Explicit type if provided
2. Filename-based mapping (for example, Dockerfile)
3. Extension-based mapping
4. Shebang-based mapping for script content
5. JSON if content starts with '{' or '[' and parses successfully
6. Heuristic content guessing when enabled
7. Fallback to 'text'
Args:
path: File path for filename and extension detection.
content: File content for JSON detection.
explicit_type: Override auto-detection with explicit type.
guess_content: Enable heuristic content-based lexer guessing.
debug: Optional debug logger for inference details.
Returns:
Pygments lexer name for the detected file type.
"""
if explicit_type is not None:
log_debug(debug, f"type: using explicit override {explicit_type!r}")
return explicit_type
if path is not None:
p = Path(path)
name = p.name
if name in _FILENAME_MAP:
return _FILENAME_MAP[name]
detected = _FILENAME_MAP[name]
log_debug(debug, f"type: inferred {detected!r} from filename {name!r}")
return detected
suffix = p.suffix.lower()
if suffix in EXTENSION_MAP:
return EXTENSION_MAP[suffix]
detected = EXTENSION_MAP[suffix]
log_debug(debug, f"type: inferred {detected!r} from extension {suffix!r}")
return detected
if content is not None:
stripped = content.lstrip()
if stripped and stripped[0] in ("{", "["):
shebang_type = _detect_shebang(content)
if shebang_type is not None:
log_debug(debug, f"type: inferred {shebang_type!r} from shebang")
return shebang_type
stripped = content.strip()
if stripped and (
(stripped[0] == "{" and stripped[-1] == "}")
or (stripped[0] == "[" and stripped[-1] == "]")
):
try:
json.loads(stripped[:1000])
json.loads(stripped)
log_debug(debug, "type: inferred 'json' from JSON content")
return "json"
except (json.JSONDecodeError, ValueError):
pass
log_debug(debug, "type: JSON-like content failed to parse")
if guess_content:
pygments_type = _detect_pygments_content(content, debug=debug)
if pygments_type is not None:
return pygments_type
else:
log_debug(debug, "type: skipped heuristic content guessing")
log_debug(debug, "type: fell back to 'text'")
return "text"

View File

@@ -1,40 +1,101 @@
"""Custom pager with mouse scroll support."""
"""Less-based pager integration with mouse support and fallback."""
from __future__ import annotations
import re
import signal
import shutil
import subprocess
from rich.pager import Pager, SystemPager
from rp.debug import DebugLogger, log_debug
def _is_mouse_capable_less() -> bool:
def _mouse_capable_less(debug: DebugLogger | None = None) -> str | None:
"""Return a ``less`` path that likely supports ``--mouse``.
Mouse support was added in ``less`` 543. This probe is best-effort: it
checks that ``less`` exists, asks for its version string, and extracts the
version number from the leading ``less <version>`` banner. Any unexpected
output is treated as unsupported so the caller can fall back safely.
Returns:
The resolved ``less`` executable path when it appears mouse-capable,
otherwise ``None``.
"""
less = shutil.which("less")
if less is None:
return False
log_debug(debug, "pager: 'less' was not found")
return None
try:
result = subprocess.run(
[less, "--version"], capture_output=True, text=True, timeout=5
)
version_str = result.stdout or result.stderr or ""
return "GNU" in version_str or "less" in version_str.lower()
except (subprocess.TimeoutExpired, OSError):
return False
log_debug(debug, "pager: failed to probe the installed 'less' binary")
return None
version_str = result.stdout or result.stderr or ""
match = re.search(r"\bless\s+(\d+)\b", version_str, re.IGNORECASE)
if match is None:
log_debug(debug, "pager: could not parse the installed 'less' version")
return None
version = int(match.group(1))
if version < 543:
log_debug(debug, f"pager: installed 'less' {version} is too old for --mouse")
return None
log_debug(debug, f"pager: using 'less' {version} from {less!r}")
return less
class MousePager(Pager):
"""Pager that prefers ``less --mouse`` and falls back to the system pager."""
"""Pager that prefers ``less -R --mouse`` for mouse scrolling.
``less`` is the prerequisite for mouse-enabled paging in this project. If
``less`` is missing, too old, cannot be launched, or exits with a non-zero
status, we fall back to Rich's ``SystemPager``. A user interrupt does not
trigger a second pager session.
"""
def __init__(self, debug: DebugLogger | None = None) -> None:
"""Initialize the pager with an optional debug logger."""
self._debug = debug
def show(self, content: str) -> None:
if _is_mouse_capable_less():
try:
subprocess.run(
["less", "-R", "--mouse"],
input=content,
text=True,
check=True,
)
return
except (subprocess.CalledProcessError, FileNotFoundError):
pass
SystemPager().show(content)
"""Display rendered content in ``less -R --mouse`` when possible.
Args:
content: Fully rendered text from Rich.
"""
less = _mouse_capable_less(self._debug)
if less is None:
log_debug(self._debug, "pager: falling back to Rich's SystemPager")
SystemPager().show(content)
return
try:
log_debug(self._debug, "pager: launching 'less -R --mouse'")
result = subprocess.run(
[less, "-R", "--mouse"],
input=content,
text=True,
)
except OSError:
log_debug(self._debug, "pager: failed to launch 'less', using SystemPager")
SystemPager().show(content)
return
if result.returncode in (-signal.SIGINT, 128 + signal.SIGINT):
log_debug(self._debug, "pager: interrupted by user")
return
if result.returncode != 0:
log_debug(
self._debug,
f"pager: 'less' exited with status {result.returncode}, using SystemPager",
)
SystemPager().show(content)

View File

@@ -4,59 +4,119 @@ from __future__ import annotations
from dataclasses import dataclass
from rich.console import Console
from rich.console import Console, RenderableType
from rich.json import JSON as RichJSON
from rich.markdown import Markdown
from rich.padding import Padding
from rich.syntax import Syntax
from rp.debug import DebugLogger, log_debug
from rp.pager import MousePager
@dataclass
class RenderOptions:
"""Rendering options for file display.
Attributes:
theme: Pygments color theme name.
line_numbers: Show line numbers. ``None`` enables them for code only.
pager: Use pager for output. ``None`` auto-detects terminal output.
debug: Optional debug logger for render and pager decisions.
"""
theme: str = "monokai"
line_numbers: bool | None = None
pager: bool | None = None
debug: DebugLogger | None = None
_MARKDOWN_TYPES = {"markdown", "md"}
_JSON_TYPES = {"json"}
_MARKDOWN_TYPES: set[str] = {"markdown", "md"}
_JSON_TYPES: set[str] = {"json"}
def render(
content: str, file_type: str, console: Console, options: RenderOptions
) -> None:
"""Render content to the console with syntax highlighting.
Renders Markdown with Rich Markdown, JSON with Rich JSON with a Syntax
fallback, and all other types with Pygments Syntax highlighting.
Args:
content: File content to render.
file_type: Pygments lexer name.
console: Rich console to render to.
options: Rendering options (theme, line numbers, pager).
"""
line_numbers = options.line_numbers
if line_numbers is None:
line_numbers = file_type not in _MARKDOWN_TYPES and file_type not in _JSON_TYPES
log_debug(
options.debug,
f"render: auto-set line_numbers={line_numbers} for {file_type!r}",
)
use_pager = options.pager
if use_pager is None:
import sys
use_pager = sys.stdout.isatty()
stdout_is_tty = sys.stdout.isatty()
use_pager = stdout_is_tty
log_debug(
options.debug,
f"pager: auto-detected {'enabled' if use_pager else 'disabled'} "
f"because stdout is {'a' if stdout_is_tty else 'not a'} TTY",
)
else:
log_debug(
options.debug,
f"pager: explicitly {'enabled' if use_pager else 'disabled'}",
)
renderable: RenderableType
if file_type in _MARKDOWN_TYPES:
renderable = Markdown(content)
elif file_type in _JSON_TYPES:
try:
renderable = RichJSON(content)
except (SyntaxError, ValueError):
log_debug(options.debug, "render: invalid JSON, falling back to syntax")
renderable = Syntax(
content, "json", theme=options.theme, line_numbers=line_numbers
content,
"json",
theme=options.theme,
line_numbers=line_numbers,
word_wrap=True,
)
else:
try:
renderable = Syntax(
content, file_type, theme=options.theme, line_numbers=line_numbers
content,
file_type,
theme=options.theme,
line_numbers=line_numbers,
word_wrap=True,
)
except Exception:
log_debug(
options.debug,
f"render: unknown lexer {file_type!r}, falling back to plain text",
)
renderable = Syntax(
content, "text", theme=options.theme, line_numbers=line_numbers
content,
"text",
theme=options.theme,
line_numbers=line_numbers,
word_wrap=True,
)
padded = Padding(renderable, (1, 2))
if use_pager:
with console.pager(pager=MousePager(), styles=True):
console.print(renderable)
log_debug(options.debug, "pager: opening pager")
with console.pager(pager=MousePager(debug=options.debug), styles=True):
console.print(padded)
else:
console.print(renderable)
log_debug(options.debug, "pager: writing directly to the console")
console.print(padded)

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'

419
tests/test_cli.py Normal file
View File

@@ -0,0 +1,419 @@
"""Tests for the CLI entry point."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pytest
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,
guess_content=False,
debug=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_watch_rejects_stdin(self) -> None:
result = runner.invoke(app, ["--watch"], input="hello\n")
assert result.exit_code == 1
assert "--watch only supports file input" in result.output
def test_watch_rejects_dash_input(self) -> None:
result = runner.invoke(app, ["-", "--watch"], input="hello\n")
assert result.exit_code == 1
assert "--watch only supports file input" in result.output
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,
guess_content=False,
debug=None,
)
def test_debug_env_enables_debug_logger(self) -> 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", env={"RP_DEBUG": "1"}
)
_, kwargs = mock_detect_type.call_args
assert kwargs["debug"] is not None
def test_debug_output_stays_on_stderr(self, tmp_file) -> None:
file = tmp_file("# Hello\n", suffix=".md")
result = runner.invoke(app, [str(file), "--no-pager"], env={"RP_DEBUG": "1"})
normalized_stderr = result.stderr.replace("\n", "")
assert result.exit_code == 0
assert "[rp debug]" not in result.stdout
assert "Hello" in result.stdout
assert "[rp debug] input: reading file" in result.stderr
assert repr(str(file)) in normalized_stderr
assert (
"[rp debug] type: inferred 'markdown' from extension '.md'" in result.stderr
)
assert (
"[rp debug] render: auto-set line_numbers=False for 'markdown'"
in result.stderr
)
assert "[rp debug] pager: explicitly disabled" in result.stderr
assert "[rp debug] pager: writing directly to the console" in result.stderr
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"
assert kwargs["guess_content"] is False
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 TestGuessContentOption:
"""Tests for the guess-content flag."""
def test_guess_content_is_forwarded(self) -> None:
with patch("rp.cli.detect_type") as mock_detect_type:
mock_detect_type.return_value = "diff"
with patch("rp.cli.render"):
runner.invoke(app, ["--guess-content", "--no-pager"], input="hello\n")
_, kwargs = mock_detect_type.call_args
assert kwargs["guess_content"] is True
def test_guess_content_disabled_by_default(self) -> 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")
_, kwargs = mock_detect_type.call_args
assert kwargs["guess_content"] is False
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
class TestWatchMode:
"""Tests for the watch mode loop."""
def test_watch_disables_pager(self, tmp_file) -> None:
file = tmp_file("x = 1\n", suffix=".py")
with patch("rp.cli.detect_type", return_value="python"):
with patch("rp.cli.time.sleep", side_effect=KeyboardInterrupt):
with patch("rp.cli.render") as mock_render:
with pytest.raises(KeyboardInterrupt):
main(file=file, pager=True, watch=True)
call_args = mock_render.call_args
assert call_args is not None
options = call_args[0][3]
assert options.pager is False
def test_watch_rerenders_on_file_change(self, tmp_file) -> None:
file = tmp_file("x = 1\n", suffix=".py")
sleep_calls = 0
def fake_sleep(_: float) -> None:
nonlocal sleep_calls
sleep_calls += 1
if sleep_calls == 1:
file.write_text("x = 2\n", encoding="utf-8")
return
raise KeyboardInterrupt
with patch("rp.cli.detect_type", return_value="python"):
with patch("rp.cli.time.sleep", side_effect=fake_sleep):
with patch("rp.cli.render") as mock_render:
with pytest.raises(KeyboardInterrupt):
main(file=file, watch=True)
assert [call.args[0] for call in mock_render.call_args_list] == [
"x = 1\n",
"x = 2\n",
]
def test_watch_retries_after_missing_file_and_recovers(self, tmp_file) -> None:
file = tmp_file("x = 1\n", suffix=".py")
sleep_calls = 0
def fake_sleep(_: float) -> None:
nonlocal sleep_calls
sleep_calls += 1
if sleep_calls == 1:
file.unlink()
return
if sleep_calls == 2:
file.write_text("x = 3\n", encoding="utf-8")
return
raise KeyboardInterrupt
with patch("rp.cli.detect_type", return_value="python"):
with patch("rp.cli.time.sleep", side_effect=fake_sleep):
with patch("rp.cli.Console.print") as mock_print:
with patch("rp.cli.render") as mock_render:
with pytest.raises(KeyboardInterrupt):
main(file=file, watch=True)
assert [call.args[0] for call in mock_render.call_args_list] == [
"x = 1\n",
"x = 3\n",
]
mock_print.assert_any_call(f"[red]Error:[/red] File not found: {file}")
def test_watch_recovers_when_file_is_missing_at_startup(
self, tmp_path: Path
) -> None:
file = tmp_path / "startup.py"
sleep_calls = 0
def fake_sleep(_: float) -> None:
nonlocal sleep_calls
sleep_calls += 1
if sleep_calls == 1:
file.write_text("x = 4\n", encoding="utf-8")
return
raise KeyboardInterrupt
with patch("rp.cli.detect_type", return_value="python"):
with patch("rp.cli.time.sleep", side_effect=fake_sleep):
with patch("rp.cli.Console.print") as mock_print:
with patch("rp.cli.render") as mock_render:
with pytest.raises(KeyboardInterrupt):
main(file=file, watch=True)
assert [call.args[0] for call in mock_render.call_args_list] == ["x = 4\n"]
mock_print.assert_any_call(f"[red]Error:[/red] File not found: {file}")

60
tests/test_debug.py Normal file
View File

@@ -0,0 +1,60 @@
"""Tests for debug helpers."""
from __future__ import annotations
import io
from rich.console import Console
from rp.debug import DEBUG_ENV_VAR, env_debug_enabled, log_debug, make_debug_logger
class TestEnvDebugEnabled:
"""Environment-based debug mode toggles."""
def test_disabled_by_default(self) -> None:
assert env_debug_enabled({}) is False
def test_false_like_values_disable_debug(self) -> None:
for value in ["", "0", "false", "no", "off"]:
assert env_debug_enabled({DEBUG_ENV_VAR: value}) is False
def test_false_like_values_are_trimmed_and_case_insensitive(self) -> None:
for value in [" False ", "\tNO\n", " Off "]:
assert env_debug_enabled({DEBUG_ENV_VAR: value}) is False
def test_truthy_values_enable_debug(self) -> None:
assert env_debug_enabled({DEBUG_ENV_VAR: "1"}) is True
assert env_debug_enabled({DEBUG_ENV_VAR: "true"}) is True
def test_truthy_values_are_trimmed_and_case_insensitive(self) -> None:
for value in [" TRUE ", " Yes ", "\ton\n"]:
assert env_debug_enabled({DEBUG_ENV_VAR: value}) is True
class TestMakeDebugLogger:
"""Console-backed debug logger output."""
def test_logs_visible_prefix(self) -> None:
stream = io.StringIO()
console = Console(file=stream, force_terminal=False)
logger = make_debug_logger(console, enabled=True)
assert logger is not None
logger("hello")
assert stream.getvalue() == "[rp debug] hello\n"
class TestLogDebug:
"""Nil-safe debug logger forwarding."""
def test_noop_when_logger_is_none(self) -> None:
log_debug(None, "hello")
def test_forwards_to_logger(self) -> None:
messages: list[str] = []
log_debug(messages.append, "hello")
assert messages == ["hello"]

259
tests/test_detect.py Normal file
View File

@@ -0,0 +1,259 @@
"""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") == "bash"
def test_zshrc(self) -> None:
assert detect_type(path=".zshrc") == "bash"
def test_profile(self) -> None:
assert detect_type(path=".profile") == "bash"
@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 TestShebangDetection:
"""Priority 4: detect common scripts from shebangs."""
def test_env_python3_shebang(self) -> None:
assert detect_type(content="#!/usr/bin/env python3\nprint('hi')\n") == "python"
def test_env_python3_with_assignment(self) -> None:
assert (
detect_type(content="#!/usr/bin/env FOO=1 python3\nprint('hi')\n")
== "python"
)
def test_env_ignore_environment_before_program(self) -> None:
assert detect_type(content="#!/usr/bin/env -i bash\necho hi\n") == "bash"
def test_env_split_string_shebang(self) -> None:
assert (
detect_type(content="#!/usr/bin/env -S python3 -u\nprint('hi')\n")
== "python"
)
def test_env_compact_split_string_shebang(self) -> None:
assert (
detect_type(content="#!/usr/bin/env -Spython3 -u\nprint('hi')\n")
== "python"
)
def test_env_quoted_split_string_shebang(self) -> None:
assert (
detect_type(
content='#!/usr/bin/env --split-string="python3 -u"\nprint("hi")\n'
)
== "python"
)
def test_env_long_option_with_equals(self) -> None:
assert (
detect_type(content="#!/usr/bin/env --ignore-signal=TERM bash\necho hi\n")
== "bash"
)
def test_direct_python_shebang(self) -> None:
assert detect_type(content="#!/usr/bin/python3\nprint('hi')\n") == "python"
def test_bash_shebang(self) -> None:
assert detect_type(content="#!/bin/bash\necho hi\n") == "bash"
def test_sh_shebang(self) -> None:
assert detect_type(content="#!/bin/sh\necho hi\n") == "bash"
def test_zsh_shebang(self) -> None:
assert detect_type(content="#!/bin/zsh\necho hi\n") == "bash"
def test_extension_wins_over_shebang(self) -> None:
assert (
detect_type(path="script.py", content="#!/bin/bash\necho hi\n") == "python"
)
def test_filename_wins_over_shebang(self) -> None:
assert detect_type(path=".bashrc", content="#!/usr/bin/env python3\n") == "bash"
def test_unrecognized_shebang_falls_through(self) -> None:
assert detect_type(content="#!/usr/bin/env ruby\nputs 'hi'\n") == "text"
class TestContentJsonDetection:
"""Priority 5: 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"
def test_shebang_takes_priority_over_json(self) -> None:
assert detect_type(content='#!/usr/bin/env python3\n{"a": 1}\n') == "python"
class TestGuessContent:
"""Priority 6: optional heuristic content guessing."""
def test_guess_content_disabled_by_default(self) -> None:
diff = "--- a/file\n+++ b/file\n@@ -1 +1 @@\n-old\n+new\n"
assert detect_type(content=diff) == "text"
def test_guess_content_detects_diff(self) -> None:
diff = "--- a/file\n+++ b/file\n@@ -1 +1 @@\n-old\n+new\n"
assert detect_type(content=diff, guess_content=True) == "diff"
def test_guess_content_rejects_low_confidence_false_positive(self) -> None:
python_snippet = "def f():\n return 1\n"
assert detect_type(content=python_snippet, guess_content=True) == "text"
def test_json_still_wins_over_guess_content(self, sample_json: str) -> None:
assert detect_type(content=sample_json, guess_content=True) == "json"
class TestDebugLogging:
"""Debug logging describes inference decisions."""
def test_logs_extension_source(self) -> None:
messages: list[str] = []
assert detect_type(path="script.py", debug=messages.append) == "python"
assert messages == ["type: inferred 'python' from extension '.py'"]
def test_logs_content_guess_and_score(self) -> None:
messages: list[str] = []
diff = "--- a/file\n+++ b/file\n@@ -1 +1 @@\n-old\n+new\n"
assert (
detect_type(content=diff, guess_content=True, debug=messages.append)
== "diff"
)
assert any(
"type: content guess -> lexer='diff', score=" in message
for message in messages
)
assert "type: inferred 'diff' from heuristic content guessing" in messages
class TestFallback:
"""Priority 7: 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"

188
tests/test_pager.py Normal file
View File

@@ -0,0 +1,188 @@
"""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_logs_less_not_found(self) -> None:
messages: list[str] = []
with patch("rp.pager.shutil.which", return_value=None):
assert _mouse_capable_less(messages.append) is None
assert messages == ["pager: 'less' was not found"]
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_logs_system_pager_fallback(self) -> None:
messages: list[str] = []
pager = MousePager(debug=messages.append)
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")
assert messages == ["pager: falling back to Rich's SystemPager"]
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")

327
tests/test_render.py Normal file
View File

@@ -0,0 +1,327 @@
"""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
def test_default_debug_is_none(self) -> None:
opts = RenderOptions()
assert opts.debug is None
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,
word_wrap=True,
)
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,
word_wrap=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,
"word_wrap": 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,
"word_wrap": 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,
word_wrap=True,
)
class TestCodeWrap:
"""Code wrapping (word_wrap) for syntax-highlighted output."""
def test_python_syntax_has_word_wrap(
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)
mock_syntax.assert_called_once()
assert mock_syntax.call_args.kwargs["word_wrap"] is True
def test_long_line_wraps_in_output(self, capture_console) -> None:
long_line = "x = " + "'a' * " * 200 + "1"
opts = RenderOptions(pager=False, line_numbers=False)
render(long_line, "python", capture_console, opts)
output = capture_console.file.getvalue()
assert len(output) > 0
lines = output.split("\n")
non_empty = [line for line in lines if line.strip()]
assert len(non_empty) > 1, "Long line should wrap across multiple visual lines"
def test_text_fallback_has_word_wrap(self, capture_console) -> None:
opts = RenderOptions(pager=False)
def raise_on_bad_lexer(*args, **kwargs):
if args[1] == "unknown_xyz":
raise Exception("bad lexer")
return Syntax(*args, **kwargs)
with patch("rp.render.Syntax", side_effect=raise_on_bad_lexer) as mock_syntax:
render("some text", "unknown_xyz", capture_console, opts)
fallback_call = mock_syntax.call_args_list[-1]
assert fallback_call.args[1] == "text"
assert fallback_call.kwargs["word_wrap"] is True
def test_json_fallback_has_word_wrap(self, capture_console) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
render("{invalid}", "json", capture_console, opts)
mock_syntax.assert_called_once()
assert mock_syntax.call_args.kwargs["word_wrap"] is 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()
def test_logs_auto_pager_decision(
self, capture_console, sample_python: str
) -> None:
messages: list[str] = []
opts = RenderOptions(pager=None, debug=messages.append)
with patch("sys.stdout.isatty", return_value=False):
render(sample_python, "python", capture_console, opts)
assert "pager: auto-detected disabled because stdout is not a TTY" in messages
assert "pager: writing directly to the console" in messages
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

339
uv.lock generated
View File

@@ -32,6 +32,159 @@ 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"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" },
{ url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" },
{ url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" },
{ url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" },
{ url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" },
{ url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" },
{ url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" },
{ url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" },
{ url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" },
{ url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" },
{ url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" },
{ url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" },
{ url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" },
{ url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" },
{ url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" },
{ url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" },
{ url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" },
{ url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" },
{ url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" },
{ url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" },
{ url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" },
{ url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" },
{ url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" },
{ url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" },
{ url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" },
{ url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" },
{ url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" },
{ url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" },
{ url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" },
{ url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" },
{ url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" },
{ url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" },
{ url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" },
{ url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" },
{ url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" },
{ url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" },
{ url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" },
{ url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" },
{ url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" },
{ url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" },
{ url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" },
{ url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" },
{ url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" },
{ url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" },
{ url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" },
{ url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" },
{ url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" },
{ url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" },
{ url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" },
{ url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" },
{ url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.0.0"
@@ -53,6 +206,85 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "mypy"
version = "1.20.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/b0089fe7fef0a994ae5ee07029ced0526082c6cfaaa4c10d40a10e33b097/mypy-1.20.0.tar.gz", hash = "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3", size = 3815028, upload-time = "2026-03-31T16:55:14.959Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/dd/3afa29b58c2e57c79116ed55d700721c3c3b15955e2b6251dd165d377c0e/mypy-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:002b613ae19f4ac7d18b7e168ffe1cb9013b37c57f7411984abbd3b817b0a214", size = 14509525, upload-time = "2026-03-31T16:55:01.824Z" },
{ url = "https://files.pythonhosted.org/packages/54/eb/227b516ab8cad9f2a13c5e7a98d28cd6aa75e9c83e82776ae6c1c4c046c7/mypy-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9336b5e6712f4adaf5afc3203a99a40b379049104349d747eb3e5a3aa23ac2e", size = 13326469, upload-time = "2026-03-31T16:51:41.23Z" },
{ url = "https://files.pythonhosted.org/packages/57/d4/1ddb799860c1b5ac6117ec307b965f65deeb47044395ff01ab793248a591/mypy-1.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f13b3e41bce9d257eded794c0f12878af3129d80aacd8a3ee0dee51f3a978651", size = 13705953, upload-time = "2026-03-31T16:48:55.69Z" },
{ url = "https://files.pythonhosted.org/packages/c5/b7/54a720f565a87b893182a2a393370289ae7149e4715859e10e1c05e49154/mypy-1.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9804c3ad27f78e54e58b32e7cb532d128b43dbfb9f3f9f06262b821a0f6bd3f5", size = 14710363, upload-time = "2026-03-31T16:53:26.948Z" },
{ url = "https://files.pythonhosted.org/packages/b2/2a/74810274848d061f8a8ea4ac23aaad43bd3d8c1882457999c2e568341c57/mypy-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:697f102c5c1d526bdd761a69f17c6070f9892eebcb94b1a5963d679288c09e78", size = 14947005, upload-time = "2026-03-31T16:50:17.591Z" },
{ url = "https://files.pythonhosted.org/packages/77/91/21b8ba75f958bcda75690951ce6fa6b7138b03471618959529d74b8544e2/mypy-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ecd63f75fdd30327e4ad8b5704bd6d91fc6c1b2e029f8ee14705e1207212489", size = 10880616, upload-time = "2026-03-31T16:52:19.986Z" },
{ url = "https://files.pythonhosted.org/packages/8a/15/3d8198ef97c1ca03aea010cce4f1d4f3bc5d9849e8c0140111ca2ead9fdd/mypy-1.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:f194db59657c58593a3c47c6dfd7bad4ef4ac12dbc94d01b3a95521f78177e33", size = 9813091, upload-time = "2026-03-31T16:53:44.385Z" },
{ url = "https://files.pythonhosted.org/packages/d6/a7/f64ea7bd592fa431cb597418b6dec4a47f7d0c36325fec7ac67bc8402b94/mypy-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b20c8b0fd5877abdf402e79a3af987053de07e6fb208c18df6659f708b535134", size = 14485344, upload-time = "2026-03-31T16:49:16.78Z" },
{ url = "https://files.pythonhosted.org/packages/bb/72/8927d84cfc90c6abea6e96663576e2e417589347eb538749a464c4c218a0/mypy-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:367e5c993ba34d5054d11937d0485ad6dfc60ba760fa326c01090fc256adf15c", size = 13327400, upload-time = "2026-03-31T16:53:08.02Z" },
{ url = "https://files.pythonhosted.org/packages/ab/4a/11ab99f9afa41aa350178d24a7d2da17043228ea10f6456523f64b5a6cf6/mypy-1.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f799d9db89fc00446f03281f84a221e50018fc40113a3ba9864b132895619ebe", size = 13706384, upload-time = "2026-03-31T16:52:28.577Z" },
{ url = "https://files.pythonhosted.org/packages/42/79/694ca73979cfb3535ebfe78733844cd5aff2e63304f59bf90585110d975a/mypy-1.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555658c611099455b2da507582ea20d2043dfdfe7f5ad0add472b1c6238b433f", size = 14700378, upload-time = "2026-03-31T16:48:45.527Z" },
{ url = "https://files.pythonhosted.org/packages/84/24/a022ccab3a46e3d2cdf2e0e260648633640eb396c7e75d5a42818a8d3971/mypy-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:efe8d70949c3023698c3fca1e94527e7e790a361ab8116f90d11221421cd8726", size = 14932170, upload-time = "2026-03-31T16:49:36.038Z" },
{ url = "https://files.pythonhosted.org/packages/d8/9b/549228d88f574d04117e736f55958bd4908f980f9f5700a07aeb85df005b/mypy-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:f49590891d2c2f8a9de15614e32e459a794bcba84693c2394291a2038bbaaa69", size = 10888526, upload-time = "2026-03-31T16:50:59.827Z" },
{ url = "https://files.pythonhosted.org/packages/91/17/15095c0e54a8bc04d22d4ff06b2139d5f142c2e87520b4e39010c4862771/mypy-1.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:76a70bf840495729be47510856b978f1b0ec7d08f257ca38c9d932720bf6b43e", size = 9816456, upload-time = "2026-03-31T16:49:59.537Z" },
{ url = "https://files.pythonhosted.org/packages/4e/0e/6ca4a84cbed9e62384bc0b2974c90395ece5ed672393e553996501625fc5/mypy-1.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0f42dfaab7ec1baff3b383ad7af562ab0de573c5f6edb44b2dab016082b89948", size = 14483331, upload-time = "2026-03-31T16:52:57.999Z" },
{ url = "https://files.pythonhosted.org/packages/7d/c5/5fe9d8a729dd9605064691816243ae6c49fde0bd28f6e5e17f6a24203c43/mypy-1.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:31b5dbb55293c1bd27c0fc813a0d2bb5ceef9d65ac5afa2e58f829dab7921fd5", size = 13342047, upload-time = "2026-03-31T16:54:21.555Z" },
{ url = "https://files.pythonhosted.org/packages/4c/33/e18bcfa338ca4e6b2771c85d4c5203e627d0c69d9de5c1a2cf2ba13320ba/mypy-1.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49d11c6f573a5a08f77fad13faff2139f6d0730ebed2cfa9b3d2702671dd7188", size = 13719585, upload-time = "2026-03-31T16:51:53.89Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8d/93491ff7b79419edc7eabf95cb3b3f7490e2e574b2855c7c7e7394ff933f/mypy-1.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d3243c406773185144527f83be0e0aefc7bf4601b0b2b956665608bf7c98a83", size = 14685075, upload-time = "2026-03-31T16:54:04.464Z" },
{ url = "https://files.pythonhosted.org/packages/b5/9d/d924b38a4923f8d164bf2b4ec98bf13beaf6e10a5348b4b137eadae40a6e/mypy-1.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a79c1eba7ac4209f2d850f0edd0a2f8bba88cbfdfefe6fb76a19e9d4fe5e71a2", size = 14919141, upload-time = "2026-03-31T16:54:51.785Z" },
{ url = "https://files.pythonhosted.org/packages/59/98/1da9977016678c0b99d43afe52ed00bb3c1a0c4c995d3e6acca1a6ebb9b4/mypy-1.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:00e047c74d3ec6e71a2eb88e9ea551a2edb90c21f993aefa9e0d2a898e0bb732", size = 11050925, upload-time = "2026-03-31T16:51:30.758Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e3/ba0b7a3143e49a9c4f5967dde6ea4bf8e0b10ecbbcca69af84027160ee89/mypy-1.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:931a7630bba591593dcf6e97224a21ff80fb357e7982628d25e3c618e7f598ef", size = 10001089, upload-time = "2026-03-31T16:49:43.632Z" },
{ url = "https://files.pythonhosted.org/packages/12/28/e617e67b3be9d213cda7277913269c874eb26472489f95d09d89765ce2d8/mypy-1.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:26c8b52627b6552f47ff11adb4e1509605f094e29815323e487fc0053ebe93d1", size = 15534710, upload-time = "2026-03-31T16:52:12.506Z" },
{ url = "https://files.pythonhosted.org/packages/6e/0c/3b5f2d3e45dc7169b811adce8451679d9430399d03b168f9b0489f43adaa/mypy-1.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39362cdb4ba5f916e7976fccecaab1ba3a83e35f60fa68b64e9a70e221bb2436", size = 14393013, upload-time = "2026-03-31T16:54:41.186Z" },
{ url = "https://files.pythonhosted.org/packages/a3/49/edc8b0aa145cc09c1c74f7ce2858eead9329931dcbbb26e2ad40906daa4e/mypy-1.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34506397dbf40c15dc567635d18a21d33827e9ab29014fb83d292a8f4f8953b6", size = 15047240, upload-time = "2026-03-31T16:54:31.955Z" },
{ url = "https://files.pythonhosted.org/packages/42/37/a946bb416e37a57fa752b3100fd5ede0e28df94f92366d1716555d47c454/mypy-1.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555493c44a4f5a1b58d611a43333e71a9981c6dbe26270377b6f8174126a0526", size = 15858565, upload-time = "2026-03-31T16:53:36.997Z" },
{ url = "https://files.pythonhosted.org/packages/2f/99/7690b5b5b552db1bd4ff362e4c0eb3107b98d680835e65823fbe888c8b78/mypy-1.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2721f0ce49cb74a38f00c50da67cb7d36317b5eda38877a49614dc018e91c787", size = 16087874, upload-time = "2026-03-31T16:52:48.313Z" },
{ url = "https://files.pythonhosted.org/packages/aa/76/53e893a498138066acd28192b77495c9357e5a58cc4be753182846b43315/mypy-1.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:47781555a7aa5fedcc2d16bcd72e0dc83eb272c10dd657f9fb3f9cc08e2e6abb", size = 12572380, upload-time = "2026-03-31T16:49:52.454Z" },
{ url = "https://files.pythonhosted.org/packages/76/9c/6dbdae21f01b7aacddc2c0bbf3c5557aa547827fdf271770fe1e521e7093/mypy-1.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c70380fe5d64010f79fb863b9081c7004dd65225d2277333c219d93a10dad4dd", size = 10381174, upload-time = "2026-03-31T16:51:20.179Z" },
{ url = "https://files.pythonhosted.org/packages/21/66/4d734961ce167f0fd8380769b3b7c06dbdd6ff54c2190f3f2ecd22528158/mypy-1.20.0-py3-none-any.whl", hash = "sha256:a6e0641147cbfa7e4e94efdb95c2dab1aff8cfc159ded13e07f308ddccc8c48e", size = 2636365, upload-time = "2026-03-31T16:51:44.911Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
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"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
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"
@@ -62,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"
@@ -77,19 +339,64 @@ wheels = [
[[package]]
name = "rp"
version = "0.1.0"
version = "0.3.1"
source = { editable = "." }
dependencies = [
{ name = "pygments" },
{ name = "rich" },
{ name = "typer" },
]
[package.dev-dependencies]
dev = [
{ name = "mypy" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "ruff" },
{ name = "types-pygments" },
]
[package.metadata]
requires-dist = [
{ name = "pygments", specifier = ">=2.13.0" },
{ name = "rich", specifier = ">=13.0" },
{ name = "typer", specifier = ">=0.12" },
]
[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" },
{ name = "types-pygments", specifier = ">=2.19.0.20260402" },
]
[[package]]
name = "ruff"
version = "0.15.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" },
{ url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" },
{ url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" },
{ url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" },
{ url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" },
{ url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" },
{ url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" },
{ url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" },
{ url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" },
{ url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" },
{ url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" },
{ url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" },
{ url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" },
{ url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" },
{ url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" },
{ url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" },
{ url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
@@ -113,3 +420,33 @@ sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" },
]
[[package]]
name = "types-docutils"
version = "0.22.3.20260322"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/44/bb/243a87fc1605a4a94c2c343d6dbddbf0d7ef7c0b9550f360b8cda8e82c39/types_docutils-0.22.3.20260322.tar.gz", hash = "sha256:e2450bb997283c3141ec5db3e436b91f0aa26efe35eb9165178ca976ccb4930b", size = 57311, upload-time = "2026-03-22T04:08:44.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/4a/22c090cd4615a16917dff817cbe7c5956da376c961e024c241cd962d2c3d/types_docutils-0.22.3.20260322-py3-none-any.whl", hash = "sha256:681d4510ce9b80a0c6a593f0f9843d81f8caa786db7b39ba04d9fd5480ac4442", size = 91978, upload-time = "2026-03-22T04:08:43.117Z" },
]
[[package]]
name = "types-pygments"
version = "2.19.0.20260402"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "types-docutils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/a8/5834c55d900ce31b31367eedb82e664347dfa551a957b74a0ce0cd9f4f9a/types_pygments-2.19.0.20260402.tar.gz", hash = "sha256:bd26e1f662c9a3b8ea56668ddc099b809ffd54931bee97b15853bc4a8e5d4250", size = 18808, upload-time = "2026-04-02T04:21:13.058Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/f2/b23659df4219fc4f39a45ede527cc209d961c7ff957678536a7a9d9ac9c5/types_pygments-2.19.0.20260402-py3-none-any.whl", hash = "sha256:5b0d863cec1c43ba38c946fb6e89d389c1cf287806f72360336fba4482f8daeb", size = 25672, upload-time = "2026-04-02T04:21:11.916Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]