Click 提供了豐富的引數型別和選項,讓開發者可以輕鬆處理命令列輸入。除了內建型別,還可以自訂引數型別,例如解析 Python 類別路徑。為提升使用者經驗,Click 支援自訂自動完成,方便使用者快速輸入引數。此外,Click 也內建了 --help 和 --version 等常見選項,簡化開發流程。更進一步,可以利用 Entry Points 機制開發外掛系統,讓其他開發者擴充套件程式功能,無需修改核心程式碼。本文將探討這些技術,並提供實用的程式碼範例。
使用 Click 提升命令列工具的解析能力
Click 提供了一系列內建的引數型別,可以自動解析命令列輸入的值,簡化命令列工具的開發流程。這些內建型別包括 click.STRING、click.INT、click.FLOAT 和 click.BOOL,能夠直接將命令列的輸入轉換為 Python 的基本資料型別。例如,使用 click.FLOAT 可以自動呼叫 float(...) 對輸入值進行解析,而 click.BOOL 則會檢查輸入是否符合特定的真假值表示法,如 y/n、t/f、1/0 等。
自訂 Click 引數型別
除了 Click 提供的內建型別外,開發者還可以自訂引數型別,以滿足特定的解析需求。自訂型別需要繼承 ParamType 並實作必要的解析邏輯。例如,下面的程式碼定義了一個用於解析 Python 類別路徑的自訂型別 PythonClassParameterType:
from click.types import ParamType
import importlib
import functools
class PythonClassParameterType(ParamType):
name = "pythonclass"
def __init__(self, superclass=type):
self.superclass = superclass
def get_sensor_by_path(self, sensor_path: str, fail: Callable[[str], None]) -> Any:
try:
module_name, sensor_name = sensor_path.split(":")
except ValueError:
return fail("類別路徑必須符合 dotted.path.to.module:ClassName 的格式")
try:
module = importlib.import_module(module_name)
except ImportError:
return fail(f"無法匯入模組 {module_name}")
try:
sensor_class = getattr(module, sensor_name)
except AttributeError:
return fail(f"在 {module_name} 中找不到屬性 {sensor_name}")
if isinstance(sensor_class, type) and issubclass(sensor_class, self.superclass) and sensor_class != self.superclass:
return sensor_class
else:
return fail(f"檢測到的物件 {sensor_class!r} 無法識別為 {self.superclass} 型別")
def convert(self, value, param, ctx):
fail = functools.partial(self.fail, param=param, ctx=ctx)
return self.get_sensor_by_path(value, fail)
def __repr__(self):
return "PythonClass"
# 建立一個只接受 Sensor 類別的 PythonClassParameterType 例項
SensorClassParameter = PythonClassParameterType(Sensor)
內容解密:
PythonClassParameterType類別定義:該類別繼承自ParamType,用於解析 Python 類別的路徑。__init__方法:初始化時可指定一個父類別 (superclass),預設為type。get_sensor_by_path方法:根據提供的路徑字串(如module_name:ClassName),嘗試匯入模組並取得指定的類別。如果過程中出現錯誤(如格式不正確、模組無法匯入、類別不存在),則會呼叫fail回呼函式報告錯誤。convert方法:Click 在解析命令列引數時會呼叫此方法,將輸入的字串轉換為指定的 Python 類別。SensorClassParameter例項:建立了一個PythonClassParameterType的例項,並指定Sensor為其接受的類別型別,用於後續的命令列引數解析。
使用自訂引數型別
定義好自訂的引數型別後,可以在 Click 的選項或引數定義中直接使用:
@click.option(
"--develop",
required=False,
metavar="path",
help="透過 Python 路徑載入感測器",
type=SensorClassParameter,
)
內容解密:
--develop選項:定義了一個名為--develop的命令列選項,允許使用者透過 Python 路徑指定要載入的感測器類別。type=SensorClassParameter:指定該選項的值應該使用SensorClassParameter進行解析,確保輸入的是有效的Sensor類別路徑。
自動完成支援與常見選項實作
在命令列工具的開發過程中,為使用者提供便利的自動完成(Autocomplete)功能是一項重要的提升使用者經驗的措施。Click 函式庫提供了豐富的自訂自動完成支援,讓開發者能夠針對特定需求實作自訂的自動完成邏輯。
自訂自動完成實作
對於某些選項引數,例如 --develop 旗標,完成 Python 模組名稱的自動實作可能較為困難,因為這需要掃描環境中的所有可能性。然而,一旦模組名稱被輸入,完成類別名稱相對較容易。開發者可以參考以下方法簽名來實作自訂的自動完成:
def AutocompleteSensorPath(
ctx: click.core.Context, args: list, incomplete: str
) -> t.List[t.Tuple[str, str]]:
# 自動完成邏輯實作
pass
內容解密:
ctx: click.core.Context:代表目前 Click 命令的執行上下文,包含命令相關資訊。args: list:目前已輸入的命令列引數列表。incomplete: str:目前使用者正在輸入且尚未完成的引數。- 傳回值
t.List[t.Tuple[str, str]]:包含自動完成建議的列表,每個建議由顯示值和實際值組成。
要啟用自訂的自動完成,需要在選項中加入 autocompletion=AutocompleteSensorPath 引數:
@click.option("--develop", autocompletion=AutocompleteSensorPath)
def show_sensors(develop: str) -> int:
# 命令邏輯實作
pass
測試自動完成功能
要在本地測試自動完成功能,需要手動在虛擬環境中啟用該功能。以 Bash shell 為例,可以使用以下命令:
> pipenv shell
> eval "$(_SENSORS_COMPLETE=source_bash sensors)"
內容解密:
pipenv shell:啟動虛擬環境。eval "$(_SENSORS_COMPLETE=source_bash sensors)":告訴 Click 生成 Bash 自動完成組態並立即執行。
常見選項實作
除了自訂自動完成,Click 還內建支援一些常見的選項,例如 --help 和 --version。
--help 選項
Click 自動為所有命令新增 --help 選項,除非在 @click.command() 中指定 add_help_option=False。開發者也可以手動新增自訂的幫助選項:
@click.command(help="顯示感測器的值")
@click.help_option("--hilfe")
def show_sensors(develop: str) -> int:
# 命令邏輯實作
pass
內容解密:
@click.help_option("--hilfe"):新增一個名為--hilfe的幫助選項。- 這允許開發者支援多語言的幫助選項。
--version 選項
可以使用 @click.version_option() 裝飾器來新增 --version 選項,以顯示目前安裝的命令版本:
@click.version_option()
def show_sensors(develop: str) -> int:
# 命令邏輯實作
pass
內容解密:
@click.version_option()自動從 Python 環境中查詢目前安裝的版本。- 這使得維護版本資訊變得更加簡單。
第三方感測器外掛支援
為了讓使用者無需每次都指定 Python 路徑,我們需要動態生成可用的感測器列表。這可以透過兩種方法實作:自動檢測和組態檔案。
自動檢測 vs 組態檔案
| 方法 | 說明 | 優點 | 缺點 |
|---|---|---|---|
| 自動檢測 | 感測器在執行時註冊 | 無需額外組態,動態載入 | 可能需要額外的註冊邏輯 |
| 組態檔案 | 使用者維護一個指向感測器的檔案 | 明確控制載入哪些感測器 | 需要使用者手動維護組態 |
開發者應根據具體的使用場景選擇合適的方法。
從指令碼到框架:外掛架構的設計考量
在開發一個可擴充套件的系統時,外掛架構的設計是一個重要的考慮因素。一個好的外掛架構可以讓開發者輕鬆地新增新功能,而不需要修改核心程式碼。在本章中,我們將探討兩種常見的外掛架構設計方法:根據組態的系統和自動檢測。
根據組態的系統
根據組態的系統允許開發者對外掛系統進行細粒度的控制。這種方法非常適合於需要高度自定義的外掛架構,例如 Django 的應用系統。在 Django 中,應用程式被安裝到本地環境中,但直到它們被新增到 settings.py 檔案中才會生效。這種方法允許開發者對外掛進行組態,並將組態儲存在版本控制中。
這種方法的優點是它提供了高度的靈活性,開發者可以根據需要組態外掛。然而,這種方法也需要開發者具備一定的技術背景,並且需要手動編輯組態檔案。
自動檢測
自動檢測是一種更加使用者友好的方法,它不需要開發者手動編輯組態檔案。在 Python 中,可以使用 entrypoints 來實作自動檢測。entrypoints 是一種機制,允許 Python 包宣告它們提供的入口點。這些入口點可以被其他程式碼使用,以實作外掛的自動檢測。
使用固定名稱的外掛檢測
另一種方法是使用固定名稱的外掛檢測。這種方法涉及建立一個自定義的 Python 檔案,例如 custom_sensors.py,並在其中定義感測器。然後,可以使用 vars() 函式來檢測該檔案中定義的感測器。
def get_sensors() -> t.Iterable[Sensor[t.Any]]:
try:
import custom_sensors
except ImportError:
discovered = []
else:
discovered = [
attribute
for attribute in vars(custom_sensors).values()
if isinstance(attribute, type)
and issubclass(attribute, Sensor)
]
return discovered
使用 entrypoints 的外掛檢測
使用 entrypoints 可以實作更加靈活和使用者友好的外掛檢測。要使用 entrypoints,需要在 Python 包的後設資料中宣告入口點。然後,可以使用 entrypoints 來檢測已安裝的外掛。
內容解密:
get_sensors()函式嘗試匯入custom_sensors模組,如果匯入失敗,則傳回空列表。- 如果匯入成功,則使用
vars()函式取得custom_sensors模組中定義的所有屬性。 - 然後,使用列表推導式篩選出那些是
Sensor子類別的屬性,並將它們新增到discovered列表中。 - 最後,傳回
discovered列表,其中包含所有檢測到的感測器。
對比與選擇
| 特性 | 根據組態的系統 | 自動檢測 |
|---|---|---|
| 安裝難度 | 需要編輯組態檔案 | 只需安裝包 |
| 重新排序外掛 | 可能 | 不可能 |
| 覆寫內建外掛 | 可能 | 不可能 |
| 排除已安裝外掛 | 可能 | 不可能 |
| 外掛引陣列態 | 可能 | 不可能 |
| 使用者友好度 | 需要使用者編輯組態檔案 | 無需額外步驟 |
對於我們的使用案例來說,自動檢測是一種更加合適的方法,因為它提供了更好的使用者經驗。然而,根據組態的系統提供了更多的靈活性,可以根據需要組態外掛。
使用 Entry Points 實作外掛系統
在開發 Python 套件時,entry points 提供了一種有效的機制,讓程式碼能夠在安裝後自動被發現,從而實作外掛系統的功能。這使得不同的套件可以透過 entry points 提供擴充套件點,而無需修改核心程式碼。
Entry Points 的名稱空間
Entry points 使用一個雙層名稱空間。外層是 entry point group,它是一個簡單的字串識別符號。例如,console_scripts 和 gui_scripts 是用於自動生成命令列工具的 entry point group。內層則是個別 entry point 的名稱,這些名稱在同一個 group 內必須是唯一的。
發現 Entry Points
你可以使用 pkg_resources 模組來查詢 Python 環境中已使用的 entry point groups。以下是一個列出所有 entry point groups 的範例程式:
>>> functools.reduce(
... set.union,
... [
... set(package.get_entry_map(group=None).keys())
... for package in pkg_resources.working_set
... ],
... set()
... )
{'nbconvert.exporters', 'egg_info.writers', 'gui_scripts', 'pygments.lexers',
'console_scripts', 'babel.extractors', 'setuptools.installation',
'distutils.setup_keywords', 'distutils.commands'}
內容解密:
- 使用
functools.reduce合併集合:這段程式碼使用functools.reduce將多個集合合併成一個,展示瞭如何扁平化列表(或集合)。 pkg_resources.working_set:這裡遍歷了當前 Python 環境中所有已安裝的套件。package.get_entry_map(group=None).keys():取得每個套件提供的 entry point groups。set.union:用於合併集合的操作,確保結果不重複。
定義和使用 Entry Points
要在你的套件中使用 entry points,首先需要決定一個 entry point group 名稱。在這個例子中,我們使用 apd.sensors.sensor。然後,在 setup.cfg 檔案中定義 entry points:
[options.entry_points]
apd.sensors.sensor =
PythonVersion = apd.sensors.sensors:PythonVersion
IPAddresses = apd.sensors.sensors:IPAddresses
CPULoad = apd.sensors.sensors:CPULoad
RAMAvailable = apd.sensors.sensors:RAMAvailable
ACStatus = apd.sensors.sensors:ACStatus
Temperature = apd.sensors.sensors:Temperature
RelativeHumidity = apd.sensors.sensors:RelativeHumidity
其他套件可以透過相同的 entry point group 名稱新增新的外掛:
[options.entry_points]
apd.sensors.sensor =
SolarCumulativeOutput = apd.sunnyboy_solar.sensor:SolarCumulativeOutput
載入 Entry Points
要使用 entry points 載入外掛,需要修改核心程式碼中的 get_sensors 方法:
def get_sensors() -> t.Iterable[Sensor[t.Any]]:
sensors = []
for sensor_class in pkg_resources.iter_entry_points("apd.sensors.sensor"):
class_ = sensor_class.load()
sensors.append(t.cast(Sensor[t.Any], class_()))
return sensors
內容解密:
pkg_resources.iter_entry_points("apd.sensors.sensor"):遍歷指定 entry point group 中的所有 entry points。sensor_class.load():載入 entry point 對應的類別。t.cast(Sensor[t.Any], class_()):將載入的類別例項轉換為Sensor型別,這裡假設外掛作者提供的 entry points 都是有效的感測器類別。