Example #1
0
def create_kwonly_decorator(
    sig_info,  # type: SignatureInfo
    decorator_function,
    disambiguator,
    function_for_metadata,
):
    """
    Utility method to create a decorator that has only keyword arguments and is implemented by `decorator_function`, in
    implementation-first mode or usage-first mode.

    When the decorator to create has a mandatory argument, it is exposed "as-is" because it is directly protected.

    Otherwise (if all arguments are optional and keyword-only), we modify the created decorator's signature to add a
    leading var-args, so that users will be able to call the decorator without parenthesis.
    When called it checks the length of the var-positional received:
     - 0 positional=called with parenthesis,
     - 1 and the positional argument is not a callable/class : called with parenthesis
     - 1 and the positional argument is a callable/class: disambiguation is required to know if this is without
     parenthesis or with positional arg
     - 2 positional=error).

    Note: we prefer to use this var-arg signature rather than a "(_=None)" signature, because it is more readable for
    the decorator's help.

    :param sig_info:
    :param decorator_function:
    :param function_for_metadata: an alternate function to use for the documentation and module metadata of the
        generated function
    :return:
    """
    if sig_info.is_first_arg_mandatory:
        # The first argument is mandatory AND keyword. So we do not need to change the signature to be fully protected
        # indeed python will automatically raise a `TypeError` when users will use this decorator without parenthesis
        # or with positional arguments.
        @with_signature(sig_info.exposed_signature,
                        func_name=function_for_metadata.__name__,
                        doc=function_for_metadata.__doc__,
                        modulename=function_for_metadata.__module__)
        def new_decorator(*no_args, **kwargs):
            # this is a parenthesis call, because otherwise a `TypeError` would already have been raised by python.
            return with_parenthesis_usage(decorator_function, *no_args,
                                          **kwargs)

        return new_decorator
    elif sig_info.use_signature_trick:
        # no need to modify the signature, we will expose *args, **kwargs
        pass
    else:
        # modify the signature to add a var-positional first
        gen_varpos_param = Parameter(_GENERATED_VARPOS_NAME,
                                     kind=Parameter.VAR_POSITIONAL)
        sig_info.exposed_signature = add_signature_parameters(
            sig_info.exposed_signature, first=[gen_varpos_param])

    # we can fallback to the same case than varpositional
    return create_general_case_decorator(
        sig_info,
        decorator_function,
        disambiguator,
        function_for_metadata=function_for_metadata)
Example #2
0
        def my_decorator(decorator_arg1=None, decorator_arg2=False, func=DECORATED):

            # (1) capture the signature of the function to wrap ...
            func_sig = signature(func)
            # ... and modify it to add new optional parameters 'new_arg1' and 'new_arg2'.
            # (if they are optional that's where you provide their defaults)
            new_arg1 = Parameter('new_arg1', kind=Parameter.POSITIONAL_OR_KEYWORD, default=False)
            new_arg2 = Parameter('new_arg2', kind=Parameter.POSITIONAL_OR_KEYWORD, default=None)
            new_sig = add_signature_parameters(func_sig, last=[new_arg1, new_arg2])

            # (2) create a wrapper with the new signature
            @wraps(func, new_sig=new_sig)
            def func_wrapper(*args, **kwds):
                # Inside the wrapping function

                # Pop the extra args (they will always be there, no need to provide default)
                new_arg1 = kwds.pop('new_arg1')
                new_arg2 = kwds.pop('new_arg2')

                # Calling the wrapped function
                if new_arg1:
                    print("new_arg1 True branch; new_arg2 is {}".format(new_arg2))
                    return func(*args, **kwds)
                else:
                    print("new_arg1 False branch; new_arg2 is {}".format(new_arg2))
                    # do something with new_arg2
                    return func(*args, **kwds)

            def added_function():
                # Do Something 2
                print('added_function')

            func_wrapper.added_function = added_function
            return func_wrapper
Example #3
0
    def make_call(function):
        func_sig = signature(function)
        new_sig = func_sig
        if not func_sig.parameters.get('token_info', None):
            token_info_param = Parameter('token_info',
                                         kind=Parameter.KEYWORD_ONLY,
                                         default=None)
            new_sig = add_signature_parameters(func_sig,
                                               last=[token_info_param])

        @wraps(function, new_sig=new_sig)
        def wrapper(*args, **kwargs):
            token_info = kwargs.get('token_info', None)

            if not token_info:
                logger.error("Token info not received in validate_jwt_claims!")
                return unauthorize()

            for claim_name, claim_value in claims.items():
                if token_info.get(claim_name, None) != claim_value:
                    logger.error(f"Token claim {claim_name} wih value "
                                 f"{claim_value} doesn't match expected "
                                 f"value!")
                    return unauthorize()

            logger.info(
                "Validated claims on call to %s with token %s and claims: %s",
                function, token_info, kwargs)
            if not add_token_info:
                kwargs.pop("token_info")
            return function(*args, **kwargs)

        return wrapper
def add_fixture_params(func, new_names):
    """Creates a wrapper of the given function with additional arguments"""

    old_sig = signature(func)

    # prepend all new parameters if needed
    for n in new_names:
        if n in old_sig.parameters:
            raise ValueError("argument named %s already present in signature" % n)
    new_sig = add_signature_parameters(old_sig,
                                       first=[Parameter(n, kind=Parameter.POSITIONAL_OR_KEYWORD) for n in new_names])

    assert not isgeneratorfunction(func)

    # normal function with return statement
    @wraps(func, new_sig=new_sig)
    def wrapped_func(**kwargs):
        for n in new_names:
            kwargs.pop(n)
        return func(**kwargs)

    # else:
    #     # generator function (with a yield statement)
    #     @wraps(fixture_func, new_sig=new_sig)
    #     def wrapped_fixture_func(*args, **kwargs):
    #         request = kwargs['request'] if func_needs_request else kwargs.pop('request')
    #         if is_used_request(request):
    #             for res in fixture_func(*args, **kwargs):
    #                 yield res
    #         else:
    #             yield NOT_USED

    return wrapped_func
Example #5
0
    def _decorator(f):
        s_name = name if name is not None else f.__name__
        for pyv, _param in product(all_python, all_params):
            if (pyv, _param) not in envs:
                # create a dummy folder to avoid creating a useless venv ?
                env_dir = Path(".nox") / ("%s-%s-%s-%s" % (s_name, pyv.replace('.', '-'), grid_param_name, _param))
                env_dir.mkdir(parents=True, exist_ok=True)

        # check the signature of f
        foo_sig = signature(f)
        missing = env_contents_names - set(foo_sig.parameters)
        if len(missing) > 0:
            raise ValueError("Session function %r does not contain environment parameter(s) %r" % (f.__name__, missing))

        # modify the exposed signature if needed
        new_sig = None
        if len(env_contents_names) > 0:
            new_sig = remove_signature_parameters(foo_sig, *env_contents_names)

        if has_parameter:
            if grid_param_name in foo_sig.parameters:
                raise ValueError("Internal error, this parameter has a reserved name: %r" % grid_param_name)
            else:
                new_sig = add_signature_parameters(new_sig, last=(grid_param_name,))

        @wraps(f, new_sig=new_sig)
        def _f_wrapper(**kwargs):
            # find the session arg
            session = kwargs['session']    # type: Session

            # get the versions to use for this environment
            try:
                if has_parameter:
                    grid_param = kwargs.pop(grid_param_name)
                    params_dct = envs[(session.python, grid_param)]
                else:
                    params_dct = envs[session.python]
            except KeyError:
                # Skip this session, it is a dummy one
                nox_logger.warning(
                    "Skipping configuration, this is not supported in python version %r" % session.python)
                return

            # inject the parameters in the args:
            kwargs.update(params_dct)

            # finally execute the session
            return f(**kwargs)

        if has_parameter:
            _f_wrapper = nox.parametrize(grid_param_name, all_params)(_f_wrapper)

        _f_wrapper = nox.session(python=all_python, reuse_venv=reuse_venv, name=name,
                                 venv_backend=venv_backend, venv_params=venv_params)(_f_wrapper)
        return _f_wrapper
def _create_fixture_without_marks(fixture_func, scope, autouse, **kwargs):
    """
    creates a fixture for decorated fixture function `fixture_func`.

    :param fixture_func:
    :param scope:
    :param autouse:
    :param kwargs:
    :return:
    """
    # IMPORTANT: even if 'params' is not in kwargs, the fixture
    # can be used in a fixture union and therefore a param will be received
    # on some calls (and the fixture will be called several times - only once for real)
    # - we have to handle the NOT_USED.

    # --create a wrapper where we will be able to auto-detect
    # TODO we could put this in a dedicated wrapper 'ignore_unsused'..

    old_sig = signature(fixture_func)
    # add request if needed
    func_needs_request = 'request' in old_sig.parameters
    if not func_needs_request:
        new_sig = add_signature_parameters(old_sig,
                                           first=Parameter('request', kind=Parameter.POSITIONAL_OR_KEYWORD))
    else:
        new_sig = old_sig
    if not isgeneratorfunction(fixture_func):
        # normal function with return statement
        @wraps(fixture_func, new_sig=new_sig)
        def wrapped_fixture_func(*args, **kwargs):
            request = kwargs['request'] if func_needs_request else kwargs.pop('request')
            if is_used_request(request):
                return fixture_func(*args, **kwargs)
            else:
                return NOT_USED

        # transform the created wrapper into a fixture
        fixture_decorator = pytest.fixture(scope=scope, autouse=autouse, **kwargs)
        return fixture_decorator(wrapped_fixture_func)

    else:
        # generator function (with a yield statement)
        @wraps(fixture_func, new_sig=new_sig)
        def wrapped_fixture_func(*args, **kwargs):
            request = kwargs['request'] if func_needs_request else kwargs.pop('request')
            if is_used_request(request):
                for res in fixture_func(*args, **kwargs):
                    yield res
            else:
                yield NOT_USED

        # transform the created wrapper into a fixture
        fixture_decorator = yield_fixture(scope=scope, autouse=autouse, **kwargs)
        return fixture_decorator(wrapped_fixture_func)
Example #7
0
def ignore_unused(fixture_func):
    """
    A decorator for fixture functions so that they are compliant with fixture unions.
    It

     - adds the `request` fixture dependency to their signature if needed
     - filters the calls based on presence of the `NOT_USED` token in the request params.

    IMPORTANT: even if 'params' is not in kwargs, the fixture can be used in a fixture union and therefore a param
    *will* be received on some calls (and the fixture will be called several times - only once for real) - we have to
    handle the NOT_USED.

    :param fixture_func:
    :return:
    """
    old_sig = signature(fixture_func)

    # add request if needed
    func_needs_request = 'request' in old_sig.parameters
    if not func_needs_request:
        # Add it last so that `self` argument in class functions remains the first
        new_sig = add_signature_parameters(
            old_sig,
            last=Parameter('request', kind=Parameter.POSITIONAL_OR_KEYWORD))
    else:
        new_sig = old_sig

    if not isgeneratorfunction(fixture_func):
        # normal function with return statement
        @wraps(fixture_func, new_sig=new_sig)
        def wrapped_fixture_func(*args, **kwargs):
            request = kwargs['request'] if func_needs_request else kwargs.pop(
                'request')
            if is_used_request(request):
                return fixture_func(*args, **kwargs)
            else:
                return NOT_USED

    else:
        # generator function (with a yield statement)
        @wraps(fixture_func, new_sig=new_sig)
        def wrapped_fixture_func(*args, **kwargs):
            request = kwargs['request'] if func_needs_request else kwargs.pop(
                'request')
            if is_used_request(request):
                for res in fixture_func(*args, **kwargs):
                    yield res
            else:
                yield NOT_USED

    return wrapped_fixture_func
Example #8
0
def test_helper_functions():
    """ Tests that the signature modification helpers work """
    def foo(b, c, a=0):
        pass

    # original signature
    foo_sig = signature(foo)
    print("original signature: %s" % foo_sig)

    # let's modify it
    new_sig = add_signature_parameters(foo_sig,
                                       first=Parameter('z', kind=Parameter.POSITIONAL_OR_KEYWORD),
                                       last=Parameter('o', kind=Parameter.POSITIONAL_OR_KEYWORD,
                                                      default=True)
                                       )
    new_sig = remove_signature_parameters(new_sig, 'b', 'a')
    print("modified signature: %s" % new_sig)
    assert str(new_sig) == '(z, c, o=True)'
Example #9
0
def one_fixture_per_step_decorate(fixture_fun):
    """ Implementation of the @one_fixture_per_step decorator, for manual decoration"""
    def _check_scope(request):
        scope = get_scope(request)
        if scope != 'function':
            # session- or module-scope
            raise Exception(
                "The `@one_fixture_per_step` decorator is only useful for function-scope fixtures. `%s`"
                " seems to have scope='%s'. Consider removing `@one_fixture_per_step` or changing "
                "the scope to 'function'." % (fixture_fun, scope))

    # We will expose a new signature with additional arguments
    orig_sig = signature(fixture_fun)
    func_needs_request = 'request' in orig_sig.parameters
    if not func_needs_request:
        new_sig = add_signature_parameters(
            orig_sig,
            first=Parameter('request', kind=Parameter.POSITIONAL_OR_KEYWORD))
    else:
        new_sig = orig_sig

    if not isgeneratorfunction(fixture_fun):

        @wraps(fixture_fun, new_sig=new_sig)
        def _steps_aware_decorated_function(*args, **kwargs):
            request = kwargs['request'] if func_needs_request else kwargs.pop(
                'request')
            _check_scope(request)
            res = fixture_fun(*args, **kwargs)
            return _OnePerStepFixtureProxy(res)
    else:

        @wraps(fixture_fun, new_sig=new_sig)
        def _steps_aware_decorated_function(*args, **kwargs):
            request = kwargs['request'] if func_needs_request else kwargs.pop(
                'request')
            _check_scope(request)
            gen = fixture_fun(*args, **kwargs)
            res = next(gen)
            yield _OnePerStepFixtureProxy(res)
            next(gen)

    return _steps_aware_decorated_function
Example #10
0
def cross_steps_fixture_decorate(fixture_fun, step_param_names=None):
    """
    Implementation of the @cross_steps_fixture decorator, for manual decoration

    :param fixture_fun:
    :param step_param_names: a singleton or iterable containing the names of the test step parameters used in the
        tests. By default the list is `[GENERATOR_MODE_STEP_ARGNAME, TEST_STEP_ARGNAME_DEFAULT]` to cover both
        generator-mode and legacy manual mode.
    :return:
    """
    ref_dct = dict()

    # Create the function wrapper.
    # We will expose a new signature with additional 'request' arguments if needed, and the test step
    orig_sig = signature(fixture_fun)
    func_needs_request = 'request' in orig_sig.parameters
    if not func_needs_request:
        new_sig = add_signature_parameters(
            orig_sig,
            first=Parameter('request', kind=Parameter.POSITIONAL_OR_KEYWORD))
    else:
        new_sig = orig_sig

    def _init_and_check(request):
        """
        Checks that the current request is not session but a specific node.
        :param request:
        :return:
        """
        scope = get_scope(request)
        if scope == 'function':
            # function-scope: ok
            id_without_steps = get_pytest_node_hash_id(
                request.node,
                params_to_ignore=_get_step_param_names_or_default(
                    step_param_names))
            return id_without_steps
        else:
            # session- or module-scope
            raise Exception(
                "The `@cross_steps_fixture` decorator is only useful for function-scope fixtures. `%s`"
                " seems to have scope='%s'. Consider removing `@cross_steps_fixture` or changing "
                "the scope to 'function'." % (fixture_fun, scope))

    if not isgeneratorfunction(fixture_fun):

        @wraps(fixture_fun, new_sig=new_sig)
        def _steps_aware_decorated_function(*args, **kwargs):
            request = kwargs['request'] if func_needs_request else kwargs.pop(
                'request')
            id_without_steps = _init_and_check(request)
            try:
                # already available: this is a subsequent step.
                return ref_dct[id_without_steps]
            except KeyError:
                # not yet cached, this is probably the first step
                res = fixture_fun(*args, **kwargs)
                ref_dct[id_without_steps] = res
                return res
    else:

        @wraps(fixture_fun, new_sig=new_sig)
        def _steps_aware_decorated_function(*args, **kwargs):
            request = kwargs['request'] if func_needs_request else kwargs.pop(
                'request')
            id_without_steps = _init_and_check(request)
            try:
                # already available: this is a subsequent step.
                yield ref_dct[id_without_steps]
            except KeyError:
                # not yet cached, this is probably the first step
                gen = fixture_fun(*args, **kwargs)
                res = next(gen)
                ref_dct[id_without_steps] = res
                yield res
                # TODO this teardown hook should actually be executed after all steps...
                next(gen)

    # Tag the function as being "cross-step" for future usage
    setattr(_steps_aware_decorated_function, CROSS_STEPS_MARK, True)
    return _steps_aware_decorated_function
Example #11
0
    def steps_decorator(test_func):
        """
        The generated test function decorator.

        It is equivalent to @mark.parametrize('case_data', cases) where cases is a tuple containing a CaseDataGetter for
        all case generator functions

        :param test_func:
        :return:
        """

        # Step ids
        step_ids = [create_pytest_param_str_id(f) for f in steps]

        # Depending on the presence of steps_data_holder_name in signature, create a cached fixture for steps data
        s = signature(test_func)
        if steps_data_holder_name in s.parameters:
            # the user wishes to share results across test steps. Create a cached fixture
            @lru_cache(maxsize=None)
            def get_results_holder(**kwargs):
                """
                A factory for the StepsDataHolder objects. Since it uses @lru_cache, the same StepsDataHolder will be
                returned when the keyword arguments are the same.

                :param kwargs:
                :return:
                """
                return StepsDataHolder(
                )  # TODO use Munch or MaxiMunch from `mixture` project, when publicly available?

            def results(request):
                """
                The fixture for the StepsDataHolder.

                It is function-scoped (so oit is called for each step of each param combination)
                but it implements an intelligent cache so that the same StepsDataHolder object is returned across all
                test steps belonging to the same param combination.

                :param request:
                :return:
                """
                # Get a good unique identifier of the test.
                # The id should be different everytime anything changes, except when the test step changes
                # Note: when the id was using not only param values but also fixture values we had to discard
                # steps_data_holder_name and 'request'. But that's not the case anymore,simply discard "test step" param
                test_id = get_pytest_node_hash_id(
                    request.node, params_to_ignore={test_step_argname})

                # Get or create the cached Result holder for this combination of parameters
                return get_results_holder(id=test_id)

            # Create a fixture with custom name : this seems to work also for old pytest versions
            results.__name__ = steps_data_holder_name
            results = pytest.fixture(results)

            # Add the fixture dynamically: we have to add it to the function holder module as explained in
            # https://github.com/pytest-dev/pytest/issues/2424
            module = getmodule(test_func)
            if steps_data_holder_name not in dir(module):
                setattr(module, steps_data_holder_name, results)
            else:
                raise ValueError(
                    "The {} fixture already exists in module {}: please specify a different "
                    "`steps_data_holder_name` in `@test_steps`".format(
                        steps_data_holder_name, module))

        # Parametrize the function with the test steps
        parametrizer = pytest.mark.parametrize(test_step_argname,
                                               steps,
                                               ids=step_ids)

        # We will expose a new signature with additional 'request' arguments if needed
        orig_sig = signature(test_func)
        func_needs_request = 'request' in orig_sig.parameters
        if not func_needs_request:
            # add request parameter last, as first may be 'self'
            new_sig = add_signature_parameters(
                orig_sig,
                last=Parameter('request',
                               kind=Parameter.POSITIONAL_OR_KEYWORD))
        else:
            new_sig = orig_sig

        # Finally, if there are some steps that are marked as having a dependency,
        use_dependency = any(hasattr(step, DEPENDS_ON_FIELD) for step in steps)
        if not use_dependency:
            # no dependencies: no need to do complex things
            # Create a light function wrapper that will allow for manual execution
            @wraps(test_func, new_sig=new_sig)
            def wrapped_test_function(*args, **kwargs):
                request = kwargs[
                    'request'] if func_needs_request else kwargs.pop('request')
                if request is None:
                    # manual call (maybe for pre-loading?), ability to execute several steps
                    _execute_manually(test_func, s, test_step_argname,
                                      step_ids, steps, args, kwargs)
                else:
                    return test_func(*args, **kwargs)
        else:
            # Create a test function wrapper that will replace the test steps with monitored ones before injecting them
            @wraps(test_func, new_sig=new_sig)
            def wrapped_test_function(*args, **kwargs):
                """Executes the current step only if its dependencies are correct, and registers its execution result"""
                request = kwargs[
                    'request'] if func_needs_request else kwargs.pop('request')
                if request is None:
                    # manual call (maybe for pre-loading?), no dependency management, ability to execute several steps
                    _execute_manually(test_func, s, test_step_argname,
                                      step_ids, steps, args, kwargs)
                else:
                    # (a) retrieve the "current step" function
                    current_step_fun = get_fixture_or_param_value(
                        request, test_step_argname)

                    # Get the unique id that is shared between the steps of the same execution
                    # Note: when the id was using not only param values but also fixture values we had to discard
                    # steps_data_holder_name and 'request'. But that's not the case anymore, simply discard "test step"
                    test_id_without_steps = get_pytest_node_hash_id(
                        request.node, params_to_ignore={test_step_argname})

                    # Make sure that it has a field to store its execution success
                    if not hasattr(current_step_fun, STEP_SUCCESS_FIELD):
                        # this is a dict where the key is the `test_id_without_steps` and the value is a boolean
                        setattr(current_step_fun, STEP_SUCCESS_FIELD, dict())

                    # (b) skip or fail it if needed
                    dependencies, should_fail = getattr(
                        current_step_fun, DEPENDS_ON_FIELD, ([], False))
                    # -- check that dependencies have all run (execution order is correct)
                    if not all(
                            hasattr(step, STEP_SUCCESS_FIELD)
                            for step in dependencies):
                        raise ValueError(
                            "Test step {} depends on another step that has not yet been executed. In "
                            "current version the steps execution order is manual, make sure it is correct."
                            "".format(current_step_fun.__name__))
                    # -- check that dependencies all ran with success
                    deps_successess = {
                        step: getattr(step, STEP_SUCCESS_FIELD).get(
                            test_id_without_steps, False)
                        for step in dependencies
                    }
                    failed_deps = [
                        d.__name__ for d, res in deps_successess.items()
                        if res is False
                    ]
                    if not all(deps_successess.values()):
                        msg = "This test step depends on other steps, and the following have failed: %s" % failed_deps
                        if should_fail:
                            pytest.fail(msg)
                        else:
                            pytest.skip(msg)

                    # (c) execute the test function for this step
                    res = test_func(*args, **kwargs)

                    # (d) declare execution as a success
                    getattr(current_step_fun,
                            STEP_SUCCESS_FIELD)[test_id_without_steps] = True

                    return res

        # With this hack we will be ordered correctly by pytest https://github.com/pytest-dev/pytest/issues/4429
        wrapped_test_function.place_as = test_func

        # finally apply parametrizer
        wrapped_parametrized_test_function = parametrizer(
            wrapped_test_function)
        return wrapped_parametrized_test_function
Example #12
0
def pytest_fixture_plus(scope="function",
                        autouse=False,
                        name=None,
                        unpack_into=None,
                        fixture_func=DECORATED,
                        **kwargs):
    """ decorator to mark a fixture factory function.

    Identical to `@pytest.fixture` decorator, except that

     - it supports multi-parametrization with `@pytest.mark.parametrize` as requested in
       https://github.com/pytest-dev/pytest/issues/3960. As a consequence it does not support the `params` and `ids`
       arguments anymore.

     - it supports a new argument `unpack_into` where you can provide names for fixtures where to unpack this fixture
       into.

    :param scope: the scope for which this fixture is shared, one of
                "function" (default), "class", "module" or "session".
    :param autouse: if True, the fixture func is activated for all tests that
                can see it.  If False (the default) then an explicit
                reference is needed to activate the fixture.
    :param name: the name of the fixture. This defaults to the name of the
                decorated function. Note: If a fixture is used in the same module in
                which it is defined, the function name of the fixture will be
                shadowed by the function arg that requests the fixture; one way
                to resolve this is to name the decorated function
                ``fixture_<fixturename>`` and then use
                ``@pytest.fixture(name='<fixturename>')``.
    :param unpack_into: an optional iterable of names, or string containing coma-separated names, for additional
        fixtures to create to represent parts of this fixture. See `unpack_fixture` for details.
    :param kwargs: other keyword arguments for `@pytest.fixture`
    """
    if name is not None:
        # Compatibility for the 'name' argument
        if LooseVersion(pytest.__version__) >= LooseVersion('3.0.0'):
            # pytest version supports "name" keyword argument
            kwargs['name'] = name
        elif name is not None:
            # 'name' argument is not supported in this old version, use the __name__ trick.
            fixture_func.__name__ = name

    # if unpacking is requested, do it first
    if unpack_into is not None:
        # get the future fixture name if needed
        if name is None:
            name = fixture_func.__name__

        # get caller module to create the symbols
        caller_module = get_caller_module(frame_offset=2)
        _unpack_fixture(caller_module, unpack_into, name)

    # (1) Collect all @pytest.mark.parametrize markers (including those created by usage of @cases_data)
    parametrizer_marks = get_pytest_parametrize_marks(fixture_func)
    if len(parametrizer_marks) < 1:
        return _create_fixture_without_marks(fixture_func, scope, autouse,
                                             **kwargs)
    else:
        if 'params' in kwargs:
            raise ValueError(
                "With `pytest_fixture_plus` you cannot mix usage of the keyword argument `params` and of "
                "the pytest.mark.parametrize marks")

    # (2) create the huge "param" containing all params combined
    # --loop (use the same order to get it right)
    params_names_or_name_combinations = []
    params_values = []
    params_ids = []
    params_marks = []
    for pmark in parametrizer_marks:
        # check number of parameter names in this parameterset
        if len(pmark.param_names) < 1:
            raise ValueError(
                "Fixture function '%s' decorated with '@pytest_fixture_plus' has an empty parameter "
                "name in a @pytest.mark.parametrize mark")

        # remember
        params_names_or_name_combinations.append(pmark.param_names)

        # extract all parameters that have a specific configuration (pytest.param())
        _pids, _pmarks, _pvalues = extract_parameterset_info(
            pmark.param_names, pmark)

        # Create the proper id for each test
        if pmark.param_ids is not None:
            # overridden at global pytest.mark.parametrize level - this takes precedence.
            try:  # an explicit list of ids ?
                paramids = list(pmark.param_ids)
            except TypeError:  # a callable to apply on the values
                paramids = list(pmark.param_ids(v) for v in _pvalues)
        else:
            # default: values-based...
            paramids = get_test_ids_from_param_values(pmark.param_names,
                                                      _pvalues)
            # ...but local pytest.param takes precedence
            for i, _id in enumerate(_pids):
                if _id is not None:
                    paramids[i] = _id

        # Finally store the ids, marks, and values for this parameterset
        params_ids.append(paramids)
        params_marks.append(tuple(_pmarks))
        params_values.append(tuple(_pvalues))

    # (3) generate the ids and values, possibly reapplying marks
    if len(params_names_or_name_combinations) == 1:
        # we can simplify - that will be more readable
        final_ids = params_ids[0]
        final_marks = params_marks[0]
        final_values = list(params_values[0])

        # reapply the marks
        for i, marks in enumerate(final_marks):
            if marks is not None:
                final_values[i] = make_marked_parameter_value(final_values[i],
                                                              marks=marks)
    else:
        final_values = list(product(*params_values))
        final_ids = get_test_ids_from_param_values(
            params_names_or_name_combinations, product(*params_ids))
        final_marks = tuple(product(*params_marks))

        # reapply the marks
        for i, marks in enumerate(final_marks):
            ms = [m for mm in marks if mm is not None for m in mm]
            if len(ms) > 0:
                final_values[i] = make_marked_parameter_value(final_values[i],
                                                              marks=ms)

    if len(final_values) != len(final_ids):
        raise ValueError(
            "Internal error related to fixture parametrization- please report")

    # (4) wrap the fixture function so as to remove the parameter names and add 'request' if needed
    all_param_names = tuple(v for l in params_names_or_name_combinations
                            for v in l)

    # --create the new signature that we want to expose to pytest
    old_sig = signature(fixture_func)
    for p in all_param_names:
        if p not in old_sig.parameters:
            raise ValueError(
                "parameter '%s' not found in fixture signature '%s%s'"
                "" % (p, fixture_func.__name__, old_sig))
    new_sig = remove_signature_parameters(old_sig, *all_param_names)
    # add request if needed
    func_needs_request = 'request' in old_sig.parameters
    if not func_needs_request:
        new_sig = add_signature_parameters(
            new_sig,
            first=Parameter('request', kind=Parameter.POSITIONAL_OR_KEYWORD))

    # --common routine used below. Fills kwargs with the appropriate names and values from fixture_params
    def _get_arguments(*args, **kwargs):
        request = kwargs['request'] if func_needs_request else kwargs.pop(
            'request')

        # populate the parameters
        if len(params_names_or_name_combinations) == 1:
            _params = [request.param]  # remove the simplification
        else:
            _params = request.param
        for p_names, fixture_param_value in zip(
                params_names_or_name_combinations, _params):
            if len(p_names) == 1:
                # a single parameter for that generated fixture (@pytest.mark.parametrize with a single name)
                kwargs[p_names[0]] = fixture_param_value
            else:
                # several parameters for that generated fixture (@pytest.mark.parametrize with several names)
                # unpack all of them and inject them in the kwargs
                for old_p_name, old_p_value in zip(p_names,
                                                   fixture_param_value):
                    kwargs[old_p_name] = old_p_value

        return args, kwargs

    # --Finally create the fixture function, a wrapper of user-provided fixture with the new signature
    if not isgeneratorfunction(fixture_func):
        # normal function with return statement
        @wraps(fixture_func, new_sig=new_sig)
        def wrapped_fixture_func(*args, **kwargs):
            if not is_used_request(kwargs['request']):
                return NOT_USED
            else:
                args, kwargs = _get_arguments(*args, **kwargs)
                return fixture_func(*args, **kwargs)

        # transform the created wrapper into a fixture
        fixture_decorator = pytest.fixture(scope=scope,
                                           params=final_values,
                                           autouse=autouse,
                                           ids=final_ids,
                                           **kwargs)
        return fixture_decorator(wrapped_fixture_func)

    else:
        # generator function (with a yield statement)
        @wraps(fixture_func, new_sig=new_sig)
        def wrapped_fixture_func(*args, **kwargs):
            if not is_used_request(kwargs['request']):
                yield NOT_USED
            else:
                args, kwargs = _get_arguments(*args, **kwargs)
                for res in fixture_func(*args, **kwargs):
                    yield res

        # transform the created wrapper into a fixture
        fixture_decorator = yield_fixture(scope=scope,
                                          params=final_values,
                                          autouse=autouse,
                                          ids=final_ids,
                                          **kwargs)
        return fixture_decorator(wrapped_fixture_func)
Example #13
0
        def parametrize_plus_decorate(test_func):
            """
            A decorator that wraps the test function so that instead of receiving the parameter names, it receives the
            new fixture. All other decorations are unchanged.

            :param test_func:
            :return:
            """
            # first check if the test function has the parameters as arguments
            old_sig = signature(test_func)
            for p in all_param_names:
                if p not in old_sig.parameters:
                    raise ValueError(
                        "parameter '%s' not found in test function signature '%s%s'"
                        "" % (p, test_func.__name__, old_sig))

            # The base name for all fixtures that will be created below
            # style_template = "%s_param__%s"
            style_template = "%s_%s"
            base_name = style_template % (test_func.__name__,
                                          argnames.replace(' ', '').replace(
                                              ',', '_'))
            base_name = check_name_available(caller_module,
                                             base_name,
                                             if_name_exists=CHANGE,
                                             caller=pytest_parametrize_plus)

            # Retrieve (if ref) or create (for normal argvalues) the fixtures that we will union
            # TODO important note: we could either wish to create one fixture for parameter value or to create one for
            #  each consecutive group as shown below. This should not lead to different results but perf might differ.
            #  maybe add a parameter in the signature so that users can test it ?
            fixtures_to_union = []
            fixtures_to_union_names_for_ids = []
            prev_i = -1
            for i in fixture_indices:
                if i > prev_i + 1:
                    param_fix = create_param_fixture(prev_i + 1, i, base_name)
                    fixtures_to_union.append(param_fix)
                    fixtures_to_union_names_for_ids.append(
                        get_fixture_name(param_fix))

                fixtures_to_union.append(argvalues[i].fixture)
                id_for_fixture = apply_id_style(
                    get_fixture_name(argvalues[i].fixture), base_name,
                    IdStyle.explicit)
                fixtures_to_union_names_for_ids.append(id_for_fixture)
                prev_i = i

            # last bit if any
            i = len(argvalues)
            if i > prev_i + 1:
                param_fix = create_param_fixture(prev_i + 1, i, base_name)
                fixtures_to_union.append(param_fix)
                fixtures_to_union_names_for_ids.append(
                    get_fixture_name(param_fix))

            # Finally create a "main" fixture with a unique name for this test function
            # note: the function automatically registers it in the module
            # note 2: idstyle is set to None because we provide an explicit enough list of ids
            big_param_fixture = _fixture_union(
                caller_module,
                base_name,
                fixtures_to_union,
                idstyle=None,
                ids=fixtures_to_union_names_for_ids)

            # --create the new test function's signature that we want to expose to pytest
            # it is the same than existing, except that we want to replace all parameters with the new fixture

            new_sig = remove_signature_parameters(old_sig, *all_param_names)
            new_sig = add_signature_parameters(
                new_sig,
                Parameter(base_name, kind=Parameter.POSITIONAL_OR_KEYWORD))

            # --Finally create the fixture function, a wrapper of user-provided fixture with the new signature
            def replace_paramfixture_with_values(kwargs):
                # remove the created fixture value
                encompassing_fixture = kwargs.pop(base_name)
                # and add instead the parameter values
                if len(all_param_names) > 1:
                    for i, p in enumerate(all_param_names):
                        kwargs[p] = encompassing_fixture[i]
                else:
                    kwargs[all_param_names[0]] = encompassing_fixture
                # return
                return kwargs

            if not isgeneratorfunction(test_func):
                # normal test function with return statement
                @wraps(test_func, new_sig=new_sig)
                def wrapped_test_func(*args, **kwargs):
                    if kwargs.get(base_name, None) is NOT_USED:
                        return NOT_USED
                    else:
                        replace_paramfixture_with_values(kwargs)
                        return test_func(*args, **kwargs)

            else:
                # generator test function (with one or several yield statement)
                @wraps(test_func, new_sig=new_sig)
                def wrapped_test_func(*args, **kwargs):
                    if kwargs.get(base_name, None) is NOT_USED:
                        yield NOT_USED
                    else:
                        replace_paramfixture_with_values(kwargs)
                        for res in test_func(*args, **kwargs):
                            yield res

            # move all pytest marks from the test function to the wrapper
            # not needed because the __dict__ is automatically copied when we use @wraps
            #   move_all_pytest_marks(test_func, wrapped_test_func)

            # With this hack we will be ordered correctly by pytest https://github.com/pytest-dev/pytest/issues/4429
            wrapped_test_func.place_as = test_func

            # return the new test function
            return wrapped_test_func
Example #14
0
    def steps_decorator(test_func):
        """
        The test function decorator. When a function is decorated it
         - checks that the function is a generator
         - checks that the function signature does not contain our private name `GENERATOR_MODE_STEP_ARGNAME` "by chance"
         - wraps the function
        :param test_func:
        :return:
        """

        # ------VALIDATION -------
        # Check if function is a generator
        if not isgeneratorfunction(test_func):
            raise ValueError(
                "Decorated function is not a generator. You either forgot to add `yield` statements in "
                "its body, or you are using mode='generator' instead of mode='parametrizer' or 'auto'."
                "See help(pytest_steps) for details")

        # check that our name for the additional 'test step' parameter is valid (it does not exist in f signature)
        f_sig = signature(test_func)
        test_step_argname = GENERATOR_MODE_STEP_ARGNAME
        if test_step_argname in f_sig.parameters:
            raise ValueError(
                "Your test function relies on arg name %s that is needed by @test_steps in generator "
                "mode" % test_step_argname)

        # ------CORE -------
        # Transform the steps into ids if needed
        step_ids = [create_pytest_param_str_id(f) for f in steps]

        # Create the container that will hold all execution monitors for this function
        # TODO maybe have later a single 'monitor' instance at plugin level... like in pytest-benchmark
        all_monitors = StepMonitorsContainer(test_func, step_ids)

        # Create the function wrapper.
        # We will expose a new signature with additional 'request' arguments if needed, and the test step
        orig_sig = signature(test_func)
        func_needs_request = 'request' in orig_sig.parameters
        additional_params = (Parameter(test_step_argname, kind=Parameter.POSITIONAL_OR_KEYWORD), ) \
                            + ((Parameter('request', kind=Parameter.POSITIONAL_OR_KEYWORD), )
                               if not func_needs_request else ())
        # add request parameter last, as first may be 'self'
        new_sig = add_signature_parameters(orig_sig, last=additional_params)

        # -- first create the logic
        @wraps(test_func, new_sig=new_sig)
        def wrapped_test_function(*args, **kwargs):
            step_name = kwargs.pop(test_step_argname)
            request = kwargs['request'] if func_needs_request else kwargs.pop(
                'request')
            if request is None:
                # we are manually called outside of pytest. let's execute all steps at nce
                if step_name is None:
                    # print("@test_steps - decorated function '%s' is being called manually. The `%s` parameter is set "
                    #       "to None so all steps will be executed in order" % (f, test_step_argname))
                    step_names = step_ids
                else:
                    # print("@test_steps - decorated function '%s' is being called manually. The `%s` parameter is set "
                    #       "to %s so only these steps will be executed in order. Note that the order should be feasible"
                    #       "" % (f, test_step_argname, step_name))
                    if not isinstance(step_name, (list, tuple)):
                        step_names = [create_pytest_param_str_id(step_name)]
                    else:
                        step_names = [
                            create_pytest_param_str_id(f) for f in step_name
                        ]
                steps_monitor = StepsMonitor(step_ids, test_func, args, kwargs)
                for i, (step_name,
                        ref_step_name) in enumerate(zip(step_names, step_ids)):
                    if step_name != ref_step_name:
                        raise ValueError(
                            "Incorrect sequence of steps provided for manual execution. Step #%s should"
                            " be named '%s', found '%s'" %
                            (i + 1, ref_step_name, step_name))
                    steps_monitor.execute(step_name, args, kwargs)
            else:
                # Retrieve or create the corresponding execution monitor
                steps_monitor = all_monitors.get_execution_monitor(
                    request.node, args, kwargs)

                # execute the step
                # print("DEBUG - executing step %s" % step_name)
                steps_monitor.execute(step_name, args, kwargs)

        # With this hack we will be ordered correctly by pytest https://github.com/pytest-dev/pytest/issues/4429
        wrapped_test_function.place_as = test_func

        # Parametrize the wrapper function with the test step ids
        parametrizer = pytest.mark.parametrize(test_step_argname,
                                               step_ids,
                                               ids=str)

        # finally apply parametrizer
        parametrized_step_function_wrapper = parametrizer(
            wrapped_test_function)
        return parametrized_step_function_wrapper
        def parametrize_plus_decorate(test_func):
            """
            A decorator that wraps the test function so that instead of receiving the parameter names, it receives the
            new fixture. All other decorations are unchanged.

            :param test_func:
            :return:
            """
            test_func_name = test_func.__name__

            # Are there explicit ids provided ?
            try:
                if len(ids) != len(argvalues):
                    raise ValueError("Explicit list of `ids` provided has a different length (%s) than the number of "
                                     "parameter sets (%s)" % (len(ids), len(argvalues)))
                explicit_ids_to_use = []
            except TypeError:
                explicit_ids_to_use = None

            # first check if the test function has the parameters as arguments
            old_sig = signature(test_func)
            for p in argnames:
                if p not in old_sig.parameters:
                    raise ValueError("parameter '%s' not found in test function signature '%s%s'"
                                     "" % (p, test_func_name, old_sig))

            # The name for the final "union" fixture
            # style_template = "%s_param__%s"
            main_fixture_style_template = "%s_%s"
            fixture_union_name = main_fixture_style_template % (test_func_name, param_names_str)
            fixture_union_name = check_name_available(caller_module, fixture_union_name, if_name_exists=CHANGE,
                                                      caller=parametrize_plus)

            # Retrieve (if ref) or create (for normal argvalues) the fixtures that we will union
            fixture_alternatives = []
            prev_i = -1
            for i, j_list in fixture_indices:  # noqa
                # A/ Is there any non-empty group of 'normal' parameters before the fixture_ref at <i> ? If so, handle.
                if i > prev_i + 1:
                    # create a new "param" fixture parametrized with all of that consecutive group.
                    # Important note: we could either wish to create one fixture for parameter value or to create
                    #  one for each consecutive group as shown below. This should not lead to different results but perf
                    #  might differ. Maybe add a parameter in the signature so that users can test it ?
                    #  this would make the ids more readable by removing the "P2toP3"-like ids
                    p_fix_name, p_fix_alt = _create_params_alt(test_func_name=test_func_name, hook=hook,
                                                               union_name=fixture_union_name, from_i=prev_i + 1, to_i=i)
                    fixture_alternatives.append((p_fix_name, p_fix_alt))
                    if explicit_ids_to_use is not None:
                        if isinstance(p_fix_alt, SingleParamAlternative):
                            explicit_ids_to_use.append(ids[prev_i + 1])
                        else:
                            # the ids provided by the user are propagated to the params of this fix, so we need an id
                            explicit_ids_to_use.append(ParamIdMakers.explicit(p_fix_alt))

                # B/ Now handle the fixture ref at position <i>
                if j_list is None:
                    # argvalues[i] contains a single argvalue that is a fixture_ref : add the referenced fixture
                    f_fix_name, f_fix_alt = _create_fixture_ref_alt(union_name=fixture_union_name, i=i)
                    fixture_alternatives.append((f_fix_name, f_fix_alt))
                    if explicit_ids_to_use is not None:
                        explicit_ids_to_use.append(ids[i])

                else:
                    # argvalues[i] is a tuple, some of them being fixture_ref. create a fixture refering to all of them
                    prod_fix_name, prod_fix_alt = _create_fixture_ref_product(union_name=fixture_union_name, i=i,
                                                                              fixture_ref_positions=j_list,
                                                                              test_func_name=test_func_name, hook=hook)
                    fixture_alternatives.append((prod_fix_name, prod_fix_alt))
                    if explicit_ids_to_use is not None:
                        explicit_ids_to_use.append(ids[i])

                prev_i = i

            # C/ handle last consecutive group of normal parameters, if any
            i = len(argvalues)  # noqa
            if i > prev_i + 1:
                p_fix_name, p_fix_alt = _create_params_alt(test_func_name=test_func_name, union_name=fixture_union_name,
                                                           from_i=prev_i + 1, to_i=i, hook=hook)
                fixture_alternatives.append((p_fix_name, p_fix_alt))
                if explicit_ids_to_use is not None:
                    if isinstance(p_fix_alt, SingleParamAlternative):
                        explicit_ids_to_use.append(ids[prev_i + 1])
                    else:
                        # the ids provided by the user are propagated to the params of this fix, so we need an id
                        explicit_ids_to_use.append(ParamIdMakers.explicit(p_fix_alt))

            # TO DO if fixtures_to_union has length 1, simplify ? >> No, we leave such "optimization" to the end user

            # consolidate the list of alternatives
            fix_alternatives = tuple(a[1] for a in fixture_alternatives)

            # and the list of their names. Duplicates should be removed here
            fix_alt_names = []
            for a, alt in fixture_alternatives:
                if a not in fix_alt_names:
                    fix_alt_names.append(a)
                else:
                    # this should only happen when the alternative is directly a fixture reference
                    assert isinstance(alt, FixtureParamAlternative), \
                        "Created fixture names are not unique, please report"

            # Finally create a "main" fixture with a unique name for this test function
            if debug:
                print("Creating final union fixture %r with alternatives %r" % (fixture_union_name, fix_alternatives))

            # note: the function automatically registers it in the module
            _make_fixture_union(caller_module, name=fixture_union_name, hook=hook, caller=parametrize_plus,
                                fix_alternatives=fix_alternatives, unique_fix_alt_names=fix_alt_names,
                                ids=explicit_ids_to_use or ids or ParamIdMakers.get(idstyle))

            # --create the new test function's signature that we want to expose to pytest
            # it is the same than existing, except that we want to replace all parameters with the new fixture
            # first check where we should insert the new parameters (where is the first param we remove)
            _first_idx = -1
            for _first_idx, _n in enumerate(old_sig.parameters):
                if _n in argnames:
                    break
            # then remove all parameters that will be replaced by the new fixture
            new_sig = remove_signature_parameters(old_sig, *argnames)
            # finally insert the new fixture in that position. Indeed we can not insert first or last, because
            # 'self' arg (case of test class methods) should stay first and exec order should be preserved when possible
            new_sig = add_signature_parameters(new_sig, custom_idx=_first_idx,
                                               custom=Parameter(fixture_union_name,
                                                                kind=Parameter.POSITIONAL_OR_KEYWORD))

            if debug:
                print("Creating final test function wrapper with signature %s%s" % (test_func_name, new_sig))

            # --Finally create the fixture function, a wrapper of user-provided fixture with the new signature
            def replace_paramfixture_with_values(kwargs):  # noqa
                # remove the created fixture value
                encompassing_fixture = kwargs.pop(fixture_union_name)
                # and add instead the parameter values
                if nb_params > 1:
                    for i, p in enumerate(argnames):  # noqa
                        kwargs[p] = encompassing_fixture[i]
                else:
                    kwargs[argnames[0]] = encompassing_fixture
                # return
                return kwargs

            if not isgeneratorfunction(test_func):
                # normal test function with return statement
                @wraps(test_func, new_sig=new_sig)
                def wrapped_test_func(*args, **kwargs):  # noqa
                    if kwargs.get(fixture_union_name, None) is NOT_USED:
                        # TODO why this ? it is probably useless: this fixture
                        #  is private and will never end up in another union
                        return NOT_USED
                    else:
                        replace_paramfixture_with_values(kwargs)
                        return test_func(*args, **kwargs)

            else:
                # generator test function (with one or several yield statements)
                @wraps(test_func, new_sig=new_sig)
                def wrapped_test_func(*args, **kwargs):  # noqa
                    if kwargs.get(fixture_union_name, None) is NOT_USED:
                        # TODO why this ? it is probably useless: this fixture
                        #  is private and will never end up in another union
                        yield NOT_USED
                    else:
                        replace_paramfixture_with_values(kwargs)
                        for res in test_func(*args, **kwargs):
                            yield res

            # move all pytest marks from the test function to the wrapper
            # not needed because the __dict__ is automatically copied when we use @wraps
            #   move_all_pytest_marks(test_func, wrapped_test_func)

            # With this hack we will be ordered correctly by pytest https://github.com/pytest-dev/pytest/issues/4429
            wrapped_test_func.place_as = test_func

            # return the new test function
            return wrapped_test_func
def _decorate_fixture_plus(
        fixture_func,
        scope="function",  # type: str
        autouse=False,  # type: bool
        name=None,  # type: str
        unpack_into=None,  # type: Iterable[str]
        hook=None,  # type: Callable[[Callable], Callable]
        _caller_module_offset_when_unpack=3,  # type: int
        **kwargs):
    """ decorator to mark a fixture factory function.

    Identical to `@pytest.fixture` decorator, except that

     - it supports multi-parametrization with `@pytest.mark.parametrize` as requested in
       https://github.com/pytest-dev/pytest/issues/3960. As a consequence it does not support the `params` and `ids`
       arguments anymore.

     - it supports a new argument `unpack_into` where you can provide names for fixtures where to unpack this fixture
       into.

    :param scope: the scope for which this fixture is shared, one of "function" (default), "class", "module" or
        "session".
    :param autouse: if True, the fixture func is activated for all tests that can see it.  If False (the default) then
        an explicit reference is needed to activate the fixture.
    :param name: the name of the fixture. This defaults to the name of the decorated function. Note: If a fixture is
        used in the same module in which it is defined, the function name of the fixture will be shadowed by the
        function arg that requests the fixture; one way to resolve this is to name the decorated function
        ``fixture_<fixturename>`` and then use ``@pytest.fixture(name='<fixturename>')``.
    :param unpack_into: an optional iterable of names, or string containing coma-separated names, for additional
        fixtures to create to represent parts of this fixture. See `unpack_fixture` for details.
    :param hook: an optional hook to apply to each fixture function that is created during this call. The hook function
        will be called everytime a fixture is about to be created. It will receive a single argument (the function
        implementing the fixture) and should return the function to use. For example you can use `saved_fixture` from
        `pytest-harvest` as a hook in order to save all such created fixtures in the fixture store.
    :param kwargs: other keyword arguments for `@pytest.fixture`
    """
    if name is not None:
        # Compatibility for the 'name' argument
        if LooseVersion(pytest.__version__) >= LooseVersion('3.0.0'):
            # pytest version supports "name" keyword argument
            kwargs['name'] = name
        elif name is not None:
            # 'name' argument is not supported in this old version, use the __name__ trick.
            fixture_func.__name__ = name

    # if unpacking is requested, do it first
    if unpack_into is not None:
        # get the future fixture name if needed
        if name is None:
            name = fixture_func.__name__

        # get caller module to create the symbols
        caller_module = get_caller_module(
            frame_offset=_caller_module_offset_when_unpack)
        _make_unpack_fixture(caller_module, unpack_into, name, hook=hook)

    # (1) Collect all @pytest.mark.parametrize markers (including those created by usage of @cases_data)
    parametrizer_marks = get_pytest_parametrize_marks(fixture_func)
    if len(parametrizer_marks) < 1:
        # make the fixture union-aware
        wrapped_fixture_func = ignore_unused(fixture_func)

        # transform the created wrapper into a fixture
        return pytest_fixture(scope=scope,
                              autouse=autouse,
                              hook=hook,
                              **kwargs)(wrapped_fixture_func)

    else:
        if 'params' in kwargs:
            raise ValueError(
                "With `fixture_plus` you cannot mix usage of the keyword argument `params` and of "
                "the pytest.mark.parametrize marks")

    # (2) create the huge "param" containing all params combined
    # --loop (use the same order to get it right)
    params_names_or_name_combinations = []
    params_values = []
    params_ids = []
    params_marks = []
    for pmark in parametrizer_marks:
        # -- pmark is a single @pytest.parametrize mark. --

        # check number of parameter names in this parameterset
        if len(pmark.param_names) < 1:
            raise ValueError(
                "Fixture function '%s' decorated with '@fixture_plus' has an empty parameter "
                "name in a @pytest.mark.parametrize mark")

        # remember the argnames
        params_names_or_name_combinations.append(pmark.param_names)

        # analyse contents, extract marks and custom ids, apply custom ids
        _paramids, _pmarks, _pvalues = analyze_parameter_set(pmark=pmark,
                                                             check_nb=True)

        # Finally store the ids, marks, and values for this parameterset
        params_ids.append(_paramids)
        params_marks.append(tuple(_pmarks))
        params_values.append(tuple(_pvalues))

    # (3) generate the ids and values, possibly reapplying marks
    if len(params_names_or_name_combinations) == 1:
        # we can simplify - that will be more readable
        final_ids = params_ids[0]
        final_marks = params_marks[0]
        final_values = list(params_values[0])

        # reapply the marks
        for i, marks in enumerate(final_marks):
            if marks is not None:
                final_values[i] = make_marked_parameter_value(
                    (final_values[i], ), marks=marks)
    else:
        final_values = list(product(*params_values))
        final_ids = combine_ids(product(*params_ids))
        final_marks = tuple(product(*params_marks))

        # reapply the marks
        for i, marks in enumerate(final_marks):
            ms = [m for mm in marks if mm is not None for m in mm]
            if len(ms) > 0:
                final_values[i] = make_marked_parameter_value(
                    (final_values[i], ), marks=ms)

    if len(final_values) != len(final_ids):
        raise ValueError(
            "Internal error related to fixture parametrization- please report")

    # (4) wrap the fixture function so as to remove the parameter names and add 'request' if needed
    all_param_names = tuple(v for pnames in params_names_or_name_combinations
                            for v in pnames)

    # --create the new signature that we want to expose to pytest
    old_sig = signature(fixture_func)
    for p in all_param_names:
        if p not in old_sig.parameters:
            raise ValueError(
                "parameter '%s' not found in fixture signature '%s%s'"
                "" % (p, fixture_func.__name__, old_sig))
    new_sig = remove_signature_parameters(old_sig, *all_param_names)
    # add request if needed
    func_needs_request = 'request' in old_sig.parameters
    if not func_needs_request:
        new_sig = add_signature_parameters(
            new_sig,
            first=Parameter('request', kind=Parameter.POSITIONAL_OR_KEYWORD))

    # --common routine used below. Fills kwargs with the appropriate names and values from fixture_params
    def _map_arguments(*_args, **_kwargs):
        request = _kwargs['request'] if func_needs_request else _kwargs.pop(
            'request')

        # populate the parameters
        if len(params_names_or_name_combinations) == 1:
            _params = [request.param]  # remove the simplification
        else:
            _params = request.param
        for p_names, fixture_param_value in zip(
                params_names_or_name_combinations, _params):
            if len(p_names) == 1:
                # a single parameter for that generated fixture (@pytest.mark.parametrize with a single name)
                _kwargs[p_names[0]] = get_lazy_args(fixture_param_value)
            else:
                # several parameters for that generated fixture (@pytest.mark.parametrize with several names)
                # unpack all of them and inject them in the kwargs
                for old_p_name, old_p_value in zip(p_names,
                                                   fixture_param_value):
                    _kwargs[old_p_name] = get_lazy_args(old_p_value)

        return _args, _kwargs

    # --Finally create the fixture function, a wrapper of user-provided fixture with the new signature
    if not isgeneratorfunction(fixture_func):
        # normal function with return statement
        @wraps(fixture_func, new_sig=new_sig)
        def wrapped_fixture_func(*_args, **_kwargs):
            if not is_used_request(_kwargs['request']):
                return NOT_USED
            else:
                _args, _kwargs = _map_arguments(*_args, **_kwargs)
                return fixture_func(*_args, **_kwargs)

    else:
        # generator function (with a yield statement)
        @wraps(fixture_func, new_sig=new_sig)
        def wrapped_fixture_func(*_args, **_kwargs):
            if not is_used_request(_kwargs['request']):
                yield NOT_USED
            else:
                _args, _kwargs = _map_arguments(*_args, **_kwargs)
                for res in fixture_func(*_args, **_kwargs):
                    yield res

    # transform the created wrapper into a fixture
    _make_fix = pytest_fixture(scope=scope,
                               params=final_values,
                               autouse=autouse,
                               hook=hook,
                               ids=final_ids,
                               **kwargs)
    return _make_fix(wrapped_fixture_func)
def saved_fixture(
        store='fixture_store',  # type: Union[str, Dict[str, Any]]
        key=None,  # type: str
        views=None,  # type: Dict[str, Callable[[Any], Any]]
        save_raw=None,  # type: bool
        fixture_fun=DECORATED):
    """
    Decorates a fixture so that it is saved in `store`. `store` can be a dict-like variable or a string
    representing a fixture name to a dict-like session-scoped fixture. By default it uses the global 'fixture_store'
    fixture provided by this plugin.

    After executing all tests, `<store>` will contain a new item under key `<key>` (default is the name of the fixture).
    This item is a dictionary <test_id>: <fixture_value> for each test node where the fixture was setup.

    ```python
    import pytest
    from pytest_harvest import saved_fixture

    @pytest.fixture
    @saved_fixture
    def dummy():
        return 1

    def test_dummy(dummy):
        pass

    def test_synthesis(fixture_store):
        print(fixture_store['dummy'])
    ```

    Note that for session-scoped and module-scoped fixtures, not all test ids will appear in the store - only those
    for which the fixture was (re)created.

    Users can save additional views created from the fixture instance by applying transforms (callable functions). To
    do this, users can provide a dictionary under the `views` argument, containing a `{<key>: <procedure>}` dict-like.
    For each entry, `<procedure>` will be applied on the fixture instance, and the result will be stored under `<key>`.
    `save_raw` controls whether the fixture instance should still be saved in this case (default: `True` if
    `views is None`, `False` otherwise).

    :param store: a dict-like object or a fixture name corresponding to a dict-like object. in this dictionary, a new
        entry will be added for the fixture. This entry will contain a dictionary <test_id>: <fixture_value> for each
        test node. By default fixtures are stored in the `fixture_store``fixture.
    :param key: the name associated with the stored fixture in the store. By default this is the fixture name.
    :param views: an optional dictionary that can be provided to store views created from the fixture, rather than (or
        in addition to, if `save_raw=True`) the fixture itself. The dict should contain a `{<key>: <procedure>}`
        dict-like. For each entry, `<procedure>` will be applied on the fixture instance, and the result will be stored
        under `<key>`.
    :param save_raw: controls whether the fixture instance should be saved. `None` (Default) is an automatic behaviour
        meaning "`True` if `views is None`, `False` otherwise".
    :return: a fixture that will be stored
    """
    # default: if views is None, we save the raw fixture. If user wants to save views, we do not save the raw
    if save_raw is None:
        save_raw = views is None

    # Simply get the function name
    fixture_name = getattr(fixture_fun, '__name__', None) or str(fixture_fun)

    # Name to use for storage
    key = key or fixture_name

    # is the store a fixture or an object ?
    store_is_a_fixture = isinstance(store, str)

    # if the store object is already available, we can ensure that it is initialized. Otherwise trust pytest for that
    if not store_is_a_fixture:
        if key in store.keys():
            raise ValueError(
                "Key '%s' already exists in store object. Please make sure that your store object is "
                "properly initialized as an empty dict-like object, and/or provide a different custom "
                "`name` if two stored fixtures share the same storage key name."
            )

    # Note: we can not init the storage[key] entry here because when storage is a fixture, it does not yet exist.

    def _init_and_check(request, store):
        """Performs a few checks and returns the key to use for saving (the node id)"""
        # find the current node id depending on the scope
        scope = get_scope(request)
        if scope == 'function':
            nodeid = request.node.nodeid
        else:
            # session- or module-scope
            # raise Exception("The `@saved_fixture` decorator is only applicable to function-scope fixtures. `%s`"
            #                 " seems to have scope='%s'. Consider removing `@saved_fixture` or changing "
            #                 "the scope to 'function'." % (fixture_fun, scope))
            nodeid = request._pyfuncitem.nodeid

        # Init storage if needed
        if save_raw:
            # init
            if key not in store:
                store[key] = OrderedDict()
            # Check that the node id is unique
            if nodeid in store[key]:
                raise KeyError(
                    "Internal Error - This fixture '%s' was already "
                    "stored for test id '%s'" % (key, nodeid))

        if views is not None:
            for k in views.keys():
                if k not in store:
                    store[k] = OrderedDict()
                # Check that the node id is unique
                if nodeid in store[k]:
                    raise KeyError(
                        "Internal Error - This fixture view '%s' was already "
                        "stored for test id '%s'" % (k, nodeid))

        return nodeid

    # We will expose a new signature with additional arguments
    orig_sig = signature(fixture_fun)
    func_needs_request = 'request' in orig_sig.parameters
    new_args = ((Parameter('request', kind=Parameter.POSITIONAL_OR_KEYWORD),) if not func_needs_request else ()) \
               + ((Parameter(store, kind=Parameter.POSITIONAL_OR_KEYWORD),) if store_is_a_fixture else ())
    new_sig = add_signature_parameters(orig_sig, first=new_args)

    # Wrap the fixture in the correct mode (generator or not)
    if not isgeneratorfunction(fixture_fun):

        @wraps(fixture_fun, new_sig=new_sig)
        def stored_fixture_function(*args, **kwargs):
            """Wraps a fixture so as to store it before it is returned"""
            # get the actual store object
            if store_is_a_fixture:
                store_ = kwargs.pop(store)  # read and remove it
            else:
                # use the variable from outer scope (from `make_saved_fixture`)
                store_ = store
            request = kwargs['request'] if func_needs_request else kwargs.pop(
                'request')
            nodeid = _init_and_check(request, store_)
            fixture_value = fixture_fun(*args, **kwargs)  # Get the fixture
            _store_fixture_and_views(store_, nodeid, key, fixture_value, views,
                                     save_raw)  # Store it
            return fixture_value  # Return it

    else:

        @wraps(fixture_fun, new_sig=new_sig)
        def stored_fixture_function(*args, **kwargs):
            """Wraps a fixture so as to store it before it is returned (generator mode)"""
            # get the actual store object
            if store_is_a_fixture:
                store_ = kwargs.pop(store)
            else:
                # use the variable from outer scope (from `make_saved_fixture`)
                store_ = store
            request = kwargs['request'] if func_needs_request else kwargs.pop(
                'request')
            nodeid = _init_and_check(request, store_)
            gen = fixture_fun(*args, **kwargs)
            fixture_value = next(gen)  # Get the fixture
            _store_fixture_and_views(store_, nodeid, key, fixture_value, views,
                                     save_raw)  # Store it
            yield fixture_value  # Return it

            # Make sure to terminate the underlying generator
            try:
                next(gen)
            except StopIteration:
                pass

    return stored_fixture_function