From 6a5fec6dfd94ac8e51ee74ec9389aba8d4c6d521 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sun, 14 Aug 2022 17:54:15 +0200 Subject: [PATCH 1/7] Remove most code related to parsing the delay. --- src/pytask_parallel/build.py | 9 --------- src/pytask_parallel/callbacks.py | 16 ---------------- src/pytask_parallel/config.py | 9 +-------- tests/test_callbacks.py | 19 ------------------- tests/test_cli.py | 27 --------------------------- tests/test_config.py | 2 -- tests/test_execute.py | 29 ----------------------------- 7 files changed, 1 insertion(+), 110 deletions(-) delete mode 100644 tests/test_cli.py diff --git a/src/pytask_parallel/build.py b/src/pytask_parallel/build.py index 65c8b5d..d16a674 100644 --- a/src/pytask_parallel/build.py +++ b/src/pytask_parallel/build.py @@ -29,14 +29,5 @@ def pytask_extend_command_line_interface(cli: click.Group) -> None: ), default=None, ), - click.Option( - ["--delay"], - help=( - "Delay between checking whether tasks have finished. [default: 0.1 " - "(seconds)]" - ), - metavar="NUMBER > 0", - default=None, - ), ] cli.commands["build"].params.extend(additional_parameters) diff --git a/src/pytask_parallel/callbacks.py b/src/pytask_parallel/callbacks.py index 1c28ad9..6061f4c 100644 --- a/src/pytask_parallel/callbacks.py +++ b/src/pytask_parallel/callbacks.py @@ -33,19 +33,3 @@ def parallel_backend_callback(value: Any) -> str | None: f"parallel_backend has to be one of {list(PARALLEL_BACKENDS)}." ) return value - - -def delay_callback(value: Any) -> float | None: - """Validate the delay option.""" - if value in [None, "None", "none"]: - value = None - else: - try: - value = float(value) - except ValueError: - pass - - if not (isinstance(value, float) and value > 0): - raise ValueError("delay has to be a number greater than 0.") - - return value diff --git a/src/pytask_parallel/config.py b/src/pytask_parallel/config.py index d8ac251..a329900 100644 --- a/src/pytask_parallel/config.py +++ b/src/pytask_parallel/config.py @@ -7,7 +7,6 @@ from pytask import hookimpl from pytask_parallel.backends import PARALLEL_BACKENDS_DEFAULT -from pytask_parallel.callbacks import delay_callback from pytask_parallel.callbacks import n_workers_callback from pytask_parallel.callbacks import parallel_backend_callback @@ -29,13 +28,7 @@ def pytask_parse_config( if config["n_workers"] == "auto": config["n_workers"] = max(os.cpu_count() - 1, 1) - config["delay"] = _get_first_non_none_value( - config_from_cli, - config_from_file, - key="delay", - default=0.1, - callback=delay_callback, - ) + config["delay"] = 0.1 config["parallel_backend"] = _get_first_non_none_value( config_from_cli, diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index bfb7473..40555a7 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -4,7 +4,6 @@ import pytest from pytask_parallel.backends import PARALLEL_BACKENDS -from pytask_parallel.callbacks import delay_callback from pytask_parallel.callbacks import n_workers_callback from pytask_parallel.callbacks import parallel_backend_callback @@ -45,21 +44,3 @@ def test_n_workers_callback(value, expectation): def test_parallel_backend_callback(value, expectation): with expectation: parallel_backend_callback(value) - - -@pytest.mark.unit -@pytest.mark.parametrize( - "value, expectation", - [ - (-1, pytest.raises(ValueError)), - (0.1, does_not_raise()), - (1, does_not_raise()), - ("asdad", pytest.raises(ValueError)), - (None, does_not_raise()), - ("None", does_not_raise()), - ("none", does_not_raise()), - ], -) -def test_delay_callback(value, expectation): - with expectation: - delay_callback(value) diff --git a/tests/test_cli.py b/tests/test_cli.py deleted file mode 100644 index f9cb0ec..0000000 --- a/tests/test_cli.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import annotations - -import textwrap -from time import time - -import pytest -from pytask import cli -from pytask import ExitCode - - -@pytest.mark.end_to_end -def test_delay_via_cli(runner, tmp_path): - source = """ - import pytask - - @pytask.mark.produces("out_1.txt") - def task_1(produces): - produces.write_text("1") - """ - tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(source)) - - start = time() - result = runner.invoke(cli, [tmp_path.as_posix(), "-n", "2", "--delay", "5"]) - end = time() - - assert result.exit_code == ExitCode.OK - assert end - start > 5 diff --git a/tests/test_config.py b/tests/test_config.py index faca99f..b399030 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -35,8 +35,6 @@ def test_interplay_between_debugging_and_parallel(tmp_path, pdb, n_workers, expe ("n_workers", "auto", ExitCode.OK), ("n_workers", 1, ExitCode.OK), ("n_workers", 2, ExitCode.OK), - ("delay", 0.1, ExitCode.OK), - ("delay", 1, ExitCode.OK), ("parallel_backend", "unknown_backend", ExitCode.CONFIGURATION_FAILED), ] + [ diff --git a/tests/test_execute.py b/tests/test_execute.py index 495d849..7453db8 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -141,35 +141,6 @@ def myfunc(): assert exception is None -@pytest.mark.end_to_end -@pytest.mark.parametrize("parallel_backend", PARALLEL_BACKENDS) -def test_parallel_execution_delay(tmp_path, parallel_backend): - source = """ - import pytask - - @pytask.mark.produces("out_1.txt") - def task_1(produces): - produces.write_text("1") - - @pytask.mark.produces("out_2.txt") - def task_2(produces): - produces.write_text("2") - """ - tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(source)) - - session = main( - { - "paths": tmp_path, - "delay": 3, - "n_workers": 2, - "parallel_backend": parallel_backend, - } - ) - - assert session.exit_code == ExitCode.OK - assert 3 < session.execution_end - session.execution_start < 10 - - @pytest.mark.end_to_end @pytest.mark.parametrize("parallel_backend", PARALLEL_BACKENDS) def test_stop_execution_when_max_failures_is_reached(tmp_path, parallel_backend): From d542e1c0180bf4abcc553d262189634995bf92f3 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sun, 14 Aug 2022 18:02:46 +0200 Subject: [PATCH 2/7] add to changes. --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 45e4caf..86662c6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask-parallel) and - {pull}`43` adds docformatter. - {pull}`44` allows to capture warnings from subprocesses. Fixes {issue}`41`. +- {pull}`45` replaces the delay command line option with an internal, dynamic parameter. ## 0.2.0 - 2022-04-15 From f76e4789e81771bc5067408595210f656300a290 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 19 Aug 2022 18:52:45 +0200 Subject: [PATCH 3/7] Implement dynamic sleeper. --- .pre-commit-config.yaml | 2 +- CHANGES.md | 1 + src/pytask_parallel/execute.py | 32 +++++++++++++++++++++++++++++++- tests/test_execute.py | 22 ++++++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b45d7a..3151086 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: rev: v2.0.0 hooks: - id: setup-cfg-fmt -- repo: https://github.com/myint/docformatter +- repo: https://github.com/PYCQA/docformatter rev: v1.4 hooks: - id: docformatter diff --git a/CHANGES.md b/CHANGES.md index 86662c6..79ea50c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask-parallel) and - {pull}`43` adds docformatter. - {pull}`44` allows to capture warnings from subprocesses. Fixes {issue}`41`. - {pull}`45` replaces the delay command line option with an internal, dynamic parameter. + Fixes {issue}`41`. ## 0.2.0 - 2022-04-15 diff --git a/src/pytask_parallel/execute.py b/src/pytask_parallel/execute.py index b9a750e..a614cc8 100644 --- a/src/pytask_parallel/execute.py +++ b/src/pytask_parallel/execute.py @@ -9,7 +9,9 @@ from types import TracebackType from typing import Any from typing import Callable +from typing import List +import attr import cloudpickle from pybaum.tree_util import tree_map from pytask import console @@ -65,6 +67,7 @@ def pytask_execute_build(session: Session) -> bool | None: with parallel_backend(max_workers=session.config["n_workers"]) as executor: session.executor = executor + sleeper = _Sleeper() while session.scheduler.is_active(): @@ -96,6 +99,10 @@ def pytask_execute_build(session: Session) -> bool | None: running_tasks[task_name] = session.hook.pytask_execute_task( session=session, task=task ) + sleeper.reset() + + if not ready_tasks: + sleeper.increment() for task_name in list(running_tasks): future = running_tasks[task_name] @@ -146,7 +153,7 @@ def pytask_execute_build(session: Session) -> bool | None: if session.should_stop: break else: - time.sleep(session.config["delay"]) + sleeper.sleep() except KeyboardInterrupt: break @@ -316,3 +323,26 @@ def _create_kwargs_for_task(task: Task) -> dict[Any, Any]: kwargs[arg_name] = tree_map(lambda x: x.value, attribute) return kwargs + + +@attr.s(kw_only=True) +class _Sleeper: + """A sleeper that always sleeps a bit and up to 1 second if you don't wake it up. + + This class controls when the next iteration of the execution loop starts. If new + tasks are scheduled, the time spent sleeping is reset to a lower value. + + """ + + timings = attr.ib(type=List[float], default=[(i / 10) ** 2 for i in range(1, 11)]) + timing_idx = attr.ib(type=int, default=0) + + def reset(self) -> None: + self.timing_idx = 0 + + def increment(self) -> None: + if self.timing_idx < len(self.timings) - 1: + self.timing_idx += 1 + + def sleep(self) -> None: + time.sleep(self.timings[self.timing_idx]) diff --git a/tests/test_execute.py b/tests/test_execute.py index 7453db8..e9306d0 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -11,6 +11,7 @@ from pytask import main from pytask import Task from pytask_parallel.backends import PARALLEL_BACKENDS +from pytask_parallel.execute import _Sleeper from pytask_parallel.execute import DefaultBackendNameSpace from pytask_parallel.execute import ProcessesNameSpace @@ -296,3 +297,24 @@ def task_example(produces): warnings_block = result.output.split("Warnings")[1] assert "task_example.py::task_example[0]" in warnings_block assert "task_example.py::task_example[1]" in warnings_block + + +def test_sleeper(): + sleeper = _Sleeper(timings=[1, 2, 3], timing_idx=0) + + assert sleeper.timings == [1, 2, 3] + assert sleeper.timing_idx == 0 + + sleeper.increment() + assert sleeper.timing_idx == 1 + + sleeper.increment() + assert sleeper.timing_idx == 2 + + sleeper.reset() + assert sleeper.timing_idx == 0 + + start = time.time() + sleeper.sleep() + end = time.time() + assert 1 <= end - start <= 1.000001 From bb2d97027f5f4d0f79741fd50b66f338187d27ac Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 19 Aug 2022 18:55:13 +0200 Subject: [PATCH 4/7] Fix test. --- tests/test_execute.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_execute.py b/tests/test_execute.py index e9306d0..6c6a7cd 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -2,8 +2,8 @@ import pickle import textwrap +import time from pathlib import Path -from time import time import pytest from pytask import cli @@ -317,4 +317,4 @@ def test_sleeper(): start = time.time() sleeper.sleep() end = time.time() - assert 1 <= end - start <= 1.000001 + assert 1 <= end - start <= 1.01 From 2e5094b6e950f214d5387adb84b1dabf098de57c Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 19 Aug 2022 19:31:15 +0200 Subject: [PATCH 5/7] Prepare release. --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 73dce65..b303614 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ chronological order. Releases follow [semantic versioning](https://semver.org/) releases are available on [PyPI](https://pypi.org/project/pytask-parallel) and [Anaconda.org](https://anaconda.org/conda-forge/pytask-parallel). -## 0.2.1 - 2022-08-xx +## 0.2.1 - 2022-08-19 - {pull}`43` adds docformatter. - {pull}`44` allows to capture warnings from subprocesses. Fixes {issue}`41`. From fd839bd6622c3a95d65686dd285a64ff4be9f0a5 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 19 Aug 2022 19:37:19 +0200 Subject: [PATCH 6/7] Fix tests. --- CHANGES.md | 2 +- tests/test_execute.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b303614..1591ce8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,7 +11,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask-parallel) and - {pull}`44` allows to capture warnings from subprocesses. Fixes {issue}`41`. - {pull}`45` replaces the delay command line option with an internal, dynamic parameter. Fixes {issue}`41`. -- {pull}`46` adds a dynamic sleep duration during the execution. +- {pull}`46` adds a dynamic sleep duration during the execution. Fixes {issue}`42`. ## 0.2.0 - 2022-04-15 diff --git a/tests/test_execute.py b/tests/test_execute.py index 6c6a7cd..6f6d8f8 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -2,8 +2,8 @@ import pickle import textwrap -import time from pathlib import Path +from time import time import pytest from pytask import cli @@ -314,7 +314,7 @@ def test_sleeper(): sleeper.reset() assert sleeper.timing_idx == 0 - start = time.time() + start = time() sleeper.sleep() - end = time.time() + end = time() assert 1 <= end - start <= 1.01 From e0b5cc82f3f988a08560c10ddc5568ad1f13a992 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 19 Aug 2022 19:46:53 +0200 Subject: [PATCH 7/7] Make test less flaky. --- tests/test_execute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_execute.py b/tests/test_execute.py index 6f6d8f8..958e6e4 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -317,4 +317,4 @@ def test_sleeper(): start = time() sleeper.sleep() end = time() - assert 1 <= end - start <= 1.01 + assert 1 <= end - start <= 2