這裡已經不再維護了,可以到我的個人部落格 看更新的文章
前面幾篇從套件管理、虛擬環境、測試、風格檢查到自動排版,提到了很多的工具
每一個工具又有各自的參數和設定
這麼多指令怎麼可能記得起來 😱
所以在這篇,我要來介紹如何用 invoke 管理這些指令
invoke 是什麼
invoke 是一套任務執行工具 (task execution tool),主要的功能是統一管理各種指令
並提供各種指令列上用得到的功能
有點像是 Python 的 Makefile
安裝 invoke
不同於 pytest, flake8 等套件,我會同時將 invoke 安裝在系統和虛擬環境中
# 安裝 invoke 到系統
pipx install invoke
# 安裝 invoke 到虛擬環境中
pipenv install invoke --dev
裝在系統的原因是想把它當成類似 make
的指令來使用
在虛擬環境還需要裝的原因是,部署時系統通常不會預裝 invoke
如果想在部署時也能使用 invoke 的功能,最方便的方法透過 pipenv 安裝
如何使用 invoke
我們再回到 pycontw-postevent-report-generator 的例子
checkout 到 commit 83e4,回到使用 invoke 管理指令前
從當時的文件可以看到,還需要使用相對冗長的指令
到了 commit 72ad (實作已經在 commit bc98 完成,只是在 commit 72ad 才更新文件),已經可以使用較短且較為統一的指令 (以 inv
作為開頭)
除此之外,引入 invoke 後,也不用每次都去翻文件
invoke 可以列出所有可用的指令
# 列出可用的 invoke 指令
# p.s. invoke 指令可以縮寫為 inv
$ inv --list
Available tasks:
clean Remove all the tmp files in .gitignore
develop Install script in pipenv environement in development mode
init-dev Install development dependencies
install Install script in pipenv environement
test Run testcase
執行的方式則是在 invoke 後面直接加上任務
e.g.,
inv clean
實作 invoke 任務
我們先試著將前面的 python setup.py develop
改成 invoke 的任務
預設 invoke 會去找目錄下的 tasks.py
所以我們先在專案最上層創 tasks.py
接著撰寫一個函式叫 develop
,這就會是之後的任務名稱 (i.e., 透過 inv develop
執行)
在函式前加上裝飾器 (decorator) @task
invoke 會傳入一個 context 做為第一個參數 (Read More 👉 what exactly is this ‘context’ arg anyway?)
(p.s. 原本的程式碼中是使用 cmd
,那是錯誤的用法)
最後就可以用這個 context (參數 ctx
) 執行指令
from invoke import task
@task
def develop(ctx):
ctx.run("python setup.py develop")
如同先前所提到的,操作都應該在虛擬環境內被完成
所以我將 pipenv run
的前綴提出來
這樣就能確保之後的操作一定都會在虛擬環境內執行
如果之後更換了管理虛擬環境的工具(e.g. poetry run
),也可以更輕易的改動 tasks.py
from invoke import task
PIPENV_PREFIX = "pipenv run"
@task
def develop(ctx):
ctx.run(f"{PIPENV_PREFIX} python setup.py develop")
任務相依
如果任務之間有相依性,可以在 @task
後加入 pre
或 post
表示任務執行前或後還要執行其他任務
e.g., 在初始環境 (init
) 前,常會先清除不必要的檔案 (clean
)
from invoke import task
@task
def clean():
print("clean up")
@task(pre=[clean])
def init():
print("initial")
任務模組化
當任務便多時,為了方便維護,就會傾向將相似的任務模組化
這時候就可以使用到 namespace
的概念
(Read More 👉 Constructing namespaces)
不過我不會在這篇文章深入探討要怎麼說
一般來說,除非任務真的非常多,這並不是必要的
如果有興趣看我怎麼把 pycontw-postevent-report-generator 的任務模組化
可以參考 commit f105
最後修改後 tasks
套件中有這些模組
├── tasks
│ ├── __init__.py
│ ├── build.py
│ ├── common.py
│ ├── env.py
│ ├── style.py
│ └── test.py
和各個模組下的指令
$ inv --list
Available tasks:
secure Check package security
build.clean Remove all the tmp files in .gitignore
build.develop Install script in pipenv environment in development mode
build.install Install script in pipenv environment
build.test-cli Test whether the cli is runnable
env.clean Remove virtual environment
env.init Install production dependencies
env.init-dev Install development dependencies
style.flake8 Check style through flake8
style.mypy Check style through mypy
style.pylint Check style through pylint
style.reformat Reformat python files throgh black
style.run (style) Check style throgh linter (Note that pylint is not included)
test.cov Run test coverage check
test.run (test) Run test cases
自動補完 (Auto-completion)
工程師是很懶惰的
這種指令列工具沒有自動補完怎麼行
invoke 當然也想到了
透過參數 --print-completion-script=[shell]
就能取得 invoke 預先寫好的自動補完腳步
目前支援 bash, zsh, fish 三種 shell
e.g., 將 invoke 產生的 zsh 自動補完腳本寫入 ~/.zsh
inv --print-completion-script=zsh >> ~/.zshrc
為什麼不用 Makefile 就好?
因為有些 shell script 並不見得那麼好寫
(也可能只是我不熟啦...)
e.g.,
from invoke import task
PIPENV_PREFIX = "pipenv run"
@task
def clean(cmd):
"""Remove all the tmp files in .gitignore"""
files_to_remove = []
with open('.gitignore') as input_file:
for line in input_file.readlines():
if not line.startswith('#'):
files_to_remove.append(line.strip())
cmd.run(f"rm -rf {' '.join(files_to_remove)}")
(雖然後來發現上面的一串,可以用 git clean -Xdf
取代...)
除此之外,invoke 也可以讓 shell script 的結果,直接跟 Python 互動
更容易實作出想要的功能
其他進階應用
- 如何在任務加上參數?
- 任務的函式後面加上除了 context 意外的參數
- 👉 Task parameters
- 為什麼用 invoke 的結果跟指令列上的執行結果不完全相同?
- 在
run
函式中加上pty=True
參數通常能解決,但建議可以看看 invoke 為什麼不將這設定為預設行為 - 👉 Why is my command behaving differently under Invoke versus being run by hand?
- 在
- 如何在任務執行失敗時,只做警告,不要跳錯
- 在
run
函式加上warn=True
(預設是False
) - 👉 invoke.runners.Runner.run
- 在