编写添加和列出待办事项功能的代码
第 5 步:编写添加和列出待办事项功能的代码
在本节中,您将对待办事项应用程序的主要功能之一进行编码。您将为您的用户提供一个命令,将新的待办事项添加到他们的当前列表中。您还将允许用户以表格格式在屏幕上列出他们的待办事项。
在处理这些功能之前,您将为您的代码设置一个最小的测试套件。在编写代码之前编写测试套件将帮助您了解什么是测试驱动开发 (TDD)。
要下载代码、单元测试以及您将在本节中添加的所有其他资源,只需单击下面的链接并转到source_code_step_5/
目录:
定义单元测试 Todoer.add()
在本节中,您将使用 pytest 为Todoer.add()
. 此方法将负责向数据库添加新的待办事项。测试套件就位后,您将编写所需的代码以通过测试,这是 TDD 背后的基本思想。
注意:如果您下载本教程每个部分的源代码和资源,那么您将找到本部分和后续部分的其他单元测试。
看看他们,试着理解他们的逻辑。运行它们以确保应用程序正常工作。扩展它们以添加新的测试用例。在这个过程中你会学到很多东西。
在为 编写测试之前.add()
,想想这个方法需要做什么:
- 获取待办事项描述和优先级
- 创建一个字典来保存待办事项信息
- 从数据库中读取待办事项列表
- 将新的待办事项附加到当前的待办事项列表中
- 将更新的待办事项列表写回数据库
- 将新添加的待办事项连同返回码返回给调用者
代码测试的常见做法是从给定方法或函数的主要功能开始。您将首先创建测试用例来检查是否.add()
正确地向数据库添加了新的待办事项。
要进行测试.add()
,您必须Todoer
使用适当的 JSON 文件创建一个实例作为目标数据库。要提供该文件,您将使用 pytest fixture。
返回到您的代码编辑器并test_rptodo.py
从tests/
目录中打开。向其中添加以下代码:
# tests/test_rptodo.py
import json
import pytest
from typer.testing import CliRunner
from rptodo import (
DB_READ_ERROR,
SUCCESS,
__app_name__,
__version__,
cli,
rptodo,
)
# ...
@pytest.fixture
def mock_json_file(tmp_path):
todo = [{"Description": "Get some milk.", "Priority": 2, "Done": False}]
db_file = tmp_path / "todo.json"
with db_file.open("w") as db:
json.dump(todo, db, indent=4)
return db_file
在这里,您首先更新您的导入以完成一些要求。夹具mock_json_file()
创建并返回一个临时 JSON 文件db_file
,其中包含一个单项待办事项列表。在此夹具中,您使用tmp_path
,它是pathlib.Path
pytest 用于提供临时目录以进行测试的对象。
您已经有一个临时的待办事项数据库可以使用。现在您需要一些数据来创建您的测试用例:
# tests/test_rptodo.py
# ...
test_data1 = {
"description": ["Clean", "the", "house"],
"priority": 1,
"todo": {
"Description": "Clean the house.",
"Priority": 1,
"Done": False,
},
}
test_data2 = {
"description": ["Wash the car"],
"priority": 2,
"todo": {
"Description": "Wash the car.",
"Priority": 2,
"Done": False,
},
}
这两个字典提供了要测试的数据Todoer.add()
。前两个键表示您将用作 参数的数据.add()
,而第三个键保存方法的预期返回值。
现在是时候来写你的第一个测试功能的.add()
。使用 pytest,您可以使用参数化为单个测试函数提供多组参数和预期结果。这是一个非常简洁的功能。它使单个测试函数的行为类似于运行不同测试用例的多个测试函数。
以下是在 pytest 中使用参数化创建测试函数的方法:
1# tests/test_rptodo.py
2# ...
3
4@pytest.mark.parametrize(
5 "description, priority, expected",
6 [
7 pytest.param(
8 test_data1["description"],
9 test_data1["priority"],
10 (test_data1["todo"], SUCCESS),
11 ),
12 pytest.param(
13 test_data2["description"],
14 test_data2["priority"],
15 (test_data2["todo"], SUCCESS),
16 ),
17 ],
18)
19def test_add(mock_json_file, description, priority, expected):
20 todoer = rptodo.Todoer(mock_json_file)
21 assert todoer.add(description, priority) == expected
22 read = todoer._db_handler.read_todos()
23 assert len(read.todo_list) == 2
用于参数化的@pytest.mark.parametrize()
装饰器标记test_add()
。当 pytest 运行此测试时,它会调用test_add()
两次。每个调用都使用第 7 行到第 11 行和第 12 行到第 16 行的参数集之一。
第 5 行的字符串包含两个必需参数的描述性名称以及描述性返回值名称。请注意,test_add()
具有相同的参数。此外,的第一个参数与test_add()
您刚刚定义的夹具具有相同的名称。
在内部test_add()
,代码执行以下操作:
-
第 20 行创建了一个
Todoer
with实例mock_json_file
作为参数。 -
第 21 行断言对
.add()
usingdescription
和priority
as 参数的调用应该返回expected
。 -
第 22行从临时数据库中读取待办事项列表并将其存储在
read
. -
第 23 行断言待办事项列表的长度为
2
。为什么2
?因为mock_json_file()
返回一个包含一个待办事项的列表,现在您要添加第二个。
凉爽的!您有一个涵盖.add()
. 现在是时候再次运行您的测试套件了。返回您的命令行并运行python -m pytest tests/
. 您将获得类似于以下内容的输出:
======================== test session starts ==========================
platform linux -- Python 3.8.5, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: .../rptodo
plugins: Faker-8.1.1, cov-2.12.0, celery-4.4.7
collected 3 items
tests/test_rptodo.py .FF [100%]
============================== FAILURES ===============================
# Output cropped
突出显示行中的F字母表示您的两个测试用例失败了。测试失败是 TDD 的第一步。第二步是编写代码以通过这些测试。这就是你接下来要做的。
实施add
CLI 命令
在本节中,您将.add()
在Todoer
类中编码。您还将add
在 Typer CLI 中对命令进行编码。有了这两段代码,您的用户就可以将新项目添加到他们的待办事项列表中。
每次待办应用程序运行时,它都需要访问Todoer
该类并将 CLI 与数据库连接。为了满足这一要求,您将实现一个名为 的函数get_todoer()
。
返回到您的代码编辑器并打开cli.py
. 输入以下代码:
# rptodo/cli.py
from pathlib import Path
from typing import List, Optional
import typer
from rptodo import (
ERRORS, __app_name__, __version__, config, database, rptodo
)
app = typer.Typer()
@app.command()
def init(
# ...
def get_todoer() -> rptodo.Todoer:
if config.CONFIG_FILE_PATH.exists():
db_path = database.get_database_path(config.CONFIG_FILE_PATH)
else:
typer.secho(
'Config file not found. Please, run "rptodo init"',
fg=typer.colors.RED,
)
raise typer.Exit(1)
if db_path.exists():
return rptodo.Todoer(db_path)
else:
typer.secho(
'Database not found. Please, run "rptodo init"',
fg=typer.colors.RED,
)
raise typer.Exit(1)
def _version_callback(value: bool) -> None:
# ...
更新导入后,您get_todoer()
在第 18 行定义。第 19 行定义一个条件,用于检查应用程序的配置文件是否存在。为此,它使用Path.exists()
.
如果配置文件存在,则第 20 行从中获取到数据库的路径。else
如果文件不存在,则该子句运行。该子句将错误消息打印到屏幕上,并使用退出代码 退出应用程序1
以发出错误信号。
第 27 行检查数据库路径是否存在。如果是,则第 28 行创建一个Todoer
以路径为参数的实例。否则,从else
第 29 行开始的子句会打印一条错误消息并退出应用程序。
现在您有一个Todoer
具有有效数据库路径的实例,您可以编写.add()
. 返回rptodo.py
模块并更新Todoer
:
# rptodo/rptodo.py
from pathlib import Path
from typing import Any, Dict, List, NamedTuple
from rptodo import DB_READ_ERROR
from rptodo.database import DatabaseHandler
# ...
class Todoer:
def __init__(self, db_path: Path) -> None:
self._db_handler = DatabaseHandler(db_path)
def add(self, description: List[str], priority: int = 2) -> CurrentTodo:
"""Add a new to-do to the database."""
description_text = " ".join(description)
if not description_text.endswith("."):
description_text += "."
todo = {
"Description": description_text,
"Priority": priority,
"Done": False,
}
read = self._db_handler.read_todos()
if read.error == DB_READ_ERROR:
return CurrentTodo(todo, read.error)
read.todo_list.append(todo)
write = self._db_handler.write_todos(read.todo_list)
return CurrentTodo(todo, write.error)
以下是.add()
逐行工作的方式:
-
第 14 行定义了
.add()
,它接受description
和priority
作为参数。描述是一个字符串列表。Typer 根据您在命令行中输入的词构建此列表,以描述当前的待办事项。在 的情况下priority
,它是一个表示待办事项优先级的整数值。默认为2
,表示中等优先级。 -
第 16 行使用将描述组件连接成单个字符串
.join()
。 -
"."
如果用户未添加句点 ( ),则第 17 行和第 18 行会在描述末尾添加句点 ( )。 -
第 19 到 23 行根据用户的输入构建了一个新的待办事项。
-
第 24 行通过调用
.read_todos()
数据库处理程序从数据库中读取待办事项列表。 -
第 25 行检查是否
.read_todos()
返回了DB_READ_ERROR
. 如果是,则第 26 行返回一个命名元组,CurrentTodo
,包含当前待办事项和错误代码。 -
第 27行将新的待办事项添加到列表中。
-
第 28 行通过调用
.write_todos()
数据库处理程序将更新后的待办事项列表写回数据库。 -
第 29 行返回一个
CurrentTodo
带有当前待办事项和适当返回代码的实例。
现在您可以再次运行您的测试套件以检查是否.add()
正常工作。快跑吧python -m pytest tests/
。您将获得类似于以下内容的输出:
========================= test session starts =========================
platform linux -- Python 3.9.5, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
plugins: Faker-8.1.1, cov-2.12.0, celery-4.4.7
rootdir: .../rptodo
collected 2 items
tests/test_rptodo.py ... [100%]
========================== 3 passed in 0.09s ==========================
三个绿点表示您通过了三个测试。如果您从 GitHub 上的项目存储库下载了代码,那么您将获得包含更多成功测试的输出。
完成编写后.add()
,您可以前往cli.py
并add
为应用程序的 CLI编写命令:
# rptodo/cli.py
# ...
def get_todoer() -> rptodo.Todoer:
# ...
@app.command()
def add(
description: List[str] = typer.Argument(...),
priority: int = typer.Option(2, "--priority", "-p", min=1, max=3),
) -> None:
"""Add a new to-do with a DESCRIPTION."""
todoer = get_todoer()
todo, error = todoer.add(description, priority)
if error:
typer.secho(
f'Adding to-do failed with "{ERRORS[error]}"', fg=typer.colors.RED
)
raise typer.Exit(1)
else:
typer.secho(
f"""to-do: "{todo['Description']}" was added """
f"""with priority: {priority}""",
fg=typer.colors.GREEN,
)
def _version_callback(value: bool) -> None:
# ...
以下是该add
命令的作用细分:
-
第 7 行和第 8 行定义
add()
为使用@app.command()
Python 装饰器的 Typer 命令。 -
第 9 行定义
description
为 的参数add()
。此参数包含表示待办事项描述的字符串列表。要建立论点,请使用typer.Argument
. 当您将省略号 (...
) 作为 的构造函数的第一个参数传递时Argument
,您是在告诉 Typer 这description
是必需的。需要此参数的事实意味着用户必须在命令行中提供待办事项描述。 -
第 10 行定义
priority
为 Typer 选项,默认值为2
。选项名称是--priority
和-p
。正如前面所决定,priority
只接受三个可能的值:1
,2
或3
。为保证这一条件,可以设置min
到1
和max
到3
。这样,Typer 会自动验证用户的输入,并且只接受指定间隔内的数字。 -
第 13 行获取
Todoer
要使用的实例。 -
第14个调用
.add()
上todoer
和拆包结果成todo
和error
。 -
第 15 到 25 行定义了一个条件语句,如果在将新的待办事项添加到数据库时发生错误,该语句会打印一条错误消息并退出应用程序。如果没有发生错误,则
else
第 20 行的子句会在屏幕上显示一条成功消息。
现在你可以回到你的终端并add
尝试你的命令:
(venv) $ python -m rptodo add Get some milk -p 1
to-do: "Get some milk." was added with priority: 1
(venv) $ python -m rptodo add Clean the house --priority 3
to-do: "Clean the house." was added with priority: 3
(venv) $ python -m rptodo add Wash the car
to-do: "Wash the car." was added with priority: 2
(venv) $ python -m rptodo add Go for a walk -p 5
Usage: rptodo add [OPTIONS] DESCRIPTION...
Try 'rptodo add --help' for help.
Error: Invalid value for '--priority' / '-p': 5 is not in the valid range...
在第一个示例中,您执行add
带有描述"Get some milk"
和优先级的命令1
。要设置优先级,请使用该-p
选项。按 后Enter,应用程序会添加待办事项并通知您添加成功。第二个示例的工作原理非常相似。这次你--priority
用来设置待办事项的优先级为3
。
在第三个示例中,您提供了一个待办事项描述而不提供优先级。在这种情况下,应用程序使用默认优先级值,即2
。
在第四个示例中,您尝试添加优先级为 的新待办事项5
。由于此优先级值超出了允许范围,Typer 会显示一条使用消息和一条错误消息。请注意,Typer 会自动为您显示这些消息。您不需要为此添加额外的代码。
伟大的!您的待办事项应用程序已经具有一些很酷的功能。现在,您需要一种方法来列出所有待办事项,以了解您有多少工作要做。在下一节中,您将实施list
命令来帮助您完成此任务。
执行list
命令
在本节中,您将把list
命令添加到应用程序的 CLI。此命令将允许您的用户列出他们当前的所有待办事项。在向 CLI 添加任何代码之前,您需要一种方法来从数据库中检索整个待办事项列表。要完成此任务,您将添加.get_todo_list()
到Todoer
类中。
rptodo.py
在您的代码编辑器或 IDE 中打开并添加以下代码:
# rptodo/rptodo.py
# ...
class Todoer:
# ...
def get_todo_list(self) -> List[Dict[str, Any]]:
"""Return the current to-do list."""
read = self._db_handler.read_todos()
return read.todo_list
在 中.get_todo_list()
,您首先通过调用.read_todos()
数据库处理程序从数据库中获取整个待办事项列表。调用.read_todos()
返回一个命名元组,DBResponse
,包含待办事项列表和返回代码。但是,您只需要待办事项列表,因此仅.get_todo_list()
返回该.todo_list
字段。
随着.get_todo_list()
在地方,你现在就可以实现list
应用程序的CLI命令。继续并添加list_all()
到cli.py
:
# rptodo/cli.py
# ...
@app.command()
def add(
# ...
@app.command(name="list")
def list_all() -> None:
"""List all to-dos."""
todoer = get_todoer()
todo_list = todoer.get_todo_list()
if len(todo_list) == 0:
typer.secho(
"There are no tasks in the to-do list yet", fg=typer.colors.RED
)
raise typer.Exit()
typer.secho("\nto-do list:\n", fg=typer.colors.BLUE, bold=True)
columns = (
"ID. ",
"| Priority ",
"| Done ",
"| Description ",
)
headers = "".join(columns)
typer.secho(headers, fg=typer.colors.BLUE, bold=True)
typer.secho("-" * len(headers), fg=typer.colors.BLUE)
for id, todo in enumerate(todo_list, 1):
desc, priority, done = todo.values()
typer.secho(
f"{id}{(len(columns[0]) - len(str(id))) * ' '}"
f"| ({priority}){(len(columns[1]) - len(str(priority)) - 4) * ' '}"
f"| {done}{(len(columns[2]) - len(str(done)) - 2) * ' '}"
f"| {desc}",
fg=typer.colors.BLUE,
)
typer.secho("-" * len(headers) + "\n", fg=typer.colors.BLUE)
def _version_callback(value: bool) -> None:
# ...
以下是list_all()
工作原理:
-
第 8 行和第 9 行
list_all()
使用@app.command()
装饰器定义为 Typer 命令。name
这个装饰器的参数为命令设置了一个自定义名称,在list
这里。请注意,list_all()
它不接受任何参数或选项。它只列出用户list
从命令行运行时的待办事项。 -
第 11 行获取
Todoer
您将使用的实例。 -
12号线通过调用数据库中获取待完成列表
.get_todo_list()
上todoer
。 -
第 13 到 17 行定义了一个条件语句来检查列表中是否至少有一个待办事项。如果不是,则
if
代码块将错误消息打印到屏幕并退出应用程序。 -
第 18 行打印一个顶级标题以显示待办事项列表。在这种情况下,
secho()
需要一个名为 的附加布尔参数bold
,它使您能够以粗体格式显示文本。 -
第 19 到 27 行定义并打印所需的列,以表格格式显示待办事项列表。
-
第 28 到 36 行运行一个
for
循环,用适当的填充和分隔符将每个待办事项打印在自己的行上。 -
第 37行打印一行带有最后一个换行符(
\n
)的破折号,以在视觉上将待办事项列表与下一个命令行提示分开。
如果使用该list
命令运行应用程序,则会得到以下输出:
(venv) $ python -m rptodo list
to-do list:
ID. | Priority | Done | Description
----------------------------------------
1 | (1) | False | Get some milk.
2 | (3) | False | Clean the house.
3 | (2) | False | Wash the car.
----------------------------------------
此输出在格式良好的表格中显示所有当前待办事项。这样,您的用户就可以跟踪他们的任务列表的状态。请注意,输出应以蓝色字体显示在终端窗口中。
编写待办事项完成功能
- 点赞
- 收藏
- 关注作者
评论(0)