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))