Use Case
/
代码维护
重构代码
Model
•
Claude 3.7 十四行诗
将一个 300 行的单文件 Python 待办事项 CLI 重构为包含类型提示、文档字符串和 pytest 测试的模块化类——交付重构后的代码。
重构 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 Nonetests/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 == 3tests/__init__.py
1"""Tests package."""改进摘要
- 模块化结构:将代码拆分为逻辑模块(模型、存储、服务、CLI)
- 键入提示: 在整个代码库中添加了全面的类型提示
- 文档字符串: 为所有类和方法添加了详细的文档字符串
- 单元测试: 为所有组件创建了全面的测试
- 关注点分离:
- 模型处理数据表示
- 存储处理持久性
- 服务处理业务逻辑
- CLI 处理用户交互
- 错误处理: 在整个代码库中添加了正确的错误处理
- 可配置性: 使组件可配置(例如,自定义存储路径)
- 干净的代码实践: 使用一致的命名、格式和结构
现在,这种重构后的代码更具可维护、可测试和可扩展性,同时保留了所有原始功能。
Experience Ninja's AI Assistant
Try for free today. Plans starting at $19/month.

