33
44from functools import partial
55from importlib import import_module
6- from inspect import getmembers
6+ from inspect import getmembers , isfunction , ismethod
77import re
88from warnings import warn
99
10+ import makefun
11+
1012try :
1113 from typing import Union , Callable , Iterable , Any , Type , List , Tuple # noqa
1214except ImportError :
1315 pass
1416
1517from .common_mini_six import string_types
16- from .common_others import get_code_first_line , AUTO , AUTO2
18+ from .common_others import get_code_first_line , AUTO , AUTO2 , qname
1719from .common_pytest_marks import copy_pytest_marks , make_marked_parameter_value
1820from .common_pytest_lazy_values import lazy_value
19- from .common_pytest import safe_isclass , MiniMetafunc
21+ from .common_pytest import safe_isclass , MiniMetafunc , is_fixture , get_fixture_name , inject_host
2022
2123from . import fixture
2224from .case_funcs_new import matches_tag_query , is_case_function , is_case_class , CaseInfo , CASE_PREFIX_FUN
@@ -43,6 +45,7 @@ def parametrize_with_cases(argnames, # type: str
4345 glob = None , # type: str
4446 has_tag = None , # type: Any
4547 filter = None , # type: Callable[[Callable], bool] # noqa
48+ debug = False , # type: bool
4649 ** kwargs
4750 ):
4851 # type: (...) -> Callable[[Callable], Callable]
@@ -68,7 +71,7 @@ def parametrize_with_cases(argnames, # type: str
6871 cases_funs = get_all_cases(f, cases=cases, prefix=prefix, glob=glob, has_tag=has_tag, filter=filter)
6972
7073 # Transform the various functions found
71- argvalues = get_parametrize_args(cases_funs, prefix=prefix )
74+ argvalues = get_parametrize_args(host_class_or_module, cases_funs, debug=False )
7275 ```
7376
7477 :param argnames: same than in @pytest.mark.parametrize
@@ -88,21 +91,29 @@ def parametrize_with_cases(argnames, # type: str
8891 decorator on the case function(s) to be selected.
8992 :param filter: a callable receiving the case function and returning True or a truth value in case the function
9093 needs to be selected.
94+ :param debug: a boolean flag to debug what happens behind the scenes
9195 :return:
9296 """
93- def _apply_parametrization (f ):
97+ @inject_host
98+ def _apply_parametrization (f , host_class_or_module ):
9499 """ execute parametrization of test function or fixture `f` """
95100
96101 # Collect all cases
97102 cases_funs = get_all_cases (f , cases = cases , prefix = prefix , glob = glob , has_tag = has_tag , filter = filter )
98103
99- # Transform the various functions found
100- argvalues = get_parametrize_args (cases_funs )
104+ # Transform the various case functions found into `lazy_value` (for case functions not requiring fixtures)
105+ # or `fixture_ref` (for case functions requiring fixtures - for them we create associated case fixtures in
106+ # `host_class_or_module`)
107+ argvalues = get_parametrize_args (host_class_or_module , cases_funs , debug = debug )
101108
102109 # Finally apply parametrization - note that we need to call the private method so that fixture are created in
103110 # the right module (not here)
104- _parametrize_with_cases = _parametrize_plus (argnames , argvalues , ** kwargs )
105- return _parametrize_with_cases (f )
111+ _parametrize_with_cases , needs_inject = _parametrize_plus (argnames , argvalues , debug = debug , ** kwargs )
112+
113+ if needs_inject :
114+ return _parametrize_with_cases (f , host_class_or_module )
115+ else :
116+ return _parametrize_with_cases (f )
106117
107118 return _apply_parametrization
108119
@@ -220,26 +231,32 @@ def get_all_cases(parametrization_target, # type: Callable
220231 and matches_tag_query (c , has_tag = has_tag , filter = filters )]
221232
222233
223- def get_parametrize_args (cases_funs , # type: List[Callable]
234+ def get_parametrize_args (host_class_or_module , # type: Union[Type, ModuleType]
235+ cases_funs , # type: List[Callable]
236+ debug = False # type: bool
224237 ):
225238 # type: (...) -> List[Union[lazy_value, fixture_ref]]
226239 """
227240 Transforms a list of cases (obtained from `get_all_cases`) into a list of argvalues for `@parametrize`.
228241 Each case function `case_fun` is transformed into one or several `lazy_value`(s) or a `fixture_ref`:
229242
230243 - If `case_fun` requires at least on fixture, a fixture will be created if not yet present, and a `fixture_ref`
231- will be returned.
244+ will be returned. The fixture will be created in `host_class_or_module`
232245 - If `case_fun` is a parametrized case, one `lazy_value` with a partialized version will be created for each
233246 parameter combination.
234247 - Otherwise, `case_fun` represents a single case: in that case a single `lazy_value` is returned.
235248
236- :param cases_funs: a list of case functions returned typically by `get_all_cases`
249+ :param host_class_or_module: host of the parametrization target. A class or a module.
250+ :param cases_funs: a list of case functions, returned typically by `get_all_cases`
251+ :param debug: a boolean flag, turn it to True to print debug messages.
237252 :return:
238253 """
239- return [c for _f in cases_funs for c in case_to_argvalues (_f )]
254+ return [c for _f in cases_funs for c in case_to_argvalues (host_class_or_module , _f , debug )]
240255
241256
242- def case_to_argvalues (case_fun , # type: Callable
257+ def case_to_argvalues (host_class_or_module , # type: Union[Type, ModuleType]
258+ case_fun , # type: Callable
259+ debug = False # type: bool
243260 ):
244261 # type: (...) -> Tuple[lazy_value]
245262 """Transform a single case into one or several `lazy_value`(s) or a `fixture_ref` to be used in `@parametrize`
@@ -265,38 +282,145 @@ def case_to_argvalues(case_fun, # type: Callable
265282 if not meta .requires_fixtures :
266283 if not meta .is_parametrized :
267284 # single unparametrized case function
285+ if debug :
286+ case_fun_str = qname (case_fun .func if isinstance (case_fun , partial ) else case_fun )
287+ print ("Case function %s > 1 lazy_value() with id %s and marks %s" % (case_fun_str , case_id , case_marks ))
268288 return (lazy_value (case_fun , id = case_id , marks = case_marks ),)
269289 else :
270290 # parametrized. create one version of the callable for each parametrized call
291+ if debug :
292+ case_fun_str = qname (case_fun .func if isinstance (case_fun , partial ) else case_fun )
293+ print ("Case function %s > tuple of lazy_value() with ids %s and marks %s"
294+ % (case_fun_str , ["%s-%s" % (case_id , c .id ) for c in meta ._calls ], [c .marks for c in meta ._calls ]))
271295 return tuple (lazy_value (partial (case_fun , ** c .funcargs ), id = "%s-%s" % (case_id , c .id ), marks = c .marks )
272296 for c in meta ._calls )
273297 else :
274- # at least a required fixture: create a fixture
275- # unwrap any partial that would have been created by us because the fixture was in a class
276- if isinstance (case_fun , partial ):
277- host_cls = case_fun .host_class
278- case_fun = case_fun .func
279- else :
280- host_cls = None
298+ # at least a required fixture:
299+ # create or reuse a fixture in the host (pytest collector: module or class) of the parametrization target
300+ fix_name = get_or_create_case_fixture (case_id , case_fun , host_class_or_module , debug )
281301
282- host_module = import_module (case_fun .__module__ )
283-
284- # create a new fixture and place it on the host
285- # we have to create a unique fixture name if the fixture already exists.
286- def name_changer (name , i ):
287- return name + '_' * i
288- new_fix_name = check_name_available (host_cls or host_module , name = case_id , if_name_exists = CHANGE ,
289- name_changer = name_changer )
290302 # if meta.is_parametrized:
291303 # nothing to do, the parametrization marks are already there
292- new_fix = fixture (name = new_fix_name )(case_fun )
293- setattr (host_cls or host_module , new_fix_name , new_fix )
294304
295- # now reference the new or existing fixture
296- argvalues_tuple = (fixture_ref (new_fix_name ),)
305+ # reference that case fixture
306+ argvalues_tuple = (fixture_ref (fix_name ),)
307+ if debug :
308+ case_fun_str = qname (case_fun .func if isinstance (case_fun , partial ) else case_fun )
309+ print ("Case function %s > fixture_ref(%r) with marks %s" % (case_fun_str , fix_name , case_marks ))
297310 return make_marked_parameter_value (argvalues_tuple , marks = case_marks ) if case_marks else argvalues_tuple
298311
299312
313+ def get_or_create_case_fixture (case_id , # type: str
314+ case_fun , # type: Callable
315+ target_host , # type: Union[Type, ModuleType]
316+ debug = False # type: bool
317+ ):
318+ # type: (...) -> str
319+ """
320+ When case functions require fixtures, we want to rely on pytest to inject everything. Therefore
321+ we create a fixture wrapping the case function. Since a case function may not be located in the same place
322+ than the test/fixture requiring it (decorated with @parametrize_with_cases), we create that fixture in the
323+ appropriate module/class (the host of the test/fixture function).
324+
325+ :param case_id:
326+ :param case_fun:
327+ :param host_class_or_module:
328+ :param debug:
329+ :return: the newly created fixture name
330+ """
331+ if is_fixture (case_fun ):
332+ raise ValueError ("A case function can not be decorated as a `@fixture`. This seems to be the case for"
333+ " %s. If you did not decorate it but still see this error, please report this issue"
334+ % case_fun )
335+
336+ # source
337+ case_in_class = isinstance (case_fun , partial ) and hasattr (case_fun , 'host_class' )
338+ true_case_func = case_fun .func if case_in_class else case_fun
339+ # case_host = case_fun.host_class if case_in_class else import_module(case_fun.__module__)
340+
341+ # for checks
342+ orig_name = true_case_func .__name__
343+ orig_case = true_case_func
344+
345+ # destination
346+ target_in_class = safe_isclass (target_host )
347+ fix_cases_dct = _get_fixture_cases (target_host ) # get our "storage unit" in this module
348+
349+ # shortcut if the case fixture is already known/registered in target host
350+ try :
351+ fix_name = fix_cases_dct [true_case_func ]
352+ if debug :
353+ print ("Case function %s > Reusing fixture %r" % (qname (true_case_func ), fix_name ))
354+ return fix_name
355+ except KeyError :
356+ pass
357+
358+ # not yet known there. Create a new symbol in the target host :
359+ # we need a "free" fixture name, and a "free" symbol name
360+ existing_fixture_names = []
361+ for n , symb in getmembers (target_host , lambda f : isfunction (f ) or ismethod (f )):
362+ if is_fixture (symb ):
363+ existing_fixture_names .append (get_fixture_name (symb ))
364+
365+ def name_changer (name , i ):
366+ return name + '_' * i
367+
368+ # start with name = case_id and find a name that does not exist
369+ fix_name = check_name_available (target_host , extra_forbidden_names = existing_fixture_names , name = case_id ,
370+ if_name_exists = CHANGE , name_changer = name_changer )
371+
372+ if debug :
373+ print ("Case function %s > Creating fixture %r in %s" % (qname (true_case_func ), fix_name , target_host ))
374+
375+ def funcopy (f ):
376+ # apparently it is not possible to create an actual copy with copy() !
377+ return makefun .partial (f )
378+
379+ if case_in_class :
380+ if target_in_class :
381+ # both in class: direct copy of the non-partialized version
382+ case_fun = funcopy (case_fun .func )
383+ else :
384+ # case in class and target in module: use the already existing partialized version
385+ case_fun = funcopy (case_fun )
386+ else :
387+ if target_in_class :
388+ # case in module and target in class: create a static method
389+ case_fun = staticmethod (case_fun )
390+ else :
391+ # none in class: direct copy
392+ case_fun = funcopy (case_fun )
393+
394+ # create a new fixture from a copy of the case function, and place it on the target host
395+ new_fix = fixture (name = fix_name )(case_fun )
396+ # mark as generated by pytest-cases so that we skip it during cases collection
397+ new_fix ._pytestcasesgen = True
398+ setattr (target_host , fix_name , new_fix )
399+
400+ # remember it for next time
401+ fix_cases_dct [true_case_func ] = fix_name
402+
403+ # check that we did not touch the original case
404+ assert not is_fixture (orig_case )
405+ assert orig_case .__name__ == orig_name
406+
407+ return fix_name
408+
409+
410+ def _get_fixture_cases (module # type: ModuleType
411+ ):
412+ """
413+ Returns our 'storage unit' in a module, used to remember the fixtures created from case functions.
414+ That way we can reuse fixtures already created for cases, in a given module/class.
415+ """
416+ try :
417+ cache = module ._fixture_cases
418+ except AttributeError :
419+ cache = dict ()
420+ module ._fixture_cases = cache
421+ return cache
422+
423+
300424def import_default_cases_module (f , alt_name = False ):
301425 """
302426 Implements the `module=AUTO` behaviour of `@parameterize_cases`: based on the decorated test function `f`,
0 commit comments