Coverage for arguably/_modifiers.py: 90%
80 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-10 01:01 +0000
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-10 01:01 +0000
1from __future__ import annotations
3import abc
4import enum
5import inspect
6from dataclasses import dataclass
7from typing import Callable, Any, Union, List, Dict, Tuple
9import arguably._argparse_extensions as ap_ext
10import arguably._commands as cmds
11import arguably._util as util
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()"""
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"""
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()"""
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"""
30 missing_value: Any
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)
36@dataclass(frozen=True)
37class CountedModifier(CommandArgModifier):
38 """Counts the number of times a flag is provided"""
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"]
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."""
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.")
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)
70@dataclass(frozen=True)
71class ListModifier(CommandArgModifier):
72 """Sets up arguably list handling. Sensitive to the `_RequiredModifier`."""
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_)
86@dataclass(frozen=True)
87class TupleModifier(CommandArgModifier):
88 """Sets up arguably tuple handling"""
90 tuple_arg: List[type]
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)
98@dataclass(frozen=True)
99class BuilderModifier(CommandArgModifier):
100 """Sets up arguably builder"""
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_)
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 """
113 handler: Callable[[str], Any]
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)
119@dataclass(frozen=True)
120class ChoicesModifier(CommandArgModifier):
121 """Restricts inputs to one of a given set of choices"""
123 choices: Tuple[Union[str, enum.Enum], ...]
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()`")
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")
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 )
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)