使用 aider 进行代码重构
在这个示例中,我们将展示如何使用 aider 来重构一个 Python 代码库。我们将从一个简单的 Flask web 应用开始,然后逐步改进它。
初始代码
我们从一个基本的 Flask 应用开始,它有两个端点:一个用于获取待办事项列表,另一个用于添加新待办事项。代码组织在一个单独的文件中:
# app.py
from flask import Flask, request, jsonify
app = Flask(__name__)
todos = []
@app.route('/todos', methods=['GET'])
def get_todos():
return jsonify(todos)
@app.route('/todos', methods=['POST'])
def add_todo():
todo = request.json
todos.append(todo)
return jsonify(todo), 201
if __name__ == '__main__':
app.run(debug=True)
重构步骤
1. 将代码拆分为模块
首先,我们要求 aider 将代码拆分为更模块化的结构:
请将这个 Flask 应用重构为更模块化的结构,将路由处理逻辑分离到单独的模块中
aider 回应:
# app.py
from flask import Flask
from routes import todo_routes
app = Flask(__name__)
app.register_blueprint(todo_routes)
if __name__ == '__main__':
app.run(debug=True)
# routes.py
from flask import Blueprint, request, jsonify
todo_routes = Blueprint('todos', __name__)
todos = []
@todo_routes.route('/todos', methods=['GET'])
def get_todos():
return jsonify(todos)
@todo_routes.route('/todos', methods=['POST'])
def add_todo():
todo = request.json
todos.append(todo)
return jsonify(todo), 201
2. 添加数据层
接下来,我们要求将数据访问逻辑分离出来:
请创建一个单独的数据访问层来处理待办事项的存储操作
aider 创建了一个新的仓库模块:
# repository.py
todos = []
def get_all_todos():
return todos.copy()
def add_todo(todo):
todos.append(todo)
return todo
并更新了路由模块:
# routes.py
from flask import Blueprint, request, jsonify
from repository import get_all_todos, add_todo
todo_routes = Blueprint('todos', __name__)
@todo_routes.route('/todos', methods=['GET'])
def get_todos():
return jsonify(get_all_todos())
@todo_routes.route('/todos', methods=['POST'])
def add_todo():
todo = request.json
added_todo = add_todo(todo)
return jsonify(added_todo), 201
3. 添加模型验证
然后,我们要求添加对输入数据的验证:
请为待办事项添加模型验证,确保每个待办事项必须有 title 字段且不为空
aider 添加了一个模型类并更新了路由:
# models.py
class Todo:
def __init__(self, title):
if not title or not isinstance(title, str):
raise ValueError("Title must be a non-empty string")
self.title = title
def to_dict(self):
return {"title": self.title}
# repository.py
from models import Todo
todos = []
def get_all_todos():
return [todo.to_dict() for todo in todos]
def add_todo(todo_data):
todo = Todo(todo_data['title'])
todos.append(todo)
return todo.to_dict()
4. 添加错误处理
最后,我们要求添加适当的错误处理:
请添加错误处理,当请求数据无效时返回 400 错误
aider 更新了路由:
# routes.py
from flask import Blueprint, request, jsonify
from repository import get_all_todos, add_todo
from models import Todo
todo_routes = Blueprint('todos', __name__)
@todo_routes.route('/todos', methods=['GET'])
def get_todos():
return jsonify(get_all_todos())
@todo_routes.route('/todos', methods=['POST'])
def add_todo():
try:
todo = request.json
if not todo or 'title' not in todo:
raise ValueError("Missing title")
added_todo = add_todo(todo)
return jsonify(added_todo), 201
except ValueError as e:
return jsonify({"error": str(e)}), 400
最终结构
经过这些重构步骤后,我们的代码库现在具有更清晰的结构:
.
├── app.py
├── models.py
├── repository.py
└── routes.py
每个文件都有明确的职责:
app.py
: 应用入口和配置models.py
: 数据模型和验证repository.py
: 数据存储和访问routes.py
: HTTP 路由和处理
总结
这个示例展示了如何使用 aider 逐步重构代码:
- 将代码拆分为模块
- 分离数据访问层
- 添加模型验证
- 改进错误处理
aider 能够理解我们的意图,并根据最佳实践生成相应的重构代码。这使得重构过程更加高效和安全。
创建”黑盒”测试用例
这段对话展示了 aider 如何创建一个黑盒测试用例,在未获得被测试函数源代码或仓库中任何其他代码访问权限的情况下。
aider 完全依赖于基于 ctags 的仓库高层级映射来操作,该映射提供了仓库中所有符号和函数签名的详细信息。仅使用映射中的元数据,大型语言模型就能理解如何调用待测试的方法,以及如何实例化多个为测试准备所需的类对象。
具体来说,aider 能够利用仓库映射:
- 找到用户想要测试的
cmd_add()
函数的签名 - 确定它是
Command
类的一个方法,因此测试用例需要实例化一个对象来进行测试 - 识别出创建
Command
实例需要传入InputOutput
和Coder
实例 - 找出实例化
InputOutput
实例所需的参数 - 判断
Coder
类足够复杂,适合使用MagickMock
cmd_add()
方法定义时没有类型注解,所以 aider 合理但错误地猜测它接受一个文件 list
。用户执行 /run
命令来运行新单元测试时,由于这个误解而抛出错误。
大型语言模型检查错误信息后,修正了对 cmd_add()
的调用,正确地传入一个用空格分隔文件名的 str
。测试在下一次 /run
时通过了。
$ aider tests/test_commands.py
创建空文件 tests/test_commands.py
将 tests/test_commands.py 添加到对话
未在 .git 中跟踪的文件:
- tests/test_commands.py
是否添加? y
将 tests/test_commands.py 添加到 git 仓库
提交 b9cea2c 将新文件添加到 git 仓库:tests/test_commands.py
使用 git 仓库:.git
为 cmd_add 添加一个测试,传入 ‘foo.txt’ 和 ‘bar.txt’ 并确保测试后这两个文件都被创建。测试在临时目录中运行。
我将在 tests/test_commands.py
文件中添加一个名为 test_cmd_add
的新测试方法。这个测试会创建一个临时目录,将当前工作目录切换到临时目录,然后调用 cmd_add
方法并传入 ‘foo.txt’ 和 ‘bar.txt’ 作为参数。之后,它会检查这两个文件是否已在临时目录中被创建。
以下是代码变更:
tests/test_commands.py
<<<<<<< ORIGINAL
=======
import os
import tempfile
from unittest import TestCase
from unittest.mock import MagicMock
from aider.commands import Commands
from aider.io import InputOutput
class TestCommands(TestCase):
def test_cmd_add(self):
# 创建临时目录并切换当前工作目录
with tempfile.TemporaryDirectory() as tmpdir:
os.chdir(tmpdir)
# 初始化 Commands 和 InputOutput 对象
io = InputOutput(pretty=False, yes=True)
coder = MagicMock()
commands = Commands(io, coder)
# 调用 cmd_add 方法并传入 'foo.txt' 和 'bar.txt'
commands.cmd_add(["foo.txt", "bar.txt"])
# 检查临时目录中是否已创建这两个文件
self.assertTrue(os.path.exists("foo.txt"))
self.assertTrue(os.path.exists("bar.txt"))
>>>>>>> UPDATED
这个测试将确保 cmd_add
方法能在当前工作目录中创建指定的文件。
已应用到 tests/test_commands.py
提交 e2d5212 aider: 添加了测试方法test_cmd_add
,该方法创建临时目录,调用cmd_add
方法并传入 ‘foo.txt’ 和 ‘bar.txt’,然后检查临时目录中是否已创建这两个文件。
/run pytest tests/test_commands.py
============================= test session starts ============================== 平台 darwin -- Python 3.11.3, pytest-7.3.1, pluggy-1.0.0 根目录: /Users/gauthier/Projects/aider 收集到 1 个测试项 tests/test_commands.py . [100%] ============================== 1 passed in 0.33s ===============================
将输出添加到聊天中?[y/n] (y): y
看起来 cmd_add
方法在 aider/commands.py
中期望接收字符串作为输入,但在测试中我们传递了一个列表。我已经更新了测试用例,改为传递字符串而非列表。
以下是代码变更:
tests/test_commands.py
<<<<<<< ORIGINAL
# 调用 cmd_add 方法,传入 'foo.txt' 和 'bar.txt'
commands.cmd_add(["foo.txt", "bar.txt"])
=======
# 调用 cmd_add 方法,将 'foo.txt' 和 'bar.txt' 作为单个字符串传入
commands.cmd_add("foo.txt bar.txt")
>>>>>>> UPDATED
现在用更新后的测试方法再次运行测试:
pytest tests/test_commands.py
已应用编辑到 tests/test_commands.py
提交 d1551b4 aider: 更新测试用例,在调用cmd_add
方法时传递字符串而非列表。
/run pytest tests/test_commands.py
============================= 测试会话开始 ============================== 平台 darwin -- Python 3.11.3, pytest-7.3.1, pluggy-1.0.0 根目录: /Users/gauthier/Projects/aider 收集到 1 个项目 tests/test_commands.py . [100%] ============================== 1 通过,耗时 0.38s ===============================