Skip to content

Commit 935b872

Browse files
author
Sylvain MARIE
committed
Improved filtering capabilities: new glob argument, and filter callables now receive the case function. Also, the case info located at c._pytestcase is now always present, with an id even if all the defaults are used.
`get_pytest_parametrize_args` renamed `get_parametrize_args` now that returned arguments are all lazy values or fixture refs and therefore can't work with pytest parametrize.
1 parent 2fbb150 commit 935b872

File tree

3 files changed

+182
-107
lines changed

3 files changed

+182
-107
lines changed

pytest_cases/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
get_pytest_parametrize_args_legacy, cases_fixture
1515

1616
from .case_funcs_new import case, CaseInfo
17-
from .case_parametrizer_new import parametrize_with_cases, THIS_MODULE, get_all_cases, get_pytest_parametrize_args
17+
from .case_parametrizer_new import parametrize_with_cases, THIS_MODULE, get_all_cases, get_parametrize_args
1818

1919
try:
2020
# -- Distribution mode --
@@ -56,7 +56,7 @@
5656
# case functions
5757
'case', 'CaseInfo', 'get_all_cases',
5858
# test functions
59-
'parametrize_with_cases', 'THIS_MODULE', 'get_pytest_parametrize_args'
59+
'parametrize_with_cases', 'THIS_MODULE', 'get_parametrize_args'
6060
]
6161

6262
try: # python 3.5+ type hints

pytest_cases/case_funcs_new.py

Lines changed: 79 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,30 @@ def __init__(self,
3737
self.add_tags(tags)
3838

3939
@classmethod
40-
def get_from(cls, case_func, create=False):
41-
"""return the CaseInfo associated with case_fun ; create it and attach it if needed and required"""
40+
def get_from(cls, case_func, create=False, prefix_for_ids='case_'):
41+
"""
42+
Returns the CaseInfo associated with case_fun ; creates it and attaches it if needed and required.
43+
If not present, a case id is automatically created from the function name based on the collection prefix.
44+
45+
:param case_func:
46+
:param create:
47+
:param prefix_for_ids:
48+
:return:
49+
"""
4250
case_info = getattr(case_func, CASE_FIELD, None)
43-
if case_info is None and create:
44-
case_info = CaseInfo()
45-
case_info.attach_to(case_func)
51+
52+
if create:
53+
if case_info is None:
54+
case_info = CaseInfo()
55+
case_info.attach_to(case_func)
56+
57+
if case_info.id is None:
58+
# default test id from function name
59+
if case_func.__name__.startswith(prefix_for_ids):
60+
case_info.id = case_func.__name__[len(prefix_for_ids):]
61+
else:
62+
case_info.id = case_func.__name__
63+
4664
return case_info
4765

4866
def attach_to(self,
@@ -63,23 +81,21 @@ def add_tags(self,
6381
self.tags += tuple(tags)
6482

6583
def matches_tag_query(self,
66-
has_tag=None, # type: Any
67-
filter=None # type: Callable[[Iterable[Any]], bool] # noqa
84+
has_tag=None, # type: Union[str, Iterable[str]]
6885
):
6986
"""
7087
Returns True if the case function with this case_info is selected by the query
7188
7289
:param has_tag:
73-
:param filter: a callable
7490
:return:
7591
"""
76-
selected = True # by default select the case, then AND the conditions
77-
if has_tag is not None:
78-
selected = selected and (has_tag in self.tags)
79-
if filter is not None:
80-
selected = selected and filter(self.tags)
92+
if has_tag is None:
93+
return True
94+
95+
if not isinstance(has_tag, (tuple, list, set)):
96+
has_tag = (has_tag,)
8197

82-
return selected
98+
return all(t in self.tags for t in has_tag)
8399

84100
@classmethod
85101
def copy_info(cls, from_case_func, to_case_func):
@@ -90,50 +106,75 @@ def copy_info(cls, from_case_func, to_case_func):
90106

91107

92108
def matches_tag_query(case_fun,
93-
has_tag=None, # type: Any
94-
filter=None # type: Callable[[Iterable[Any]], bool] # noqa
109+
has_tag=None, # type: Union[str, Iterable[str]]
110+
filter=None, # type: Union[Callable[[Iterable[Any]], bool], Iterable[Callable[[Iterable[Any]], bool]]] # noqa
95111
):
96112
"""
97-
Returns True if the case function is selected by the query
113+
Returns True if the case function is selected by the query:
114+
115+
- if `has_tag` contains one or several tags, they should ALL be present in the tags
116+
set on `case_fun` (`case_fun._pytestcase.tags`)
117+
118+
- if `filter` contains one or several filter callables, they are all called in sequence and the
119+
case_fun is only selected if ALL of them return a True truth value
98120
99121
:param case_fun:
100122
:param has_tag:
101123
:param filter:
102-
:return:
124+
:return: True if the case_fun is selected by the query.
103125
"""
104-
# no query = match
105-
if has_tag is None and filter is None:
106-
return True
107-
108-
# query = first get info
109-
case_info = CaseInfo.get_from(case_fun)
110-
if not case_info:
111-
# no tags: do the test on an empty case info
112-
return CaseInfo().matches_tag_query(has_tag, filter)
113-
else:
114-
return case_info.matches_tag_query(has_tag, filter)
126+
selected = True
127+
128+
# query on tags
129+
if has_tag is not None:
130+
selected = selected and CaseInfo.get_from(case_fun).matches_tag_query(has_tag)
131+
132+
# filter function
133+
if filter is not None:
134+
if not isinstance(filter, (tuple, set, list)):
135+
filter = (filter,)
136+
137+
for _filter in filter:
138+
# break if already unselected
139+
if not selected:
140+
return selected
141+
142+
# try next filter
143+
try:
144+
res = _filter(case_fun)
145+
# keep this in the try catch in case there is an issue with the truth value of result
146+
selected = selected and res
147+
except: # noqa
148+
# any error leads to a no-match
149+
selected = False
150+
151+
return selected
115152

116153

117154
@function_decorator
118155
def case(id=None, # type: str # noqa
119156
tags=None, # type: Union[Any, Iterable[Any]]
120-
# lru_cache=False, # type: bool
121157
marks=(), # type: Union[MarkDecorator, Iterable[MarkDecorator]]
122158
case_func=DECORATED # noqa
123159
):
124160
"""
161+
Optional decorator for case functions so as to customize some information.
162+
163+
```python
164+
@case(id='hey')
165+
def case_hi():
166+
return 1
167+
```
125168
126169
:param id: the custom pytest id that should be used when this case is active. Replaces the deprecated `@case_name`
127-
:param tags: custom tags to be used for filtering. Replaces the deprecated `@case_tags`
128-
:param lru_cache:
129-
:param marks:
170+
decorator from v1. If no id is provided, the id is generated from case functions by removing their prefix,
171+
see `@parametrize_with_cases(prefix='case_')`.
172+
:param tags: custom tags to be used for filtering in `@parametrize_with_cases(has_tags)`. Replaces the deprecated
173+
`@case_tags` and `@target` decorators.
174+
:param marks: optional pytest marks to add on the case. Note that decorating the function directly with the mark
175+
also works, and if marks are provided in both places they are merged.
130176
:return:
131177
"""
132-
# if lru_cache:
133-
# nb_cases = 1 # TODO change when fixture dependencies are taken into account. What about creating a dedicated independent cache decorator pytest goodie ?
134-
# # decorate the function with the appropriate lru cache size
135-
# case_func = lru(maxsize=nb_cases)(case_func)
136-
137178
case_info = CaseInfo(id, marks, tags)
138179
case_info.attach_to(case_func)
139180
return case_func
@@ -175,7 +216,5 @@ def is_case_function(f, prefix=CASE_PREFIX_FUN, check_prefix=True):
175216
return False
176217
elif safe_isclass(f):
177218
return False
178-
elif hasattr(f, CASE_FIELD):
179-
return True
180219
else:
181220
return f.__name__.startswith(prefix) if check_prefix else True

0 commit comments

Comments
 (0)