cs-cmdutils 20240412__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cs/cmdutils.py
ADDED
|
@@ -0,0 +1,1482 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Command line stuff. - Cameron Simpson <cs@cskk.id.au> 03sep2015
|
|
4
|
+
#
|
|
5
|
+
# pylint: disable=too-many-lines
|
|
6
|
+
|
|
7
|
+
''' Convenience functions for working with the Cmd module,
|
|
8
|
+
the BaseCommand class for constructing command line programmes,
|
|
9
|
+
and other command line related stuff.
|
|
10
|
+
'''
|
|
11
|
+
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from cmd import Cmd
|
|
14
|
+
from code import interact
|
|
15
|
+
from collections import namedtuple
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from getopt import getopt, GetoptError
|
|
19
|
+
from inspect import isclass, ismethod
|
|
20
|
+
from os.path import basename
|
|
21
|
+
try:
|
|
22
|
+
import readline # pylint: disable=unused-import
|
|
23
|
+
except ImportError:
|
|
24
|
+
pass
|
|
25
|
+
import shlex
|
|
26
|
+
from signal import SIGHUP, SIGINT, SIGQUIT, SIGTERM
|
|
27
|
+
import sys
|
|
28
|
+
from typing import Callable, List, Mapping, Optional, Tuple
|
|
29
|
+
|
|
30
|
+
from typeguard import typechecked
|
|
31
|
+
|
|
32
|
+
from cs.context import stackattrs
|
|
33
|
+
from cs.deco import default_params, fmtdoc, Promotable
|
|
34
|
+
from cs.lex import (
|
|
35
|
+
cutprefix,
|
|
36
|
+
cutsuffix,
|
|
37
|
+
format_escape,
|
|
38
|
+
is_identifier,
|
|
39
|
+
r,
|
|
40
|
+
stripped_dedent,
|
|
41
|
+
)
|
|
42
|
+
from cs.logutils import setup_logging, warning, error, exception
|
|
43
|
+
from cs.pfx import Pfx, pfx_call, pfx_method
|
|
44
|
+
from cs.py.doc import obj_docstring
|
|
45
|
+
from cs.resources import RunState, uses_runstate
|
|
46
|
+
from cs.result import CancellationError
|
|
47
|
+
from cs.threads import HasThreadState, ThreadState
|
|
48
|
+
from cs.typingutils import subtype
|
|
49
|
+
from cs.upd import Upd, uses_upd
|
|
50
|
+
|
|
51
|
+
__version__ = '20240412'
|
|
52
|
+
|
|
53
|
+
DISTINFO = {
|
|
54
|
+
'keywords': ["python2", "python3"],
|
|
55
|
+
'classifiers': [
|
|
56
|
+
"Programming Language :: Python",
|
|
57
|
+
"Programming Language :: Python :: 3",
|
|
58
|
+
],
|
|
59
|
+
'install_requires': [
|
|
60
|
+
'cs.context',
|
|
61
|
+
'cs.deco',
|
|
62
|
+
'cs.lex',
|
|
63
|
+
'cs.logutils',
|
|
64
|
+
'cs.pfx',
|
|
65
|
+
'cs.py.doc',
|
|
66
|
+
'cs.resources',
|
|
67
|
+
'cs.result',
|
|
68
|
+
'cs.threads',
|
|
69
|
+
'cs.typingutils',
|
|
70
|
+
'cs.upd',
|
|
71
|
+
'typeguard',
|
|
72
|
+
],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def docmd(dofunc):
|
|
76
|
+
''' Decorator for `cmd.Cmd` subclass methods
|
|
77
|
+
to supply some basic quality of service.
|
|
78
|
+
|
|
79
|
+
This decorator:
|
|
80
|
+
- wraps the function call in a `cs.pfx.Pfx` for context
|
|
81
|
+
- intercepts `getopt.GetoptError`s, issues a `warning`
|
|
82
|
+
and runs `self.do_help` with the method name,
|
|
83
|
+
then returns `None`
|
|
84
|
+
- intercepts other `Exception`s,
|
|
85
|
+
issues an `exception` log message
|
|
86
|
+
and returns `None`
|
|
87
|
+
|
|
88
|
+
The intended use is to decorate `cmd.Cmd` `do_`* methods:
|
|
89
|
+
|
|
90
|
+
from cmd import Cmd
|
|
91
|
+
from cs.cmdutils import docmd
|
|
92
|
+
...
|
|
93
|
+
class MyCmd(Cmd):
|
|
94
|
+
@docmd
|
|
95
|
+
def do_something(...):
|
|
96
|
+
... do something ...
|
|
97
|
+
'''
|
|
98
|
+
funcname = dofunc.__name__
|
|
99
|
+
|
|
100
|
+
def docmd_wrapper(self, *a, **kw):
|
|
101
|
+
''' Run a `Cmd` "do" method with some context and handling.
|
|
102
|
+
'''
|
|
103
|
+
if not funcname.startswith('do_'):
|
|
104
|
+
raise ValueError("function does not start with 'do_': %s" % (funcname,))
|
|
105
|
+
argv0 = funcname[3:]
|
|
106
|
+
with Pfx(argv0):
|
|
107
|
+
try:
|
|
108
|
+
return dofunc(self, *a, **kw)
|
|
109
|
+
except GetoptError as e:
|
|
110
|
+
warning("%s", e)
|
|
111
|
+
self.do_help(argv0)
|
|
112
|
+
return None
|
|
113
|
+
except Exception as e: # pylint: disable=broad-except
|
|
114
|
+
exception("%s", e)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
docmd_wrapper.__name__ = '@docmd(%s)' % (funcname,)
|
|
118
|
+
docmd_wrapper.__doc__ = dofunc.__doc__
|
|
119
|
+
return docmd_wrapper
|
|
120
|
+
|
|
121
|
+
class _BaseSubCommand(ABC):
|
|
122
|
+
''' The basis for the classes implementing subcommands.
|
|
123
|
+
'''
|
|
124
|
+
|
|
125
|
+
def __init__(self, cmd, method, *, usage_mapping=None):
|
|
126
|
+
self.cmd = cmd
|
|
127
|
+
self.method = method
|
|
128
|
+
self.usage_mapping = usage_mapping or {}
|
|
129
|
+
|
|
130
|
+
def __str__(self):
|
|
131
|
+
return "%s(cmd=%r,method=%s,..)" % (
|
|
132
|
+
type(self).__name__, self.cmd, self.method
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@abstractmethod
|
|
136
|
+
def __call__(
|
|
137
|
+
self, subcmd: str, base_command: "BaseCommandSubType", argv: List[str]
|
|
138
|
+
):
|
|
139
|
+
''' Run the subcommand.
|
|
140
|
+
|
|
141
|
+
Parameters:
|
|
142
|
+
* `subcmd`: the subcommand name
|
|
143
|
+
* `base_command`: the instance of `BaseCommand`
|
|
144
|
+
* `argv`: the command line arguments after the subcommand name
|
|
145
|
+
'''
|
|
146
|
+
raise NotImplementedError
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def from_class(command_cls: "BaseCommandSubType") -> Mapping[str, Callable]:
|
|
150
|
+
''' Return a mapping of subcommand names to subcommand specifications
|
|
151
|
+
for class attributes which commence with
|
|
152
|
+
`command_cls.SUBCOMMAND_METHOD_PREFIX`,
|
|
153
|
+
by default `'cmd_'`.
|
|
154
|
+
'''
|
|
155
|
+
prefix = command_cls.SUBCOMMAND_METHOD_PREFIX
|
|
156
|
+
subcommands_map = {}
|
|
157
|
+
for attr in dir(command_cls):
|
|
158
|
+
if attr.startswith(prefix):
|
|
159
|
+
subcmd = cutprefix(attr, prefix)
|
|
160
|
+
method = getattr(command_cls, attr)
|
|
161
|
+
if isclass(method):
|
|
162
|
+
subcommands_map[subcmd] = _ClassSubCommand(
|
|
163
|
+
subcmd,
|
|
164
|
+
method,
|
|
165
|
+
usage_mapping=dict(getattr(method, 'USAGE_KEYWORDS', ())),
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
subcommands_map[subcmd] = _MethodSubCommand(
|
|
169
|
+
subcmd,
|
|
170
|
+
method,
|
|
171
|
+
usage_mapping=dict(getattr(command_cls, 'USAGE_KEYWORDS', ())),
|
|
172
|
+
)
|
|
173
|
+
return subcommands_map
|
|
174
|
+
|
|
175
|
+
def usage_text(
|
|
176
|
+
self,
|
|
177
|
+
short: bool,
|
|
178
|
+
usage_format_mapping: Optional[Mapping] = None
|
|
179
|
+
) -> str:
|
|
180
|
+
''' Return the filled out usage text for this subcommand.
|
|
181
|
+
'''
|
|
182
|
+
usage_format_mapping = usage_format_mapping or {}
|
|
183
|
+
subusage_format = self.usage_format() # pylint: disable=no-member
|
|
184
|
+
if subusage_format:
|
|
185
|
+
if short:
|
|
186
|
+
# the summary line and opening sentence of the description
|
|
187
|
+
lines = subusage_format.split('\n')
|
|
188
|
+
subusage_lines = [lines.pop(0)]
|
|
189
|
+
while subusage_lines[-1].endswith('\\'):
|
|
190
|
+
subusage_lines.append(lines.pop(0))
|
|
191
|
+
if lines and lines[0].endswith('.'):
|
|
192
|
+
subusage_lines.append(lines.pop(0))
|
|
193
|
+
subusage_format = '\n'.join(subusage_lines)
|
|
194
|
+
mapping = {
|
|
195
|
+
k: v
|
|
196
|
+
for k, v in sys.modules[self.method.__module__].__dict__.items()
|
|
197
|
+
if k and not k.startswith('_')
|
|
198
|
+
}
|
|
199
|
+
if usage_format_mapping:
|
|
200
|
+
mapping.update(usage_format_mapping)
|
|
201
|
+
if self.usage_mapping:
|
|
202
|
+
mapping.update(self.usage_mapping)
|
|
203
|
+
mapping.update(cmd=self.cmd)
|
|
204
|
+
with Pfx("format %r using %r", subusage_format, mapping):
|
|
205
|
+
subusage = subusage_format.format_map(mapping)
|
|
206
|
+
return subusage or None
|
|
207
|
+
|
|
208
|
+
class _MethodSubCommand(_BaseSubCommand):
|
|
209
|
+
''' A class to represent a subcommand implemented with a method.
|
|
210
|
+
'''
|
|
211
|
+
|
|
212
|
+
def __call__(
|
|
213
|
+
self, subcmd: str, command: "BaseCommandSubType", argv: List[str]
|
|
214
|
+
):
|
|
215
|
+
with Pfx(subcmd):
|
|
216
|
+
method = self.method
|
|
217
|
+
if ismethod(method):
|
|
218
|
+
# already bound
|
|
219
|
+
return method(argv)
|
|
220
|
+
# unbound - supply the instance for use as self
|
|
221
|
+
return method(command, argv)
|
|
222
|
+
|
|
223
|
+
def usage_format(self):
|
|
224
|
+
''' Return the usage format string from the method docstring.
|
|
225
|
+
'''
|
|
226
|
+
doc = obj_docstring(self.method)
|
|
227
|
+
if doc:
|
|
228
|
+
if 'Usage:' in doc:
|
|
229
|
+
# extract the Usage: paragraph
|
|
230
|
+
pre_usage, post_usage = doc.split('Usage:', 1)
|
|
231
|
+
pre_usage = pre_usage.strip()
|
|
232
|
+
post_usage_format, *_ = post_usage.split('\n\n', 1)
|
|
233
|
+
subusage_format = stripped_dedent(post_usage_format)
|
|
234
|
+
else:
|
|
235
|
+
# extract the first paragraph
|
|
236
|
+
lines = ['{cmd} ...']
|
|
237
|
+
doc_p1 = stripped_dedent(doc.split('\n\n', 1)[0])
|
|
238
|
+
if doc_p1:
|
|
239
|
+
lines.extend(map(format_escape, doc_p1.split('\n')))
|
|
240
|
+
subusage_format = "\n ".join(lines)
|
|
241
|
+
else:
|
|
242
|
+
# default usage text
|
|
243
|
+
subusage_format = '{cmd} ...'
|
|
244
|
+
return subusage_format
|
|
245
|
+
|
|
246
|
+
class _ClassSubCommand(_BaseSubCommand):
|
|
247
|
+
''' A class to represent a subcommand implemented with a `BaseCommand` subclass.
|
|
248
|
+
'''
|
|
249
|
+
|
|
250
|
+
def __call__(
|
|
251
|
+
self, subcmd: str, command: "BaseCommandSubType", argv: List[str]
|
|
252
|
+
):
|
|
253
|
+
subcmd_class = self.method
|
|
254
|
+
updates = dict(command.options.__dict__)
|
|
255
|
+
updates.update(cmd=subcmd)
|
|
256
|
+
command = pfx_call(subcmd_class, argv, **updates)
|
|
257
|
+
return command.run()
|
|
258
|
+
|
|
259
|
+
def usage_format(self) -> str:
|
|
260
|
+
''' Return the usage format string from the class.
|
|
261
|
+
'''
|
|
262
|
+
doc = self.method.usage_text(cmd=self.cmd)
|
|
263
|
+
subusage_format, *_ = cutprefix(doc, 'Usage:').lstrip().split("\n\n", 1)
|
|
264
|
+
return subusage_format
|
|
265
|
+
|
|
266
|
+
# gimmkicked name to support @fmtdoc on BaseCommandOptions.popopts
|
|
267
|
+
_COMMON_OPT_SPECS = dict(
|
|
268
|
+
n='dry_run',
|
|
269
|
+
q='quiet',
|
|
270
|
+
v='verbose',
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
@dataclass
|
|
274
|
+
class BaseCommandOptions(HasThreadState):
|
|
275
|
+
''' A base class for the `BaseCommand` `options` object.
|
|
276
|
+
|
|
277
|
+
This is the default class for the `self.options` object
|
|
278
|
+
available during `BaseCommand.run()`,
|
|
279
|
+
and available as the `BaseCommand.Options` attribute.
|
|
280
|
+
|
|
281
|
+
Any keyword arguments are applied as field updates to the instance.
|
|
282
|
+
|
|
283
|
+
It comes prefilled with:
|
|
284
|
+
* `.dry_run=False`
|
|
285
|
+
* `.force=False`
|
|
286
|
+
* `.quiet=False`
|
|
287
|
+
* `.verbose=False`
|
|
288
|
+
and a `.doit` property which is the inverse of `.dry_run`.
|
|
289
|
+
|
|
290
|
+
It is recommended that if ``BaseCommand` subclasses use a
|
|
291
|
+
different type for their `Options` that it should be a
|
|
292
|
+
subclass of `BaseCommandOptions`.
|
|
293
|
+
Since `BaseCommandOptions` is a data class, this typically looks like:
|
|
294
|
+
|
|
295
|
+
@dataclass
|
|
296
|
+
class Options(BaseCommand.Options):
|
|
297
|
+
... optional extra fields etc ...
|
|
298
|
+
'''
|
|
299
|
+
|
|
300
|
+
DEFAULT_SIGNALS = SIGHUP, SIGINT, SIGQUIT, SIGTERM
|
|
301
|
+
COMMON_OPT_SPECS = _COMMON_OPT_SPECS
|
|
302
|
+
|
|
303
|
+
cmd: Optional[str] = None
|
|
304
|
+
dry_run: bool = False
|
|
305
|
+
force: bool = False
|
|
306
|
+
quiet: bool = False
|
|
307
|
+
runstate_signals: Tuple[int] = DEFAULT_SIGNALS
|
|
308
|
+
verbose: bool = False
|
|
309
|
+
|
|
310
|
+
perthread_state = ThreadState()
|
|
311
|
+
|
|
312
|
+
def copy(self, **updates):
|
|
313
|
+
''' Return a new instance of `BaseCommandOptions` (well, `type(self)`)
|
|
314
|
+
which is a shallow copy of the public attributes from `self.__dict__`.
|
|
315
|
+
|
|
316
|
+
Any keyword arguments are applied as attribute updates to the copy.
|
|
317
|
+
'''
|
|
318
|
+
copied = pfx_call(
|
|
319
|
+
type(self),
|
|
320
|
+
**{
|
|
321
|
+
k: v
|
|
322
|
+
for k, v in self.__dict__.items()
|
|
323
|
+
if not k.startswith('_')
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
for k, v in updates.items():
|
|
327
|
+
setattr(copied, k, v)
|
|
328
|
+
return copied
|
|
329
|
+
|
|
330
|
+
# TODO: remove this - the overt make-a-copy-and-with-the-copy is clearer
|
|
331
|
+
@contextmanager
|
|
332
|
+
def __call__(self, **updates):
|
|
333
|
+
''' Calling the options object returns a context manager whose
|
|
334
|
+
value is a copy of the options with any `suboptions` applied.
|
|
335
|
+
|
|
336
|
+
Example showing the semantics:
|
|
337
|
+
|
|
338
|
+
>>> from cs.cmdutils import BaseCommandOptions
|
|
339
|
+
>>> options = BaseCommandOptions(x=1)
|
|
340
|
+
>>> assert options.x == 1
|
|
341
|
+
>>> assert not options.verbose
|
|
342
|
+
>>> with options(verbose=True) as subopts:
|
|
343
|
+
... assert options is not subopts
|
|
344
|
+
... assert options.x == 1
|
|
345
|
+
... assert not options.verbose
|
|
346
|
+
... assert subopts.x == 1
|
|
347
|
+
... assert subopts.verbose
|
|
348
|
+
...
|
|
349
|
+
>>> assert options.x == 1
|
|
350
|
+
>>> assert not options.verbose
|
|
351
|
+
|
|
352
|
+
'''
|
|
353
|
+
suboptions = self.copy(**updates)
|
|
354
|
+
yield suboptions
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def doit(self):
|
|
358
|
+
''' I usually use a `doit` flag,
|
|
359
|
+
the inverse of `dry_run`.
|
|
360
|
+
'''
|
|
361
|
+
return not self.dry_run
|
|
362
|
+
|
|
363
|
+
@doit.setter
|
|
364
|
+
def doit(self, new_doit):
|
|
365
|
+
''' Set `dry_run` to the inverse of `new_doit`.
|
|
366
|
+
'''
|
|
367
|
+
self.dry_run = not new_doit
|
|
368
|
+
|
|
369
|
+
@fmtdoc
|
|
370
|
+
def popopts(self, argv, **opt_specs):
|
|
371
|
+
''' Convenience method to appply `BaseCommand.popopts` to the options (`self`).
|
|
372
|
+
|
|
373
|
+
Example for a `BaseCommand` `cmd_foo` method:
|
|
374
|
+
|
|
375
|
+
def cmd_foo(self, argv):
|
|
376
|
+
self.options.popopts(
|
|
377
|
+
c_='config',
|
|
378
|
+
l='long',
|
|
379
|
+
x='trace',
|
|
380
|
+
)
|
|
381
|
+
if self.options.dry_run:
|
|
382
|
+
print("dry run!")
|
|
383
|
+
|
|
384
|
+
The class attribute `COMMON_OPT_SPECS` is a mapping of
|
|
385
|
+
options which are always supported. `BaseCommandOptions`
|
|
386
|
+
has: `COMMON_OPT_SPECS={_COMMON_OPT_SPECS!r}`.
|
|
387
|
+
|
|
388
|
+
A subclass with more common options might extend this like so,
|
|
389
|
+
from `cs.hashindex`:
|
|
390
|
+
|
|
391
|
+
COMMON_OPT_SPECS = dict(
|
|
392
|
+
e='ssh_exe',
|
|
393
|
+
h_='hashname',
|
|
394
|
+
H_='hashindex_exe',
|
|
395
|
+
**BaseCommand.Options.COMMON_OPT_SPECS,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
'''
|
|
399
|
+
for k, v in self.COMMON_OPT_SPECS.items():
|
|
400
|
+
opt_specs.setdefault(k, v)
|
|
401
|
+
BaseCommand.popopts(argv, self, **opt_specs)
|
|
402
|
+
|
|
403
|
+
def uses_cmd_options(
|
|
404
|
+
func, cls=BaseCommandOptions, options_param_name='options'
|
|
405
|
+
):
|
|
406
|
+
''' A decorator to provide a default parameter containing the
|
|
407
|
+
prevailing `BaseCommandOptions` instance as the `options` keyword
|
|
408
|
+
argument, using the `cs.deco.default_params` decorator factory.
|
|
409
|
+
|
|
410
|
+
This allows functions to utilitse global options set by a
|
|
411
|
+
command such as `options.dry_run` or `options.verbose` without
|
|
412
|
+
the tedious plumbing through the entire call stack.
|
|
413
|
+
|
|
414
|
+
Parameters:
|
|
415
|
+
* `cls`: the `BaseCommandOptions` or `BaseCommand` class,
|
|
416
|
+
default `BaseCommandOptions`. If a `BaseCommand` subclass is
|
|
417
|
+
provided its `cls.Options` class is used.
|
|
418
|
+
* `options_param_name`: the parameter name to provide, default `options`
|
|
419
|
+
|
|
420
|
+
Examples:
|
|
421
|
+
|
|
422
|
+
@uses_cmd_options
|
|
423
|
+
def f(x,*,options):
|
|
424
|
+
""" Run directly from the prevailing options. """
|
|
425
|
+
if options.verbose:
|
|
426
|
+
print("doing f with x =", x)
|
|
427
|
+
....
|
|
428
|
+
|
|
429
|
+
@uses_cmd_options
|
|
430
|
+
def f(x,*,verbose=None,options):
|
|
431
|
+
""" Get defaults from the prevailing options. """
|
|
432
|
+
if verbose is None:
|
|
433
|
+
verbose = options.verbose
|
|
434
|
+
if verbose:
|
|
435
|
+
print("doing f with x =", x)
|
|
436
|
+
....
|
|
437
|
+
'''
|
|
438
|
+
if issubclass(cls, BaseCommand):
|
|
439
|
+
cls = cls.Options
|
|
440
|
+
return default_params(
|
|
441
|
+
func, **{options_param_name: lambda: cls.default() or cls()}
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
class BaseCommand:
|
|
445
|
+
''' A base class for handling nestable command lines.
|
|
446
|
+
|
|
447
|
+
This class provides the basic parse and dispatch mechanisms
|
|
448
|
+
for command lines.
|
|
449
|
+
To implement a command line
|
|
450
|
+
one instantiates a subclass of `BaseCommand`:
|
|
451
|
+
|
|
452
|
+
class MyCommand(BaseCommand):
|
|
453
|
+
GETOPT_SPEC = 'ab:c'
|
|
454
|
+
USAGE_FORMAT = r"""Usage: {cmd} [-a] [-b bvalue] [-c] [--] arguments...
|
|
455
|
+
-a Do it all.
|
|
456
|
+
-b But using bvalue.
|
|
457
|
+
-c The 'c' option!
|
|
458
|
+
"""
|
|
459
|
+
...
|
|
460
|
+
|
|
461
|
+
and provides either a `main` method if the command has no subcommands
|
|
462
|
+
or a suite of `cmd_`*subcommand* methods, one per subcommand.
|
|
463
|
+
|
|
464
|
+
Running a command is done by:
|
|
465
|
+
|
|
466
|
+
MyCommand(argv).run()
|
|
467
|
+
|
|
468
|
+
Modules which implement a command line mode generally look like this:
|
|
469
|
+
|
|
470
|
+
... imports etc ...
|
|
471
|
+
def main(argv=None, **run_kw):
|
|
472
|
+
""" The command line mode.
|
|
473
|
+
"""
|
|
474
|
+
return MyCommand(argv).run(**run_kw)
|
|
475
|
+
... other code ...
|
|
476
|
+
class MyCommand(BaseCommand):
|
|
477
|
+
... other code ...
|
|
478
|
+
if __name__ == '__main__':
|
|
479
|
+
sys.exit(main(sys.argv))
|
|
480
|
+
|
|
481
|
+
Instances have a `self.options` attribute on which optional
|
|
482
|
+
modes are set,
|
|
483
|
+
avoiding conflict with the attributes of `self`.
|
|
484
|
+
|
|
485
|
+
Subclasses with no subcommands
|
|
486
|
+
generally just implement a `main(argv)` method.
|
|
487
|
+
|
|
488
|
+
Subclasses with subcommands
|
|
489
|
+
should implement a `cmd_`*subcommand*`(argv)` instance method
|
|
490
|
+
for each subcommand.
|
|
491
|
+
If a subcommand is itself implemented using `BaseCommand`
|
|
492
|
+
then it can be a simple attribute:
|
|
493
|
+
|
|
494
|
+
cmd_subthing = SubThingCommand
|
|
495
|
+
|
|
496
|
+
Returning to methods, if there is a paragraph in the method docstring
|
|
497
|
+
commencing with `Usage:`
|
|
498
|
+
then that paragraph is incorporated automatically
|
|
499
|
+
into the main usage message.
|
|
500
|
+
Example:
|
|
501
|
+
|
|
502
|
+
def cmd_ls(self, argv):
|
|
503
|
+
""" Usage: {cmd} [paths...]
|
|
504
|
+
Emit a listing for the named paths.
|
|
505
|
+
|
|
506
|
+
Further docstring non-usage information here.
|
|
507
|
+
"""
|
|
508
|
+
... do the "ls" subcommand ...
|
|
509
|
+
|
|
510
|
+
The subclass is customised by overriding the following methods:
|
|
511
|
+
* `apply_opt(opt,val)`:
|
|
512
|
+
apply an individual getopt global command line option
|
|
513
|
+
to `self.options`.
|
|
514
|
+
* `apply_opts(opts)`:
|
|
515
|
+
apply the `opts` to `self.options`.
|
|
516
|
+
`opts` is an `(option,value)` sequence
|
|
517
|
+
as returned by `getopot.getopt`.
|
|
518
|
+
The default implementation iterates over these and calls `apply_opt`.
|
|
519
|
+
* `cmd_`*subcmd*`(argv)`:
|
|
520
|
+
if the command line options are followed by an argument
|
|
521
|
+
whose value is *subcmd*,
|
|
522
|
+
then the method `cmd_`*subcmd*`(subcmd_argv)`
|
|
523
|
+
will be called where `subcmd_argv` contains the command line arguments
|
|
524
|
+
following *subcmd*.
|
|
525
|
+
* `main(argv)`:
|
|
526
|
+
if there are no command line arguments after the options
|
|
527
|
+
or the first argument does not have a corresponding
|
|
528
|
+
`cmd_`*subcmd* method
|
|
529
|
+
then method `main(argv)`
|
|
530
|
+
will be called where `argv` contains the command line arguments.
|
|
531
|
+
* `run_context()`:
|
|
532
|
+
a context manager to provide setup or teardown actions
|
|
533
|
+
to occur before and after the command implementation respectively,
|
|
534
|
+
such as to open and close a database.
|
|
535
|
+
|
|
536
|
+
Editorial: why not arparse?
|
|
537
|
+
Primarily because when incorrectly invoked
|
|
538
|
+
an argparse command line prints the help/usage messgae
|
|
539
|
+
and aborts the whole programme with `SystemExit`.
|
|
540
|
+
But also, I find the whole argparse `add_argument` thing cumbersome.
|
|
541
|
+
'''
|
|
542
|
+
|
|
543
|
+
SUBCOMMAND_METHOD_PREFIX = 'cmd_'
|
|
544
|
+
GETOPT_SPEC = ''
|
|
545
|
+
SUBCOMMAND_ARGV_DEFAULT = None
|
|
546
|
+
Options = BaseCommandOptions
|
|
547
|
+
|
|
548
|
+
def __init_subclass__(cls):
|
|
549
|
+
''' Update subclasses of `BaseCommand`.
|
|
550
|
+
|
|
551
|
+
Appends the usage message to the class docstring.
|
|
552
|
+
'''
|
|
553
|
+
usage_message = cls.usage_text()
|
|
554
|
+
usage_doc = (
|
|
555
|
+
'Command line usage:\n\n ' + usage_message.replace('\n', '\n ')
|
|
556
|
+
)
|
|
557
|
+
cls_doc = obj_docstring(cls)
|
|
558
|
+
cls_doc = cls_doc + '\n\n' + usage_doc if cls_doc else usage_doc
|
|
559
|
+
cls.__doc__ = cls_doc
|
|
560
|
+
|
|
561
|
+
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
|
|
562
|
+
def __init__(self, argv=None, *, cmd=None, options=None, **kw_options):
|
|
563
|
+
''' Initialise the command line.
|
|
564
|
+
Raises `GetoptError` for unrecognised options.
|
|
565
|
+
|
|
566
|
+
Parameters:
|
|
567
|
+
* `argv`:
|
|
568
|
+
optional command line arguments
|
|
569
|
+
including the main command name if `cmd` is not specified.
|
|
570
|
+
The default is `sys.argv`.
|
|
571
|
+
The contents of `argv` are copied,
|
|
572
|
+
permitting desctructive parsing of `argv`.
|
|
573
|
+
* `cmd`:
|
|
574
|
+
optional keyword specifying the command name for context;
|
|
575
|
+
if this is not specified it is taken from `argv.pop(0)`.
|
|
576
|
+
* `options`:
|
|
577
|
+
an optional keyword providing object for command state and context.
|
|
578
|
+
If not specified a new `self.Options` instance
|
|
579
|
+
is allocated for use as `options`.
|
|
580
|
+
The default `Options` class is `BaseCommandOptions`,
|
|
581
|
+
a dataclass with some prefilled attributes and properties
|
|
582
|
+
to aid use later.
|
|
583
|
+
Other keyword arguments are applied to `self.options`
|
|
584
|
+
as attributes.
|
|
585
|
+
|
|
586
|
+
The `cmd` and `argv` parameters have some fiddly semantics for convenience.
|
|
587
|
+
There are 3 basic ways to initialise:
|
|
588
|
+
* `BaseCommand()`: `argv` comes from `sys.argv`
|
|
589
|
+
and the value for `cmd` is derived from `argv[0]`
|
|
590
|
+
* `BaseCommand(argv)`: `argv` is the complete command line
|
|
591
|
+
including the command name and the value for `cmd` is
|
|
592
|
+
derived from `argv[0]`
|
|
593
|
+
* `BaseCommand(argv, cmd=foo)`: `argv` is the command
|
|
594
|
+
arguments _after_ the command name and `cmd` is set to
|
|
595
|
+
`foo`
|
|
596
|
+
|
|
597
|
+
The command line arguments are parsed according to
|
|
598
|
+
the optional `GETOPT_SPEC` class attribute (default `''`).
|
|
599
|
+
If `getopt_spec` is not empty
|
|
600
|
+
then `apply_opts(opts)` is called
|
|
601
|
+
to apply the supplied options to the state
|
|
602
|
+
where `opts` is the return from `getopt.getopt(argv,getopt_spec)`.
|
|
603
|
+
|
|
604
|
+
After the option parse,
|
|
605
|
+
if the first command line argument *foo*
|
|
606
|
+
has a corresponding method `cmd_`*foo*
|
|
607
|
+
then that argument is removed from the start of `argv`
|
|
608
|
+
and `self.cmd_`*foo*`(argv,options,cmd=`*foo*`)` is called
|
|
609
|
+
and its value returned.
|
|
610
|
+
Otherwise `self.main(argv,options)` is called
|
|
611
|
+
and its value returned.
|
|
612
|
+
|
|
613
|
+
If the command implementation requires some setup or teardown
|
|
614
|
+
then this may be provided by the `run_context`
|
|
615
|
+
context manager method,
|
|
616
|
+
called with `cmd=`*subcmd* for subcommands
|
|
617
|
+
and with `cmd=None` for `main`.
|
|
618
|
+
'''
|
|
619
|
+
subcmds = self.subcommands()
|
|
620
|
+
has_subcmds = subcmds and sorted(subcmds) != ['help', 'shell']
|
|
621
|
+
if argv is None:
|
|
622
|
+
# using sys.argv:
|
|
623
|
+
# argv0 comes from sys.argv[0], which is discarded
|
|
624
|
+
argv = list(sys.argv)
|
|
625
|
+
argv0 = argv.pop(0)
|
|
626
|
+
else:
|
|
627
|
+
# argv provided:
|
|
628
|
+
# if cmd is None, pop argv0 from argv
|
|
629
|
+
# otherwise set argv0=cmd
|
|
630
|
+
argv = list(argv)
|
|
631
|
+
if cmd is None:
|
|
632
|
+
argv0 = argv.pop(0)
|
|
633
|
+
else:
|
|
634
|
+
argv0 = cmd
|
|
635
|
+
if cmd is None:
|
|
636
|
+
cmd = basename(argv0)
|
|
637
|
+
self.cmd = cmd
|
|
638
|
+
log_level = getattr(options, 'log_level', None)
|
|
639
|
+
loginfo = setup_logging(cmd, level=log_level)
|
|
640
|
+
# post: argv is list of arguments after the command name
|
|
641
|
+
self.loginfo = loginfo
|
|
642
|
+
options = self.options = self.Options(cmd=self.cmd)
|
|
643
|
+
# override the default options
|
|
644
|
+
for option, value in kw_options.items():
|
|
645
|
+
setattr(options, option, value)
|
|
646
|
+
self._argv = argv
|
|
647
|
+
self._run = lambda subcmd, command, argv: 2
|
|
648
|
+
self._subcmd = None
|
|
649
|
+
self._printed_usage = False
|
|
650
|
+
# we catch GetoptError from this suite...
|
|
651
|
+
subcmd = None # default: no subcmd specific usage available
|
|
652
|
+
short_usage = False
|
|
653
|
+
try:
|
|
654
|
+
getopt_spec = getattr(self, 'GETOPT_SPEC', '')
|
|
655
|
+
# catch bare -h or --help if no 'h' in the getopt_spec
|
|
656
|
+
if ('h' not in getopt_spec and len(argv) == 1
|
|
657
|
+
and argv[0] in ('-h', '-help', '--help')):
|
|
658
|
+
argv = ['help']
|
|
659
|
+
else:
|
|
660
|
+
# we do this regardless in order to honour '--'
|
|
661
|
+
try:
|
|
662
|
+
opts, argv = getopt(argv, getopt_spec, '')
|
|
663
|
+
except GetoptError:
|
|
664
|
+
short_usage = True
|
|
665
|
+
raise
|
|
666
|
+
self.apply_opts(opts)
|
|
667
|
+
# we do this regardless so that subclasses can do some presubcommand parsing
|
|
668
|
+
# after any command line options
|
|
669
|
+
argv = self._argv = self.apply_preargv(argv)
|
|
670
|
+
|
|
671
|
+
# now prepare self._run, a callable
|
|
672
|
+
if not has_subcmds:
|
|
673
|
+
# no subcommands, just use the main() method
|
|
674
|
+
try:
|
|
675
|
+
main = self.main
|
|
676
|
+
except AttributeError:
|
|
677
|
+
# pylint: disable=raise-missing-from
|
|
678
|
+
raise GetoptError("no main method and no subcommand methods")
|
|
679
|
+
self._run = _MethodSubCommand(None, main)
|
|
680
|
+
else:
|
|
681
|
+
# expect a subcommand on the command line
|
|
682
|
+
if not argv:
|
|
683
|
+
default_argv = getattr(self, 'SUBCOMMAND_ARGV_DEFAULT', None)
|
|
684
|
+
if not default_argv:
|
|
685
|
+
short_usage = True
|
|
686
|
+
raise GetoptError(
|
|
687
|
+
"missing subcommand, expected one of: %s" %
|
|
688
|
+
(', '.join(sorted(subcmds.keys())),)
|
|
689
|
+
)
|
|
690
|
+
argv = (
|
|
691
|
+
[default_argv]
|
|
692
|
+
if isinstance(default_argv, str) else list(default_argv)
|
|
693
|
+
)
|
|
694
|
+
subcmd = argv.pop(0)
|
|
695
|
+
subcmd_ = subcmd.replace('-', '_').replace('.', '_')
|
|
696
|
+
try:
|
|
697
|
+
subcommand = subcmds[subcmd_]
|
|
698
|
+
except KeyError:
|
|
699
|
+
# pylint: disable=raise-missing-from
|
|
700
|
+
short_usage = True
|
|
701
|
+
bad_subcmd = subcmd
|
|
702
|
+
subcmd = None
|
|
703
|
+
raise GetoptError(
|
|
704
|
+
"%s: unrecognised subcommand, expected one of: %s" % (
|
|
705
|
+
bad_subcmd,
|
|
706
|
+
', '.join(sorted(subcmds.keys())),
|
|
707
|
+
)
|
|
708
|
+
)
|
|
709
|
+
self._run = subcommand
|
|
710
|
+
self._subcmd = subcmd
|
|
711
|
+
except GetoptError as e:
|
|
712
|
+
if self.getopt_error_handler(
|
|
713
|
+
cmd,
|
|
714
|
+
self.options,
|
|
715
|
+
e,
|
|
716
|
+
self.usage_text(subcmd=subcmd, short=short_usage),
|
|
717
|
+
):
|
|
718
|
+
self._printed_usage = True
|
|
719
|
+
return
|
|
720
|
+
raise
|
|
721
|
+
|
|
722
|
+
@classmethod
|
|
723
|
+
def subcommands(cls):
|
|
724
|
+
''' Return a mapping of subcommand names to subcommand specifications
|
|
725
|
+
for class attributes which commence with `cls.SUBCOMMAND_METHOD_PREFIX`
|
|
726
|
+
by default `'cmd_'`.
|
|
727
|
+
'''
|
|
728
|
+
try:
|
|
729
|
+
subcmds = cls.__dict__['_subcommands']
|
|
730
|
+
except KeyError:
|
|
731
|
+
subcmds = cls._subcommands = _BaseSubCommand.from_class(cls)
|
|
732
|
+
return subcmds
|
|
733
|
+
|
|
734
|
+
@classmethod
|
|
735
|
+
def usage_text(
|
|
736
|
+
cls, *, cmd=None, format_mapping=None, subcmd=None, short=False
|
|
737
|
+
):
|
|
738
|
+
''' Compute the "Usage:" message for this class
|
|
739
|
+
from the top level `USAGE_FORMAT`
|
|
740
|
+
and the `'Usage:'`-containing docstrings of its `cmd_*` methods.
|
|
741
|
+
|
|
742
|
+
Parameters:
|
|
743
|
+
* `cmd`: optional command name, default derived from the class name
|
|
744
|
+
* `format_mapping`: an optional format mapping for filling
|
|
745
|
+
in format strings in the usage text
|
|
746
|
+
* `subcmd`: constrain the usage to a particular subcommand named `subcmd`;
|
|
747
|
+
this is used to produce a shorter usage for subcommand usage failures
|
|
748
|
+
'''
|
|
749
|
+
if cmd is None:
|
|
750
|
+
cmd = cutsuffix(cls.__name__, 'Command').lower()
|
|
751
|
+
if format_mapping is None:
|
|
752
|
+
format_mapping = {}
|
|
753
|
+
format_mapping.setdefault('cmd', cmd)
|
|
754
|
+
subcmds = cls.subcommands()
|
|
755
|
+
has_subcmds = subcmds and list(subcmds) != ['help']
|
|
756
|
+
usage_format_mapping = dict(getattr(cls, 'USAGE_KEYWORDS', {}))
|
|
757
|
+
usage_format_mapping.update(format_mapping)
|
|
758
|
+
usage_format = getattr(
|
|
759
|
+
cls,
|
|
760
|
+
'USAGE_FORMAT',
|
|
761
|
+
(
|
|
762
|
+
'Usage: {cmd} subcommand [...]'
|
|
763
|
+
if has_subcmds else 'Usage: {cmd} [...]'
|
|
764
|
+
),
|
|
765
|
+
)
|
|
766
|
+
usage_message = usage_format.format_map(usage_format_mapping)
|
|
767
|
+
if subcmd:
|
|
768
|
+
if not has_subcmds:
|
|
769
|
+
raise ValueError("subcmd=%r: no subcommands!" % (subcmd,))
|
|
770
|
+
subcmd_ = subcmd.replace('-', '_').replace('.', '_')
|
|
771
|
+
try:
|
|
772
|
+
subcmds[subcmd_]
|
|
773
|
+
except KeyError:
|
|
774
|
+
# pylint: disable=raise-missing-from
|
|
775
|
+
raise ValueError(
|
|
776
|
+
"subcmd=%r: unknown subcommand, I know %r" %
|
|
777
|
+
(subcmd, sorted(subcmds.keys()))
|
|
778
|
+
)
|
|
779
|
+
subcmd = subcmd_
|
|
780
|
+
if has_subcmds:
|
|
781
|
+
subusages = []
|
|
782
|
+
for attr, subcmd_spec in (sorted(subcmds.items()) if subcmd is None else
|
|
783
|
+
((subcmd, subcmds[subcmd]),)):
|
|
784
|
+
with Pfx(attr):
|
|
785
|
+
subusage = subcmd_spec.usage_text(
|
|
786
|
+
short=short, usage_format_mapping=usage_format_mapping
|
|
787
|
+
)
|
|
788
|
+
cls.subcommand_usage_text(
|
|
789
|
+
attr, usage_format_mapping=usage_format_mapping, short=short
|
|
790
|
+
)
|
|
791
|
+
if subusage:
|
|
792
|
+
subusages.append(subusage.replace('\n', '\n '))
|
|
793
|
+
if subusages:
|
|
794
|
+
subcmds_header = 'Subcommands' if subcmd is None else 'Subcommand'
|
|
795
|
+
if short:
|
|
796
|
+
subcmds_header += ' (short form, long form with "help", "-h" or "--help")'
|
|
797
|
+
usage_message = '\n'.join(
|
|
798
|
+
[
|
|
799
|
+
usage_message,
|
|
800
|
+
' ' + subcmds_header + ':',
|
|
801
|
+
] + [
|
|
802
|
+
' ' + subusage.replace('\n', '\n ')
|
|
803
|
+
for subusage in subusages
|
|
804
|
+
]
|
|
805
|
+
)
|
|
806
|
+
return usage_message
|
|
807
|
+
|
|
808
|
+
@classmethod
|
|
809
|
+
def subcommand_usage_text(
|
|
810
|
+
cls, subcmd, usage_format_mapping=None, short=False
|
|
811
|
+
):
|
|
812
|
+
''' Return the usage text for a subcommand.
|
|
813
|
+
|
|
814
|
+
Parameters:
|
|
815
|
+
* `subcmd`: the subcommand name
|
|
816
|
+
* `short`: just include the first line of the usage message,
|
|
817
|
+
intented for when there are many subcommands
|
|
818
|
+
'''
|
|
819
|
+
method = cls.subcommands()[subcmd].method
|
|
820
|
+
subusage = None
|
|
821
|
+
# support (method, get_suboptions)
|
|
822
|
+
try:
|
|
823
|
+
classy = issubclass(method, BaseCommand)
|
|
824
|
+
except TypeError:
|
|
825
|
+
classy = False
|
|
826
|
+
if classy:
|
|
827
|
+
# first paragraph of the class usage text
|
|
828
|
+
doc = method.usage_text(cmd=subcmd)
|
|
829
|
+
subusage_format, *_ = cutprefix(doc, 'Usage:').lstrip().split("\n\n", 1)
|
|
830
|
+
else:
|
|
831
|
+
# extract the usage from the object docstring
|
|
832
|
+
doc = obj_docstring(method)
|
|
833
|
+
if doc:
|
|
834
|
+
if 'Usage:' in doc:
|
|
835
|
+
# extract the Usage: paragraph
|
|
836
|
+
pre_usage, post_usage = doc.split('Usage:', 1)
|
|
837
|
+
pre_usage = pre_usage.strip()
|
|
838
|
+
post_usage_format, *_ = post_usage.split('\n\n', 1)
|
|
839
|
+
subusage_format = stripped_dedent(post_usage_format)
|
|
840
|
+
else:
|
|
841
|
+
# extract the first paragraph
|
|
842
|
+
subusage_format, *_ = doc.split('\n\n', 1)
|
|
843
|
+
else:
|
|
844
|
+
# default usage text - include the docstring below a header
|
|
845
|
+
subusage_format = "\n ".join(
|
|
846
|
+
['{cmd} ...'] + [doc.split('\n\n', 1)[0]]
|
|
847
|
+
)
|
|
848
|
+
if subusage_format:
|
|
849
|
+
if short:
|
|
850
|
+
subusage_format, *_ = subusage_format.split('\n', 1)
|
|
851
|
+
mapping = dict(sys.modules[method.__module__].__dict__)
|
|
852
|
+
if usage_format_mapping:
|
|
853
|
+
mapping.update(usage_format_mapping)
|
|
854
|
+
mapping.update(cmd=subcmd)
|
|
855
|
+
subusage = subusage_format.format_map(mapping)
|
|
856
|
+
return subusage or None
|
|
857
|
+
|
|
858
|
+
@pfx_method
|
|
859
|
+
# pylint: disable=no-self-use
|
|
860
|
+
def apply_opt(self, opt, val):
|
|
861
|
+
''' Handle an individual global command line option.
|
|
862
|
+
|
|
863
|
+
This default implementation raises a `RuntimeError`.
|
|
864
|
+
It only fires if `getopt` actually gathered arguments
|
|
865
|
+
and would imply that a `GETOPT_SPEC` was supplied
|
|
866
|
+
without an `apply_opt` or `apply_opts` method to implement the options.
|
|
867
|
+
'''
|
|
868
|
+
raise RuntimeError("unhandled option %r" % (opt,))
|
|
869
|
+
|
|
870
|
+
def apply_opts(self, opts):
|
|
871
|
+
''' Apply command line options.
|
|
872
|
+
|
|
873
|
+
Subclasses can override this
|
|
874
|
+
but it is usually easier to override `apply_opt(opt,val)`.
|
|
875
|
+
'''
|
|
876
|
+
badopts = False
|
|
877
|
+
for opt, val in opts:
|
|
878
|
+
with Pfx(opt if val is None else "%s %r" % (opt, val)):
|
|
879
|
+
try:
|
|
880
|
+
self.apply_opt(opt, val)
|
|
881
|
+
except GetoptError as e:
|
|
882
|
+
warning("%s", e)
|
|
883
|
+
badopts = True
|
|
884
|
+
if badopts:
|
|
885
|
+
raise GetoptError("bad options")
|
|
886
|
+
|
|
887
|
+
# pylint: disable=no-self-use
|
|
888
|
+
def apply_preargv(self, argv):
|
|
889
|
+
''' Do any preparsing of `argv` before the subcommand/main-args.
|
|
890
|
+
Return the remaining arguments.
|
|
891
|
+
|
|
892
|
+
This default implementation returns `argv` unchanged.
|
|
893
|
+
'''
|
|
894
|
+
return argv
|
|
895
|
+
|
|
896
|
+
class _OptSpec(
|
|
897
|
+
namedtuple('_OptSpec',
|
|
898
|
+
'help_text, parse, validate, unvalidated_message'),
|
|
899
|
+
Promotable,
|
|
900
|
+
):
|
|
901
|
+
''' A class to support parsing an option value.
|
|
902
|
+
'''
|
|
903
|
+
|
|
904
|
+
@classmethod
|
|
905
|
+
def promote(cls, obj):
|
|
906
|
+
''' Construct an `_OptSpec` from a list of positional parameters
|
|
907
|
+
as for `poparg()`.
|
|
908
|
+
'''
|
|
909
|
+
if isinstance(obj, cls):
|
|
910
|
+
return obj
|
|
911
|
+
if isinstance(obj, str):
|
|
912
|
+
# the help text
|
|
913
|
+
specs = (obj,)
|
|
914
|
+
elif callable(obj):
|
|
915
|
+
# the factory
|
|
916
|
+
specs = (obj,)
|
|
917
|
+
else:
|
|
918
|
+
# some iterable
|
|
919
|
+
specs = obj
|
|
920
|
+
parse = None
|
|
921
|
+
help_text = None
|
|
922
|
+
validate = None
|
|
923
|
+
unvalidated_message = None
|
|
924
|
+
for spec in specs:
|
|
925
|
+
with Pfx("%r", spec):
|
|
926
|
+
if help_text is None and isinstance(spec, str):
|
|
927
|
+
help_text = spec
|
|
928
|
+
elif unvalidated_message is None and isinstance(spec, str):
|
|
929
|
+
unvalidated_message = spec
|
|
930
|
+
elif parse is None and callable(spec):
|
|
931
|
+
parse = spec
|
|
932
|
+
elif validate is None and callable(spec):
|
|
933
|
+
validate = spec
|
|
934
|
+
else:
|
|
935
|
+
raise TypeError(
|
|
936
|
+
"unexpected argument, expected help_text or parse,"
|
|
937
|
+
" then optional validate and optional invalid message,"
|
|
938
|
+
" received %s" % (r(spec),)
|
|
939
|
+
)
|
|
940
|
+
if help_text is None:
|
|
941
|
+
help_text = (
|
|
942
|
+
"string value" if parse is None else "value for %s" % (parse,)
|
|
943
|
+
)
|
|
944
|
+
if parse is None:
|
|
945
|
+
# pass option value through unchanged
|
|
946
|
+
parse = lambda val: val # pylint: disable=unnecessary-lambda-assignment
|
|
947
|
+
if unvalidated_message is None:
|
|
948
|
+
unvalidated_message = "invalid value"
|
|
949
|
+
return cls(
|
|
950
|
+
help_text=help_text,
|
|
951
|
+
parse=parse,
|
|
952
|
+
validate=validate,
|
|
953
|
+
unvalidated_message=unvalidated_message,
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
def parse_value(self, value):
|
|
957
|
+
''' Parse `value` according to the spec.
|
|
958
|
+
Raises a `GetoptError` for invalid values.
|
|
959
|
+
'''
|
|
960
|
+
with Pfx("%s %r", self.help_text, value):
|
|
961
|
+
try:
|
|
962
|
+
value = pfx_call(self.parse, value)
|
|
963
|
+
if self.validate is not None:
|
|
964
|
+
if not pfx_call(self.validate, value):
|
|
965
|
+
raise ValueError(self.unvalidated_message)
|
|
966
|
+
except ValueError as e:
|
|
967
|
+
raise GetoptError(str(e)) # pylint: disable=raise-missing-from
|
|
968
|
+
return value
|
|
969
|
+
|
|
970
|
+
@classmethod
|
|
971
|
+
def poparg(cls, argv: List[str], *a, unpop_on_error=False):
|
|
972
|
+
''' Pop the leading argument off `argv` and parse it.
|
|
973
|
+
Return the parsed argument.
|
|
974
|
+
Raises `getopt.GetoptError` on a missing or invalid argument.
|
|
975
|
+
|
|
976
|
+
This is expected to be used inside a `main` or `cmd_*`
|
|
977
|
+
command handler method or inside `apply_preargv`.
|
|
978
|
+
|
|
979
|
+
You can just use:
|
|
980
|
+
|
|
981
|
+
value = argv.pop(0)
|
|
982
|
+
|
|
983
|
+
but this method provides conversion and valuation
|
|
984
|
+
and a richer failure mode.
|
|
985
|
+
|
|
986
|
+
Parameters:
|
|
987
|
+
* `argv`: the argument list, which is modified in place with `argv.pop(0)`
|
|
988
|
+
* the argument list `argv` may be followed by some help text
|
|
989
|
+
and/or an argument parser function.
|
|
990
|
+
* `validate`: an optional function to validate the parsed value;
|
|
991
|
+
this should return a true value if valid,
|
|
992
|
+
or return a false value or raise a `ValueError` if invalid
|
|
993
|
+
* `unvalidated_message`: an optional message after `validate`
|
|
994
|
+
for values failing the validation
|
|
995
|
+
* `unpop_on_error`: optional keyword parameter, default `False`;
|
|
996
|
+
if true then push the argument back onto the front of `argv`
|
|
997
|
+
if it fails to parse; `GetoptError` is still raised
|
|
998
|
+
|
|
999
|
+
Typical use inside a `main` or `cmd_*` method might look like:
|
|
1000
|
+
|
|
1001
|
+
self.options.word = self.poparg(argv, int, "a count value")
|
|
1002
|
+
self.options.word = self.poparg(
|
|
1003
|
+
argv, int, "a count value",
|
|
1004
|
+
lambda count: count > 0, "count should be positive")
|
|
1005
|
+
|
|
1006
|
+
Because it raises `GetoptError` on a bad argument
|
|
1007
|
+
the normal usage message failure mode follows automatically.
|
|
1008
|
+
|
|
1009
|
+
Demonstration:
|
|
1010
|
+
|
|
1011
|
+
>>> argv = ['word', '3', 'nine', '4']
|
|
1012
|
+
>>> BaseCommand.poparg(argv, "word to process")
|
|
1013
|
+
'word'
|
|
1014
|
+
>>> BaseCommand.poparg(argv, int, "count value")
|
|
1015
|
+
3
|
|
1016
|
+
>>> BaseCommand.poparg(argv, float, "length")
|
|
1017
|
+
Traceback (most recent call last):
|
|
1018
|
+
...
|
|
1019
|
+
getopt.GetoptError: length 'nine': float('nine'): could not convert string to float: 'nine'
|
|
1020
|
+
>>> BaseCommand.poparg(argv, float, "width", lambda width: width > 5)
|
|
1021
|
+
Traceback (most recent call last):
|
|
1022
|
+
...
|
|
1023
|
+
getopt.GetoptError: width '4': invalid value
|
|
1024
|
+
>>> BaseCommand.poparg(argv, float, "length")
|
|
1025
|
+
Traceback (most recent call last):
|
|
1026
|
+
...
|
|
1027
|
+
getopt.GetoptError: length: missing argument
|
|
1028
|
+
>>> argv = ['-5', 'zz']
|
|
1029
|
+
>>> BaseCommand.poparg(argv, float, "size", lambda f: f>0, "size should be >0")
|
|
1030
|
+
Traceback (most recent call last):
|
|
1031
|
+
...
|
|
1032
|
+
getopt.GetoptError: size '-5': size should be >0
|
|
1033
|
+
>>> argv # -5 was still consumed
|
|
1034
|
+
['zz']
|
|
1035
|
+
>>> BaseCommand.poparg(argv, float, "size2", unpop_on_error=True)
|
|
1036
|
+
Traceback (most recent call last):
|
|
1037
|
+
...
|
|
1038
|
+
getopt.GetoptError: size2 'zz': float('zz'): could not convert string to float: 'zz'
|
|
1039
|
+
>>> argv # zz was pushed back
|
|
1040
|
+
['zz']
|
|
1041
|
+
'''
|
|
1042
|
+
opt_spec = cls._OptSpec.promote(a)
|
|
1043
|
+
with Pfx(opt_spec.help_text):
|
|
1044
|
+
if not argv:
|
|
1045
|
+
raise GetoptError("missing argument")
|
|
1046
|
+
arg0 = argv.pop(0)
|
|
1047
|
+
try:
|
|
1048
|
+
return opt_spec.parse_value(arg0)
|
|
1049
|
+
except GetoptError:
|
|
1050
|
+
if unpop_on_error:
|
|
1051
|
+
argv.insert(0, arg0)
|
|
1052
|
+
raise
|
|
1053
|
+
|
|
1054
|
+
@classmethod
|
|
1055
|
+
def popopts(
|
|
1056
|
+
cls,
|
|
1057
|
+
argv,
|
|
1058
|
+
attrfor=None,
|
|
1059
|
+
**opt_specs,
|
|
1060
|
+
):
|
|
1061
|
+
''' Parse option switches from `argv`, a list of command line strings
|
|
1062
|
+
with leading option switches.
|
|
1063
|
+
Modify `argv` in place and return a dict mapping switch names to values.
|
|
1064
|
+
|
|
1065
|
+
The optional positional argument `attrfor`
|
|
1066
|
+
may supply an object whose attributes may be set by the options,
|
|
1067
|
+
for example:
|
|
1068
|
+
|
|
1069
|
+
def cmd_foo(self, argv):
|
|
1070
|
+
self.popopts(argv, self.options, a='all', j_=('jobs', int))
|
|
1071
|
+
... use self.options.jobs etc ...
|
|
1072
|
+
|
|
1073
|
+
The expected options are specified by the keyword parameters
|
|
1074
|
+
in `opt_specs`:
|
|
1075
|
+
* options not starting with a letter may be preceeded by an underscore
|
|
1076
|
+
to allow use in the parameter list, for example `_1='once'`
|
|
1077
|
+
for a `-1` option setting the `once` option name
|
|
1078
|
+
* a single letter name specifies a short option
|
|
1079
|
+
and a multiletter name specifies a long option
|
|
1080
|
+
* options requiring an argument have a trailing underscore
|
|
1081
|
+
* options not requiring an argument normally imply a value
|
|
1082
|
+
of `True`; if their synonym commences with a dash they will
|
|
1083
|
+
imply a value of `False`, for example `n='dry_run',y='-dry_run'`
|
|
1084
|
+
|
|
1085
|
+
As it happens, the `BaseCommandOptions` class provided a `popopts` method
|
|
1086
|
+
which is a shim for this method with `attrfor=self` i.e. the options object.
|
|
1087
|
+
So common use in a command method might look like this:
|
|
1088
|
+
|
|
1089
|
+
class SomeCommand(BaseCommand):
|
|
1090
|
+
|
|
1091
|
+
def cmd_foo(self, argv):
|
|
1092
|
+
options = self.options
|
|
1093
|
+
# accept a -j or --jobs options
|
|
1094
|
+
options.poopts(argv, jobs=1, j='jobs')
|
|
1095
|
+
print("jobs =", options.jobs)
|
|
1096
|
+
|
|
1097
|
+
Example:
|
|
1098
|
+
|
|
1099
|
+
>>> import os.path
|
|
1100
|
+
>>> options = SimpleNamespace(
|
|
1101
|
+
... all=False,
|
|
1102
|
+
... jobs=1,
|
|
1103
|
+
... number=0,
|
|
1104
|
+
... once=False,
|
|
1105
|
+
... path=None,
|
|
1106
|
+
... trace_exec=True,
|
|
1107
|
+
... verbose=False,
|
|
1108
|
+
... dry_run=False)
|
|
1109
|
+
>>> argv = ['-1', '-v', '-y', '-j4', '--path=/foo', 'bah', '-x']
|
|
1110
|
+
>>> opt_dict = BaseCommand.popopts(
|
|
1111
|
+
... argv,
|
|
1112
|
+
... options,
|
|
1113
|
+
... _1='once',
|
|
1114
|
+
... a='all',
|
|
1115
|
+
... j_=('jobs',int),
|
|
1116
|
+
... n='dry_run',
|
|
1117
|
+
... v='verbose',
|
|
1118
|
+
... x='-trace_exec',
|
|
1119
|
+
... y='-dry_run',
|
|
1120
|
+
... dry_run=None,
|
|
1121
|
+
... path_=(str, os.path.isabs, 'not an absolute path'),
|
|
1122
|
+
... verbose=None,
|
|
1123
|
+
... )
|
|
1124
|
+
>>> opt_dict
|
|
1125
|
+
{'once': True, 'verbose': True, 'dry_run': False, 'jobs': 4, 'path': '/foo'}
|
|
1126
|
+
>>> options
|
|
1127
|
+
namespace(all=False, jobs=4, number=0, once=True, path='/foo', trace_exec=True, verbose=True, dry_run=False)
|
|
1128
|
+
'''
|
|
1129
|
+
keyfor = {}
|
|
1130
|
+
shortopts = ''
|
|
1131
|
+
longopts = []
|
|
1132
|
+
opt_spec_map = {}
|
|
1133
|
+
opt_name_map = {}
|
|
1134
|
+
for opt_name, opt_spec in opt_specs.items():
|
|
1135
|
+
with Pfx("opt_spec[%r]=%r", opt_name, opt_spec):
|
|
1136
|
+
needs_arg = False
|
|
1137
|
+
if opt_name.startswith('_'):
|
|
1138
|
+
opt_name = opt_name[1:]
|
|
1139
|
+
if is_identifier(opt_name):
|
|
1140
|
+
warning(
|
|
1141
|
+
"unnecessary leading underscore on valid identifier option"
|
|
1142
|
+
)
|
|
1143
|
+
if opt_name.endswith('_'):
|
|
1144
|
+
needs_arg = True
|
|
1145
|
+
opt_name = opt_name[:-1]
|
|
1146
|
+
if len(opt_name) == 1:
|
|
1147
|
+
opt = '-' + opt_name
|
|
1148
|
+
shortopts += opt_name
|
|
1149
|
+
if needs_arg:
|
|
1150
|
+
shortopts += ':'
|
|
1151
|
+
elif len(opt_name) > 1:
|
|
1152
|
+
opt_dashed = opt_name.replace('_', '-')
|
|
1153
|
+
opt = '--' + opt_dashed
|
|
1154
|
+
longopts.append(opt_dashed + '=' if needs_arg else opt_dashed)
|
|
1155
|
+
default_help_text = opt
|
|
1156
|
+
else:
|
|
1157
|
+
raise ValueError("unexpected opt_name %s" % (r(opt_name),))
|
|
1158
|
+
if opt_spec is None:
|
|
1159
|
+
specs = [opt_name, str]
|
|
1160
|
+
elif isinstance(opt_spec, (list, tuple)):
|
|
1161
|
+
specs = list(opt_spec)
|
|
1162
|
+
else:
|
|
1163
|
+
specs = [opt_spec]
|
|
1164
|
+
if specs:
|
|
1165
|
+
spec0 = specs[0]
|
|
1166
|
+
if isinstance(spec0, str) and (is_identifier(spec0) or
|
|
1167
|
+
(spec0.startswith('-')
|
|
1168
|
+
and is_identifier(spec0[1:]))):
|
|
1169
|
+
opt_name = specs[0]
|
|
1170
|
+
if len(specs) > 1 and isinstance(specs[1], str):
|
|
1171
|
+
specs.pop(0)
|
|
1172
|
+
if not specs or not isinstance(specs[0], str):
|
|
1173
|
+
specs.insert(0, default_help_text)
|
|
1174
|
+
if needs_arg:
|
|
1175
|
+
opt_spec = cls._OptSpec.promote(specs)
|
|
1176
|
+
opt_spec_map[opt] = opt_spec
|
|
1177
|
+
opt_name_map[opt] = opt_name
|
|
1178
|
+
opts, post_argv = getopt(argv, shortopts, longopts)
|
|
1179
|
+
argv[:] = post_argv
|
|
1180
|
+
for opt, val in opts:
|
|
1181
|
+
with Pfx(opt):
|
|
1182
|
+
opt_name = opt_name_map[opt]
|
|
1183
|
+
try:
|
|
1184
|
+
opt_spec = opt_spec_map[opt]
|
|
1185
|
+
except KeyError:
|
|
1186
|
+
# option expected no arguments
|
|
1187
|
+
assert val == ''
|
|
1188
|
+
if opt_name.startswith('-'):
|
|
1189
|
+
value = False
|
|
1190
|
+
opt_name = opt_name[1:]
|
|
1191
|
+
else:
|
|
1192
|
+
value = True
|
|
1193
|
+
else:
|
|
1194
|
+
value = opt_spec.parse_value(val)
|
|
1195
|
+
keyfor[opt_name] = value
|
|
1196
|
+
if attrfor is not None:
|
|
1197
|
+
setattr(attrfor, opt_name, value)
|
|
1198
|
+
return keyfor
|
|
1199
|
+
|
|
1200
|
+
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
|
|
1201
|
+
def run(self, **kw_options):
|
|
1202
|
+
''' Run a command.
|
|
1203
|
+
Returns the exit status of the command.
|
|
1204
|
+
May raise `GetoptError` from subcommands.
|
|
1205
|
+
|
|
1206
|
+
Any keyword arguments are used to override `self.options` attributes
|
|
1207
|
+
for the duration of the run,
|
|
1208
|
+
for example to presupply a shared `Upd` from an outer context.
|
|
1209
|
+
|
|
1210
|
+
If the first command line argument *foo*
|
|
1211
|
+
has a corresponding method `cmd_`*foo*
|
|
1212
|
+
then that argument is removed from the start of `argv`
|
|
1213
|
+
and `self.cmd_`*foo*`(cmd=`*foo*`)` is called
|
|
1214
|
+
and its value returned.
|
|
1215
|
+
Otherwise `self.main(argv)` is called
|
|
1216
|
+
and its value returned.
|
|
1217
|
+
|
|
1218
|
+
If the command implementation requires some setup or teardown
|
|
1219
|
+
then this may be provided by the `run_context()`
|
|
1220
|
+
context manager method.
|
|
1221
|
+
'''
|
|
1222
|
+
# short circuit if we've already complainted about bad invocation
|
|
1223
|
+
if self._printed_usage:
|
|
1224
|
+
return 2
|
|
1225
|
+
options = self.options
|
|
1226
|
+
try:
|
|
1227
|
+
with self.run_context(**kw_options):
|
|
1228
|
+
try:
|
|
1229
|
+
return self._run(self._subcmd, self, self._argv)
|
|
1230
|
+
except CancellationError:
|
|
1231
|
+
error("cancelled")
|
|
1232
|
+
return 1
|
|
1233
|
+
except GetoptError as e:
|
|
1234
|
+
if self.getopt_error_handler(
|
|
1235
|
+
self.cmd,
|
|
1236
|
+
options,
|
|
1237
|
+
e,
|
|
1238
|
+
(None if self._printed_usage else self.usage_text(
|
|
1239
|
+
cmd=self.cmd, subcmd=self._subcmd)),
|
|
1240
|
+
subcmd=self._subcmd,
|
|
1241
|
+
):
|
|
1242
|
+
self._printed_usage = True
|
|
1243
|
+
return 2
|
|
1244
|
+
raise
|
|
1245
|
+
|
|
1246
|
+
@classmethod
|
|
1247
|
+
def cmdloop(cls, intro=None):
|
|
1248
|
+
''' Use `cmd.Cmd` to run a command loop which calls the `cmd_`* methods.
|
|
1249
|
+
'''
|
|
1250
|
+
# TODO: get intro from usage/help
|
|
1251
|
+
cmdobj = BaseCommandCmd(cls)
|
|
1252
|
+
cmdobj.cmdloop(intro)
|
|
1253
|
+
|
|
1254
|
+
# pylint: disable=unused-argument
|
|
1255
|
+
@staticmethod
|
|
1256
|
+
def getopt_error_handler(cmd, options, e, usage, subcmd=None): # pylint: disable=unused-argument
|
|
1257
|
+
''' The `getopt_error_handler` method
|
|
1258
|
+
is used to control the handling of `GetoptError`s raised
|
|
1259
|
+
during the command line parse
|
|
1260
|
+
or during the `main` or `cmd_`*subcmd*` calls.
|
|
1261
|
+
|
|
1262
|
+
This default handler issues a warning containing the exception text,
|
|
1263
|
+
prints the usage message to standard error,
|
|
1264
|
+
and returns `True` to indicate that the error has been handled.
|
|
1265
|
+
|
|
1266
|
+
The handler is called with these parameters:
|
|
1267
|
+
* `cmd`: the command name
|
|
1268
|
+
* `options`: the `options` object
|
|
1269
|
+
* `e`: the `GetoptError` exception
|
|
1270
|
+
* `usage`: the command usage or `None` if this was not provided
|
|
1271
|
+
* `subcmd`: optional subcommand name;
|
|
1272
|
+
if not `None`, is the name of the subcommand which caused the error
|
|
1273
|
+
|
|
1274
|
+
It returns a true value if the exception is considered handled,
|
|
1275
|
+
in which case the main `run` method returns 2.
|
|
1276
|
+
It returns a false value if the exception is considered unhandled,
|
|
1277
|
+
in which case the main `run` method reraises the `GetoptError`.
|
|
1278
|
+
|
|
1279
|
+
To let the exceptions out unhandled
|
|
1280
|
+
this can be overridden with a method which just returns `False`.
|
|
1281
|
+
|
|
1282
|
+
Otherwise,
|
|
1283
|
+
the handler may perform any suitable action
|
|
1284
|
+
and return `True` to contain the exception
|
|
1285
|
+
or `False` to cause the exception to be reraised.
|
|
1286
|
+
'''
|
|
1287
|
+
warning("%s", e)
|
|
1288
|
+
if usage:
|
|
1289
|
+
print(usage.rstrip(), file=sys.stderr)
|
|
1290
|
+
return True
|
|
1291
|
+
|
|
1292
|
+
@uses_runstate
|
|
1293
|
+
def handle_signal(self, sig, frame, *, runstate: RunState):
|
|
1294
|
+
''' The default signal handler, which cancels the default `RunState`.
|
|
1295
|
+
'''
|
|
1296
|
+
runstate.cancel()
|
|
1297
|
+
|
|
1298
|
+
@contextmanager
|
|
1299
|
+
@uses_runstate
|
|
1300
|
+
@uses_upd
|
|
1301
|
+
def run_context(self, *, runstate: RunState, upd: Upd, **kw_options):
|
|
1302
|
+
''' The context manager which surrounds `main` or `cmd_`*subcmd*.
|
|
1303
|
+
|
|
1304
|
+
This default does several things, and subclasses should
|
|
1305
|
+
override it like this:
|
|
1306
|
+
|
|
1307
|
+
@contextmanager
|
|
1308
|
+
def run_context(self):
|
|
1309
|
+
with super().run_context():
|
|
1310
|
+
try:
|
|
1311
|
+
... subclass context setup ...
|
|
1312
|
+
yield
|
|
1313
|
+
finally:
|
|
1314
|
+
... any unconditional cleanup ...
|
|
1315
|
+
'''
|
|
1316
|
+
# redundant try/finally to remind subclassers of correct structure
|
|
1317
|
+
try:
|
|
1318
|
+
run_options = self.options.copy(**kw_options)
|
|
1319
|
+
with run_options: # make the default ThreadState
|
|
1320
|
+
with stackattrs(self, options=run_options):
|
|
1321
|
+
with stackattrs(self, cmd=self._subcmd or self.cmd):
|
|
1322
|
+
with upd:
|
|
1323
|
+
with runstate:
|
|
1324
|
+
with runstate.catch_signal(
|
|
1325
|
+
run_options.runstate_signals,
|
|
1326
|
+
call_previous=False,
|
|
1327
|
+
handle_signal=self.handle_signal,
|
|
1328
|
+
):
|
|
1329
|
+
yield
|
|
1330
|
+
|
|
1331
|
+
finally:
|
|
1332
|
+
pass
|
|
1333
|
+
|
|
1334
|
+
# pylint: disable=unused-argument
|
|
1335
|
+
@classmethod
|
|
1336
|
+
def cmd_help(cls, argv):
|
|
1337
|
+
''' Usage: {cmd} [-l] [subcommand-names...]
|
|
1338
|
+
Print help for subcommands.
|
|
1339
|
+
This outputs the full help for the named subcommands,
|
|
1340
|
+
or the short help for all subcommands if no names are specified.
|
|
1341
|
+
-l Long help even if no subcommand-names provided.
|
|
1342
|
+
'''
|
|
1343
|
+
subcmds = cls.subcommands()
|
|
1344
|
+
if argv and argv[0] == '-l':
|
|
1345
|
+
argv.pop(0)
|
|
1346
|
+
short = False
|
|
1347
|
+
elif argv:
|
|
1348
|
+
short = False
|
|
1349
|
+
else:
|
|
1350
|
+
short = True
|
|
1351
|
+
argv = argv or sorted(subcmds)
|
|
1352
|
+
xit = 0
|
|
1353
|
+
print("help:")
|
|
1354
|
+
unknown = False
|
|
1355
|
+
for subcmd in argv:
|
|
1356
|
+
with Pfx(subcmd):
|
|
1357
|
+
subcmd_ = subcmd.replace('-', '_').replace('.', '_')
|
|
1358
|
+
try:
|
|
1359
|
+
subcommand = subcmds[subcmd_]
|
|
1360
|
+
except KeyError:
|
|
1361
|
+
warning("unknown subcommand")
|
|
1362
|
+
unknown = True
|
|
1363
|
+
xit = 1
|
|
1364
|
+
continue
|
|
1365
|
+
subusage = subcommand.usage_text(short)
|
|
1366
|
+
if not subusage:
|
|
1367
|
+
warning("no help")
|
|
1368
|
+
xit = 1
|
|
1369
|
+
continue
|
|
1370
|
+
print(' ', subusage.replace('\n', '\n '))
|
|
1371
|
+
if unknown:
|
|
1372
|
+
warning("I know: %s", ', '.join(sorted(subcmds.keys())))
|
|
1373
|
+
return xit
|
|
1374
|
+
|
|
1375
|
+
def cmd_shell(self, argv):
|
|
1376
|
+
''' Usage: {cmd}
|
|
1377
|
+
Run a command prompt via cmd.Cmd using this command's subcommands.
|
|
1378
|
+
'''
|
|
1379
|
+
self.cmdloop()
|
|
1380
|
+
|
|
1381
|
+
def repl(self, *argv, banner=None, local=None):
|
|
1382
|
+
''' Run an interactive Python prompt with some predefined local names.
|
|
1383
|
+
Aka REPL (Read Evaluate Print Loop).
|
|
1384
|
+
|
|
1385
|
+
Parameters:
|
|
1386
|
+
* `argv`: any notional command line arguments
|
|
1387
|
+
* `banner`: optional banner string
|
|
1388
|
+
* `local`: optional local names mapping
|
|
1389
|
+
|
|
1390
|
+
The default `local` mapping is a `dict` containing:
|
|
1391
|
+
* `argv`: from `argv`
|
|
1392
|
+
* `options`: from `self.options`
|
|
1393
|
+
* `self`: from `self`
|
|
1394
|
+
* the attributes of `options`
|
|
1395
|
+
* the attributes of `self`
|
|
1396
|
+
|
|
1397
|
+
This is not presented automatically as a subcommand, but
|
|
1398
|
+
commands wishing such a command should provide something
|
|
1399
|
+
like this:
|
|
1400
|
+
|
|
1401
|
+
def cmd_repl(self, argv):
|
|
1402
|
+
""" Usage: {cmd}
|
|
1403
|
+
Run an interactive Python prompt with some predefined local names.
|
|
1404
|
+
"""
|
|
1405
|
+
return self.repl(*argv)
|
|
1406
|
+
'''
|
|
1407
|
+
options = self.options
|
|
1408
|
+
if banner is None:
|
|
1409
|
+
banner = self.cmd
|
|
1410
|
+
try:
|
|
1411
|
+
sqltags = options.sqltags
|
|
1412
|
+
except AttributeError:
|
|
1413
|
+
pass
|
|
1414
|
+
else:
|
|
1415
|
+
banner += f': {sqltags}'
|
|
1416
|
+
if local is None:
|
|
1417
|
+
local = dict(self.__dict__)
|
|
1418
|
+
local.update(options.__dict__)
|
|
1419
|
+
local.update(argv=argv, cmd=self.cmd, options=options, self=self)
|
|
1420
|
+
try:
|
|
1421
|
+
# pylint: disable=import-outside-toplevel
|
|
1422
|
+
from bpython import embed
|
|
1423
|
+
except ImportError:
|
|
1424
|
+
return interact(
|
|
1425
|
+
banner=banner,
|
|
1426
|
+
local=local,
|
|
1427
|
+
)
|
|
1428
|
+
return embed(
|
|
1429
|
+
banner=banner,
|
|
1430
|
+
locals_=local,
|
|
1431
|
+
)
|
|
1432
|
+
|
|
1433
|
+
BaseCommandSubType = subtype(BaseCommand)
|
|
1434
|
+
|
|
1435
|
+
class BaseCommandCmd(Cmd):
|
|
1436
|
+
''' A `cmd.Cmd` subclass used to provide interactive use of a
|
|
1437
|
+
command's subcommands.
|
|
1438
|
+
|
|
1439
|
+
The `BaseCommand.cmdloop()` class method instantiates an
|
|
1440
|
+
instance of this cand calls its `.cmdloop()` method
|
|
1441
|
+
i.e. `cmd.Cmd.cmdloop`.
|
|
1442
|
+
'''
|
|
1443
|
+
|
|
1444
|
+
def __init__(self, command_class: BaseCommandSubType):
|
|
1445
|
+
super().__init__()
|
|
1446
|
+
self.command_class = command_class
|
|
1447
|
+
|
|
1448
|
+
@typechecked
|
|
1449
|
+
def _doarg(self, subcmd: str, arg: str):
|
|
1450
|
+
cls = self.command_class
|
|
1451
|
+
argv = shlex.split(arg)
|
|
1452
|
+
command = cls([cls.__name__, subcmd] + argv)
|
|
1453
|
+
with stackattrs(command, _subcmd=subcmd):
|
|
1454
|
+
command.run()
|
|
1455
|
+
|
|
1456
|
+
def get_names(self):
|
|
1457
|
+
cls = self.command_class
|
|
1458
|
+
names = []
|
|
1459
|
+
for method_name in dir(cls):
|
|
1460
|
+
if method_name.startswith(cls.SUBCOMMAND_METHOD_PREFIX):
|
|
1461
|
+
subcmd = cutprefix(method_name, cls.SUBCOMMAND_METHOD_PREFIX)
|
|
1462
|
+
names.append('do_' + subcmd)
|
|
1463
|
+
##names.append('help_' + subcmd)
|
|
1464
|
+
return names
|
|
1465
|
+
|
|
1466
|
+
def __getattr__(self, attr):
|
|
1467
|
+
cls = self.command_class
|
|
1468
|
+
subcmd = cutprefix(attr, 'do_')
|
|
1469
|
+
if subcmd is not attr:
|
|
1470
|
+
method_name = cls.SUBCOMMAND_METHOD_PREFIX + subcmd
|
|
1471
|
+
try:
|
|
1472
|
+
method = getattr(cls, method_name)
|
|
1473
|
+
except AttributeError:
|
|
1474
|
+
pass
|
|
1475
|
+
else:
|
|
1476
|
+
do_subcmd = lambda arg: self._doarg(subcmd, arg)
|
|
1477
|
+
do_subcmd.__name__ = attr
|
|
1478
|
+
do_subcmd.__doc__ = method.__doc__.format(cmd=subcmd)
|
|
1479
|
+
return do_subcmd
|
|
1480
|
+
if subcmd in ('EOF', 'exit', 'quit'):
|
|
1481
|
+
return lambda _: True
|
|
1482
|
+
raise AttributeError("%s.%s" % (self.__class__.__name__, attr))
|