ci/lint: prek commit gateway (ruff check/format + basedpyright)

Add prek.toml so git commit runs ruff-pre-commit (fix then format) and
pdm-based basedpyright. Document install/install-hook; clear type warnings
that made basedpyright exit non-zero under recommended mode.
This commit is contained in:
2026-07-18 14:10:04 +08:00
parent 39f9a7f636
commit 9b38a3b333
8 changed files with 60 additions and 37 deletions
+6 -1
View File
@@ -17,9 +17,14 @@ pdm run ruff format . # apply formatting
pdm run ruff format --check . # CI: fail if unformatted (do not skip) pdm run ruff format --check . # CI: fail if unformatted (do not skip)
pdm run basedpyright . # type checking (basedpyright, "recommended" strictness) pdm run basedpyright . # type checking (basedpyright, "recommended" strictness)
pdm run pytest # tests (pytest-asyncio auto mode) pdm run pytest # tests (pytest-asyncio auto mode)
# Commit gateway (prek — https://prek.j178.dev/): ruff check/format + basedpyright
uv tool install prek # once
prek install # wire git pre-commit hook (once per clone)
prek run --all-files # run all hooks on demand
``` ```
GitHub Actions runs `ruff check`, `ruff format --check`, `basedpyright`, then `pytest`. Format locally with `pdm run ruff format .` so CI does not fail after publish. GitHub Actions runs `ruff check`, `ruff format --check`, `basedpyright`, then `pytest`. Local commits run the same checks via `prek.toml` so `ruff format` is not forgotten.
## Architecture ## Architecture
+9 -1
View File
@@ -71,7 +71,15 @@ pdm run basedpyright .
pdm run pytest pdm run pytest
``` ```
CI fails if sources are not Ruff-formatted (`ruff format --check`). Run `pdm run ruff format .` before push. **Commit gateway** ([prek](https://prek.j178.dev/)): runs ruff check + format and basedpyright on `git commit` so format is not forgotten.
```bash
uv tool install prek # once
prek install # once per clone (installs .git/hooks/pre-commit)
prek run --all-files # run all hooks on demand
```
Config: `prek.toml`. CI still runs the same checks in GitHub Actions.
## Basic usage ## Basic usage
+27
View File
@@ -0,0 +1,27 @@
# Commit gateway via prek (https://prek.j178.dev/) — drop-in pre-commit compatible.
# Install: uv tool install prek (or: pipx install prek)
# Wire git: prek install
# Manual: prek run --all-files
default_install_hook_types = ["pre-commit"]
default_stages = ["pre-commit"]
default_language_version = { python = "python3.14" }
# Ruff: lint (with autofix) then format — order matters (fix before format).
# https://docs.astral.sh/ruff/integrations/#pre-commit
[[repos]]
repo = "https://github.com/astral-sh/ruff-pre-commit"
rev = "v0.15.22"
hooks = [
{ id = "ruff-check", args = ["--fix"], types_or = ["python", "pyi"] },
{ id = "ruff-format", types_or = ["python", "pyi"] },
]
# basedpyright: full-project typecheck (same as CI `pdm run basedpyright .`).
# Mirror hook: https://docs.basedpyright.com/latest/installation/prek-hook/
# Uses system+PDM so imports resolve from the project venv (isolated hook envs lack deps).
[[repos]]
repo = "local"
hooks = [
{ id = "basedpyright", name = "basedpyright", language = "system", entry = "pdm run basedpyright .", pass_filenames = false, files = "\\.pyi?$" },
]
+3 -5
View File
@@ -10,7 +10,6 @@ from plyngent.lmproto.openai_compatible.model import (
ChatCompletionsParam, ChatCompletionsParam,
DeveloperChatMessage, DeveloperChatMessage,
SystemChatMessage, SystemChatMessage,
ToolChatMessage,
UserChatMessage, UserChatMessage,
) )
@@ -44,7 +43,7 @@ _SEED_MESSAGE_TEMPLATE = (
) )
def format_transcript(messages: Sequence[AnyChatMessage]) -> str: # noqa: C901 def format_transcript(messages: Sequence[AnyChatMessage]) -> str:
"""Render messages as plain text for a summarization prompt.""" """Render messages as plain text for a summarization prompt."""
lines: list[str] = [] lines: list[str] = []
for msg in messages: for msg in messages:
@@ -65,10 +64,9 @@ def format_transcript(messages: Sequence[AnyChatMessage]) -> str: # noqa: C901
lines.append(f"[assistant tool_call] {call.function.name}({call.function.arguments})") lines.append(f"[assistant tool_call] {call.function.name}({call.function.arguments})")
else: else:
lines.append(f"[assistant tool_call] custom id={call.id}") lines.append(f"[assistant tool_call] custom id={call.id}")
elif isinstance(msg, ToolChatMessage):
lines.append(f"[tool {msg.tool_call_id}] {msg.content}")
else: else:
lines.append(f"[message] {msg!r}") # ToolChatMessage (remaining AnyChatMessage arm)
lines.append(f"[tool {msg.tool_call_id}] {msg.content}")
return "\n".join(lines) return "\n".join(lines)
+3 -10
View File
@@ -33,7 +33,6 @@ from plyngent.lmproto.openai_compatible.model import (
StreamFunctionDelta, StreamFunctionDelta,
StreamToolCallDelta, StreamToolCallDelta,
SystemChatMessage, SystemChatMessage,
ToolChatMessage,
ToolFunctionItem, ToolFunctionItem,
UserChatMessage, UserChatMessage,
) )
@@ -106,17 +105,14 @@ def chat_messages_to_responses_input(
items.append(ResponseEasyInputMessage(role="user", content=message.content)) items.append(ResponseEasyInputMessage(role="user", content=message.content))
elif isinstance(message, AssistantChatMessage): elif isinstance(message, AssistantChatMessage):
items.extend(_assistant_to_input_items(message)) items.extend(_assistant_to_input_items(message))
elif isinstance(message, ToolChatMessage): else:
# ToolChatMessage (remaining AnyChatMessage arm)
items.append( items.append(
ResponseFunctionToolCallOutput( ResponseFunctionToolCallOutput(
call_id=message.tool_call_id, call_id=message.tool_call_id,
output=message.content, output=message.content,
) )
) )
else:
content = getattr(message, "content", None)
if isinstance(content, str) and content:
items.append(ResponseEasyInputMessage(role="user", content=content))
instructions = "\n\n".join(instructions_parts) if instructions_parts else None instructions = "\n\n".join(instructions_parts) if instructions_parts else None
return instructions, items return instructions, items
@@ -173,12 +169,9 @@ def responses_status_to_finish_reason(
status_s = status if isinstance(status, str) else None status_s = status if isinstance(status, str) else None
if status_s == "incomplete": if status_s == "incomplete":
details = response.incomplete_details details = response.incomplete_details
reason = None
if details is not UNSET and details is not None: if details is not UNSET and details is not None:
raw_reason = details.reason raw_reason = details.reason
if raw_reason is not UNSET and isinstance(raw_reason, str): if raw_reason is not UNSET and raw_reason == "content_filter":
reason = raw_reason
if reason == "content_filter":
return "content_filter" return "content_filter"
return "length" return "length"
if status_s in {"failed", "cancelled"}: if status_s in {"failed", "cancelled"}:
+2 -1
View File
@@ -95,7 +95,8 @@ class TodoStack:
for frame in cast("list[object]", frames_raw): for frame in cast("list[object]", frames_raw):
if not isinstance(frame, dict): if not isinstance(frame, dict):
continue continue
frame_items = msgspec.convert(frame.get("items"), type=list[TodoItem]) frame_map = cast("dict[str, object]", frame)
frame_items = msgspec.convert(frame_map.get("items"), type=list[TodoItem])
items.extend(frame_items) items.extend(frame_items)
return cls(TodoStackData(items=items, next_id=next_id)) return cls(TodoStackData(items=items, next_id=next_id))
except msgspec.ValidationError, TypeError, ValueError: except msgspec.ValidationError, TypeError, ValueError:
+3 -13
View File
@@ -17,7 +17,6 @@ from plyngent.lmproto.openai_compatible.model import (
AssistantFunctionToolCall, AssistantFunctionToolCall,
DeveloperChatMessage, DeveloperChatMessage,
SystemChatMessage, SystemChatMessage,
ToolChatMessage,
UserChatMessage, UserChatMessage,
) )
from plyngent.runtime import ProviderNotSupportedError from plyngent.runtime import ProviderNotSupportedError
@@ -36,7 +35,7 @@ _YOLO_MODE_CHOICES = ("on", "off", "once")
_EXPORT_FORMAT_CHOICES = ("md", "json") _EXPORT_FORMAT_CHOICES = ("md", "json")
class HistoryLimitType(click.ParamType): class HistoryLimitType(click.ParamType[int]):
"""``N`` (int >= 1) or the shortcut ``last`` (equivalent to ``1``).""" """``N`` (int >= 1) or the shortcut ``last`` (equivalent to ``1``)."""
name: str = "history_limit" name: str = "history_limit"
@@ -1100,16 +1099,10 @@ def _print_history_message_full(index: int, message: AnyChatMessage) -> None:
if isinstance(message, AssistantChatMessage): if isinstance(message, AssistantChatMessage):
_print_history_assistant_full(index, message) _print_history_assistant_full(index, message)
return return
if isinstance(message, ToolChatMessage): # ToolChatMessage (remaining AnyChatMessage arm)
click.secho(f"{index}. tool({message.tool_call_id}):", fg="magenta") click.secho(f"{index}. tool({message.tool_call_id}):", fg="magenta")
click.echo(message.content or "") click.echo(message.content or "")
click.echo() click.echo()
return
role = getattr(message, "role", type(message).__name__)
content = getattr(message, "content", "")
click.echo(f"{index}. {role}:")
click.echo(str(content))
click.echo()
def _format_history_message(index: int, message: AnyChatMessage) -> str: def _format_history_message(index: int, message: AnyChatMessage) -> str:
@@ -1134,11 +1127,8 @@ def _format_history_message(index: int, message: AnyChatMessage) -> str:
parts.append(f"tool_calls=[{', '.join(names)}]") parts.append(f"tool_calls=[{', '.join(names)}]")
body = " ".join(parts) if parts else "(empty)" body = " ".join(parts) if parts else "(empty)"
return f"{index}. assistant: {body}" return f"{index}. assistant: {body}"
if isinstance(message, ToolChatMessage): # ToolChatMessage
return f"{index}. tool({message.tool_call_id}): {_preview_content(message.content)}" return f"{index}. tool({message.tool_call_id}): {_preview_content(message.content)}"
role = getattr(message, "role", type(message).__name__)
content = getattr(message, "content", "")
return f"{index}. {role}: {_preview_content(str(content))}"
def _run_slash_argv(args: Sequence[str], state: ReplState) -> None: def _run_slash_argv(args: Sequence[str], state: ReplState) -> None:
+3 -2
View File
@@ -30,6 +30,7 @@ from plyngent.lmproto.openai_compatible.model import (
ChatCompletionsParam, ChatCompletionsParam,
ChunkChoice, ChunkChoice,
DeltaMessage, DeltaMessage,
FinishReason,
StreamFunctionDelta, StreamFunctionDelta,
StreamToolCallDelta, StreamToolCallDelta,
ToolChatMessage, ToolChatMessage,
@@ -174,8 +175,8 @@ class ScriptedClient:
def _response( def _response(
message: AssistantChatMessage, message: AssistantChatMessage,
*, *,
usage: dict[str, int] | None = None, usage: dict[str, object] | None = None,
finish_reason: str | None = "stop", finish_reason: FinishReason | None = "stop",
) -> ChatCompletionResponse: ) -> ChatCompletionResponse:
return ChatCompletionResponse( return ChatCompletionResponse(
id="1", id="1",