到目前为止,我们已经远远超出了主要的石头剪刀布学校项目 - 让我们直接进入它。 通过本教程我们将实现什么目标? 在 中,我们成功创建了与 Trello SDK 交互的业务逻辑。 如何为 Trello Board 管理创建 Python CLI 程序(第 1 部分) 以下是我们 CLI 程序架构的快速回顾: 在本教程中,我们将研究如何将我们的项目转换为 CLI 程序,重点关注功能和非功能需求。 另一方面,我们还将学习如何将我们的程序作为包分发到 上。 PyPI 让我们开始吧 文件夹结构 之前,我们设法建立了一个框架来托管我们的 模块。这一次,我们想要实现一个 文件夹,其中包含不同功能的模块,即: trelloservice cli 配置 使用权 列表 这个想法是,对于每个命令组,其命令将存储在其自己的模块中。至于 命令,我们将其存储在主 CLI 文件中,因为它不属于任何命令组。 list 另一方面,让我们研究一下清理文件夹结构。更具体地说,我们应该通过确保目录不混乱来开始考虑软件的可扩展性。 这是对我们的文件夹结构的建议: trellocli/ __init__.py __main__.py trelloservice.py shared/ models.py custom_exceptions.py cli/ cli.py cli_config.py cli_create.py tests/ test_cli.py test_trelloservice.py assets/ images/ README.md pyproject.toml .env .gitignore 注意 文件夹是怎样的吗?这将用于存储我们的 文件的相关资源,而 中有一个新实现的 文件夹,我们将使用它来存储要在整个软件中使用的模块。 assets README trellocli shared 设置 让我们首先修改入口点文件 。看看导入本身,因为我们决定将相关模块存储在它们自己的子文件夹中,所以我们必须适应此类更改。另一方面,我们还假设主 CLI 模块 有一个可以运行的 实例。 __main__.py cli.py app # trellocli/__main__.py # module imports from trellocli import __app_name__ from trellocli.cli import cli from trellocli.trelloservice import TrelloService # dependencies imports # misc imports def main(): cli.app(prog_name=__app_name__) if __name__ == "__main__": main() 快进到我们的 文件;我们将在这里存储我们的 实例。这个想法是初始化一个在软件中共享的 对象。 cli.py app Typer # trellocli/cli/cli.py # module imports # dependencies imports from typer import Typer # misc imports # singleton instances app = Typer() 继续这个概念,让我们修改 以指定我们的命令行脚本。在这里,我们将为包提供一个名称并定义入口点。 pyproject.toml # pyproject.toml [project.scripts] trellocli = "trellocli.__main__:main" 基于上面的示例,我们将 定义为包名称,并在 脚本中定义了 函数,该函数存储在 模块中,将在运行时执行。 trellocli __main__ main trellocli 现在我们软件的 CLI 部分已经设置完毕,让我们修改 模块以更好地为我们的 CLI 程序提供服务。您还记得,我们的 模块设置为递归地请求用户授权,直到获得批准。我们将对此进行修改,以便在未授予授权的情况下程序将退出,并敦促用户运行 命令。这将确保我们的程序在指令方面更清晰、更具描述性。 trelloservice trelloservice config access 简而言之,我们将修改这些函数: __init__ __load_oauth_token_env_var authorize is_authorized 从 函数开始,我们将初始化一个空客户端,而不是在这里处理客户端设置。 __init__ # trellocli/trelloservice.py class TrelloService: def __init__(self) -> None: self.__client = None 💡你能不能修改一下我们的 函数,让它不再递归提示用户授权?提示:递归函数是调用自身的函数。 挑战角 __load_oauth_token_env_var 转到 和 辅助函数,其想法是, 将利用 函数执行设置客户端的业务逻辑,而 函数仅返回是否授予授权的布尔值。 authorize is_authorized authorize __load_oauth_token_env_var is_authorized # trellocli/trelloservice.py class TrelloService: def authorize(self) -> AuthorizeResponse: """Method to authorize program to user's trello account Returns AuthorizeResponse: success / error """ self.__load_oauth_token_env_var() load_dotenv() if not os.getenv("TRELLO_OAUTH_TOKEN"): return AuthorizeResponse(status_code=TRELLO_AUTHORIZATION_ERROR) else: self.__client = TrelloClient( api_key=os.getenv("TRELLO_API_KEY"), api_secret=os.getenv("TRELLO_API_SECRET"), token=os.getenv("TRELLO_OAUTH_TOKEN") ) return AuthorizeResponse(status_code=SUCCESS) def is_authorized(self) -> bool: """Method to check authorization to user's trello account Returns bool: authorization to user's account """ if not self.__client: return False else: return True 了解 和 之间的区别在于 是一个 ,用于将授权令牌存储为环境变量,而 是面向公众的函数,它尝试检索所有必要的凭据并初始化 Trello 客户端。 __load_oauth_token_env_var authorize __load_oauth_token_env_var 面向内部的函数 authorize 💡注意我们的 函数如何返回 数据类型。您可以实现具有 属性的模型吗?请参阅 (提示:看看我们如何创建模型) 挑战角 authorize AuthorizeResponse status_code 如何为 Trello Board 管理创建 Python CLI 程序的第 1 部分 最后,让我们在模块底部实例化一个单例 对象。请随意参考此补丁以查看完整代码: TrelloService trello-cli-kit # trellocli/trelloservice.py trellojob = TrelloService() 最后,我们想要初始化一些在程序中共享的自定义异常。这与初始化程序中定义的 不同,因为这些异常是 的子类,并且充当典型的用户定义异常,而 更多地充当从 0 开始的常量值。 ERRORS BaseException ERRORS 让我们将例外情况保持在最低限度,并考虑一些常见的用例,最值得注意的是: 读取错误:从 Trello 读取错误时引发 写入错误:写入 Trello 时出错时引发 授权错误:未向 Trello 授予授权时引发 无效的用户输入错误:无法识别用户的 CLI 输入时引发 # trellocli/shared/custom_exceptions.py class TrelloReadError(BaseException): pass class TrelloWriteError(BaseException): pass class TrelloAuthorizationError(BaseException): pass class InvalidUserInputError(BaseException): pass 单元测试 正如第一部分中提到的,我们不会在本教程中广泛讨论单元测试,所以让我们只使用必要的元素: 测试配置访问 测试配置 trello board 测试创建新的 trello 卡 测试显示 trello board 详细信息 测试显示 trello board 详细信息(详细视图) 这个想法是模拟一个命令行解释器,就像一个 来测试预期结果。 模块的伟大之处在于它带有自己的 对象。至于运行测试,我们将其与 模块配对。更多信息请查看 。 shell Typer runner pytest Typer 的官方文档 让我们一起完成第一个测试,即配置访问。了解我们正在测试该函数是否正确执行。为此,我们将检查系统响应以及退出代码是否 (即 0)。这是 RedHat 撰写的一篇精彩文章,介绍了 。 success 退出代码是什么以及系统如何使用它们来进行进程通信 # trellocli/tests/test_cli.py # module imports from trellocli.cli.cli import app # dependencies imports from typer.testing import CliRunner # misc imports runner = CliRunner() def test_config_access(): res = runner.invoke(app, ["config", "access"]) assert result.exit_code == 0 assert "Go to the following link in your browser:" in result.stdout 💡现在您已经掌握了要点,您可以自己实现其他测试用例吗? (提示:您还应该考虑测试失败案例) 挑战角 商业逻辑 主 CLI 模块 了解这将是我们的主要 模块 - 对于所有命令组(配置、创建),它们的业务逻辑将存储在自己的单独文件中,以获得更好的可读性。 cli 在本模块中,我们将存储 命令。深入研究该命令,我们知道我们想要实现以下选项: list board_name:如果之前未设置 则需要该名称 config board 详细:以详细视图显示 从 board_name 必需选项开始,有几种方法可以实现此目的,其中之一是使用回调函数(有关更多信息,请参阅 )或仅使用默认环境变量。然而,对于我们的用例,让我们通过在不满足条件时引发 自定义异常来保持简单。 官方文档 InvalidUserInputError 为了构建命令,我们首先定义选项。在 Typer 中,正如其 中提到的,定义选项的关键要素是: 官方文档 数据类型 辅助文本 默认值 例如,要创建具有以下条件的 选项: detailed 数据类型:布尔型 帮助文本:“启用详细视图” 默认值:无 我们的代码如下所示: detailed: Annotated[bool, typer.Option(help=”Enable detailed view)] = None 总的来说,为了定义带有所需选项的 命令,我们将 视为 Python 函数,并将其选项视为必需参数。 list list # trellocli/cli/cli.py @app.command() def list( detailed: Annotated[bool, Option(help="Enable detailed view")] = None, board_name: Annotated[str, Option(help="Trello board to search")] = "" ) -> None: pass 请注意,我们将命令添加到在文件顶部初始化的 实例中。请随意浏览 ,根据您的喜好修改选项。 app 官方 Typer 代码库 至于命令的工作流程,我们要做的是这样的: 检查授权 配置以使用适当的板(检查是否提供了 选项) board_name 设置要读取的 Trello 看板 检索适当的 Trello 卡片数据并根据 Trello 列表进行分类 显示数据(检查是否选择了 选项) detailed 有几点需要注意…… 当 trellojob 生成 以外的状态代码时,我们希望引发异常。通过使用 块,我们可以防止程序发生致命崩溃。 SUCCESS try-catch 配置要使用的适当板时,我们将尝试根据检索到的 设置 Trello 板以供使用。因此,我们希望涵盖以下用例 board_id 如果通过使用 trellojob 中的 函数检查匹配来显式提供 ,则检索 get_all_boards board_name board_id 如果未使用 选项,则检索存储为环境变量的 board_name board_id 我们将显示的数据将使用 包中的 功能进行格式化。有关 的更多信息,请参阅他们的 rich Table rich 官方文档 详细:显示 Trello 列表数量、卡片数量和定义标签的摘要。对于每个 Trello 列表,显示所有卡片及其相应的名称、描述和关联标签 非详细:显示 Trello 列表数量、卡片数量和定义标签的摘要 将所有内容放在一起,我们得到如下结果。免责声明: 可能缺少一些我们尚未实现的功能。如果您需要帮助实现它们,请参阅此补丁: TrelloService trello-cli-kit # trellocli/cli/cli.py # module imports from trellocli.trelloservice import trellojob from trellocli.cli import cli_config, cli_create from trellocli.misc.custom_exceptions import * from trellocli import SUCCESS # dependencies imports from typer import Typer, Option from rich import print from rich.console import Console from rich.table import Table from dotenv import load_dotenv # misc imports from typing_extensions import Annotated import os # singleton instances app = Typer() console = Console() # init command groups app.add_typer(cli_config.app, name="config", help="COMMAND GROUP to initialize configurations") app.add_typer(cli_create.app, name="create", help="COMMAND GROUP to create new Trello elements") @app.command() def list( detailed: Annotated[ bool, Option(help="Enable detailed view") ] = None, board_name: Annotated[str, Option()] = "" ) -> None: """COMMAND to list board details in a simplified (default)/detailed view OPTIONS detailed (bool): request for detailed view board_name (str): board to use """ try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # if board_name OPTION was given, attempt to retrieve board id using the name # else attempt to retrieve board id stored as an env var board_id = None if not board_name: load_dotenv() if not os.getenv("TRELLO_BOARD_ID"): print("[bold red]Error![/] A trello board hasn't been configured to use. Try running `trellocli config board`") raise InvalidUserInputError board_id = os.getenv("TRELLO_BOARD_ID") else: res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving boards from trello") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # retrieve all board id(s) and find matching board name if board_name not in boards_list: print("[bold red]Error![/] An invalid trello board name was provided. Try running `trellocli config board`") raise InvalidUserInputError board_id = boards_list[board_name] # configure board to use res_get_board = trellojob.get_board(board_id=board_id) if res_get_board.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when configuring the trello board to use") raise TrelloReadError board = res_get_board.res # retrieve data (labels, trellolists) from board res_get_all_labels = trellojob.get_all_labels(board=board) if res_get_all_labels.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving data from board") raise TrelloReadError labels_list = res_get_all_labels.res res_get_all_lists = trellojob.get_all_lists(board=board) if res_get_all_lists.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving data from board") raise TrelloReadError trellolists_list = res_get_all_lists.res # store data on cards for each trellolist trellolists_dict = {trellolist: [] for trellolist in trellolists_list} for trellolist in trellolists_list: res_get_all_cards = trellojob.get_all_cards(trellolist=trellolist) if res_get_all_cards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving cards from trellolist") raise TrelloReadError cards_list = res_get_all_cards.res trellolists_dict[trellolist] = cards_list # display data (lists count, cards count, labels) # if is_detailed OPTION is selected, display data (name, description, labels) for each card in each trellolist print() table = Table(title="Board: "+board.name, title_justify="left", show_header=False) table.add_row("[bold]Lists count[/]", str(len(trellolists_list))) table.add_row("[bold]Cards count[/]", str(sum([len(cards_list) for cards_list in trellolists_dict.values()]))) table.add_row("[bold]Labels[/]", ", ".join([label.name for label in labels_list if label.name])) console.print(table) if detailed: for trellolist, cards_list in trellolists_dict.items(): table = Table("Name", "Desc", "Labels", title="List: "+trellolist.name, title_justify="left") for card in cards_list: table.add_row(card.name, card.description, ", ".join([label.name for label in card.labels if label.name])) console.print(table) print() except (AuthorizationError, InvalidUserInputError, TrelloReadError): print("Program exited...") 要查看我们的软件的运行情况,只需在终端中运行 即可。默认情况下,Typer 模块将自行填充 命令的输出。请注意我们如何将 称为包名称 - 还记得之前在我们的 中是如何定义的吗? python -m trellocli --help --help trellocli pyproject.toml 让我们快进一点并初始化 和 命令组。为此,我们只需在 对象上使用 函数即可。这个想法是命令组将有自己的 对象,我们只需将其添加到 中的主 中,以及命令组的名称和帮助程序文本。它应该看起来像这样 create config app add_typer app cli.py app # trellocli/cli/cli.py app.add_typer(cli_config.app, name="config", help="COMMAND GROUP to initialize configurations") 💡你能自己导入 命令组吗?请随意参考此补丁寻求帮助: 挑战角 create trello-cli-kit 子命令 要为 设置命令组,我们将其各自的命令存储在其自己的模块中。该设置与 类似,需要实例化 Typer 对象。至于命令,我们还希望遵守使用自定义异常的需要。我们想要讨论的另一个主题是当用户按下 时,或者换句话说,中断进程。我们之所以没有在 命令中介绍这一点,是因为这里的区别在于 命令组由交互式命令组成。交互式命令之间的主要区别在于它们需要持续的用户交互。当然,假设我们直接的命令执行起来需要很长时间。处理潜在的键盘中断也是最佳实践。 create cli.py Ctrl + C list config 从 命令开始,我们最终将使用在 中创建的 函数。由于 函数自行处理所有配置,因此我们只需验证该过程的执行情况。 access TrelloService authorize authorize # trellocli/cli/cli_config.py @app.command() def access() -> None: """COMMAND to configure authorization for program to access user's Trello account""" try: # check authorization res_authorize = trellojob.authorize() if res_authorize.status_code != SUCCESS: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except AuthorizationError: print("Program exited...") 至于 命令,我们将利用各种模块来提供良好的用户体验,包括 来显示终端 GUI 以供用户交互。主要思想如下: board 简单的终端菜单 检查授权 从用户帐户中检索所有 Trello 看板 显示 Trello 看板的单选终端菜单 将选定的 Trello board ID 设置为环境变量 # trellocli/cli/cli_config.py @app.command() def board() -> None: """COMMAND to initialize Trello board""" try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # retrieve all boards res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving trello boards") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # for easy access to board id when given board name # display menu to select board boards_menu = TerminalMenu( boards_list.keys(), title="Select board:", raise_error_on_interrupt=True ) boards_menu.show() selected_board = boards_menu.chosen_menu_entry # set board ID as env var dotenv_path = find_dotenv() set_key( dotenv_path=dotenv_path, key_to_set="TRELLO_BOARD_ID", value_to_set=boards_list[selected_board] ) except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except (AuthorizationError, TrelloReadError): print("Program exited...") 最后,我们将讨论软件的核心功能需求 - 将新卡添加到 Trello 看板上的列表中。我们将使用 命令中的相同步骤,直到从板上检索数据。 list 除此之外,我们将交互地请求用户输入以正确配置新卡: 要添加到的 Trello 列表:单选 卡牌名称:文字 [可选]卡片描述:文本 [可选]标签:多选 确认:是/否 对于需要用户从列表中选择的所有提示,我们将像以前一样使用 包。至于其他提示和杂项,例如需要文本输入或用户确认,我们将简单地使用 包。同样重要的是要注意我们必须正确处理可选值: Simple Terminal Menu rich 用户可以跳过提供描述 用户可以为标签提供空选择 # trellocli/cli/cli_create.py @app.command() def card( board_name: Annotated[str, Option()] = "" ) -> None: """COMMAND to add a new trello card OPTIONS board_name (str): board to use """ try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # if board_name OPTION was given, attempt to retrieve board id using the name # else attempt to retrieve board id stored as an env var board_id = None if not board_name: load_dotenv() if not os.getenv("TRELLO_BOARD_ID"): print("[bold red]Error![/] A trello board hasn't been configured to use. Try running `trellocli config board`") raise InvalidUserInputError board_id = os.getenv("TRELLO_BOARD_ID") else: res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving boards from trello") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # retrieve all board id(s) and find matching board name if board_name not in boards_list: print("[bold red]Error![/] An invalid trello board name was provided. Try running `trellocli config board`") raise InvalidUserInputError board_id = boards_list[board_name] # configure board to use res_get_board = trellojob.get_board(board_id=board_id) if res_get_board.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when configuring the trello board to use") raise TrelloReadError board = res_get_board.res # retrieve data (labels, trellolists) from board res_get_all_labels = trellojob.get_all_labels(board=board) if res_get_all_labels.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving the labels from the trello board") raise TrelloReadError labels_list = res_get_all_labels.res labels_dict = {label.name: label for label in labels_list if label.name} res_get_all_lists = trellojob.get_all_lists(board=board) if res_get_all_lists.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving the lists from the trello board") raise TrelloReadError trellolists_list = res_get_all_lists.res trellolists_dict = {trellolist.name: trellolist for trellolist in trellolists_list} # for easy access to trellolist when given name of trellolist # request for user input (trellolist, card name, description, labels to include) interactively to configure new card to be added trellolist_menu = TerminalMenu( trellolists_dict.keys(), title="Select list:", raise_error_on_interrupt=True ) # Prompt: trellolist trellolist_menu.show() print(trellolist_menu.chosen_menu_entry) selected_trellolist = trellolists_dict[trellolist_menu.chosen_menu_entry] selected_name = Prompt.ask("Card name") # Prompt: card name selected_desc = Prompt.ask("Description (Optional)", default=None) # Prompt (Optional) description labels_menu = TerminalMenu( labels_dict.keys(), title="Select labels (Optional):", multi_select=True, multi_select_empty_ok=True, multi_select_select_on_accept=False, show_multi_select_hint=True, raise_error_on_interrupt=True ) # Prompt (Optional): labels labels_menu.show() selected_labels = [labels_dict[label] for label in list(labels_menu.chosen_menu_entries)] if labels_menu.chosen_menu_entries else None # display user selection and request confirmation print() confirmation_table = Table(title="Card to be Added", show_header=False) confirmation_table.add_row("List", selected_trellolist.name) confirmation_table.add_row("Name", selected_name) confirmation_table.add_row("Description", selected_desc) confirmation_table.add_row("Labels", ", ".join([label.name for label in selected_labels]) if selected_labels else None) console.print(confirmation_table) confirm = Confirm.ask("Confirm") # if confirm, attempt to add card to trello # else, exit if confirm: res_add_card = trellojob.add_card( col=selected_trellolist, name=selected_name, desc=selected_desc, labels=selected_labels ) if res_add_card.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when adding a new card to trello") raise TrelloWriteError else: print("Process cancelled...") except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except (AuthorizationError, InvalidUserInputError, TrelloReadError, TrelloWriteError): print("Program exited...") 💡可以显示 过程的进度条吗?提示:看看如何使用 的 挑战角 add rich 状态功能 包裹配送 有趣的部分来了 - 在 PyPI 上正式分发我们的软件。我们将遵循此管道来执行此操作: 配置元数据+更新README 上传到测试 PyPI 配置 GitHub 操作 推送代码到标签 v1.0.0 将代码分发到 PyPI 🎉 有关详细说明,请查看 Ramit Mittal 撰写的有关 Python 打包的精彩教程。 元数据配置 需要的最后一个细节是指定哪个模块存储包本身。在我们的例子中,这将是 。以下是要添加的元数据: pyproject.toml trellocli # pyproject.toml [tool.setuptools] packages = ["trellocli"] 至于我们的 ,提供某种指南是很好的做法,无论是使用指南还是如何开始。如果您在 中包含图像,则应使用其绝对 URL,通常采用以下格式 README.md README.md https://raw.githubusercontent.com/<user>/<repo>/<branch>/<path-to-image> 测试PyPI 我们将使用 和 工具来构建和发布我们的包。在终端中运行以下命令来为您的包创建源存档和轮子: build twine python -m build 确保您已经在 TestPyPI 上设置了帐户,然后运行以下命令 twine upload -r testpypi dist/* 系统会提示您输入用户名和密码。由于启用了两因素身份验证,您将需要使用 API 令牌(有关如何获取 TestPyPI API 令牌的更多信息: )。只需输入以下值: 文档链接 用户名: 代币 密码:<您的 TestPyPI 令牌> 完成后,您应该能够前往 TestPyPI 查看新分发的包! GitHub 设置 目标是利用 GitHub 作为根据标签不断更新包的新版本的手段。 首先,转到 GitHub 工作流程上的 选项卡并选择一个新工作流程。我们将使用由 GitHub Actions 创建的 工作流程。请注意工作流程如何需要从存储库中读取机密?确保您已将 PyPI 令牌存储在指定名称下(获取 PyPI API 令牌与 TestPyPI 的获取类似)。 Actions Publish Python Package 创建工作流程后,我们将把代码推送到 v1.0.0 标签。有关版本命名语法的更多信息,请参阅 Py-Pkgs 的精彩解释: 链接到文档 只需运行常用的 、 和 命令即可。接下来,通过运行以下命令为最新提交创建标签(有关标签的更多信息: ) pull add commit 链接到文档 git tag <tagname> HEAD 最后,将新标签推送到远程存储库 git push <remote name> <tag name> 如果您想了解更多信息,请阅读 精彩文章。但现在,坐下来享受你的最新成就吧🎉。当 GitHub Actions 工作流程将您的包分发到 PyPI 时,请随意观看神奇的揭秘。 Karol Horosin 撰写的关于将 CI/CD 与 Python 包集成的 包起来 这是一篇很长的文章😓。通过本教程,您学习了使用 模块将软件转换为 CLI 程序并将包分发到 PyPI。为了更深入地了解,您学习了定义命令和命令组、开发交互式 CLI 会话,并涉足键盘中断等常见 CLI 场景。 Typer 你绝对是一个能坚持到底的奇才。您愿意加入我的第 3 部分吗?我们将在其中实现可选功能?