Skip to content

Commit fe6f022

Browse files
authored
Merge branch 'main' into pre-commit-ci-update-config
2 parents 6e73ce4 + 0d2749b commit fe6f022

26 files changed

+414
-59
lines changed

docs/source/changes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and
2323
- {pull}`528` improves the codecov setup and coverage.
2424
- {pull}`535` reenables and fixes tests with Jupyter.
2525
- {pull}`536` allows partialed functions to be task functions.
26+
- {pull}`539` implements the {confval}`hook_module` configuration value and
27+
`--hook-module` commandline option to register hooks.
2628
- {pull}`540` changes the CLI entry-point and allow `pytask.build(tasks=task_func)` as
2729
the signatures suggested.
2830
- {pull}`542` refactors the plugin manager.
31+
- {pull}`543` fixes imports in tests and related issues.
2932

3033
## 0.4.4 - 2023-12-04
3134

docs/source/how_to_guides/how_to_write_a_plugin.md renamed to docs/source/how_to_guides/extending_pytask.md

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,71 @@
1-
# How to write a plugin
1+
# Extending pytask
22

3-
Since pytask is based on pluggy, it is extensible. In this section, you will learn some
4-
key concepts you need to know to write a plugin. It won't deal with pluggy in detail,
5-
but if you are interested feel free to read [pluggy](../explanations/pluggy.md). A quick
6-
look at the first paragraphs might be useful nonetheless.
3+
pytask can be extended since it is built upon
4+
[pluggy](https://pluggy.readthedocs.io/en/latest/), a plugin system for Python.
75

8-
## Preparation
6+
How does it work? Throughout the execution, pytask arrives at entrypoints, called hook
7+
functions. When pytask calls a hook function it loops through hook implementations and
8+
each hook implementation can alter the result of the entrypoint.
9+
10+
The full list of hook functions is specified in {doc}`../reference_guides/hookspecs`.
11+
12+
More general information about pluggy can be found in its
13+
[documentation](https://pluggy.readthedocs.io/en/latest/).
14+
15+
There are two ways to add new hook implementations.
16+
17+
1. Using the {option}`pytask build --hook-module` commandline option or the
18+
{confval}`hook_module` configuration value.
19+
1. Packaging your plugin as a Python package to publish and share it.
20+
21+
(hook-module)=
22+
23+
## Using `--hook-module` and `hook_module`
24+
25+
The easiest and quickest way to extend pytask is to create a module, for example,
26+
`hooks.py` and register it temporarily via the commandline option or permanently via the
27+
configuration.
28+
29+
```console
30+
pytask --hook-module hooks.py
31+
```
32+
33+
or
34+
35+
```toml
36+
[tool.pytask.ini_options]
37+
hook_module = ["hooks.py"]
38+
```
39+
40+
The value can be a path. If the path is relative it is assumed to be relative to the
41+
configuration file or relative to the current working directory as a fallback.
42+
43+
The value can also be a module name. For example, if `hooks.py` lies your projects
44+
package called `myproject` which is importable, then, you can also use
45+
46+
```toml
47+
[tool.pytask.ini_options]
48+
hook_module = ["myproject.hooks"]
49+
```
50+
51+
In `hooks.py` we can add another commandline option to `pytask build` by providing an
52+
addition hook implementation for the hook specification
53+
{func}`~_pytask.hookspecs.pytask_extend_command_line_interface`.
54+
55+
```python
56+
import click
57+
from _pytask.pluginmanager import hookimpl
58+
59+
60+
@hookimpl
61+
def pytask_extend_command_line_interface(cli):
62+
"""Add parameters to the command line interface."""
63+
cli.commands["build"].params.append(click.Option(["--hello"]))
64+
```
65+
66+
## Packaging a plugin
67+
68+
### Preparation
969

1070
Before you start implementing your plugin, the following notes may help you.
1171

@@ -24,11 +84,11 @@ Before you start implementing your plugin, the following notes may help you.
2484
for your plugin to get feedback from other developers. Your proposal should be concise
2585
and explain what problem you want to solve and how.
2686

27-
## Writing your plugin
87+
### Writing your plugin
2888

2989
This section explains some steps which are required for all plugins.
3090

31-
### Set up the setuptools entry-point
91+
#### Set up the setuptools entry-point
3292

3393
pytask discovers plugins via `setuptools` entry-points. Following the approach advocated
3494
for by [setuptools_scm](https://github.com/pypa/setuptools_scm), the entry-point is
@@ -65,7 +125,7 @@ For a complete example with `setuptools_scm` and `pyproject.toml` see the
65125
The entry-point for pytask is called `"pytask"` and points to a module called
66126
`pytask_plugin.plugin`.
67127

68-
### `plugin.py`
128+
#### `plugin.py`
69129

70130
`plugin.py` is the entrypoint for pytask to your package. You can put all of your hook
71131
implementations in this module, but it is recommended to imitate the structure of pytask

docs/source/how_to_guides/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ how_to_influence_build_order
2020
hashing_inputs_of_tasks
2121
using_task_returns
2222
writing_custom_nodes
23-
how_to_write_a_plugin
23+
extending_pytask
2424
the_data_catalog
2525
```
2626

docs/source/reference_guides/configuration.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,21 @@ editor_url_scheme = "no_link"
9797
9898
````
9999

100+
````{confval} hook_module
101+
102+
Register additional modules containing hook implementations.
103+
104+
```toml
105+
hook_modules = ["myproject.hooks", "hooks.py"]
106+
```
107+
108+
You can use module names and paths as values. Relative paths are assumed to be relative
109+
to the configuration file or the current working directory.
110+
111+
{ref}`This how-to guide <hook-module>` has more information.
112+
113+
````
114+
100115
````{confval} ignore
101116
102117
pytask can ignore files and directories and exclude some tasks or reduce the duration of

docs/source/tutorials/plugins.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ You can find plugins in many places.
1919
- Search on [anaconda.org](https://anaconda.org/search?q=pytask) or
2020
[prefix.dev](https://prefix.dev) for related packages.
2121

22-
## How to implement your plugin
22+
## How to extend pytask
2323

24-
Follow the {doc}`guide on writing a plugin <../how_to_guides/how_to_write_a_plugin>` to
25-
write your plugin.
24+
Follow the {doc}`guide on extending pytask <../how_to_guides/extending_pytask>` to add
25+
your own hook implementations or write your plugin.

src/_pytask/click.py

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,36 @@
11
"""Contains code related to click."""
22
from __future__ import annotations
33

4-
import enum
54
import inspect
5+
from enum import Enum
66
from gettext import gettext as _
7+
from gettext import ngettext
78
from typing import Any
89
from typing import ClassVar
10+
from typing import TYPE_CHECKING
911

1012
import click
1113
from _pytask import __version__ as version
1214
from _pytask.console import console
15+
from click import Choice
16+
from click import Command
17+
from click import Context
18+
from click import Parameter
1319
from click.parser import split_opt
1420
from click_default_group import DefaultGroup
1521
from rich.highlighter import RegexHighlighter
1622
from rich.panel import Panel
1723
from rich.table import Table
1824
from rich.text import Text
1925

26+
if TYPE_CHECKING:
27+
from collections.abc import Sequence
28+
2029

2130
__all__ = ["ColoredCommand", "ColoredGroup", "EnumChoice"]
2231

2332

24-
class EnumChoice(click.Choice):
33+
class EnumChoice(Choice):
2534
"""An enum-based choice type.
2635
2736
The implementation is copied from https://github.com/pallets/click/pull/2210 and
@@ -35,17 +44,15 @@ class EnumChoice(click.Choice):
3544
3645
"""
3746

38-
def __init__(self, enum_type: type[enum.Enum], case_sensitive: bool = True) -> None:
47+
def __init__(self, enum_type: type[Enum], case_sensitive: bool = True) -> None:
3948
super().__init__(
4049
choices=[element.value for element in enum_type],
4150
case_sensitive=case_sensitive,
4251
)
4352
self.enum_type = enum_type
4453

45-
def convert(
46-
self, value: Any, param: click.Parameter | None, ctx: click.Context | None
47-
) -> Any:
48-
if isinstance(value, enum.Enum):
54+
def convert(self, value: Any, param: Parameter | None, ctx: Context | None) -> Any:
55+
if isinstance(value, Enum):
4956
value = value.value
5057
value = super().convert(value=value, param=param, ctx=ctx)
5158
if value is None:
@@ -68,7 +75,7 @@ class ColoredGroup(DefaultGroup):
6875

6976
def format_help(
7077
self: DefaultGroup,
71-
ctx: click.Context,
78+
ctx: Context,
7279
formatter: Any, # noqa: ARG002
7380
) -> None:
7481
"""Format the help text."""
@@ -114,12 +121,62 @@ def format_help(
114121
)
115122

116123

117-
class ColoredCommand(click.Command):
124+
def _iter_params_for_processing(
125+
invocation_order: Sequence[Parameter], declaration_order: Sequence[Parameter]
126+
) -> list[Parameter]:
127+
def sort_key(item: Parameter) -> tuple[bool, float]:
128+
# Hardcode the order of the config and paths parameters so that they are always
129+
# processed first even if other eager parameters are chosen. The rest follows
130+
# https://click.palletsprojects.com/en/8.1.x/advanced/#callback-evaluation-order.
131+
if item.name == "paths":
132+
return False, -3
133+
134+
if item.name == "config":
135+
return False, -2
136+
137+
if item.name == "hook_module":
138+
return False, -1
139+
140+
try:
141+
idx: float = invocation_order.index(item)
142+
except ValueError:
143+
idx = float("inf")
144+
145+
return not item.is_eager, idx
146+
147+
return sorted(declaration_order, key=sort_key)
148+
149+
150+
class ColoredCommand(Command):
118151
"""A command with colored help pages."""
119152

153+
def parse_args(self, ctx: Context, args: list[str]) -> list[str]:
154+
if not args and self.no_args_is_help and not ctx.resilient_parsing:
155+
click.echo(ctx.get_help(), color=ctx.color)
156+
ctx.exit()
157+
158+
parser = self.make_parser(ctx)
159+
opts, args, param_order = parser.parse_args(args=args)
160+
161+
for param in _iter_params_for_processing(param_order, self.get_params(ctx)):
162+
value, args = param.handle_parse_result(ctx, opts, args)
163+
164+
if args and not ctx.allow_extra_args and not ctx.resilient_parsing:
165+
ctx.fail(
166+
ngettext(
167+
"Got unexpected extra argument ({args})",
168+
"Got unexpected extra arguments ({args})",
169+
len(args),
170+
).format(args=" ".join(map(str, args)))
171+
)
172+
173+
ctx.args = args
174+
ctx._opt_prefixes.update(parser._opt_prefixes)
175+
return args
176+
120177
def format_help(
121-
self: click.Command,
122-
ctx: click.Context,
178+
self: Command,
179+
ctx: Context,
123180
formatter: Any, # noqa: ARG002
124181
) -> None:
125182
"""Format the help text."""
@@ -142,9 +199,7 @@ def format_help(
142199
)
143200

144201

145-
def _print_options(
146-
group_or_command: click.Command | DefaultGroup, ctx: click.Context
147-
) -> None:
202+
def _print_options(group_or_command: Command | DefaultGroup, ctx: Context) -> None:
148203
"""Print options formatted with a table in a panel."""
149204
highlighter = _OptionHighlighter()
150205

@@ -195,7 +250,7 @@ def _print_options(
195250

196251

197252
def _format_help_text( # noqa: C901, PLR0912, PLR0915
198-
param: click.Parameter, ctx: click.Context
253+
param: Parameter, ctx: Context
199254
) -> Text:
200255
"""Format the help of a click parameter.
201256
@@ -264,7 +319,7 @@ def _format_help_text( # noqa: C901, PLR0912, PLR0915
264319
and not default_value
265320
):
266321
default_string = ""
267-
elif isinstance(default_value, enum.Enum):
322+
elif isinstance(default_value, Enum):
268323
default_string = str(default_value.value)
269324
else:
270325
default_string = str(default_value)

0 commit comments

Comments
 (0)