Skip to content

Commit 64e2dbf

Browse files
author
Sylvain MARIE
committed
New in_cls argument in unpack_fixtures so that it can be used inside classes. Fixes #201
1 parent f05748b commit 64e2dbf

File tree

6 files changed

+115
-17
lines changed

6 files changed

+115
-17
lines changed

docs/api_reference.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ As a consequence it does not support the `params` and `ids` arguments anymore.
380380
- **scope**: the scope for which this fixture is shared, one of "function" (default), "class", "module" or "session".
381381
- **autouse**: if True, the fixture func is activated for all tests that can see it. If False (the default) then an explicitreference is needed to activate the fixture.
382382
- **name**: the name of the fixture. This defaults to the name of the decorated function. Note: If a fixture is used in the same module in which it is defined, the function name of the fixture will be shadowed by the function arg that requests the fixture; one wayto resolve this is to name the decorated function ``fixture_<fixturename>`` and then use ``@pytest.fixture(name='<fixturename>')``.
383-
- **unpack_into**: an optional iterable of names, or string containing coma-separated names, for additional fixtures to create to represent parts of this fixture. See `unpack_fixture` for details.
383+
- **unpack_into**: an optional iterable of names, or string containing coma-separated names, for additional fixtures to create to represent parts of this fixture. See [`unpack_fixture`](#unpack_fixture) for details.
384384
- **hook**: an optional hook to apply to each fixture function that is created during this call. The hook function will be called everytime a fixture is about to be created. It will receive a single argument (the function implementing the fixture) and should return the function to use. For example you can use `saved_fixture` from `pytest-harvest` as a hook in order to save all such created fixtures in the fixture store.
385385
- **kwargs**: other keyword arguments for `@pytest.fixture`
386386

@@ -389,8 +389,9 @@ As a consequence it does not support the `params` and `ids` arguments anymore.
389389
```python
390390
def unpack_fixture(argnames: str,
391391
fixture: Union[str, Callable],
392+
in_cls: bool = False,
392393
hook: Callable = None
393-
) -> Tuple[<Fixture>]
394+
) -> Tuple[<Fixture>, ...]
394395
```
395396

396397
Creates several fixtures with names `argnames` from the source `fixture`. Created fixtures will correspond to elements unpacked from `fixture` in order. For example if `fixture` is a tuple of length 2, `argnames="a,b"` will create two fixtures containing the first and second element respectively.
@@ -412,10 +413,29 @@ def test_function(a, b):
412413
assert a[0] == b
413414
```
414415

416+
You can also use this function inside a class with `in_cls=True`. In that case you MUST assign the output of the function to variables, as the created fixtures won't be registered with the encompassing module.
417+
418+
```python
419+
import pytest
420+
from pytest_cases import unpack_fixture, fixture
421+
422+
@fixture
423+
@pytest.mark.parametrize("o", ['hello', 'world'])
424+
def c(o):
425+
return o, o[0]
426+
427+
class TestClass:
428+
a, b = unpack_fixture("a,b", c, in_cls=True)
429+
430+
def test_function(self, a, b):
431+
assert a[0] == b
432+
```
433+
415434
**Parameters**
416435

417436
- **argnames**: same as `@pytest.mark.parametrize` `argnames`.
418437
- **fixture**: a fixture name string or a fixture symbol. If a fixture symbol is provided, the created fixtures will have the same scope. If a name is provided, they will have scope='function'. Note that in practice the performance loss resulting from using `function` rather than a higher scope is negligible since the created fixtures' body is a one-liner.
438+
- **in_cls**: a boolean (default `False`). You may wish to turn this to `True` to use this function inside a class. If you do so, you **MUST** assign the output to variables in the class.
419439
- **hook**: an optional hook to apply to each fixture function that is created during this call. The hook function will be called everytime a fixture is about to be created. It will receive a single argument (the function implementing the fixture) and should return the function to use. For example you can use `saved_fixture` from `pytest-harvest` as a hook in order to save all such created fixtures in the fixture store.
420440

421441
**Outputs:** the created fixtures.

pytest_cases/fixture_core1_unions.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,10 @@ def fixture_union(name, # type: str
330330

331331
# if unpacking is requested, do it here
332332
if unpack_into is not None:
333-
_make_unpack_fixture(caller_module, argnames=unpack_into, fixture=name, hook=hook)
333+
# Note: we can't expose the `in_cls` argument as we would not be able to output both the union and the
334+
# unpacked fixtures. However there is a simple workaround for this scenario of unpacking a union inside a class:
335+
# call unpack_fixture separately.
336+
_make_unpack_fixture(caller_module, argnames=unpack_into, fixture=name, hook=hook, in_cls=False)
334337

335338
return union_fix
336339

@@ -409,9 +412,10 @@ def _new_fixture(request, **all_fixtures):
409412
"""A readable alias for callers not using the returned symbol"""
410413

411414

412-
def unpack_fixture(argnames, # type: str
413-
fixture, # type: Union[str, Callable]
414-
hook=None # type: Callable[[Callable], Callable]
415+
def unpack_fixture(argnames, # type: str
416+
fixture, # type: Union[str, Callable]
417+
in_cls=False, # type: bool
418+
hook=None # type: Callable[[Callable], Callable]
415419
):
416420
"""
417421
Creates several fixtures with names `argnames` from the source `fixture`. Created fixtures will correspond to
@@ -437,33 +441,59 @@ def test_function(a, b):
437441
assert a[0] == b
438442
```
439443
444+
You can also use this function inside a class with `in_cls=True`. In that case you MUST assign the output of the
445+
function to variables, as the created fixtures won't be registered with the encompassing module.
446+
447+
```python
448+
import pytest
449+
from pytest_cases import unpack_fixture, fixture
450+
451+
@fixture
452+
@pytest.mark.parametrize("o", ['hello', 'world'])
453+
def c(o):
454+
return o, o[0]
455+
456+
class TestClass:
457+
a, b = unpack_fixture("a,b", c, in_cls=True)
458+
459+
def test_function(self, a, b):
460+
assert a[0] == b
461+
```
462+
440463
:param argnames: same as `@pytest.mark.parametrize` `argnames`.
441464
:param fixture: a fixture name string or a fixture symbol. If a fixture symbol is provided, the created fixtures
442465
will have the same scope. If a name is provided, they will have scope='function'. Note that in practice the
443466
performance loss resulting from using `function` rather than a higher scope is negligible since the created
444467
fixtures' body is a one-liner.
468+
:param in_cls: a boolean (default False). You may wish to turn this to `True` to use this function inside a class.
469+
If you do so, you **MUST** assign the output to variables in the class.
445470
:param hook: an optional hook to apply to each fixture function that is created during this call. The hook function
446471
will be called everytime a fixture is about to be created. It will receive a single argument (the function
447472
implementing the fixture) and should return the function to use. For example you can use `saved_fixture` from
448473
`pytest-harvest` as a hook in order to save all such created fixtures in the fixture store.
449474
:return: the created fixtures.
450475
"""
451-
# get caller module to create the symbols
452-
# todo what if this is called in a class ?
453-
caller_module = get_caller_module()
454-
return _unpack_fixture(caller_module, argnames, fixture, hook=hook)
476+
if in_cls:
477+
# the user needs to capture the outputs of the function in symbols in the class
478+
caller_module = None
479+
else:
480+
# get the caller module to create the symbols in it. Assigning outputs is optional
481+
caller_module = get_caller_module()
482+
return _unpack_fixture(caller_module, argnames, fixture, hook=hook, in_cls=in_cls)
455483

456484

457485
def _unpack_fixture(fixtures_dest, # type: ModuleType
458486
argnames, # type: Union[str, Iterable[str]]
459487
fixture, # type: Union[str, Callable]
488+
in_cls, # type: bool
460489
hook # type: Callable[[Callable], Callable]
461490
):
462491
"""
463492
464-
:param fixtures_dest:
493+
:param fixtures_dest: if this is `None` the fixtures wont be registered anywhere (just returned)
465494
:param argnames:
466495
:param fixture:
496+
:param in_cls: a boolean indicating if the `self` argument should be prepended.
467497
:param hook: an optional hook to apply to each fixture function that is created during this call. The hook function
468498
will be called everytime a fixture is about to be created. It will receive a single argument (the function
469499
implementing the fixture) and should return the function to use. For example you can use `saved_fixture` from
@@ -483,14 +513,21 @@ def _unpack_fixture(fixtures_dest, # type: ModuleType
483513

484514
# finally create the sub-fixtures
485515
created_fixtures = []
516+
517+
# we'll need to create their signature
518+
if in_cls:
519+
_sig = "(self, %s, request)" % source_f_name
520+
else:
521+
_sig = "(%s, request)" % source_f_name
522+
486523
for value_idx, argname in enumerate(argnames_lst):
487524
# create the fixture
488525
# To fix late binding issue with `value_idx` we add an extra layer of scope: a factory function
489526
# See https://stackoverflow.com/questions/3431676/creating-functions-in-a-loop
490527
def _create_fixture(_value_idx):
491528
# no need to autouse=True: this fixture does not bring any added value in terms of setup.
492529
@pytest_fixture(name=argname, scope=scope, autouse=False, hook=hook)
493-
@with_signature("%s(%s, request)" % (argname, source_f_name))
530+
@with_signature(argname + _sig)
494531
def _param_fixture(request, **kwargs):
495532
# ignore the "not used" marks, like in @ignore_unused
496533
if not is_used_request(request):
@@ -505,9 +542,10 @@ def _param_fixture(request, **kwargs):
505542
# create it
506543
fix = _create_fixture(value_idx)
507544

508-
# add to module
509-
check_name_available(fixtures_dest, argname, if_name_exists=WARN, caller=unpack_fixture)
510-
setattr(fixtures_dest, argname, fix)
545+
if fixtures_dest is not None:
546+
# add to module
547+
check_name_available(fixtures_dest, argname, if_name_exists=WARN, caller=unpack_fixture)
548+
setattr(fixtures_dest, argname, fix)
511549

512550
# collect to return the whole list eventually
513551
created_fixtures.append(fix)

pytest_cases/fixture_core2.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,9 @@ def _decorate_fixture_plus(fixture_func,
403403

404404
# get caller module to create the symbols
405405
caller_module = get_caller_module(frame_offset=_caller_module_offset_when_unpack)
406-
_make_unpack_fixture(caller_module, unpack_into, name, hook=hook)
406+
407+
# note that we cannot use in_cls=True since we have no way to assign the unpacked fixtures to the class
408+
_make_unpack_fixture(caller_module, unpack_into, name, hook=hook, in_cls=False)
407409

408410
# (1) Collect all @pytest.mark.parametrize markers (including those created by usage of @cases_data)
409411
parametrizer_marks = get_pytest_parametrize_marks(fixture_func)

pytest_cases/fixture_parametrize_plus.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ def _new_fixture(request, **all_fixtures):
116116

117117
# if unpacking is requested, do it here
118118
if unpack_into is not None:
119-
_make_unpack_fixture(fixtures_dest, argnames=unpack_into, fixture=name, hook=hook)
119+
# note that as for fixture unions, we can not expose the `in_cls` parameter.
120+
# but there is an easy workaround if unpacking is needed: call unpack_fixture separately
121+
_make_unpack_fixture(fixtures_dest, argnames=unpack_into, fixture=name, hook=hook, in_cls=False)
120122

121123
return fix
122124

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import pytest
2+
3+
from pytest_cases import fixture, unpack_fixture
4+
5+
6+
@fixture
7+
@pytest.mark.parametrize("o", ['hello', 'world'])
8+
def c(o):
9+
return o, o[0]
10+
11+
12+
class TestClass:
13+
a, b = unpack_fixture("a,b", c, in_cls=True)
14+
15+
def test_function(self, a, b):
16+
assert a[0] == b
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from pytest_cases import fixture, parametrize_with_cases, unpack_fixture
2+
3+
4+
class Cases:
5+
def case_one(self):
6+
return "/", {"param": "value"}
7+
8+
9+
@fixture
10+
@parametrize_with_cases("case", Cases)
11+
def case(case):
12+
return case
13+
14+
15+
class TestAPIView(object):
16+
url, data = unpack_fixture("url, data", case, in_cls=True)
17+
18+
def test_foo(self, url, data):
19+
assert url == "/"
20+
assert data == {"param": "value"}

0 commit comments

Comments
 (0)