Skip to content

Commit 3489432

Browse files
authored
Merge pull request #127 from smarie/fix_issue_126
Fixed ` ValueError` when two functions parametrized with the same cases were sitting in the same file
2 parents b24ee2b + a2aa2fa commit 3489432

File tree

10 files changed

+418
-101
lines changed

10 files changed

+418
-101
lines changed

docs/api_reference.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ cases_funs = get_all_cases(f, cases=cases, prefix=prefix,
6161
glob=glob, has_tag=has_tag, filter=filter)
6262

6363
# Transform the various functions found
64-
argvalues = get_parametrize_args(cases_funs)
64+
argvalues = get_parametrize_args(host_class_or_module_of_f, cases_funs)
6565
```
6666

6767
**Parameters**
@@ -97,7 +97,9 @@ Lists all desired cases for a given `parametrization_target` (a test function or
9797
### `get_parametrize_args`
9898

9999
```python
100-
def get_parametrize_args(cases_funs: List[Callable],
100+
def get_parametrize_args(host_class_or_module: Union[Type, ModuleType],
101+
cases_funs: List[Callable],
102+
debug: bool = False
101103
) -> List[Union[lazy_value, fixture_ref]]:
102104
```
103105

pytest_cases/case_funcs_new.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,5 +219,8 @@ def is_case_function(f, prefix=CASE_PREFIX_FUN, check_prefix=True):
219219
return False
220220
elif safe_isclass(f):
221221
return False
222+
elif hasattr(f, '_pytestcasesgen'):
223+
# a function generated by us. ignore this
224+
return False
222225
else:
223226
return f.__name__.startswith(prefix) if check_prefix else True

pytest_cases/case_parametrizer_new.py

Lines changed: 157 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,22 @@
33

44
from functools import partial
55
from importlib import import_module
6-
from inspect import getmembers
6+
from inspect import getmembers, isfunction, ismethod
77
import re
88
from warnings import warn
99

10+
import makefun
11+
1012
try:
1113
from typing import Union, Callable, Iterable, Any, Type, List, Tuple # noqa
1214
except ImportError:
1315
pass
1416

1517
from .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
1719
from .common_pytest_marks import copy_pytest_marks, make_marked_parameter_value
1820
from .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

2123
from . import fixture
2224
from .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+
300424
def 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`,

pytest_cases/common_mini_six.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import sys
22

3-
PY3 = sys.version_info[0] == 3
3+
PY3 = sys.version_info[0] >= 3
44
PY34 = sys.version_info[0:2] >= (3, 4)
55

66
if PY3:
@@ -9,6 +9,42 @@
99
string_types = basestring,
1010

1111

12+
# if PY3:
13+
# def reraise(tp, value, tb=None):
14+
# try:
15+
# if value is None:
16+
# value = tp()
17+
# else:
18+
# # HACK to fix bug
19+
# value = tp(*value)
20+
# if value.__traceback__ is not tb:
21+
# raise value.with_traceback(tb)
22+
# raise value
23+
# finally:
24+
# value = None
25+
# tb = None
26+
#
27+
# else:
28+
# def exec_(_code_, _globs_=None, _locs_=None):
29+
# """Execute code in a namespace."""
30+
# if _globs_ is None:
31+
# frame = sys._getframe(1)
32+
# _globs_ = frame.f_globals
33+
# if _locs_ is None:
34+
# _locs_ = frame.f_locals
35+
# del frame
36+
# elif _locs_ is None:
37+
# _locs_ = _globs_
38+
# exec("""exec _code_ in _globs_, _locs_""")
39+
#
40+
# exec_("""def reraise(tp, value, tb=None):
41+
# try:
42+
# raise tp, value, tb
43+
# finally:
44+
# tb = None
45+
# """)
46+
47+
1248
def with_metaclass(meta, *bases):
1349
"""Create a base class with a metaclass."""
1450
# This requires a bit of explanation: the basic idea is to make a dummy

0 commit comments

Comments
 (0)