跳到主要内容

startup

初始化 PytestPluginManager 和 Config

src_pytest\config__init__.py
def console_main() -> int:

code = main()
src_pytest\config__init__.py
def main(
args: list[str] | os.PathLike[str] | None = None,
plugins: Sequence[str | _PluggyPlugin] | None = None,
) -> int | ExitCode:

config = _prepareconfig(args, plugins)

ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)
src_pytest\config__init__.py
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:]

config = get_config(args, plugins)

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
)
src_pytest\config__init__.py
def get_config(
args: list[str] | None = None,
plugins: Sequence[str | _PluggyPlugin] | None = None,
) -> Config:

pluginmanager = 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)
src_pytest\config__init__.py
class PytestPluginManager(PluginManager):

self.add_hookspecs(_pytest.hookspec)
self.register(self)
if os.environ.get("PYTEST_DEBUG"):

self.trace.root.setwriter(err.write)
self.enable_tracing()

self.rewrite_hook = _pytest.assertion.DummyRewriteHook()
# Used to know when we are importing conftests after the pytest_configure stage.
self._configured = False
src_pytest\config__init__.py
class Config:

def __init__(
self,
pluginmanager: PytestPluginManager,
*,
invocation_params: InvocationParams | None = None,
) -> None:

self.option = argparse.Namespace()

self._parser = Parser(
usage=f"%(prog)s [options] [{_a}] [{_a}] [...]",
processopt=self._processopt,
_ispytest=True,
)
self.pluginmanager = pluginmanager

self.pluginmanager.register(self, "pytestconfig")
self._configured = False
self.hook.pytest_addoption.call_historic(
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
)
src_pytest\config__init__.py
class PytestPluginManager(PluginManager):

def consider_preparse(
self, args: Sequence[str], *, exclude_only: bool = False
) -> None:

self.consider_pluginarg(parg)
src_pytest\config__init__.py
class PytestPluginManager(PluginManager):

def consider_pluginarg(self, arg: str) -> None:

if arg.startswith("no:"):

self.set_blocked(name)
src_pytest\config__init__.py
class PytestPluginManager(PluginManager):

def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:

if self.is_blocked(modname) or self.get_plugin(modname) is not None:
return

importspec = "_pytest." + modname if modname in builtin_plugins else modname

mod = sys.modules[importspec]
self.register(mod, modname)

注册default_plugins

使能的default_plugins的pytest_addoption hook被调用

src_pytest\config__init__.py
def get_config(
args: list[str] | None = None,
plugins: Sequence[str | _PluggyPlugin] | None = None,
) -> Config:

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)
src_pytest\hookspec.py
def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None:
"""Register argparse-style options and ini-style config values,
called once at the beginning of a test run.

pytest_cmdline_parse

三方的plugin和conftest的pytest_addoption hook被调用

src_pytest\config__init__.py
class Config:

def pytest_cmdline_parse(
self, pluginmanager: PytestPluginManager, args: list[str]
) -> Config:

self.parse(args)

def parse(self, args: list[str], addopts: bool = True) -> None:

self.hook.pytest_addhooks.call_historic(
kwargs=dict(pluginmanager=self.pluginmanager)
)
self._preparse(args, addopts=addopts)
self._parser.after_preparse = True # type: ignore

args = self._parser.parse_setoption(
args, self.option, namespace=self.option
)
self.args, self.args_source = self._decide_args(
args=args,
pyargs=self.known_args_namespace.pyargs,
testpaths=self.getini("testpaths"),
invocation_dir=self.invocation_params.dir,
rootpath=self.rootpath,
warn=True,
)

Config._preparse

src_pytest\config__init__.py
class Config:

def _preparse(self, args: list[str], addopts: bool = True) -> None:
if addopts:
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
if len(env_addopts):
args[:] = (
self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
+ args
)
self._initini(args)
if addopts:
args[:] = (
self._validate_args(self.getini("addopts"), "via addopts config") + args
)

self.known_args_namespace = self._parser.parse_known_args(
args, namespace=copy.copy(self.option)
)
self._checkversion()
self._consider_importhook(args)
self._configure_python_path()
self.pluginmanager.consider_preparse(args, exclude_only=False)
if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
# Don't autoload from distribution package entry point. Only
# explicitly specified plugins are going to be loaded.
self.pluginmanager.load_setuptools_entrypoints("pytest11")
self.pluginmanager.consider_env()

self.known_args_namespace = self._parser.parse_known_args(
args, namespace=copy.copy(self.known_args_namespace)
)

self._validate_plugins()
self._warn_about_skipped_plugins()

confcutdir = str(self.inipath.parent)

self.known_args_namespace.confcutdir = confcutdir

self.hook.pytest_load_initial_conftests(
early_config=self, args=args, parser=self._parser
)
src_pytest\config__init__.py
class Config:

def _consider_importhook(self, args: Sequence[str]) -> None:

hook = _pytest.assertion.install_importhook(self)

self._mark_plugins_for_rewrite(hook)

def _mark_plugins_for_rewrite(self, hook) -> None:

self.pluginmanager.rewrite_hook = hook

for name in _iter_rewritable_modules(package_files):
hook.mark_rewrite(name)
src_pytest\config__init__.py
class PytestPluginManager(PluginManager):

def consider_preparse(
self, args: Sequence[str], *, exclude_only: bool = False
) -> None:

if opt == "-p":

self.consider_pluginarg(parg)

def consider_pluginarg(self, arg: str) -> None:

self.import_plugin(arg, consider_entry_points=True)
src_pytest\hookspec.py
def pytest_load_initial_conftests(
early_config: Config, parser: Parser, args: list[str]
) -> None:
"""Called to implement the loading of :ref:`initial conftest files
<pluginorder>` ahead of command line option parsing.

capture.pytest_load_initial_conftests

通过修改sys.stdout等将对应流定位到tempfile,如果不出错则最后放到report中, 出错则恢复并输出

src_pytest\capture.py
def pytest_load_initial_conftests(early_config: Config) -> Generator[None]:

capman.start_global_capturing()
try:
try:
yield
finally:
capman.suspend_global_capture()
except BaseException:
out, err = capman.read_global_capture()
sys.stdout.write(out)
sys.stderr.write(err)
Config.pytest_load_initial_conftests

如果命令行指定了目录则使用,否则使用当前目录

src_pytest\config__init__.py
class Config:

def pytest_load_initial_conftests(self, early_config: Config) -> None:

args, args_source = early_config._decide_args(
args=early_config.known_args_namespace.file_or_dir,
pyargs=early_config.known_args_namespace.pyargs,
testpaths=early_config.getini("testpaths"),
invocation_dir=early_config.invocation_params.dir,
rootpath=early_config.rootpath,
warn=False,
)
self.pluginmanager._set_initial_conftests(
args=args,
pyargs=early_config.known_args_namespace.pyargs,
noconftest=early_config.known_args_namespace.noconftest,
rootpath=early_config.rootpath,
confcutdir=early_config.known_args_namespace.confcutdir,
invocation_dir=early_config.invocation_params.dir,
importmode=early_config.known_args_namespace.importmode,
consider_namespace_packages=early_config.getini(
"consider_namespace_packages"
),
)

def _set_initial_conftests(
self,
args: Sequence[str | pathlib.Path],
pyargs: bool,
noconftest: bool,
rootpath: pathlib.Path,
confcutdir: pathlib.Path | None,
invocation_dir: pathlib.Path,
importmode: ImportMode | str,
*,
consider_namespace_packages: bool,
) -> None:

self._try_load_conftest(
anchor,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)

def _try_load_conftest(
self,
anchor: pathlib.Path,
importmode: str | ImportMode,
rootpath: pathlib.Path,
*,
consider_namespace_packages: bool,
) -> None:

self._loadconftestmodules(
anchor,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)

if anchor.is_dir():
for x in anchor.glob("test*"):
if x.is_dir():
self._loadconftestmodules(
x,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)

def _loadconftestmodules(
self,
path: pathlib.Path,
importmode: str | ImportMode,
rootpath: pathlib.Path,
*,
consider_namespace_packages: bool,
) -> None:

directory = self._get_directory(path)

for parent in reversed((directory, *directory.parents)):

conftestpath = parent / "conftest.py"
if conftestpath.is_file():
mod = self._importconftest(
conftestpath,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
clist.append(mod)
self._dirpath2confmods[directory] = clist

def _importconftest(
self,
conftestpath: pathlib.Path,
importmode: str | ImportMode,
rootpath: pathlib.Path,
*,
consider_namespace_packages: bool,
) -> types.ModuleType:

mod = import_path(
conftestpath,
mode=importmode,
root=rootpath,
consider_namespace_packages=consider_namespace_packages,
)

self.consider_conftest(mod, registration_name=conftestpath_plugin_name)

def consider_conftest(
self, conftestmodule: types.ModuleType, registration_name: str
) -> None:
""":meta private:"""
self.register(conftestmodule, name=registration_name)

Parser.parse_setoption

src_pytest\config\argparsing.py
class Parser:

def parse_setoption(
self,
args: Sequence[str | os.PathLike[str]],
option: argparse.Namespace,
namespace: argparse.Namespace | None = None,
) -> list[str]:
parsedoption = self.parse(args, namespace=namespace)
for name, value in parsedoption.__dict__.items():
setattr(option, name, value)
return cast(List[str], getattr(parsedoption, FILE_OR_DIR))

pytest_cmdline_main

src_pytest\main.py
def pytest_cmdline_main(config: Config) -> int | ExitCode:
return wrap_session(config, _main)

wrap_session

def wrap_session(
config: Config, doit: Callable[[Config, Session], int | ExitCode | None]
) -> int | ExitCode:

session = Session.from_config(config)

config._do_configure()
initstate = 1
config.hook.pytest_sessionstart(session=session)
initstate = 2
session.exitstatus = doit(config, session) or 0

finally:

config.hook.pytest_sessionfinish(
session=session, exitstatus=session.exitstatus
)

config._ensure_unconfigure()

_main

def _main(config: Config, session: Session) -> int | ExitCode | None:

config.hook.pytest_collection(session=session)
config.hook.pytest_runtestloop(session=session)