前文介绍到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方法。