Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
added itertools module with named_product function
- Loading branch information
1 parent
be113a2
commit 8038cc4
Showing
4 changed files
with
176 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
--------------------- | ||
pytb.itertools module | ||
--------------------- | ||
|
||
************************************************************ | ||
Flexibly test a number possible configurations of a function | ||
************************************************************ | ||
|
||
Assume you have a function that takes a number of parameters: | ||
|
||
>>> def my_func(a, b, c=2, **kwargs): | ||
... print(' '.join((a, b, c)), kwargs) | ||
|
||
And you want to call it with multiple parameter combinations | ||
|
||
>>> my_params = { | ||
... 'a': 'a1', | ||
... 'b': ('b1','b2'), | ||
... 'c': ('c1', 'c2'), | ||
... 'additional_arg': 'val' | ||
... } | ||
|
||
You can use the :meth:`named_tuple` function of this module to create any | ||
possible combination of the provided parameters | ||
|
||
>>> for params in named_product(my_params): | ||
... my_func(**params) | ||
a1 b1 c1 {'additional_arg': 'val'} | ||
a1 b1 c2 {'additional_arg': 'val'} | ||
a1 b2 c1 {'additional_arg': 'val'} | ||
a1 b2 c2 {'additional_arg': 'val'} | ||
|
||
Excluding some combinations | ||
--------------------------- | ||
|
||
If some parameter combinations are not allowed, you can use the | ||
functions ability to work with nested dicts to overwrite values defined in | ||
an outer dict | ||
|
||
>>> my_params = { | ||
... 'a': 'a1', | ||
... 'b': ('b1','b2'), | ||
... 'c': { | ||
... 'c1': {'b': 'b1'}, | ||
... 'c2': {}, | ||
... 'c3': { | ||
... 'additional_arg': 'other val', | ||
... 'another arg': 'yet another val'} | ||
... }, | ||
... 'additional_arg': 'val' | ||
... } | ||
|
||
>>> for params in named_product(my_params): | ||
... my_func(**params) | ||
a1 b1 c1 {'additional_arg': 'val'} | ||
a1 b1 c2 {'additional_arg': 'val'} | ||
a1 b2 c2 {'additional_arg': 'val'} | ||
a1 b1 c3 {'additional_arg': 'other val', 'another arg': 'yet another val'} | ||
a1 b2 c3 {'additional_arg': 'other val', 'another arg': 'yet another val'} | ||
|
||
Note that for ``c='c1'`` only ``b='b1'`` was used. You can also define | ||
new variables inside each dict that only get used for combinations in | ||
this branch. | ||
|
||
***************** | ||
API Documentation | ||
***************** | ||
|
||
.. automodule:: pytb.itertools | ||
:members: | ||
:show-inheritance: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
""" | ||
Methods to work with iterables conveniently. | ||
(methods that could be in the python stdlib itertools package) | ||
""" | ||
|
||
import itertools | ||
from collections.abc import Iterable | ||
from typing import Optional, Any, Mapping, Generator | ||
|
||
|
||
def named_product( | ||
values: Optional[Mapping[Any, Any]] = None, | ||
repeat: int = 1, | ||
**kwargs: Mapping[Any, Any] | ||
) -> Generator[Any, None, None]: | ||
""" | ||
.. testsetup:: * | ||
from pytb.itertools import named_product | ||
Return each possible combination of the input parameters (cartesian product), | ||
thus this provides the same basic functionality of :meth:``itertools.product``. | ||
However this method provides more flexibility as it: | ||
1. returns dicts instead of tuples | ||
.. doctest:: | ||
>>> list(named_product(a=('X', 'Y'), b=(1, 2))) | ||
[{'a': 'X', 'b': 1}, {'a': 'X', 'b': 2}, {'a': 'Y', 'b': 1}, {'a': 'Y', 'b': 2}] | ||
2. accepts either a dict or kwargs | ||
.. doctest:: | ||
>>> list(named_product({ 'a':('X', 'Y') }, b=(1, 2))) | ||
[{'a': 'X', 'b': 1}, {'a': 'X', 'b': 2}, {'a': 'Y', 'b': 1}, {'a': 'Y', 'b': 2}] | ||
3. accepts nested dicts | ||
.. doctest:: | ||
>>> list(named_product( | ||
... a=( | ||
... {'X': {'b':(1,2)}}, | ||
... {'Y': { | ||
... 'b': (3, 4), | ||
... 'c': (5, ) | ||
... } | ||
... } | ||
... ) | ||
... )) | ||
[{'a': {'X': {'b': (1, 2)}}}, {'a': {'Y': {'b': (3, 4), 'c': (5,)}}}] | ||
4. accepts scalar values | ||
.. doctest:: | ||
>>> list(named_product(b='Xy', c=('a', 'b'))) | ||
[{'b': 'Xy', 'c': 'a'}, {'b': 'Xy', 'c': 'b'}] | ||
:param values: a dict of iterables used to create the cartesian product | ||
:param repeat: repeat iteration of the product N-times | ||
:param \**kwargs: optional keyword arguments. The dict of | ||
keyword arguments is merged with the values dict, | ||
with ``kwargs`` overwriting values in ``values`` | ||
""" | ||
# merge the values dict with the kwargs, giving | ||
# precedence to the kwargs | ||
if values is not None: | ||
kwargs = {**values, **kwargs} | ||
|
||
# convert scalar values to 1-tuples | ||
for name, entry in kwargs.copy().items(): | ||
# always pack strings as they are iterable, | ||
# but most likely used as scalars | ||
if isinstance(entry, str): | ||
kwargs.update({name: (entry,)}) | ||
elif not isinstance(entry, Iterable): | ||
# pack the value into a tuple | ||
kwargs.update({name: (entry,)}) | ||
|
||
if any(isinstance(v, dict) for v in kwargs.values()): | ||
# recursivley expand all dict elements to the set of values | ||
for key_outer, val_outer in kwargs.items(): | ||
if isinstance(val_outer, dict): | ||
for key_inner, val_inner in val_outer.items(): | ||
subproduct = {key_outer: key_inner, **val_inner} | ||
# yield from to exhaust the recursive call to the iterator | ||
yield from named_product(repeat=repeat, **{**kwargs, **subproduct}) | ||
else: | ||
# non-recursive exit point yields the product of all values | ||
for combination in itertools.product(*kwargs.values(), repeat=repeat): | ||
yield dict(zip(list(kwargs.keys()), combination)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import doctest | ||
import unittest | ||
|
||
import pytb.itertools | ||
|
||
suite = unittest.TestSuite() | ||
suite.addTest(doctest.DocTestSuite(pytb.itertools)) | ||
|
||
runner = unittest.TextTestRunner(verbosity=2) | ||
runner.run(suite) |