Bonsai API

pytest_bonsai.resolve

Resolve the passed argument in the context of the current FixtureRequest.

  • When passed a value, it will be returned as is.
  • When passed a fixture, will evaulate the fixture for the current request.
  • When passed a function taking (non-default) arguments, will inspect the argument names, then (recursively) resolve fixtures witch matching names, and finally call the function with resolved arguments.

Note

This is the low level interface to the whole machinery.

Parameters:
Source code in pytest_bonsai/parametrize.py
def resolve(request, func_or_value):
    """
    :param request: The pytest's [FixtureRequest object](https://docs.pytest.org/en/7.1.x/reference/reference.html#request).
    :param func_or_value: The function or value to resolved.
    """
    if not isinstance(func_or_value, Callable):
        return func_or_value

    # parameters might refer to fixtures
    if getattr(func_or_value, "_pytestfixturefunction", None):
        return request.getfixturevalue(func_or_value.__name__)

    # or may be functions taking fixtures
    try:
        parameters = inspect.signature(func_or_value).parameters.values()
    except ValueError:
        # inspect.signature fails in some cases
        kwargs = {}
    else:
        kwargs = {
            parameter.name: request.getfixturevalue(parameter.name)
            for parameter in parameters
            if parameter.default == inspect.Parameter.empty
        }

    return func_or_value(**kwargs)

pytest_bonsai.parametrized_fixture

The argument-less variant: when applied to a function, creates a fixture which resolves its parameter via pytest_bonsai.resolve.

Note

All tests using such a fixture must be parametrized.

Warning

The decorated function will not be called, because the value always comes from the parameter.

pytest_bonsai.parametrized_fixture(dataclass)

Variant with an argument: when applied to a function, creates a fixture which expects the parameter to be a dictionary and wraps it in a specified dataclass, resolving all intermediate values via pytest_bonsai.resolve.

Parameters:
  • model_or_func

    A dataclass, or a function that takes a single parameter called request.

Source code in pytest_bonsai/parametrize.py
def parametrized_fixture(model_or_func):
    """
    :param model_or_func: A dataclass, or a function that takes a single parameter called `request`.
    """
    if inspect.isfunction(model_or_func):
        assert inspect.signature(model_or_func).parameters.keys() == {
            "request"
        }, f"{model_or_func.__name__} must take a single parameter called 'request'"

        @functools.wraps(model_or_func)
        def wrapper(*args, request, **kwargs):
            assert hasattr(request, "param"), f"Fixture {request.fixturename} must always be parametrized"
            return resolve(request, request.param)

        fixture = pytest.fixture(wrapper)
        fixture.parametrize = functools.partial(parametrize_arg, wrapper.__name__)
        fixture._indirect = True

        return fixture

    assert dataclasses.is_dataclass(model_or_func), f"{model_or_func.__name__} must be a dataclass"

    def make_param_model(request, **kwargs):
        for name, field in model_or_func.__dataclass_fields__.items():
            value = kwargs.get(name, ...)

            if value is Ellipsis and field.default_factory != dataclasses.MISSING:
                kwargs[name] = resolve(request, field.default_factory)

            if inspect.isfunction(value):
                kwargs[name] = resolve(request, value)

            if isinstance(kwargs.get(name), expand):
                raise NotADirectoryError("Using expand() in default_factory is not supported")

        return model_or_func(**kwargs)

    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, request, **kwargs):
            try:
                param = getattr(request, "param", {})

                if inspect.isfunction(param):
                    value = resolve(request, param)
                elif isinstance(param, Mapping):
                    # otherwise, make the parameter a dataclass
                    request.param = make_param_model(request, **param)
                    value = func(*args, request=request, **kwargs)
                else:
                    raise TypeError(
                        f"Invalid parameter type {type(param)} for fixture "
                        f"{func.__module__}.{func.__name__}, expected function or a mapping"
                    )

                logger.debug(
                    "%s.%s(request.param=%r) -> %r",
                    func.__module__,
                    func.__name__,
                    request.param,
                    value,
                )

                return value

            except TypeError as ex:
                raise TypeError(f"Cannot parametrize fixture {func.__name__}") from ex

        fixture = pytest.fixture(wrapper)
        fixture.parametrize = functools.partial(parametrize_kwargs, wrapper.__name__)
        fixture._indirect = True

        return fixture

    return decorator