如何将你的Python代码打包成一个CLI应用程序

1,501 阅读11分钟

这篇文章涵盖了如何将你的Python代码打包成一个CLI应用程序,只使用官方的PyPA提供的工具,而不安装额外的外部依赖。

如果你喜欢读代码而不是读文字,你可以在这里找到本文讨论的完整的示例演示代码:用PyPA setuptools build打包的Python CLI示例 repo

从命令行运行你的Python代码

将Python文件作为脚本运行

由于Python是一种脚本语言,你可以很容易地从CLI用Python解释器运行你的Python代码,就像这样:

# run a python source file as a script
$ python mycode.py

# run a python module
$ python -m mycode

创建一个 CLI 快捷方式来启动你的 Python 应用程序

如果你想把你的Python脚本作为一个CLI应用程序来运行,并且有一个用户友好的名字,不必在前面输入Python解释器&路径,当然你可以直接在你的/bin 目录中创建一个可执行的快捷方式文件,像这样:

#!/bin/sh

python3 /path/to/mycode.py "$@"

💡 "$@" 将所有 CLI 参数从你的快捷方式启动器传递给你的 Python 脚本。

但是当你真正想发布你的代码时,这就不是那么有用了,因为除了提供实际的 Python 依赖项和你的应用程序本身外,你还必须以某种方式在你所有的最终用户的机器上创建并允许这个可执行文件。

值得庆幸的是,Python 有很好的、经过测试的、被广泛使用的内置机制,可以完全为你做到这一点--所以,你甚至根本不需要像这样偷工减料。

如何将你的 Python 代码打包成 CLI 应用程序的正确方法

打包 Python 代码的标准方法是使用setuptools。你使用setuptools来创建可以用pip 安装的发行版。

setuptools已经存在了很久,目前 (2021年8月) 正处于一个过渡阶段。这种情况已经持续了数年。这意味着使用这个工具集有不同的方式来实现同样的事情,因为新的和改进的方式已经慢慢取代了旧的:

  • setup.py- 老方法
  • setup.cfg-- 一种较新的方式
  • pyproject.toml(又称PEP 517和PEP 518)--新的、闪亮的方法。

创建你自己的CLI应用程序的关键是在你的setup.cfgsetup.py文件中指定一个入口_point

pyproject.toml规范确实定义了这个属性(如[project.scripts] ),但标准的PyPA构建还没有实现对这个属性的实际操作。

你应该使用 setup.cfg、setup.py 还是 pyproject.toml 来配置 Python 包装?

简短的回答是:就目前而言,你可能应该把这三样都用上。

现在说说更长的答案。你不一定要有这三个,但如果你没有,你需要确定你清楚地知道你在做什么和为什么,否则你就会为下一步的神秘错误而设置自己。如果你对这些机制的演变和背景不感兴趣,可以跳到下一节。

一开始是setup.py

setup.py是旧的、传统的打包 Python 项目的方式。由于setup.py本身就是一个 Python 脚本,它非常强大,因为你可以将任何你想要的高级安装功能作为安装的一部分进行编写。

但仅仅因为你可以,并不意味着你应该。作为安装的一部分,你做的脚本越多,你的安装就越脆弱,在不同的客户机上就越不可预测,因为你不一定能严格控制这些机器的状态和配置。

向 setup.cfg 的演变

相比之下,setup.cfg是一个配置文件,而不是像setup.py 那样的安装脚本。setup.cfg是静态的,setup.py 是动态的。

setup.cfg可以让你指定声明性的配置--这意味着你可以定义你的项目元数据,而不必担心脚本的问题。这是一件好事,因为你可以避免在安装过程中运行任意代码,这将使你的安全和运营团队满意,而且你不必在你的源代码中维护模板代码。奖励!

虽然它从一开始就和setup.py一起存在,但这些年来setup.cfg已经发挥了更重要的作用。你或多或少可以用这两种方法完成同样的事情,所以从这个角度来看,你用哪种方法并不重要。

然而,即使你在setup.cfg中做了所有的配置,你仍然需要一个存根setup.py文件*,除非*你运行的是PEP517构建。我们将在下一节中讨论这个新的构建系统。

输入 pyproject.toml

pyproject.tomlsetup.pysetup.cfg的官方指定继任者,但它还没有达到与前辈同等的功能。这种新的文件格式是PEP517构建规范的结果。

PEP517中规定的新的Python构建机制的一个显著特点是,你不必使用setuptools构建系统--其他构建和打包工具,如PoetryFlit,可以使用相同的pyproject.toml规范文件(PEP621)来打包python项目。

最终,所有这些工具都应该使用完全相同的pyproject.toml文件格式,但是要注意,历史上除了setuptools之外,其他的构建工具都有自己的方式来指定 CLI 入口点,所以一定要检查你最终使用的任何工具的文档,仔细检查它是否符合最新的 PEP621 标准。在这里,我们只关注如何用setuptools做这件事。

虽然最新版本的pyproject.toml规范确实增加了项目元数据的定义,你通常会在setup.cfg和/或setup.py中找到,但setuptools构建工具还不支持使用pyproject.toml中的元数据。其他符合PEP517标准的工具,比如Flit和Poetry,确实支持 只有 pyproject.toml文件的项目,所以如果你使用这些工具,就不需要setup.py和/或setup.cfg

你可以在PEP621中找到pyproject.toml的完整文件格式规范

关于在setuptools中实现对pyproject.toml 元数据完全支持的所有细节和进展,你可以在这里跟踪讨论:https://github.com/pypa/setuptools/issues/1688

2021年推荐的Python打包设置

如果你在PyPA的setuptools中使用Python打包的过渡阶段,虽然你可以使用setup.pysetup.cfgpyproject.toml的一个或另一个组合来指定你的元数据和构建属性,但你可能希望通过以下方式来覆盖你的基础,避免细微的问题:

  1. 有一个最小的pyproject.toml来指定构建系统
  2. 把所有与项目有关的配置放在setup.cfg
  3. 有一个简单的 shimsetup.py

我所说的 "微妙的问题 "是指不一致的问题,比如可编辑的安装不工作,或者构建看起来像在工作,但实际上没有使用你认为指定的元数据(你可能在部署时才发现,呃!)。所以,让我们避免这些不愉快的事情吧!

在这个设置中,由于pyproject.tomlsetup.py只是极简的垫片,你的个人项目相关配置只包含在setup.cfg的一个地方。因此,你不会在不同的文件之间无谓地重复数值。

为你的Python项目创建CLI入口点配置

项目结构示例

让我们通过一个简单的 CLI 应用程序的例子来工作。

该项目结构看起来像这样:

.
│ my-repo/
	│- mypackage/
		│- mymodule.py
	│- pyproject.toml
	│- setup.cfg
	│- setup.py

mypackage/mymodule.py

这只是一些任意的代码,我们想从CLI中直接调用:

def my_function():
    print('hello from my_function')


def another_function():
    print('hello from another_function')


if __name__ == "__main__":
    """This runs when you execute '$ python3 mypackage/mymodule.py'"""
    my_function()

setup.py

为了允许可编辑的安装(对你的本地开发机器有用),你需要一个shimsetup.py文件。

在这个文件中,你所需要的只是这一点模板:

from setuptools import setup

setup()

💡 实际上,你可以跳过setup.cfg文件,在setup.py中设置你在 本身的属性,但这将使你在将来新的PEP517构建系统,像一颗死亡之星一样,完全投入使用时的迁移更加困难。我提到这一点是因为你会在setup() Stack Overflow和朋友圈中看到很多这样的例子--这本身并没有错,但要注意这是旧的做事方式。

一个老式的setup.py文件应该是这样的:

from setuptools import setup

setup(
	name='mypackage',
	version='0.0.1',
    # To provide executable scripts, use entry points in preference to the
    # "scripts" keyword. Entry points provide cross-platform support and allow
    # pip to create the appropriate form of executable for the target platform.
    entry_points={
        'console_scripts': [
            'myapplication=mypackage.mymodule:my_function'
        ]
    },
)

setup.cfg

setup.cfg文件是真正神奇的地方。这是你设置项目特定属性的地方:

[metadata]
name = mypackage
version = 0.0.1

[options]
packages = mypackage

[options.entry_points]
console_scripts =
    my-application = mypackage.mymodule:my_function
    another-application = mypackage.mymodule:another_function
  • 名称
    • 构建系统使用这个值来生成构建输出文件。
    • 如果你不指定这个,你的输出文件名将是 "UNKNOWN",而不是一个更方便用户的名字。
  • 版本
    • 构建系统使用这个值来给你的输出文件添加一个版本号。
    • 如果你不指定这个,你的输出文件名将包含 "0.0.0"。
    • 使用这个属性来告诉构建系统要构建哪些软件包。
    • 这是一个列表,所以你可以指定一个以上的软件包。
    • 如果你不确定 Python 中的 "包" 是什么,就把它想象成你的代码所在目录的名称。
    • ❗如果你不指定这个,你的构建输出将不会真正包含你的代码。如果你忘了指定这个,你的打包和部署将看起来像在工作,但它实际上不会打包你想运行的代码,而且它实际上不会正确部署。
  • 控制台脚本(console_scripts
    • 这个属性告诉构建系统创建一个快捷的 CLI 封装脚本来运行一个 Python 函数。
    • 这是一个列表,所以你可以从同一个代码库创建多个 CLI 应用程序。
    • 在这个例子中,我们正在创建两个CLI快捷方式。
      • my-application,它调用mypackage/mymodule.py 中的my_function
      • another-application,调用mypackage/mymodule.py中的另一个函数
    • 一个条目的语法是: = [.[.]][:.] 。
    • 左边的名称将成为你的CLI应用程序的名称。这就是终端用户在CLI中输入的内容,以调用你的应用程序。
    • 如果你不指定这个属性,你的构建将不会为你的代码创建任何CLI快捷方式。
    • ❗记住,你必须在options.packages ,包括你在这里引用的代码的根包,否则构建工具将不会实际打包你在这里引用的代码
    • setup.cfg中还有很多元数据属性,你可以(也许应该!)指定它们--这里有一个更全面的setup.cfg例子。这里给出的是一个整洁的构建和打包体验的最低限度。

      💡 在其他未列出的属性中,特别感兴趣的是install_requires,你可以用它来指定依赖关系--换句话说,就是你的代码所依赖的任何外部软件包,你希望安装程序与你的应用程序一起安装。

      [options]
      install_requires =
          requests
          importlib; python_version == "2.6"
      

      pyproject.toml

      你在简约的pyproject.toml文件中所需要的就是:

      [build-system]
      build-backend = "setuptools.build_meta"
      requires = ["setuptools", "wheel"]
      

      💡pyproject.toml规范中, 相当于project.scripts setup.pysetup.cfg 中的 。然而,目前console_scripts setuptools构建系统还没有实现这一功能。

      使用 python -m build 来创建一个 python 发行版

      build,又称PyPA build,是更现代的PEP517,相当于你可能熟悉的老式setup.py sdist bdist_wheel build命令。

      如果你以前没有这样做,你可以像这样安装构建工具:

      $ pip install build
      

      现在,在你项目的根目录下,你可以运行:

      $ python -m build
      

      这将导致在dist 目录中出现两个输出文件:

      • dist/mypackage-0.0.1.tar.gz
      • dist/mypackage-0.0.1-py3-none-any.whl

      如果**./dist**目录不存在,该工具将为你创建这个目录。

      这个命令所做的是创建一个源码发行的tarball(tar.gz文件),然后从这个源码发行中创建一个轮子。轮子(.whl)是一种版本化的发行版格式,它的部署速度更快,因为在安装过程中,你可以跳过源代码发行版所需的构建步骤,而且有更好的缓存机制。

      你在这里看到的输出文件名遵循一个定义的格式,你可以在PEP427轮子文件名惯例中找到指定的格式。

      你会注意到,构建工具使用setup.cfg中的名称版本来生成这些文件名--这就是为什么,尽管严格来说你不 需要指定这些属性,但如果你想要有好的命名和容易识别的输出,它们还是很有用。

      用pip安装你的轮子

      你可以用pip来安装你刚刚创建的发行版。(我相信pip不需要向任何Pythonista介绍...)

      $ pip install dist/mypackage-0.0.1-py3-none-any.whl
      

      PyPA build如何创建CLI快捷方式

      pip install 命令将安装你的软件包,并在当前 Python 环境的bin 目录中创建 CLI 快捷方式 (你在setup.cfg 中指定的快捷方式)。

      • {Python Path}/bin/my-application
      • {Python Path}/bin/another-application

      在引擎盖下,这些快捷方式文件实际上只是我们在开始时创建的快速和肮脏的bash文件的一个更复杂的版本。在bin/ 目录中自动生成的my-application快捷方式文件看起来像这样:

      #!/bin/python3
      # -*- coding: utf-8 -*-
      import re
      import sys
      from mypackage.mymodule import my_function
      if __name__ == '__main__':
          sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
          sys.exit(my_function())
      

      在一个干净的环境中测试你的安装

      💡如果你想测试你闪亮的新包是否可以安装,创建一个新的虚拟环境,并将你的包安装到其中,这样你就可以单独测试它:

      # create virtual environment
      $ python3 -m venv .env/fresh-install-test
      
      # activate your virtual environment
      $ . .env/fresh-install-test/bin/activate
      
      # install your package into this fresh environment
      $ pip install dist/mypackage-0.0.0-py3-none-any.whl
      
      # your shortcuts are now in the venv bin directory
      $ ls .env/fresh-install-test/bin/
      my-application
      another-application
      
      # so you can run it directly from the cli
      $ my-application
      hello from my_function
      
      # and run the second application
      $ another-application
      hello from another_function
      

      发布和分发你的 Python 包

      发布意味着你如何将你的 Python 包提供给你的终端用户。

      如何发布你的包取决于你的具体要求的部署计划。对这些的全面讨论超出了本文的范围,但只是为了让你开始,一些选择是:

      • 你可以发布到一个私有的 git 仓库,并使用 pip 从该仓库进行安装
      • 你可以创建你自己的私有 Python 仓库管理器
      • 你可以使用pip从你组织中的文件共享中安装whlsdist
      • 如果你打算将你的应用程序公开发布到官方的PyPI仓库,你可以使用twine将发行版上传到PyPi。
        • 请注意,如果你打算创建一个公共包,你在填写项目的元数据时很可能要比这里刻意给出的最简单的例子详细得多。
      • pip会安装到当时处于活动状态的Python环境中,这在你无法控制的终端用户机器上可能会变得很混乱--例如,共享的依赖关系会与其他应用程序的要求发生冲突。
        • 如果你想把你的应用程序安装到一个孤立的环境中,特意为你的应用程序分开,并把你的应用程序的依赖性与整个系统的 Python 安装隔离开来,你可以使用pipx从 git repo (比如你组织中的私有 repo) 或甚至只是一个文件路径来安装。
      • 你可以把你的轮子作为附件用电子邮件发送,并告诉人们去安装。开玩笑的,开玩笑的!不要这样做--仅仅因为它被称为发生,并不意味着它是正确的。.

      如何构造一个Python CLI项目

      为了清楚起见,这个例子只是从 CLI 直接调用一个简单的 Python 函数。你的代码很可能涉及更多。

      当然,在任何特定的应用中,如何最好地构造你的代码是一个非常....值得商榷。...的话题😬 。所以我们不要对什么是 "最好的 "做出大胆的宣称,而只是看看一个典型的整齐的结构是什么样的......也就是说,虽然这是一个相对常见的做事方式,但它不一定是最好的方式:

      .
      │ my-repo/
        │- mypackage/
          │- mynamespace/
            │- anothermodule.py
          │- anothernamespace/
            │- arbmodule.py
          │- mymodule.py
          │- cli.py
          │- pyproject.toml
          │- setup.cfg
          │- setup.py
      

      如果你在cli.py中创建了你的入口点函数def main() ,那么你的setup.cfg文件中entry_points 的配置就会变成:

      [options.entry_points]
      console_scripts =
          my-application = mypackage.cli:main
      

      你可以把你的功能代码看作是一个库,而CLI实际上是这个库的一个客户或消费者。把你的代码分成对你有意义的命名空间和模块--你可以按功能区,或按依赖关系,或按对象,或按任何适合你的分类方案把代码分组。

      如果你认为CLI是你的库的API的消费者,那么将CLI处理的具体代码封装在自己的模块中是有意义的。你可以随心所欲地命名这个模块,但cli.py的好处是简洁明了。在这个模块中,你很可能会导入像argparse这样的东西,来解析你的CLI输入参数,当有人用错误的参数调用你的CLI时打印出错误,分配默认值并生成帮助和使用信息。

      这里有一个像这样结构的大型项目的真实例子,有一个CLI处理模块,它封装了所有的CLI功能,并像调用API一样调用底层程序。

      Python中的其他打包工具

      在这篇文章中,我们只关注使用 "官方 "的最小化方式来打包和构建你的 Python 项目。但是还有其他的第三方选择,它们在vanilla setuptools构建工具的基础上提供了一些额外的功能。

      我们已经提到了符合 PEP517 标准的构建工具poetryflit。使用这些工具,就像使用标准的PyPA构建工具一样,终端用户必须在他们的机器上有一个活跃的Python运行时间。你的代码会安装到那个Python环境中。

      而其他工具采用完全不同的方法,为你的应用程序和它的Python依赖项创建一个可执行文件--这些第三方工具为你的应用程序创建一个独立的平台原生可执行文件。这意味着最终用户甚至不需要在他们的机器上有一个Python发行版--他们可以自己运行你的可执行文件。