创建和打包命令行工具¶
Creating and packaging command-line tools
本指南将引导你创建和打包一个独立的命令行应用程序,该应用程序可以使用 pipx 安装。pipx 是一个工具,用于创建和管理 Python 虚拟环境,并暴露包的可执行脚本(以及可用的手册页)供命令行使用。
This guide will walk you through creating and packaging a standalone command-line application that can be installed with pipx, a tool creating and managing Python Virtual Environments and exposing the executable scripts of packages (and available manual pages) for use on the command-line.
创建包¶
Creating the package
首先,为 项目 创建一个源代码目录结构。为了举例说明,我们将构建一个简单的工具,该工具根据命令行中给出的参数输出一个问候语(一个字符串)。
待处理
在另一个指南或讨论中建议 Python 包的最佳结构,并在此处提供链接。
该项目将遵循 src-layout 结构,最终将呈现以下文件树,顶级文件夹和包名为 greetings
:
.
├── pyproject.toml
└── src
└── greetings
├── cli.py
├── greet.py
├── __init__.py
└── __main__.py
负责工具功能的实际代码将存储在 greet.py
文件中,文件名与主模块相同:
import typer
from typing_extensions import Annotated
def greet(
name: Annotated[str, typer.Argument(help="The (last, if --gender is given) name of the person to greet")] = "",
gender: Annotated[str, typer.Option(help="The gender of the person to greet")] = "",
knight: Annotated[bool, typer.Option(help="Whether the person is a knight")] = False,
count: Annotated[int, typer.Option(help="Number of times to greet the person")] = 1
):
greeting = "Greetings, dear "
masculine = gender == "masculine"
feminine = gender == "feminine"
if gender or knight:
salutation = ""
if knight:
salutation = "Sir "
elif masculine:
salutation = "Mr. "
elif feminine:
salutation = "Ms. "
greeting += salutation
if name:
greeting += f"{name}!"
else:
pronoun = "her" if feminine else "his" if masculine or knight else "its"
greeting += f"what's-{pronoun}-name"
else:
if name:
greeting += f"{name}!"
elif not gender:
greeting += "friend!"
for i in range(0, count):
print(greeting)
上述函数接受多个关键字参数,用于确定如何构建输出的问候语。接下来,构建命令行接口以提供这些功能,这将在 cli.py
中完成:
import typer
from .greet import greet
app = typer.Typer()
app.command()(greet)
if __name__ == "__main__":
app()
命令行接口是使用 typer 构建的,它是一个基于 Python 类型提示的易用 CLI 解析器。它提供了自动补全和漂亮的命令行帮助。另一种选择是 argparse
,这是 Python 标准库中包含的命令行解析器,适用于大多数需求,但通常需要在 cli.py
中编写大量代码以确保正常工作。另一个选择是 docopt,它使得仅基于文档字符串创建 CLI 接口成为可能;高级用户可以使用 click (typer
就是基于 click 构建的)。
现在,添加一个空的 __init__.py
文件,以将项目定义为常规的 导入包。
文件 __main__.py
标志着通过 runpy
运行应用程序时的主要入口点(即 python -m greetings
,在平面布局中可以立即使用,但需要安装具有 src 布局的包),因此在这里初始化命令行接口:
if __name__ == "__main__":
from greetings.cli import app
app()
备注
为了支持直接从 源代码目录 调用命令行接口,
即作为 python src/greetings
,可以在此文件中加入某些 hack;更多信息请阅读
使用 src-layout 从源代码运行命令行界面 。
First of all, create a source tree for the project. For the sake of an example, we'll build a simple tool outputting a greeting (a string) for a person based on arguments given on the command-line.
待处理
Advise on the optimal structure of a Python package in another guide or discussion and link to it here.
This project will adhere to src-layout and in the end be alike this file tree,
with the top-level folder and package name greetings
:
.
├── pyproject.toml
└── src
└── greetings
├── cli.py
├── greet.py
├── __init__.py
└── __main__.py
The actual code responsible for the tool's functionality will be stored in the file greet.py
,
named after the main module:
import typer
from typing_extensions import Annotated
def greet(
name: Annotated[str, typer.Argument(help="The (last, if --gender is given) name of the person to greet")] = "",
gender: Annotated[str, typer.Option(help="The gender of the person to greet")] = "",
knight: Annotated[bool, typer.Option(help="Whether the person is a knight")] = False,
count: Annotated[int, typer.Option(help="Number of times to greet the person")] = 1
):
greeting = "Greetings, dear "
masculine = gender == "masculine"
feminine = gender == "feminine"
if gender or knight:
salutation = ""
if knight:
salutation = "Sir "
elif masculine:
salutation = "Mr. "
elif feminine:
salutation = "Ms. "
greeting += salutation
if name:
greeting += f"{name}!"
else:
pronoun = "her" if feminine else "his" if masculine or knight else "its"
greeting += f"what's-{pronoun}-name"
else:
if name:
greeting += f"{name}!"
elif not gender:
greeting += "friend!"
for i in range(0, count):
print(greeting)
The above function receives several keyword arguments that determine how the greeting to output is constructed.
Now, construct the command-line interface to provision it with the same, which is done
in cli.py
:
import typer
from .greet import greet
app = typer.Typer()
app.command()(greet)
if __name__ == "__main__":
app()
The command-line interface is built with typer, an easy-to-use CLI parser based on Python type hints. It provides
auto-completion and nicely styled command-line help out of the box. Another option would be argparse
,
a command-line parser which is included in Python's standard library. It is sufficient for most needs, but requires
a lot of code, usually in cli.py
, to function properly. Alternatively, docopt makes it possible to create CLI
interfaces based solely on docstrings; advanced users are encouraged to make use of click (on which typer
is based).
Now, add an empty __init__.py
file, to define the project as a regular import package.
The file __main__.py
marks the main entry point for the application when running it via runpy
(i.e. python -m greetings
, which works immediately with flat layout, but requires installation of the package with src layout),
so initizalize the command-line interface here:
if __name__ == "__main__":
from greetings.cli import app
app()
备注
In order to enable calling the command-line interface directly from the source tree,
i.e. as python src/greetings
, a certain hack could be placed in this file; read more at
使用 src-layout 从源代码运行命令行界面.
pyproject.toml
¶
项目的 元数据 放在 pyproject.toml 文件中。可以按照 编写你的 pyproject.toml 中的说明填写 pyproject 元数据键 和 [build-system]
表格,并添加对 typer
的依赖(本教程使用版本 0.12.3)。
为了使该项目被识别为一个命令行工具,还需要添加一个 console_scripts
入口点 (参见 创建可执行脚本),作为一个 子键 :
[project.scripts]
greet = "greetings.cli:app"
现在,项目的源代码目录已准备好转化为一个 分发包, 使其能够被安装。
The project's metadata is placed in pyproject.toml. The pyproject metadata keys and the [build-system]
table may be filled in as described in 编写你的 pyproject.toml, adding a dependency
on typer
(this tutorial uses version 0.12.3).
For the project to be recognised as a command-line tool, additionally a console_scripts
entry point (see 创建可执行脚本) needs to be added as a subkey:
[project.scripts]
greet = "greetings.cli:app"
Now, the project's source tree is ready to be transformed into a distribution package, which makes it installable.
使用 pipx
安装包¶
Installing the package with pipx
按照 安装独立的命令行工具 中的说明安装 pipx
后,安装你的项目:
$ cd path/to/greetings/
$ pipx install .
这将暴露我们定义的作为入口点的可执行脚本,并使得 greet
命令可用。我们来测试一下:
$ greet --knight Lancelot
Greetings, dear Sir Lancelot!
$ greet --gender feminine Parks
Greetings, dear Ms. Parks!
$ greet --gender masculine
Greetings, dear Mr. what's-his-name!
由于这个示例使用了 typer
,你现在也可以通过添加 --help
选项来查看程序的使用概览,或者通过 --install-completion
选项配置自动补全。
如果你只想运行程序而不永久安装,可以使用 pipx run
,它将为该程序创建一个临时(但会缓存的)虚拟环境:
$ pipx run --spec . greet --knight
然而,这种语法有点不方便;由于我们上面定义的入口点名称与包名不匹配,因此我们需要明确指定要运行的可执行脚本(即使目前只有一个脚本)。
不过,解决这个问题有一个更实用的办法,通过为 pipx run
定义特定的入口点。可以在 pyproject.toml
中如下定义:
[project.entry-points."pipx.run"]
greetings = "greetings.cli:app"
由于这个入口点(必须 与包名匹配),pipx
将把可执行脚本作为默认脚本来运行,这样就可以执行以下命令:
$ pipx run . --knight
After installing pipx
as described in 安装独立的命令行工具, install your project:
$ cd path/to/greetings/
$ pipx install .
This will expose the executable script we defined as an entry point and make the command greet
available.
Let's test it:
$ greet --knight Lancelot
Greetings, dear Sir Lancelot!
$ greet --gender feminine Parks
Greetings, dear Ms. Parks!
$ greet --gender masculine
Greetings, dear Mr. what's-his-name!
Since this example uses typer
, you could now also get an overview of the program's usage by calling it with
the --help
option, or configure completions via the --install-completion
option.
To just run the program without installing it permanently, use pipx run
, which will create a temporary
(but cached) virtual environment for it:
$ pipx run --spec . greet --knight
This syntax is a bit unpractical, however; as the name of the entry point we defined above does not match the package name, we need to state explicitly which executable script to run (even though there is only on in existence).
There is, however, a more practical solution to this problem, in the form of an entry point specific to pipx run
.
The same can be defined as follows in pyproject.toml
:
[project.entry-points."pipx.run"]
greetings = "greetings.cli:app"
Thanks to this entry point (which must match the package name), pipx
will pick up the executable script as the
default one and run it, which makes this command possible:
$ pipx run . --knight
结论¶
Conclusion
到现在为止,你已经知道如何打包一个用 Python 编写的命令行应用程序。下一步可以是将你的包发布出去,即将其上传到 包索引,最常见的是 PyPI。为此,按照 打包您的项目 中的说明进行操作。完成后,别忘了 做一些研究,了解你的包是如何被接受的!
You know by now how to package a command-line application written in Python. A further step could be to distribute you package, meaning uploading it to a package index, most commonly PyPI. To do that, follow the instructions at 打包您的项目. And once you're done, don't forget to do some research on how your package is received!