From 3d2dd4aca604b5c9536d2d539cfbbe7782fa4eba Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 5 Apr 2024 16:27:31 +0200 Subject: [PATCH 1/8] Add docs. --- .readthedocs.yaml | 17 ++ MANIFEST.in | 1 + README.md | 64 +----- docs/Makefile | 20 ++ docs/make.bat | 35 +++ docs/source/_static/images/pytask.ico | Bin 0 -> 3872 bytes docs/source/_static/images/pytask.png | Bin 0 -> 41610 bytes docs/source/_static/images/pytask.svg | 29 +++ docs/source/_static/images/pytask_w_text.png | Bin 0 -> 10387 bytes .../_static/images/pytask_w_text_dark.svg | 32 +++ .../_static/images/pytask_w_text_light.svg | 32 +++ docs/source/changes.md | 110 +++++++++ docs/source/conf.py | 217 ++++++++++++++++++ docs/source/custom_executors.md | 58 +++++ docs/source/developers_guide.md | 1 + docs/source/index.md | 28 +++ environment.yml | 11 + pyproject.toml | 1 + tox.ini | 6 + 19 files changed, 601 insertions(+), 61 deletions(-) create mode 100644 .readthedocs.yaml create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/_static/images/pytask.ico create mode 100644 docs/source/_static/images/pytask.png create mode 100644 docs/source/_static/images/pytask.svg create mode 100644 docs/source/_static/images/pytask_w_text.png create mode 100644 docs/source/_static/images/pytask_w_text_dark.svg create mode 100644 docs/source/_static/images/pytask_w_text_light.svg create mode 100644 docs/source/changes.md create mode 100644 docs/source/conf.py create mode 100644 docs/source/custom_executors.md create mode 100644 docs/source/developers_guide.md create mode 100644 docs/source/index.md diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..1478786 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.10" + +sphinx: + configuration: docs/source/conf.py + fail_on_warning: true + +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/MANIFEST.in b/MANIFEST.in index 1eb48e3..b3ea241 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ +prune docs prune tests exclude *.md diff --git a/README.md b/README.md index ee44b20..0c01b39 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![image](https://img.shields.io/github/actions/workflow/status/pytask-dev/pytask-parallel/main.yml?branch=main)](https://github.com/pytask-dev/pytask-parallel/actions?query=branch%3Amain) [![image](https://codecov.io/gh/pytask-dev/pytask-parallel/branch/main/graph/badge.svg)](https://codecov.io/gh/pytask-dev/pytask-parallel) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pytask-dev/pytask-parallel/main.svg)](https://results.pre-commit.ci/latest/github/pytask-dev/pytask-parallel/main) -[![image](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) ______________________________________________________________________ @@ -68,65 +68,6 @@ n_workers = 1 parallel_backend = "processes" # or loky or threads ``` -## Custom Executor - -> [!NOTE] -> -> The interface for custom executors is rudimentary right now and there is not a lot of -> support by public functions. Please, give some feedback if you are trying or managed -> to use a custom backend. -> -> Also, please contribute your custom executors if you consider them useful to others. - -pytask-parallel allows you to use your parallel backend as long as it follows the -interface defined by -[`concurrent.futures.Executor`](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor). - -In some cases, adding a new backend can be as easy as registering a builder function -that receives some arguments (currently only `n_workers`) and returns the instantiated -executor. - -```python -from concurrent.futures import Executor -from my_project.executor import CustomExecutor - -from pytask_parallel import ParallelBackend, registry - - -def build_custom_executor(n_workers: int) -> Executor: - return CustomExecutor(max_workers=n_workers) - - -registry.register_parallel_backend(ParallelBackend.CUSTOM, build_custom_executor) -``` - -Now, build the project requesting your custom backend. - -```console -pytask --parallel-backend custom -``` - -Realistically, it is not the only necessary adjustment for a nice user experience. There -are two other important things. pytask-parallel does not implement them by default since -it seems more tightly coupled to your backend. - -1. A wrapper for the executed function that captures warnings, catches exceptions and - saves products of the task (within the child process!). - - As an example, see - [`def _execute_task()`](https://github.com/pytask-dev/pytask-parallel/blob/c441dbb75fa6ab3ab17d8ad5061840c802dc1c41/src/pytask_parallel/processes.py#L91-L155) - that does all that for the processes and loky backend. - -1. To apply the wrapper, you need to write a custom hook implementation for - `def pytask_execute_task()`. See - [`def pytask_execute_task()`](https://github.com/pytask-dev/pytask-parallel/blob/c441dbb75fa6ab3ab17d8ad5061840c802dc1c41/src/pytask_parallel/processes.py#L41-L65) - for an example. Use the - [`hook_module`](https://pytask-dev.readthedocs.io/en/stable/how_to_guides/extending_pytask.html#using-hook-module-and-hook-module) - configuration value to register your implementation. - -Another example of an implementation can be found as a -[test](https://github.com/pytask-dev/pytask-parallel/blob/c441dbb75fa6ab3ab17d8ad5061840c802dc1c41/tests/test_backends.py#L35-L78). - ## Some implementation details ### Parallelization and Debugging @@ -144,7 +85,8 @@ when tasks are parallelized with `--parallel-backend threads`. ## Changes -Consult the [release notes](CHANGES.md) to find out about what is new. +Consult the [release notes](https://pytask-parallel.readthedocs.io/en/stable/changes.html) to +find out about what is new. ## Development diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..8b6275a --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= -W --keep-going +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..9534b01 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/_static/images/pytask.ico b/docs/source/_static/images/pytask.ico new file mode 100644 index 0000000000000000000000000000000000000000..caaf9f9e2eb8bcb47b20283d10588491f8de1b3c GIT binary patch literal 3872 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

Z`rFoLu>lQ_`X-VQq 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 + + + + + + + + + + + diff --git a/docs/source/_static/images/pytask_w_text.png b/docs/source/_static/images/pytask_w_text.png new file mode 100644 index 0000000000000000000000000000000000000000..2f7ab1b71b2700643541386e90411431ad0a9a52 GIT binary patch literal 10387 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 S0V`VoKtoOE9!lkD=>GvB|Fii3 literal 0 HcmV?d00001 diff --git a/docs/source/_static/images/pytask_w_text_dark.svg b/docs/source/_static/images/pytask_w_text_dark.svg new file mode 100644 index 0000000..ef06dc0 --- /dev/null +++ b/docs/source/_static/images/pytask_w_text_dark.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + +pytask + diff --git a/docs/source/_static/images/pytask_w_text_light.svg b/docs/source/_static/images/pytask_w_text_light.svg new file mode 100644 index 0000000..eca9a69 --- /dev/null +++ b/docs/source/_static/images/pytask_w_text_light.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + +pytask + diff --git a/docs/source/changes.md b/docs/source/changes.md new file mode 100644 index 0000000..f07f9b6 --- /dev/null +++ b/docs/source/changes.md @@ -0,0 +1,110 @@ +# Changes + +This is a record of all past pytask-parallel releases and what went into them in reverse +chronological order. Releases follow [semantic versioning](https://semver.org/) and all +releases are available on [PyPI](https://pypi.org/project/pytask-parallel) and +[Anaconda.org](https://anaconda.org/conda-forge/pytask-parallel). + +## 0.5.0 - 2024-xx-xx + +- {pull}`85` simplifies code since loky is a dependency. +- {pull}`86` adds support for dask. +- {pull}`88` updates handling `Traceback`. +- {pull}`89` restructures the package. +- {pull}`92` redirects stdout and stderr from processes and loky and shows them in error + reports. + +## 0.4.1 - 2024-01-12 + +- {pull}`72` moves the project to `pyproject.toml`. +- {pull}`75` updates the release strategy. +- {pull}`79` add tests for Jupyter and fix parallelization with `PythonNode`s. +- {pull}`80` adds support for partialed functions. +- {pull}`82` fixes testing with pytask v0.4.5. + +## 0.4.0 - 2023-10-07 + +- {pull}`62` deprecates Python 3.7. +- {pull}`64` aligns pytask-parallel with pytask v0.4.0rc2. +- {pull}`66` deactivates parallelization for dry-runs. +- {pull}`67` fixes parallelization with partialed task functions. +- {pull}`68` raises more informative error message when `breakpoint()` was uses when + parallelizing with processes or loky. + +## 0.3.1 - 2023-05-27 + +- {pull}`56` refactors the `ProcessPoolExecutor`. +- {pull}`57` does some housekeeping. +- {pull}`59` sets the default backend to `ProcessPoolExecutor` even when loky is + installed. + +## 0.3.0 - 2023-01-23 + +- {pull}`50` deprecates INI configurations and aligns the package with pytask v0.3. +- {pull}`51` adds ruff and refurb. + +## 0.2.1 - 2022-08-19 + +- {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`. +- {pull}`46` adds a dynamic sleep duration during the execution. Fixes {issue}`42`. + +## 0.2.0 - 2022-04-15 + +- {pull}`31` adds types to the package. +- {pull}`36` adds a test for . +- {pull}`37` aligns pytask-parallel with pytask v0.2. + +## 0.1.1 - 2022-02-08 + +- {pull}`30` removes unnecessary content from `tox.ini`. +- {pull}`33` skips concurrent CI builds. +- {pull}`34` deprecates Python 3.6 and adds support for Python 3.10. + +## 0.1.0 - 2021-07-20 + +- {pull}`19` adds `conda-forge` to the `README.rst`. +- {pull}`22` add note that the debugger cannot be used together with pytask-parallel. +- {pull}`24` replaces versioneer with setuptools-scm. +- {pull}`25` aborts build and prints reports on `KeyboardInterrupt`. +- {pull}`27` enables rich tracebacks from subprocesses. + +## 0.0.8 - 2021-03-05 + +- {pull}`17` fixes the unidentifiable version. + +## 0.0.7 - 2021-03-04 + +- {pull}`14` fixes some post-release issues. +- {pull}`16` add dependencies to `setup.py` and changes the default backend to `loky`. + +## 0.0.6 - 2021-02-27 + +- {pull}`12` replaces all occurrences of `n_processes` with `n_workers`. +- {pull}`13` adds a license, versioneer, and allows publishing on PyPI. + +## 0.0.5 - 2020-12-28 + +- {pull}`5` fixes the CI and other smaller issues. +- {pull}`8` aligns pytask-parallel with task priorities in pytask v0.0.11. +- {pull}`9` enables --max-failures. Closes {issue}`7`. +- {pull}`10` releases v0.0.5. + +## 0.0.4 - 2020-10-30 + +- {pull}`4` implement an executor with `loky`. + +## 0.0.3 - 2020-09-12 + +- {pull}`3` align the program with pytask v0.0.6. + +## 0.0.2 - 2020-08-12 + +- {pull}`1` prepares the plugin for pytask v0.0.5. +- {pull}`2` better parsing and callbacks. + +## 0.0.1 - 2020-07-17 + +- Initial commit which combined the whole effort to release v0.0.1. diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..84b0d6b --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,217 @@ +"""Configuration file for the Sphinx documentation builder. + +This file only contains a selection of the most common options. For a full list see the +documentation: https://www.sphinx-doc.org/en/master/usage/configuration.html + +""" + +from __future__ import annotations + +import inspect +import os +import sys +import warnings +from importlib.metadata import version +from pathlib import Path +from typing import TYPE_CHECKING + +import pytask + +if TYPE_CHECKING: + import sphinx + + +# -- Project information --------------------------------------------------------------- + +project = "pytask" +author = "Tobias Raabe" +copyright = f"2020, {author}" # noqa: A001 + +# The version, including alpha/beta/rc tags, but not commit hash and datestamps +release = version("pytask") +# The short X.Y version. +version = ".".join(release.split(".")[:2]) + +# -- General configuration ------------------------------------------------------------- + +master_doc = "index" + +# Add any Sphinx extension module names here, as strings. They can be extensions coming +# with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + "IPython.sphinxext.ipython_console_highlighting", + "IPython.sphinxext.ipython_directive", + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", + "sphinx.ext.linkcode", + "sphinx.ext.napoleon", + "sphinxext.opengraph", + "sphinx_copybutton", + "sphinx_click", + "sphinx_toolbox.more_autodoc.autoprotocol", + "nbsphinx", + "myst_parser", + "sphinx_design", +] + +# List of patterns, relative to source directory, that match files and directories to +# ignore when looking for source files. This pattern also affects html_static_path and +# html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"] + + +pygments_style = "sphinx" +pygments_dark_style = "monokai" + +# -- Extensions configuration ---------------------------------------------------------- + +# Configuration for autodoc. +add_module_names = True + +# Remove prefixed $ for bash, >>> for Python prompts, and In [1]: for IPython prompts. +copybutton_prompt_text = r"\$ |>>> |In \[\d\]: " +copybutton_prompt_is_regexp = True + +_repo = "https://github.com/pytask-dev/pytask" +extlinks = { + "pypi": ("https://pypi.org/project/%s/", "%s"), + "issue": (f"{_repo}/issues/%s", "#%s"), + "pull": (f"{_repo}/pull/%s", "#%s"), + "user": ("https://github.com/%s", "@%s"), +} + +intersphinx_mapping = { + "click": ("https://click.palletsprojects.com/en/8.0.x/", None), + "deepdiff": ("https://zepworks.com/deepdiff/current/", None), + "networkx": ("https://networkx.org/documentation/stable", None), + "nx": ("https://networkx.org/documentation/stable", None), + "pandas": ("https://pandas.pydata.org/docs", None), + "pd": ("https://pandas.pydata.org/docs", None), + "pluggy": ("https://pluggy.readthedocs.io/en/latest", None), + "pygraphviz": ("https://pygraphviz.github.io/documentation/stable/", None), + "python": ("https://docs.python.org/3.10", None), +} + +# MyST +myst_enable_extensions = ["deflist", "dollarmath"] +myst_footnote_transition = False + +# Open Graph +ogp_social_cards = {"image": "_static/images/pytask_w_text.png"} + + +# Linkcode, based on numpy doc/source/conf.py +def linkcode_resolve(domain: str, info: dict[str, str]) -> str: # noqa: C901, PLR0912 + """Determine the URL corresponding to Python object.""" + if domain != "py": + return None + + modname = info["module"] + fullname = info["fullname"] + + submod = sys.modules.get(modname) + if submod is None: + return None + + obj = submod + for part in fullname.split("."): + try: + with warnings.catch_warnings(): + # Accessing deprecated objects will generate noisy warnings + warnings.simplefilter("ignore", FutureWarning) + obj = getattr(obj, part) + except AttributeError: # noqa: PERF203 + return None + + try: + fn = inspect.getsourcefile(inspect.unwrap(obj)) + except TypeError: + try: # property + fn = inspect.getsourcefile(inspect.unwrap(obj.fget)) + except (AttributeError, TypeError): + fn = None + if not fn: + return None + + try: + source, lineno = inspect.getsourcelines(obj) + except TypeError: + try: # property + source, lineno = inspect.getsourcelines(obj.fget) + except (AttributeError, TypeError): + lineno = None + except OSError: + lineno = None + + linespec = f"#L{lineno}-L{lineno + len(source) - 1}" if lineno else "" + + fn = os.path.relpath(fn, start=Path(pytask.__file__).parent) + + if "+" in pytask.__version__: + return ( + f"https://github.com/pytask-dev/pytask/blob/main/src/pytask/{fn}{linespec}" + ) + return ( + f"https://github.com/pytask-dev/pytask/blob/" + f"v{pytask.__version__}/src/pytask/{fn}{linespec}" + ) + + +# -- Options for HTML output ----------------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for a list of +# built-in themes. +html_theme = "furo" + +# Add any paths that contain custom static files (such as style sheets) here, relative +# to this directory. They are copied after the built-in static files, so a file named +# "default.css" will overwrite the built-in "default.css". +html_css_files = ["css/termynal.css", "css/termynal_custom.css", "css/custom.css"] + +html_js_files = ["js/termynal.js", "js/custom.js"] + +# The name of an image file (within the static path) to use as favicon of the docs. +# This file should be a Windows icon file (.ico) being 16x16 or 32x32 pixels large. +html_favicon = "_static/images/pytask.ico" + +# Add any paths that contain custom static files (such as style sheets) here, relative +# to this directory. They are copied after the builtin static files, so a file named +# "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# If false, no module index is generated. +html_domain_indices = True + +# If false, no index is generated. +html_use_index = True + +# If true, the index is split into individual pages for each letter. +html_split_index = False + +# If true, links to the reST sources are added to the pages. +html_show_sourcelink = False + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +html_show_copyright = True + +html_theme_options = { + "sidebar_hide_name": True, + "navigation_with_keys": True, + "light_logo": "images/pytask_w_text_light.svg", + "dark_logo": "images/pytask_w_text_dark.svg", +} + + +def setup(app: sphinx.application.Sphinx) -> None: + """Configure sphinx.""" + app.add_object_type( + "confval", + "confval", + objname="configuration value", + indextemplate="pair: %s; configuration value", + ) diff --git a/docs/source/custom_executors.md b/docs/source/custom_executors.md new file mode 100644 index 0000000..6d4a635 --- /dev/null +++ b/docs/source/custom_executors.md @@ -0,0 +1,58 @@ +# Custom Executors + +> [!NOTE] +> +> The interface for custom executors is rudimentary right now and there is not a lot of +> support by public functions. Please, give some feedback if you are trying or managed +> to use a custom backend. +> +> Also, please contribute your custom executors if you consider them useful to others. + +pytask-parallel allows you to use your parallel backend as long as it follows the +interface defined by +[`concurrent.futures.Executor`](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor). + +In some cases, adding a new backend can be as easy as registering a builder function +that receives some arguments (currently only `n_workers`) and returns the instantiated +executor. + +```python +from concurrent.futures import Executor +from my_project.executor import CustomExecutor + +from pytask_parallel import ParallelBackend, registry + + +def build_custom_executor(n_workers: int) -> Executor: + return CustomExecutor(max_workers=n_workers) + + +registry.register_parallel_backend(ParallelBackend.CUSTOM, build_custom_executor) +``` + +Now, build the project requesting your custom backend. + +```console +pytask --parallel-backend custom +``` + +Realistically, it is not the only necessary adjustment for a nice user experience. There +are two other important things. pytask-parallel does not implement them by default since +it seems more tightly coupled to your backend. + +1. A wrapper for the executed function that captures warnings, catches exceptions and + saves products of the task (within the child process!). + + As an example, see + [`def _execute_task()`](https://github.com/pytask-dev/pytask-parallel/blob/c441dbb75fa6ab3ab17d8ad5061840c802dc1c41/src/pytask_parallel/processes.py#L91-L155) + that does all that for the processes and loky backend. + +1. To apply the wrapper, you need to write a custom hook implementation for + `def pytask_execute_task()`. See + [`def pytask_execute_task()`](https://github.com/pytask-dev/pytask-parallel/blob/c441dbb75fa6ab3ab17d8ad5061840c802dc1c41/src/pytask_parallel/processes.py#L41-L65) + for an example. Use the + [`hook_module`](https://pytask-dev.readthedocs.io/en/stable/how_to_guides/extending_pytask.html#using-hook-module-and-hook-module) + configuration value to register your implementation. + +Another example of an implementation can be found as a +[test](https://github.com/pytask-dev/pytask-parallel/blob/c441dbb75fa6ab3ab17d8ad5061840c802dc1c41/tests/test_backends.py#L35-L78). diff --git a/docs/source/developers_guide.md b/docs/source/developers_guide.md new file mode 100644 index 0000000..8b5d88d --- /dev/null +++ b/docs/source/developers_guide.md @@ -0,0 +1 @@ +# Developer's Guide diff --git a/docs/source/index.md b/docs/source/index.md new file mode 100644 index 0000000..604e281 --- /dev/null +++ b/docs/source/index.md @@ -0,0 +1,28 @@ +# pytask-parallel + + + +[![PyPI](https://img.shields.io/pypi/v/pytask-parallel?color=blue)](https://pypi.org/project/pytask-parallel) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pytask-parallel)](https://pypi.org/project/pytask-parallel) +[![image](https://img.shields.io/conda/vn/conda-forge/pytask-parallel.svg)](https://anaconda.org/conda-forge/pytask-parallel) +[![image](https://img.shields.io/conda/pn/conda-forge/pytask-parallel.svg)](https://anaconda.org/conda-forge/pytask-parallel) +[![PyPI - License](https://img.shields.io/pypi/l/pytask-parallel)](https://pypi.org/project/pytask-parallel) +[![image](https://img.shields.io/github/actions/workflow/status/pytask-dev/pytask-parallel/main.yml?branch=main)](https://github.com/pytask-dev/pytask-parallel/actions?query=branch%3Amain) +[![image](https://codecov.io/gh/pytask-dev/pytask-parallel/branch/main/graph/badge.svg)](https://codecov.io/gh/pytask-dev/pytask-parallel) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pytask-dev/pytask-parallel/main.svg)](https://results.pre-commit.ci/latest/github/pytask-dev/pytask-parallel/main) +[![image](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) + +pytask-parallel allows to execute workflows defined with +[pytask](https://pytask-dev.readthedocs.io/) in parallel using local or remote clusters. + +## Documentation + +```{toctree} +--- +maxdepth: 1 +--- +developers_guide +changes +On Github +``` diff --git a/environment.yml b/environment.yml index dba1d27..a7ad075 100644 --- a/environment.yml +++ b/environment.yml @@ -28,5 +28,16 @@ dependencies: - nbmake - pytest-cov + # Documentation + - furo + - myst-parser + - nbsphinx + - sphinx + - sphinx-click + - sphinx-copybutton + - sphinx-design >=0.3.0 + - sphinx-toolbox + - sphinxext-opengraph + - pip: - -e . diff --git a/pyproject.toml b/pyproject.toml index 189d796..426d776 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,6 +106,7 @@ select = ["ALL"] [tool.ruff.lint.per-file-ignores] "tests/*" = ["D", "ANN", "PLR2004", "S101"] +"docs/source/conf.py" = ["INP001"] [tool.ruff.lint.isort] force-single-line = true diff --git a/tox.ini b/tox.ini index a0c7cd6..cb55d00 100644 --- a/tox.ini +++ b/tox.ini @@ -11,3 +11,9 @@ deps = git+https://github.com/pytask-dev/pytask.git@main commands = pytest {posargs} + +[testenv:docs] +extras = docs, test +commands = + - sphinx-build -n -T -b html -d {envtmpdir}/doctrees docs/source docs/build/html + - sphinx-build -n -T -b doctest -d {envtmpdir}/doctrees docs/source docs/build/html From 835ab1790bb8c58a2a7f79b9a9aa2518d68a35e5 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 5 Apr 2024 16:31:05 +0200 Subject: [PATCH 2/8] fix. --- CHANGES.md | 110 ----------------------------------------- docs/source/changes.md | 1 + 2 files changed, 1 insertion(+), 110 deletions(-) delete mode 100644 CHANGES.md diff --git a/CHANGES.md b/CHANGES.md deleted file mode 100644 index f07f9b6..0000000 --- a/CHANGES.md +++ /dev/null @@ -1,110 +0,0 @@ -# Changes - -This is a record of all past pytask-parallel releases and what went into them in reverse -chronological order. Releases follow [semantic versioning](https://semver.org/) and all -releases are available on [PyPI](https://pypi.org/project/pytask-parallel) and -[Anaconda.org](https://anaconda.org/conda-forge/pytask-parallel). - -## 0.5.0 - 2024-xx-xx - -- {pull}`85` simplifies code since loky is a dependency. -- {pull}`86` adds support for dask. -- {pull}`88` updates handling `Traceback`. -- {pull}`89` restructures the package. -- {pull}`92` redirects stdout and stderr from processes and loky and shows them in error - reports. - -## 0.4.1 - 2024-01-12 - -- {pull}`72` moves the project to `pyproject.toml`. -- {pull}`75` updates the release strategy. -- {pull}`79` add tests for Jupyter and fix parallelization with `PythonNode`s. -- {pull}`80` adds support for partialed functions. -- {pull}`82` fixes testing with pytask v0.4.5. - -## 0.4.0 - 2023-10-07 - -- {pull}`62` deprecates Python 3.7. -- {pull}`64` aligns pytask-parallel with pytask v0.4.0rc2. -- {pull}`66` deactivates parallelization for dry-runs. -- {pull}`67` fixes parallelization with partialed task functions. -- {pull}`68` raises more informative error message when `breakpoint()` was uses when - parallelizing with processes or loky. - -## 0.3.1 - 2023-05-27 - -- {pull}`56` refactors the `ProcessPoolExecutor`. -- {pull}`57` does some housekeeping. -- {pull}`59` sets the default backend to `ProcessPoolExecutor` even when loky is - installed. - -## 0.3.0 - 2023-01-23 - -- {pull}`50` deprecates INI configurations and aligns the package with pytask v0.3. -- {pull}`51` adds ruff and refurb. - -## 0.2.1 - 2022-08-19 - -- {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`. -- {pull}`46` adds a dynamic sleep duration during the execution. Fixes {issue}`42`. - -## 0.2.0 - 2022-04-15 - -- {pull}`31` adds types to the package. -- {pull}`36` adds a test for . -- {pull}`37` aligns pytask-parallel with pytask v0.2. - -## 0.1.1 - 2022-02-08 - -- {pull}`30` removes unnecessary content from `tox.ini`. -- {pull}`33` skips concurrent CI builds. -- {pull}`34` deprecates Python 3.6 and adds support for Python 3.10. - -## 0.1.0 - 2021-07-20 - -- {pull}`19` adds `conda-forge` to the `README.rst`. -- {pull}`22` add note that the debugger cannot be used together with pytask-parallel. -- {pull}`24` replaces versioneer with setuptools-scm. -- {pull}`25` aborts build and prints reports on `KeyboardInterrupt`. -- {pull}`27` enables rich tracebacks from subprocesses. - -## 0.0.8 - 2021-03-05 - -- {pull}`17` fixes the unidentifiable version. - -## 0.0.7 - 2021-03-04 - -- {pull}`14` fixes some post-release issues. -- {pull}`16` add dependencies to `setup.py` and changes the default backend to `loky`. - -## 0.0.6 - 2021-02-27 - -- {pull}`12` replaces all occurrences of `n_processes` with `n_workers`. -- {pull}`13` adds a license, versioneer, and allows publishing on PyPI. - -## 0.0.5 - 2020-12-28 - -- {pull}`5` fixes the CI and other smaller issues. -- {pull}`8` aligns pytask-parallel with task priorities in pytask v0.0.11. -- {pull}`9` enables --max-failures. Closes {issue}`7`. -- {pull}`10` releases v0.0.5. - -## 0.0.4 - 2020-10-30 - -- {pull}`4` implement an executor with `loky`. - -## 0.0.3 - 2020-09-12 - -- {pull}`3` align the program with pytask v0.0.6. - -## 0.0.2 - 2020-08-12 - -- {pull}`1` prepares the plugin for pytask v0.0.5. -- {pull}`2` better parsing and callbacks. - -## 0.0.1 - 2020-07-17 - -- Initial commit which combined the whole effort to release v0.0.1. diff --git a/docs/source/changes.md b/docs/source/changes.md index f07f9b6..fcd06ee 100644 --- a/docs/source/changes.md +++ b/docs/source/changes.md @@ -13,6 +13,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask-parallel) and - {pull}`89` restructures the package. - {pull}`92` redirects stdout and stderr from processes and loky and shows them in error reports. +- {pull}`93` adds documentation on readthedocs. ## 0.4.1 - 2024-01-12 From ae8be36ca6af66a41076c2688e4d6f7ba55e6f08 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 5 Apr 2024 16:34:33 +0200 Subject: [PATCH 3/8] fix. --- pyproject.toml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 426d776..d28f4ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,19 @@ email = "raabe@posteo.de" [project.optional-dependencies] dask = ["dask[complete]", "distributed"] +docs = [ + "furo", + "ipython", + "matplotlib", + "myst-parser", + "nbsphinx", + "sphinx", + "sphinx-click", + "sphinx-copybutton", + "sphinx-design>=0.3", + "sphinx-toolbox", + "sphinxext-opengraph", +] test = [ "pytask-parallel[all]", "nbmake", From d082e28d301c2c1826cca891d77237d73470caac Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sat, 6 Apr 2024 13:04:28 +0200 Subject: [PATCH 4/8] Add more docs. --- .pre-commit-config.yaml | 10 +++ README.md | 39 +++------- docs/source/conf.py | 9 +-- docs/source/custom_executors.md | 17 +++-- docs/source/dask.md | 122 ++++++++++++++++++++++++++++++++ docs/source/developers_guide.md | 13 ++++ docs/source/index.md | 3 + docs/source/quickstart.md | 99 ++++++++++++++++++++++++++ src/pytask_parallel/backends.py | 2 + src/pytask_parallel/build.py | 2 +- src/pytask_parallel/config.py | 10 ++- 11 files changed, 277 insertions(+), 49 deletions(-) create mode 100644 docs/source/dask.md create mode 100644 docs/source/quickstart.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9aca857..986cb85 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,6 +35,16 @@ repos: rev: 0.7.1 hooks: - id: nbstripout +- repo: https://github.com/executablebooks/mdformat + rev: 0.7.17 + hooks: + - id: mdformat + additional_dependencies: [ + mdformat-myst, + mdformat-black, + mdformat-pyproject, + ] + files: (docs/.) # Conflicts with admonitions. # - repo: https://github.com/executablebooks/mdformat # rev: 0.7.17 diff --git a/README.md b/README.md index 0c01b39..f734800 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ______________________________________________________________________ -Parallelize the execution of tasks with `pytask-parallel` which is a plugin for +Parallelize the execution of tasks with `pytask-parallel`, a plugin for [pytask](https://github.com/pytask-dev/pytask). ## Installation @@ -28,11 +28,14 @@ $ pip install pytask-parallel $ conda install -c conda-forge pytask-parallel ``` -By default, the plugin uses `concurrent.futures.ProcessPoolExecutor`. +By default, the plugin uses loky's reusable executor. -It is also possible to select the executor from loky or `ThreadPoolExecutor` from the -[concurrent.futures](https://docs.python.org/3/library/concurrent.futures.html) module -as backends to execute tasks asynchronously. +The following backends are available: + +- loky's [`get_reusable_executor`](https://loky.readthedocs.io/en/stable/API.html#loky.get_reusable_executor) +- `ProcessPoolExecutor` or `ThreadPoolExecutor` from + [concurrent.futures](https://docs.python.org/3/library/concurrent.futures.html) +- dask's [`ClientExecutor`](https://distributed.dask.org/en/stable/api.html#distributed.Client.get_executor) allows in combination with [coiled](https://docs.coiled.io/user_guide/index.html) to spawn clusters and workers on AWS, GCP, and other providers with minimal configuration. ## Usage @@ -65,12 +68,10 @@ You can also set the options in a `pyproject.toml`. [tool.pytask.ini_options] n_workers = 1 -parallel_backend = "processes" # or loky or threads +parallel_backend = "loky" # or processes or threads ``` -## Some implementation details - -### Parallelization and Debugging +## Parallelization and Debugging It is not possible to combine parallelization with debugging. That is why `--pdb` or `--trace` deactivate parallelization. @@ -78,27 +79,7 @@ It is not possible to combine parallelization with debugging. That is why `--pdb If you parallelize the execution of your tasks using two or more workers, do not use `breakpoint()` or `import pdb; pdb.set_trace()` since both will cause exceptions. -### Threads and warnings - -Capturing warnings is not thread-safe. Therefore, warnings cannot be captured reliably -when tasks are parallelized with `--parallel-backend threads`. - ## Changes Consult the [release notes](https://pytask-parallel.readthedocs.io/en/stable/changes.html) to find out about what is new. - -## Development - -- `pytask-parallel` does not call the `pytask_execute_task_protocol` hook - specification/entry-point because `pytask_execute_task_setup` and - `pytask_execute_task` need to be separated from `pytask_execute_task_teardown`. Thus, - plugins that change this hook specification may not interact well with the - parallelization. - -- Two PRs for CPython try to re-enable setting custom reducers which should have been - working but does not. Here are the references. - - - https://bugs.python.org/issue28053 - - https://github.com/python/cpython/pull/9959 - - https://github.com/python/cpython/pull/15058 diff --git a/docs/source/conf.py b/docs/source/conf.py index 84b0d6b..46d71c5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -84,13 +84,8 @@ intersphinx_mapping = { "click": ("https://click.palletsprojects.com/en/8.0.x/", None), - "deepdiff": ("https://zepworks.com/deepdiff/current/", None), - "networkx": ("https://networkx.org/documentation/stable", None), - "nx": ("https://networkx.org/documentation/stable", None), - "pandas": ("https://pandas.pydata.org/docs", None), - "pd": ("https://pandas.pydata.org/docs", None), - "pluggy": ("https://pluggy.readthedocs.io/en/latest", None), - "pygraphviz": ("https://pygraphviz.github.io/documentation/stable/", None), + "coiled": ("https://docs.coiled.io/", None), + "dask": ("https://docs.dask.org/en/stable/", None), "python": ("https://docs.python.org/3.10", None), } diff --git a/docs/source/custom_executors.md b/docs/source/custom_executors.md index 6d4a635..49b61ec 100644 --- a/docs/source/custom_executors.md +++ b/docs/source/custom_executors.md @@ -1,16 +1,15 @@ # Custom Executors -> [!NOTE] -> -> The interface for custom executors is rudimentary right now and there is not a lot of -> support by public functions. Please, give some feedback if you are trying or managed -> to use a custom backend. -> -> Also, please contribute your custom executors if you consider them useful to others. +```{important} +The interface for custom executors is rudimentary right now. Please, give some feedback +if you managed to implement a custom executor or have suggestions for improvement. + +Please, also consider contributing your executor to pytask-parallel if you believe it +could be helpful to other people. Start by creating an issue or a draft PR. +``` pytask-parallel allows you to use your parallel backend as long as it follows the -interface defined by -[`concurrent.futures.Executor`](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor). +interface defined by {class}`concurrent.futures.Executor`. In some cases, adding a new backend can be as easy as registering a builder function that receives some arguments (currently only `n_workers`) and returns the instantiated diff --git a/docs/source/dask.md b/docs/source/dask.md new file mode 100644 index 0000000..aed69e5 --- /dev/null +++ b/docs/source/dask.md @@ -0,0 +1,122 @@ +# Dask + +Dask is a flexible library for parallel and distributed computing. You probably know it +from its {class}`dask.dataframe` that allows lazy processing of big data. Here, we use +{class}`dask.distributed` that provides an interface similar to +{class}`concurrent.futures.Executor` to parallelize our execution. + +There are a couple of ways in how we can use dask. + +## Local + +By default, using dask as the parallel backend will launch a {class}`dask.LocalCluster` +with processes on your local machine. + +`````{tab-set} +````{tab-item} CLI +```console +pytask --parallel-backend dask -n 2 +``` +```` +````{tab-item} Configuration +```toml +[tool.pytask.ini_options] +parallel_backend = "dask" +n_workers = 2 +``` +```` +````` + +## Local or Remote - Connecting to a Scheduler + +It is also possible to connect to an existing scheduler and use it to execute tasks. The +scheduler can be launched on your local machine or in some remote environment. It also +has the benefit of being able to inspect the dask dashboard for more information on the +execution. + +Start by launching a scheduler in some terminal on some machine. + +```console +dask scheduler +``` + +After the launch, the IP of the scheduler will be displayed. Copy it. Then, open more +terminals to launch as many dask workers as you like with + +```console +dask worker +``` + +Finally, write a function to build the dask client and register it as the dask backend. +Place the code somewhere in your codebase, preferably, where you store the main +configuration of your project in `config.py` or another module that will be imported +during execution. + +```python +from pytask_parallel import ParallelBackend +from pytask_parallel import registry +from concurrent.futures import Executor +from dask.distributed import Client + + +def _build_dask_executor(n_workers: int) -> Executor: + return Client(address="").get_executor() + + +registry.register_parallel_backend(ParallelBackend.DASK, _build_dask_executor) +``` + +You can also register it as the custom executor using +{class}`pytask_parallel.ParallelBackend.CUSTOM` to switch back to the default dask +executor quickly. + +```{seealso} +You can find more information in the documentation for +[`dask.distributed`](https://distributed.dask.org/en/stable/). +``` + +## Remote - Using cloud providers with coiled + +[coiled](https://www.coiled.io/) is a product built on top of dask that eases the +deployment of your workflow to many cloud providers like AWS, GCP, and Azure. + +They offer a [free monthly tier](https://www.coiled.io/pricing) where you only +need to pay the costs for your cloud provider and you can get started without a credit +card. + +Furthermore, they offer the following benefits which are especially helpful to people +who are not familiar with cloud providers or remote computing. + +- A [four step short process](https://docs.coiled.io/user_guide/setup/index.html) to set + up your local environment and configure your cloud provider. +- coiled manages your resources by spawning workers if you need them and shutting them + down if they are idle. +- Synchronization of your local environment to remote workers. + +So, how can you run your pytask workflow on a cloud infrastructure with coiled? + +1. Follow their [guide on getting + started](https://docs.coiled.io/user_guide/setup/index.html) by creating a coiled + account and syncing it with your cloud provider. + +1. Register a function that builds an executor using {class}`coiled.Cluster`. + + ```python + import coiled + from pytask_parallel import ParallelBackend + from pytask_parallel import registry + from concurrent.futures import Executor + + + def _build_coiled_executor(n_workers: int) -> Executor: + return coiled.Cluster(n_workers=n_workers).get_client().get_executor() + + + registry.register_parallel_backend(ParallelBackend.CUSTOM, _build_coiled_executor) + ``` + +1. Execute your workflow with + + ```console + pytask --parallel-backend custom + ``` diff --git a/docs/source/developers_guide.md b/docs/source/developers_guide.md index 8b5d88d..f2fa790 100644 --- a/docs/source/developers_guide.md +++ b/docs/source/developers_guide.md @@ -1 +1,14 @@ # Developer's Guide + +`pytask-parallel` does not call the `pytask_execute_task_protocol` hook +specification/entry-point because `pytask_execute_task_setup` and +`pytask_execute_task` need to be separated from `pytask_execute_task_teardown`. Thus, +plugins that change this hook specification may not interact well with the +parallelization. + +Two PRs for CPython try to re-enable setting custom reducers which should have been +working but does not. Here are the references. + +- https://bugs.python.org/issue28053 +- https://github.com/python/cpython/pull/9959 +- https://github.com/python/cpython/pull/15058 diff --git a/docs/source/index.md b/docs/source/index.md index 604e281..6209512 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -22,6 +22,9 @@ pytask-parallel allows to execute workflows defined with --- maxdepth: 1 --- +quickstart +dask +custom_executors developers_guide changes On Github diff --git a/docs/source/quickstart.md b/docs/source/quickstart.md new file mode 100644 index 0000000..7da156b --- /dev/null +++ b/docs/source/quickstart.md @@ -0,0 +1,99 @@ +# Quickstart + +## Installation + +pytask-parallel is available on [PyPI](https://pypi.org/project/pytask-parallel) and +[Anaconda.org](https://anaconda.org/conda-forge/pytask-parallel). Install it with + +```console +$ pip install pytask-parallel + +# or + +$ conda install -c conda-forge pytask-parallel +``` + +## Usage + +To parallelize the execution of your workflow using the default backend, +[loky](https://loky.readthedocs.io/), pass an integer greater than 1 or `'auto'` to the +command-line interface. By default, only one worker is used. + +```console +$ pytask -n 2 +$ pytask --n-workers 2 + +# Starts os.cpu_count() - 1 workers. +$ pytask -n auto +``` + +To use a different backend, pass the `--parallel-backend` option. The following command +will execute the workflow with one worker and the loky backend. + +```console +pytask --parallel-backend loky +``` + +The options can also be specified in the configuration file. + +```toml +[tool.pytask.ini_options] +n_workers = 2 +parallel_backend = "loky" +``` + +## Backends + +```{important} +It is not possible to combine parallelization with debugging. That is why `--pdb` or +`--trace` deactivate parallelization. + +If you parallelize the execution of your tasks using two or more workers, do not use +`breakpoint()` or `import pdb; pdb.set_trace()` since both will cause exceptions. +``` + +### loky + +There are multiple backends available. The default is the backend provided by loky which aims to be a more robust implementation of {class}`multiprocessing.pool.Pool` and in {class}`concurrent.futures.ProcessPoolExecutor`. + +```console +pytask --parallel-backend loky +``` + +As it spawns workers in new processes to run the tasks, it is especially suited for +CPU-bound tasks. ([Here](https://stackoverflow.com/a/868577/7523785) is an +explanation of what CPU- or IO-bound means.) + +### `concurrent.futures` + +You can use the values `threads` and `processes` to use the {class}`concurrent.futures.ThreadPoolExecutor` or the {class}`concurrent.futures.ProcessPoolExecutor` respectively. + +The `ThreadPoolExecutor` might be an interesting option for you if you have many IO-bound tasks and you do not need to create many expensive processes. + +```console +pytask --parallel-backend threads +pytask --parallel-backend processes +``` + +```{important} +Capturing warnings is not thread-safe. Therefore, warnings cannot be captured reliably +when tasks are parallelized with `--parallel-backend threads`. +``` + +### dask + coiled + +dask and coiled together provide the option to execute your workflow on cloud providers like AWS, GCP or Azure. Check out the [dedicated guide](dask.md) if you are interested in that. + +Using the default mode, dask will spawn multiple local workers to process the tasks. + +```console +pytask --parallel-backend dask +``` + +### Custom executors + +You can also use any custom executor that implements the {class}`concurrent.futures.Executor` interface. Read more about it in [](custom_executors.md). + +```{important} +Please, consider contributing your executor to pytask-parallel if you believe it could be helpful to other people. Start by creating an issue or a draft PR. +``` diff --git a/src/pytask_parallel/backends.py b/src/pytask_parallel/backends.py index 9db8060..26f56e2 100644 --- a/src/pytask_parallel/backends.py +++ b/src/pytask_parallel/backends.py @@ -84,6 +84,8 @@ def _get_thread_pool_executor(n_workers: int) -> Executor: class ParallelBackend(Enum): """Choices for parallel backends.""" + NONE = "none" + CUSTOM = "custom" DASK = "dask" LOKY = "loky" diff --git a/src/pytask_parallel/build.py b/src/pytask_parallel/build.py index 11a1a0b..b54bd08 100644 --- a/src/pytask_parallel/build.py +++ b/src/pytask_parallel/build.py @@ -26,7 +26,7 @@ def pytask_extend_command_line_interface(cli: click.Group) -> None: ["--parallel-backend"], type=EnumChoice(ParallelBackend), help="Backend for the parallelization.", - default=ParallelBackend.LOKY, + default=ParallelBackend.NONE, ), ] cli.commands["build"].params.extend(additional_parameters) diff --git a/src/pytask_parallel/config.py b/src/pytask_parallel/config.py index c60c828..fc799d4 100644 --- a/src/pytask_parallel/config.py +++ b/src/pytask_parallel/config.py @@ -21,9 +21,6 @@ def pytask_parse_config(config: dict[str, Any]) -> None: """Parse the configuration.""" __tracebackhide__ = True - if config["n_workers"] == "auto": - config["n_workers"] = max(os.cpu_count() - 1, 1) - try: config["parallel_backend"] = ParallelBackend(config["parallel_backend"]) except ValueError: @@ -33,6 +30,13 @@ def pytask_parse_config(config: dict[str, Any]) -> None: ) raise ValueError(msg) from None + if config["n_workers"] == "auto": + config["n_workers"] = max(os.cpu_count() - 1, 1) + + # If more than one worker is used, and no backend is set, use loky. + if config["n_workers"] > 1 and config["parallel_backend"] == ParallelBackend.NONE: + config["parallel_backend"] = ParallelBackend.LOKY + config["delay"] = 0.1 From 805a771f1d13a9177985e2a678ebf8f496edddb1 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sat, 6 Apr 2024 13:11:00 +0200 Subject: [PATCH 5/8] Extract changes. --- docs/source/changes.md | 1 + src/pytask_parallel/backends.py | 2 -- src/pytask_parallel/build.py | 2 +- src/pytask_parallel/config.py | 10 +++------- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/docs/source/changes.md b/docs/source/changes.md index fcd06ee..ec9cb68 100644 --- a/docs/source/changes.md +++ b/docs/source/changes.md @@ -14,6 +14,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask-parallel) and - {pull}`92` redirects stdout and stderr from processes and loky and shows them in error reports. - {pull}`93` adds documentation on readthedocs. +- {pull}`94` implements `ParallelBackend.NONE` as the default backend. ## 0.4.1 - 2024-01-12 diff --git a/src/pytask_parallel/backends.py b/src/pytask_parallel/backends.py index 26f56e2..9db8060 100644 --- a/src/pytask_parallel/backends.py +++ b/src/pytask_parallel/backends.py @@ -84,8 +84,6 @@ def _get_thread_pool_executor(n_workers: int) -> Executor: class ParallelBackend(Enum): """Choices for parallel backends.""" - NONE = "none" - CUSTOM = "custom" DASK = "dask" LOKY = "loky" diff --git a/src/pytask_parallel/build.py b/src/pytask_parallel/build.py index b54bd08..11a1a0b 100644 --- a/src/pytask_parallel/build.py +++ b/src/pytask_parallel/build.py @@ -26,7 +26,7 @@ def pytask_extend_command_line_interface(cli: click.Group) -> None: ["--parallel-backend"], type=EnumChoice(ParallelBackend), help="Backend for the parallelization.", - default=ParallelBackend.NONE, + default=ParallelBackend.LOKY, ), ] cli.commands["build"].params.extend(additional_parameters) diff --git a/src/pytask_parallel/config.py b/src/pytask_parallel/config.py index fc799d4..c60c828 100644 --- a/src/pytask_parallel/config.py +++ b/src/pytask_parallel/config.py @@ -21,6 +21,9 @@ def pytask_parse_config(config: dict[str, Any]) -> None: """Parse the configuration.""" __tracebackhide__ = True + if config["n_workers"] == "auto": + config["n_workers"] = max(os.cpu_count() - 1, 1) + try: config["parallel_backend"] = ParallelBackend(config["parallel_backend"]) except ValueError: @@ -30,13 +33,6 @@ def pytask_parse_config(config: dict[str, Any]) -> None: ) raise ValueError(msg) from None - if config["n_workers"] == "auto": - config["n_workers"] = max(os.cpu_count() - 1, 1) - - # If more than one worker is used, and no backend is set, use loky. - if config["n_workers"] > 1 and config["parallel_backend"] == ParallelBackend.NONE: - config["parallel_backend"] = ParallelBackend.LOKY - config["delay"] = 0.1 From 916fa8b64c100563b47970df41b3521154f1c459 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sat, 6 Apr 2024 13:18:45 +0200 Subject: [PATCH 6/8] Document daks issue. --- docs/source/dask.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/dask.md b/docs/source/dask.md index aed69e5..c0a5645 100644 --- a/docs/source/dask.md +++ b/docs/source/dask.md @@ -1,5 +1,11 @@ # Dask +```{important} +Currently, the dask backend can only be used if your workflow code is organized in a +package due to how pytask imports your code and dask serializes task functions +([issue](https://github.com/dask/distributed/issues/8607)). +``` + Dask is a flexible library for parallel and distributed computing. You probably know it from its {class}`dask.dataframe` that allows lazy processing of big data. Here, we use {class}`dask.distributed` that provides an interface similar to From 30d03e882ce275f218c1eadd7d07f3b1be6055b6 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sun, 7 Apr 2024 14:49:07 +0200 Subject: [PATCH 7/8] fix. --- docs/source/conf.py | 21 +++--- docs/source/custom_executors.md | 4 +- docs/source/dask.md | 10 +-- docs/source/quickstart.md | 110 +++++++++++++++++++++++++++----- 4 files changed, 112 insertions(+), 33 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 46d71c5..f537001 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,7 +15,7 @@ from pathlib import Path from typing import TYPE_CHECKING -import pytask +import pytask_parallel if TYPE_CHECKING: import sphinx @@ -23,12 +23,12 @@ # -- Project information --------------------------------------------------------------- -project = "pytask" +project = "pytask_parallel" author = "Tobias Raabe" copyright = f"2020, {author}" # noqa: A001 # The version, including alpha/beta/rc tags, but not commit hash and datestamps -release = version("pytask") +release = version("pytask_parallel") # The short X.Y version. version = ".".join(release.split(".")[:2]) @@ -74,7 +74,7 @@ copybutton_prompt_text = r"\$ |>>> |In \[\d\]: " copybutton_prompt_is_regexp = True -_repo = "https://github.com/pytask-dev/pytask" +_repo = "https://github.com/pytask-dev/pytask-parallel" extlinks = { "pypi": ("https://pypi.org/project/%s/", "%s"), "issue": (f"{_repo}/issues/%s", "#%s"), @@ -86,6 +86,7 @@ "click": ("https://click.palletsprojects.com/en/8.0.x/", None), "coiled": ("https://docs.coiled.io/", None), "dask": ("https://docs.dask.org/en/stable/", None), + "distributed": ("https://distributed.dask.org/en/stable/", None), "python": ("https://docs.python.org/3.10", None), } @@ -142,15 +143,13 @@ def linkcode_resolve(domain: str, info: dict[str, str]) -> str: # noqa: C901, P linespec = f"#L{lineno}-L{lineno + len(source) - 1}" if lineno else "" - fn = os.path.relpath(fn, start=Path(pytask.__file__).parent) + fn = os.path.relpath(fn, start=Path(pytask_parallel.__file__).parent) - if "+" in pytask.__version__: - return ( - f"https://github.com/pytask-dev/pytask/blob/main/src/pytask/{fn}{linespec}" - ) + if "+" in pytask_parallel.__version__: + return f"https://github.com/pytask-dev/pytask-parallel/blob/main/src/pytask_parallel/{fn}{linespec}" return ( - f"https://github.com/pytask-dev/pytask/blob/" - f"v{pytask.__version__}/src/pytask/{fn}{linespec}" + f"https://github.com/pytask-dev/pytask-parallel/blob/" + f"v{pytask_parallel.__version__}/src/pytask_parallel/{fn}{linespec}" ) diff --git a/docs/source/custom_executors.md b/docs/source/custom_executors.md index 49b61ec..f44b377 100644 --- a/docs/source/custom_executors.md +++ b/docs/source/custom_executors.md @@ -1,6 +1,6 @@ # Custom Executors -```{important} +```{caution} The interface for custom executors is rudimentary right now. Please, give some feedback if you managed to implement a custom executor or have suggestions for improvement. @@ -9,7 +9,7 @@ could be helpful to other people. Start by creating an issue or a draft PR. ``` pytask-parallel allows you to use your parallel backend as long as it follows the -interface defined by {class}`concurrent.futures.Executor`. +interface defined by {class}`~concurrent.futures.Executor`. In some cases, adding a new backend can be as easy as registering a builder function that receives some arguments (currently only `n_workers`) and returns the instantiated diff --git a/docs/source/dask.md b/docs/source/dask.md index c0a5645..e0b2ac1 100644 --- a/docs/source/dask.md +++ b/docs/source/dask.md @@ -1,6 +1,6 @@ # Dask -```{important} +```{caution} Currently, the dask backend can only be used if your workflow code is organized in a package due to how pytask imports your code and dask serializes task functions ([issue](https://github.com/dask/distributed/issues/8607)). @@ -8,15 +8,15 @@ package due to how pytask imports your code and dask serializes task functions Dask is a flexible library for parallel and distributed computing. You probably know it from its {class}`dask.dataframe` that allows lazy processing of big data. Here, we use -{class}`dask.distributed` that provides an interface similar to -{class}`concurrent.futures.Executor` to parallelize our execution. +{mod}`distributed` that provides an interface similar to +{class}`~concurrent.futures.Executor` to parallelize our execution. There are a couple of ways in how we can use dask. ## Local -By default, using dask as the parallel backend will launch a {class}`dask.LocalCluster` -with processes on your local machine. +By default, using dask as the parallel backend will launch a +{class}`distributed.LocalCluster` with processes on your local machine. `````{tab-set} ````{tab-item} CLI diff --git a/docs/source/quickstart.md b/docs/source/quickstart.md index 7da156b..b780fcc 100644 --- a/docs/source/quickstart.md +++ b/docs/source/quickstart.md @@ -15,32 +15,56 @@ $ conda install -c conda-forge pytask-parallel ## Usage -To parallelize the execution of your workflow using the default backend, -[loky](https://loky.readthedocs.io/), pass an integer greater than 1 or `'auto'` to the -command-line interface. By default, only one worker is used. +When the plugin is only installed and pytask executed, the tasks are not run in +parallel. + +For parallelization with the default backend [loky](https://loky.readthedocs.io/), you need to launch multiple workers. + +`````{tab-set} +````{tab-item} CLI +:sync: cli ```console -$ pytask -n 2 -$ pytask --n-workers 2 +pytask -n 2 +pytask --n-workers 2 # Starts os.cpu_count() - 1 workers. -$ pytask -n auto +pytask -n auto ``` +```` +````{tab-item} Configuration +:sync: configuration + +```toml +[tool.pytask.ini_options] +n_workers = 2 + +# Starts os.cpu_count() - 1 workers. +n_workers = "auto" +``` +```` +````` To use a different backend, pass the `--parallel-backend` option. The following command will execute the workflow with one worker and the loky backend. +`````{tab-set} +````{tab-item} CLI +:sync: cli + ```console pytask --parallel-backend loky ``` - -The options can also be specified in the configuration file. +```` +````{tab-item} Configuration +:sync: configuration ```toml [tool.pytask.ini_options] -n_workers = 2 parallel_backend = "loky" ``` +```` +````` ## Backends @@ -54,7 +78,9 @@ If you parallelize the execution of your tasks using two or more workers, do not ### loky -There are multiple backends available. The default is the backend provided by loky which aims to be a more robust implementation of {class}`multiprocessing.pool.Pool` and in {class}`concurrent.futures.ProcessPoolExecutor`. +There are multiple backends available. The default is the backend provided by loky which +aims to be a more robust implementation of {class}`~multiprocessing.pool.Pool` and in +{class}`~concurrent.futures.ProcessPoolExecutor`. ```console pytask --parallel-backend loky @@ -66,14 +92,49 @@ explanation of what CPU- or IO-bound means.) ### `concurrent.futures` -You can use the values `threads` and `processes` to use the {class}`concurrent.futures.ThreadPoolExecutor` or the {class}`concurrent.futures.ProcessPoolExecutor` respectively. +You can use the values `threads` and `processes` to use the +{class}`~concurrent.futures.ThreadPoolExecutor` or the +{class}`~concurrent.futures.ProcessPoolExecutor` respectively. -The `ThreadPoolExecutor` might be an interesting option for you if you have many IO-bound tasks and you do not need to create many expensive processes. +The {class}`~concurrent.futures.ThreadPoolExecutor` might be an interesting option for +you if you have many IO-bound tasks and you do not need to create many expensive +processes. + +`````{tab-set} +````{tab-item} CLI +:sync: cli ```console pytask --parallel-backend threads +``` +```` +````{tab-item} Configuration +:sync: configuration + +```toml +[tool.pytask.ini_options] +parallel_backend = "threads" +``` +```` +````` + +`````{tab-set} +````{tab-item} CLI +:sync: cli + +```console pytask --parallel-backend processes ``` +```` +````{tab-item} Configuration +:sync: configuration + +```toml +[tool.pytask.ini_options] +parallel_backend = "processes" +``` +```` +````` ```{important} Capturing warnings is not thread-safe. Therefore, warnings cannot be captured reliably @@ -82,18 +143,37 @@ when tasks are parallelized with `--parallel-backend threads`. ### dask + coiled -dask and coiled together provide the option to execute your workflow on cloud providers like AWS, GCP or Azure. Check out the [dedicated guide](dask.md) if you are interested in that. +dask and coiled together provide the option to execute your workflow on cloud providers +like AWS, GCP or Azure. Check out the [dedicated guide](dask.md) if you are interested +in that. Using the default mode, dask will spawn multiple local workers to process the tasks. +`````{tab-set} +````{tab-item} CLI +:sync: cli + ```console pytask --parallel-backend dask ``` +```` +````{tab-item} Configuration +:sync: configuration + +```toml +[tool.pytask.ini_options] +parallel_backend = "dask" +``` +```` +````` ### Custom executors -You can also use any custom executor that implements the {class}`concurrent.futures.Executor` interface. Read more about it in [](custom_executors.md). +You can also use any custom executor that implements the +{class}`~concurrent.futures.Executor` interface. Read more about it in +[](custom_executors.md). ```{important} -Please, consider contributing your executor to pytask-parallel if you believe it could be helpful to other people. Start by creating an issue or a draft PR. +Please, consider contributing your executor to pytask-parallel if you believe it could +be helpful to other people. Start by creating an issue or a draft PR. ``` From e00ff03df2420cc07d7f5c1344fa2b465410a5f7 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sun, 7 Apr 2024 14:51:23 +0200 Subject: [PATCH 8/8] fix. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index f734800..61b96e6 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,10 @@ It is not possible to combine parallelization with debugging. That is why `--pdb If you parallelize the execution of your tasks using two or more workers, do not use `breakpoint()` or `import pdb; pdb.set_trace()` since both will cause exceptions. +## Documentation + +You find the documentation at . + ## Changes Consult the [release notes](https://pytask-parallel.readthedocs.io/en/stable/changes.html) to