Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee67bcb768 | |||
| 868656ea12 | |||
| 7df3acde9b | |||
| 579df72869 | |||
| 537238f56f | |||
| 598450d9e2 | |||
| 0f93d3a095 | |||
| d94cf2df30 | |||
| 93c777cf6a | |||
| 37502c3083 | |||
| 63102473da | |||
| 1ac724603a | |||
| 907b911e95 | |||
| 383447d050 | |||
| 695e5b6541 | |||
| f32da588fb | |||
| 0e7f30d6c3 | |||
| 65ff34dd2c | |||
| 217feab7de | |||
| c3a71c2ff9 | |||
| 32a0471910 | |||
| 32a8a1d4ad | |||
| 5935140620 | |||
| 5b336f2ce0 |
64
.github/workflows/ci.yml
vendored
Normal file
64
.github/workflows/ci.yml
vendored
Normal 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
124
.github/workflows/release.yml
vendored
Normal 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
2
.gitignore
vendored
@@ -11,4 +11,6 @@ build/
|
||||
.ruff_cache/
|
||||
.mypy_cache/
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
.env
|
||||
.opencode/state
|
||||
|
||||
102
AGENTS.md
102
AGENTS.md
@@ -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
21
LICENSE
Normal 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
120
README.md
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
204
src/rp/cli.py
204
src/rp/cli.py
@@ -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
40
src/rp/debug.py
Normal 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)
|
||||
228
src/rp/detect.py
228
src/rp/detect.py
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
57
tests/conftest.py
Normal 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
419
tests/test_cli.py
Normal 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
60
tests/test_debug.py
Normal 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
259
tests/test_detect.py
Normal 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
188
tests/test_pager.py
Normal 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
327
tests/test_render.py
Normal 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
339
uv.lock
generated
@@ -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" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user