插件式程序設計與開發實踐總結
By:授客 QQ:1033553122
開發環境
win 10
python 3.6.5
代碼結構
需求描述
如上,以user.py為程序入口腳本,運行該腳本時,需要創建一個user類對象,執行一系列動作(包含一系列動作的列表)。程序執行動作前,要求先獲取動作名稱,根據該名稱,執行不同的操作。這些操作,對應不同的類函數。
實現思路
大致實現思路就是,把user對象需要運行的類函數(使用@classmethod修飾的函數,可不用創建對象進行調用),當作插件函數,並設置為user的屬性,這樣程序運行時,可通過該屬性來調用對應的類函數。這里的問題是,程序怎么知道執行哪個類函數呢?到目前為止,程序只能根據動作名稱來判斷待執行的操作,所以,需要建立動作名稱和類函數的映射關系。
怎么建立動作名稱和類函數的映射關系呢?這里用到了裝飾器,新建一個裝飾器類ActionDecorator,為該類設置一個字典類型的類屬性ACTION_FUNC_CLASS_MODULE_MAP,用這個類來存放動作名稱和類函數的映射關系。我們把需要當作插件函數的類函數都用該裝飾器進行修飾。
這里,筆者發現一個特性,就是對應模塊被導入時,對應模塊,對應類函數如果使用了裝飾器,該裝飾器函數會被執行。同時,筆者還發現另外一個特性,
首次對某個包執行import操作時,該包下面的__init__.py文件將優先被執行。基於這兩個特性,我們把裝飾器放在用於管理插件類函數的外圍軟件包下(例中的components包),同時,在該外圍軟件包下的__init__.py中加入動態加載插件模塊的代碼:遍歷外圍軟件包下的所有非__init__.py文件,並且動態加載改模塊。這樣,當在user.py入口文件中,執行from components.decoraters.action_decorater import ActionDecorator時,會自動執行components/__init__.py文件,動態加載所有插件模塊,並且自動觸發裝飾器的執行,裝飾器方法執行,會自動根據提供的方法參數建立動作名稱和類函數的映射關系。
然后,在初始化user對象時,給該對象動態設置屬性,屬性名稱設置為動作名稱,屬性值設置為類方法,這樣,執行動作時,就可以根據動作名稱調用對應的類方法了。
代碼實現
action_decorate.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
'''
@CreateTime: 2020/12/09 14:58
@Author : shouke
'''
class ActionDecorator(object):
'''
action 裝飾器
'''
ACTION_FUNC_CLASS_MODULE_MAP = {}
@classmethod
def action_register(cls, action, class_name, function_name, module_path):
def wrapper(func):
cls.ACTION_FUNC_CLASS_MODULE_MAP.update({action: {'class_name':class_name, 'function_name':function_name, 'module_path':module_path}})
return func
return wrapper
components/__init__.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
'''
@Author : shouke
'''
import os.path
import importlib
def load_plugin_modules():
'''遞歸加載當前目錄下的所有模塊'''
head, tail = os.path.split(__file__)
package_father_path, package = os.path.split(head)
def load_modules(dir_path):
nonlocal package_father_path
if not os.path.isdir(dir_path):
return
for name in os.listdir(dir_path):
full_path = os.path.join(dir_path, name)
if os.path.isdir(full_path):
load_modules(full_path)
elif not name.startswith('_') and name.endswith('.py'):
temp_path = full_path.replace(package_father_path, '')
relative_path = temp_path.replace('\\', '/').lstrip('/').replace('/', '.')
importlib.import_module(relative_path.rstrip('.py'), package=package)
load_modules(head)
# 加載模塊,自動觸發裝飾器,獲取相關插件函數相關信息
load_plugin_modules()
assertioner.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
'''
@Author : shouke
'''
from components.decoraters.action_decorater import ActionDecorator
class Assertioner(object):
@classmethod
@ActionDecorator.action_register('assert_equal', 'Assertioner', 'assert_equal', __name__)
def assert_equal(self, config:dict, *args, **kwargs):
print('執行斷言')
print('斷言配置:\n', config)
send_request.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
'''
@Author : shouke
'''
from components.decoraters.action_decorater import ActionDecorator
class Requester(object):
@ActionDecorator.action_register('send_request', 'Requester', 'send_request', __name__)
@classmethod
def send_request(self, config:dict, *args, **kwargs):
print('發送請求')
print('請求配置:')
print(config)
example.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
'''
@CreateTime: 2020/12/10 15:51
@Author : shouke
'''
from components.decoraters.action_decorater import ActionDecorator
class CustomClassName(object):
@ActionDecorator.action_register('custom_action_name', 'CustomClassName', 'action_func_name', __name__)
@classmethod
def action_func_name(self, config:dict, *args, **kwargs):
'''
example
user_instance: kwargs['user'] # 壓測用戶實例
'''
# do something you want
# 說明 plugings目錄下可自由創建python包,管理插件,當然,也可以位於components包下其它任意位置創建python包,管理插件(不推薦)
user.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
'''
@Author : shouke
'''
from components.decoraters.action_decorater import ActionDecorator
class User(object):
def __init__(self):
for action, action_map in ActionDecorator.ACTION_FUNC_CLASS_MODULE_MAP.items():
module = __import__(action_map.get('module_path'), fromlist=['True'])
class_cls = getattr(module, action_map.get('class_name'))
setattr(self, action, getattr(class_cls, action_map.get('function_name')))
def run_actions(self, actions):
''' 執行一系列動作 '''
for step in actions:
action = step.get('action')
if hasattr(self, action):
getattr(self, action)(step, user=self)
if __name__ == '__main__':
actions = [{
"action": "send_request",
"name": "請求登錄", #可選配置,默認為None
"method": "POST",
"path": "/api/v1/login",
"body": {
"account": "shouke",
"password": "123456"
},
"headers": {
"Content-Type": "application/json"
}
},
{
"action": "assert_equal",
"name": "請求響應斷言",
"target": "body",
"rule": "assert_contain",
"patterns": ["shouke","token"],
"logic":"or"
}]
User().run_actions(actions)
運行結果
發送請求
請求配置:
{'action': 'send_request', 'name': '請求登錄', 'method': 'POST', 'path': '/api/v1/login', 'body': {'account': 'shouke', 'password': '123456'}, 'headers': {'Content-Type': 'application/json'}}
執行斷言
斷言配置:
{'action': 'assert_equal', 'name': '請求響應斷言', 'target': 'body', 'rule': 'assert_contain', 'patterns': ['shouke', 'token'], 'logic': 'or'}
