Skip to content

Commit cc19be9

Browse files
author
Sylvain MARIE
committed
Now using decopatch to create the decorators.
1 parent f9132fb commit cc19be9

File tree

4 files changed

+50
-101
lines changed

4 files changed

+50
-101
lines changed

ci_tools/requirements-pip.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pytest-runner
33

44
# -- to install
55
makefun
6+
decopatch
67

78
# --- to generate the reports (see scripts in ci_tools, called by .travis)
89
pytest-html$PYTEST_HTML_VERSION

pytest_cases/case_funcs.py

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import division
22

3+
from decopatch import DECORATED, function_decorator, with_parenthesis
4+
35
try: # python 3.2+
46
from functools import lru_cache as lru
57
except ImportError:
@@ -40,7 +42,9 @@
4042
"""Internal marker used for cases generators"""
4143

4244

43-
def case_name(name # type: str
45+
@function_decorator
46+
def case_name(name, # type: str
47+
test_func=DECORATED
4448
):
4549
"""
4650
Decorator to override the name of a case function. The new name will be used instead of the function name,
@@ -55,16 +59,14 @@ def case_simple():
5559
:param name: the name that will be used in the test case instead of the case function name
5660
:return:
5761
"""
58-
def case_name_decorator(test_func):
59-
test_func.__name__ = name
60-
return test_func
61-
62-
return case_name_decorator
62+
test_func.__name__ = name
63+
return test_func
6364

6465

6566
CASE_TAGS_FIELD = '__case_tags__'
6667

6768

69+
@function_decorator(custom_disambiguator=with_parenthesis)
6870
def case_tags(*tags # type: Any
6971
):
7072
"""
@@ -75,17 +77,18 @@ def case_tags(*tags # type: Any
7577
functions...)
7678
:return:
7779
"""
78-
def case_tags_decorator(test_func):
79-
existing_tags = getattr(test_func, CASE_TAGS_FIELD, None)
80+
# we have to use "nested" mode for this decorator because in the decorator signature we have a var-positional
81+
def _apply(case_func):
82+
existing_tags = getattr(case_func, CASE_TAGS_FIELD, None)
8083
if existing_tags is None:
8184
# there are no tags yet. Use the provided
82-
setattr(test_func, CASE_TAGS_FIELD, list(tags))
85+
setattr(case_func, CASE_TAGS_FIELD, list(tags))
8386
else:
8487
# there are some tags already, let's try to add the new to the existing
85-
setattr(test_func, CASE_TAGS_FIELD, existing_tags + list(tags))
86-
return test_func
88+
setattr(case_func, CASE_TAGS_FIELD, existing_tags + list(tags))
89+
return case_func
8790

88-
return case_tags_decorator
91+
return _apply
8992

9093

9194
def test_target(target # type: Any
@@ -108,8 +111,10 @@ def test_target(target # type: Any
108111
test_target.__test__ = False # disable this function in pytest (otherwise name starts with 'test' > it will appear)
109112

110113

114+
@function_decorator
111115
def cases_generator(names=None, # type: Union[str, Callable[[Any], str], Iterable[str]]
112-
lru_cache=False, # type: bool
116+
lru_cache=False, # type: bool,
117+
case_func=DECORATED,
113118
**param_ranges # type: Iterable[Any]
114119
):
115120
"""
@@ -140,15 +145,11 @@ def cases_generator(names=None, # type: Union[str, Callable[[Any], str], I
140145
function so they should have names the underlying function can handle.
141146
:return:
142147
"""
143-
144-
def cases_generator_decorator(test_func):
145-
kwarg_values = list(product(*param_ranges.values()))
146-
setattr(test_func, _GENERATOR_FIELD, (names, param_ranges.keys(), kwarg_values))
147-
if lru_cache:
148-
nb_cases = len(kwarg_values)
149-
# decorate the function with the appropriate lru cache size
150-
test_func = lru(maxsize=nb_cases)(test_func)
151-
152-
return test_func
153-
154-
return cases_generator_decorator
148+
kwarg_values = list(product(*param_ranges.values()))
149+
setattr(case_func, _GENERATOR_FIELD, (names, param_ranges.keys(), kwarg_values))
150+
if lru_cache:
151+
nb_cases = len(kwarg_values)
152+
# decorate the function with the appropriate lru cache size
153+
case_func = lru(maxsize=nb_cases)(case_func)
154+
155+
return case_func

pytest_cases/main.py

Lines changed: 22 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from distutils.version import LooseVersion
88
from inspect import getmembers, isgeneratorfunction, getmodule
99

10+
from decopatch import function_decorator, DECORATED, with_parenthesis
1011
from makefun import with_signature, add_signature_parameters, remove_signature_parameters
1112

1213
try: # python 3.3+
@@ -167,11 +168,13 @@ def get_pytest_marks_on_case_func(f):
167168
"""Marker that can be used instead of a module name to indicate that the module is the current one"""
168169

169170

171+
@function_decorator
170172
def cases_fixture(cases=None, # type: Union[Callable[[Any], Any], Iterable[Callable[[Any], Any]]]
171173
module=None, # type: Union[ModuleType, Iterable[ModuleType]]
172174
case_data_argname='case_data', # type: str
173175
has_tag=None, # type: Any
174176
filter=None, # type: Callable[[List[Any]], bool]
177+
f=DECORATED,
175178
**kwargs
176179
):
177180
"""
@@ -239,23 +242,22 @@ def foo_fixture(request):
239242
`module`. It both `has_tag` and `filter` are set, both will be applied in sequence.
240243
:return:
241244
"""
242-
def _double_decorator(f):
243-
# apply @cases_data (that will translate to a @pytest.mark.parametrize)
244-
parametrized_f = cases_data(cases=cases, module=module,
245-
case_data_argname=case_data_argname, has_tag=has_tag, filter=filter)(f)
246-
# apply @pytest_fixture_plus
247-
return pytest_fixture_plus(**kwargs)(parametrized_f)
248-
249-
return _double_decorator
245+
# apply @cases_data (that will translate to a @pytest.mark.parametrize)
246+
parametrized_f = cases_data(cases=cases, module=module,
247+
case_data_argname=case_data_argname, has_tag=has_tag, filter=filter)(f)
248+
# apply @pytest_fixture_plus
249+
return pytest_fixture_plus(**kwargs)(parametrized_f)
250250

251251

252+
@function_decorator
252253
def pytest_fixture_plus(scope="function",
253254
params=None,
254255
autouse=False,
255256
ids=None,
256257
name=None,
258+
fixture_func=DECORATED,
257259
**kwargs):
258-
""" (return a) decorator to mark a fixture factory function.
260+
""" decorator to mark a fixture factory function.
259261
260262
Identical to `@pytest.fixture` decorator, except that it supports multi-parametrization with
261263
`@pytest.mark.parametrize` as requested in https://github.com/pytest-dev/pytest/issues/3960.
@@ -280,52 +282,6 @@ def pytest_fixture_plus(scope="function",
280282
``@pytest.fixture(name='<fixturename>')``.
281283
:param kwargs: other keyword arguments for `@pytest.fixture`
282284
"""
283-
284-
if callable(scope) and params is None and autouse is False:
285-
# direct decoration without arguments
286-
return decorate_pytest_fixture_plus(scope)
287-
else:
288-
# arguments have been provided
289-
def _decorator(f):
290-
return decorate_pytest_fixture_plus(f,
291-
scope=scope, params=params, autouse=autouse, ids=ids, name=name,
292-
**kwargs)
293-
return _decorator
294-
295-
296-
def decorate_pytest_fixture_plus(fixture_func,
297-
scope="function",
298-
params=None,
299-
autouse=False,
300-
ids=None,
301-
name=None,
302-
**kwargs):
303-
"""
304-
Manual decorator equivalent to `@pytest_fixture_plus`
305-
306-
:param fixture_func: the function to decorate
307-
308-
:param scope: the scope for which this fixture is shared, one of
309-
"function" (default), "class", "module" or "session".
310-
:param params: an optional list of parameters which will cause multiple
311-
invocations of the fixture function and all of the tests
312-
using it.
313-
:param autouse: if True, the fixture func is activated for all tests that
314-
can see it. If False (the default) then an explicit
315-
reference is needed to activate the fixture.
316-
:param ids: list of string ids each corresponding to the params
317-
so that they are part of the test id. If no ids are provided
318-
they will be generated automatically from the params.
319-
:param name: the name of the fixture. This defaults to the name of the
320-
decorated function. If a fixture is used in the same module in
321-
which it is defined, the function name of the fixture will be
322-
shadowed by the function arg that requests the fixture; one way
323-
to resolve this is to name the decorated function
324-
``fixture_<fixturename>`` and then use
325-
``@pytest.fixture(name='<fixturename>')``.
326-
:param kwargs:
327-
:return:
328-
"""
329285
# Compatibility for the 'name' argument
330286
if LooseVersion(pytest.__version__) >= LooseVersion('3.0.0'):
331287
# pytest version supports "name" keyword argument
@@ -436,11 +392,13 @@ def wrapped_fixture_func(*args, **kwargs):
436392
return fixture_decorator(wrapped_fixture_func)
437393

438394

395+
@function_decorator(custom_disambiguator=with_parenthesis)
439396
def cases_data(cases=None, # type: Union[Callable[[Any], Any], Iterable[Callable[[Any], Any]]]
440397
module=None, # type: Union[ModuleType, Iterable[ModuleType]]
441398
case_data_argname='case_data', # type: str
442399
has_tag=None, # type: Any
443-
filter=None # type: Callable[[List[Any]], bool]
400+
filter=None, # type: Callable[[List[Any]], bool]
401+
test_func=DECORATED,
444402
):
445403
"""
446404
Decorates a test function so as to automatically parametrize it with all cases listed in module `module`, or with
@@ -496,28 +454,18 @@ def test_foo(case_data: CaseData):
496454
`module`. It both `has_tag` and `filter` are set, both will be applied in sequence.
497455
:return:
498456
"""
499-
def datasets_decorator(test_func):
500-
"""
501-
The generated test function decorator.
502-
503-
It is equivalent to @mark.parametrize('case_data', cases) where cases is a tuple containing a CaseDataGetter for
504-
all case generator functions
457+
# equivalent to @mark.parametrize('case_data', cases) where cases is a tuple containing a CaseDataGetter for
505458

506-
:param test_func:
507-
:return:
508-
"""
509-
# First list all cases according to user preferences
510-
_cases = get_all_cases(cases, module, test_func, has_tag, filter)
459+
# First list all cases according to user preferences
460+
_cases = get_all_cases(cases, module, test_func, has_tag, filter)
511461

512-
# Then transform into required arguments for pytest (applying the pytest marks if needed)
513-
marked_cases, cases_ids = get_pytest_parametrize_args(_cases)
462+
# Then transform into required arguments for pytest (applying the pytest marks if needed)
463+
marked_cases, cases_ids = get_pytest_parametrize_args(_cases)
514464

515-
# Finally create the pytest decorator and apply it
516-
parametrizer = pytest.mark.parametrize(case_data_argname, marked_cases, ids=cases_ids)
465+
# Finally create the pytest decorator and apply it
466+
parametrizer = pytest.mark.parametrize(case_data_argname, marked_cases, ids=cases_ids)
517467

518-
return parametrizer(test_func)
519-
520-
return datasets_decorator
468+
return parametrizer(test_func)
521469

522470

523471
def get_pytest_parametrize_args(cases):
@@ -542,7 +490,6 @@ def get_pytest_parametrize_args(cases):
542490
return marked_cases, case_ids
543491

544492

545-
546493
# Compatibility for the way we put marks on single parameters in the list passed to @pytest.mark.parametrize
547494
# see https://docs.pytest.org/en/3.3.0/skipping.html?highlight=mark%20parametrize#skip-xfail-with-parametrize
548495

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
here = path.abspath(path.dirname(__file__))
1212

1313
# *************** Dependencies *********
14-
INSTALL_REQUIRES = ['makefun', 'functools32;python_version<"3.2"', 'funcsigs;python_version<"3.3"']
14+
INSTALL_REQUIRES = ['decopatch', 'makefun', 'functools32;python_version<"3.2"', 'funcsigs;python_version<"3.3"']
1515
DEPENDENCY_LINKS = []
1616
SETUP_REQUIRES = ['pytest-runner', 'setuptools_scm', 'pypandoc', 'pandoc']
1717
TESTS_REQUIRE = ['pytest', 'pytest-logging', 'pytest-cov', 'pytest-steps']

0 commit comments

Comments
 (0)