Skip to content

Commit 82186ef

Browse files
authored
Release 0.0.4 and refactor parametrization and add hooks. (#9)
1 parent 7b124b7 commit 82186ef

File tree

11 files changed

+164
-99
lines changed

11 files changed

+164
-99
lines changed

.github/workflows/continuous-integration-workflow.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ jobs:
6161
run: bash <(curl -s https://codecov.io/bash) -F end_to_end -c
6262

6363
- name: Validate codecov.yml
64-
if: runner.os == 'Linux' && matrix.python-version == '3.7'
64+
if: runner.os == 'Linux' && matrix.python-version == '3.8'
6565
shell: bash -l {0}
6666
run: cat codecov.yml | curl --data-binary @- https://codecov.io/validate
6767

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ repos:
1111
exclude: (debugging\.py|main\.py)
1212
- id: end-of-file-fixer
1313
- repo: https://github.com/asottile/pyupgrade
14-
rev: v2.6.2
14+
rev: v2.7.1
1515
hooks:
1616
- id: pyupgrade
1717
args: [--py36-plus]

docs/changes.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ This is a record of all past pytask releases and what went into them in reverse
55
chronological order. Releases follow `semantic versioning <https://semver.org/>`_ and
66
all releases are available on `Anaconda.org <https://anaconda.org/pytask/pytask>`_.
77

8+
0.0.4 - 2020-07-22
9+
------------------
10+
11+
- :gh:`9` adds hook specifications to the parametrization of tasks which allows
12+
``pytask-latex`` and ``pytask-r`` to pass different command line arguments to a
13+
parametrized task and its script. Also, it prepares the release of 0.0.4.
14+
815

916
0.0.3 - 2020-07-19
1017
------------------

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
author = "Tobias Raabe"
2020

2121
# The full version, including alpha/beta/rc tags
22-
release = "0.0.3"
22+
release = "0.0.4"
2323

2424

2525
# -- General configuration -------------------------------------------------------------

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.0.3
2+
current_version = 0.0.4
33
parse = (?P<major>\d+)\.(?P<minor>\d+)(\.(?P<patch>\d+))(\-?((dev)?(?P<dev>\d+))?)
44
serialize =
55
{major}.{minor}.{patch}dev{dev}

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
setup(
1616
name="pytask",
17-
version="0.0.3",
17+
version="0.0.4",
1818
description=DESCRIPTION,
1919
long_description=DESCRIPTION + "\n\n" + README,
2020
long_description_content_type="text/x-rst",

src/pytask/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pluggy
22
from pytask.mark import MARK_GEN as mark # noqa: F401, N811
33

4-
__version__ = "0.0.3"
4+
__version__ = "0.0.4"
55

66

77
hookimpl = pluggy.HookimplMarker("pytask")

src/pytask/collect.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import glob
33
import importlib
44
import inspect
5-
import itertools
65
import pprint
76
import sys
87
import traceback
@@ -94,7 +93,6 @@ def pytask_collect_file(session, path, reports):
9493
names_and_objects = session.hook.pytask_generate_tasks(
9594
session=session, name=name, obj=obj
9695
)
97-
names_and_objects = itertools.chain.from_iterable(names_and_objects)
9896
else:
9997
names_and_objects = [(name, obj)]
10098

src/pytask/hookspecs.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,23 @@ def pytask_collect_file(session, path, reports):
7373
"""Collect tasks from files."""
7474

7575

76-
@hookspec
76+
@hookspec(firstresult=True)
7777
def pytask_generate_tasks(session, name, obj):
7878
"""Generate multiple tasks from name and object with parametrization."""
7979

8080

81+
@hookspec(firstresult=True)
82+
def generate_product_of_names_and_functions(
83+
session, name, obj, base_arg_names, arg_names, arg_values
84+
):
85+
"""Generate names and functions from Cartesian product."""
86+
87+
88+
@hookspec
89+
def pytask_generate_tasks_add_marker(obj, kwargs):
90+
"""Add some keyword arguments as markers to task."""
91+
92+
8193
@hookspec(firstresult=True)
8294
def pytask_collect_task_protocol(session, reports, path, name, obj):
8395
"""Start protocol to collect tasks."""

src/pytask/parametrize.py

Lines changed: 109 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,42 @@
11
import copy
22
import functools
3-
import inspect
43
import itertools
54
import types
65
from collections.abc import Iterable
76

87
import 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

186214
def _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

Comments
 (0)