Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/face/utils.py: 26%
201 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:23 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:23 +0000
2import os
3import re
4import sys
5import getpass
6import keyword
8from boltons.strutils import pluralize, strip_ansi
9from boltons.iterutils import split, unique
10from boltons.typeutils import make_sentinel
12import face
14try:
15 unicode
16except NameError:
17 unicode = str
18 raw_input = input
20ERROR = make_sentinel('ERROR') # used for parse_as=ERROR
22# keep it just to subset of valid ASCII python identifiers for now
23VALID_FLAG_RE = re.compile(r"^[A-z][-_A-z0-9]*\Z")
25FRIENDLY_TYPE_NAMES = {int: 'integer',
26 float: 'decimal'}
29def process_command_name(name):
30 """Validate and canonicalize a Command's name, generally on
31 construction or at subcommand addition. Like
32 ``flag_to_identifier()``, only letters, numbers, '-', and/or
33 '_'. Must begin with a letter, and no trailing underscores or
34 dashes.
36 Python keywords are allowed, as subcommands are never used as
37 attributes or variables in injection.
39 """
41 if not name or not isinstance(name, (str, unicode)):
42 raise ValueError('expected non-zero length string for subcommand name, not: %r' % name)
44 if name.endswith('-') or name.endswith('_'):
45 raise ValueError('expected subcommand name without trailing dashes'
46 ' or underscores, not: %r' % name)
48 name_match = VALID_FLAG_RE.match(name)
49 if not name_match:
50 raise ValueError('valid subcommand name must begin with a letter, and'
51 ' consist only of letters, digits, underscores, and'
52 ' dashes, not: %r' % name)
54 subcmd_name = normalize_flag_name(name)
56 return subcmd_name
59def normalize_flag_name(flag):
60 ret = flag.lstrip('-')
61 if (len(flag) - len(ret)) > 1:
62 # only single-character flags are considered case-sensitive (like an initial)
63 ret = ret.lower()
64 ret = ret.replace('-', '_')
65 return ret
68def flag_to_identifier(flag):
69 """Validate and canonicalize a flag name to a valid Python identifier
70 (variable name).
72 Valid input strings include only letters, numbers, '-', and/or
73 '_'. Only single/double leading dash allowed (-/--). No trailing
74 dashes or underscores. Must not be a Python keyword.
76 Input case doesn't matter, output case will always be lower.
77 """
78 orig_flag = flag
79 if not flag or not isinstance(flag, (str, unicode)):
80 raise ValueError('expected non-zero length string for flag, not: %r' % flag)
82 if flag.endswith('-') or flag.endswith('_'):
83 raise ValueError('expected flag without trailing dashes'
84 ' or underscores, not: %r' % orig_flag)
86 if flag[:2] == '--':
87 flag = flag[2:]
89 flag_match = VALID_FLAG_RE.match(flag)
90 if not flag_match:
91 raise ValueError('valid flag names must begin with a letter, optionally'
92 ' prefixed by two dashes, and consist only of letters,'
93 ' digits, underscores, and dashes, not: %r' % orig_flag)
95 flag_name = normalize_flag_name(flag)
97 if keyword.iskeyword(flag_name):
98 raise ValueError('valid flag names must not be Python keywords: %r'
99 % orig_flag)
101 return flag_name
104def identifier_to_flag(identifier):
105 """
106 Turn an identifier back into its flag format (e.g., "Flag" -> --flag).
107 """
108 if identifier.startswith('-'):
109 raise ValueError('expected identifier, not flag name: %r' % identifier)
110 ret = identifier.lower().replace('_', '-')
111 return '--' + ret
114def format_flag_label(flag):
115 "The default flag label formatter, used in help and error formatting"
116 if flag.display.label is not None:
117 return flag.display.label
118 parts = [identifier_to_flag(flag.name)]
119 if flag.char:
120 parts.append('-' + flag.char)
121 ret = ' / '.join(parts)
122 if flag.display.value_name:
123 ret += ' ' + flag.display.value_name
124 return ret
127def format_posargs_label(posargspec):
128 "The default positional argument label formatter, used in help formatting"
129 if posargspec.display.label:
130 return posargspec.display.label
131 if not posargspec.accepts_args:
132 return ''
133 return get_cardinalized_args_label(posargspec.display.name, posargspec.min_count, posargspec.max_count)
136def get_cardinalized_args_label(name, min_count, max_count):
137 '''
138 Examples for parameter values: (min_count, max_count): output for name=arg:
140 1, 1: arg
141 0, 1: [arg]
142 0, None: [args ...]
143 1, 3: args ...
144 '''
145 if min_count == max_count:
146 return ' '.join([name] * min_count)
147 if min_count == 1:
148 return name + ' ' + get_cardinalized_args_label(name,
149 min_count=0,
150 max_count=max_count - 1 if max_count is not None else None)
152 tmpl = '[%s]' if min_count == 0 else '%s'
153 if max_count == 1:
154 return tmpl % name
155 return tmpl % (pluralize(name) + ' ...')
158def format_flag_post_doc(flag):
159 "The default positional argument label formatter, used in help formatting"
160 if flag.display.post_doc is not None:
161 return flag.display.post_doc
162 if flag.missing is face.ERROR:
163 return '(required)'
164 if flag.missing is None or repr(flag.missing) == object.__repr__(flag.missing):
165 # avoid displaying unhelpful defaults
166 return ''
167 return '(defaults to %r)' % (flag.missing,)
170def get_type_desc(parse_as):
171 "Kind of a hacky way to improve message readability around argument types"
172 if not callable(parse_as):
173 raise TypeError('expected parse_as to be callable, not %r' % parse_as)
174 try:
175 return 'as', FRIENDLY_TYPE_NAMES[parse_as]
176 except KeyError:
177 pass
178 try:
179 # return the type name if it looks like a type
180 return 'as', parse_as.__name__
181 except AttributeError:
182 pass
183 try:
184 # return the func name if it looks like a function
185 return 'with', parse_as.func_name
186 except AttributeError:
187 pass
188 # if all else fails
189 return 'with', repr(parse_as)
192def unwrap_text(text):
193 all_grafs = []
194 cur_graf = []
195 for line in text.splitlines():
196 line = line.strip()
197 if line:
198 cur_graf.append(line)
199 else:
200 all_grafs.append(' '.join(cur_graf))
201 cur_graf = []
202 if cur_graf:
203 all_grafs.append(' '.join(cur_graf))
204 return '\n\n'.join(all_grafs)
207def get_rdep_map(dep_map):
208 """
209 expects and returns a dict of {item: set([deps])}
211 item can be a string or any other hashable object.
212 """
213 # TODO: the way this is used, this function doesn't receive
214 # information about what functions take what args. this ends up
215 # just being args depending on args, with no mediating middleware
216 # names. this can make circular dependencies harder to debug.
217 ret = {}
218 for key in dep_map:
219 to_proc, rdeps, cur_chain = [key], set(), []
220 while to_proc:
221 cur = to_proc.pop()
222 cur_chain.append(cur)
224 cur_rdeps = dep_map.get(cur, [])
226 if key in cur_rdeps:
227 raise ValueError('dependency cycle: %r recursively depends'
228 ' on itself. full dep chain: %r' % (cur, cur_chain))
230 to_proc.extend([c for c in cur_rdeps if c not in to_proc])
231 rdeps.update(cur_rdeps)
233 ret[key] = rdeps
234 return ret
237def get_minimal_executable(executable=None, path=None, environ=os.environ):
238 executable = sys.executable if executable is None else executable
239 path = environ.get('PATH', '') if path is None else path
240 if isinstance(path, (str, unicode)):
241 path = path.split(':')
243 executable_basename = os.path.basename(executable)
244 for p in path:
245 if os.path.relpath(executable, p) == executable_basename:
246 return executable_basename
247 # TODO: support "../python" as a return?
248 return executable
251# prompt and echo owe a decent amount of design to click (and
252# pocket_protector)
253def isatty(stream):
254 try:
255 return stream.isatty()
256 except Exception:
257 return False
260def should_strip_ansi(stream=None):
261 if stream is None:
262 stream = sys.stdin
263 return not isatty(stream)
266def echo(msg, **kw):
267 msg = msg or ''
268 is_err = kw.pop('err', False)
269 _file = kw.pop('file', sys.stdout if not is_err else sys.stderr)
270 end = kw.pop('end', None)
271 enable_color = kw.pop('color', None)
273 if enable_color is None:
274 enable_color = not should_strip_ansi(_file)
276 if end is None:
277 if kw.pop('nl', True):
278 end = u'\n' if isinstance(msg, unicode) else b'\n'
279 if end:
280 msg += end
282 if msg:
283 if not enable_color:
284 msg = strip_ansi(msg)
285 _file.write(msg)
287 _file.flush()
289 return
292def echo_err(*a, **kw):
293 kw['err'] = True
294 return echo(*a, **kw)
297# variant-style shortcut to help minimize kwarg noise and imports
298echo.err = echo_err
301def _get_text(inp):
302 if not isinstance(inp, unicode):
303 return inp.decode('utf8')
304 return inp
307def prompt(label, confirm=None, confirm_label=None, hide_input=False, err=False):
308 do_confirm = confirm or confirm_label
309 if do_confirm and not confirm_label:
310 confirm_label = 'Retype %s' % (label.lower(),)
312 def prompt_func(label):
313 func = getpass.getpass if hide_input else raw_input
314 try:
315 # Write the prompt separately so that we get nice
316 # coloring through colorama on Windows (someday)
317 echo(label, nl=False, err=err)
318 ret = func('')
319 except (KeyboardInterrupt, EOFError):
320 # getpass doesn't print a newline if the user aborts input with ^C.
321 # Allegedly this behavior is inherited from getpass(3).
322 # A doc bug has been filed at https://bugs.python.org/issue24711
323 if hide_input:
324 echo(None, err=err)
325 raise
327 return ret
329 ret = prompt_func(label)
330 ret = _get_text(ret)
331 if do_confirm:
332 ret2 = prompt_func(confirm_label)
333 ret2 = _get_text(ret2)
334 if ret != ret2:
335 raise face.UsageError('Sorry, inputs did not match.')
337 return ret
340def prompt_secret(label, **kw):
341 kw['hide_input'] = True
342 kw.setdefault('err', True) # getpass usually puts prompts on stderr
343 return prompt(label, **kw)
346# variant-style shortcut to help minimize kwarg noise and imports
347prompt.secret = prompt_secret