Use Case

/

Code Maintenance

Refactor Code

Model

Claude 3.7 Sonnet

Refactor a 300-line, single-file Python to-do CLI into modular classes with type hints, docstrings, and pytest tests—deliver the refactored code.

Try Query
Claude 3.7 Sonnet
/
coder

Refactoring a Python To-Do CLI Application

I'll refactor the to-do CLI application into a modular structure with proper type hints, docstrings, and tests. Here's the complete implementation:

Project Structure

1todo/
2├── __init__.py
3├── cli.py
4├── models.py
5├── storage.py
6├── service.py
7└── tests/
8    ├── __init__.py
9    ├── test_models.py
10    ├── test_storage.py
11    └── test_service.py

Implementation

models.py

1"""Models for the todo application."""
2from dataclasses import dataclass
3from datetime import datetime
4from enum import Enum
5from typing import Optional
6
7
8class TaskStatus(Enum):
9    """Enum representing the status of a task."""
10    PENDING = "pending"
11    COMPLETED = "completed"
12
13
14@dataclass
15class Task:
16    """Represents a task in the todo application."""
17    id: int
18    title: str
19    description: Optional[str] = None
20    status: TaskStatus = TaskStatus.PENDING
21    created_at: datetime = None
22    completed_at: Optional[datetime] = None
23
24    def __post_init__(self):
25        """Initialize default values after initialization."""
26        if self.created_at is None:
27            self.created_at = datetime.now()
28
29    def complete(self) -> None:
30        """Mark the task as completed."""
31        self.status = TaskStatus.COMPLETED
32        self.completed_at = datetime.now()
33
34    def reopen(self) -> None:
35        """Mark the task as pending."""
36        self.status = TaskStatus.PENDING
37        self.completed_at = None
38
39    def to_dict(self) -> dict:
40        """Convert the task to a dictionary for storage."""
41        return {
42            "id": self.id,
43            "title": self.title,
44            "description": self.description,
45            "status": self.status.value,
46            "created_at": self.created_at.isoformat() if self.created_at else None,
47            "completed_at": self.completed_at.isoformat() if self.completed_at else None,
48        }
49
50    @classmethod
51    def from_dict(cls, data: dict) -> "Task":
52        """Create a task from a dictionary."""
53        return cls(
54            id=data["id"],
55            title=data["title"],
56            description=data.get("description"),
57            status=TaskStatus(data["status"]),
58            created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else None,
59            completed_at=datetime.fromisoformat(data["completed_at"]) if data.get("completed_at") else None,
60        )

storage.py

1"""Storage module for the todo application."""
2import json
3import os
4from pathlib import Path
5from typing import Dict, List, Optional
6
7from .models import Task
8
9
10class TaskStorage:
11    """Storage class for tasks."""
12
13    def __init__(self, storage_path: Optional[str] = None):
14        """Initialize the storage with the given path.
15        
16        Args:
17            storage_path: Path to the storage file. If None, uses ~/.todo/tasks.json
18        """
19        if storage_path is None:
20            home_dir = Path.home()
21            self.storage_dir = home_dir / ".todo"
22            self.storage_path = self.storage_dir / "tasks.json"
23        else:
24            self.storage_path = Path(storage_path)
25            self.storage_dir = self.storage_path.parent
26
27        self._ensure_storage_exists()
28
29    def _ensure_storage_exists(self) -> None:
30        """Ensure the storage directory and file exist."""
31        if not self.storage_dir.exists():
32            self.storage_dir.mkdir(parents=True)
33        
34        if not self.storage_path.exists():
35            with open(self.storage_path, "w") as f:
36                json.dump([], f)
37
38    def load_tasks(self) -> List[Task]:
39        """Load tasks from storage.
40        
41        Returns:
42            List of Task objects
43        """
44        try:
45            with open(self.storage_path, "r") as f:
46                tasks_data = json.load(f)
47                return [Task.from_dict(task_data) for task_data in tasks_data]
48        except (json.JSONDecodeError, FileNotFoundError):
49            return []
50
51    def save_tasks(self, tasks: List[Task]) -> None:
52        """Save tasks to storage.
53        
54        Args:
55            tasks: List of Task objects to save
56        """
57        tasks_data = [task.to_dict() for task in tasks]
58        with open(self.storage_path, "w") as f:
59            json.dump(tasks_data, f, indent=2)

service.py

1"""Service layer for the todo application."""
2from datetime import datetime
3from typing import List, Optional
4
5from .models import Task, TaskStatus
6from .storage import TaskStorage
7
8
9class TodoService:
10    """Service class for managing tasks."""
11
12    def __init__(self, storage: Optional[TaskStorage] = None):
13        """Initialize the service with the given storage.
14        
15        Args:
16            storage: TaskStorage instance. If None, creates a new one.
17        """
18        self.storage = storage or TaskStorage()
19        self.tasks = self.storage.load_tasks()
20
21    def add_task(self, title: str, description: Optional[str] = None) -> Task:
22        """Add a new task.
23        
24        Args:
25            title: Title of the task
26            description: Optional description of the task
27            
28        Returns:
29            The newly created Task
30        """
31        task_id = self._generate_id()
32        task = Task(
33            id=task_id,
34            title=title,
35            description=description,
36            created_at=datetime.now()
37        )
38        self.tasks.append(task)
39        self._save()
40        return task
41
42    def get_task(self, task_id: int) -> Optional[Task]:
43        """Get a task by ID.
44        
45        Args:
46            task_id: ID of the task to get
47            
48        Returns:
49            The Task if found, None otherwise
50        """
51        for task in self.tasks:
52            if task.id == task_id:
53                return task
54        return None
55
56    def update_task(self, task_id: int, title: Optional[str] = None, 
57                   description: Optional[str] = None) -> Optional[Task]:
58        """Update a task.
59        
60        Args:
61            task_id: ID of the task to update
62            title: New title (if provided)
63            description: New description (if provided)
64            
65        Returns:
66            The updated Task if found, None otherwise
67        """
68        task = self.get_task(task_id)
69        if task:
70            if title is not None:
71                task.title = title
72            if description is not None:
73                task.description = description
74            self._save()
75        return task
76
77    def complete_task(self, task_id: int) -> Optional[Task]:
78        """Mark a task as completed.
79        
80        Args:
81            task_id: ID of the task to complete
82            
83        Returns:
84            The completed Task if found, None otherwise
85        """
86        task = self.get_task(task_id)
87        if task:
88            task.complete()
89            self._save()
90        return task
91
92    def reopen_task(self, task_id: int) -> Optional[Task]:
93        """Mark a task as pending.
94        
95        Args:
96            task_id: ID of the task to reopen
97            
98        Returns:
99            The reopened Task if found, None otherwise
100        """
101        task = self.get_task(task_id)
102        if task:
103            task.reopen()
104            self._save()
105        return task
106
107    def delete_task(self, task_id: int) -> bool:
108        """Delete a task.
109        
110        Args:
111            task_id: ID of the task to delete
112            
113        Returns:
114            True if the task was deleted, False otherwise
115        """
116        task = self.get_task(task_id)
117        if task:
118            self.tasks.remove(task)
119            self._save()
120            return True
121        return False
122
123    def list_tasks(self, status: Optional[TaskStatus] = None) -> List[Task]:
124        """List tasks, optionally filtered by status.
125        
126        Args:
127            status: Optional status to filter by
128            
129        Returns:
130            List of tasks matching the criteria
131        """
132        if status is None:
133            return self.tasks
134        return [task for task in self.tasks if task.status == status]
135
136    def _generate_id(self) -> int:
137        """Generate a new unique ID for a task.
138        
139        Returns:
140            A new unique ID
141        """
142        if not self.tasks:
143            return 1
144        return max(task.id for task in self.tasks) + 1
145
146    def _save(self) -> None:
147        """Save tasks to storage."""
148        self.storage.save_tasks(self.tasks)

cli.py

1"""Command-line interface for the todo application."""
2import argparse
3import sys
4from typing import List, Optional
5
6from .models import TaskStatus
7from .service import TodoService
8
9
10class TodoCLI:
11    """Command-line interface for the todo application."""
12
13    def __init__(self, service: Optional[TodoService] = None):
14        """Initialize the CLI with the given service.
15        
16        Args:
17            service: TodoService instance. If None, creates a new one.
18        """
19        self.service = service or TodoService()
20
21    def run(self, args: Optional[List[str]] = None) -> None:
22        """Run the CLI with the given arguments.
23        
24        Args:
25            args: Command-line arguments. If None, uses sys.argv[1:]
26        """
27        parser = self._create_parser()
28        parsed_args = parser.parse_args(args)
29        
30        if not hasattr(parsed_args, 'func'):
31            parser.print_help()
32            return
33
34        parsed_args.func(parsed_args)
35
36    def _create_parser(self) -> argparse.ArgumentParser:
37        """Create the argument parser.
38        
39        Returns:
40            Configured ArgumentParser
41        """
42        parser = argparse.ArgumentParser(description="Todo CLI Application")
43        subparsers = parser.add_subparsers(dest="command", help="Command to run")
44
45        # Add task
46        add_parser = subparsers.add_parser("add", help="Add a new task")
47        add_parser.add_argument("title", help="Task title")
48        add_parser.add_argument("-d", "--description", help="Task description")
49        add_parser.set_defaults(func=self._handle_add)
50
51        # List tasks
52        list_parser = subparsers.add_parser("list", help="List tasks")
53        list_parser.add_argument(
54            "-s", 
55            "--status", 
56            choices=["pending", "completed", "all"],
57            default="all",
58            help="Filter tasks by status"
59        )
60        list_parser.set_defaults(func=self._handle_list)
61
62        # View task
63        view_parser = subparsers.add_parser("view", help="View task details")
64        view_parser.add_argument("id", type=int, help="Task ID")
65        view_parser.set_defaults(func=self._handle_view)
66
67        # Update task
68        update_parser = subparsers.add_parser("update", help="Update a task")
69        update_parser.add_argument("id", type=int, help="Task ID")
70        update_parser.add_argument("-t", "--title", help="New task title")
71        update_parser.add_argument("-d", "--description", help="New task description")
72        update_parser.set_defaults(func=self._handle_update)
73
74        # Complete task
75        complete_parser = subparsers.add_parser("complete", help="Mark a task as completed")
76        complete_parser.add_argument("id", type=int, help="Task ID")
77        complete_parser.set_defaults(func=self._handle_complete)
78
79        # Reopen task
80        reopen_parser = subparsers.add_parser("reopen", help="Mark a task as pending")
81        reopen_parser.add_argument("id", type=int, help="Task ID")
82        reopen_parser.set_defaults(func=self._handle_reopen)
83
84        # Delete task
85        delete_parser = subparsers.add_parser("delete", help="Delete a task")
86        delete_parser.add_argument("id", type=int, help="Task ID")
87        delete_parser.set_defaults(func=self._handle_delete)
88
89        return parser
90
91    def _handle_add(self, args: argparse.Namespace) -> None:
92        """Handle the 'add' command."""
93        task = self.service.add_task(args.title, args.description)
94        print(f"Task added with ID: {task.id}")
95
96    def _handle_list(self, args: argparse.Namespace) -> None:
97        """Handle the 'list' command."""
98        status_filter = None
99        if args.status == "pending":
100            status_filter = TaskStatus.PENDING
101        elif args.status == "completed":
102            status_filter = TaskStatus.COMPLETED
103
104        tasks = self.service.list_tasks(status_filter)
105        
106        if not tasks:
107            print("No tasks found.")
108            return
109
110        print(f"{'ID':<5} {'Status':<10} {'Title':<30}")
111        print("-" * 45)
112        
113        for task in tasks:
114            status_str = "✓" if task.status == TaskStatus.COMPLETED else "○"
115            print(f"{task.id:<5} {status_str:<10} {task.title:<30}")
116
117    def _handle_view(self, args: argparse.Namespace) -> None:
118        """Handle the 'view' command."""
119        task = self.service.get_task(args.id)
120        if not task:
121            print(f"Task with ID {args.id} not found.")
122            return
123
124        status_str = "Completed" if task.status == TaskStatus.COMPLETED else "Pending"
125        print(f"ID: {task.id}")
126        print(f"Title: {task.title}")
127        print(f"Status: {status_str}")
128        
129        if task.description:
130            print(f"Description: {task.description}")
131        
132        print(f"Created: {task.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
133        
134        if task.completed_at:
135            print(f"Completed: {task.completed_at.strftime('%Y-%m-%d %H:%M:%S')}")
136
137    def _handle_update(self, args: argparse.Namespace) -> None:
138        """Handle the 'update' command."""
139        if not args.title and not args.description:
140            print("Nothing to update. Specify --title or --description.")
141            return
142
143        task = self.service.update_task(args.id, args.title, args.description)
144        if not task:
145            print(f"Task with ID {args.id} not found.")
146            return
147
148        print(f"Task {args.id} updated successfully.")
149
150    def _handle_complete(self, args: argparse.Namespace) -> None:
151        """Handle the 'complete' command."""
152        task = self.service.complete_task(args.id)
153        if not task:
154            print(f"Task with ID {args.id} not found.")
155            return
156
157        print(f"Task {args.id} marked as completed.")
158
159    def _handle_reopen(self, args: argparse.Namespace) -> None:
160        """Handle the 'reopen' command."""
161        task = self.service.reopen_task(args.id)
162        if not task:
163            print(f"Task with ID {args.id} not found.")
164            return
165
166        print(f"Task {args.id} marked as pending.")
167
168    def _handle_delete(self, args: argparse.Namespace) -> None:
169        """Handle the 'delete' command."""
170        success = self.service.delete_task(args.id)
171        if not success:
172            print(f"Task with ID {args.id} not found.")
173            return
174
175        print(f"Task {args.id} deleted successfully.")
176
177
178def main() -> None:
179    """Entry point for the CLI."""
180    cli = TodoCLI()
181    cli.run()
182
183
184if __name__ == "__main__":
185    main()

__init__.py

1"""Todo application package."""
2from .models import Task, TaskStatus
3from .service import TodoService
4from .storage import TaskStorage
5from .cli import TodoCLI
6
7__all__ = ["Task", "TaskStatus", "TodoService", "TaskStorage", "TodoCLI"]

Tests

tests/test_models.py

1"""Tests for the models module."""
2import pytest
3from datetime import datetime
4from todo.models import Task, TaskStatus
5
6
7def test_task_initialization():
8    """Test that a task can be initialized with the correct values."""
9    task = Task(id=1, title="Test Task", description="Test Description")
10    
11    assert task.id == 1
12    assert task.title == "Test Task"
13    assert task.description == "Test Description"
14    assert task.status == TaskStatus.PENDING
15    assert task.created_at is not None
16    assert task.completed_at is None
17
18
19def test_task_complete():
20    """Test that a task can be marked as completed."""
21    task = Task(id=1, title="Test Task")
22    task.complete()
23    
24    assert task.status == TaskStatus.COMPLETED
25    assert task.completed_at is not None
26
27
28def test_task_reopen():
29    """Test that a task can be reopened."""
30    task = Task(id=1, title="Test Task")
31    task.complete()
32    task.reopen()
33    
34    assert task.status == TaskStatus.PENDING
35    assert task.completed_at is None
36
37
38def test_task_to_dict():
39    """Test that a task can be converted to a dictionary."""
40    created_at = datetime(2023, 1, 1, 12, 0, 0)
41    task = Task(id=1, title="Test Task", description="Test Description", created_at=created_at)
42    
43    task_dict = task.to_dict()
44    
45    assert task_dict["id"] == 1
46    assert task_dict["title"] == "Test Task"
47    assert task_dict["description"] == "Test Description"
48    assert task_dict["status"] == "pending"
49    assert task_dict["created_at"] == created_at.isoformat()
50    assert task_dict["completed_at"] is None
51
52
53def test_task_from_dict():
54    """Test that a task can be created from a dictionary."""
55    created_at = datetime(2023, 1, 1, 12, 0, 0)
56    task_dict = {
57        "id": 1,
58        "title": "Test Task",
59        "description": "Test Description",
60        "status": "pending",
61        "created_at": created_at.isoformat(),
62        "completed_at": None
63    }
64    
65    task = Task.from_dict(task_dict)
66    
67    assert task.id == 1
68    assert task.title == "Test Task"
69    assert task.description == "Test Description"
70    assert task.status == TaskStatus.PENDING
71    assert task.created_at == created_at
72    assert task.completed_at is None

tests/test_storage.py

1"""Tests for the storage module."""
2import json
3import os
4import tempfile
5from pathlib import Path
6
7import pytest
8
9from todo.models import Task
10from todo.storage import TaskStorage
11
12
13@pytest.fixture
14def temp_storage_path():
15    """Create a temporary file for storage."""
16    with tempfile.NamedTemporaryFile(delete=False) as f:
17        yield f.name
18    os.unlink(f.name)
19
20
21def test_storage_initialization(temp_storage_path):
22    """Test that storage is initialized correctly."""
23    storage = TaskStorage(temp_storage_path)
24    
25    assert storage.storage_path == Path(temp_storage_path)
26    assert storage.storage_path.exists()
27
28
29def test_save_and_load_tasks(temp_storage_path):
30    """Test that tasks can be saved and loaded."""
31    storage = TaskStorage(temp_storage_path)
32    
33    task1 = Task(id=1, title="Task 1")
34    task2 = Task(id=2, title="Task 2", description="Description 2")
35    
36    storage.save_tasks([task1, task2])
37    
38    loaded_tasks = storage.load_tasks()
39    
40    assert len(loaded_tasks) == 2
41    assert loaded_tasks[0].id == 1
42    assert loaded_tasks[0].title == "Task 1"
43    assert loaded_tasks[1].id == 2
44    assert loaded_tasks[1].title == "Task 2"
45    assert loaded_tasks[1].description == "Description 2"
46
47
48def test_load_tasks_empty_file(temp_storage_path):
49    """Test that loading from an empty file returns an empty list."""
50    with open(temp_storage_path, "w") as f:
51        f.write("")
52    
53    storage = TaskStorage(temp_storage_path)
54    tasks = storage.load_tasks()
55    
56    assert tasks == []
57
58
59def test_load_tasks_invalid_json(temp_storage_path):
60    """Test that loading from an invalid JSON file returns an empty list."""
61    with open(temp_storage_path, "w") as f:
62        f.write("invalid json")
63    
64    storage = TaskStorage(temp_storage_path)
65    tasks = storage.load_tasks()
66    
67    assert tasks == []

tests/test_service.py

1"""Tests for the service module."""
2import pytest
3from unittest.mock import MagicMock
4
5from todo.models import Task, TaskStatus
6from todo.service import TodoService
7from todo.storage import TaskStorage
8
9
10@pytest.fixture
11def mock_storage():
12    """Create a mock storage."""
13    storage = MagicMock(spec=TaskStorage)
14    storage.load_tasks.return_value = []
15    return storage
16
17
18@pytest.fixture
19def service(mock_storage):
20    """Create a service with mock storage."""
21    return TodoService(storage=mock_storage)
22
23
24def test_add_task(service, mock_storage):
25    """Test adding a task."""
26    task = service.add_task("Test Task", "Test Description")
27    
28    assert task.id == 1
29    assert task.title == "Test Task"
30    assert task.description == "Test Description"
31    assert task.status == TaskStatus.PENDING
32    assert mock_storage.save_tasks.called
33
34
35def test_get_task(service):
36    """Test getting a task by ID."""
37    task1 = service.add_task("Task 1")
38    task2 = service.add_task("Task 2")
39    
40    assert service.get_task(1) == task1
41    assert service.get_task(2) == task2
42    assert service.get_task(3) is None
43
44
45def test_update_task(service, mock_storage):
46    """Test updating a task."""
47    task = service.add_task("Task 1", "Description 1")
48    
49    updated_task = service.update_task(1, title="Updated Task", description="Updated Description")
50    
51    assert updated_task.title == "Updated Task"
52    assert updated_task.description == "Updated Description"
53    assert mock_storage.save_tasks.call_count == 2
54
55
56def test_update_task_not_found(service, mock_storage):
57    """Test updating a non-existent task."""
58    result = service.update_task(999, title="Updated Task")
59    
60    assert result is None
61    assert mock_storage.save_tasks.call_count == 0
62
63
64def test_complete_task(service, mock_storage):
65    """Test completing a task."""
66    task = service.add_task("Task 1")
67    
68    completed_task = service.complete_task(1)
69    
70    assert completed_task.status == TaskStatus.COMPLETED
71    assert completed_task.completed_at is not None
72    assert mock_storage.save_tasks.call_count == 2
73
74
75def test_complete_task_not_found(service, mock_storage):
76    """Test completing a non-existent task."""
77    result = service.complete_task(999)
78    
79    assert result is None
80    assert mock_storage.save_tasks.call_count == 0
81
82
83def test_reopen_task(service, mock_storage):
84    """Test reopening a task."""
85    task = service.add_task("Task 1")
86    service.complete_task(1)
87    
88    reopened_task = service.reopen_task(1)
89    
90    assert reopened_task.status == TaskStatus.PENDING
91    assert reopened_task.completed_at is None
92    assert mock_storage.save_tasks.call_count == 3
93
94
95def test_delete_task(service, mock_storage):
96    """Test deleting a task."""
97    task = service.add_task("Task 1")
98    
99    result = service.delete_task(1)
100    
101    assert result is True
102    assert len(service.tasks) == 0
103    assert mock_storage.save_tasks.call_count == 2
104
105
106def test_delete_task_not_found(service, mock_storage):
107    """Test deleting a non-existent task."""
108    result = service.delete_task(999)
109    
110    assert result is False
111    assert mock_storage.save_tasks.call_count == 0
112
113
114def test_list_tasks(service):
115    """Test listing tasks."""
116    task1 = service.add_task("Task 1")
117    task2 = service.add_task("Task 2")
118    service.complete_task(2)
119    
120    all_tasks = service.list_tasks()
121    pending_tasks = service.list_tasks(TaskStatus.PENDING)
122    completed_tasks = service.list_tasks(TaskStatus.COMPLETED)
123    
124    assert len(all_tasks) == 2
125    assert len(pending_tasks) == 1
126    assert pending_tasks[0] == task1
127    assert len(completed_tasks) == 1
128    assert completed_tasks[0] == task2
129
130
131def test_generate_id(service):
132    """Test ID generation."""
133    task1 = service.add_task("Task 1")
134    task2 = service.add_task("Task 2")
135    
136    assert task1.id == 1
137    assert task2.id == 2
138    
139    service.delete_task(1)
140    task3 = service.add_task("Task 3")
141    
142    assert task3.id == 3

tests/__init__.py

1"""Tests package."""

Summary of Improvements

  1. Modular Structure: Split the code into logical modules (models, storage, service, CLI)
  2. Type Hints: Added comprehensive type hints throughout the codebase
  3. Docstrings: Added detailed docstrings for all classes and methods
  4. Unit Tests: Created comprehensive tests for all components
  5. Separation of Concerns:
    • Models handle data representation
    • Storage handles persistence
    • Service handles business logic
    • CLI handles user interaction
  6. Error Handling: Added proper error handling throughout the codebase
  7. Configurability: Made components configurable (e.g., custom storage path)
  8. Clean Code Practices: Used consistent naming, formatting, and structure

This refactored code is now more maintainable, testable, and extensible while preserving all the original functionality.

Experience Ninja's AI Assistant

Try for free today. Plans starting at $5/month.