fix(types): clear all 10 pre-existing mypy errors

Master CI lint job runs both ruff and mypy. Previous commit cleaned ruff;
this knocks out the mypy backlog too so the parallel-queue PR can ship
fully green.

- fragments.py: FragmentLibrary defines a `list()` method, which shadows
  the builtin in class-scope name resolution and broke `list[str]`
  annotations in `resolve()`. Qualify with `builtins.list` (imported
  under TYPE_CHECKING since it's static-only).
- remote.py: `result.get("civitai", result)` returns Any to mypy because
  dict.get widens. Capture into a typed local first.
- cli.py:
  - Drop redundant type re-annotation on `civitai_results` in the
    non-remote branch (same name was annotated in the early-return
    remote branch above; mypy treats class/module-scope re-annotation
    as no-redef even when control flow rules out overlap).
  - Guard `p.name is not None` before passing to `get_parameter_source`
    (click stubs type Parameter.name as `str | None`).
  - Parameterize bare `list | dict` in `_load_json_file_or_inline`.
  - Widen `_write_sweep_manifest`'s template_path arg to `Path | None`
    (callers already pass None when --list is used without --template);
    serialize as empty string in manifest to keep schema stable.
  - `# type: ignore[arg-type]` on the deprecated `tsr comfy generate`
    delegator that passes a typer function where click.Command is
    expected — duck-typed at runtime, only matters for the deprecation
    shim.

Tests: 374 still passing. Ruff: clean. Mypy: clean.
This commit is contained in:
2026-05-18 23:43:15 +02:00
parent b0b5bca5f8
commit 5a9b935753
3 changed files with 30 additions and 10 deletions
+16 -7
View File
@@ -346,7 +346,10 @@ def search(
return return
key = api_key or load_api_key() key = api_key or load_api_key()
civitai_results: dict[str, Any] | None = None # Reuse the name from the remote-mode branch above (which already returned)
# without redeclaring its type — mypy treats class-scope re-annotation as
# a no-redef even when control flow guarantees the branches don't overlap.
civitai_results = None
hf_results: list[dict[str, Any]] | None = None hf_results: list[dict[str, Any]] | None = None
# Search CivitAI # Search CivitAI
@@ -946,7 +949,9 @@ def generate( # noqa: PLR0915
{ {
p.name p.name
for p in click_ctx.command.params for p in click_ctx.command.params
if click_ctx.get_parameter_source(p.name) == click.core.ParameterSource.COMMANDLINE # click's Parameter.name is typed `str | None` in stubs but is always
# a real string at runtime for any param that's been registered.
if p.name is not None and click_ctx.get_parameter_source(p.name) == click.core.ParameterSource.COMMANDLINE
} }
if hasattr(click_ctx, "get_parameter_source") if hasattr(click_ctx, "get_parameter_source")
else set() else set()
@@ -1709,7 +1714,7 @@ _STYLE_SWEEP_TEMPLATE_KEYS = {
} }
def _load_json_file_or_inline(value: str | list | dict, *, what: str) -> Any: def _load_json_file_or_inline(value: str | list[Any] | dict[str, Any], *, what: str) -> Any:
"""Load JSON from a file path or accept already-parsed inline data. """Load JSON from a file path or accept already-parsed inline data.
`value` may be a path string, a JSON string, or an already-parsed list/dict `value` may be a path string, a JSON string, or an already-parsed list/dict
@@ -2188,14 +2193,16 @@ def style_sweep( # noqa: PLR0915
def _write_sweep_manifest( def _write_sweep_manifest(
out_dir: Path, out_dir: Path,
template_path: Path, template_path: Path | None,
styles_origin: str, styles_origin: str,
results: list[dict[str, Any]], results: list[dict[str, Any]],
) -> Path: ) -> Path:
"""Write the per-sweep manifest JSON. Returns the path.""" """Write the per-sweep manifest JSON. Returns the path."""
manifest_path = out_dir / "_sweep.json" manifest_path = out_dir / "_sweep.json"
manifest: dict[str, Any] = { manifest: dict[str, Any] = {
"template": str(template_path), # template_path is None when --list is used with only --styles (no template
# required). Serialize as empty string to keep manifest schema stable.
"template": str(template_path) if template_path is not None else "",
"styles_source": styles_origin, "styles_source": styles_origin,
"results": results, "results": results,
} }
@@ -3628,8 +3635,10 @@ def comfy_generate(
) -> None: ) -> None:
"""[Deprecated] Use 'tsr generate' instead. All features have been merged into the top-level command.""" """[Deprecated] Use 'tsr generate' instead. All features have been merged into the top-level command."""
console.print("[yellow]Warning: 'tsr comfy generate' is deprecated. Use 'tsr generate' instead.[/yellow]") console.print("[yellow]Warning: 'tsr comfy generate' is deprecated. Use 'tsr generate' instead.[/yellow]")
# Delegate to the unified generate command via context invocation # Delegate to the unified generate command via context invocation.
ctx = typer.Context(generate) # typer.Context expects a click.Command, but passing the typer function directly
# works at runtime via duck-typing — keeping it for back-compat with deprecated alias.
ctx = typer.Context(generate) # type: ignore[arg-type]
generate( generate(
ctx=ctx, ctx=ctx,
prompt=prompt, prompt=prompt,
+12 -2
View File
@@ -16,9 +16,16 @@ from __future__ import annotations
import json import json
import re import re
from pathlib import Path # noqa: TC003 # used in runtime return annotations exposed to typer from pathlib import Path # noqa: TC003 # used in runtime return annotations exposed to typer
from typing import TYPE_CHECKING
from tensors.config import DATA_DIR from tensors.config import DATA_DIR
if TYPE_CHECKING:
# Qualified `builtins.list` is referenced in annotations inside FragmentLibrary
# because the class defines a method named `list` that shadows the builtin
# at class-scope name resolution. Static-only — not needed at runtime.
import builtins
# Restrict fragment names to a safe subset so they can't escape the storage dir # Restrict fragment names to a safe subset so they can't escape the storage dir
# via path traversal and so file listings stay tidy. # via path traversal and so file listings stay tidy.
_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]+$") _NAME_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
@@ -132,8 +139,11 @@ class FragmentLibrary:
*, *,
name: str | None = None, name: str | None = None,
inline: str | None = None, inline: str | None = None,
extra: list[str] | None = None, # NOTE: `builtins.list` qualifier needed because this class defines a
) -> list[str]: # `list()` method below, which shadows the builtin in class-scope name
# resolution. Affects mypy/pyright even with `from __future__ import annotations`.
extra: builtins.list[str] | None = None,
) -> builtins.list[str]:
"""Merge a named fragment with an inline CSV string and optional extras. """Merge a named fragment with an inline CSV string and optional extras.
Resolution order (first match wins per duplicate): named → inline → extra. Resolution order (first match wins per duplicate): named → inline → extra.
+2 -1
View File
@@ -209,7 +209,8 @@ def remote_search(
response.raise_for_status() response.raise_for_status()
result: dict[str, Any] = response.json() result: dict[str, Any] = response.json()
# The remote API wraps CivitAI results under "civitai" key # The remote API wraps CivitAI results under "civitai" key
return result.get("civitai", result) civitai_section: dict[str, Any] = result.get("civitai", result)
return civitai_section
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
if console: if console:
console.print(f"[red]Remote API error: {e.response.status_code}[/red]") console.print(f"[red]Remote API error: {e.response.status_code}[/red]")