Coverage for src/duty/decorator.py: 95.92%
37 statements
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-17 17:18 +0200
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-17 17:18 +0200
1"""Module containing the decorator provided to users."""
3from __future__ import annotations
5import inspect
6from functools import wraps
7from typing import TYPE_CHECKING, Any, Callable, overload
9from duty.collection import Duty, DutyListType
11if TYPE_CHECKING:
12 from collections.abc import Iterable
14 from duty.context import Context
17def _skip(func: Callable, reason: str) -> Callable:
18 @wraps(func)
19 def wrapper(ctx: Context, *args, **kwargs) -> None: # noqa: ARG001,ANN002,ANN003
20 ctx.run(lambda: True, title=reason)
22 return wrapper
25def create_duty(
26 func: Callable,
27 *,
28 name: str | None = None,
29 aliases: Iterable[str] | None = None,
30 pre: DutyListType | None = None,
31 post: DutyListType | None = None,
32 skip_if: bool = False,
33 skip_reason: str | None = None,
34 **opts: Any,
35) -> Duty:
36 """Register a duty in the collection.
38 Parameters:
39 func: The callable to register as a duty.
40 name: The duty name.
41 aliases: A set of aliases for this duty.
42 pre: Pre-duties.
43 post: Post-duties.
44 skip_if: Skip running the duty if the given condition is met.
45 skip_reason: Custom message when skipping.
46 opts: Options passed to the context.
48 Returns:
49 The registered duty.
50 """
51 aliases = set(aliases) if aliases else set()
52 name = name or func.__name__
53 dash_name = name.replace("_", "-")
54 if name != dash_name:
55 aliases.add(name)
56 name = dash_name
57 description = inspect.getdoc(func) or ""
58 if skip_if:
59 func = _skip(func, skip_reason or f"{dash_name}: skipped")
60 duty = Duty(name, description, func, aliases=aliases, pre=pre, post=post, opts=opts)
61 duty.__name__ = name # type: ignore[attr-defined]
62 duty.__doc__ = description
63 duty.__wrapped__ = func # type: ignore[attr-defined]
64 return duty
67@overload
68def duty(**kwargs: Any) -> Callable[[Callable], Duty]: ... 68 ↛ exitline 68 didn't return from function 'duty' because
71@overload
72def duty(func: Callable) -> Duty: ... 72 ↛ exitline 72 didn't return from function 'duty' because
75def duty(*args: Any, **kwargs: Any) -> Callable | Duty:
76 """Decorate a callable to transform it and register it as a duty.
78 Parameters:
79 args: One callable.
80 kwargs: Context options.
82 Raises:
83 ValueError: When the decorator is misused.
85 Examples:
86 Decorate a function:
88 ```python
89 @duty
90 def clean(ctx):
91 ctx.run("rm -rf build", silent=True)
92 ```
94 Pass options to the context:
96 ```python
97 @duty(silent=True)
98 def clean(ctx):
99 ctx.run("rm -rf build") # silent=True is implied
100 ```
102 Returns:
103 A duty when used without parentheses, a decorator otherwise.
104 """
105 if args:
106 if len(args) > 1:
107 raise ValueError("The duty decorator accepts only one positional argument")
108 return create_duty(args[0], **kwargs)
110 def decorator(func: Callable) -> Duty:
111 return create_duty(func, **kwargs)
113 return decorator