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
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 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
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={})
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)'
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
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)
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
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