예제 #1
0
    def _decorator(f):
        # check the signature of f
        foo_sig = signature(f)
        needs_logfile_injection = logfile_arg in foo_sig.parameters
        needs_logfilehandler_injection = logfile_handler_arg in foo_sig.parameters

        # modify the exposed signature if needed
        new_sig = None
        if needs_logfile_injection:
            new_sig = remove_signature_parameters(foo_sig, logfile_arg)
        if needs_logfilehandler_injection:
            new_sig = remove_signature_parameters(foo_sig, logfile_handler_arg)

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

            # add file handler to logger
            logfile = logs_dir / ("%s.log" %
                                  PowerSession.get_session_id(session))
            error_logfile = logfile.with_name("ERROR_%s" % logfile.name)
            success_logfile = logfile.with_name("SUCCESS_%s" % logfile.name)
            # delete old files if present
            for _f in (logfile, error_logfile, success_logfile):
                if _f.exists():
                    _f.unlink()

            # add a FileHandler to the logger
            logfile_handler = log_to_file(logfile)

            # inject the log file / log file handler in the args:
            if needs_logfile_injection:
                kwargs[logfile_arg] = logfile
            if needs_logfilehandler_injection:
                kwargs[logfile_handler_arg] = logfile_handler

            # finally execute the session
            try:
                res = f(**kwargs)
            except Exception as e:
                # close and detach the file logger and rename as ERROR_....log
                remove_file_logger()
                logfile.rename(error_logfile)
                raise e
            else:
                # close and detach the file logger and rename as SUCCESS_....log
                remove_file_logger()
                logfile.rename(success_logfile)
                return res

        return _f_wrapper
예제 #2
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
예제 #3
0
            def more_vars(f=DECORATED, **extras):
                # (1) capture the signature of the function to wrap and remove the invisible
                func_sig = signature(f)
                new_sig = remove_signature_parameters(func_sig, 'invisible_args')

                # (2) create a wrapper with the new signature
                @wraps(f, new_sig=new_sig)
                def wrapped(*args, **kwargs):
                    kwargs['invisible_args'] = extras
                    return f(*args, **kwargs)

                return wrapped
예제 #4
0
    def __construct(self, *args):
        constructor_signature = inspect.signature(Instance.__init__)
        constructor_args = self.prog_args[:self.constructor_nargs]
        new_constructor_signature = makefun.remove_signature_parameters(
            constructor_signature, 'self')

        @makefun.with_signature(new_constructor_signature,
                                func_name='instance')
        def super_init(**init_kwargs):
            print(f'{init_kwargs}')
            Instance.__init__(self, **init_kwargs)

        constructor_args.extend(args)
        defopt.run(super_init,
                   argv=constructor_args,
                   strict_kwonly=False,
                   short={})
예제 #5
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)'
예제 #6
0
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 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
예제 #8
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)
예제 #9
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
예제 #10
0
def make_decorator_spec(
        impl_function,
        flat_mode_decorated_name=None  # type: str
):
    """
    Analyzes the implementation function


    If `flat_mode_decorated_name` is set, this is a shortcut for flat mode. In that case the implementation function
    is not analyzed.

    :param impl_function:
    :param flat_mode_decorated_name:
    :return: sig_info, function_for_metadata, nested_impl_function
    """
    # extract the implementation's signature
    implementors_signature = signature(impl_function)

    # determine the mode (nested, flat, double-flat) and check signature
    mode, injected_name, contains_varpositional, injected_pos, \
    injected_arg, f_args_name, f_kwargs_name = extract_mode_info(implementors_signature, flat_mode_decorated_name)

    # create the signature of the decorator function to create, according to mode
    if mode is None:
        # *nested: keep the signature 'as is'
        exposed_signature = implementors_signature
        function_for_metadata = impl_function
        nested_impl_function = impl_function

    elif mode is DECORATED:  # flat mode
        # use the same signature, but remove the injected arg.
        exposed_signature = remove_signature_parameters(
            implementors_signature, injected_name)

        # use the original function for the docstring/module metadata
        function_for_metadata = impl_function

        # generate the corresponding nested decorator
        nested_impl_function = make_nested_impl_for_flat_mode(
            exposed_signature, impl_function, injected_name, injected_pos)

    elif mode is WRAPPED:
        # *double-flat: the same signature, but we remove the injected args.
        args_to_remove = (injected_name,) + ((f_args_name,) if f_args_name is not None else ()) \
                         + ((f_kwargs_name,) if f_kwargs_name is not None else ())
        exposed_signature = remove_signature_parameters(
            implementors_signature, *args_to_remove)

        # use the original function for the docstring/module metadata
        function_for_metadata = impl_function

        # generate the corresponding nested decorator
        nested_impl_function = make_nested_impl_for_doubleflat_mode(
            exposed_signature, impl_function, injected_name, f_args_name,
            f_kwargs_name, injected_pos)

    else:
        raise ValueError("Unknown mode: %s" % mode)

    # create an object to easily access the exposed signature information afterwards
    sig_info = SignatureInfo(exposed_signature, contains_varpositional,
                             injected_pos)

    return sig_info, function_for_metadata, nested_impl_function