Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/invoke/parser/context.py: 20%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

107 statements  

1import itertools 

2from typing import Any, Dict, List, Iterable, Optional, Tuple, Union 

3 

4try: 

5 from ..vendor.lexicon import Lexicon 

6except ImportError: 

7 from lexicon import Lexicon # type: ignore[no-redef] 

8 

9from .argument import Argument 

10 

11 

12def translate_underscores(name: str) -> str: 

13 return name.lstrip("_").rstrip("_").replace("_", "-") 

14 

15 

16def to_flag(name: str) -> str: 

17 name = translate_underscores(name) 

18 if len(name) == 1: 

19 return "-" + name 

20 return "--" + name 

21 

22 

23def sort_candidate(arg: Argument) -> str: 

24 names = arg.names 

25 # TODO: is there no "split into two buckets on predicate" builtin? 

26 shorts = {x for x in names if len(x.strip("-")) == 1} 

27 longs = {x for x in names if x not in shorts} 

28 return str(sorted(shorts if shorts else longs)[0]) 

29 

30 

31def flag_key(arg: Argument) -> List[Union[int, str]]: 

32 """ 

33 Obtain useful key list-of-ints for sorting CLI flags. 

34 

35 .. versionadded:: 1.0 

36 """ 

37 # Setup 

38 ret: List[Union[int, str]] = [] 

39 x = sort_candidate(arg) 

40 # Long-style flags win over short-style ones, so the first item of 

41 # comparison is simply whether the flag is a single character long (with 

42 # non-length-1 flags coming "first" [lower number]) 

43 ret.append(1 if len(x) == 1 else 0) 

44 # Next item of comparison is simply the strings themselves, 

45 # case-insensitive. They will compare alphabetically if compared at this 

46 # stage. 

47 ret.append(x.lower()) 

48 # Finally, if the case-insensitive test also matched, compare 

49 # case-sensitive, but inverse (with lowercase letters coming first) 

50 inversed = "" 

51 for char in x: 

52 inversed += char.lower() if char.isupper() else char.upper() 

53 ret.append(inversed) 

54 return ret 

55 

56 

57# Named slightly more verbose so Sphinx references can be unambiguous. 

58# Got real sick of fully qualified paths. 

59class ParserContext: 

60 """ 

61 Parsing context with knowledge of flags & their format. 

62 

63 Generally associated with the core program or a task. 

64 

65 When run through a parser, will also hold runtime values filled in by the 

66 parser. 

67 

68 .. versionadded:: 1.0 

69 """ 

70 

71 def __init__( 

72 self, 

73 name: Optional[str] = None, 

74 aliases: Iterable[str] = (), 

75 args: Iterable[Argument] = (), 

76 ) -> None: 

77 """ 

78 Create a new ``ParserContext`` named ``name``, with ``aliases``. 

79 

80 ``name`` is optional, and should be a string if given. It's used to 

81 tell ParserContext objects apart, and for use in a Parser when 

82 determining what chunk of input might belong to a given ParserContext. 

83 

84 ``aliases`` is also optional and should be an iterable containing 

85 strings. Parsing will honor any aliases when trying to "find" a given 

86 context in its input. 

87 

88 May give one or more ``args``, which is a quick alternative to calling 

89 ``for arg in args: self.add_arg(arg)`` after initialization. 

90 """ 

91 self.args = Lexicon() 

92 self.positional_args: List[Argument] = [] 

93 self.flags = Lexicon() 

94 self.inverse_flags: Dict[str, str] = {} # No need for Lexicon here 

95 self.name = name 

96 self.aliases = aliases 

97 for arg in args: 

98 self.add_arg(arg) 

99 

100 def __repr__(self) -> str: 

101 aliases = "" 

102 if self.aliases: 

103 aliases = " ({})".format(", ".join(self.aliases)) 

104 name = (" {!r}{}".format(self.name, aliases)) if self.name else "" 

105 args = (": {!r}".format(self.args)) if self.args else "" 

106 return "<parser/Context{}{}>".format(name, args) 

107 

108 def add_arg(self, *args: Any, **kwargs: Any) -> None: 

109 """ 

110 Adds given ``Argument`` (or constructor args for one) to this context. 

111 

112 The Argument in question is added to the following dict attributes: 

113 

114 * ``args``: "normal" access, i.e. the given names are directly exposed 

115 as keys. 

116 * ``flags``: "flaglike" access, i.e. the given names are translated 

117 into CLI flags, e.g. ``"foo"`` is accessible via ``flags['--foo']``. 

118 * ``inverse_flags``: similar to ``flags`` but containing only the 

119 "inverse" versions of boolean flags which default to True. This 

120 allows the parser to track e.g. ``--no-myflag`` and turn it into a 

121 False value for the ``myflag`` Argument. 

122 

123 .. versionadded:: 1.0 

124 """ 

125 # Normalize 

126 if len(args) == 1 and isinstance(args[0], Argument): 

127 arg = args[0] 

128 else: 

129 arg = Argument(*args, **kwargs) 

130 # Uniqueness constraint: no name collisions 

131 for name in arg.names: 

132 if name in self.args: 

133 msg = "Tried to add an argument named {!r} but one already exists!" # noqa 

134 raise ValueError(msg.format(name)) 

135 # First name used as "main" name for purposes of aliasing 

136 main = arg.names[0] # NOT arg.name 

137 self.args[main] = arg 

138 # Note positionals in distinct, ordered list attribute 

139 if arg.positional: 

140 self.positional_args.append(arg) 

141 # Add names & nicknames to flags, args 

142 self.flags[to_flag(main)] = arg 

143 for name in arg.nicknames: 

144 self.args.alias(name, to=main) 

145 self.flags.alias(to_flag(name), to=to_flag(main)) 

146 # Add attr_name to args, but not flags 

147 if arg.attr_name: 

148 self.args.alias(arg.attr_name, to=main) 

149 # Add to inverse_flags if required 

150 if arg.kind == bool and arg.default is True: 

151 # Invert the 'main' flag name here, which will be a dashed version 

152 # of the primary argument name if underscore-to-dash transformation 

153 # occurred. 

154 inverse_name = to_flag("no-{}".format(main)) 

155 self.inverse_flags[inverse_name] = to_flag(main) 

156 

157 @property 

158 def missing_positional_args(self) -> List[Argument]: 

159 return [x for x in self.positional_args if x.value is None] 

160 

161 @property 

162 def as_kwargs(self) -> Dict[str, Any]: 

163 """ 

164 This context's arguments' values keyed by their ``.name`` attribute. 

165 

166 Results in a dict suitable for use in Python contexts, where e.g. an 

167 arg named ``foo-bar`` becomes accessible as ``foo_bar``. 

168 

169 .. versionadded:: 1.0 

170 """ 

171 ret = {} 

172 for arg in self.args.values(): 

173 ret[arg.name] = arg.value 

174 return ret 

175 

176 def names_for(self, flag: str) -> List[str]: 

177 # TODO: should probably be a method on Lexicon/AliasDict 

178 return list(set([flag] + self.flags.aliases_of(flag))) 

179 

180 def help_for(self, flag: str) -> Tuple[str, str]: 

181 """ 

182 Return 2-tuple of ``(flag-spec, help-string)`` for given ``flag``. 

183 

184 .. versionadded:: 1.0 

185 """ 

186 # Obtain arg obj 

187 if flag not in self.flags: 

188 err = "{!r} is not a valid flag for this context! Valid flags are: {!r}" # noqa 

189 raise ValueError(err.format(flag, self.flags.keys())) 

190 arg = self.flags[flag] 

191 # Determine expected value type, if any 

192 value = {str: "STRING", int: "INT"}.get(arg.kind) 

193 # Format & go 

194 full_names = [] 

195 for name in self.names_for(flag): 

196 if value: 

197 # Short flags are -f VAL, long are --foo=VAL 

198 # When optional, also, -f [VAL] and --foo[=VAL] 

199 if len(name.strip("-")) == 1: 

200 value_ = ("[{}]".format(value)) if arg.optional else value 

201 valuestr = " {}".format(value_) 

202 else: 

203 valuestr = "={}".format(value) 

204 if arg.optional: 

205 valuestr = "[{}]".format(valuestr) 

206 else: 

207 # no value => boolean 

208 # check for inverse 

209 if name in self.inverse_flags.values(): 

210 name = "--[no-]{}".format(name[2:]) 

211 

212 valuestr = "" 

213 # Tack together 

214 full_names.append(name + valuestr) 

215 namestr = ", ".join(sorted(full_names, key=len)) 

216 helpstr = arg.help or "" 

217 return namestr, helpstr 

218 

219 def help_tuples(self) -> List[Tuple[str, Optional[str]]]: 

220 """ 

221 Return sorted iterable of help tuples for all member Arguments. 

222 

223 Sorts like so: 

224 

225 * General sort is alphanumerically 

226 * Short flags win over long flags 

227 * Arguments with *only* long flags and *no* short flags will come 

228 first. 

229 * When an Argument has multiple long or short flags, it will sort using 

230 the most favorable (lowest alphabetically) candidate. 

231 

232 This will result in a help list like so:: 

233 

234 --alpha, --zeta # 'alpha' wins 

235 --beta 

236 -a, --query # short flag wins 

237 -b, --argh 

238 -c 

239 

240 .. versionadded:: 1.0 

241 """ 

242 # TODO: argument/flag API must change :( 

243 # having to call to_flag on 1st name of an Argument is just dumb. 

244 # To pass in an Argument object to help_for may require moderate 

245 # changes? 

246 return list( 

247 map( 

248 lambda x: self.help_for(to_flag(x.name)), 

249 sorted(self.flags.values(), key=flag_key), 

250 ) 

251 ) 

252 

253 def flag_names(self) -> Tuple[str, ...]: 

254 """ 

255 Similar to `help_tuples` but returns flag names only, no helpstrs. 

256 

257 Specifically, all flag names, flattened, in rough order. 

258 

259 .. versionadded:: 1.0 

260 """ 

261 # Regular flag names 

262 flags = sorted(self.flags.values(), key=flag_key) 

263 names = [self.names_for(to_flag(x.name)) for x in flags] 

264 # Inverse flag names sold separately 

265 names.append(list(self.inverse_flags.keys())) 

266 return tuple(itertools.chain.from_iterable(names))