Coverage for arguably/_modifiers.py: 90%

80 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-10 01:01 +0000

1from __future__ import annotations 

2 

3import abc 

4import enum 

5import inspect 

6from dataclasses import dataclass 

7from typing import Callable, Any, Union, List, Dict, Tuple 

8 

9import arguably._argparse_extensions as ap_ext 

10import arguably._commands as cmds 

11import arguably._util as util 

12 

13 

14@dataclass(frozen=True) 

15class CommandArgModifier(abc.ABC): 

16 """A class that encapsulates a change to the kwargs dict to be passed to parser.add_argument()""" 

17 

18 def check_valid(self, value_type: type, param: inspect.Parameter, function_name: str) -> None: 

19 """Checks whether this modifier is valid for the parameter""" 

20 

21 @abc.abstractmethod 

22 def modify_arg_dict(self, command: cmds.Command, arg_: cmds.CommandArg, kwargs_dict: Dict[str, Any]) -> None: 

23 """Modifies the kwargs passed to parser.add_argument()""" 

24 

25 

26@dataclass(frozen=True) 

27class MissingArgDefaultModifier(CommandArgModifier): 

28 """Allows an option to be a flag, passing a default value instead of a value provided via the command line""" 

29 

30 missing_value: Any 

31 

32 def modify_arg_dict(self, command: cmds.Command, arg_: cmds.CommandArg, kwargs_dict: Dict[str, Any]) -> None: 

33 kwargs_dict.update(nargs="?", const=self.missing_value) 

34 

35 

36@dataclass(frozen=True) 

37class CountedModifier(CommandArgModifier): 

38 """Counts the number of times a flag is provided""" 

39 

40 def modify_arg_dict(self, command: cmds.Command, arg_: cmds.CommandArg, kwargs_dict: Dict[str, Any]) -> None: 

41 if arg_.input_method != cmds.InputMethod.OPTION: 

42 raise util.ArguablyException( 

43 f"`arguably.Counted` should only be used on {cmds.InputMethod.OPTION.name}, but was used on " 

44 f"{arg_.func_arg_name}, which is {arg_.input_method.name}." 

45 ) 

46 kwargs_dict.update(action="count") 

47 if "type" in kwargs_dict: 

48 del kwargs_dict["type"] 

49 if "nargs" in kwargs_dict: 

50 del kwargs_dict["nargs"] 

51 

52 

53@dataclass(frozen=True) 

54class RequiredModifier(CommandArgModifier): 

55 """Marks an input as required. In the case of a variadic positional arg, uses the '+' symbol to represent this.""" 

56 

57 def check_valid(self, value_type: type, param: inspect.Parameter, function_name: str) -> None: 

58 if issubclass(value_type, bool): 

59 raise util.ArguablyException("Cannot mark a bool as required.") 

60 

61 def modify_arg_dict(self, command: cmds.Command, arg_: cmds.CommandArg, kwargs_dict: Dict[str, Any]) -> None: 

62 if arg_.is_variadic: 

63 kwargs_dict.update(nargs="+") 

64 if "default" in kwargs_dict: 

65 del kwargs_dict["default"] 

66 else: 

67 kwargs_dict.update(required=True) 

68 

69 

70@dataclass(frozen=True) 

71class ListModifier(CommandArgModifier): 

72 """Sets up arguably list handling. Sensitive to the `_RequiredModifier`.""" 

73 

74 def modify_arg_dict(self, command: cmds.Command, arg_: cmds.CommandArg, kwargs_dict: Dict[str, Any]) -> None: 

75 if arg_.input_method is cmds.InputMethod.OPTIONAL_POSITIONAL: 

76 kwargs_dict.update(nargs="?") 

77 if arg_.input_method is not cmds.InputMethod.REQUIRED_POSITIONAL: 

78 kwargs_dict.update(default=list()) 

79 if (arg_.default is util.NoDefault and arg_.input_method is cmds.InputMethod.OPTION) or RequiredModifier in [ 

80 type(mod) for mod in arg_.modifiers 

81 ]: 

82 kwargs_dict.update(required=True) 

83 kwargs_dict.update(action=ap_ext.ListTupleBuilderAction, command_arg=arg_) 

84 

85 

86@dataclass(frozen=True) 

87class TupleModifier(CommandArgModifier): 

88 """Sets up arguably tuple handling""" 

89 

90 tuple_arg: List[type] 

91 

92 def modify_arg_dict(self, command: cmds.Command, arg_: cmds.CommandArg, kwargs_dict: Dict[str, Any]) -> None: 

93 if arg_.metavars is None: 

94 kwargs_dict.update(metavar=",".join([arg_.cli_arg_name] * len(self.tuple_arg))) 

95 kwargs_dict.update(action=ap_ext.ListTupleBuilderAction, command_arg=arg_, type=self.tuple_arg) 

96 

97 

98@dataclass(frozen=True) 

99class BuilderModifier(CommandArgModifier): 

100 """Sets up arguably builder""" 

101 

102 def modify_arg_dict(self, command: cmds.Command, arg_: cmds.CommandArg, kwargs_dict: Dict[str, Any]) -> None: 

103 kwargs_dict.update(action=ap_ext.ListTupleBuilderAction, command_arg=arg_) 

104 

105 

106@dataclass(frozen=True) 

107class HandlerModifier(CommandArgModifier): 

108 """ 

109 Allows full user control over how an input is handled, a function should be passed in to parse the string from the 

110 command line 

111 """ 

112 

113 handler: Callable[[str], Any] 

114 

115 def modify_arg_dict(self, command: cmds.Command, arg_: cmds.CommandArg, kwargs_dict: Dict[str, Any]) -> None: 

116 kwargs_dict.update(type=self.handler) 

117 

118 

119@dataclass(frozen=True) 

120class ChoicesModifier(CommandArgModifier): 

121 """Restricts inputs to one of a given set of choices""" 

122 

123 choices: Tuple[Union[str, enum.Enum], ...] 

124 

125 def check_valid(self, value_type: type, param: inspect.Parameter, function_name: str) -> None: 

126 if len(self.choices) == 0: 

127 raise util.ArguablyException("At least one choice is required for `arguably.arg.choices()`") 

128 

129 first_type = type(self.choices[0]) 

130 if not all(issubclass(type(c), first_type) or issubclass(first_type, type(c)) for c in self.choices): 

131 raise util.ArguablyException("Choices must all be of the same type") 

132 

133 for choice in self.choices: 

134 if not isinstance(choice, value_type): 

135 raise util.ArguablyException( 

136 f"Function argument `{param.name}` in `{function_name}` specifies choices, but choice {choice} is " 

137 f"not a subtype of {value_type}." 

138 ) 

139 

140 def modify_arg_dict(self, command: cmds.Command, arg_: cmds.CommandArg, kwargs_dict: Dict[str, Any]) -> None: 

141 kwargs_dict.update(choices=self.choices)