编写添加和列出待办事项功能的代码

举报
Yuchuan 发表于 2021/11/19 09:20:00 2021/11/19
【摘要】 在本节中,您将对待办事项应用程序的主要功能之一进行编码。您将为您的用户提供一个命令,将新的待办事项添加到他们的当前列表中。您还将允许用户以表格格式在屏幕上列出他们的待办事项。

第 5 步:编写添加和列出待办事项功能的代码

在本节中,您将对待办事项应用程序的主要功能之一进行编码。您将为您的用户提供一个命令,将新的待办事项添加到他们的当前列表中。您还将允许用户以表格格式在屏幕上列出他们的待办事项。

在处理这些功能之前,您将为您的代码设置一个最小的测试套件。在编写代码之前编写测试套件将帮助您了解什么是测试驱动开发 (TDD)

要下载代码、单元测试以及您将在本节中添加的所有其他资源,只需单击下面的链接并转到source_code_step_5/目录:

定义单元测试 Todoer.add()

在本节中,您将使用 pytest 为Todoer.add(). 此方法将负责向数据库添加新的待办事项。测试套件就位后,您将编写所需的代码以通过测试,这是 TDD 背后的基本思想。

注意:如果您下载本教程每个部分的源代码和资源,那么您将找到本部分和后续部分的其他单元测试。

看看他们,试着理解他们的逻辑。运行它们以确保应用程序正常工作。扩展它们以添加新的测试用例。在这个过程中你会学到很多东西。

在为 编写测试之前.add(),想想这个方法需要做什么:

  1. 获取待办事项描述优先级
  2. 创建一个字典来保存待办事项信息
  3. 数据库中读取待办事项列表
  4. 新的待办事项附加到当前的待办事项列表中
  5. 更新的待办事项列表写回数据库
  6. 新添加的待办事项连同返回码返回给调用者

代码测试的常见做法是从给定方法或函数的主要功能开始。您将首先创建测试用例来检查是否.add()正确地向数据库添加了新的待办事项。

要进行测试.add(),您必须Todoer使用适当的 JSON 文件创建一个实例作为目标数据库。要提供该文件,您将使用 pytest fixture

返回到您的代码编辑器并test_rptodo.pytests/目录中打开。向其中添加以下代码:

# 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.Pathpytest 用于提供临时目录以进行测试的对象。

您已经有一个临时的待办事项数据库可以使用。现在您需要一些数据来创建您的测试用例

# 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 行创建了一个Todoerwith实例mock_json_file作为参数。

  • 第 21 行断言对.add()usingdescriptionpriorityas 参数的调用应该返回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 的第一步。第二步是编写代码以通过这些测试。这就是你接下来要做的。

实施addCLI 命令

在本节中,您将.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(),它接受descriptionpriority作为参数。描述是一个字符串列表。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.pyadd为应用程序的 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只接受三个可能的值:123。为保证这一条件,可以设置min1max3。这样,Typer 会自动验证用户的输入,并且只接受指定间隔内的数字。

  • 第 13 行获取Todoer要使用的实例。

  • 第14个调用.add()todoer和拆包结果成todoerror

  • 第 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.
----------------------------------------

此输出在格式良好的表格中显示所有当前待办事项。这样,您的用户就可以跟踪他们的任务列表的状态。请注意,输出应以蓝色字体显示在终端窗口中。

编写待办事项完成功能

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。