1
1
import copy
2
2
import functools
3
- import inspect
4
3
import itertools
5
4
import types
6
5
from collections .abc import Iterable
7
6
8
7
import pytask
9
- from pytask .mark import Mark
10
8
11
9
12
- def parametrize (argnames , argvalues ):
10
+ def parametrize (arg_names , arg_values ):
13
11
"""Parametrize task function.
14
12
15
13
Parameters
16
14
----------
17
- argnames : str, tuple of str, list of str
15
+ arg_names : str, tuple of str, list of str
18
16
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 ``.
21
19
22
20
This functions is more a helper function to parse the arguments of the decorator and
23
21
to document the marker than a real function.
24
22
25
23
"""
26
- return argnames , argvalues
24
+ return arg_names , arg_values
27
25
28
26
29
27
@pytask .hookimpl
30
- def pytask_generate_tasks (name , obj ):
28
+ def pytask_generate_tasks (session , name , obj ):
31
29
if callable (obj ):
32
30
obj , markers = _remove_parametrize_markers_from_func (obj )
33
31
base_arg_names , arg_names , arg_values = _parse_parametrize_markers (markers )
34
32
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 ,
48
40
)
49
41
50
42
return names_and_functions
@@ -58,40 +50,60 @@ def _remove_parametrize_markers_from_func(obj):
58
50
return obj , parametrize
59
51
60
52
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 )
65
74
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 )
68
77
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 )
71
79
72
- n_runs = len ( parsed_arg_values )
80
+ expanded_arg_names = _expand_arg_names ( parsed_arg_names , n_runs )
73
81
74
- expanded_arg_names = _expand_arg_names ( parsed_arg_names , n_runs )
82
+ return parsed_arg_names , expanded_arg_names , parsed_arg_values
75
83
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 ]
79
91
80
92
return base_arg_names , processed_arg_names , processed_arg_values
81
93
82
94
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.
85
97
86
98
Parameters
87
99
----------
88
- argnames : str, tuple of str, list or str
100
+ arg_names : str, tuple of str, list or str
89
101
The names of the arguments which are parametrized.
90
102
91
103
Returns
92
104
-------
93
105
out : str, tuples of str
94
- The parse argnames .
106
+ The parse arg_names .
95
107
96
108
Example
97
109
-------
@@ -101,10 +113,10 @@ def _parse_arg_names(argnames):
101
113
('i', 'j')
102
114
103
115
"""
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 )
108
120
109
121
return out
110
122
@@ -126,12 +138,12 @@ def _parse_arg_values(arg_values):
126
138
]
127
139
128
140
129
- def _expand_arg_names (argnames , n_runs ):
141
+ def _expand_arg_names (arg_names , n_runs ):
130
142
"""Expands the names of the arguments for each run.
131
143
132
144
Parameters
133
145
----------
134
- argnames : str, list of str
146
+ arg_names : str, list of str
135
147
The names of the arguments of the parametrized function.
136
148
n_runs : int
137
149
How many argument values are passed to the function.
@@ -145,57 +157,73 @@ def _expand_arg_names(argnames, n_runs):
145
157
[('i0', 'j0'), ('i1', 'j1')]
146
158
147
159
"""
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 )]
149
161
150
162
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
153
166
):
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.
165
168
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.
169
171
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 ),
174
186
)
187
+ )
188
+
189
+ # Copy function and attributes to allow in-place changes.
190
+ func = _copy_func (obj )
191
+ func .pytestmark = copy .deepcopy (obj .pytestmark )
175
192
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 )
179
198
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 ))
182
201
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 )
184
212
185
213
186
214
def _to_tuple (x ):
187
215
return (x ,) if not isinstance (x , Iterable ) or isinstance (x , str ) else tuple (x )
188
216
189
217
190
- def _copy_func (f ):
218
+ def _copy_func (func ):
191
219
"""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__ ,
198
226
)
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