前文介紹到pytest整體是運用插件來實現其運行流程的。這里仔細介紹下具體過程。
首先進入main方法
def main(args: list[str] | os.PathLike[str] | None = None,plugins: Sequence[str | _PluggyPlugin] | None = None,
) -> int | ExitCode:"""Perform an in-process test run.:param args:List of command line arguments. If `None` or not given, defaults to readingarguments directly from the process command line (:data:`sys.argv`).:param plugins: List of plugin objects to be auto-registered during initialization.:returns: An exit code."""old_pytest_version = os.environ.get("PYTEST_VERSION")try:os.environ["PYTEST_VERSION"] = __version__try:config = _prepareconfig(args, plugins)except ConftestImportFailure as e:exc_info = ExceptionInfo.from_exception(e.cause)tw = TerminalWriter(sys.stderr)tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)exc_info.traceback = exc_info.traceback.filter(filter_traceback_for_conftest_import_failure)exc_repr = (exc_info.getrepr(style="short", chain=False)if exc_info.tracebackelse exc_info.exconly())formatted_tb = str(exc_repr)for line in formatted_tb.splitlines():tw.line(line.rstrip(), red=True)return ExitCode.USAGE_ERRORelse:try:ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)try:return ExitCode(ret)except ValueError:return retfinally:config._ensure_unconfigure()except UsageError as e:tw = TerminalWriter(sys.stderr)for msg in e.args:tw.line(f"ERROR: {msg}\n", red=True)return ExitCode.USAGE_ERRORfinally:if old_pytest_version is None:os.environ.pop("PYTEST_VERSION", None)else:os.environ["PYTEST_VERSION"] = old_pytest_version
這個main方法,最重要的有兩步,第一步是
config = _prepareconfig(args, plugins)
這一步就是讀取配置以及注冊插件等動作。這里附下_prepareconfig方法
def _prepareconfig(args: list[str] | os.PathLike[str] | None = None,plugins: Sequence[str | _PluggyPlugin] | None = None,
) -> Config:if args is None:args = sys.argv[1:]elif isinstance(args, os.PathLike):args = [os.fspath(args)]elif not isinstance(args, list):msg = ( # type:ignore[unreachable]"`args` parameter expected to be a list of strings, got: {!r} (type: {})")raise TypeError(msg.format(args, type(args)))config = get_config(args, plugins)pluginmanager = config.pluginmanagertry:if plugins:for plugin in plugins:if isinstance(plugin, str):pluginmanager.consider_pluginarg(plugin)else:pluginmanager.register(plugin)config = pluginmanager.hook.pytest_cmdline_parse(pluginmanager=pluginmanager, args=args)return configexcept BaseException:config._ensure_unconfigure()raise
這里這個方法可以看到,如果有plugin傳入,則會注冊。但是我們知道有些默認的插件是沒有傳入的,也注冊了。其是通過get_config(args, plugins)這個方法來注冊的
def get_config(args: list[str] | None = None,plugins: Sequence[str | _PluggyPlugin] | None = None,
) -> Config:# subsequent calls to main will create a fresh instancepluginmanager = PytestPluginManager()config = Config(pluginmanager,invocation_params=Config.InvocationParams(args=args or (),plugins=plugins,dir=pathlib.Path.cwd(),),)if args is not None:# Handle any "-p no:plugin" args.pluginmanager.consider_preparse(args, exclude_only=True)for spec in default_plugins:pluginmanager.import_plugin(spec)return config
這里初始化pluginmanager時,添加了默認的插件接口類
可以看到這個文件里都是插件的接口類,并且都是以pytest_開頭的。
注意這里有些方法沒有加@hookspec的裝飾器,但是也添加進去了,這是因為pytest對其做了一層處理。我們知道add hooksepcs時,主要是判斷其有無對應的spec_opts,沒有添加@hooksepc的就沒有sepc_opts。
對于這種沒有添加@hookspec的方法,pytest重寫了parse_hookspec_opts方法
這里可以看到,其先取了下對應的接口方法有無opts參數,如果沒有,則判斷一下方法是否是以pytest_開頭的,如果是,則添加opts參數。所以這里添加了hooksepc.py文件中所有的接口類方法。
然后PytestPluginManager類中self.register(self)注冊了它自己類中的插件,然后運行到get_config方法中的
for spec in default_plugins:pluginmanager.import_plugin(spec)
這里將default_plugins中的插件也都注冊了,default_plugin如下
import_plugin方法如下
def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:"""Import a plugin with ``modname``.If ``consider_entry_points`` is True, entry point names are alsoconsidered to find a plugin."""# Most often modname refers to builtin modules, e.g. "pytester",# "terminal" or "capture". Those plugins are registered under their# basename for historic purposes but must be imported with the# _pytest prefix.assert isinstance(modname, str), f"module name as text required, got {modname!r}"if self.is_blocked(modname) or self.get_plugin(modname) is not None:returnimportspec = "_pytest." + modname if modname in builtin_plugins else modnameself.rewrite_hook.mark_rewrite(importspec)if consider_entry_points:loaded = self.load_setuptools_entrypoints("pytest11", name=modname)if loaded:returntry:__import__(importspec)except ImportError as e:raise ImportError(f'Error importing plugin "{modname}": {e.args[0]}').with_traceback(e.__traceback__) from eexcept Skipped as e:self.skipped_plugins.append((modname, e.msg or ""))else:mod = sys.modules[importspec]self.register(mod, modname)
這里主要就是把default_plugins中提到的文件中的插件實現都注冊了。(注意這里有些接口實現方法也是未加hookimpl裝飾器的,但是也能注冊,同上面添加spec的方法 ,pytest重新實現了parse_hookimpl_opts方法,只要是以pytest_開頭的方法都可以正常注冊)
回到main方法,_prepareconfig這一步就是將配置讀取完,將默認插件注冊完成。
接下來main方法執行到config.hook.pytest_cmdline_main(config=config),這個方法在hookspec中有接口,實現的方法也有多處。
對比可以看到這些文件中都有實現,其中mark,setuonly,setupplan中的實現方法都加了@pytest.hookimpl(tryfirst=True),按照之前介紹的原則,后加的先執行,加了tryfirst的先執行,這里的執行順序為setupplan,setuponly,mark,cacheprovider,…中實現的pytest_cmdline_main方法。