概述
pytest-lazy-fixtures
是一个 pytest
插件,它允许你以惰性(延迟)的方式使用 fixtures
。这意味着 fixtures
将不会在测试开始时立即执行,而是在真正需要它们的时候才进行加载和执行。这种特性对于以下几种情况特别有用:
-
减少资源消耗:当
fixtures
的设置或拆除过程成本高昂时,延迟它们的执行可以节省资源。 -
避免不必要的执行:如果某些
fixtures
只在特定条件下需要,惰性加载可以避免在不需要时执行它们。 -
提高测试速度:通过延迟非必要的
fixtures
加载,可以减少测试套件的整体启动时间。 -
复杂的测试依赖:在测试依赖于多个复杂的
fixtures
时,可以更灵活地控制它们的加载顺序。 -
参数化测试:它支持与参数化测试结合使用,允许更复杂的测试场景定义。
使用 pytest-lazy-fixtures
插件,你可以定义一个 fixture
,然后在测试函数中以惰性方式引用它。当测试运行到需要该 fixture
的时候,它才会被加载和执行。
安装 pytest-lazy-fixtures
插件非常简单,可以通过 pip
进行安装:
pip install pytest-lazy-fixtures
使用插件后,你可以通过 lazy_fixture
函数来引用 fixtures
,该函数接受一个或多个 fixture
名称作为参数,并返回一个或多个 LazyFixture
对象。在测试函数中使用这些对象,pyteset
会在适当的时候解析并加载相应的 fixtures
。
例如:
import pytest
from pytest_lazy_fixtures import lazy_fixture
@pytest.mark.usefixtures("db_fixture")
def test_example(lazy_fixture("db_session")):
# 在这里,db_session fixture 将被延迟加载
session = lazy_fixture("db_session") # 获取实际的 fixture 实例
# 使用 session 进行测试...
在这个例子中,db_session fixture
只有在 test_example
测试函数实际需要它时才会被加载。如果测试函数有多个参数,所有参数都将按需加载。这为测试提供了更高的灵活性和效率。
源码解读
源码目录结构
从官网下载解压到本地:
root@Gavin:~/pytest_plugin/pytest_lazy_fixtures-1.1.1# ll
total 36
drwxr-xr-x 4 root root 4096 Jul 24 13:46 ./
drwxr-xr-x 21 root root 4096 Jul 24 13:46 ../
-rw-r--r-- 1 root root 1069 Jul 23 05:39 LICENSE
-rw-r--r-- 1 root root 4502 Jan 1 1970 PKG-INFO
-rw-r--r-- 1 root root 1666 Jul 23 05:39 pyproject.toml
drwxr-xr-x 2 root root 4096 Jul 24 13:46 pytest_lazy_fixtures/
-rw-r--r-- 1 root root 3683 Jul 23 05:39 README.md
drwxr-xr-x 2 root root 4096 Jul 24 13:46 tests/
root@Gavin:~/pytest_plugin/pytest_lazy_fixtures-1.1.1# cd pytest_lazy_fixtures/
root@Gavin:~/pytest_plugin/pytest_lazy_fixtures-1.1.1/pytest_lazy_fixtures# ll
total 32
drwxr-xr-x 2 root root 4096 Jul 24 13:46 ./
drwxr-xr-x 4 root root 4096 Jul 24 13:46 ../
-rw-r--r-- 1 root root 93 Jul 23 05:39 __init__.py
-rw-r--r-- 1 root root 966 Jul 23 05:39 lazy_fixture_callable.py
-rw-r--r-- 1 root root 839 Jul 23 05:39 lazy_fixture.py
-rw-r--r-- 1 root root 864 Jul 23 05:39 loader.py
-rw-r--r-- 1 root root 3322 Jul 23 05:39 normalizer.py
-rw-r--r-- 1 root root 600 Jul 23 05:39 plugin.py
-rw-r--r-- 1 root root 0 Jul 23 05:39 py.typed
root@Gavin:~/pytest_plugin/pytest_lazy_fixtures-1.1.1/pytest_lazy_fixtures#
lazy_fixture.py文件
from dataclasses import dataclass
from operator import attrgetter
from typing import Optional
import pytest
@dataclass
class LazyFixtureWrapper:
name: str
@property
def fixture_name(self) -> str:
return self.name.split(".", maxsplit=1)[0]
def _get_attr(self, fixture) -> Optional[str]:
splitted = self.name.split(".", maxsplit=1)
if len(splitted) == 1:
return fixture
return attrgetter(splitted[1])(fixture)
def __repr__(self) -> str:
return self.name
def load_fixture(self, request: pytest.FixtureRequest):
return self._get_attr(request.getfixturevalue(self.fixture_name))
def __hash__(self) -> int:
return hash(self.name)
def lf(name: str) -> LazyFixtureWrapper:
"""lf is a lazy fixture."""
return LazyFixtureWrapper(name)
逻辑说明
-
LazyFixtureWrapper
类封装了延迟加载fixture
的逻辑,允许用户通过字符串名称引用fixture
,并在实际使用时才加载其值。 -
load_fixture
方法是核心,它使用pytest
的request
对象来获取fixture
的值,并根据需要获取其属性。 -
lf
函数提供了一个简洁的接口,用于创建LazyFixtureWrapper
实例。
详细介绍如下:
导入依赖:
-
导入 Python 标准库中的
dataclasses
模块,用于创建数据类。 -
导入
operator
模块中的attrgetter
函数,用于获取对象的属性。 -
导入
typing
模块中的Optional
类型注解。 -
导入
pytest
模块,用于与pytest
框架集成。
定义 LazyFixtureWrapper 数据类:
-
使用
@dataclass
装饰器定义了一个名为LazyFixtureWrapper
的数据类,用于包装延迟加载的fixture
。 -
name
: 一个字符串属性,存储fixture
的名称或其路径(如果需要访问fixture
的属性)。
fixture_name 属性:
-
一个只读属性,返回
name
属性的第一部分(通过.
分隔),这通常是fixture
的基本名称。
. _get_attr 方法:
-
一个私有方法,如果
name
只包含一个部分,直接返回fixture
;如果包含多个部分,使用attrgetter
获取fixture
对象的指定属性。
__repr__
方法:
-
定义了类的字符串表示方法,返回
name
属性的值。
load_fixture 方法:
-
一个公共方法,接受一个
request
参数(pytest.FixtureRequest
类型),调用request.getfixturevalue
方法获取fixture
的值,然后使用_get_attr
方法获取最终的属性值。
__hash__
方法:
-
定义了类的哈希方法,基于
name
属性的哈希值。
lf 函数:
-
一个工厂函数,接受一个字符串参数
name
,创建并返回一个新的LazyFixtureWrapper
实例。
示例
假设有一个 fixture db_session
,你可能想在测试中引用它,但只在实际需要时才加载它:
def test_query(lf("db_session.my_attribute")):
# 只有在这一行时,db_session 及其 my_attribute 才会被加载
my_attribute = lf("db_session.my_attribute").load_fixture(request)
# 使用 my_attribute 进行测试...
在这个例子中,lf
函数用于创建一个延迟加载的 fixture
引用,只有当 load_fixture
被调用时,db_session
和它的 my_attribute
属性才会被实际加载。这有助于减少不必要的资源消耗和初始化。
lazy_fixture_callable.py文件
from typing import Callable, Optional, Union
import pytest
from .lazy_fixture import LazyFixtureWrapper
class LazyFixtureCallableWrapper(LazyFixtureWrapper):
_func: Optional[Callable]
args: tuple
kwargs: dict
def __init__(self, func_or_name: Union[Callable, str], *args, **kwargs):
if callable(func_or_name):
self._func = func_or_name
self.name = func_or_name.__name__
else:
self.name = func_or_name
self._func = None
self.args = args
self.kwargs = kwargs
def get_func(self, request: pytest.FixtureRequest) -> Callable:
func = self._func
if func is None:
func = self.load_fixture(request)
assert callable(func)
return func
def lfc(name: Union[Callable, str], *args, **kwargs) -> LazyFixtureCallableWrapper:
"""lfc is a lazy fixture callable."""
return LazyFixtureCallableWrapper(name, *args, **kwargs)
这段代码提供了一种方式来延迟调用 fixtures
或任意可调用对象,直到它们真正需要被使用时。
逻辑说明
-
LazyFixtureCallableWrapper
类封装了延迟调用fixture
或任意可调用对象的逻辑。 -
如果提供的名称是一个可调用对象,它将被直接存储;如果是一个字符串,则在需要时通过
pytest
的fixture
系统加载。 -
get_func
方法用于获取实际的函数对象,如果之前没有提供函数,则通过加载fixture
获取。 -
lfc
函数提供了一个简洁的接口,用于创建LazyFixtureCallableWrapper
实例。
详细介绍如下:
导入依赖:
-
导入
Callable
,Optional
,Union
等类型注解,用于注解函数和方法的参数和返回值。 -
导入
pytest
模块,用于与pytest
框架集成。 -
从
lazy_fixture
模块导入LazyFixtureWrapper
类。
定义 LazyFixtureCallableWrapper 类:
-
LazyFixtureCallableWrapper
类继承自LazyFixtureWrapper
。
类属性:
-
_func
: 用于存储延迟调用的函数或 None。 -
args
: 存储传递给延迟调用函数的位置参数。 -
kwargs
: 存储传递给延迟调用函数的关键字参数。
构造函数:
-
接受
func_or_name
参数,它可以是一个可调用对象或一个字符串名称。 -
如果
func_or_name
是可调用的,将其存储在_func
中,并使用__name__
属性作为name
。 -
如果
func_or_name
不是可调用的,假定它是一个 fixture 名称,存储在name
中,并将_func
设置为 None。 -
args
和kwargs
分别存储构造函数的位置参数和关键字参数。
get_func 方法:
-
接受一个
request
参数(pytest.FixtureRequest
类型)。 -
如果
_func
是 None,则使用load_fixture
方法加载fixture
,并断言加载的结果是可调用的。 -
返回加载的函数或之前存储的
_func
。
lfc 函数:
-
一个工厂函数,接受
name
参数(可以是函数或字符串名称),以及任意数量的位置参数和关键字参数。 -
创建并返回一个新的
LazyFixtureCallableWrapper
实例。
示例
假设有一个 fixture db_session
,你可能想延迟调用它,直到实际需要执行数据库操作:
def test_query(lfc(db_session)):
# 只有在这一行时,db_session 才会被加载和调用
db = lfc(db_session).get_func(request)
result = db.execute("SELECT * FROM table")
# 使用 result 进行测试...
在这个例子中,lfc
函数用于创建一个延迟调用的 fixture
引用,只有当 get_func
被调用时,db_session
才会被实际加载和调用。这有助于减少不必要的初始化和延迟执行。
loader.py文件
import pytest
from .lazy_fixture import LazyFixtureWrapper
from .lazy_fixture_callable import LazyFixtureCallableWrapper
def load_lazy_fixtures(value, request: pytest.FixtureRequest):
if isinstance(value, LazyFixtureCallableWrapper):
return value.get_func(request)(
*load_lazy_fixtures(value.args, request),
**load_lazy_fixtures(value.kwargs, request),
)
if isinstance(value, LazyFixtureWrapper):
return value.load_fixture(request)
# we need to check exact type
if type(value) is dict: # noqa: E721
return {load_lazy_fixtures(key, request): load_lazy_fixtures(value, request) for key, value in value.items()}
# we need to check exact type
elif type(value) in {list, tuple, set}:
return type(value)([load_lazy_fixtures(value, request) for value in value])
return value
递归地加载延迟加载的 fixtures
。
代码逻辑
-
load_lazy_fixtures
函数负责处理延迟加载的fixtures
,包括LazyFixtureWrapper
和LazyFixtureCallableWrapper
实例。 -
它递归地加载
value
中的fixtures
,无论是作为位置参数、关键字参数,还是嵌套在字典、列表、元组或集合中。 -
函数使用类型检查(
isinstance
)来确定如何处理value
,对于dict
、list
、tuple
和set
,它使用确切的类型检查(type
),以避免错误地处理继承自这些类型的自定义类型。
详细信息描述如下:
导入依赖:
-
导入
pytest
模块,用于与pytest
框架集成。 -
从
lazy_fixture
模块导入LazyFixtureWrapper
类,用于包装延迟加载的fixture
。 -
从
lazy_fixture_callable
模块导入LazyFixtureCallableWrapper
类,用于包装延迟调用的fixture
。
load_lazy_fixtures 函数:
-
函数接受两个参数:
value
,可能是延迟加载的fixture
或其他值;request
,pytest
的FixtureRequest
对象,用于访问fixtures
。 -
函数首先检查
value
是否是LazyFixtureCallableWrapper
的实例。如果是,调用get_func
方法获取实际的函数,并使用load_lazy_fixtures
递归加载args
和kwargs
,然后调用该函数。 -
接下来,检查
value
是否是LazyFixtureWrapper
的实例。如果是,调用load_fixture
方法加载fixture
。 -
然后,检查
value
是否是字典(dict
)的确切类型。如果是,递归地加载字典的键和值。 -
检查
value
是否是列表(list
)、元组(tuple
)或集合(set
)的确切类型。如果是,递归地加载这些可迭代对象中的每个元素,并返回相应的类型。 -
如果
value
不是上述任何一种类型,直接返回value
。
示例
假设你有一些延迟加载的 fixtures
和一个需要这些 fixtures
的测试函数:
def test_example(lfc(db_session)):
# 加载 fixtures 并执行测试逻辑
session = load_lazy_fixtures(lfc(db_session), request)
# 使用 session 进行测试...
在这个例子中,lfc(db_session)
创建了一个延迟加载的 fixture
引用。load_lazy_fixtures
函数用于在需要时加载这个 fixture
,并可以处理更复杂的参数,如字典、列表等,确保所有延迟加载的 fixtures
都被正确加载。
normalizer.py文件
import copy
from typing import Any, Dict, List, Tuple
import pytest
from .lazy_fixture import LazyFixtureWrapper
from .lazy_fixture_callable import LazyFixtureCallableWrapper
def _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, value) -> Tuple[List[str], Dict[str, Any]]:
if isinstance(value, LazyFixtureCallableWrapper):
extra_fixturenames_args, arg2fixturedefs_args = _get_fixturenames_closure_and_arg2fixturedefs(
fm,
metafunc,
value.args,
)
extra_fixturenames_kwargs, arg2fixturedefs_kwargs = _get_fixturenames_closure_and_arg2fixturedefs(
fm,
metafunc,
value.kwargs,
)
return [*extra_fixturenames_args, *extra_fixturenames_kwargs], {
**arg2fixturedefs_args,
**arg2fixturedefs_kwargs,
}
if isinstance(value, LazyFixtureWrapper):
if pytest.version_tuple >= (8, 0, 0):
fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure(metafunc.definition.parent, [value.name], {})
else: # pragma: no cover
# TODO: add tox
_, fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure([value.name], metafunc.definition.parent)
return fixturenames_closure, arg2fixturedefs
extra_fixturenames, arg2fixturedefs = [], {}
# we need to check exact type
if type(value) is dict: # noqa: E721
value = list(value.values())
# we need to check exact type
if type(value) in {list, tuple, set}:
for val in value:
ef, arg2f = _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, val)
extra_fixturenames.extend(ef)
arg2fixturedefs.update(arg2f)
return extra_fixturenames, arg2fixturedefs
def normalize_metafunc_calls(metafunc, used_keys=None):
newcalls = []
for callspec in metafunc._calls:
calls = _normalize_call(callspec, metafunc, used_keys)
newcalls.extend(calls)
metafunc._calls = newcalls
def _copy_metafunc(metafunc):
copied = copy.copy(metafunc)
copied.fixturenames = copy.copy(metafunc.fixturenames)
copied._calls = []
copied._arg2fixturedefs = copy.copy(metafunc._arg2fixturedefs)
return copied
def _normalize_call(callspec, metafunc, used_keys):
fm = metafunc.config.pluginmanager.get_plugin("funcmanage")
used_keys = used_keys or set()
params = callspec.params.copy() if pytest.version_tuple >= (8, 0, 0) else {**callspec.params, **callspec.funcargs}
valtype_keys = params.keys() - used_keys
for arg in valtype_keys:
value = params[arg]
fixturenames_closure, arg2fixturedefs = _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, value)
if fixturenames_closure and arg2fixturedefs:
extra_fixturenames = [fname for fname in fixturenames_closure if fname not in params]
newmetafunc = _copy_metafunc(metafunc)
newmetafunc.fixturenames = extra_fixturenames
newmetafunc._arg2fixturedefs.update(arg2fixturedefs)
newmetafunc._calls = [callspec]
fm.pytest_generate_tests(newmetafunc)
normalize_metafunc_calls(newmetafunc, used_keys | {arg})
return newmetafunc._calls
used_keys.add(arg)
return [callspec]
_get_fixturenames_closure_and_arg2fixturedefs方法
_get_fixturenames_closure_and_arg2fixturedefs
函数递归地处理输入值,以提取与 pytest fixtures
相关的信息。这个函数的目的是找出所有由延迟加载的 fixtures
引用所隐含的 fixtures
名称,并收集它们的详细信息。
代码逻辑:
-
函数首先检查
value
是否是LazyFixtureCallableWrapper
或LazyFixtureWrapper
的实例,并相应地递归处理。 -
对于
LazyFixtureCallableWrapper
,处理附加的位置参数和关键字参数。 -
对于
LazyFixtureWrapper
,调用fm.getfixtureclosure
获取fixtures
的名称闭包和详细信息。 -
对于字典、列表、元组或集合,递归地处理其中的每个元素,以提取所有相关的
fixtures
信息。 -
函数通过递归和类型检查,确保所有可能包含
fixtures
引用的地方都被检查并处理。
详细介绍如下:
-
函数签名:
def _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, value)
: 定义了函数及其参数:fm
:pytest
的funcmanage
插件实例,用于管理fixtures
。metafunc
: 代表当前测试函数的元数据的对象。value
: 可能是LazyFixtureWrapper
或LazyFixtureCallableWrapper
的实例,或者是要检查的值。
-
返回类型注解:
-> Tuple[List[str], Dict[str, Any]]
: 函数返回一个元组,包含一个字符串列表(fixtures
名称的闭包)和一个字典(fixtures
的详细信息)。
-
处理 LazyFixtureCallableWrapper:
- 如果
value
是LazyFixtureCallableWrapper
的实例,递归地处理args
和kwargs
,合并结果并返回。
- 如果
-
处理 LazyFixtureWrapper:
- 如果
value
是LazyFixtureWrapper
的实例,根据pytest
的版本使用不同的方法从fm
获取fixtures
的名称闭包和详细信息。
- 如果
-
版本兼容处理:
- 根据
pytest
的版本(8.0.0 及以上),使用不同的参数顺序调用fm.getfixtureclosure
。
- 根据
-
返回 fixtures 信息:
- 对于
LazyFixtureWrapper
,返回获取的 `fixtures 名称闭包和详细信息。
- 对于
-
处理字典:
- 如果
value
是字典的确切类型,将值转换为列表并递归处理。
- 如果
-
处理列表、元组、集合:
- 如果
value
是列表、元组或集合的确切类型,遍历每个元素,递归调用_get_fixturenames_closure_and_arg2fixturedefs
,并更新结果。
- 如果
-
返回空结果:
- 如果
value
不是上述任何类型,返回空列表和空字典作为默认结果。
- 如果
_copy_metafunc方法
这个函数的作用是创建一个与传入的 metafunc
对象相似但某些属性被重新初始化或复制的新对象。这种复制操作在一些需要对对象进行修改但又不想影响原始对象的场景中可能会用到。
逐步解释:
-
copied = copy.copy(metafunc)
:使用copy.copy
函数创建了metafunc
的浅复制,并将其存储在copied
变量中。 -
copied.fixturenames = copy.copy(metafunc.fixturenames)
:复制了metafunc
对象的fixturenames
属性。 -
copied._calls = []
:将copied
对象的_calls
属性初始化为一个空列表。 -
copied._arg2fixturedefs = copy.copy(metafunc._arg2fixturedefs)
:复制了metafunc
对象的_arg2fixturedefs
属性。 -
最后,函数返回复制后的
copied
对象。
_normalize_call方法
_normalize_call
函数用于处理 pytest
的调用规范(callspec
)和元数据(metafunc
)。
代码逻辑如下:
-
函数的主要目的是处理
callspec
中的参数,特别是那些可能是延迟加载fixtures
的参数。 -
它通过
_get_fixturenames_closure_and_arg2fixturedefs
函数来提取fixtures
信息,并根据这些信息创建新的测试用例。 -
如果参数值包含延迟加载的
fixtures
,会创建metafunc
的副本并更新其属性,然后生成新的测试。 -
函数递归地处理
metafunc
,以确保所有相关的fixtures
都被正确处理。
-
函数定义:
def _normalize_call(callspec, metafunc, used_keys)
: 定义了函数,接受三个参数:callspec
(调用规范),metafunc
(元数据函数),used_keys
(已使用的关键字参数集合)。
-
获取 funcmanage 插件:
fm = metafunc.config.pluginmanager.get_plugin("funcmanage")
: 从pytest
的插件管理器中获取funcmanage
插件的实例。
-
初始化 used_keys:
used_keys = used_keys or set()
: 如果used_keys
未提供或为None
,则初始化为一个新的空集合。
-
复制 params:
params = callspec.params.copy()
: 复制callspec
中的params
字典。对于pytest 8.0.0
及以上版本,params
已经包含了所有参数;对于更低版本,还需要合并callspec.funcargs
。
-
计算 valtype_keys:
valtype_keys = params.keys() - used_keys
: 找出params
中尚未使用的关键字参数键。
-
遍历 valtype_keys:
- 遍历
valtype_keys
中的每个参数名arg
。
- 遍历
-
提取 fixtures 信息:
fixturenames_closure, arg2fixturedefs = _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, value)
: 对于每个参数值value
,调用_get_fixturenames_closure_and_arg2fixturedefs
函数来获取fixtures
名称闭包和详细信息。
-
处理额外的 fixtures:
- 如果
fixturenames_closure
和arg2fixturedefs
都不为空,表示找到了额外的fixtures
:- 从
fixturenames_closure
中筛选出尚未在params
中的fixtures
名称。 - 创建
metafunc
的副本newmetafunc
。 - 更新
newmetafunc
的fixturenames
和_arg2fixturedefs
。 - 设置
newmetafunc._calls
为当前的callspec
。 - 调用
fm.pytest_generate_tests(newmetafunc)
来生成新的测试。 - 递归调用
normalize_metafunc_calls
来进一步处理newmetafunc
。 - 返回
newmetafunc._calls
。
- 从
- 如果
-
更新 used_keys:
used_keys.add(arg)
: 将当前处理的参数名arg
添加到used_keys
集合中。
-
返回 callspec:
- 如果没有额外的
fixtures
需要处理,或者在处理过程中没有找到新的fixtures
,返回原始的callspec
列表。
- 如果没有额外的
normalize_metafunc_calls方法
normalize_metafunc_calls
函数用于处理 pytest
的元数据函数(metafunc
)和调用规范(callspec
)。
代码逻辑如下:
-
函数的目的是规范化
metafunc
中的调用规范,特别是处理延迟加载的fixtures
。 -
它通过
_normalize_call
函数来处理每个callspec
,这可能会生成新的调用规范,特别是当参数值包含延迟加载的fixtures
时。 -
处理后的调用规范被收集到
newcalls
列表中,并最终替换metafunc
的原始_calls
列表。
详细解释如下:
-
函数定义:
def normalize_metafunc_calls(metafunc, used_keys=None)
: 定义了函数,接受两个参数:metafunc
: 一个pytest
测试函数的元数据对象,包含有关测试函数的信息,如参数和fixtures
。used_keys
: 一个可选参数,表示已经使用的关键字参数的集合。如果没有提供,它将被初始化为None
。
-
初始化新调用列表:
newcalls = []
: 创建一个新的空列表newcalls
,用于存储处理后的调用规范。
-
遍历原始调用规范:
for callspec in metafunc._calls
: 遍历metafunc
中的_calls
属性,这是一个包含所有调用规范的列表。
-
处理每个调用规范:
calls = _normalize_call(callspec, metafunc, used_keys)
: 对每个callspec
调用_normalize_call
函数,传入当前的callspec
、metafunc
和used_keys
。这将处理延迟加载的 fixtures 并生成新的调用规范。
-
扩展新调用列表:
newcalls.extend(calls)
: 将_normalize_call
函数返回的新调用规范列表扩展到newcalls
列表中。
-
更新元数据函数的调用规范:
metafunc._calls = newcalls
: 用处理后的newcalls
列表更新metafunc
的_calls
属性。
normalizer.py文件
import copy
from typing import Any, Dict, List, Tuple
import pytest
from .lazy_fixture import LazyFixtureWrapper
from .lazy_fixture_callable import LazyFixtureCallableWrapper
def _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, value) -> Tuple[List[str], Dict[str, Any]]:
if isinstance(value, LazyFixtureCallableWrapper):
extra_fixturenames_args, arg2fixturedefs_args = _get_fixturenames_closure_and_arg2fixturedefs(
fm,
metafunc,
value.args,
)
extra_fixturenames_kwargs, arg2fixturedefs_kwargs = _get_fixturenames_closure_and_arg2fixturedefs(
fm,
metafunc,
value.kwargs,
)
return [*extra_fixturenames_args, *extra_fixturenames_kwargs], {
**arg2fixturedefs_args,
**arg2fixturedefs_kwargs,
}
if isinstance(value, LazyFixtureWrapper):
if pytest.version_tuple >= (8, 0, 0):
fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure(metafunc.definition.parent, [value.name], {})
else: # pragma: no cover
# TODO: add tox
_, fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure([value.name], metafunc.definition.parent)
return fixturenames_closure, arg2fixturedefs
extra_fixturenames, arg2fixturedefs = [], {}
# we need to check exact type
if type(value) is dict: # noqa: E721
value = list(value.values())
# we need to check exact type
if type(value) in {list, tuple, set}:
for val in value:
ef, arg2f = _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, val)
extra_fixturenames.extend(ef)
arg2fixturedefs.update(arg2f)
return extra_fixturenames, arg2fixturedefs
def normalize_metafunc_calls(metafunc, used_keys=None):
newcalls = []
for callspec in metafunc._calls:
calls = _normalize_call(callspec, metafunc, used_keys)
newcalls.extend(calls)
metafunc._calls = newcalls
def _copy_metafunc(metafunc):
copied = copy.copy(metafunc)
copied.fixturenames = copy.copy(metafunc.fixturenames)
copied._calls = []
copied._arg2fixturedefs = copy.copy(metafunc._arg2fixturedefs)
return copied
def _normalize_call(callspec, metafunc, used_keys):
fm = metafunc.config.pluginmanager.get_plugin("funcmanage")
used_keys = used_keys or set()
params = callspec.params.copy() if pytest.version_tuple >= (8, 0, 0) else {**callspec.params, **callspec.funcargs}
valtype_keys = params.keys() - used_keys
for arg in valtype_keys:
value = params[arg]
fixturenames_closure, arg2fixturedefs = _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, value)
if fixturenames_closure and arg2fixturedefs:
extra_fixturenames = [fname for fname in fixturenames_closure if fname not in params]
newmetafunc = _copy_metafunc(metafunc)
newmetafunc.fixturenames = extra_fixturenames
newmetafunc._arg2fixturedefs.update(arg2fixturedefs)
newmetafunc._calls = [callspec]
fm.pytest_generate_tests(newmetafunc)
normalize_metafunc_calls(newmetafunc, used_keys | {arg})
return newmetafunc._calls
used_keys.add(arg)
return [callspec]
对代解析如下:
导入依赖
-
import copy
:导入Python
标准库中的copy
模块,用于复制对象。 -
from typing import Any, Dict, List, Tuple
:导入类型注解,用于函数返回类型和参数类型。 -
import pytest
:导入pytest
模块,用于与pytest
框架集成。 -
from .lazy_fixture import LazyFixtureWrapper
和from .lazy_fixture_callable import LazyFixtureCallableWrapper
:从同一包中导入LazyFixtureWrapper
和LazyFixtureCallableWrapper
类。
_get_fixturenames_closure_and_arg2fixturedefs 函数
-
功能:获取与延迟加载的
fixture
相关的fixtures
名称闭包和fixtures
定义。 -
参数:
fm
:pytest
的funcmanage
插件实例。metafunc
:代表当前测试函数的元数据对象。value
:可能是延迟加载的fixture
或其他值。
-
返回值:一个元组,包含
fixtures
名称的列表和fixtures
定义的字典。
normalize_metafunc_calls 函数
-
功能:规范化
metafunc
中的调用规范,处理延迟加载的fixtures
。 -
参数:
metafunc
:pytest
测试函数的元数据对象。used_keys
:一个可选参数,表示已经使用的关键字参数的集合。
-
逻辑:遍历
metafunc._calls
中的每个callspec
,调用_normalize_call
函数处理,并更新metafunc._calls
。
_copy_metafunc 函数
-
功能:复制
metafunc
对象,创建一个新的元数据对象。 -
参数:
metafunc
。 -
返回值:一个新的
metafunc
副本。
_normalize_call 函数
-
功能:处理单个调用规范,提取延迟加载的
fixtures
并生成新的调用规范。 -
参数:
callspec
:当前的调用规范。metafunc
:pytest
测试函数的元数据对象。used_keys
:表示已经使用的关键字参数的集合。
-
逻辑:
- 获取
funcmanage
插件实例。 - 复制
params
字典,并计算未使用的关键字参数键(valtype_keys
)。 - 遍历
valtype_keys
,对于每个参数值value
,调用_get_fixturenames_closure_and_arg2fixturedefs
函数获取fixtures
名称闭包和定义。 - 如果获取到额外的
fixtures
名称和定义,创建metafunc
的副本,更新其fixturenames
和_arg2fixturedefs
,生成新的调用规范并返回。 - 如果没有额外的
fixtures
,将当前的callspec
添加到used_keys
并返回。
- 获取
代码逻辑总结
-
这段代码通过递归和类型检查,确保所有可能包含延迟加载
fixtures
的参数都被检查并处理。 -
_get_fixturenames_closure_and_arg2fixturedefs
函数是核心,它负责提取fixtures
信息。 -
normalize_metafunc_calls
和_normalize_call
函数处理调用规范,生成新的测试用例。 -
_copy_metafunc
函数用于创建metafunc
的副本,以便在生成新测试时保持原始metafunc
不变。
plugin.py文件
import pytest
from .lazy_fixture import LazyFixtureWrapper
from .loader import load_lazy_fixtures
from .normalizer import normalize_metafunc_calls
@pytest.hookimpl(tryfirst=True)
def pytest_fixture_setup(fixturedef, request):
val = getattr(request, "param", None)
if val is not None:
request.param = load_lazy_fixtures(val, request)
def pytest_make_parametrize_id(config, val, argname):
if isinstance(val, LazyFixtureWrapper):
return val.name
@pytest.hookimpl(hookwrapper=True)
def pytest_generate_tests(metafunc):
yield
normalize_metafunc_calls(metafunc)
代码详细解析:
导入依赖
-
import pytest
:导入pytest
模块,用于与pytest
框架集成。 -
from .lazy_fixture import LazyFixtureWrapper
:从同一包中导入LazyFixtureWrapper
类,用于包装延迟加载的fixture
。 -
from .loader import load_lazy_fixtures
:从loader
模块导入load_lazy_fixtures
函数,用于加载延迟加载的fixtures
。 -
from .normalizer import normalize_metafunc_calls
:从normalizer
模块导入normalize_metafunc_calls
函数,用于规范化metafunc
中的调用规范。
pytest_fixture_setup 钩子函数
-
@pytest.hookimpl(tryfirst=True)
:装饰器,将函数注册为pytest
的钩子,tryfirst=True
表示尽可能早地执行此钩子。 -
功能:在 fixture 设置阶段被调用,用于处理延迟加载的
fixtures
。 -
参数:
fixturedef
:fixture
的定义。request
:包含测试函数和fixture
请求的上下文。
-
逻辑:
- 获取
request
的param
属性,如果存在,则使用load_lazy_fixtures
函数加载延迟加载的fixtures
,并将结果重新赋值给request.param
。
- 获取
pytest_make_parametrize_id 钩子函数
-
功能:用于生成参数化测试的 ID。
-
参数:
config
:pytest
的配置对象。val
:参数值。argname
:参数名称。
-
逻辑:
- 如果
val
是LazyFixtureWrapper
的实例,返回其名称name
作为参数化测试的ID
。
- 如果
pytest_generate_tests 钩子函数
-
@pytest.hookimpl(hookwrapper=True)
:装饰器,将函数注册为pytest
的钩子,hookwrapper=True
表示此钩子会包装其他钩子的执行。 -
功能:在生成测试用例时被调用,用于处理延迟加载的
fixtures
。 -
参数:
metafunc
:代表当前测试函数的元数据对象。
-
逻辑:
- 使用
yield
挂起函数执行,等待其他钩子执行完毕后再继续。 - 调用
normalize_metafunc_calls
函数处理metafunc
,确保所有延迟加载的fixtures
被正确处理。
- 使用
代码逻辑总结
-
这段代码通过
pytest
的钩子机制,实现了对延迟加载fixtures
的支持。 -
pytest_fixture_setup
钩子在fixture
设置阶段加载延迟加载的fixtures
,并更新request.param
。 -
pytest_make_parametrize_id
钩子为参数化测试生成 ID,特别处理LazyFixtureWrapper
实例。 -
pytest_generate_tests
钩子在生成测试用例时规范化metafunc
,确保所有延迟加载的fixtures
被正确处理。
结语
pytest_lazy_fixtures
是一个 pytest
插件,它允许你以惰性方式加载 fixtures
,即直到真正需要它们时才进行加载。这可以减少测试的初始化时间,并降低资源消耗,特别是在处理复杂的或耗时的 fixture
时非常有用。
-
编写测试:在你的测试模块中,使用
lf
或lfc
函数来引用fixtures
,而不是直接使用pytest.fixture
装饰器。 -
使用 LazyFixtureWrapper:
lf
函数返回一个LazyFixtureWrapper
对象,它包装了fixture
的名称,并在运行时才加载它。 -
使用 LazyFixtureCallableWrapper:
lfc
函数允许你延迟调用fixtures
,并且可以传递参数和关键字参数。 -
运行测试:使用
pytest
运行测试,插件会自动处理延迟加载的fixtures
。 -
配置 pytest:如果需要,你可以在
conftest.py
或测试模块中配置插件,例如设置默认的fixtures
。
通过使用 pytest_lazy_fixtures
插件,你可以使测试套件更加高效,特别是在处理多个或复杂的 fixtures
时。它提供了一种灵活的方式来组织和执行测试,同时保持了测试的清晰性和可维护性。