Skip to content

Commit 5ec99ab

Browse files
author
Sylvain MARIE
committed
@pytest.mark.usefixtures can be now be used on case functions. Fixes #152
1 parent ea5b9e0 commit 5ec99ab

File tree

5 files changed

+145
-20
lines changed

5 files changed

+145
-20
lines changed

pytest_cases/case_parametrizer_new.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,22 @@
55
# Use true division operator always even in old python 2.x (used in `_extract_cases_from_module`)
66
from __future__ import division
77

8-
from functools import partial
8+
import functools
99
from importlib import import_module
1010
from inspect import getmembers, isfunction, ismethod
1111
import re
1212
from warnings import warn
1313

14-
import makefun
15-
1614
try:
1715
from typing import Union, Callable, Iterable, Any, Type, List, Tuple # noqa
1816
except ImportError:
1917
pass
2018

2119
from .common_mini_six import string_types
22-
from .common_others import get_code_first_line, AUTO, AUTO2, qname
23-
from .common_pytest_marks import copy_pytest_marks, make_marked_parameter_value
20+
from .common_others import get_code_first_line, AUTO, AUTO2, qname, funcopy
21+
from .common_pytest_marks import copy_pytest_marks, make_marked_parameter_value, remove_pytest_mark
2422
from .common_pytest_lazy_values import lazy_value
25-
from .common_pytest import safe_isclass, MiniMetafunc, is_fixture, get_fixture_name, inject_host
23+
from .common_pytest import safe_isclass, MiniMetafunc, is_fixture, get_fixture_name, inject_host, add_fixture_params
2624

2725
from . import fixture
2826
from .case_funcs_new import matches_tag_query, is_case_function, is_case_class, CaseInfo, CASE_PREFIX_FUN
@@ -305,19 +303,26 @@ def case_to_argvalues(host_class_or_module, # type: Union[Type, ModuleType]
305303
if not meta.is_parametrized:
306304
# single unparametrized case function
307305
if debug:
308-
case_fun_str = qname(case_fun.func if isinstance(case_fun, partial) else case_fun)
306+
case_fun_str = qname(case_fun.func if isinstance(case_fun, functools.partial) else case_fun)
309307
print("Case function %s > 1 lazy_value() with id %s and marks %s" % (case_fun_str, case_id, case_marks))
310308
return (lazy_value(case_fun, id=case_id, marks=case_marks),)
311309
else:
312310
# parametrized. create one version of the callable for each parametrized call
313311
if debug:
314-
case_fun_str = qname(case_fun.func if isinstance(case_fun, partial) else case_fun)
312+
case_fun_str = qname(case_fun.func if isinstance(case_fun, functools.partial) else case_fun)
315313
print("Case function %s > tuple of lazy_value() with ids %s and marks %s"
316314
% (case_fun_str, ["%s-%s" % (case_id, c.id) for c in meta._calls], [c.marks for c in meta._calls]))
317-
return tuple(lazy_value(partial(case_fun, **c.funcargs), id="%s-%s" % (case_id, c.id), marks=c.marks)
315+
return tuple(lazy_value(functools.partial(case_fun, **c.funcargs),
316+
id="%s-%s" % (case_id, c.id), marks=c.marks)
318317
for c in meta._calls)
319318
else:
320-
# at least a required fixture:
319+
# at least a required fixture (direct req or through @pytest.mark.usefixtures ):
320+
# handle @pytest.mark.usefixtures by creating a wrapper where the fixture is added to the signature
321+
if meta.fixturenames_not_in_sig:
322+
# create a wrapper with an explicit requirement for the fixtures
323+
case_fun = add_fixture_params(case_fun, meta.fixturenames_not_in_sig)
324+
# remove the `usefixtures` mark
325+
remove_pytest_mark(case_fun, "usefixtures")
321326
# create or reuse a fixture in the host (pytest collector: module or class) of the parametrization target
322327
fix_name = get_or_create_case_fixture(case_id, case_fun, host_class_or_module, debug)
323328

@@ -327,7 +332,7 @@ def case_to_argvalues(host_class_or_module, # type: Union[Type, ModuleType]
327332
# reference that case fixture
328333
argvalues_tuple = (fixture_ref(fix_name),)
329334
if debug:
330-
case_fun_str = qname(case_fun.func if isinstance(case_fun, partial) else case_fun)
335+
case_fun_str = qname(case_fun.func if isinstance(case_fun, functools.partial) else case_fun)
331336
print("Case function %s > fixture_ref(%r) with marks %s" % (case_fun_str, fix_name, case_marks))
332337
return make_marked_parameter_value(argvalues_tuple, marks=case_marks) if case_marks else argvalues_tuple
333338

@@ -358,8 +363,8 @@ def get_or_create_case_fixture(case_id, # type: str
358363
" %s. If you did not decorate it but still see this error, please report this issue"
359364
% case_fun)
360365

361-
# source
362-
case_in_class = isinstance(case_fun, partial) and hasattr(case_fun, 'host_class')
366+
# source: detect a functools.partial wrapper created by us because of a host class
367+
case_in_class = isinstance(case_fun, functools.partial) and hasattr(case_fun, 'host_class')
363368
true_case_func = case_fun.func if case_in_class else case_fun
364369
# case_host = case_fun.host_class if case_in_class else import_module(case_fun.__module__)
365370

@@ -397,11 +402,6 @@ def name_changer(name, i):
397402
if debug:
398403
print("Case function %s > Creating fixture %r in %s" % (qname(true_case_func), fix_name, target_host))
399404

400-
def funcopy(f):
401-
# apparently it is not possible to create an actual copy with copy() !
402-
# Use makefun.partial which preserves the parametrization marks (we need them)
403-
return makefun.partial(f)
404-
405405
if case_in_class:
406406
if target_in_class:
407407
# both in class: direct copy of the non-partialized version
@@ -614,7 +614,7 @@ def _of_interest(x): # noqa
614614
# skip it
615615
continue
616616
# partialize the function to get one without the 'self' argument
617-
new_m = partial(m, cls())
617+
new_m = functools.partial(m, cls())
618618
# remember the class
619619
new_m.host_class = cls
620620
# we have to recopy all metadata concerning the case function

pytest_cases/common_others.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>
55
import functools
66
import inspect
7+
import makefun
78
from importlib import import_module
89
from inspect import findsource
910
import re
@@ -264,3 +265,25 @@ def qname(func):
264265
else:
265266
# host class: recurse (note that in python 2 nested classes do not have a way to know their parent class)
266267
return "%s.%s" % (qname(hostclass), func.__name__)
268+
269+
270+
# if sys.version_info > (3, ):
271+
def funcopy(f):
272+
# see https://stackoverflow.com/a/6527746/7262247
273+
# and https://stackoverflow.com/a/13503277/7262247
274+
# apparently it is not possible to create an actual copy with copy() !
275+
# Use makefun.partial which preserves the parametrization marks (we need them)
276+
return makefun.partial(f)
277+
# fun = FunctionType(f.__code__, f.__globals__, f.__name__, f.__defaults__, f.__closure__)
278+
# fun.__dict__.update(f.__dict__)
279+
# fun = functools.update_wrapper(fun, f)
280+
# fun.__kwdefaults__ = f.__kwdefaults__
281+
# return fun
282+
# else:
283+
# def funcopy(f):
284+
# fun = FunctionType(f.func_code, f.func_globals, name=f.func_name, argdefs=f.func_defaults,
285+
# closure=f.func_closure)
286+
# fun.__dict__.update(f.__dict__)
287+
# fun = functools.update_wrapper(fun, f)
288+
# fun.__kwdefaults__ = f.__kwdefaults__
289+
# return fun

pytest_cases/common_pytest.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>
55
from __future__ import division
66

7+
from makefun import add_signature_parameters, wraps
8+
79
try: # python 3.3+
810
from inspect import signature, Parameter
911
except ImportError:
@@ -23,7 +25,7 @@
2325
from .common_mini_six import string_types
2426
from .common_others import get_function_host
2527
from .common_pytest_marks import make_marked_parameter_value, get_param_argnames_as_list, has_pytest_param, \
26-
get_pytest_parametrize_marks
28+
get_pytest_parametrize_marks, get_pytest_usefixture_marks
2729
from .common_pytest_lazy_values import is_lazy_value
2830

2931

@@ -550,6 +552,10 @@ def __init__(self, func):
550552
self._calls = []
551553
# non-default parameters
552554
self.fixturenames = getfuncargnames(func)
555+
# add declared used fixtures with @pytest.mark.usefixtures
556+
self.fixturenames_not_in_sig = [f for f in get_pytest_usefixture_marks(func) if f not in self.fixturenames]
557+
if self.fixturenames_not_in_sig:
558+
self.fixturenames = tuple(self.fixturenames_not_in_sig + list(self.fixturenames))
553559
# get parametrization marks
554560
self.pmarks = get_pytest_parametrize_marks(self.function)
555561
if self.is_parametrized:
@@ -588,6 +594,41 @@ def update_callspecs(self):
588594
c.marks = list(c.keywords.values())
589595

590596

597+
def add_fixture_params(func, new_names):
598+
"""Creates a wrapper of the given function with additional arguments"""
599+
600+
old_sig = signature(func)
601+
602+
# prepend all new parameters if needed
603+
for n in new_names:
604+
if n in old_sig.parameters:
605+
raise ValueError("argument named %s already present in signature" % n)
606+
new_sig = add_signature_parameters(old_sig,
607+
first=[Parameter(n, kind=Parameter.POSITIONAL_OR_KEYWORD) for n in new_names])
608+
609+
assert not isgeneratorfunction(func)
610+
611+
# normal function with return statement
612+
@wraps(func, new_sig=new_sig)
613+
def wrapped_func(**kwargs):
614+
for n in new_names:
615+
kwargs.pop(n)
616+
return func(**kwargs)
617+
618+
# else:
619+
# # generator function (with a yield statement)
620+
# @wraps(fixture_func, new_sig=new_sig)
621+
# def wrapped_fixture_func(*args, **kwargs):
622+
# request = kwargs['request'] if func_needs_request else kwargs.pop('request')
623+
# if is_used_request(request):
624+
# for res in fixture_func(*args, **kwargs):
625+
# yield res
626+
# else:
627+
# yield NOT_USED
628+
629+
return wrapped_func
630+
631+
591632
def get_callspecs(func):
592633
"""
593634
Returns a list of pytest CallSpec objects corresponding to calls that should be made for this parametrized function.

pytest_cases/common_pytest_marks.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# + All contributors to <https://github.com/smarie/python-pytest-cases>
33
#
44
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>
5+
import itertools
6+
57
import warnings
68
from distutils.version import LooseVersion
79

@@ -114,6 +116,37 @@ def get_pytest_marks_on_function(f, as_decorators=False):
114116
return mks
115117

116118

119+
def get_pytest_usefixture_marks(f):
120+
# pytest > 3.2.0
121+
marks = getattr(f, 'pytestmark', None)
122+
if marks is not None:
123+
return tuple(itertools.chain.from_iterable(
124+
mark.args for mark in marks if mark.name == 'usefixtures'
125+
))
126+
else:
127+
# older versions
128+
mark_info = getattr(f, 'usefixtures', None)
129+
if mark_info is not None:
130+
return mark_info.args
131+
else:
132+
return ()
133+
134+
135+
def remove_pytest_mark(f, mark_name):
136+
marks = getattr(f, 'pytestmark', None)
137+
if marks is not None:
138+
# pytest > 3.2.0
139+
new_marks = [m for m in marks if m.name != mark_name]
140+
setattr(f, 'pytestmark', new_marks)
141+
else:
142+
# older versions
143+
try:
144+
delattr(f, mark_name)
145+
except AttributeError:
146+
pass
147+
return f
148+
149+
117150
def get_pytest_parametrize_marks(f):
118151
"""
119152
Returns the @pytest.mark.parametrize marks associated with a function (and only those)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import pytest
2+
3+
from pytest_cases import fixture, parametrize_with_cases
4+
5+
used = False
6+
7+
8+
@fixture
9+
def validationOff():
10+
global used
11+
used = True
12+
yield
13+
used = False
14+
15+
16+
def case_first(validationOff):
17+
pass
18+
19+
20+
@pytest.mark.usefixtures("validationOff")
21+
def case_second():
22+
pass
23+
24+
25+
@parametrize_with_cases("a", cases='.')
26+
def test_uses_fixture(a):
27+
global used
28+
assert used

0 commit comments

Comments
 (0)