diff --git a/cli/localci/cli/analyze.py b/cli/localci/cli/analyze.py index 7ed42ce..eaa6a1e 100644 --- a/cli/localci/cli/analyze.py +++ b/cli/localci/cli/analyze.py @@ -13,7 +13,8 @@ from localci.cli.list import _MATRIX_TABLE_COLUMNS, _PLATFORM_COLOR from localci.core.serialization import workflow_summary, workflow_to_json -from localci.core.workflow import Workflow, WorkflowAnalyzer, WorkflowError +from localci.core.workflow import Workflow, WorkflowAnalyzer +from localci.errors import WorkflowError from localci.utils.output import ( console, make_table, @@ -39,7 +40,7 @@ def _print_workflow_header(wf: Workflow) -> None: console.print() -def _print_jobs_table(wf) -> None: +def _print_jobs_table(wf: Workflow) -> None: """Print summary of all jobs.""" table = make_table( "#", "Job ID", "Matrix", "Steps", "Depends On", "Timeout", title="Jobs" @@ -84,7 +85,7 @@ def _print_matrix_table(wf: Workflow) -> None: console.print() -def _print_platform_summary(wf) -> None: +def _print_platform_summary(wf: Workflow) -> None: """Print platform breakdown.""" summary = wf.platform_summary() if not summary: diff --git a/cli/localci/cli/config.py b/cli/localci/cli/config.py index 48dc09a..5706d2c 100644 --- a/cli/localci/cli/config.py +++ b/cli/localci/cli/config.py @@ -6,6 +6,7 @@ from __future__ import annotations from pathlib import Path +from typing import Any import click import yaml @@ -103,7 +104,7 @@ def config_set(ctx: click.Context, key: str, value: str) -> None: ctx.exit(1) with open(config_path, encoding="utf-8") as fh: - data: dict = yaml.safe_load(fh) or {} + data: dict[str, Any] = yaml.safe_load(fh) or {} # Navigate dot-notation key. keys = key.split(".") diff --git a/cli/localci/cli/list.py b/cli/localci/cli/list.py index 4e79ef7..795e30a 100644 --- a/cli/localci/cli/list.py +++ b/cli/localci/cli/list.py @@ -15,8 +15,8 @@ CompilerFamily, Platform, WorkflowAnalyzer, - WorkflowError, ) +from localci.errors import WorkflowError from localci.utils.output import ( console, make_table, diff --git a/cli/localci/cli/logs.py b/cli/localci/cli/logs.py index bc28d80..9bb0c1d 100644 --- a/cli/localci/cli/logs.py +++ b/cli/localci/cli/logs.py @@ -10,6 +10,7 @@ import click +from localci.core.executor import JobResult from localci.core.results import ExecutionSummary from localci.utils.output import ( console, @@ -151,7 +152,7 @@ def logs( click.echo(content) -def _find_job(summary: ExecutionSummary, query: str): +def _find_job(summary: ExecutionSummary, query: str) -> JobResult | None: """Find a job by index or name. Matching priority: diff --git a/cli/localci/cli/run/click_options.py b/cli/localci/cli/run/click_options.py index 349acfb..690cff5 100644 --- a/cli/localci/cli/run/click_options.py +++ b/cli/localci/cli/run/click_options.py @@ -97,9 +97,8 @@ def run_options(command: F) -> F: "Run in offline mode (no action downloads, requires pre-cached actions)." ), ), - click.pass_context, ] - wrapped: F = command + wrapped: F = click.pass_context(command) # type: ignore[assignment] for opt in reversed(opts): - wrapped = opt(wrapped) # type: ignore[assignment] + wrapped = opt(wrapped) return wrapped diff --git a/cli/localci/cli/run/container.py b/cli/localci/cli/run/container.py index 05371b3..639c50e 100644 --- a/cli/localci/cli/run/container.py +++ b/cli/localci/cli/run/container.py @@ -8,7 +8,7 @@ from localci.core.boost_cache import ensure_boost_cache from localci.core.ccache_stats import get_ccache_stats -from localci.core.config import LocalCIConfig, resolve_cache_paths +from localci.core.config import LocalCIConfig, ResolvedCachePaths, resolve_cache_paths from localci.core.executor import JobExecutor from localci.core.orchestrator import OrchestratorConfig, ParallelExecutionManager from localci.core.progress import ProgressTracker @@ -30,7 +30,7 @@ class RunDependencies: priority_config_factory: Callable[[LocalCIConfig], PriorityConfig] ensure_boost_cache_fn: Callable[..., bool] get_ccache_stats_fn: Callable[..., str | None] - resolve_cache_paths_fn: Callable[..., object] + resolve_cache_paths_fn: Callable[..., ResolvedCachePaths | None] workflow_patcher: Callable[..., Path] diff --git a/cli/localci/cli/run/patcher.py b/cli/localci/cli/run/patcher.py index 443af62..9c70cf0 100644 --- a/cli/localci/cli/run/patcher.py +++ b/cli/localci/cli/run/patcher.py @@ -9,11 +9,14 @@ from localci.core.config import LocalCIConfig from localci.core.patch_pipeline import PatchContext, PatchPipeline +from localci.core.queue import PriorityJobQueue from localci.core.workflow import MatrixEntry from localci.utils.output import console, print_info, print_key_value -def _print_execution_plan(queue, workflow_path: Path, timeout: int) -> None: +def _print_execution_plan( + queue: PriorityJobQueue, workflow_path: Path, timeout: int +) -> None: """Print dry-run execution plan from the queue.""" from rich.table import Table diff --git a/cli/localci/cli/run/run_flow.py b/cli/localci/cli/run/run_flow.py index 9c27ec9..1e41ad4 100644 --- a/cli/localci/cli/run/run_flow.py +++ b/cli/localci/cli/run/run_flow.py @@ -3,17 +3,18 @@ from __future__ import annotations from pathlib import Path +from typing import Any from localci.cli.run.container import RunDependencies, build_run_container from localci.cli.run.params import RunOptions from localci.cli.run.patcher import _print_execution_plan, make_workflow_patcher from localci.core.config import LocalCIConfig -from localci.core.executor import ActNotFoundError, DockerNotAvailableError +from localci.core.executor import JobExecutor from localci.core.github_token import resolve_github_token, warn_sentinel_github_token from localci.core.models import JobEvent, JobEventType from localci.core.results import ExecutionSummary -from localci.core.workflow import MatrixEntry, Platform -from localci.errors import WorkflowError +from localci.core.workflow import MatrixEntry, Platform, Workflow +from localci.errors import ActNotFoundError, DockerNotAvailableError, WorkflowError from localci.utils.output import print_error, print_info, print_warning _PLATFORM_MAP = { @@ -72,9 +73,10 @@ def execute_run( return 0 priority_config = container.priority_config_factory(cfg) - registry_path = project_dir / "image-registry.yml" - if not registry_path.exists(): - registry_path = None + registry_candidate = project_dir / "image-registry.yml" + registry_path: Path | None = ( + registry_candidate if registry_candidate.exists() else None + ) matrix_include, matrix_exclude = _resolve_matrix_filters(options, cfg) plat_filter = _PLATFORM_MAP.get(options.platform) if options.platform else None @@ -176,7 +178,7 @@ def execute_run( return 0 if summary.all_passed else 1 -def _collect_matrix_pairs(wf) -> list[tuple[str, MatrixEntry]]: +def _collect_matrix_pairs(wf: Workflow) -> list[tuple[str, MatrixEntry]]: pairs: list[tuple[str, MatrixEntry]] = [] for job_id, job in wf.jobs.items(): for entry in job.matrix: @@ -229,8 +231,8 @@ def _filter_matrix_pairs( def _resolve_matrix_filters( options: RunOptions, cfg: LocalCIConfig -) -> tuple[list[dict] | None, list[dict] | None]: - cli_matrix_include: list[dict] | None = None +) -> tuple[list[dict[str, Any]] | None, list[dict[str, Any]] | None]: + cli_matrix_include: list[dict[str, Any]] | None = None if options.matrix_filters: cli_matrix_include = [{}] for s in options.matrix_filters: @@ -257,7 +259,7 @@ def _resolve_matrix_filters( return matrix_include, matrix_exclude -def _run_preflight(executor) -> int: +def _run_preflight(executor: JobExecutor) -> int: try: act_version = executor.check_act() print_info(f"Using {act_version}") diff --git a/cli/localci/cli/status.py b/cli/localci/cli/status.py index b008013..0d04b6c 100644 --- a/cli/localci/cli/status.py +++ b/cli/localci/cli/status.py @@ -9,6 +9,7 @@ import json import time from pathlib import Path +from typing import Any import click @@ -178,13 +179,19 @@ def _safe_float(value: object, default: float = 0.0) -> float: """Coerce *value* to float, returning *default* on None or parse failure.""" if value is None: return default - try: - return float(value) - except (TypeError, ValueError): + if isinstance(value, bool): return default + if isinstance(value, (int, float)): + return float(value) + if isinstance(value, str): + try: + return float(value) + except ValueError: + return default + return default -def _job_list(data: dict, key: str) -> list[dict]: +def _job_list(data: dict[str, Any], key: str) -> list[dict[str, Any]]: """Return a list of dict items from *data[key]*, skipping non-dict entries.""" raw = data.get(key) if isinstance(data, dict) else None if not isinstance(raw, list): @@ -247,7 +254,7 @@ def _follow_status(status_file: Path, output_format: str) -> None: """Poll status file and refresh display until Ctrl+C.""" console.print("\nFollowing... (Ctrl+C to stop)") try: - last_data: dict | None = None + last_data: dict[str, Any] | None = None while True: time.sleep(1) if not status_file.exists(): diff --git a/cli/pyproject.toml b/cli/pyproject.toml index fc9f2d9..0b16b09 100644 --- a/cli/pyproject.toml +++ b/cli/pyproject.toml @@ -42,10 +42,7 @@ markers = [ ] norecursedirs = ["integration"] -# Mypy baseline: legacy CLI modules suppressed via per-module ignore_errors -# overrides below. localci.core.* and localci.utils.* pass strict mypy with -# zero errors. New modules must not use ignore_errors; remove overrides as -# legacy CLI modules are typed. +# Mypy: strict mode for all localci packages (core, utils, cli). [tool.mypy] python_version = "3.10" strict = true @@ -54,19 +51,6 @@ warn_redundant_casts = true warn_unused_ignores = true packages = ["localci"] -[[tool.mypy.overrides]] -module = [ - "localci.cli.analyze", - "localci.cli.config", - "localci.cli.list", - "localci.cli.logs", - "localci.cli.run.click_options", - "localci.cli.run.patcher", - "localci.cli.run.run_flow", - "localci.cli.status", -] -ignore_errors = true - [tool.ruff] target-version = "py310" line-length = 88