11import copy
22import functools
3- import inspect
43import itertools
54import types
65from collections .abc import Iterable
76
87import pytask
9- from pytask .mark import Mark
108
119
12- def parametrize (argnames , argvalues ):
10+ def parametrize (arg_names , arg_values ):
1311 """Parametrize task function.
1412
1513 Parameters
1614 ----------
17- argnames : str, tuple of str, list of str
15+ arg_names : str, tuple of str, list of str
1816 The names of the arguments.
19- argvalues : list, list of iterables
20- The values which correspond to names in ``argnames ``.
17+ arg_values : iterable
18+ The values which correspond to names in ``arg_names ``.
2119
2220 This functions is more a helper function to parse the arguments of the decorator and
2321 to document the marker than a real function.
2422
2523 """
26- return argnames , argvalues
24+ return arg_names , arg_values
2725
2826
2927@pytask .hookimpl
30- def pytask_generate_tasks (name , obj ):
28+ def pytask_generate_tasks (session , name , obj ):
3129 if callable (obj ):
3230 obj , markers = _remove_parametrize_markers_from_func (obj )
3331 base_arg_names , arg_names , arg_values = _parse_parametrize_markers (markers )
3432
35- diff_arg_names = (
36- set (itertools .chain .from_iterable (base_arg_names ))
37- - set (inspect .getfullargspec (obj ).args )
38- - {"depends_on" , "produces" }
39- )
40- if diff_arg_names :
41- raise ValueError (
42- f"Parametrized function '{ name } ' does not have the following "
43- f"parametrized arguments: { diff_arg_names } ."
44- )
45-
46- names_and_functions = _generate_product_of_names_and_functions (
47- name , obj , base_arg_names , arg_names , arg_values
33+ names_and_functions = session .hook .generate_product_of_names_and_functions (
34+ session = session ,
35+ name = name ,
36+ obj = obj ,
37+ base_arg_names = base_arg_names ,
38+ arg_names = arg_names ,
39+ arg_values = arg_values ,
4840 )
4941
5042 return names_and_functions
@@ -58,40 +50,60 @@ def _remove_parametrize_markers_from_func(obj):
5850 return obj , parametrize
5951
6052
61- def _parse_parametrize_markers (markers ):
62- base_arg_names = []
63- processed_arg_names = []
64- processed_arg_values = []
53+ def _parse_parametrize_marker (marker ):
54+ """Parse parametrize marker.
55+
56+ Parameters
57+ ----------
58+ marker : pytask.mark.Mark
59+ A parametrize mark.
60+
61+ Returns
62+ -------
63+ base_arg_names : tuple of str
64+ Contains the names of the arguments.
65+ processed_arg_names : list of tuple of str
66+ Each tuple in the list represents the processed names of the arguments suffixed
67+ with a number indicating the iteration.
68+ processed_arg_values : list of tuple of obj
69+ Each tuple in the list represents the values of the arguments for each
70+ iteration.
71+
72+ """
73+ arg_names , arg_values = parametrize (* marker .args , ** marker .kwargs )
6574
66- for marker in markers :
67- arg_names , arg_values = parametrize ( * marker . args , ** marker . kwargs )
75+ parsed_arg_names = _parse_arg_names ( arg_names )
76+ parsed_arg_values = _parse_arg_values ( arg_values )
6877
69- parsed_arg_names = _parse_arg_names (arg_names )
70- parsed_arg_values = _parse_arg_values (arg_values )
78+ n_runs = len (parsed_arg_values )
7179
72- n_runs = len ( parsed_arg_values )
80+ expanded_arg_names = _expand_arg_names ( parsed_arg_names , n_runs )
7381
74- expanded_arg_names = _expand_arg_names ( parsed_arg_names , n_runs )
82+ return parsed_arg_names , expanded_arg_names , parsed_arg_values
7583
76- base_arg_names .append (parsed_arg_names )
77- processed_arg_names .append (expanded_arg_names )
78- processed_arg_values .append (parsed_arg_values )
84+
85+ def _parse_parametrize_markers (markers ):
86+ """Parse parametrize markers."""
87+ parsed_markers = [_parse_parametrize_marker (marker ) for marker in markers ]
88+ base_arg_names = [i [0 ] for i in parsed_markers ]
89+ processed_arg_names = [i [1 ] for i in parsed_markers ]
90+ processed_arg_values = [i [2 ] for i in parsed_markers ]
7991
8092 return base_arg_names , processed_arg_names , processed_arg_values
8193
8294
83- def _parse_arg_names (argnames ):
84- """Parse argnames argument of parametrize decorator.
95+ def _parse_arg_names (arg_names ):
96+ """Parse arg_names argument of parametrize decorator.
8597
8698 Parameters
8799 ----------
88- argnames : str, tuple of str, list or str
100+ arg_names : str, tuple of str, list or str
89101 The names of the arguments which are parametrized.
90102
91103 Returns
92104 -------
93105 out : str, tuples of str
94- The parse argnames .
106+ The parse arg_names .
95107
96108 Example
97109 -------
@@ -101,10 +113,10 @@ def _parse_arg_names(argnames):
101113 ('i', 'j')
102114
103115 """
104- if isinstance (argnames , str ):
105- out = tuple (i .strip () for i in argnames .split ("," ))
106- elif isinstance (argnames , (tuple , list )):
107- out = tuple (argnames )
116+ if isinstance (arg_names , str ):
117+ out = tuple (i .strip () for i in arg_names .split ("," ))
118+ elif isinstance (arg_names , (tuple , list )):
119+ out = tuple (arg_names )
108120
109121 return out
110122
@@ -126,12 +138,12 @@ def _parse_arg_values(arg_values):
126138 ]
127139
128140
129- def _expand_arg_names (argnames , n_runs ):
141+ def _expand_arg_names (arg_names , n_runs ):
130142 """Expands the names of the arguments for each run.
131143
132144 Parameters
133145 ----------
134- argnames : str, list of str
146+ arg_names : str, list of str
135147 The names of the arguments of the parametrized function.
136148 n_runs : int
137149 How many argument values are passed to the function.
@@ -145,57 +157,73 @@ def _expand_arg_names(argnames, n_runs):
145157 [('i0', 'j0'), ('i1', 'j1')]
146158
147159 """
148- return [tuple (name + str (i ) for name in argnames ) for i in range (n_runs )]
160+ return [tuple (name + str (i ) for name in arg_names ) for i in range (n_runs )]
149161
150162
151- def _generate_product_of_names_and_functions (
152- name , obj , base_arg_names , arg_names , arg_values
163+ @pytask .hookimpl
164+ def generate_product_of_names_and_functions (
165+ session , name , obj , base_arg_names , arg_names , arg_values
153166):
154- names_and_functions = []
155- product_arg_names = list (itertools .product (* arg_names ))
156- product_arg_values = list (itertools .product (* arg_values ))
157-
158- for names , values in zip (product_arg_names , product_arg_values ):
159- kwargs = dict (
160- zip (
161- itertools .chain .from_iterable (base_arg_names ),
162- itertools .chain .from_iterable (values ),
163- )
164- )
167+ """Generate product of names and functions.
165168
166- # Convert parametrized dependencies and products to decorator.
167- func = _copy_func (obj )
168- func .pytestmark = copy .deepcopy (obj .pytestmark )
169+ This function takes all ``@pytask.mark.parametrize`` decorators applied to a
170+ function and generates all combinations of parametrized arguments.
169171
170- for marker_name in ["depends_on" , "produces" ]:
171- if marker_name in kwargs :
172- func .pytestmark .append (
173- Mark (marker_name , _to_tuple (kwargs .pop (marker_name )), {})
172+ Note that, while a single :func:`parametrize` is handled like a loop or a
173+ :func:`zip`, two :func:`parametrize` decorators form a Cartesian product.
174+
175+ """
176+ if callable (obj ):
177+ names_and_functions = []
178+ product_arg_names = list (itertools .product (* arg_names ))
179+ product_arg_values = list (itertools .product (* arg_values ))
180+
181+ for names , values in zip (product_arg_names , product_arg_values ):
182+ kwargs = dict (
183+ zip (
184+ itertools .chain .from_iterable (base_arg_names ),
185+ itertools .chain .from_iterable (values ),
174186 )
187+ )
188+
189+ # Copy function and attributes to allow in-place changes.
190+ func = _copy_func (obj )
191+ func .pytestmark = copy .deepcopy (obj .pytestmark )
175192
176- # Attach remaining parametrized arguments to the function.
177- partialed_func = functools .partial (func , ** kwargs )
178- wrapped_func = functools .update_wrapper (partialed_func , func )
193+ # Convert parametrized dependencies and products to decorator.
194+ session .hook .pytask_generate_tasks_add_marker (obj = func , kwargs = kwargs )
195+ # Attach remaining parametrized arguments to the function.
196+ partialed_func = functools .partial (func , ** kwargs )
197+ wrapped_func = functools .update_wrapper (partialed_func , func )
179198
180- name_ = f"{ name } [{ '-' .join (itertools .chain .from_iterable (names ))} ]"
181- names_and_functions .append ((name_ , wrapped_func ))
199+ name_ = f"{ name } [{ '-' .join (itertools .chain .from_iterable (names ))} ]"
200+ names_and_functions .append ((name_ , wrapped_func ))
182201
183- return names_and_functions
202+ return names_and_functions
203+
204+
205+ @pytask .hookimpl
206+ def pytask_generate_tasks_add_marker (obj , kwargs ):
207+ """Add some parametrized keyword arguments as decorator."""
208+ if callable (obj ):
209+ for marker_name in ["depends_on" , "produces" ]:
210+ if marker_name in kwargs :
211+ pytask .mark .__getattr__ (marker_name )(kwargs .pop (marker_name ))(obj )
184212
185213
186214def _to_tuple (x ):
187215 return (x ,) if not isinstance (x , Iterable ) or isinstance (x , str ) else tuple (x )
188216
189217
190- def _copy_func (f ):
218+ def _copy_func (func ):
191219 """Based on https://stackoverflow.com/a/13503277/7523785."""
192- g = types .FunctionType (
193- f .__code__ ,
194- f .__globals__ ,
195- name = f .__name__ ,
196- argdefs = f .__defaults__ ,
197- closure = f .__closure__ ,
220+ new_func = types .FunctionType (
221+ func .__code__ ,
222+ func .__globals__ ,
223+ name = func .__name__ ,
224+ argdefs = func .__defaults__ ,
225+ closure = func .__closure__ ,
198226 )
199- g = functools .update_wrapper (g , f )
200- g .__kwdefaults__ = f .__kwdefaults__
201- return g
227+ new_func = functools .update_wrapper (new_func , func )
228+ new_func .__kwdefaults__ = func .__kwdefaults__
229+ return new_func
0 commit comments