diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8c0e0d4..1025c04 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,7 +21,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] runs-on: ${{ matrix.os }} steps: - name: Check out repository diff --git a/pyproject.toml b/pyproject.toml index 41e6be7..4985ffd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,18 +24,18 @@ classifiers = [ "Topic :: Utilities", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [] [project.urls] @@ -52,3 +52,6 @@ dev = [ "pytest>=8.3.4", "ruff>=0.9.7", ] + +[tool.ruff.lint.pyupgrade] +keep-runtime-typing = true diff --git a/pytinytex/__init__.py b/pytinytex/__init__.py index 351eae0..f6ece0d 100644 --- a/pytinytex/__init__.py +++ b/pytinytex/__init__.py @@ -5,8 +5,9 @@ import warnings from collections import deque from contextlib import suppress +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as _metadata_version from pathlib import Path -from importlib.metadata import version as _metadata_version, PackageNotFoundError try: __version__ = _metadata_version("PyTinyTeX") @@ -14,9 +15,9 @@ __version__ = "unknown" from .tinytex_download import download_tinytex, DEFAULT_TARGET_FOLDER # noqa -from .log_parser import LogEntry, ParsedLog, parse_log # noqa -from .compiler import CompileResult, compile # noqa -from .doctor import DoctorCheck, DoctorResult, doctor # noqa +from .log_parser import LogEntry, ParsedLog, parse_log +from .compiler import CompileResult, compile +from .doctor import DoctorCheck, DoctorResult, doctor from .tlmgr import ( # noqa install, remove, @@ -53,35 +54,35 @@ __tinytex_path = None __all__ = [ - "download_tinytex", "DEFAULT_TARGET_FOLDER", - "ensure_tinytex_installed", - "get_tinytex_path", + "CompileResult", + "DoctorCheck", + "DoctorResult", + "LogEntry", + "ParsedLog", + "__version__", "clear_path_cache", + "compile", + "doctor", + "download_tinytex", + "ensure_tinytex_installed", "get_engine", - "get_pdflatex_engine", - "get_xelatex_engine", "get_lualatex_engine", "get_pdf_latex_engine", + "get_pdflatex_engine", + "get_tinytex_path", "get_version", + "get_xelatex_engine", + "help", + "info", "install", - "remove", "list_installed", + "parse_log", + "remove", "search", - "info", - "update", - "uninstall", - "help", "shell", - "compile", - "CompileResult", - "LogEntry", - "ParsedLog", - "parse_log", - "doctor", - "DoctorResult", - "DoctorCheck", - "__version__", + "uninstall", + "update", ] diff --git a/pytinytex/cli.py b/pytinytex/cli.py index 2b7aeef..0b20b67 100644 --- a/pytinytex/cli.py +++ b/pytinytex/cli.py @@ -16,7 +16,7 @@ class _CliFormatter(logging.Formatter): def format(self, record): if record.levelno == logging.INFO: return record.getMessage() - return "%s: %s" % (record.levelname.lower(), record.getMessage()) + return f"{record.levelname.lower()}: {record.getMessage()}" def _setup_logging(): @@ -160,7 +160,7 @@ def main(argv=None): loc = " (" + loc + ")" print(" ! " + entry.message + loc) if result.warnings: - print("Warnings: %d" % len(result.warnings)) + print(f"Warnings: {len(result.warnings)}") if result.success: print("Success: " + str(result.pdf_path)) else: @@ -193,7 +193,7 @@ def main(argv=None): result = pytinytex.doctor() for check in result.checks: status = "PASS" if check.passed else "FAIL" - print(" [%s] %s: %s" % (status, check.name, check.message)) + print(f" [{status}] {check.name}: {check.message}") if result.healthy: print("\nAll checks passed.") else: @@ -214,7 +214,7 @@ def main(argv=None): print("TinyTeX uninstalled.") except RuntimeError as e: - print("Error: %s" % e, file=sys.stderr) + print(f"Error: {e}", file=sys.stderr) return 1 return 0 diff --git a/pytinytex/compiler.py b/pytinytex/compiler.py index bc9b44c..9aa79bd 100644 --- a/pytinytex/compiler.py +++ b/pytinytex/compiler.py @@ -8,7 +8,7 @@ import subprocess import sys from pathlib import Path -from typing import List, Optional +from typing import Optional from .log_parser import LogEntry, ParsedLog, parse_log @@ -24,9 +24,9 @@ class CompileResult: log_path: Optional[str] = None engine: str = "pdflatex" runs: int = 0 - errors: List[LogEntry] = dataclasses.field(default_factory=list) - warnings: List[LogEntry] = dataclasses.field(default_factory=list) - installed_packages: List[str] = dataclasses.field(default_factory=list) + errors: list[LogEntry] = dataclasses.field(default_factory=list) + warnings: list[LogEntry] = dataclasses.field(default_factory=list) + installed_packages: list[str] = dataclasses.field(default_factory=list) output: str = "" @@ -110,6 +110,7 @@ def compile( try: proc = subprocess.run( cmd, + check=False, capture_output=True, creationflags=creation_flag, ) @@ -119,7 +120,7 @@ def compile( if stderr: output_lines.append(stderr) exit_code = proc.returncode - except Exception as e: + except OSError as e: output_lines.append(str(e)) exit_code = 1 break @@ -134,7 +135,7 @@ def compile( with open(log_path, encoding="utf-8", errors="replace") as f: log_content = f.read() parsed = parse_log(log_content) - except Exception as e: + except (OSError, ValueError) as e: logger.warning("Failed to parse log file: %s", e) result.errors = parsed.errors diff --git a/pytinytex/doctor.py b/pytinytex/doctor.py index b2b5234..84f0a2e 100644 --- a/pytinytex/doctor.py +++ b/pytinytex/doctor.py @@ -3,7 +3,6 @@ import dataclasses import os import platform -from typing import List @dataclasses.dataclass @@ -20,7 +19,7 @@ class DoctorResult: """Overall health check result.""" healthy: bool - checks: List[DoctorCheck] = dataclasses.field(default_factory=list) + checks: list[DoctorCheck] = dataclasses.field(default_factory=list) def doctor(): @@ -29,7 +28,7 @@ def doctor(): Returns: DoctorResult with individual check outcomes. """ - from . import get_tinytex_path, _LATEX_ENGINES, _find_file, _is_on_path + from . import _LATEX_ENGINES, _find_file, _is_on_path, get_tinytex_path from .tlmgr import get_version checks = [] @@ -37,7 +36,7 @@ def doctor(): # 1. TinyTeX installed? try: path = get_tinytex_path() - checks.append(DoctorCheck("TinyTeX installed", True, "Found at %s" % path)) + checks.append(DoctorCheck("TinyTeX installed", True, f"Found at {path}")) except RuntimeError as e: checks.append(DoctorCheck("TinyTeX installed", False, str(e))) return DoctorResult(healthy=False, checks=checks) @@ -52,7 +51,7 @@ def doctor(): DoctorCheck( "PATH configured", False, - "TinyTeX bin directory (%s) is not on PATH" % path, + f"TinyTeX bin directory ({path}) is not on PATH", ) ) @@ -61,7 +60,7 @@ def doctor(): if tlmgr: checks.append(DoctorCheck("tlmgr found", True, tlmgr)) else: - checks.append(DoctorCheck("tlmgr found", False, "tlmgr not found in %s" % path)) + checks.append(DoctorCheck("tlmgr found", False, f"tlmgr not found in {path}")) # 4. tlmgr functional? if tlmgr: @@ -76,11 +75,11 @@ def doctor(): for engine_name in _LATEX_ENGINES: engine_path = os.path.join(path, engine_name + suffix) if os.path.isfile(engine_path): - checks.append(DoctorCheck("Engine: %s" % engine_name, True, engine_path)) + checks.append(DoctorCheck(f"Engine: {engine_name}", True, engine_path)) else: checks.append( DoctorCheck( - "Engine: %s" % engine_name, + f"Engine: {engine_name}", False, "Not found (requires TinyTeX variation >= 1)", ) diff --git a/pytinytex/log_parser.py b/pytinytex/log_parser.py index 03cb8cc..1110cba 100644 --- a/pytinytex/log_parser.py +++ b/pytinytex/log_parser.py @@ -4,7 +4,7 @@ import dataclasses import re -from typing import List, Optional +from typing import Optional @dataclasses.dataclass @@ -22,9 +22,9 @@ class LogEntry: class ParsedLog: """Structured result of parsing a LaTeX log file.""" - errors: List[LogEntry] = dataclasses.field(default_factory=list) - warnings: List[LogEntry] = dataclasses.field(default_factory=list) - missing_packages: List[str] = dataclasses.field(default_factory=list) + errors: list[LogEntry] = dataclasses.field(default_factory=list) + warnings: list[LogEntry] = dataclasses.field(default_factory=list) + missing_packages: list[str] = dataclasses.field(default_factory=list) # Patterns for extracting information from LaTeX logs diff --git a/pytinytex/tinytex_download.py b/pytinytex/tinytex_download.py index 4db3513..6884f15 100644 --- a/pytinytex/tinytex_download.py +++ b/pytinytex/tinytex_download.py @@ -34,7 +34,7 @@ def _default_progress(downloaded, total): mb = downloaded / (1024 * 1024) mb_total = total / (1024 * 1024) sys.stdout.write( - "\rDownloading TinyTeX: %.1f/%.1f MB (%d%%)" % (mb, mb_total, pct) + f"\rDownloading TinyTeX: {mb:.1f}/{mb_total:.1f} MB ({pct}%)" ) sys.stdout.flush() if downloaded >= total: @@ -50,9 +50,7 @@ def download_tinytex( ): if variation not in [None, 0, 1, 2]: raise RuntimeError( - "Invalid TinyTeX variation {}. Valid variations are None, 0, 1, 2.".format( - variation - ) + f"Invalid TinyTeX variation {variation}. Valid variations are None, 0, 1, 2." ) if version == "latest" and variation == 2: logger.info("TinyTeX-2 is only published in the daily release, using it.") @@ -65,10 +63,10 @@ def download_tinytex( version = "v" + version elif version not in ("latest", "daily"): raise RuntimeError( - "Invalid TinyTeX version {}. TinyTeX version has to be in the format " + f"Invalid TinyTeX version {version}. TinyTeX version has to be in the format " "'latest' for the latest available version, 'daily' for the daily " "build, or year.month, for example: '2024.12', '2024.09' for a " - "specific version.".format(version) + "specific version." ) if progress_callback is None and sys.stdout.isatty(): progress_callback = _default_progress @@ -135,7 +133,7 @@ def download_tinytex( with tarfile.open(filename, "r:*") as tf: tf.extractall(tmpdirname) else: - raise RuntimeError("File {0} not supported".format(filename)) + raise RuntimeError(f"File {filename} not supported") # archives contain a single "TinyTeX" (or ".TinyTeX" on Linux) folder tinytex_extracted = next( p for p in tmpdirname.iterdir() if p.name.lstrip(".") == "TinyTeX" @@ -160,7 +158,7 @@ def _get_tinytex_urls(version, variation): version_url_frags = response.url.split("/") version = version_url_frags[-1] except urllib.error.HTTPError: - raise RuntimeError("Can't find TinyTeX version %s" % version) + raise RuntimeError(f"Can't find TinyTeX version {version}") response = urlopen( "https://github.com/rstudio/tinytex-releases/releases/expanded_assets/" + version @@ -206,11 +204,17 @@ def _select_tinytex_urls(asset_paths, variation, arm64, musl=False): new_style["darwin"] = url elif os_name == "windows": new_style["win32"] = url - elif os_name == "linux-arm64" and arm64 and not musl: - new_style["linux"] = url - elif os_name == "linux-x86_64" and not arm64 and not musl: - new_style["linux"] = url - elif os_name == "linuxmusl-x86_64" and not arm64 and musl: + elif ( + os_name == "linux-arm64" + and arm64 + and not musl + or os_name == "linux-x86_64" + and not arm64 + and not musl + or os_name == "linuxmusl-x86_64" + and not arm64 + and musl + ): new_style["linux"] = url elif os_name is None: if ext == "zip": diff --git a/pytinytex/tlmgr.py b/pytinytex/tlmgr.py index 64747a2..38da425 100644 --- a/pytinytex/tlmgr.py +++ b/pytinytex/tlmgr.py @@ -361,7 +361,7 @@ def _refresh_filename_db(path): for raw_line in proc.stdout: logger.debug(raw_line.decode("utf-8", errors="replace").rstrip()) proc.wait() - except Exception: + except OSError: logger.debug("mktexlsr failed, continuing anyway") @@ -374,9 +374,8 @@ def _run_tlmgr_command( original_args = list(args) # save before mutation from . import _find_file - if machine_readable: - if "--machine-readable" not in args: - args.insert(0, "--machine-readable") + if machine_readable and "--machine-readable" not in args: + args.insert(0, "--machine-readable") tlmgr_executable = _find_file(path, "tlmgr") if not tlmgr_executable: raise RuntimeError(f"Unable to find tlmgr in {path}") @@ -456,7 +455,7 @@ async def _read_stdout(process, output_buffer): line = line.decode("utf-8", errors="replace").rstrip() output_buffer.append(line) logger.info(line) - except Exception as e: + except (OSError, ValueError) as e: logger.error("Error in _read_stdout: %s", e) finally: process._transport.close() @@ -474,7 +473,7 @@ async def _send_stdin(process): break process.stdin.write(user_input.encode("utf-8")) await process.stdin.drain() - except Exception as e: + except (OSError, ValueError) as e: logger.error("Error in _send_stdin: %s", e) finally: if process.stdin: diff --git a/readme.md b/readme.md index ef1ca72..820482c 100644 --- a/readme.md +++ b/readme.md @@ -29,7 +29,7 @@ No system-wide TeX installation required. Missing packages are installed automat pip install pytinytex ``` -Python 3.8+ on Linux, macOS, and Windows. +Python 3.9+ on Linux, macOS, and Windows. ## Quick start diff --git a/tests/test_cli.py b/tests/test_cli.py index ea1df45..05ddf75 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,6 +6,7 @@ from unittest.mock import Mock import pytest + import pytinytex from pytinytex.cli import main diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 008fcc2..bfa758e 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -6,7 +6,8 @@ import pytest import pytinytex -from .utils import download_tinytex, TINYTEX_DISTRIBUTION # noqa + +from .utils import TINYTEX_DISTRIBUTION, download_tinytex # noqa @pytest.fixture diff --git a/tests/test_doctor.py b/tests/test_doctor.py index 50f478b..7369cab 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -3,7 +3,8 @@ import pytest import pytinytex -from .utils import download_tinytex, TINYTEX_DISTRIBUTION # noqa + +from .utils import TINYTEX_DISTRIBUTION, download_tinytex # noqa def test_doctor_result_structure(download_tinytex): # noqa @@ -53,10 +54,7 @@ def test_doctor_reports_healthy(download_tinytex): # noqa # All non-engine checks should pass core_checks = [c for c in result.checks if not c.name.startswith("Engine:")] for check in core_checks: - assert check.passed is True, "Check '%s' failed: %s" % ( - check.name, - check.message, - ) + assert check.passed is True, f"Check '{check.name}' failed: {check.message}" @pytest.mark.parametrize("download_tinytex", [1], indirect=True) diff --git a/tests/test_engine_discovery.py b/tests/test_engine_discovery.py index bd45364..a5c6592 100644 --- a/tests/test_engine_discovery.py +++ b/tests/test_engine_discovery.py @@ -3,7 +3,8 @@ import pytest import pytinytex -from .utils import download_tinytex, TINYTEX_DISTRIBUTION # noqa + +from .utils import TINYTEX_DISTRIBUTION, download_tinytex # noqa @pytest.mark.parametrize("download_tinytex", [1], indirect=True) diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py new file mode 100644 index 0000000..f4a80cf --- /dev/null +++ b/tests/test_error_handling.py @@ -0,0 +1,103 @@ +import asyncio +from subprocess import CompletedProcess +from types import SimpleNamespace +from unittest.mock import AsyncMock, Mock + +import pytest + +import pytinytex +from pytinytex import compiler, tlmgr + + +@pytest.mark.parametrize( + "stage, error", + [ + ("launch", PermissionError("failure")), + ("launch", TypeError("failure")), + ("log", PermissionError("failure")), + ("log", ValueError("failure")), + ("log", TypeError("failure")), + ], +) +def test_compile_error_handling(monkeypatch, tmp_path, caplog, stage, error): + tex = tmp_path / "test.tex" + tex.touch() + monkeypatch.setattr(pytinytex, "get_engine", lambda engine: "dummy") + run = Mock(return_value=CompletedProcess([], 0, stdout=b"", stderr=b"")) + monkeypatch.setattr(compiler.subprocess, "run", run) + if stage == "launch": + run.side_effect = error + else: + tex.with_suffix(".log").touch() + tex.with_suffix(".pdf").touch() + monkeypatch.setattr(compiler, "parse_log", Mock(side_effect=error)) + + if isinstance(error, TypeError): + with pytest.raises(TypeError, match="failure"): + compiler.compile(tex) + else: + result = compiler.compile(tex) + assert result.success is (stage == "log") + if stage == "launch": + assert "failure" in result.output + else: + assert "Failed to parse log file: failure" in caplog.text + + +@pytest.mark.parametrize("error", [PermissionError("failure"), TypeError("failure")]) +def test_refresh_error_handling(monkeypatch, tmp_path, error): + (tmp_path / "mktexlsr").touch() + monkeypatch.setattr(tlmgr.subprocess, "Popen", Mock(side_effect=error)) + if isinstance(error, TypeError): + with pytest.raises(TypeError, match="failure"): + tlmgr._refresh_filename_db(tmp_path) + else: + tlmgr._refresh_filename_db(tmp_path) + + +@pytest.mark.parametrize( + "error", + [ + OSError("failure"), + ValueError("failure"), + TypeError("failure"), + asyncio.CancelledError("failure"), + ], +) +def test_stdout_error_handling(error): + process = SimpleNamespace( + pid=1, + stdout=SimpleNamespace(readline=AsyncMock(side_effect=error)), + _transport=Mock(), + wait=AsyncMock(return_value=0), + ) + if isinstance(error, (OSError, ValueError)): + assert asyncio.run(tlmgr._read_stdout(process, [])) == 0 + else: + with pytest.raises(type(error)): + asyncio.run(tlmgr._read_stdout(process, [])) + process._transport.close.assert_called_once() + + +@pytest.mark.parametrize( + "error", + [ + BrokenPipeError("failure"), + ValueError("failure"), + TypeError("failure"), + asyncio.CancelledError("failure"), + ], +) +def test_stdin_error_handling(monkeypatch, error): + monkeypatch.setattr(tlmgr.sys, "stdin", SimpleNamespace(readline=lambda: "hello\n")) + process = SimpleNamespace( + pid=1, + stdin=SimpleNamespace(write=Mock(side_effect=error)), + _transport=Mock(), + ) + if isinstance(error, (OSError, ValueError)): + asyncio.run(tlmgr._send_stdin(process)) + else: + with pytest.raises(type(error)): + asyncio.run(tlmgr._send_stdin(process)) + process._transport.close.assert_called_once() diff --git a/tests/test_log_parser.py b/tests/test_log_parser.py index 703edfc..4750eb8 100644 --- a/tests/test_log_parser.py +++ b/tests/test_log_parser.py @@ -1,6 +1,6 @@ """Tests for the LaTeX log parser.""" -from pytinytex.log_parser import parse_log, LogEntry, ParsedLog +from pytinytex.log_parser import LogEntry, ParsedLog, parse_log def test_parse_empty_log(): diff --git a/tests/test_package_management.py b/tests/test_package_management.py index a75e130..f4f1f15 100644 --- a/tests/test_package_management.py +++ b/tests/test_package_management.py @@ -1,7 +1,8 @@ import pytest import pytinytex -from .utils import download_tinytex, TINYTEX_DISTRIBUTION # noqa + +from .utils import TINYTEX_DISTRIBUTION, download_tinytex # noqa def test_list_installed(download_tinytex): # noqa @@ -93,7 +94,7 @@ def test_update(download_tinytex): # noqa pytinytex.ensure_tinytex_installed(TINYTEX_DISTRIBUTION) # update -all may fail transiently due to tlmgr internal errors on CI try: - exit_code, output = pytinytex.update() + exit_code, _output = pytinytex.update() assert exit_code == 0 except RuntimeError: pytest.skip("tlmgr update failed (transient CI issue)") diff --git a/tests/test_tinytex_download.py b/tests/test_tinytex_download.py index a22483b..4d43b1b 100644 --- a/tests/test_tinytex_download.py +++ b/tests/test_tinytex_download.py @@ -3,6 +3,7 @@ import pytest import pytinytex + from .utils import TINYTEX_DISTRIBUTION, cleanup diff --git a/tests/test_tinytex_path_resolver.py b/tests/test_tinytex_path_resolver.py index fe1e221..4f6dd6d 100644 --- a/tests/test_tinytex_path_resolver.py +++ b/tests/test_tinytex_path_resolver.py @@ -7,7 +7,8 @@ import pytest import pytinytex -from .utils import download_tinytex, TINYTEX_DISTRIBUTION # noqa + +from .utils import TINYTEX_DISTRIBUTION, download_tinytex # noqa def test_failing_resolver(download_tinytex): # noqa diff --git a/tests/test_tinytex_runner.py b/tests/test_tinytex_runner.py index c444b3f..fb332d9 100644 --- a/tests/test_tinytex_runner.py +++ b/tests/test_tinytex_runner.py @@ -1,5 +1,6 @@ import pytinytex -from .utils import download_tinytex, TINYTEX_DISTRIBUTION # noqa + +from .utils import TINYTEX_DISTRIBUTION, download_tinytex # noqa def test_run_help(download_tinytex): # noqa diff --git a/tests/test_tlmgr.py b/tests/test_tlmgr.py index 6b771ab..c9a01e3 100644 --- a/tests/test_tlmgr.py +++ b/tests/test_tlmgr.py @@ -4,7 +4,8 @@ import pytinytex from pytinytex.tlmgr import _needs_self_update -from .utils import download_tinytex, TINYTEX_DISTRIBUTION # noqa + +from .utils import TINYTEX_DISTRIBUTION, download_tinytex # noqa class TestNeedsSelfUpdate: diff --git a/tests/utils.py b/tests/utils.py index 745b37d..7255b26 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,5 @@ -import shutil import os +import shutil import pytest diff --git a/uv.lock b/uv.lock index 5bb701e..a3877d3 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.8" +requires-python = ">=3.9" [[package]] name = "colorama" @@ -85,27 +85,27 @@ dev = [ [[package]] name = "ruff" -version = "0.15.17" +version = "0.16.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/a9/3abdf488f1bf3d24c699415e454ed554a6350d5d89ce183be1ee0a3361ac/ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", size = 4743346, upload-time = "2026-06-11T17:54:47.663Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/4d/e11259f5da07cb6afb2d074c31bf09da9671993f7329d4f15d2fdc458301/ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", size = 10856677, upload-time = "2026-06-11T17:54:49.533Z" }, - { url = "https://files.pythonhosted.org/packages/29/3e/772d679e1a0dc058e58875bd2c0cb713a0530877b4a76fee3c7966df0d49/ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", size = 11223443, upload-time = "2026-06-11T17:55:00.573Z" }, - { url = "https://files.pythonhosted.org/packages/68/58/bd41f7688b2fd5623012605130ed70e60aa7f2244baa3d5066bdd61530c8/ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d", size = 10566458, upload-time = "2026-06-11T17:55:07.52Z" }, - { url = "https://files.pythonhosted.org/packages/d8/5b/733371013fcf1ec339e477ece6ab42bfe10bdd9bba8ee88a9516aa56bfc0/ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", size = 10914483, upload-time = "2026-06-11T17:55:05.501Z" }, - { url = "https://files.pythonhosted.org/packages/bd/cc/6f24251cc0252f7239391ccb85833f320efad14ebe5b443943f37ced6332/ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", size = 10647497, upload-time = "2026-06-11T17:54:57.733Z" }, - { url = "https://files.pythonhosted.org/packages/68/dd/0d10c17ce1a1624d6fc3156309c3f834fdb5dfaad026ec90c85684f3990e/ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", size = 11416967, upload-time = "2026-06-11T17:54:51.461Z" }, - { url = "https://files.pythonhosted.org/packages/2f/91/556bfb156f6144f355e831c23db00b2fc4120f86b3ce81cc5f7fd2df51f3/ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", size = 12335770, upload-time = "2026-06-11T17:54:45.793Z" }, - { url = "https://files.pythonhosted.org/packages/88/82/8b5999aa13355e926f06d9f42a32dcca862f623bf0363785ff89d607dffd/ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", size = 11575441, upload-time = "2026-06-11T17:54:32.661Z" }, - { url = "https://files.pythonhosted.org/packages/11/93/f10377bb04109ca0e8cbc483ff1982c54b6d418210041776f93e8cdc7fa9/ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", size = 11557614, upload-time = "2026-06-11T17:54:34.698Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a6/eeeae7f7d5493df41649ab3db92f086b2d0a30199e4efdf8e3dd7a033f24/ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", size = 11544450, upload-time = "2026-06-11T17:54:39.042Z" }, - { url = "https://files.pythonhosted.org/packages/32/88/5991ce565129a24dd4a00db1254b3b5db2e53018cbe4018ea5a89738e727/ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", size = 10892524, upload-time = "2026-06-11T17:55:09.432Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1d/0fdd248313425f55223968af04b0a42125466a8d88d21c1d99c6af0a51e8/ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", size = 10659573, upload-time = "2026-06-11T17:54:36.824Z" }, - { url = "https://files.pythonhosted.org/packages/9e/0e/072e8260deb9461062ce9311ced27a8e541229a6ffd483013dd37661e43e/ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", size = 11127818, upload-time = "2026-06-11T17:55:03.124Z" }, - { url = "https://files.pythonhosted.org/packages/ab/b4/55060a34163121498014696b5f656db5b8c6963768f227dbf0d76b311073/ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", size = 11655901, upload-time = "2026-06-11T17:54:53.562Z" }, - { url = "https://files.pythonhosted.org/packages/49/71/9b29d6b87cef468d697f43c6a91e3fae4a80185779d7d5a4ef27d173439f/ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", size = 10925574, upload-time = "2026-06-11T17:54:55.723Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b2/8fc77f3723228836fa5d12497eb71c808f83782e10d058d2b15cfa14640b/ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", size = 12058788, upload-time = "2026-06-11T17:54:41.042Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" }, + { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" }, + { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" }, + { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" }, + { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" }, + { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, ] [[package]]