将你的 Python 脚本转变为命令行应用程序

通过 Python 中的 scaffold 和 click,你可以将即使是一个简单的实用程序升级为一个功能齐全的命令行界面工具。
2 位读者喜欢这篇文章。
Python programming language logo and Tux the Penguin logo for Linux

Opensource.com

在我的职业生涯中,我编写、使用和见过很多零散的脚本。它们最初是有人需要半自动化一些任务而编写的。过一段时间,它们会成长。它们的一生中可能会多次易手。我经常希望这些脚本能具有更像命令行 *工具* 的感觉。但是,将质量级别从一次性脚本提升到合适的工具到底有多难呢?事实证明,在 Python 中并不难。

脚手架

在本文中,我将从一小段 Python 代码开始。我将其放入一个 scaffold 模块中,并使用 click 扩展它以接受命令行参数。

#!/usr/bin/python

from glob import glob
from os.path import join, basename
from shutil import move
from datetime import datetime
from os import link, unlink

LATEST = 'latest.txt'
ARCHIVE = '/Users/mark/archive'
INCOMING = '/Users/mark/incoming'
TPATTERN = '%Y-%m-%d'

def transmogrify_filename(fname):
    bname = basename(fname)
    ts = datetime.now().strftime(TPATTERN)
    return '-'.join([ts, bname])

def set_current_latest(file):
    latest = join(ARCHIVE, LATEST)
    try:
        unlink(latest)
    except:
        pass
    link(file, latest)

def rotate_file(source):
    target = join(ARCHIVE, transmogrify_filename(source))
    move(source, target)
    set_current_latest(target)

def rotoscope():
    file_no = 0
    folder = join(INCOMING, '*.txt')
    print(f'Looking in {INCOMING}')
    for file in glob(folder):
        rotate_file(file)
        print(f'Rotated: {file}')
        file_no = file_no + 1
    print(f'Total files rotated: {file_no}')

if __name__ == '__main__':
    print('This is rotoscope 0.4.1. Bleep, bloop.')
    rotoscope()

本文中的所有非内联代码示例都指的是可以在 https://codeberg.org/ofosos/rotoscope 找到的特定代码版本。该仓库中的每个提交都描述了本文教程中的一些有意义的步骤。

此代码段执行以下几项操作

  • 检查 INCOMING 中指定的路径中是否存在任何文本文件
  • 如果存在,它会创建一个具有当前时间戳的新文件名,并将文件移动到 ARCHIVE
  • 删除当前的 ARCHIVE/latest.txt 链接,并创建一个指向刚刚添加的文件的新链接

作为一个例子,这非常小,但它可以让你了解这个过程。

使用 pyscaffold 创建一个应用程序

首先,你需要安装 scaffoldclicktox Python 模块

$ python3 -m pip install scaffold click tox

安装 scaffold 后,切换到示例 rotoscope 项目所在的目录,然后执行以下命令

$ putup rotoscope -p rotoscope \
--force --no-skeleton -n rotoscope \
-d 'Move some files around.' -l GLWT \
-u http://codeberg.org/ofosos/rotoscope \
--save-config --pre-commit --markdown

Pyscaffold 覆盖了我的 README.md,所以从 Git 恢复它

$ git checkout README.md

Pyscaffold 在 docs 层次结构中设置了一个完整的示例项目,我不会在这里介绍,但你可以稍后随意探索它。 除此之外,Pyscaffold 还可以为你的项目提供持续集成 (CI) 模板。

  • 打包:你的项目现在已启用 PyPi,因此你可以将其上传到仓库并从那里安装。
  • 文档:你的项目现在具有基于 Sphinx 的完整文档文件夹层次结构,包括 readthedocs.org 构建器。
  • 测试:你的项目现在可以与 tox 测试运行器一起使用,并且 tests 文件夹包含运行基于 pytest 的测试所需的所有样板。
  • 依赖管理:打包和测试基础架构都需要一种管理依赖项的方法。 setup.cfg 文件解决了这个问题,并包含依赖项。
  • pre-commit 钩子:这包括 Python 源代码格式化程序“black”和 “flake8” Python 样式检查器。

查看 tests 文件夹并在项目目录中运行 tox 命令。 它立即输出一个错误。 打包基础架构找不到你的包。

现在创建一个 Git 标签(例如,v0.2),该工具将其识别为可安装版本。 在提交更改之前,通读自动生成的 setup.cfg 并对其进行编辑以适应你的用例。 对于此示例,你可以调整 LICENSE 和项目说明。 将这些更改添加到 Git 的暂存区,我必须禁用 pre-commit 钩子来提交它们。 否则,我会遇到一个错误,因为 flake8,Python 样式检查器,抱怨糟糕的样式。

$ PRE_COMMIT_ALLOW_NO_CONFIG=1 git commit 

如果有一个用户可以从命令行调用的脚本入口点也很好。 现在,你只能通过找到 .py 文件并手动执行它来运行它。 幸运的是,Python 的打包基础架构有一种很好的 “罐装” 方法,可以轻松进行配置更改。 将以下内容添加到 setup.cfgoptions.entry_points 部分

console_scripts =
    roto = rotoscope.rotoscope:rotoscope

此更改创建一个名为 roto 的 shell 命令,你可以使用它来调用 rotoscope 脚本。 使用 pip 安装 rotoscope 后,你可以使用 roto 命令。

就是这样。 你已经免费从 Pyscaffold 获得了所有打包、测试和文档设置。 你还获得了一个 pre-commit 钩子来让你(大部分)诚实。

CLI 工具

现在,脚本中有一些硬编码的值,如果作为命令行 参数 会更方便。 例如,INCOMING 常量最好作为命令行参数。

首先,导入 click 库。 使用 Click 提供的命令注释来注释 rotoscope() 方法,并添加一个 Click 传递给 rotoscope 函数的参数。 Click 提供了一组验证器,因此将一个路径验证器添加到该参数。 Click 还方便地使用该函数的 here-string 作为命令行文档的一部分。 因此,你最终会得到以下方法签名

@click.command()
@click.argument('incoming', type=click.Path(exists=True))
def rotoscope(incoming):
    """
    Rotoscope 0.4 - Bleep, blooop.
    Simple sample that move files.
    """

主部分调用 rotoscope(),它现在是一个 Click 命令。 它不需要传递任何参数。

选项也可以由 环境变量 自动填充。 例如,将 ARCHIVE 常量更改为一个选项

@click.option('archive', '--archive', default='/Users/mark/archive', envvar='ROTO_ARCHIVE', type=click.Path())

相同的路径验证器再次适用。 这次,让 Click 填充环境变量,如果环境中没有提供任何内容,则默认为旧常量的值。

Click 可以做更多的事情。 它具有彩色控制台输出、提示和子命令,允许你构建复杂的 CLI 工具。 浏览 Click 文档可以发现它更多的强大功能。

现在添加一些测试。

测试

Click 对使用 CLI 运行器 运行端到端测试 提供了一些建议。 你可以使用它来实现一个完整的测试(在 示例项目 中,测试位于 tests 文件夹中。)

该测试位于一个测试类的方法中。 大部分约定都与我在任何其他 Python 项目中使用的方式非常相似,但是由于 rotoscope 使用 click,因此有一些特殊性。 在 test 方法中,我创建一个 CliRunner。 测试使用它在隔离的文件系统中运行该命令。 然后,测试在隔离的文件系统中创建 incomingarchive 目录以及一个虚拟的 incoming/test.txt 文件。 然后,它像你调用命令行应用程序一样调用 CliRunner。 运行完成后,测试检查隔离的文件系统并验证 incoming 是否为空,并且 archive 包含两个文件(最新的链接和存档的文件。)

from os import listdir, mkdir
from click.testing import CliRunner
from rotoscope.rotoscope import rotoscope

class TestRotoscope:
    def test_roto_good(self, tmp_path):
        runner = CliRunner()

        with runner.isolated_filesystem(temp_dir=tmp_path) as td:
            mkdir("incoming")
            mkdir("archive")
            with open("incoming/test.txt", "w") as f:
                f.write("hello")

            result = runner.invoke(rotoscope, ["incoming", "--archive", "archive"])
            assert result.exit_code == 0

            print(td)
            incoming_f = listdir("incoming")
            archive_f = listdir("archive")
            assert len(incoming_f) == 0
            assert len(archive_f) == 2

要在我的控制台上执行这些测试,请在项目的根目录中运行 tox

在实施测试期间,我在代码中发现了一个错误。 当我进行 Click 转换时,rotoscope 只是取消链接了最新的文件,无论它是否存在。 这些测试从一个全新的文件系统(而不是我的主文件夹)开始,并迅速失败。 我可以通过在良好隔离和自动化的测试环境中运行来防止这种错误。 这将避免很多“它在我的机器上可以工作”的问题。

脚手架和模块

这完成了我们对使用 scaffoldclick 可以做的先进事情的游览。 有很多可能升级一个临时的 Python 脚本,并将你的简单实用程序变成功能齐全的 CLI 工具。

标签
User profile image.
Mark 是德国汉堡 MaibornWolff 的 IT 架构师,在 DevOps 和云原生部门工作。 在工作中,他尝试尽可能地为开源做出贡献。 当不从事日常工作时,他也会抽出一些时间来从事开源工作。 除此之外,他对文档和文档工作流程很感兴趣。 Mark 是一位 BJCP 认证的啤酒评委,并且拥有一台相机。

1 条评论

谢谢你这篇文章

Creative Commons License本作品已获得 Creative Commons Attribution-Share Alike 4.0 International License 的许可。
© . All rights reserved.