1import re 2 3from typing import Any 4from typing import Optional 5 6from clikit.utils._compat import basestring 7from clikit.utils.string import parse_boolean 8from clikit.utils.string import parse_float 9from clikit.utils.string import parse_int 10from clikit.utils.string import parse_string 11 12 13class Argument(object): 14 """ 15 An input argument. 16 """ 17 18 REQUIRED = 1 19 OPTIONAL = 2 20 MULTI_VALUED = 4 21 STRING = 16 22 BOOLEAN = 32 23 INTEGER = 64 24 FLOAT = 128 25 NULLABLE = 256 26 27 def __init__( 28 self, name, flags=0, description=None, default=None 29 ): # type: (str, int, Optional[str], Any) -> None 30 if not isinstance(name, basestring): 31 raise ValueError( 32 "The argument name must be a string. Got: {}".format(type(name)) 33 ) 34 35 if not name: 36 raise ValueError("The argument name must not be empty.") 37 38 if not name[:1].isalpha(): 39 raise ValueError("The argument name must start with a letter") 40 41 if not re.match(r"^[a-zA-Z0-9\-]+$", name): 42 raise ValueError( 43 "The argument name must contain letters, digits and hyphens only." 44 ) 45 46 if description is not None: 47 if not isinstance(description, basestring): 48 raise ValueError( 49 "The argument description must be a string. Got: {}".format( 50 type(description) 51 ) 52 ) 53 54 if not description: 55 raise ValueError("The argument description must not be empty.") 56 57 self._validate_flags(flags) 58 59 flags = self._add_default_flags(flags) 60 61 self._name = name 62 self._flags = flags 63 self._description = description 64 65 if self.is_multi_valued(): 66 self._default = [] 67 else: 68 self._default = None 69 70 if self.is_optional() or default is not None: 71 self.set_default(default) 72 73 @property 74 def name(self): # type: () -> str 75 return self._name 76 77 @property 78 def flags(self): # type: () -> int 79 return self._flags 80 81 @property 82 def description(self): # type: () -> Optional[str] 83 return self._description 84 85 @property 86 def default(self): # type: () -> Any 87 return self._default 88 89 def is_required(self): # type: () -> bool 90 return bool(self.REQUIRED & self._flags) 91 92 def is_optional(self): # type: () -> bool 93 return bool(self.OPTIONAL & self._flags) 94 95 def is_multi_valued(self): # type: () -> bool 96 return bool(self.MULTI_VALUED & self._flags) 97 98 def set_default(self, default=None): # type: (Any) -> None 99 if self.is_required(): 100 raise ValueError("Required arguments do not accept default values.") 101 102 if self.is_multi_valued(): 103 if default is None: 104 default = [] 105 elif not isinstance(default, list): 106 raise ValueError( 107 "The default value of a multi-valued argument must be a list. " 108 "Got: {}".format(type(default)) 109 ) 110 111 self._default = default 112 113 def parse(self, value): # type: (Any) -> Any 114 nullable = bool(self._flags & self.NULLABLE) 115 116 if self._flags & self.BOOLEAN: 117 return parse_boolean(value, nullable) 118 elif self._flags & self.INTEGER: 119 return parse_int(value, nullable) 120 elif self._flags & self.FLOAT: 121 return parse_float(value, nullable) 122 123 return parse_string(value, nullable) 124 125 def _validate_flags(self, flags): # type: (int) -> None 126 if flags & self.REQUIRED and flags & self.OPTIONAL: 127 raise ValueError( 128 "The argument flags REQUIRED and OPTIONAL cannot be combined." 129 ) 130 131 if flags & self.STRING: 132 if flags & self.BOOLEAN: 133 raise ValueError( 134 "The argument flags STRING and BOOLEAN cannot be combined." 135 ) 136 137 if flags & self.INTEGER: 138 raise ValueError( 139 "The argument flags STRING and INTEGER cannot be combined." 140 ) 141 142 if flags & self.FLOAT: 143 raise ValueError( 144 "The argument flags STRING and FLOAT cannot be combined." 145 ) 146 elif flags & self.BOOLEAN: 147 if flags & self.INTEGER: 148 raise ValueError( 149 "The argument flags BOOLEAN and INTEGER cannot be combined." 150 ) 151 152 if flags & self.FLOAT: 153 raise ValueError( 154 "The argument flags BOOLEAN and FLOAT cannot be combined." 155 ) 156 elif flags & self.INTEGER: 157 if flags & self.FLOAT: 158 raise ValueError( 159 "The argument flags INTEGER and FLOAT cannot be combined." 160 ) 161 162 def _add_default_flags(self, flags): # type: (int) -> int 163 if not flags & (self.REQUIRED | self.OPTIONAL): 164 flags |= self.OPTIONAL 165 166 if not flags & (self.STRING | self.BOOLEAN | self.INTEGER | self.FLOAT): 167 flags |= self.STRING 168 169 return flags 170