Skip to content

Commit 8d63937

Browse files
authored
Better parsing of command-line options. (#2)
1 parent 7f6271b commit 8d63937

File tree

8 files changed

+229
-33
lines changed

8 files changed

+229
-33
lines changed

CHANGES.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ all releases are available on `Anaconda.org <https://anaconda.org/pytask/pytask-
99
0.0.2 - 2020-08-12
1010
------------------
1111

12-
- :gh:`2` prepares the plugin for pytask 0.0.5.
12+
- :gh:`2` prepares the plugin for pytask v0.0.5.
13+
- :gh:`3` better parsing and callbacks.
1314

1415

1516
0.0.1 - 2020-07-17

src/pytask_parallel/callbacks.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import click
2+
3+
4+
def n_workers_click_callback(ctx, name, value): # noqa: U100
5+
return n_workers_callback(value)
6+
7+
8+
def n_workers_callback(value):
9+
error_occurred = False
10+
if value == "auto":
11+
pass
12+
elif value is None:
13+
pass
14+
elif isinstance(value, int) and 1 <= value:
15+
pass
16+
else:
17+
try:
18+
value = int(value)
19+
except ValueError:
20+
error_occurred = True
21+
else:
22+
if value < 1:
23+
error_occurred = True
24+
25+
if error_occurred:
26+
raise click.UsageError("n-processes can either be an integer >= 1 or 'auto'.")
27+
28+
return value
29+
30+
31+
def parallel_backend_callback(value):
32+
if value not in ["processes", "threads", None]:
33+
raise click.UsageError("parallel_backend has to be 'processes' or 'threads'.")
34+
return value
35+
36+
37+
def delay_click_callback(ctx, name, value): # noqa: U100
38+
return delay_callback(value)
39+
40+
41+
def delay_callback(value):
42+
error_occurred = False
43+
if isinstance(value, float) and 0 < value:
44+
pass
45+
elif value is None:
46+
pass
47+
else:
48+
try:
49+
value = float(value)
50+
except ValueError:
51+
error_occurred = True
52+
else:
53+
if value < 0:
54+
error_occurred = True
55+
56+
if error_occurred:
57+
raise click.UsageError("delay has to be a number greater than 0.")
58+
59+
return value

src/pytask_parallel/cli.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,34 @@
11
import click
22
from _pytask.config import hookimpl
3+
from pytask_parallel.callbacks import delay_click_callback
4+
from pytask_parallel.callbacks import n_workers_click_callback
35

46

57
@hookimpl
68
def pytask_add_parameters_to_cli(command):
79
additional_parameters = [
810
click.Option(
9-
["-n", "--n-processes"],
11+
["-n", "--n-workers"],
1012
help=(
1113
"Max. number of pytask_parallel tasks. Integer >= 1 or 'auto' which is "
1214
"os.cpu_count() - 1. [default: 1 (no parallelization)]"
1315
),
14-
callback=_validate_n_workers,
15-
metavar="",
16+
metavar="[INTEGER|auto]",
17+
callback=n_workers_click_callback,
1618
),
1719
click.Option(
18-
["--pytask_parallel-backend"],
20+
["--parallel-backend"],
1921
type=click.Choice(["processes", "threads"]),
2022
help="Backend for the parallelization. [default: processes]",
2123
),
2224
click.Option(
2325
["--delay"],
24-
type=float,
2526
help=(
2627
"Delay between checking whether tasks have finished. [default: 0.1 "
2728
"(seconds)]"
2829
),
30+
metavar="NUMBER > 0",
31+
callback=delay_click_callback,
2932
),
3033
]
3134
command.params.extend(additional_parameters)
32-
33-
34-
def _validate_n_workers(ctx, param, value): # noqa: U100
35-
if (isinstance(value, int) and value >= 1) or value == "auto":
36-
pass
37-
else:
38-
raise click.UsageError("n-processes can either be an integer >= 1 or 'auto'.")

src/pytask_parallel/config.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,37 @@
22

33
from _pytask.config import hookimpl
44
from _pytask.shared import get_first_not_none_value
5+
from pytask_parallel.callbacks import n_workers_callback
6+
from pytask_parallel.callbacks import parallel_backend_callback
57

68

79
@hookimpl
810
def pytask_parse_config(config, config_from_cli, config_from_file):
911
config["n_workers"] = get_first_not_none_value(
10-
config_from_cli, config_from_file, key="n_workers", default=1
12+
config_from_cli,
13+
config_from_file,
14+
key="n_workers",
15+
default=1,
16+
callback=n_workers_callback,
1117
)
1218
if config["n_workers"] == "auto":
1319
config["n_workers"] = max(os.cpu_count() - 1, 1)
20+
1421
config["delay"] = get_first_not_none_value(
15-
config_from_cli, config_from_file, key="delay", default=0.1
22+
config_from_cli, config_from_file, key="delay", default=0.1, callback=float
1623
)
1724

1825
config["parallel_backend"] = get_first_not_none_value(
19-
config_from_cli, config_from_file, key="parallel_backend", default="processes"
26+
config_from_cli,
27+
config_from_file,
28+
key="parallel_backend",
29+
default="processes",
30+
callback=parallel_backend_callback,
2031
)
2132

2233

2334
@hookimpl
2435
def pytask_post_parse(config):
25-
# Disable parallelization if debugging is enabled.
36+
"""Disable parallelization if debugging is enabled."""
2637
if config["pdb"] or config["trace"]:
2738
config["n_workers"] = 1

tests/test_callbacks.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import functools
2+
from contextlib import ExitStack as does_not_raise # noqa: N813
3+
4+
import click
5+
import pytest
6+
from pytask_parallel.callbacks import delay_callback
7+
from pytask_parallel.callbacks import delay_click_callback
8+
from pytask_parallel.callbacks import n_workers_callback
9+
from pytask_parallel.callbacks import n_workers_click_callback
10+
from pytask_parallel.callbacks import parallel_backend_callback
11+
12+
13+
partialed_n_workers_callback = functools.partial(
14+
n_workers_click_callback, ctx=None, name=None
15+
)
16+
17+
18+
@pytest.mark.unit
19+
@pytest.mark.parametrize(
20+
"value, expectation",
21+
[
22+
(0, pytest.raises(click.UsageError)),
23+
(1, does_not_raise()),
24+
(2, does_not_raise()),
25+
("auto", does_not_raise()),
26+
("asdad", pytest.raises(click.UsageError)),
27+
(None, does_not_raise()),
28+
],
29+
)
30+
@pytest.mark.parametrize("func", [n_workers_callback, partialed_n_workers_callback])
31+
def test_n_workers_callback(func, value, expectation):
32+
with expectation:
33+
func(value=value)
34+
35+
36+
@pytest.mark.unit
37+
@pytest.mark.parametrize(
38+
"value, expectation",
39+
[
40+
("threads", does_not_raise()),
41+
("processes", does_not_raise()),
42+
(1, pytest.raises(click.UsageError)),
43+
("asdad", pytest.raises(click.UsageError)),
44+
(None, does_not_raise()),
45+
],
46+
)
47+
def test_parallel_backend_callback(value, expectation):
48+
with expectation:
49+
parallel_backend_callback(value)
50+
51+
52+
partialed_delay_callback = functools.partial(delay_click_callback, ctx=None, name=None)
53+
54+
55+
@pytest.mark.unit
56+
@pytest.mark.parametrize(
57+
"value, expectation",
58+
[
59+
(-1, pytest.raises(click.UsageError)),
60+
(0.1, does_not_raise()),
61+
(1, does_not_raise()),
62+
("asdad", pytest.raises(click.UsageError)),
63+
(None, does_not_raise()),
64+
],
65+
)
66+
@pytest.mark.parametrize("func", [delay_callback, partialed_delay_callback])
67+
def test_delay_callback(func, value, expectation):
68+
with expectation:
69+
func(value=value)

tests/test_cli.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
1-
from contextlib import ExitStack as does_not_raise # noqa: N813
1+
import subprocess
2+
import textwrap
23

3-
import click
44
import pytest
5-
from pytask_parallel.cli import _validate_n_workers
65

76

8-
@pytest.mark.unit
9-
@pytest.mark.parametrize(
10-
"value, expectation",
11-
[
12-
(0, pytest.raises(click.UsageError)),
13-
(1, does_not_raise()),
14-
(2, does_not_raise()),
15-
("auto", does_not_raise()),
16-
],
17-
)
18-
def test_validate_n_workers(value, expectation):
19-
with expectation:
20-
_validate_n_workers(None, None, value)
7+
@pytest.mark.end_to_end
8+
def test_delay_via_cli(tmp_path):
9+
source = """
10+
import pytask
11+
12+
@pytask.mark.produces("out_1.txt")
13+
def task_1(produces):
14+
produces.write_text("1")
15+
"""
16+
tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(source))
17+
18+
subprocess.run(["pytask", tmp_path.as_posix(), "-n", "2", "--delay", "5"])

tests/test_config.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import textwrap
23

34
import pytest
45
from pytask import main
@@ -18,3 +19,36 @@
1819
def test_interplay_between_debugging_and_parallel(tmp_path, pdb, n_workers, expected):
1920
session = main({"paths": tmp_path, "pdb": pdb, "n_workers": n_workers})
2021
assert session.config["n_workers"] == expected
22+
23+
24+
@pytest.mark.end_to_end
25+
@pytest.mark.parametrize("config_file", ["pytask.ini", "tox.ini", "setup.cfg"])
26+
@pytest.mark.parametrize(
27+
"name, value",
28+
[
29+
("n_workers", "auto"),
30+
("n_workers", 1),
31+
("n_workers", 2),
32+
("delay", 0.1),
33+
("delay", 1),
34+
("parallel_backend", "threads"),
35+
("parallel_backend", "processes"),
36+
("parallel_backend", "unknown_backend"),
37+
],
38+
)
39+
def test_reading_values_from_config_file(tmp_path, config_file, name, value):
40+
config = f"""
41+
[pytask]
42+
{name} = {value}
43+
"""
44+
tmp_path.joinpath(config_file).write_text(textwrap.dedent(config))
45+
46+
try:
47+
session = main({"paths": tmp_path})
48+
except Exception as e:
49+
assert "Error while configuring pytask" in str(e)
50+
else:
51+
assert session.exit_code == 0
52+
if value == "auto":
53+
value = os.cpu_count() - 1
54+
assert session.config[name] == value

tests/test_execute.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,31 @@ def myfunc():
9494
executor.shutdown()
9595

9696
assert future.result() is None
97+
98+
99+
@pytest.mark.end_to_end
100+
@pytest.mark.parametrize("parallel_backend", ["processes", "threads"])
101+
def test_parallel_execution_delay(tmp_path, parallel_backend):
102+
source = """
103+
import pytask
104+
105+
@pytask.mark.produces("out_1.txt")
106+
def task_1(produces):
107+
produces.write_text("1")
108+
109+
@pytask.mark.produces("out_2.txt")
110+
def task_2(produces):
111+
produces.write_text("2")
112+
"""
113+
tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(source))
114+
115+
session = main(
116+
{
117+
"paths": tmp_path,
118+
"delay": 3,
119+
"n_workers": 2,
120+
"parallel_backend": parallel_backend,
121+
}
122+
)
123+
124+
assert 3 < session.execution_end - session.execution_start < 10

0 commit comments

Comments
 (0)