Coverage for arguably/arg.py: 100%

15 statements  

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

1""" 

2A collection of methods for adding a modifier to a parameter. Should be used in `Annotated[]`. 

3 

4Examples: 

5 ```python 

6 def foo( 

7 *, 

8 verbose: Annotated[int, arguably.arg.count()], 

9 ): 

10 ``` 

11""" 

12 

13import enum 

14from typing import Union, Callable, Any 

15 

16import arguably._modifiers as mods 

17 

18 

19def required() -> mods.RequiredModifier: 

20 """ 

21 Marks a field as required. For `*args` or a `list[]`, requires at least one item. 

22 

23 Returns: 

24 A value for use with `Annotated[]`, stating that this parameter is required. 

25 

26 Examples: 

27 ```python 

28 import arguably 

29 from typing import Annotated 

30 

31 @arguably.command 

32 def email( 

33 from_: str, 

34 *to: Annotated[str, arguably.arg.required()] 

35 ): 

36 print(f"{from_=}", f"{to=}") 

37 

38 if __name__ == "__main__": 

39 arguably.run() 

40 ``` 

41 

42 ```console 

43 user@machine:~$ python3 arg-required.py -h 

44 usage: arg-required.py [-h] from to [to ...] 

45 

46 positional arguments: 

47 from (type: str) 

48 to (type: str) 

49 

50 options: 

51 -h, --help show this help message and exit 

52 ``` 

53 ```console 

54 user@machine:~$ python3 arg-required.py sender@example.com 

55 usage: arg-required.py [-h] from to [to ...] 

56 arg-required.py: error: the following arguments are required: to 

57 ``` 

58 """ 

59 return mods.RequiredModifier() 

60 

61 

62def count() -> mods.CountedModifier: 

63 """ 

64 Counts the number of times a flag is given. For example, `-vvvv` would yield `4`. 

65 

66 Returns: 

67 A value for use with `Annotated[]`, stating that this parameter should be counted. 

68 

69 Examples: 

70 ```python 

71 import arguably 

72 from typing import Annotated 

73 

74 @arguably.command 

75 def process( 

76 *, 

77 verbose: Annotated[int, arguably.arg.count()], 

78 ): 

79 \"\"\" 

80 :param verbose: [-v] verbosity 

81 \"\"\" 

82 print(f"{verbose=}") 

83 

84 if __name__ == "__main__": 

85 arguably.run() 

86 ``` 

87 

88 ```console 

89 user@machine:~$ python3 arg-count.py -vvvv 

90 verbose=4 

91 ``` 

92 """ 

93 return mods.CountedModifier() 

94 

95 

96def choices(*choices: Union[str, enum.Enum]) -> mods.ChoicesModifier: 

97 """ 

98 Specifies a fixed set of values that a parameter is allowed to be. If a parameter is an `enum.Enum` type, this 

99 logic is already used to restrict the inputs to be one of the enum entries. 

100 

101 Args: 

102 *choices: The allowed values. Must all be of the same type, and be compatible with the annotated type for 

103 this parameter. 

104 

105 Returns: 

106 A value for use with `Annotated[]`, stating that this parameter has a fixed set of choices. 

107 

108 Examples: 

109 ```python 

110 import arguably 

111 from typing import Annotated 

112 

113 @arguably.command 

114 def move( 

115 direction: Annotated[str, arguably.arg.choices("left", "right", "up", "down")] 

116 ): 

117 \"\"\"An enum is usually recommended for cases like this\"\"\" 

118 print(f"{direction=}") 

119 

120 if __name__ == "__main__": 

121 arguably.run() 

122 ``` 

123 

124 ```console 

125 user@machine:~$ python3 arg-choices.py north 

126 usage: arg-choices.py [-h] {left,right,up,down} 

127 arg-choices.py: error: argument direction: invalid choice: 'north' (choose from 'left', 'right', 'up', 'down') 

128 ``` 

129 """ 

130 return mods.ChoicesModifier(choices) 

131 

132 

133def missing(omit_value: str) -> mods.MissingArgDefaultModifier: 

134 """ 

135 Allows an option to be specified, but its value be omitted. In the case where the value is given, the value is 

136 used, but if it is omitted, `omit_value` will be used. 

137 

138 Args: 

139 omit_value: The value that will be used if the flag is present, but the value is omitted. 

140 

141 Returns: 

142 A value for use with `Annotated[]`, stating that this parameter has a special value if the flag is present, 

143 but no value is provided. 

144 

145 Examples: 

146 ```python 

147 import arguably 

148 from pathlib import Path 

149 from typing import Annotated 

150 

151 @arguably.command 

152 def do_something( 

153 *, 

154 log: Annotated[Path | None, arguably.arg.missing("~/.log.txt")] = None 

155 ): 

156 print(f"{log=}") 

157 

158 if __name__ == "__main__": 

159 arguably.run() 

160 ``` 

161 

162 ```console 

163 user@machine:~$ python3 arg-missing.py 

164 log=None 

165 user@machine:~$ python3 arg-missing.py --log 

166 log=PosixPath('~/.log.txt') 

167 user@machine:~$ python3 arg-missing.py --log foo.log 

168 log=PosixPath('foo.log') 

169 ``` 

170 """ 

171 return mods.MissingArgDefaultModifier(omit_value) 

172 

173 

174def handler(func: Callable[[str], Any]) -> mods.HandlerModifier: 

175 """ 

176 Causes a user-provided handler to be used to process the input string, instead of trying to process it using 

177 the types from type annotations. 

178 

179 Args: 

180 func: The function to call to process the input string. 

181 

182 Returns: 

183 A value for use with `Annotated[]`, stating that this parameter has a specific handler to call. 

184 

185 Examples: 

186 ```python 

187 import arguably 

188 from typing import Annotated 

189 

190 @arguably.command 

191 def handle_it( 

192 version: Annotated[int, arguably.arg.handler(lambda s: int(s.split("-")[-1]))] = None 

193 ): 

194 print(f"{version=}") 

195 

196 if __name__ == "__main__": 

197 arguably.run() 

198 ``` 

199 

200 ```console 

201 user@machine:~$ python3 arg-handler.py Python-3 

202 version=3 

203 ``` 

204 """ 

205 return mods.HandlerModifier(func) 

206 

207 

208def builder() -> mods.BuilderModifier: 

209 """ 

210 Causes the arguably builder logic to be used instead of trying to instantiate the type from the input string. 

211 

212 Returns: 

213 A value for use with `Annotated[]`, stating that this parameter should use the builder logic. 

214 

215 Examples: 

216 ```python 

217 import arguably 

218 from dataclasses import dataclass 

219 from typing import Annotated 

220 

221 class Nic: ... 

222 

223 @arguably.subtype(alias="tap") 

224 @dataclass 

225 class TapNic(Nic): 

226 model: str 

227 

228 @arguably.subtype(alias="user") 

229 @dataclass 

230 class UserNic(Nic): 

231 hostfwd: str 

232 

233 @arguably.command 

234 def qemu_style(*, nic: Annotated[list[Nic], arguably.arg.builder()]): 

235 print(f"{nic=}") 

236 

237 if __name__ == "__main__": 

238 arguably.run() 

239 ``` 

240 

241 ```console 

242 user@machine:~$ ./build.py --nic tap,model=e1000 --nic user,hostfwd=tcp::10022-:22 

243 nic=[TapNic(model='e1000'), UserNic(hostfwd='tcp::10022-:22')] 

244 ``` 

245 """ 

246 return mods.BuilderModifier()