在我的职业生涯中,我编写、使用和见过很多零散的脚本。它们最初是有人需要半自动化一些任务而编写的。过一段时间,它们会成长。它们的一生中可能会多次易手。我经常希望这些脚本能具有更像命令行 *工具* 的感觉。但是,将质量级别从一次性脚本提升到合适的工具到底有多难呢?事实证明,在 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 创建一个应用程序
首先,你需要安装 scaffold
、click
和 tox
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.cfg
的 options.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
。 测试使用它在隔离的文件系统中运行该命令。 然后,测试在隔离的文件系统中创建 incoming
和 archive
目录以及一个虚拟的 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 只是取消链接了最新的文件,无论它是否存在。 这些测试从一个全新的文件系统(而不是我的主文件夹)开始,并迅速失败。 我可以通过在良好隔离和自动化的测试环境中运行来防止这种错误。 这将避免很多“它在我的机器上可以工作”的问题。
脚手架和模块
这完成了我们对使用 scaffold
和 click
可以做的先进事情的游览。 有很多可能升级一个临时的 Python 脚本,并将你的简单实用程序变成功能齐全的 CLI 工具。
1 条评论