首頁 > 軟體

Python測試框架pytest核心庫pluggy詳解

2022-08-04 22:04:53

程式碼案例

import pluggy
# HookspecMarker 和 HookimplMarker 實質上是一個裝飾器帶引數的裝飾器類,作用是給函數增加額外的屬性設定
hookspec = pluggy.HookspecMarker("myproject")
hookimpl = pluggy.HookimplMarker("myproject")
'''
HookspeckMarker:
    傳入firstresult=True時,獲取第一個plugin執行結果後就停止繼續執行 @hookspec(firstresult=True)
    historic - 表示這個 hook 是需要儲存call history 的,當有新的 plugin 註冊的時候,需要回放歷史
hookimpl:
    當傳入tryfirst=True時,表示這個類的hook函數會優先執行,其他的仍然按照後進先出的順序執行
    當傳入trylast=True,表示當前外掛的hook函數會盡可能晚的執行,其他的仍然按照後進先出的順序執行
    當傳入hookwrapper=True時,需要在這個plugin中實現一個yield,plugin先執行yield之前的程式碼,
        然後去執行其他的pluggin,然後再回來執行yield之後的程式碼,同時通過yield可以獲取到其他外掛執行的結果
'''
# 定義自己的Spec,這裡可以理解為定義介面類
class MySpec:
    # hookspec 是一個裝飾類中的方法的裝飾器,為此方法增額外的屬性設定,這裡myhook可以理解為定義了一個介面
    # 會給當前方法新增屬性  鍵為 {self.project_name + "_spec"} 值是裝飾器傳入的引數
    @hookspec
    def myhook(self, arg1, arg2):
        pass
# 定義了一個外掛
class Plugin_1:
    # 外掛中實現了上面定義的介面,同樣這個實現介面的方法用 hookimpl裝飾器裝飾,功能是返回兩個引數的和
    @hookimpl
    def myhook(self, arg1, arg2):
        print("inside Plugin_1.myhook()")
        return arg1 + arg2
# 定義第二個外掛
class Plugin_2:
    # 外掛中實現了上面定義的介面,同樣這個實現介面的方法用 hookimpl裝飾器裝飾,功能是返回兩個引數的差
    @hookimpl(hookwrapper=True)
    def myhook(self, arg1, arg2):
        out = yield
        print("inside Plugin_2.myhook()")
        return arg1 - arg2
# 範例化一個外掛管理的物件,注意這裡的名稱要與檔案開頭定義裝飾器的時候的名稱一致
pm = pluggy.PluginManager("myproject")
# 將自定義的介面類加到勾點定義中去
pm.add_hookspecs(MySpec)
# 註冊定義的兩個外掛
pm.register(Plugin_1())
pm.register(Plugin_2())
# 通過外掛管理物件的勾點呼叫方法,這時候兩個外掛中的這個方法都會執行,而且遵循後註冊先執行即LIFO的原則,兩個外掛的結果講義列表的形式返回
results = pm.hook.myhook(arg1=1, arg2=2)
print(results)

範例化:

  • 初始化一些引數,如_name2plugin:存放後續註冊 plugin

新增到勾點定義中 (add_hookspecs)

  • 將定義的類已引數的方式傳遞進去 (module_or_class)
  • 遍歷類中全部的方法
  • 判斷: getattr(method, self.project_name + "_spec", None),判斷類方法中是否有當前定義 myproject+spec 的屬性,
    • 如果有則返回裝飾器所得到的引數,沒有則返回 None,其實就是判斷有沒有被@hookspec裝飾,因為裝飾了會設定上 myproject+spec 這個屬性以及相對應的值
  • 如果有被裝飾: 判斷一下 self.hook 中是否以及存在了這個 spec
  • 如果不存在: 建立一個_HookCaller(spec 名字,_hookexec(本質就是一個執行 hook 的方法),傳遞進來的 spec 物件,第三步獲得的引數,也就是通過裝飾器 set 到方法中的一些引數) 物件
    • init: 判斷 spec 物件是否為空,如果不為空:
      • ​ 先判斷引數是否存在,如果存在,建立一個 HookSpec 給 self.spec,傳遞引數為 當前 spec 物件,當前 spec 名字,引數,self.function 就是對應的那個被裝飾的方法,最後判斷一下這個 spec 需不需要儲存歷史,如果需要,初始化一個列表
  • 將上面初始化的物件,通過 setattr 的方式存在到 self.hook 中,名字就是被裝飾方法的名字,值是剛剛建立的物件
  • 最後會在 names 的列表中把這個新增的方法的名字新增進去,判斷一個 names 是否為空,如果為空,則丟擲異常
  • 新增 add_hookspecs 的步驟全部完成

註冊外掛 register

註冊外掛 (register): 傳遞實現外掛的實體類物件

  • 判斷是否傳遞外掛名字,如果沒傳,就獲取物件的name屬性,如果還沒有就直接用 id() 生產一個隨機字串做當前物件在外掛中的名字

  • 判斷名字是否存在,或者是否已被註冊: self._name2plugin 和 self._plugin2hookcaller,前者是用 plugin_name 做 key,後者是用 plugin object 做 key,判斷是否已經註冊過重複的 plugin

  • self._name2plugin[plugin_name(外掛名字)] = plugin(傳遞的實體類物件)
    self._plugin2hookcallers[plugin(傳遞的實體類物件)] = hookcallers = [],其實就是初始化一下 self._plugin2hookcallers[plugin],因為列表的參照傳遞,所有直接修改 hookcallers 也可以作用在 self 中

  • 遍歷實體類物件的方法列表,判斷是否被 impl 裝飾:

    • a.先獲取到方法物件
    • b.判斷物件是否是內建函數、函數、方法或者方法描述符,如果不是直接返回
    • c.獲取該方法的屬性 hook 物件建立時傳遞的名字 (myproject) + "_impl" ,沒有則返回 None
    • d.判斷獲取到的值是不是 None 並且不是一個字典,則將獲取到的 res 賦值為 None
    • e. 最後返回 res,其實就是 hookimpl 裝飾器,如果你不給值就給一堆預設值
  • 先判斷參數列是否為空: 如果不為空,進行設定預設值 (其實正常是不會出現沒有值的情況),然後從實體類物件中獲取到該方法的物件

    • 在建立一個新的物件 (HookImpl):init(self,傳遞進來的實體類物件,hook 名字 (第一步獲取或者 id 生成),method 物件,第四步返回的引數字典),並且將參數列跟新到 self.dict
  • 判斷 self.hook 中是否以及註冊了當前外掛 (就是 add_hookspecs 註冊的 spec 中是否有當前方法)

    • 如果沒有註冊,會直接註冊一個,這樣註冊 specmodule_or_class 引數會為空,意為著不會有額外的一些引數 eg:tryfirst
  • hook.has_spec() 判斷註冊 spec 的 spec 屬性不為空

    • self._verify_hook(hook(spec 物件), hookimpl(外掛物件)) a.先判斷當前物件中是否有 (_call_history 屬性),歷史 和是否 需要使用 yield b. 判斷 hookimpl 和 hook.spec 的參數列是否相等,如果不相等報錯
  • hook._maybe_apply_history(hookimpl)
    a.判斷是否有_call_history 這個屬性

  • hook._add_hookimpl(hookimpl):
    a.判斷是否為 hookwrapper 為 True,新增到不同的 wrappers 中
    b.判斷是否有 trylast tryfirst 屬性,將 hookimpl 存放到對應位置
    c.將 hook 新增到 hookcallers 中

  • 遍歷結束後,返回 plugin_name(第一步產生)

執行外掛 pm.hook.myhook

執行外掛 pm.hook.myhook(arg1=1, arg2=2):本質就是呼叫物件的call方法

  • 先判斷是否有順序引數,如果有直接報錯
  • 在判斷是否有_call_history 這個屬性
  • 判斷實際傳入引數,是否和外掛需要引數一樣
  • self._hookexec(self(hook 物件), self.get_hookimpls()(全部的已經註冊的外掛), kwargs(傳入的引數))

self._inner_hookexec(hook(hook 物件), methods(外掛), kwargs(引數))

# 實際呼叫,也就是hook.multicall的方法
self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall(
            methods,
            kwargs,
            firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
        )
  • _multicall(hook_impls(外掛), caller_kwargs(引數), firstresult=False(@hookspec傳入,預設 False))

1. 先將 hook_impls 變成一個可迭代物件 (reversed(hook_impls))

2. 先把順序引數的參數列,拿到 (列表推導式)

3. 判斷需不需要將其他外掛執行的結果傳遞進去

- 需要
- 先從 hook_impl 中拿出對應的方法並且傳遞引數,執行關鍵字 yield 前面部分
- 然後 next()
- 最後將這個方法新增到 teardowns 列表中去

- 不需要
- 先從 hook_impl 中拿出對於的方法並且傳遞引數
- 判斷執行後的返回值是不是為空,不為空則新增到 results 列表中
- 最後判斷是否有 firstresult 屬性,如果有直接結束迴圈

4. 最後執行 (finally 中程式碼)

- 如果 firstresult 為 true,那麼直接返回第一個外掛返回的結果即可
- 執行 teardowns 列表中的需要最後執行的外掛
- 通過迭代器的 send 方法,將上幾個外掛的結果傳遞進去

5. 返回 result 物件 :會判斷是否有報錯 如果沒有直接返回結果列表,如果有報錯會丟擲異常

以上就是Python測試框架pytest核心庫pluggy詳解的詳細內容,更多關於Python pytest庫pluggy的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com