feat: Complete type safety improvements for CLI and service layers

Implement comprehensive type annotations and mypy configuration as part
of code quality initiative. Achieve 100% type annotation coverage for
main CLI entry points and resolve Optional type inconsistencies.

## Key Improvements

### CLI Layer (100% Type Coverage)
- tddai_cli.py: Complete type annotations for all 21 functions
- cli/core.py: Full type coverage for CLI framework (20 functions)
- cli/commands/issues.py: Fixed Optional[List[str]] parameter types
- cli/commands/workspace.py: Improved type checker logic for Optional handling

### Service Layer Type Safety
- services/issue_service.py: Fixed Optional parameter type signatures
- services/project_service.py: Updated Optional type annotations
- tddai/issue_creator.py: Proper Optional[List[str]] usage
- tddai/project_manager.py: Fixed Optional parameter handling

### Mypy Configuration
- pyproject.toml: Added comprehensive mypy configuration
- Gradual adoption strategy with module-specific strictness
- Python 3.12 compatibility for proper type checking
- Incremental typing approach for legacy modules

## Technical Details
- Proper Optional vs Union type usage throughout
- Generic type annotations for collections
- Return type annotations for all public functions
- Fixed implicit Optional violations (PEP 484)
- Type checker logic improvements for better safety

## Benefits
- Improved IDE autocomplete and error detection
- Compile-time type checking for CLI commands
- Better maintainability and debugging capabilities
- Foundation for expanding type safety to remaining modules

Resolves #27 - Type safety improvements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-27 09:02:31 +02:00
parent f782ac1f69
commit a3093e1443
9 changed files with 132 additions and 55 deletions

View File

@@ -2,7 +2,7 @@
Issue CLI commands. Issue CLI commands.
""" """
from typing import List from typing import List, Optional, Any
from tddai import TddaiError from tddai import TddaiError
from services import IssueService from services import IssueService
@@ -12,7 +12,7 @@ from cli.presenters import OutputFormatter, IssueView
class IssueCommands: class IssueCommands:
"""Commands for issue operations.""" """Commands for issue operations."""
def __init__(self): def __init__(self) -> None:
self.service = IssueService() self.service = IssueService()
def list_issues(self) -> None: def list_issues(self) -> None:
@@ -53,8 +53,8 @@ class IssueCommands:
def create_enhancement_issue(self, title: str, use_case: str, def create_enhancement_issue(self, title: str, use_case: str,
technical_requirements: str = "", technical_requirements: str = "",
acceptance_criteria: List[str] = None, acceptance_criteria: Optional[List[str]] = None,
dependencies: List[str] = None, dependencies: Optional[List[str]] = None,
priority: str = "Medium") -> None: priority: str = "Medium") -> None:
"""Create a structured enhancement issue.""" """Create a structured enhancement issue."""
try: try:
@@ -82,7 +82,7 @@ class IssueCommands:
except TddaiError as e: except TddaiError as e:
OutputFormatter.exit_with_error(f"Error creating enhancement issue: {e}") OutputFormatter.exit_with_error(f"Error creating enhancement issue: {e}")
def create_from_template(self, template_file: str, **kwargs) -> None: def create_from_template(self, template_file: str, **kwargs: Any) -> None:
"""Create issue from template file.""" """Create issue from template file."""
try: try:
OutputFormatter.info(f"Creating issue from template: {template_file}") OutputFormatter.info(f"Creating issue from template: {template_file}")

View File

@@ -47,6 +47,7 @@ class WorkspaceCommands:
OutputFormatter.error("No active issue workspace") OutputFormatter.error("No active issue workspace")
print(" Nothing to finish") print(" Nothing to finish")
OutputFormatter.exit_with_error("", 1) OutputFormatter.exit_with_error("", 1)
return # Explicit return for type checker
# Get test count before finishing # Get test count before finishing
summary = self.service.get_workspace_summary() summary = self.service.get_workspace_summary()

View File

@@ -4,75 +4,76 @@ CLI framework core.
Provides the main CLI framework and command delegation. Provides the main CLI framework and command delegation.
""" """
from typing import Any
from .commands import WorkspaceCommands, IssueCommands, ProjectCommands, ExportCommands from .commands import WorkspaceCommands, IssueCommands, ProjectCommands, ExportCommands
class CLIFramework: class CLIFramework:
"""Main CLI framework that delegates to command classes.""" """Main CLI framework that delegates to command classes."""
def __init__(self): def __init__(self) -> None:
self.workspace = WorkspaceCommands() self.workspace = WorkspaceCommands()
self.issues = IssueCommands() self.issues = IssueCommands()
self.project = ProjectCommands() self.project = ProjectCommands()
self.export = ExportCommands() self.export = ExportCommands()
# Workspace operations # Workspace operations
def workspace_status(self): def workspace_status(self) -> None:
return self.workspace.status() return self.workspace.status()
def start_issue(self, issue_number: int): def start_issue(self, issue_number: int) -> None:
return self.workspace.start_issue(issue_number) return self.workspace.start_issue(issue_number)
def finish_issue(self): def finish_issue(self) -> None:
return self.workspace.finish_issue() return self.workspace.finish_issue()
def add_test_guidance(self): def add_test_guidance(self) -> None:
return self.workspace.add_test_guidance() return self.workspace.add_test_guidance()
# Issue operations # Issue operations
def list_issues(self): def list_issues(self) -> None:
return self.issues.list_issues() return self.issues.list_issues()
def list_open_issues(self): def list_open_issues(self) -> None:
return self.issues.list_open_issues() return self.issues.list_open_issues()
def show_issue(self, issue_number: int): def show_issue(self, issue_number: int) -> None:
return self.issues.show_issue(issue_number) return self.issues.show_issue(issue_number)
def create_issue(self, title: str, body: str, issue_type: str = "enhancement"): def create_issue(self, title: str, body: str, issue_type: str = "enhancement") -> None:
return self.issues.create_issue(title, body, issue_type) return self.issues.create_issue(title, body, issue_type)
def create_enhancement_issue(self, title: str, use_case: str, **kwargs): def create_enhancement_issue(self, title: str, use_case: str, **kwargs: Any) -> None:
return self.issues.create_enhancement_issue(title, use_case, **kwargs) return self.issues.create_enhancement_issue(title, use_case, **kwargs)
def create_from_template(self, template_file: str, **kwargs): def create_from_template(self, template_file: str, **kwargs: Any) -> None:
return self.issues.create_from_template(template_file, **kwargs) return self.issues.create_from_template(template_file, **kwargs)
def analyze_coverage(self, issue_number: int): def analyze_coverage(self, issue_number: int) -> None:
return self.issues.analyze_coverage(issue_number) return self.issues.analyze_coverage(issue_number)
# Project management operations # Project management operations
def setup_project_management(self): def setup_project_management(self) -> None:
return self.project.setup_project_management() return self.project.setup_project_management()
def move_issue_to_state(self, issue_number: int, state: str): def move_issue_to_state(self, issue_number: int, state: str) -> None:
return self.project.move_issue_to_state(issue_number, state) return self.project.move_issue_to_state(issue_number, state)
def set_issue_priority(self, issue_number: int, priority: str): def set_issue_priority(self, issue_number: int, priority: str) -> None:
return self.project.set_issue_priority(issue_number, priority) return self.project.set_issue_priority(issue_number, priority)
def create_milestone(self, title: str, description: str = ""): def create_milestone(self, title: str, description: str = "") -> None:
return self.project.create_milestone(title, description) return self.project.create_milestone(title, description)
def list_milestones(self): def list_milestones(self) -> None:
return self.project.list_milestones() return self.project.list_milestones()
def assign_issue_to_milestone(self, issue_number: int, milestone_id: int): def assign_issue_to_milestone(self, issue_number: int, milestone_id: int) -> None:
return self.project.assign_issue_to_milestone(issue_number, milestone_id) return self.project.assign_issue_to_milestone(issue_number, milestone_id)
def project_overview(self): def project_overview(self) -> None:
return self.project.project_overview() return self.project.project_overview()
# Export operations # Export operations
def issue_index(self, **kwargs): def issue_index(self, **kwargs: Any) -> None:
return self.export.issue_index(**kwargs) return self.export.issue_index(**kwargs)

View File

@@ -16,3 +16,77 @@ markitect = "markitect.cli:main"
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
include = ["markitect*"] include = ["markitect*"]
exclude = ["tests*", "wiki*", "tddai*"] exclude = ["tests*", "wiki*", "tddai*"]
[tool.mypy]
# Basic mypy configuration for MarkiTect project
python_version = "3.12"
warn_return_any = true
warn_unused_configs = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
strict_optional = true
disallow_untyped_calls = false # Gradual adoption
disallow_untyped_defs = false # Gradual adoption
disallow_incomplete_defs = false # Gradual adoption
check_untyped_defs = true
disallow_untyped_decorators = false # Gradual adoption
no_implicit_optional = true
show_error_codes = true
show_column_numbers = true
pretty = true
# File patterns to exclude from type checking
exclude = [
"^build/.*",
"^dist/.*",
"^\\.venv/.*",
"^\\.markitect_workspace/.*",
"^tests/.*", # Exclude tests for now during gradual adoption
]
# Module-specific configurations for incremental adoption
[[tool.mypy.overrides]]
module = [
"infrastructure.logging.*",
"infrastructure.repositories.*",
"infrastructure.exceptions",
"infrastructure.config",
"domain.*"
]
# Stricter settings for well-typed modules
disallow_untyped_defs = true
disallow_incomplete_defs = true
warn_unused_ignores = true
[[tool.mypy.overrides]]
module = [
"tddai_cli",
"markitect.cli",
"cli.*"
]
# Medium strictness for CLI modules (target for improvement)
disallow_incomplete_defs = true
check_untyped_defs = true
[[tool.mypy.overrides]]
module = [
"markitect.*",
"services.*",
"gitea.*"
]
# Basic type checking for legacy modules
check_untyped_defs = true
warn_return_any = false # Less strict for legacy code
# External library stubs
[[tool.mypy.overrides]]
module = [
"markdown_it.*",
"jsonpath_ng.*",
"click.*",
"tabulate.*",
"yaml.*"
]
ignore_missing_imports = true

View File

@@ -35,8 +35,8 @@ class IssueService:
def create_enhancement_issue(self, title: str, use_case: str, def create_enhancement_issue(self, title: str, use_case: str,
technical_requirements: str = "", technical_requirements: str = "",
acceptance_criteria: List[str] = None, acceptance_criteria: Optional[List[str]] = None,
dependencies: List[str] = None, dependencies: Optional[List[str]] = None,
priority: str = "Medium") -> Dict[str, Any]: priority: str = "Medium") -> Dict[str, Any]:
"""Create a structured enhancement issue.""" """Create a structured enhancement issue."""
return self.issue_creator.create_enhancement_issue( return self.issue_creator.create_enhancement_issue(

View File

@@ -2,7 +2,7 @@
Project service - business logic for project management operations. Project service - business logic for project management operations.
""" """
from typing import List, Dict, Any from typing import List, Dict, Any, Optional
from tddai.project_manager import ProjectManager, ProjectState, Priority, Milestone, Label from tddai.project_manager import ProjectManager, ProjectState, Priority, Milestone, Label
from tddai import TddaiError from tddai import TddaiError
@@ -18,7 +18,7 @@ class ProjectService:
"""Setup project management labels and structure.""" """Setup project management labels and structure."""
self.project_manager.ensure_project_labels() self.project_manager.ensure_project_labels()
def create_milestone(self, title: str, description: str = "", due_date: str = None) -> Milestone: def create_milestone(self, title: str, description: str = "", due_date: Optional[str] = None) -> Milestone:
"""Create a new milestone (project).""" """Create a new milestone (project)."""
return self.project_manager.create_milestone(title, description, due_date) return self.project_manager.create_milestone(title, description, due_date)

View File

@@ -71,8 +71,8 @@ class IssueCreator:
def create_enhancement_issue(self, title: str, use_case: str, def create_enhancement_issue(self, title: str, use_case: str,
technical_requirements: str = "", technical_requirements: str = "",
acceptance_criteria: List[str] = None, acceptance_criteria: Optional[List[str]] = None,
dependencies: List[str] = None, dependencies: Optional[List[str]] = None,
priority: str = "Medium") -> Dict[str, Any]: priority: str = "Medium") -> Dict[str, Any]:
"""Create an enhancement issue with structured format. """Create an enhancement issue with structured format.
@@ -123,7 +123,7 @@ class IssueCreator:
) )
def create_bug_issue(self, title: str, description: str, def create_bug_issue(self, title: str, description: str,
steps_to_reproduce: List[str] = None, steps_to_reproduce: Optional[List[str]] = None,
expected_behavior: str = "", expected_behavior: str = "",
actual_behavior: str = "", actual_behavior: str = "",
environment: str = "") -> Dict[str, Any]: environment: str = "") -> Dict[str, Any]:

View File

@@ -82,7 +82,7 @@ class ProjectManager:
# Milestone Management (Projects) # Milestone Management (Projects)
def create_milestone(self, title: str, description: str = "", due_date: str = None) -> Milestone: def create_milestone(self, title: str, description: str = "", due_date: Optional[str] = None) -> Milestone:
"""Create a new milestone (project).""" """Create a new milestone (project)."""
try: try:
return self.gitea_client.milestones.create(title, description, due_date) return self.gitea_client.milestones.create(title, description, due_date)

View File

@@ -9,6 +9,7 @@ Business logic is handled by services, presentation by CLI framework.
import sys import sys
import argparse import argparse
from pathlib import Path from pathlib import Path
from typing import Optional, Any
# Add current directory to path so we can import modules # Add current directory to path so we can import modules
sys.path.insert(0, str(Path(__file__).parent)) sys.path.insert(0, str(Path(__file__).parent))
@@ -16,9 +17,9 @@ sys.path.insert(0, str(Path(__file__).parent))
from cli import CLIFramework from cli import CLIFramework
# Lazy initialization of CLI framework # Lazy initialization of CLI framework
_cli_framework = None _cli_framework: Optional[CLIFramework] = None
def _get_cli(): def _get_cli() -> CLIFramework:
"""Get CLI framework instance (lazy initialization).""" """Get CLI framework instance (lazy initialization)."""
global _cli_framework global _cli_framework
if _cli_framework is None: if _cli_framework is None:
@@ -26,49 +27,49 @@ def _get_cli():
return _cli_framework return _cli_framework
def workspace_status(): def workspace_status() -> None:
"""Show current workspace status.""" """Show current workspace status."""
_get_cli().workspace_status() _get_cli().workspace_status()
def start_issue(issue_number: int): def start_issue(issue_number: int) -> None:
"""Start working on an issue.""" """Start working on an issue."""
_get_cli().start_issue(issue_number) _get_cli().start_issue(issue_number)
def finish_issue(): def finish_issue() -> None:
"""Finish current issue workspace.""" """Finish current issue workspace."""
_get_cli().finish_issue() _get_cli().finish_issue()
def add_test_guidance(): def add_test_guidance() -> None:
"""Show guidance for adding tests.""" """Show guidance for adding tests."""
_get_cli().add_test_guidance() _get_cli().add_test_guidance()
def list_issues(): def list_issues() -> None:
"""List all issues.""" """List all issues."""
_get_cli().list_issues() _get_cli().list_issues()
def list_open_issues(): def list_open_issues() -> None:
"""List only open issues.""" """List only open issues."""
_get_cli().list_open_issues() _get_cli().list_open_issues()
def show_issue(issue_number: int): def show_issue(issue_number: int) -> None:
"""Show detailed issue information.""" """Show detailed issue information."""
_get_cli().show_issue(issue_number) _get_cli().show_issue(issue_number)
def create_issue(title: str, body: str, issue_type: str = "enhancement"): def create_issue(title: str, body: str, issue_type: str = "enhancement") -> None:
"""Create a new issue.""" """Create a new issue."""
_get_cli().create_issue(title, body, issue_type) _get_cli().create_issue(title, body, issue_type)
def create_enhancement_issue(title: str, use_case: str, technical_requirements: str = "", def create_enhancement_issue(title: str, use_case: str, technical_requirements: str = "",
acceptance_criteria: str = "", dependencies: str = "", acceptance_criteria: str = "", dependencies: str = "",
priority: str = "Medium"): priority: str = "Medium") -> None:
"""Create a structured enhancement issue.""" """Create a structured enhancement issue."""
# Parse acceptance criteria if provided # Parse acceptance criteria if provided
criteria_list = [] criteria_list = []
@@ -90,52 +91,52 @@ def create_enhancement_issue(title: str, use_case: str, technical_requirements:
) )
def create_from_template(template_file: str, **kwargs): def create_from_template(template_file: str, **kwargs: Any) -> None:
"""Create issue from template file.""" """Create issue from template file."""
_get_cli().create_from_template(template_file, **kwargs) _get_cli().create_from_template(template_file, **kwargs)
def analyze_coverage(issue_number: int): def analyze_coverage(issue_number: int) -> None:
"""Analyze test coverage for a specific issue.""" """Analyze test coverage for a specific issue."""
_get_cli().analyze_coverage(issue_number) _get_cli().analyze_coverage(issue_number)
def setup_project_management(): def setup_project_management() -> None:
"""Setup project management labels and milestones.""" """Setup project management labels and milestones."""
_get_cli().setup_project_management() _get_cli().setup_project_management()
def move_issue_to_state(issue_number: int, state: str): def move_issue_to_state(issue_number: int, state: str) -> None:
"""Move issue to a specific project state.""" """Move issue to a specific project state."""
_get_cli().move_issue_to_state(issue_number, state) _get_cli().move_issue_to_state(issue_number, state)
def set_issue_priority(issue_number: int, priority: str): def set_issue_priority(issue_number: int, priority: str) -> None:
"""Set issue priority.""" """Set issue priority."""
_get_cli().set_issue_priority(issue_number, priority) _get_cli().set_issue_priority(issue_number, priority)
def create_milestone(title: str, description: str = ""): def create_milestone(title: str, description: str = "") -> None:
"""Create a new milestone (project).""" """Create a new milestone (project)."""
_get_cli().create_milestone(title, description) _get_cli().create_milestone(title, description)
def list_milestones(): def list_milestones() -> None:
"""List all milestones.""" """List all milestones."""
_get_cli().list_milestones() _get_cli().list_milestones()
def assign_issue_to_milestone(issue_number: int, milestone_id: int): def assign_issue_to_milestone(issue_number: int, milestone_id: int) -> None:
"""Assign issue to a milestone.""" """Assign issue to a milestone."""
_get_cli().assign_issue_to_milestone(issue_number, milestone_id) _get_cli().assign_issue_to_milestone(issue_number, milestone_id)
def project_overview(): def project_overview() -> None:
"""Show project management overview.""" """Show project management overview."""
_get_cli().project_overview() _get_cli().project_overview()
def issue_index(format_type="tsv", sort_by="number", filter_state=None, filter_priority=None, include_state=False): def issue_index(format_type: str = "tsv", sort_by: str = "number", filter_state: Optional[str] = None, filter_priority: Optional[str] = None, include_state: bool = False) -> None:
"""Output compact index of all issues for Unix processing.""" """Output compact index of all issues for Unix processing."""
_get_cli().issue_index( _get_cli().issue_index(
format_type=format_type, format_type=format_type,
@@ -146,7 +147,7 @@ def issue_index(format_type="tsv", sort_by="number", filter_state=None, filter_p
) )
def main(): def main() -> None:
"""Main CLI entry point.""" """Main CLI entry point."""
parser = argparse.ArgumentParser(description="tddai CLI tool") parser = argparse.ArgumentParser(description="tddai CLI tool")
subparsers = parser.add_subparsers(dest='command', help='Available commands') subparsers = parser.add_subparsers(dest='command', help='Available commands')