From 6437c8850ecd040a49a86acfebcd1e6900b2fccf Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Thu, 8 May 2025 08:16:25 +0200 Subject: [PATCH 01/10] feat: add with_case_tags decorator As proposed in #351. --- src/pytest_cases/case_funcs.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/pytest_cases/case_funcs.py b/src/pytest_cases/case_funcs.py index 02dbcafb..b2cf1ab4 100644 --- a/src/pytest_cases/case_funcs.py +++ b/src/pytest_cases/case_funcs.py @@ -366,3 +366,30 @@ def is_case_function(f, # type: Any except: # GH#287: safe fallback return False + + +def with_case_tags(*tags): + """Attach `tags` to all cases defined in the decorated class.""" + def _decorator(cls): + if is_case_function(cls): + raise ValueError( + 'Cannot use with_case_tags on a case ' + 'function. Use the @case decorator instead.' + ) + if not is_case_class(cls): + raise ValueError('with_case_tags can only be applied to classes ' + 'defining a collection of cases.') + for case_name in dir(cls): + case_ = getattr(cls, case_name) + if not is_case_function(case_): # Not a case + continue + try: + case_info = getattr(case_, CASE_FIELD) + except AttributeError: + # Not explicitly decorated with @case. Do so now. + case_ = case(case_) + case_info = getattr(case_, CASE_FIELD) + tags_to_add = tuple(t for t in tags if t not in case_info.tags) + case_info.add_tags(tags_to_add) + return cls + return _decorator From 58518837984547a2c9d274f60008a74605fa4721 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Thu, 8 May 2025 08:27:08 +0200 Subject: [PATCH 02/10] 3.8.6 changelog Document addition of with_case_tags class decorator. --- docs/changelog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 269fd663..00936a91 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # Changelog +### 3.8.6 - Add with_case_tags decorator +- Added the `with_case_tags` decorator for applying common tags to all cases + defined in a case class. Fixes [#351](https://github.com/smarie/python-pytest-cases/issues/351). + PR [#361](https://github.com/smarie/python-pytest-cases/issues/361) + by [@michele-riva](https://github.com/michele-riva). + ### 3.8.6 - compatibility fix - Fixed issue with legacy python 2.7 and 3.5. Fixes [#352](https://github.com/smarie/python-pytest-cases/issues/352). From c4b088ea7b76599a7d4f74bb72467e22a9a4653a Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Thu, 8 May 2025 08:28:24 +0200 Subject: [PATCH 03/10] feat: make with_case_tags public Can be imported as ```python from pytest_cases import with_case_tags ``` --- src/pytest_cases/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytest_cases/__init__.py b/src/pytest_cases/__init__.py index 80416ddc..01bcb8c6 100644 --- a/src/pytest_cases/__init__.py +++ b/src/pytest_cases/__init__.py @@ -10,7 +10,7 @@ from .fixture_parametrize_plus import pytest_parametrize_plus, parametrize_plus, parametrize, fixture_ref from .case_funcs import case, copy_case_info, set_case_id, get_case_id, get_case_marks, \ - get_case_tags, matches_tag_query, is_case_class, is_case_function + get_case_tags, matches_tag_query, is_case_class, is_case_function, with_case_tags from .case_parametrizer_new import parametrize_with_cases, THIS_MODULE, get_all_cases, get_parametrize_args, \ get_current_case_id, get_current_cases, get_current_params, CasesCollectionWarning From fb77df13cd718fc9d1cb250b85b15483fad33c97 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Thu, 8 May 2025 08:37:56 +0200 Subject: [PATCH 04/10] Document with_case_tags decorator --- docs/api_reference.md | 51 +++++++++++++++++++++++++++++++++++++++++++ docs/index.md | 2 +- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/docs/api_reference.md b/docs/api_reference.md index 3a2f5db8..d548cd73 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -197,6 +197,57 @@ Returns True if the provided object is a function or callable and, if `check_pre - `check_prefix`: if this boolean is True (default), the prefix will be checked. If False, any function will lead to a `True` result whatever its name. + +### `@with_case_tags` + +```python +@with_case_tags(*tags, # type: Any + ): +``` + +This decorator can be applied to a class defining cases to apply multiple +`*tags` to all case methods defined thereby. + +```python +@with_case_tags('tag_1', 'tag_2') +class CasesContainerClass: + + def case_one(self, ...): + ... + + @case(tags='another_tag') + def case_two(self, ...): + ... + + @case(tags='tag_1') + def case_three(self, ...): + ... +``` + +This is equivalent to: + + +```python +class CasesContainerClass: + + @case(tags=('tag_1', 'tag_2')) + def case_one(self, ...): + ... + + @case(tags=('another_tag', 'tag_1', 'tag_2')) + def case_two(self, ...): + ... + + @case(tags=('tag_1', 'tag_2')) + def case_three(self, ...): + ... +``` + +**Parameters:** + + - `tags`: custom tags to be added to all case methods. See also [`@case(tags=...)`](#case). + + ### The `filters` submodule This submodule contains symbols to help you create filters for `@parametrize_with_cases(filter=...)`. diff --git a/docs/index.md b/docs/index.md index deb352a5..32774007 100644 --- a/docs/index.md +++ b/docs/index.md @@ -269,7 +269,7 @@ def test_bad_datasets(data, err_type, err_msg): ``` - - 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). + - 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. ```python From 5c420a67b33f942bd8a565aa3ea0b4fa580e3641 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Fri, 9 May 2025 16:02:38 +0200 Subject: [PATCH 05/10] Bump changelog version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sylvain Marié --- docs/changelog.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 00936a91..6e0a62df 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,9 +1,10 @@ # Changelog -### 3.8.6 - Add with_case_tags decorator +### 3.9.0 - (in progress) New `with_case_tags` decorator + - Added the `with_case_tags` decorator for applying common tags to all cases defined in a case class. Fixes [#351](https://github.com/smarie/python-pytest-cases/issues/351). - PR [#361](https://github.com/smarie/python-pytest-cases/issues/361) + PR [#361](https://github.com/smarie/python-pytest-cases/pull/361) by [@michele-riva](https://github.com/michele-riva). ### 3.8.6 - compatibility fix From c205a7cf741d3ca5d1e43c3d7b533d30c7a16f3d Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Fri, 9 May 2025 16:05:26 +0200 Subject: [PATCH 06/10] chore: backticks in exception messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sylvain Marié --- src/pytest_cases/case_funcs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pytest_cases/case_funcs.py b/src/pytest_cases/case_funcs.py index b2cf1ab4..99d0a851 100644 --- a/src/pytest_cases/case_funcs.py +++ b/src/pytest_cases/case_funcs.py @@ -373,11 +373,11 @@ def with_case_tags(*tags): def _decorator(cls): if is_case_function(cls): raise ValueError( - 'Cannot use with_case_tags on a case ' - 'function. Use the @case decorator instead.' + 'Cannot use `with_case_tags` on a case ' + 'function. Use the `@case` decorator instead.' ) if not is_case_class(cls): - raise ValueError('with_case_tags can only be applied to classes ' + raise ValueError('`with_case_tags` can only be applied to classes ' 'defining a collection of cases.') for case_name in dir(cls): case_ = getattr(cls, case_name) From 31f425ccbe96e6d1c52894e057fa02c84a2687f3 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Fri, 9 May 2025 16:06:26 +0200 Subject: [PATCH 07/10] Add missing final newline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sylvain Marié --- src/pytest_cases/case_funcs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pytest_cases/case_funcs.py b/src/pytest_cases/case_funcs.py index 99d0a851..475a7391 100644 --- a/src/pytest_cases/case_funcs.py +++ b/src/pytest_cases/case_funcs.py @@ -393,3 +393,4 @@ def _decorator(cls): case_info.add_tags(tags_to_add) return cls return _decorator + From 7ff68dd53b3945c0968fd7c80253e0936191088f Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Sun, 11 May 2025 04:57:59 +0200 Subject: [PATCH 08/10] doc: Move @with_case_tags next to @case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Sylvain Marié --- docs/api_reference.md | 102 +++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/docs/api_reference.md b/docs/api_reference.md index d548cd73..a8800ddf 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -51,6 +51,57 @@ def case_hi(): - `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. +### `@with_case_tags` + +```python +@with_case_tags(*tags, # type: Any + ): +``` + +This decorator can be applied to a class defining cases to apply multiple +`*tags` to all case methods defined thereby. + +```python +@with_case_tags('tag_1', 'tag_2') +class CasesContainerClass: + + def case_one(self, ...): + ... + + @case(tags='another_tag') + def case_two(self, ...): + ... + + @case(tags='tag_1') + def case_three(self, ...): + ... +``` + +This is equivalent to: + + +```python +class CasesContainerClass: + + @case(tags=('tag_1', 'tag_2')) + def case_one(self, ...): + ... + + @case(tags=('another_tag', 'tag_1', 'tag_2')) + def case_two(self, ...): + ... + + @case(tags=('tag_1', 'tag_2')) + def case_three(self, ...): + ... +``` + +**Parameters:** + + - `tags`: custom tags to be added to all case methods. See also [`@case(tags=...)`](#case). + + + ### `copy_case_info` ```python @@ -197,57 +248,6 @@ Returns True if the provided object is a function or callable and, if `check_pre - `check_prefix`: if this boolean is True (default), the prefix will be checked. If False, any function will lead to a `True` result whatever its name. - -### `@with_case_tags` - -```python -@with_case_tags(*tags, # type: Any - ): -``` - -This decorator can be applied to a class defining cases to apply multiple -`*tags` to all case methods defined thereby. - -```python -@with_case_tags('tag_1', 'tag_2') -class CasesContainerClass: - - def case_one(self, ...): - ... - - @case(tags='another_tag') - def case_two(self, ...): - ... - - @case(tags='tag_1') - def case_three(self, ...): - ... -``` - -This is equivalent to: - - -```python -class CasesContainerClass: - - @case(tags=('tag_1', 'tag_2')) - def case_one(self, ...): - ... - - @case(tags=('another_tag', 'tag_1', 'tag_2')) - def case_two(self, ...): - ... - - @case(tags=('tag_1', 'tag_2')) - def case_three(self, ...): - ... -``` - -**Parameters:** - - - `tags`: custom tags to be added to all case methods. See also [`@case(tags=...)`](#case). - - ### The `filters` submodule This submodule contains symbols to help you create filters for `@parametrize_with_cases(filter=...)`. From 24468d400e28b8822e48ed0ec44d8051a0795496 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Sun, 11 May 2025 05:38:42 +0200 Subject: [PATCH 09/10] Add `with_case_tags` to `__all__` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Sylvain Marié --- src/pytest_cases/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pytest_cases/__init__.py b/src/pytest_cases/__init__.py index 01bcb8c6..675c9af6 100644 --- a/src/pytest_cases/__init__.py +++ b/src/pytest_cases/__init__.py @@ -53,6 +53,7 @@ # case functions 'case', 'copy_case_info', 'set_case_id', 'get_case_id', 'get_case_marks', 'get_case_tags', 'matches_tag_query', 'is_case_class', 'is_case_function', + 'with_case_tags', # test functions 'get_all_cases', 'parametrize_with_cases', 'THIS_MODULE', 'get_parametrize_args', 'get_current_case_id', 'get_current_cases', 'get_current_params', 'CasesCollectionWarning' From 6b6259ba8223715b1cded58a0dbe2862a9e29716 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Thu, 15 May 2025 04:26:24 +0200 Subject: [PATCH 10/10] Comment on current behavior of @case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As suggested by @smarie in https://github.com/smarie/python-pytest-cases/pull/361#discussion_r2081761274 Co-Authored-By: Sylvain Marié --- src/pytest_cases/case_funcs.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pytest_cases/case_funcs.py b/src/pytest_cases/case_funcs.py index 475a7391..54a1ae59 100644 --- a/src/pytest_cases/case_funcs.py +++ b/src/pytest_cases/case_funcs.py @@ -387,7 +387,11 @@ def _decorator(cls): case_info = getattr(case_, CASE_FIELD) except AttributeError: # Not explicitly decorated with @case. Do so now. - case_ = case(case_) + # NB: `case(obj) is obj`, i.e., the `@case` decorator + # only adds some attributes to `obj`. In the future, if + # `@case` will return a different object, we will have + # to `setattr(cls, case_name, case_mod)` + _ = case(case_) case_info = getattr(case_, CASE_FIELD) tags_to_add = tuple(t for t in tags if t not in case_info.tags) case_info.add_tags(tags_to_add)