將你的 Python 腳本轉換為命令行程序

在我的職業生涯中,我寫過、用過和看到過很多隨意的腳本。一些人需要半自動化完成任務,于是它們誕生了。一段時間后,它們變得越來越大。它們在一生中可能轉手很多次。我常常希望這些腳本提供更多的命令行工具式的感覺。但是,從一次性腳本到合適的工具,真正提高質量水平有多難呢?事實證明這在 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.mdPyscaffold 在文檔中說明了如何設置一個完整的示例項目,我不會在這里介紹,你之后可以探索。除此之外,Pyscaffold 還可以在項目中為你提供持續集成(CI)模板:
- 打包: 你的項目現在啟用了 PyPi,所以你可以將其上傳到一個倉庫并從那里安裝它。
- 文檔: 你的項目現在有了一個完整的文檔文件夾層次結構,它基于 Sphinx,包括一個??readthedocs.org?? 構建器。
- 測試: 你的項目現在可以與 tox 一起使用,測試文件夾包含運行基于 pytest 的測試所需的所有樣板文件。
- 依賴管理: 打包和測試基礎結構都需要一種管理依賴關系的方法。?
?setup.cfg?? 文件解決了這個問題,它包含所有依賴項。 - 預提交鉤子: 包括 Python 源代碼格式工具 black 和 Python 風格檢查器 flake8。
查看測試文件夾并在項目目錄中運行 ??tox?? 命令,它會立即輸出一個錯誤:打包基礎設施無法找到相關庫。
現在創建一個 ??Git??? 標記(例如 ??v0.2???),此工具會將其識別為可安裝版本。在提交更改之前,瀏覽一下自動生成的 ??setup.cfg??? 并根據需要編輯它。對于此示例,你可以修改 ??LICENSE?? 和項目描述,將這些更改添加到 Git 的暫存區,我必須禁用預提交鉤子,然后提交它們。否則,我會遇到錯誤,因為 Python 風格檢查器 flake8 會抱怨糟糕的格式。
$ 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 免費獲得所有打包、測試和文檔設置。你還獲得了一個預提交鉤子來保證(大部分情況下)你按照設定規則提交。
CLI 工具化
現在,一些值會硬編碼到腳本中,它們作為命令 ??參數??? 會更方便。例如,將 ??INCOMING?? 常量作為命令行參數會更好。
首先,導入 ??click??? 庫,使用 Click 提供的命令裝飾器對 ??rotoscope()??? 方法進行裝飾,并添加一個 Click 傳遞給 ??rotoscope?? 函數的參數。Click 提供了一組驗證器,因此要向參數添加一個路徑驗證器。Click 還方便地使用函數的內嵌字符串作為命令行文檔的一部分。所以你最終會得到以下方法簽名:
.command()
.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 工具。


























