跳至内容

构建包

当您使用 **Typer** 创建 CLI 程序时,您可能希望创建自己的 Python 包。

这允许您的用户安装它,并将其作为独立程序在终端中使用。

这也是 shell 自动完成工作所必需的(除非您通过 typer 命令使用您的程序)。

如今,有几种方法和工具可以创建 Python 包(您使用 pip install something 安装的内容)。

您可能已经有了自己喜欢的。

这是一个非常主观的简短指南,展示了从头开始使用 **Typer** 应用程序创建 Python 包的另一种方法。

提示

如果您已经有了自己喜欢的创建 Python 包的方法,可以随意跳过此步骤。

先决条件

在本指南中,我们将使用 Poetry

Poetry 的文档非常棒,所以请查看并安装它。

创建项目

假设我们要创建一个名为 portal-gun 的 CLI 应用程序。

为了确保您的包不会与其他人创建的包冲突,我们将使用您的姓名作为前缀。

因此,如果您的名字是 Rick,我们将将其命名为 rick-portal-gun

使用 Poetry 创建项目

$ poetry new rick-portal-gun

Created package rick_portal_gun in rick-portal-gun

// Enter the new project directory
cd ./rick-portal-gun

依赖项和环境

typer[all] 添加到您的依赖项中

$ poetry add "typer[all]"

// It creates a virtual environment for your project
Creating virtualenv rick-portal-gun-w31dJa0b-py3.10 in /home/rick/.cache/pypoetry/virtualenvs
Using version ^0.1.0 for typer

Updating dependencies
Resolving dependencies... (1.2s)

Writing lock file

---> 100%

Package operations: 15 installs, 0 updates, 0 removals

  - Installing zipp (3.1.0)
  - Installing importlib-metadata (1.5.0)
  - Installing pyparsing (2.4.6)
  - Installing six (1.14.0)
  - Installing attrs (19.3.0)
  - Installing click (7.1.1)
  - Installing colorama (0.4.3)
  - Installing more-itertools (8.2.0)
  - Installing packaging (20.3)
  - Installing pluggy (0.13.1)
  - Installing py (1.8.1)
  - Installing shellingham (1.3.2)
  - Installing wcwidth (0.1.8)
  - Installing pytest (5.4.1)
  - Installing typer (0.0.11)

// Activate that new virtual environment
$ poetry shell

Spawning shell within /home/rick/.cache/pypoetry/virtualenvs/rick-portal-gun-w31dJa0b-py3.10

// Open an editor using this new environment, for example VS Code
$ code ./

您可以看到,您已经生成了一个看起来像这样的项目结构

.
├── poetry.lock
├── pyproject.toml
├── README.md
├── rick_portal_gun
│   └── __init__.py
└── tests
    ├── __init__.py
    └── test_rick_portal_gun.py

创建您的应用程序

现在让我们创建一个非常简单的 Typer 应用程序。

创建一个名为 rick_portal_gun/main.py 的文件,其中包含

import typer


app = typer.Typer()


@app.callback()
def callback():
    """
    Awesome Portal Gun
    """


@app.command()
def shoot():
    """
    Shoot the portal gun
    """
    typer.echo("Shooting portal gun")


@app.command()
def load():
    """
    Load the portal gun
    """
    typer.echo("Loading portal gun")

提示

由于我们正在创建一个可安装的 Python 包,因此无需添加包含 if __name__ == "__main__": 的部分。

修改 README

让我们更改 README,使其包含以下内容

# Portal Gun

The awesome Portal Gun

添加一个“脚本”

我们正在创建一个可以使用 pip install 安装的 Python 包。

但我们希望它提供一个可以在 shell 中执行的 CLI 程序。

为此,我们在 pyproject.toml 中的 [tool.poetry.scripts] 部分添加一个配置

[tool.poetry]
name = "rick-portal-gun"
version = "0.1.0"
description = ""
authors = ["Rick Sanchez <rick@example.com>"]
readme = "README.md"

[tool.poetry.scripts]
rick-portal-gun = "rick_portal_gun.main:app"

[tool.poetry.dependencies]
python = "^3.10"
typer = {extras = ["all"], version = "^0.1.0"}

[tool.poetry.dev-dependencies]
pytest = "^5.2"

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

以下是该行的含义

rick-portal-gun:将是 CLI 程序的名称。这就是我们在安装后在终端中调用它的方式。例如

$ rick-portal-gun

// Something happens here ✨

rick_portal_gun.main,在 "rick_portal_gun.main:app" 部分中,使用下划线,指的是要导入的 Python 模块。这就是人们在类似以下部分中使用它的方式

from rick_portal_gun.main import # something goes here

"rick_portal_gun.main:app" 中的 app 是要从模块中导入并作为函数调用的内容,例如

from rick_portal_gun.main import app
app()

该配置部分告诉 Poetry,当安装此包时,我们希望它创建一个名为 rick-portal-gun 的命令行程序。

并且要调用的对象(例如函数)是 rick_portal_gun.main 模块中变量 app 中的对象。

安装您的包

这就是我们创建包所需的。

现在你可以安装它了。

$ poetry install

Installing dependencies from lock file

No dependencies to install or update

  - Installing rick-portal-gun (0.1.0)

尝试你的 CLI 程序

你的包安装在 Poetry 创建的环境中,但你已经可以使用它了。

// You can use the which program to check which rick-portal-gun program is available (if any)
$ which rick-portal-gun

// You get the one from your environment
/home/rick/.cache/pypoetry/virtualenvs/rick-portal-gun-w31dJa0b-py3.10/bin/rick-portal-gun

// Try it
$ rick-portal-gun

// You get all the standard help
Usage: rick-portal-gun [OPTIONS] COMMAND [ARGS]...

  Awesome Portal Gun

Options:
  --install-completion  Install completion for the current shell.
  --show-completion     Show completion for the current shell, to copy it or customize the installation.

  --help                Show this message and exit.

Commands:
  load   Load the portal gun
  shoot  Shoot the portal gun

创建轮子包

Python 包有一个标准格式,称为“轮子”。它是一个以 .whl 结尾的文件。

你可以使用 Poetry 创建轮子。

$ poetry build

Building rick-portal-gun (0.1.0)
 - Building sdist
 - Built rick-portal-gun-0.1.0.tar.gz

 - Building wheel
 - Built rick_portal_gun-0.1.0-py3-none-any.whl

之后,如果你检查你的项目目录,你应该会在 ./dist/ 中看到一些额外的文件。

.
├── dist
│   ├── rick_portal_gun-0.1.0-py3-none-any.whl
│   └── rick-portal-gun-0.1.0.tar.gz
├── pyproject.toml
├── README.md
├── ...

.whl 是轮子文件。你可以将这个轮子文件发送给任何人,他们可以使用它来安装你的程序(我们将在稍后看到如何将其上传到 PyPI)。

测试你的轮子包

现在你可以打开另一个终端,并使用以下命令从文件中为你的用户安装该包:

$ pip install --user /home/rock/code/rick-portal-gun/dist/rick_portal_gun-0.1.0-py3-none-any.whl

---> 100%

警告

--user 很重要,它确保你将它安装在你的用户目录中,而不是在全局系统中。

如果你将其安装在全局系统中(例如,使用 sudo),你可能会安装一个与你的系统不兼容的库版本(例如,子依赖项)。

提示

如果你使用 pipx 来安装它,同时为你的 Python CLI 程序保持一个隔离的环境,那么你将获得额外的奖励 🚀

现在你已经安装了你的 CLI 程序。你可以自由使用它。

$ rick-portal-gun shoot

// It works 🎉
Shooting portal gun

将其安装在全局(而不是单个环境中),你现在可以为它全局安装完成。

$ rick-portal-gun --install-completion

zsh completion installed in /home/user/.zshrc.
Completion will take effect once you restart the terminal.

提示

如果你想删除完成,你只需删除该文件中添加的行。

重新启动终端后,你将获得新 CLI 程序的完成。

$ rick-portal-gun [TAB][TAB]

// You get completion for your CLI program ✨
load   -- Load the portal gun
shoot  -- Shoot the portal gun

支持 python -m(可选)

你可能已经注意到,你可以使用 python -m some-module 将许多 Python 模块作为脚本调用。

例如,调用 pip 的一种方法是

$ pip install fastapi

但你也可以使用 -m CLI 选项 调用 Python,并传递一个模块,让它像脚本一样执行,例如

$ python -m pip install fastapi

这里我们将 pip 作为 -m 的值传递,因此 Python 将执行模块 pip,就像它是一个脚本一样。然后它将把其余的 CLI 参数install fastapi)传递给它。

这两个命令基本等效,install fastapi 会传递给 pip

提示

在使用 pip 的情况下,很多时候建议使用 python -m 来运行它,因为如果你创建了一个带有自己 python 的虚拟环境,这将确保你使用的是环境中的 pip

添加 __main__.py

你可以通过添加一个名为 __main__.py 的文件来支持对你的包/模块的相同调用方式。

Python 会查找该文件并执行它。

该文件应该与 __init__.py 位于同一目录下。

.
├── poetry.lock
├── pyproject.toml
├── README.md
├── rick_portal_gun
│   ├── __init__.py
│   ├── __main__.py
│   └── main.py
└── tests
    ├── __init__.py
    └── test_rick_portal_gun.py

其他文件不需要导入它,你也不需要在你的 pyproject.toml 或其他任何地方引用它,它默认情况下就可以工作,因为这是 Python 的标准行为。

然后,你可以在该文件中执行你的 Typer 程序。

from .main import app
app()

现在,在你安装完你的包后,如果你使用 python -m 调用它,它将正常工作(对于主要部分)。

$ python -m rick_portal_gun

Usage: __main__.py [OPTIONS] COMMAND [ARGS]...

  Awesome Portal Gun

Options:
  --install-completion  Install completion for the current shell.
  --show-completion     Show completion for the current shell, to copy it or customize the installation.

  --help                Show this message and exit.

Commands:
  load   Load the portal gun
  shoot  Shoot the portal gun

提示

请注意,你必须传递包名的可导入版本,所以是 rick_portal_gun 而不是 rick-portal-gun

它可以工作!🚀 有点... 🤔

在帮助信息中看到 __main__.py 而不是 rick-portal-gun 了吗?我们将在下一步解决这个问题。

__main__.py 中设置程序名称

我们在 pyproject.toml 文件中设置程序名称,类似于以下行:

[tool.poetry.scripts]
rick-portal-gun = "rick_portal_gun.main:app"

但是当 Python 使用 python -m 运行我们的包作为脚本时,它没有程序名称的信息。

因此,为了修复帮助文本,使其在使用 python -m 调用时使用正确的程序名称,我们可以将其传递给 __main__.py 中的应用程序。

from .main import app
app(prog_name="rick-portal-gun")

提示

你可以传递所有可以传递给 Click 应用程序的参数和关键字参数,包括 prog_name

$ python -m rick_portal_gun

Usage: rick-portal-gun [OPTIONS] COMMAND [ARGS]...

  Awesome Portal Gun

Options:
  --install-completion  Install completion for the current shell.
  --show-completion     Show completion for the current shell, to copy it or customize the installation.

  --help                Show this message and exit.

Commands:
  load   Load the portal gun
  shoot  Shoot the portal gun

太棒了!它可以正常工作了!🎉 ✅

请注意,现在它在帮助信息中使用 rick-portal-gun 而不是 __main__.py

自动完成和 python -m

请记住,使用 python -m 时,TAB 补全(shell 自动补全)将无法工作。

自动补全依赖于调用的程序名称,它与每个特定的程序名称绑定。

因此,要为 rick-portal-gun 提供 shell 补全,你必须直接调用它。

$ rick-portal-gun [TAB][TAB]

但你仍然可以支持 python -m,以便在它有用的情况下使用它。

发布到 PyPI(可选)

您可以将新包发布到 PyPI 以使其公开,以便其他人可以轻松安装它。

因此,请立即创建一个帐户(它是免费的)。

PyPI API 令牌

为此,您首先需要配置一个 PyPI 身份验证令牌。

登录到 PyPI

然后转到 https://pypi.ac.cn/manage/account/token/ 创建一个新令牌。

假设您的新 API 令牌是

pypi-wubalubadubdub-deadbeef1234

现在使用命令 poetry config pypi-token.pypi 配置 Poetry 以使用此令牌。

$ poetry config pypi-token.pypi pypi-wubalubadubdub-deadbeef1234
// It won't show any output, but it's already configured

发布到 PyPI

现在您可以使用 Poetry 发布您的包。

您可以构建包(如上所示),然后稍后发布,或者您可以告诉 Poetry 在一次操作中构建它然后再发布。

$ poetry publish --build

# There are 2 files ready for publishing. Build anyway? (yes/no) [no] $ yes

---> 100%

Building rick-portal-gun (0.1.0)
 - Building sdist
 - Built rick-portal-gun-0.1.0.tar.gz

 - Building wheel
 - Built rick_portal_gun-0.1.0-py3-none-any.whl

Publishing rick-portal-gun (0.1.0) to PyPI
 - Uploading rick-portal-gun-0.1.0.tar.gz 100%
 - Uploading rick_portal_gun-0.1.0-py3-none-any.whl 100%

现在您可以转到 PyPI 并查看您的项目,地址为 https://pypi.ac.cn/manage/projects/

您现在应该看到您的新“rick-portal-gun”包。

从 PyPI 安装

现在,要查看我们是否可以从 PyPI 安装它,请打开另一个终端并卸载当前安装的包。

$ pip uninstall rick-portal-gun

Found existing installation: rick-portal-gun 0.1.0
Uninstalling rick-portal-gun-0.1.0:
  Would remove:
    /home/user/.local/bin/rick-portal-gun
    /home/user/.local/lib/python3.10/site-packages/rick_portal_gun-0.1.0.dist-info/*
    /home/user/.local/lib/python3.10/site-packages/rick_portal_gun/*
# Proceed (y/n)? $ y
    Successfully uninstalled rick-portal-gun-0.1.0

现在再次安装它,但这次只使用名称,以便 pip 从 PyPI 获取它。

$ pip install --user rick-portal-gun

// Notice that it says "Downloading" 🚀
Collecting rick-portal-gun
  Downloading rick_portal_gun-0.1.0-py3-none-any.whl (1.8 kB)
Requirement already satisfied: typer[all]<0.0.12,>=0.0.11 in ./.local/lib/python3.10/site-packages (from rick-portal-gun) (0.0.11)
Requirement already satisfied: click<7.2.0,>=7.1.1 in ./anaconda3/lib/python3.10/site-packages (from typer[all]<0.0.12,>=0.0.11->rick-portal-gun) (7.1.1)
Requirement already satisfied: colorama; extra == "all" in ./anaconda3/lib/python3.10/site-packages (from typer[all]<0.0.12,>=0.0.11->rick-portal-gun) (0.4.3)
Requirement already satisfied: shellingham; extra == "all" in ./anaconda3/lib/python3.10/site-packages (from typer[all]<0.0.12,>=0.0.11->rick-portal-gun) (1.3.1)
Installing collected packages: rick-portal-gun
Successfully installed rick-portal-gun-0.1.0

现在测试从 PyPI 新安装的包。

$ rick-portal-gun load

// It works! 🎉
Loading portal gun

生成文档

您可以使用 typer 命令为您的包生成文档,您可以将其放在您的 README.md 中。

$ typer rick_portal_gun.main utils docs --output README.md --name rick-portal-gun

Docs saved to: README.md

您只需要将要导入的模块(rick_portal_gun.main)传递给它,它将自动检测 typer.Typer 应用程序。

通过指定程序的 --name,它将能够在生成文档时使用它。

提示

如果您安装了 typer-slim 并且没有 typer 命令,则可以使用 python -m typer 代替。

使用文档发布新版本

现在您可以使用更新的文档发布新版本。

为此,您首先需要在 pyproject.toml 中增加版本。

[tool.poetry]
name = "rick-portal-gun"
version = "0.2.0"
description = ""
authors = ["Rick Sanchez <rick@example.com>"]
readme = "README.md"

[tool.poetry.scripts]
rick-portal-gun = "rick_portal_gun.main:app"

[tool.poetry.dependencies]
python = "^3.10"
typer = {extras = ["all"], version = "^0.1.0"}

[tool.poetry.dev-dependencies]
pytest = "^5.2"

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

在文件 rick_portal_gun/__init__.py

__version__ = '0.2.0'

然后再次构建和发布

$ poetry publish --build

---> 100%

Building rick-portal-gun (0.2.0)
 - Building sdist
 - Built rick-portal-gun-0.2.0.tar.gz

 - Building wheel
 - Built rick_portal_gun-0.2.0-py3-none-any.whl

Publishing rick-portal-gun (0.2.0) to PyPI
 - Uploading rick-portal-gun-0.2.0.tar.gz 100%
 - Uploading rick_portal_gun-0.2.0-py3-none-any.whl 100%

现在你可以去 PyPI,到项目页面,重新加载它,它现在将包含你新生成的文档。

下一步

这是一个非常简单的指南。你可以添加更多步骤。

例如,你应该使用 Git,版本控制系统,来保存你的代码。

你可以在你的 pyproject.toml 中添加很多额外的元数据,查看 Poetry: Libraries 的文档。

你可以使用 pipx 在隔离的环境中管理你安装的 CLI Python 程序。

也许可以使用 Black 进行自动格式化。

你可能希望将你的代码作为开源代码发布到 GitHub 上。

然后你可以集成一个 CI 工具来自动运行你的测试并部署你的包。

等等等等。但现在你已经掌握了基础知识,你可以继续自己探索 🚀。