Use Case

/

代码维护

重构代码

Model

Claude 3.7 十四行诗

将一个 300 行的单文件 Python 待办事项 CLI 重构为包含类型提示、文档字符串和 pytest 测试的模块化类——交付重构后的代码。

Try Query
Claude 3.7 十四行诗
/
编码员

重构 Python 待办事项 CLI 应用程序

我将使用适当的类型提示、文档字符串和测试将待办事项 CLI 应用程序重构为模块化结构。以下是完整的实现:

项目结构

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

实施

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/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."""

改进摘要

  1. 模块化结构:将代码拆分为逻辑模块(模型、存储、服务、CLI)
  2. 键入提示: 在整个代码库中添加了全面的类型提示
  3. 文档字符串: 为所有类和方法添加了详细的文档字符串
  4. 单元测试: 为所有组件创建了全面的测试
  5. 关注点分离:
    • 模型处理数据表示
    • 存储处理持久性
    • 服务处理业务逻辑
    • CLI 处理用户交互
  6. 错误处理: 在整个代码库中添加了正确的错误处理
  7. 可配置性: 使组件可配置(例如,自定义存储路径)
  8. 干净的代码实践: 使用一致的命名、格式和结构

现在,这种重构后的代码更具可维护、可测试和可扩展性,同时保留了所有原始功能。

Experience Ninja's AI Assistant

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