Skip to content

Commit f030486

Browse files
michele-rivasmarie
andauthored
Add with_case_tags decorator (#361)
* feat: add with_case_tags decorator As proposed in #351. * 3.8.6 changelog Document addition of with_case_tags class decorator. * feat: make with_case_tags public Can be imported as ```python from pytest_cases import with_case_tags ``` * Document with_case_tags decorator * Bump changelog version Co-authored-by: Sylvain Marié <sylvain.marie@schneider-electric.com> * chore: backticks in exception messages Co-authored-by: Sylvain Marié <sylvain.marie@schneider-electric.com> * Add missing final newline Co-authored-by: Sylvain Marié <sylvain.marie@schneider-electric.com> * doc: Move @with_case_tags next to @case Co-Authored-By: Sylvain Marié <sylvain.marie@schneider-electric.com> * Add `with_case_tags` to `__all__` Co-Authored-By: Sylvain Marié <sylvain.marie@schneider-electric.com> * Comment on current behavior of @case As suggested by @smarie in #361 (comment) Co-Authored-By: Sylvain Marié <sylvain.marie@schneider-electric.com> --------- Co-authored-by: Sylvain Marié <sylvain.marie@schneider-electric.com>
1 parent 544f9a5 commit f030486

File tree

5 files changed

+97
-2
lines changed

5 files changed

+97
-2
lines changed

docs/api_reference.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,57 @@ def case_hi():
5151
- `marks`: optional pytest marks to add on the case. Note that decorating the function directly with the mark also works, and if marks are provided in both places they are merged.
5252

5353

54+
### `@with_case_tags`
55+
56+
```python
57+
@with_case_tags(*tags, # type: Any
58+
):
59+
```
60+
61+
This decorator can be applied to a class defining cases to apply multiple
62+
`*tags` to all case methods defined thereby.
63+
64+
```python
65+
@with_case_tags('tag_1', 'tag_2')
66+
class CasesContainerClass:
67+
68+
def case_one(self, ...):
69+
...
70+
71+
@case(tags='another_tag')
72+
def case_two(self, ...):
73+
...
74+
75+
@case(tags='tag_1')
76+
def case_three(self, ...):
77+
...
78+
```
79+
80+
This is equivalent to:
81+
82+
83+
```python
84+
class CasesContainerClass:
85+
86+
@case(tags=('tag_1', 'tag_2'))
87+
def case_one(self, ...):
88+
...
89+
90+
@case(tags=('another_tag', 'tag_1', 'tag_2'))
91+
def case_two(self, ...):
92+
...
93+
94+
@case(tags=('tag_1', 'tag_2'))
95+
def case_three(self, ...):
96+
...
97+
```
98+
99+
**Parameters:**
100+
101+
- `tags`: custom tags to be added to all case methods. See also [`@case(tags=...)`](#case).
102+
103+
104+
54105
### `copy_case_info`
55106

56107
```python

docs/changelog.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
### 3.10.0 - (in progress) New `with_case_tags` decorator
4+
5+
- Added the `with_case_tags` decorator for applying common tags to all cases
6+
defined in a case class. Fixes [#351](https://github.com/smarie/python-pytest-cases/issues/351).
7+
PR [#361](https://github.com/smarie/python-pytest-cases/pull/361)
8+
by [@michele-riva](https://github.com/michele-riva).
9+
310
### 3.9.1 - support for python 3.14 and pytest 8.4
411

512
- Fixed `AttributeError: 'MiniMetafunc' object has no attribute '_params_directness'` when a case function is
@@ -12,6 +19,10 @@
1219
[#186](https://github.com/smarie/python-pytest-cases/issues/186)
1320
- Fixed test suite for python 3.14, officializing the support for this version.
1421

22+
### 3.9.0 - yanked version
23+
24+
This version was yanked. See 3.9.1.
25+
1526
### 3.8.6 - compatibility fix
1627

1728
- Fixed issue with legacy python 2.7 and 3.5. Fixes [#352](https://github.com/smarie/python-pytest-cases/issues/352).

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ def test_bad_datasets(data, err_type, err_msg):
269269
```
270270

271271

272-
- the `has_tag` argument allows you to filter cases based on tags set on case functions using the `@case` decorator. See API reference of [`@case`](./api_reference.md#case) and [`@parametrize_with_cases`](./api_reference.md#parametrize_with_cases).
272+
- the `has_tag` argument allows you to filter cases based on tags set on case functions using the `@case` decorator. See API reference of [`@case`](./api_reference.md#case) and [`@parametrize_with_cases`](./api_reference.md#parametrize_with_cases). Tags shared by multiple cases grouped inside a class may be added automatically to all cases using the [`@with_case_tags`](./api_reference.md#with_case_tags) decorator.
273273

274274

275275
```python

src/pytest_cases/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .fixture_parametrize_plus import pytest_parametrize_plus, parametrize_plus, parametrize, fixture_ref
1111

1212
from .case_funcs import case, copy_case_info, set_case_id, get_case_id, get_case_marks, \
13-
get_case_tags, matches_tag_query, is_case_class, is_case_function
13+
get_case_tags, matches_tag_query, is_case_class, is_case_function, with_case_tags
1414
from .case_parametrizer_new import parametrize_with_cases, THIS_MODULE, get_all_cases, get_parametrize_args, \
1515
get_current_case_id, get_current_cases, get_current_params, CasesCollectionWarning
1616

@@ -53,6 +53,7 @@
5353
# case functions
5454
'case', 'copy_case_info', 'set_case_id', 'get_case_id', 'get_case_marks',
5555
'get_case_tags', 'matches_tag_query', 'is_case_class', 'is_case_function',
56+
'with_case_tags',
5657
# test functions
5758
'get_all_cases', 'parametrize_with_cases', 'THIS_MODULE', 'get_parametrize_args', 'get_current_case_id',
5859
'get_current_cases', 'get_current_params', 'CasesCollectionWarning'

src/pytest_cases/case_funcs.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,3 +366,35 @@ def is_case_function(f, # type: Any
366366
except:
367367
# GH#287: safe fallback
368368
return False
369+
370+
371+
def with_case_tags(*tags):
372+
"""Attach `tags` to all cases defined in the decorated class."""
373+
def _decorator(cls):
374+
if is_case_function(cls):
375+
raise ValueError(
376+
'Cannot use `with_case_tags` on a case '
377+
'function. Use the `@case` decorator instead.'
378+
)
379+
if not is_case_class(cls):
380+
raise ValueError('`with_case_tags` can only be applied to classes '
381+
'defining a collection of cases.')
382+
for case_name in dir(cls):
383+
case_ = getattr(cls, case_name)
384+
if not is_case_function(case_): # Not a case
385+
continue
386+
try:
387+
case_info = getattr(case_, CASE_FIELD)
388+
except AttributeError:
389+
# Not explicitly decorated with @case. Do so now.
390+
# NB: `case(obj) is obj`, i.e., the `@case` decorator
391+
# only adds some attributes to `obj`. In the future, if
392+
# `@case` will return a different object, we will have
393+
# to `setattr(cls, case_name, case_mod)`
394+
_ = case(case_)
395+
case_info = getattr(case_, CASE_FIELD)
396+
tags_to_add = tuple(t for t in tags if t not in case_info.tags)
397+
case_info.add_tags(tags_to_add)
398+
return cls
399+
return _decorator
400+

0 commit comments

Comments
 (0)