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

1"""Module containing the decorator provided to users.""" 

2 

3from __future__ import annotations 

4 

5import inspect 

6from functools import wraps 

7from typing import TYPE_CHECKING, Any, Callable, overload 

8 

9from duty.collection import Duty, DutyListType 

10 

11if TYPE_CHECKING: 

12 from collections.abc import Iterable 

13 

14 from duty.context import Context 

15 

16 

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) 

21 

22 return wrapper 

23 

24 

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. 

37 

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. 

47 

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 

65 

66 

67@overload 

68def duty(**kwargs: Any) -> Callable[[Callable], Duty]: ... 68 ↛ exitline 68 didn't return from function 'duty' because

69 

70 

71@overload 

72def duty(func: Callable) -> Duty: ... 72 ↛ exitline 72 didn't return from function 'duty' because

73 

74 

75def duty(*args: Any, **kwargs: Any) -> Callable | Duty: 

76 """Decorate a callable to transform it and register it as a duty. 

77 

78 Parameters: 

79 args: One callable. 

80 kwargs: Context options. 

81 

82 Raises: 

83 ValueError: When the decorator is misused. 

84 

85 Examples: 

86 Decorate a function: 

87 

88 ```python 

89 @duty 

90 def clean(ctx): 

91 ctx.run("rm -rf build", silent=True) 

92 ``` 

93 

94 Pass options to the context: 

95 

96 ```python 

97 @duty(silent=True) 

98 def clean(ctx): 

99 ctx.run("rm -rf build") # silent=True is implied 

100 ``` 

101 

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) 

109 

110 def decorator(func: Callable) -> Duty: 

111 return create_duty(func, **kwargs) 

112 

113 return decorator