Skip to content

Commit 6c071bc

Browse files
committed
Fix mypy type checking errors
- Add mypy configuration to pyproject.toml with overrides for third-party libraries (typer, rich, httpx, readchar, truststore, platformdirs) - Add TypedDict definitions for AgentConfigItem and StepInfo - Fix implicit Optional type annotations in function signatures - Add proper type annotations for StepTracker class attributes - Fix variable shadowing issue (rename loop variable 'f' to 'failure') - Remove unused sys._specify_tracker_active attribute - Add assertion for Optional[str] to Path conversion
1 parent f205fa3 commit 6c071bc

File tree

2 files changed

+52
-19
lines changed

2 files changed

+52
-19
lines changed

pyproject.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,21 @@ build-backend = "hatchling.build"
2222
[tool.hatch.build.targets.wheel]
2323
packages = ["src/specify_cli"]
2424

25+
[tool.mypy]
26+
python_version = "3.11"
27+
warn_return_any = true
28+
warn_unused_configs = true
29+
disallow_untyped_defs = false
30+
check_untyped_defs = true
31+
32+
[[tool.mypy.overrides]]
33+
module = [
34+
"typer.*",
35+
"rich.*",
36+
"httpx.*",
37+
"readchar.*",
38+
"truststore.*",
39+
"platformdirs.*",
40+
]
41+
ignore_missing_imports = true
42+

src/specify_cli/__init__.py

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import shlex
3434
import json
3535
from pathlib import Path
36-
from typing import Optional, Tuple
36+
from typing import Callable, Optional, Tuple, TypedDict
3737

3838
import typer
3939
import httpx
@@ -56,6 +56,22 @@
5656
ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
5757
client = httpx.Client(verify=ssl_context)
5858

59+
60+
class AgentConfigItem(TypedDict):
61+
"""Type definition for agent configuration entries."""
62+
name: str
63+
folder: str
64+
install_url: Optional[str]
65+
requires_cli: bool
66+
67+
68+
class StepInfo(TypedDict):
69+
"""Type definition for step tracking entries."""
70+
key: str
71+
label: str
72+
status: str
73+
detail: str
74+
5975
def _github_token(cli_token: str | None = None) -> str | None:
6076
"""Return sanitized GitHub token (cli arg takes precedence) or None."""
6177
return ((cli_token or os.getenv("GH_TOKEN") or os.getenv("GITHUB_TOKEN") or "").strip()) or None
@@ -123,7 +139,7 @@ def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str)
123139
return "\n".join(lines)
124140

125141
# Agent configuration with name, folder, install URL, and CLI tool requirement
126-
AGENT_CONFIG = {
142+
AGENT_CONFIG: dict[str, AgentConfigItem] = {
127143
"copilot": {
128144
"name": "GitHub Copilot",
129145
"folder": ".github/",
@@ -236,11 +252,11 @@ class StepTracker:
236252
"""
237253
def __init__(self, title: str):
238254
self.title = title
239-
self.steps = [] # list of dicts: {key, label, status, detail}
255+
self.steps: list[StepInfo] = []
240256
self.status_order = {"pending": 0, "running": 1, "done": 2, "error": 3, "skipped": 4}
241-
self._refresh_cb = None # callable to trigger UI refresh
257+
self._refresh_cb: Optional[Callable[[], None]] = None
242258

243-
def attach_refresh(self, cb):
259+
def attach_refresh(self, cb: Callable[[], None]) -> None:
244260
self._refresh_cb = cb
245261

246262
def add(self, key: str, label: str):
@@ -335,7 +351,7 @@ def get_key():
335351

336352
return key
337353

338-
def select_with_arrows(options: dict, prompt_text: str = "Select an option", default_key: str = None) -> str:
354+
def select_with_arrows(options: dict[str, str], prompt_text: str = "Select an option", default_key: Optional[str] = None) -> str:
339355
"""
340356
Interactive selection using arrow keys with Rich Live display.
341357
@@ -469,7 +485,7 @@ def run_command(cmd: list[str], check_return: bool = True, capture: bool = False
469485
raise
470486
return None
471487

472-
def check_tool(tool: str, tracker: StepTracker = None) -> bool:
488+
def check_tool(tool: str, tracker: Optional["StepTracker"] = None) -> bool:
473489
"""Check if a tool is installed. Optionally update tracker.
474490
475491
Args:
@@ -500,7 +516,7 @@ def check_tool(tool: str, tracker: StepTracker = None) -> bool:
500516

501517
return found
502518

503-
def is_git_repo(path: Path = None) -> bool:
519+
def is_git_repo(path: Optional[Path] = None) -> bool:
504520
"""Check if the specified path is inside a git repository."""
505521
if path is None:
506522
path = Path.cwd()
@@ -622,7 +638,7 @@ def deep_merge(base: dict, update: dict) -> dict:
622638

623639
return merged
624640

625-
def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None, debug: bool = False, github_token: str = None) -> Tuple[Path, dict]:
641+
def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: Optional[httpx.Client] = None, debug: bool = False, github_token: Optional[str] = None) -> Tuple[Path, dict]:
626642
repo_owner = "github"
627643
repo_name = "spec-kit"
628644
if client is None:
@@ -736,7 +752,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
736752
}
737753
return zip_path, metadata
738754

739-
def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None, debug: bool = False, github_token: str = None) -> Path:
755+
def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: Optional["StepTracker"] = None, client: Optional[httpx.Client] = None, debug: bool = False, github_token: Optional[str] = None) -> Path:
740756
"""Download the latest release and extract it to create a new project.
741757
Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup)
742758
"""
@@ -927,22 +943,22 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None =
927943
console.print(f"[cyan]Updated execute permissions on {updated} script(s) recursively[/cyan]")
928944
if failures:
929945
console.print("[yellow]Some scripts could not be updated:[/yellow]")
930-
for f in failures:
931-
console.print(f" - {f}")
946+
for failure in failures:
947+
console.print(f" - {failure}")
932948

933949
@app.command()
934950
def init(
935-
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"),
936-
ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, amp, shai, or q"),
937-
script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"),
951+
project_name: Optional[str] = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"),
952+
ai_assistant: Optional[str] = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, amp, shai, or q"),
953+
script_type: Optional[str] = typer.Option(None, "--script", help="Script type to use: sh or ps"),
938954
ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"),
939955
no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"),
940956
here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"),
941957
force: bool = typer.Option(False, "--force", help="Force merge/overwrite when using --here (skip confirmation)"),
942958
skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"),
943959
debug: bool = typer.Option(False, "--debug", help="Show verbose diagnostic output for network and extraction failures"),
944-
github_token: str = typer.Option(None, "--github-token", help="GitHub token to use for API requests (or set GH_TOKEN or GITHUB_TOKEN environment variable)"),
945-
):
960+
github_token: Optional[str] = typer.Option(None, "--github-token", help="GitHub token to use for API requests (or set GH_TOKEN or GITHUB_TOKEN environment variable)"),
961+
) -> None:
946962
"""
947963
Initialize a new Specify project from the latest template.
948964
@@ -998,6 +1014,7 @@ def init(
9981014
console.print("[yellow]Operation cancelled[/yellow]")
9991015
raise typer.Exit(0)
10001016
else:
1017+
assert project_name is not None # Validated above
10011018
project_path = Path(project_name).resolve()
10021019
if project_path.exists():
10031020
error_panel = Panel(
@@ -1081,8 +1098,6 @@ def init(
10811098

10821099
tracker = StepTracker("Initialize Specify Project")
10831100

1084-
sys._specify_tracker_active = True
1085-
10861101
tracker.add("precheck", "Check required tools")
10871102
tracker.complete("precheck", "ok")
10881103
tracker.add("ai-select", "Select AI assistant")

0 commit comments

Comments
 (0)