Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add: get_all_cases extended to support filtering and use other modules as parametrization_target #260

Merged
merged 5 commits into from Mar 21, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 14 additions & 3 deletions docs/api_reference.md
Expand Up @@ -280,7 +280,7 @@ Note that `@parametrize_with_cases` collection and parameter creation steps are

```python
# Collect all cases
cases_funs = get_all_cases(f, cases=cases, prefix=prefix,
cases_funs = get_all_cases(f, cases=cases, prefix=prefix,
glob=glob, has_tag=has_tag, filter=filter)

# Transform the various functions found
Expand Down Expand Up @@ -335,16 +335,27 @@ Note that you can get the same contents directly by using the [`current_cases`](
### `get_all_cases`

```python
def get_all_cases(parametrization_target: Callable,
def get_all_cases(parametrization_target: Callable = None,
cases: Union[Callable, Type, ModuleRef] = None,
prefix: str = 'case_',
glob: str = None,
has_tag: Union[str, Iterable[str]] = None,
filter: Callable[[Callable], bool] = None
) -> List[Callable]:
```
Collect all cases as used with [`@parametrize_with_cases`](#parametrize_with_cases). See [`@parametrize_with_cases`](#parametrize_with_cases) for details on the parameters.

Lists all desired cases for a given `parametrization_target` (a test function or a fixture). This function may be convenient for debugging purposes. See [`@parametrize_with_cases`](#parametrize_with_cases) for details on the parameters.
This can be used to lists all desired cases for a given `parametrization_target` (a test function or a fixture) which may be convenient for debugging purposes.

- If `cases` is `AUTO` or contains a string module reference, `parametrization_target` must be provided.

```python
# Without a parametrization target
cases = get_all_cases(cases=[case_1, case_2, case_3], has_tag=["banana"])

# With a parametrization target
cases = get_all_cases(f, cases=".", has_tag=["banana"])
```


### `get_parametrize_args`
Expand Down
28 changes: 19 additions & 9 deletions src/pytest_cases/case_parametrizer_new.py
Expand Up @@ -205,19 +205,20 @@ def _glob_name_filter(case_fun):
return _glob_name_filter


def get_all_cases(parametrization_target, # type: Callable
cases=None, # type: Union[Callable, Type, ModuleRef]
prefix=CASE_PREFIX_FUN, # type: str
glob=None, # type: str
has_tag=None, # type: Union[str, Iterable[str]]
filter=None # type: Callable[[Callable], bool] # noqa
def get_all_cases(parametrization_target=None, # type: Callable
cases=None, # type: Union[Callable, Type, ModuleRef]
prefix=CASE_PREFIX_FUN, # type: str
glob=None, # type: str
has_tag=None, # type: Union[str, Iterable[str]]
filter=None # type: Callable[[Callable], bool] # noqa
):
# type: (...) -> List[Callable]
"""
Lists all desired cases for a given `parametrization_target` (a test function or a fixture). This function may be
convenient for debugging purposes. See `@parametrize_with_cases` for details on the parameters.

:param parametrization_target: a test function
:param parametrization_target: a test function to get the module reference from. Required for cases that
smarie marked this conversation as resolved.
Show resolved Hide resolved
rely on module reference.
:param cases: a case function, a class containing cases, a module or a module name string (relative module
names accepted). Or a list of such items. You may use `THIS_MODULE` or `'.'` to include current module.
`AUTO` (default) means that the module named `test_<name>_cases.py` will be loaded, where `test_<name>.py` is
Expand Down Expand Up @@ -265,9 +266,18 @@ def get_all_cases(parametrization_target, # type: Callable

filters += (filter,)

# Validate that we have a parametrization target if required for retrieving cases
if parametrize_with_cases is None:
eddiebergman marked this conversation as resolved.
Show resolved Hide resolved
if any(c is AUTO or c is THIS_MODULE or isinstance(c, str) for c in cases):
raise ValueError(
"Cases beginning with '.' or using AUTO require a parametrization target,"
" please use `get_all_cases(target_func, cases=...)`"
)

# parent package
caller_module_name = getattr(parametrization_target, '__module__', None)
parent_pkg_name = '.'.join(caller_module_name.split('.')[:-1]) if caller_module_name is not None else None
if parametrization_target is not None:
caller_module_name = getattr(parametrization_target, '__module__', None)
parent_pkg_name = '.'.join(caller_module_name.split('.')[:-1]) if caller_module_name is not None else None
Copy link
Owner

@smarie smarie Feb 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be merged with the above branch of the if ?

Also, if parametrization_target is not used at all below this point and only caller_module_name / parent_pkg_name are, we could imagine to accept both

  • None (new default as per your proposal)
  • a module name (in case you wish to pre-collect various cases from an entire module, in order to resolve the relative imports and AUTO mode). Note: I may be too optimistic here, maybe a module name is not enough. An alternative is to accept a module object
  • a parametrization target

Finally, when None is passed, we could rely on the get_caller_module utility function in order to still grab the calling module (from .fixture__creation import get_caller_module). This should only be done if actually needed (son, if a relative path or AUTO mode is used)

Does it make sense ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it could be merged, just made into an else statement. I was thinking to put it into the for loop but it made more sense to catch the error early to me.

I'm not sure I understand the bullet points but given the last statement of using get_caller_module, how does this behaviour sound

# The current behaviour
def get_all_cases(f, cases=...)


# The functionality implemeneted
def get_all_cases(cases=[case_1, ..., case_n])

# Change this from being an error to being valid.
# Would use `get_caller_module` so these args work as expected
def get_all_cases(cases='.')
def get_all_cases(cases=AUTO)
def get_all_cases(cases='.xyz')

With respect to:

a module name (in case you wish to pre-collect various cases from an entire module, in order to resolve the relative imports and AUTO mode). Note: I may be too optimistic here, maybe a module name is not enough. An alternative is to accept a module object

Would this functionality be solved with the cases='.' or cases=AUTO way of calling as illustrated in the last section of the code snippet above? If not, could you provide a code snippet of what args you would pass in and what the behaviour would be?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# Change this from being an error to being valid.
def get_all_cases(cases='.')
def get_all_cases(cases=AUTO)
def get_all_cases(cases='.xyz')

Would use get_caller_module so these args work as expected: YES

We would alternately accept an explicit module object (or name ?) to explicitly help resolve the caller module

import test_module

get_all_cases(test_module, cases=AUTO)

I am just thinking that if we only need a module to perform this action, then we should not need users to pass a dummy function. So either the module is implicit (from the call stack, get_caller_module), or it is explicit and therefore we would accept that parametrization_target (or a new param) receives it.

Maybe this can be done in a separate PR if it sounds too cumbersome. For now you can stick to the use case you had in mind (implicit or none).


# start collecting all cases
cases_funs = []
Expand Down
62 changes: 62 additions & 0 deletions tests/cases/issues/test_issue_258.py
@@ -0,0 +1,62 @@
# Authors: Eddie Bergmane <eddiebergmanehs@gmail.com>
# + All contributors to <https://github.com/smarie/python-pyfields>
#
# License: 3-clause BSD, <https://github.com/smarie/python-pyfields/blob/master/LICENSE>
#
smarie marked this conversation as resolved.
Show resolved Hide resolved
# Issue: https://github.com/smarie/python-pytest-cases/issues/258
from pytest_cases import get_all_cases, case, parametrize_with_cases


@case(tags=["a", "banana"])
def case_1():
return "a_banana"


@case(tags=["a"])
def case_2():
return "a"


@case(tags=["b", "banana"])
def case_3():
return "b_banana"


@case(tags=["b"])
def case_4():
return "b"


all_cases = get_all_cases(cases=[case_1, case_2, case_3, case_4])

a_cases = get_all_cases(cases=all_cases, has_tag="a")
b_cases = get_all_cases(cases=all_cases, has_tag="b")

banana_cases = get_all_cases(cases=a_cases + b_cases, has_tag=["banana"])


@parametrize_with_cases("word", cases=all_cases)
def test_all(word):
assert word in ["a", "a_banana", "b", "b_banana"]


@parametrize_with_cases("word", cases=a_cases)
def test_a(word):
assert "a" in word


@parametrize_with_cases("word", cases=b_cases)
def test_b(word):
assert "b" in word


@parametrize_with_cases("word", cases=banana_cases)
def test_banana(word):
assert "banana" in word


def test_get_cases_without_parametrization_target():
assert len(list(all_cases)) == 4
assert len(list(a_cases)) == 2
assert len(list(b_cases)) == 2
assert len(list(banana_cases)) == 2