cmd2 2.5.11__py3-none-any.whl → 2.6.1__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.
- cmd2/__init__.py +11 -22
- cmd2/ansi.py +78 -91
- cmd2/argparse_completer.py +109 -132
- cmd2/argparse_custom.py +199 -217
- cmd2/clipboard.py +2 -6
- cmd2/cmd2.py +483 -551
- cmd2/command_definition.py +34 -44
- cmd2/constants.py +1 -3
- cmd2/decorators.py +47 -58
- cmd2/exceptions.py +23 -46
- cmd2/history.py +29 -43
- cmd2/parsing.py +59 -84
- cmd2/plugin.py +6 -10
- cmd2/py_bridge.py +20 -26
- cmd2/rl_utils.py +45 -69
- cmd2/table_creator.py +83 -106
- cmd2/transcript.py +55 -59
- cmd2/utils.py +173 -199
- {cmd2-2.5.11.dist-info → cmd2-2.6.1.dist-info}/METADATA +34 -17
- cmd2-2.6.1.dist-info/RECORD +24 -0
- {cmd2-2.5.11.dist-info → cmd2-2.6.1.dist-info}/WHEEL +1 -1
- cmd2-2.5.11.dist-info/RECORD +0 -24
- {cmd2-2.5.11.dist-info → cmd2-2.6.1.dist-info/licenses}/LICENSE +0 -0
- {cmd2-2.5.11.dist-info → cmd2-2.6.1.dist-info}/top_level.txt +0 -0
cmd2/cmd2.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# coding=utf-8
|
2
1
|
"""Variant on standard library's cmd with extra features.
|
3
2
|
|
4
3
|
To use, simply import cmd2.Cmd instead of cmd.Cmd; use precisely as though you
|
@@ -31,11 +30,13 @@ Git repository on GitHub at https://github.com/python-cmd2/cmd2
|
|
31
30
|
# setting is True
|
32
31
|
import argparse
|
33
32
|
import cmd
|
33
|
+
import contextlib
|
34
34
|
import copy
|
35
35
|
import functools
|
36
36
|
import glob
|
37
37
|
import inspect
|
38
38
|
import os
|
39
|
+
import pprint
|
39
40
|
import pydoc
|
40
41
|
import re
|
41
42
|
import sys
|
@@ -48,9 +49,7 @@ from collections import (
|
|
48
49
|
OrderedDict,
|
49
50
|
namedtuple,
|
50
51
|
)
|
51
|
-
from
|
52
|
-
redirect_stdout,
|
53
|
-
)
|
52
|
+
from collections.abc import Callable, Iterable, Mapping
|
54
53
|
from types import (
|
55
54
|
FrameType,
|
56
55
|
ModuleType,
|
@@ -59,16 +58,9 @@ from typing import (
|
|
59
58
|
IO,
|
60
59
|
TYPE_CHECKING,
|
61
60
|
Any,
|
62
|
-
|
63
|
-
Dict,
|
64
|
-
Iterable,
|
65
|
-
List,
|
66
|
-
Mapping,
|
61
|
+
ClassVar,
|
67
62
|
Optional,
|
68
|
-
Set,
|
69
63
|
TextIO,
|
70
|
-
Tuple,
|
71
|
-
Type,
|
72
64
|
TypeVar,
|
73
65
|
Union,
|
74
66
|
cast,
|
@@ -130,10 +122,8 @@ from .parsing import (
|
|
130
122
|
)
|
131
123
|
|
132
124
|
# NOTE: When using gnureadline with Python 3.13, start_ipython needs to be imported before any readline-related stuff
|
133
|
-
|
125
|
+
with contextlib.suppress(ImportError):
|
134
126
|
from IPython import start_ipython # type: ignore[import]
|
135
|
-
except ImportError:
|
136
|
-
pass
|
137
127
|
|
138
128
|
from .rl_utils import (
|
139
129
|
RlType,
|
@@ -154,6 +144,7 @@ from .table_creator import (
|
|
154
144
|
from .utils import (
|
155
145
|
Settable,
|
156
146
|
get_defining_class,
|
147
|
+
get_types,
|
157
148
|
strip_doc_annotations,
|
158
149
|
suggest_similar,
|
159
150
|
)
|
@@ -187,7 +178,7 @@ else:
|
|
187
178
|
|
188
179
|
|
189
180
|
class _SavedReadlineSettings:
|
190
|
-
"""readline settings that are backed up when switching between readline environments"""
|
181
|
+
"""readline settings that are backed up when switching between readline environments."""
|
191
182
|
|
192
183
|
def __init__(self) -> None:
|
193
184
|
self.completer = None
|
@@ -196,18 +187,18 @@ class _SavedReadlineSettings:
|
|
196
187
|
|
197
188
|
|
198
189
|
class _SavedCmd2Env:
|
199
|
-
"""cmd2 environment settings that are backed up when entering an interactive Python shell"""
|
190
|
+
"""cmd2 environment settings that are backed up when entering an interactive Python shell."""
|
200
191
|
|
201
192
|
def __init__(self) -> None:
|
202
193
|
self.readline_settings = _SavedReadlineSettings()
|
203
194
|
self.readline_module: Optional[ModuleType] = None
|
204
|
-
self.history:
|
195
|
+
self.history: list[str] = []
|
205
196
|
self.sys_stdout: Optional[TextIO] = None
|
206
197
|
self.sys_stdin: Optional[TextIO] = None
|
207
198
|
|
208
199
|
|
209
200
|
# Contains data about a disabled command which is used to restore its original functions when the command is enabled
|
210
|
-
DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function', 'completer_function'])
|
201
|
+
DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function', 'completer_function']) # noqa: PYI024
|
211
202
|
|
212
203
|
|
213
204
|
if TYPE_CHECKING: # pragma: no cover
|
@@ -219,8 +210,7 @@ else:
|
|
219
210
|
|
220
211
|
|
221
212
|
class _CommandParsers:
|
222
|
-
"""
|
223
|
-
Create and store all command method argument parsers for a given Cmd instance.
|
213
|
+
"""Create and store all command method argument parsers for a given Cmd instance.
|
224
214
|
|
225
215
|
Parser creation and retrieval are accomplished through the get() method.
|
226
216
|
"""
|
@@ -230,7 +220,7 @@ class _CommandParsers:
|
|
230
220
|
|
231
221
|
# Keyed by the fully qualified method names. This is more reliable than
|
232
222
|
# the methods themselves, since wrapping a method will change its address.
|
233
|
-
self._parsers:
|
223
|
+
self._parsers: dict[str, argparse.ArgumentParser] = {}
|
234
224
|
|
235
225
|
@staticmethod
|
236
226
|
def _fully_qualified_name(command_method: CommandFunc) -> str:
|
@@ -241,8 +231,7 @@ class _CommandParsers:
|
|
241
231
|
return ""
|
242
232
|
|
243
233
|
def __contains__(self, command_method: CommandFunc) -> bool:
|
244
|
-
"""
|
245
|
-
Return whether a given method's parser is in self.
|
234
|
+
"""Return whether a given method's parser is in self.
|
246
235
|
|
247
236
|
If the parser does not yet exist, it will be created if applicable.
|
248
237
|
This is basically for checking if a method is argarse-based.
|
@@ -251,8 +240,7 @@ class _CommandParsers:
|
|
251
240
|
return bool(parser)
|
252
241
|
|
253
242
|
def get(self, command_method: CommandFunc) -> Optional[argparse.ArgumentParser]:
|
254
|
-
"""
|
255
|
-
Return a given method's parser or None if the method is not argparse-based.
|
243
|
+
"""Return a given method's parser or None if the method is not argparse-based.
|
256
244
|
|
257
245
|
If the parser does not yet exist, it will be created.
|
258
246
|
"""
|
@@ -296,7 +284,7 @@ class _CommandParsers:
|
|
296
284
|
class Cmd(cmd.Cmd):
|
297
285
|
"""An easy but powerful framework for writing line-oriented command interpreters.
|
298
286
|
|
299
|
-
Extends the Python Standard Library
|
287
|
+
Extends the Python Standard Library's cmd package by adding a lot of useful features
|
300
288
|
to the out of the box configuration.
|
301
289
|
|
302
290
|
Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes.
|
@@ -312,6 +300,9 @@ class Cmd(cmd.Cmd):
|
|
312
300
|
ALPHABETICAL_SORT_KEY = utils.norm_fold
|
313
301
|
NATURAL_SORT_KEY = utils.natural_keys
|
314
302
|
|
303
|
+
# List for storing transcript test file names
|
304
|
+
testfiles: ClassVar[list[str]] = []
|
305
|
+
|
315
306
|
def __init__(
|
316
307
|
self,
|
317
308
|
completekey: str = 'tab',
|
@@ -325,18 +316,17 @@ class Cmd(cmd.Cmd):
|
|
325
316
|
include_py: bool = False,
|
326
317
|
include_ipy: bool = False,
|
327
318
|
allow_cli_args: bool = True,
|
328
|
-
transcript_files: Optional[
|
319
|
+
transcript_files: Optional[list[str]] = None,
|
329
320
|
allow_redirection: bool = True,
|
330
|
-
multiline_commands: Optional[
|
331
|
-
terminators: Optional[
|
332
|
-
shortcuts: Optional[
|
321
|
+
multiline_commands: Optional[list[str]] = None,
|
322
|
+
terminators: Optional[list[str]] = None,
|
323
|
+
shortcuts: Optional[dict[str, str]] = None,
|
333
324
|
command_sets: Optional[Iterable[CommandSet]] = None,
|
334
325
|
auto_load_commands: bool = True,
|
335
326
|
allow_clipboard: bool = True,
|
336
327
|
suggest_similar_command: bool = False,
|
337
328
|
) -> None:
|
338
|
-
"""
|
339
|
-
interpreters. Extends Python's cmd package.
|
329
|
+
"""Easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package.
|
340
330
|
|
341
331
|
:param completekey: readline name of a completion key, default to Tab
|
342
332
|
:param stdin: alternate input file object, if not specified, sys.stdin is used
|
@@ -388,9 +378,9 @@ class Cmd(cmd.Cmd):
|
|
388
378
|
"""
|
389
379
|
# Check if py or ipy need to be disabled in this instance
|
390
380
|
if not include_py:
|
391
|
-
setattr(self, 'do_py', None)
|
381
|
+
setattr(self, 'do_py', None) # noqa: B010
|
392
382
|
if not include_ipy:
|
393
|
-
setattr(self, 'do_ipy', None)
|
383
|
+
setattr(self, 'do_ipy', None) # noqa: B010
|
394
384
|
|
395
385
|
# initialize plugin system
|
396
386
|
# needs to be done before we call __init__(0)
|
@@ -416,20 +406,20 @@ class Cmd(cmd.Cmd):
|
|
416
406
|
# The maximum number of CompletionItems to display during tab completion. If the number of completion
|
417
407
|
# suggestions exceeds this number, they will be displayed in the typical columnized format and will
|
418
408
|
# not include the description value of the CompletionItems.
|
419
|
-
self.max_completion_items = 50
|
409
|
+
self.max_completion_items: int = 50
|
420
410
|
|
421
411
|
# A dictionary mapping settable names to their Settable instance
|
422
|
-
self._settables:
|
412
|
+
self._settables: dict[str, Settable] = {}
|
423
413
|
self._always_prefix_settables: bool = False
|
424
414
|
|
425
415
|
# CommandSet containers
|
426
|
-
self._installed_command_sets:
|
427
|
-
self._cmd_to_command_sets:
|
416
|
+
self._installed_command_sets: set[CommandSet] = set()
|
417
|
+
self._cmd_to_command_sets: dict[str, CommandSet] = {}
|
428
418
|
|
429
419
|
self.build_settables()
|
430
420
|
|
431
421
|
# Use as prompt for multiline commands on the 2nd+ line of input
|
432
|
-
self.continuation_prompt = '> '
|
422
|
+
self.continuation_prompt: str = '> '
|
433
423
|
|
434
424
|
# Allow access to your application in embedded Python shells and scripts py via self
|
435
425
|
self.self_in_py = False
|
@@ -446,21 +436,21 @@ class Cmd(cmd.Cmd):
|
|
446
436
|
self.exclude_from_history = ['eof', 'history']
|
447
437
|
|
448
438
|
# Dictionary of macro names and their values
|
449
|
-
self.macros:
|
439
|
+
self.macros: dict[str, Macro] = {}
|
450
440
|
|
451
441
|
# Keeps track of typed command history in the Python shell
|
452
|
-
self._py_history:
|
442
|
+
self._py_history: list[str] = []
|
453
443
|
|
454
444
|
# The name by which Python environments refer to the PyBridge to call app commands
|
455
445
|
self.py_bridge_name = 'app'
|
456
446
|
|
457
447
|
# Defines app-specific variables/functions available in Python shells and pyscripts
|
458
|
-
self.py_locals:
|
448
|
+
self.py_locals: dict[str, Any] = {}
|
459
449
|
|
460
450
|
# True if running inside a Python shell or pyscript, False otherwise
|
461
451
|
self._in_py = False
|
462
452
|
|
463
|
-
self.statement_parser = StatementParser(
|
453
|
+
self.statement_parser: StatementParser = StatementParser(
|
464
454
|
terminators=terminators, multiline_commands=multiline_commands, shortcuts=shortcuts
|
465
455
|
)
|
466
456
|
|
@@ -468,10 +458,10 @@ class Cmd(cmd.Cmd):
|
|
468
458
|
self.last_result: Any = None
|
469
459
|
|
470
460
|
# Used by run_script command to store current script dir as a LIFO queue to support _relative_run_script command
|
471
|
-
self._script_dir:
|
461
|
+
self._script_dir: list[str] = []
|
472
462
|
|
473
463
|
# Context manager used to protect critical sections in the main thread from stopping due to a KeyboardInterrupt
|
474
|
-
self.sigint_protection = utils.ContextFlag()
|
464
|
+
self.sigint_protection: utils.ContextFlag = utils.ContextFlag()
|
475
465
|
|
476
466
|
# If the current command created a process to pipe to, then this will be a ProcReader object.
|
477
467
|
# Otherwise it will be None. It's used to know when a pipe process can be killed and/or waited upon.
|
@@ -499,7 +489,7 @@ class Cmd(cmd.Cmd):
|
|
499
489
|
self.broken_pipe_warning = ''
|
500
490
|
|
501
491
|
# Commands that will run at the beginning of the command loop
|
502
|
-
self._startup_commands:
|
492
|
+
self._startup_commands: list[str] = []
|
503
493
|
|
504
494
|
# If a startup script is provided and exists, then execute it in the startup commands
|
505
495
|
if startup_script:
|
@@ -511,7 +501,7 @@ class Cmd(cmd.Cmd):
|
|
511
501
|
self._startup_commands.append(script_cmd)
|
512
502
|
|
513
503
|
# Transcript files to run instead of interactive command loop
|
514
|
-
self._transcript_files: Optional[
|
504
|
+
self._transcript_files: Optional[list[str]] = None
|
515
505
|
|
516
506
|
# Check for command line args
|
517
507
|
if allow_cli_args:
|
@@ -554,7 +544,7 @@ class Cmd(cmd.Cmd):
|
|
554
544
|
# Commands that have been disabled from use. This is to support commands that are only available
|
555
545
|
# during specific states of the application. This dictionary's keys are the command names and its
|
556
546
|
# values are DisabledCommand objects.
|
557
|
-
self.disabled_commands:
|
547
|
+
self.disabled_commands: dict[str, DisabledCommand] = {}
|
558
548
|
|
559
549
|
# If any command has been categorized, then all other commands that haven't been categorized
|
560
550
|
# will display under this section in the help output.
|
@@ -566,7 +556,7 @@ class Cmd(cmd.Cmd):
|
|
566
556
|
# command and category names
|
567
557
|
# alias, macro, settable, and shortcut names
|
568
558
|
# tab completion results when self.matches_sorted is False
|
569
|
-
self.default_sort_key = Cmd.ALPHABETICAL_SORT_KEY
|
559
|
+
self.default_sort_key: Callable[[str], str] = Cmd.ALPHABETICAL_SORT_KEY
|
570
560
|
|
571
561
|
############################################################################################################
|
572
562
|
# The following variables are used by tab completion functions. They are reset each time complete() is run
|
@@ -582,17 +572,17 @@ class Cmd(cmd.Cmd):
|
|
582
572
|
self.allow_closing_quote = True
|
583
573
|
|
584
574
|
# An optional hint which prints above tab completion suggestions
|
585
|
-
self.completion_hint = ''
|
575
|
+
self.completion_hint: str = ''
|
586
576
|
|
587
577
|
# Normally cmd2 uses readline's formatter to columnize the list of completion suggestions.
|
588
578
|
# If a custom format is preferred, write the formatted completions to this string. cmd2 will
|
589
579
|
# then print it instead of the readline format. ANSI style sequences and newlines are supported
|
590
580
|
# when using this value. Even when using formatted_completions, the full matches must still be returned
|
591
581
|
# from your completer function. ArgparseCompleter writes its tab completion tables to this string.
|
592
|
-
self.formatted_completions = ''
|
582
|
+
self.formatted_completions: str = ''
|
593
583
|
|
594
584
|
# Used by complete() for readline tab completion
|
595
|
-
self.completion_matches:
|
585
|
+
self.completion_matches: list[str] = []
|
596
586
|
|
597
587
|
# Use this list if you need to display tab completion suggestions that are different than the actual text
|
598
588
|
# of the matches. For instance, if you are completing strings that contain a common delimiter and you only
|
@@ -600,7 +590,7 @@ class Cmd(cmd.Cmd):
|
|
600
590
|
# still must be returned from your completer function. For an example, look at path_complete() which
|
601
591
|
# uses this to show only the basename of paths as the suggestions. delimiter_complete() also populates
|
602
592
|
# this list. These are ignored if self.formatted_completions is populated.
|
603
|
-
self.display_matches:
|
593
|
+
self.display_matches: list[str] = []
|
604
594
|
|
605
595
|
# Used by functions like path_complete() and delimiter_complete() to properly
|
606
596
|
# quote matches that are completed in a delimited fashion
|
@@ -609,10 +599,10 @@ class Cmd(cmd.Cmd):
|
|
609
599
|
# Set to True before returning matches to complete() in cases where matches have already been sorted.
|
610
600
|
# If False, then complete() will sort the matches using self.default_sort_key before they are displayed.
|
611
601
|
# This does not affect self.formatted_completions.
|
612
|
-
self.matches_sorted = False
|
602
|
+
self.matches_sorted: bool = False
|
613
603
|
|
614
604
|
# Command parsers for this Cmd instance.
|
615
|
-
self._command_parsers = _CommandParsers(self)
|
605
|
+
self._command_parsers: _CommandParsers = _CommandParsers(self)
|
616
606
|
|
617
607
|
# Add functions decorated to be subcommands
|
618
608
|
self._register_subcommands(self)
|
@@ -642,9 +632,9 @@ class Cmd(cmd.Cmd):
|
|
642
632
|
# the current command being executed
|
643
633
|
self.current_command: Optional[Statement] = None
|
644
634
|
|
645
|
-
def find_commandsets(self, commandset_type:
|
646
|
-
"""
|
647
|
-
|
635
|
+
def find_commandsets(self, commandset_type: type[CommandSet], *, subclass_match: bool = False) -> list[CommandSet]:
|
636
|
+
"""Find all CommandSets that match the provided CommandSet type.
|
637
|
+
|
648
638
|
By default, locates a CommandSet that is an exact type match but may optionally return all CommandSets that
|
649
639
|
are sub-classes of the provided type
|
650
640
|
:param commandset_type: CommandSet sub-class type to search for
|
@@ -658,8 +648,8 @@ class Cmd(cmd.Cmd):
|
|
658
648
|
]
|
659
649
|
|
660
650
|
def find_commandset_for_command(self, command_name: str) -> Optional[CommandSet]:
|
661
|
-
"""
|
662
|
-
|
651
|
+
"""Find the CommandSet that registered the command name.
|
652
|
+
|
663
653
|
:param command_name: command name to search
|
664
654
|
:return: CommandSet that provided the command
|
665
655
|
"""
|
@@ -671,7 +661,7 @@ class Cmd(cmd.Cmd):
|
|
671
661
|
all_commandset_defs = CommandSet.__subclasses__()
|
672
662
|
existing_commandset_types = [type(command_set) for command_set in self._installed_command_sets]
|
673
663
|
|
674
|
-
def load_commandset_by_type(commandset_types:
|
664
|
+
def load_commandset_by_type(commandset_types: list[type[CommandSet]]) -> None:
|
675
665
|
for cmdset_type in commandset_types:
|
676
666
|
# check if the type has sub-classes. We will only auto-load leaf class types.
|
677
667
|
subclasses = cmdset_type.__subclasses__()
|
@@ -690,8 +680,7 @@ class Cmd(cmd.Cmd):
|
|
690
680
|
load_commandset_by_type(all_commandset_defs)
|
691
681
|
|
692
682
|
def register_command_set(self, cmdset: CommandSet) -> None:
|
693
|
-
"""
|
694
|
-
Installs a CommandSet, loading all commands defined in the CommandSet
|
683
|
+
"""Installs a CommandSet, loading all commands defined in the CommandSet.
|
695
684
|
|
696
685
|
:param cmdset: CommandSet to load
|
697
686
|
"""
|
@@ -703,19 +692,19 @@ class Cmd(cmd.Cmd):
|
|
703
692
|
if self.always_prefix_settables:
|
704
693
|
if not cmdset.settable_prefix.strip():
|
705
694
|
raise CommandSetRegistrationError('CommandSet settable prefix must not be empty')
|
706
|
-
for key in cmdset.settables
|
695
|
+
for key in cmdset.settables:
|
707
696
|
prefixed_name = f'{cmdset.settable_prefix}.{key}'
|
708
697
|
if prefixed_name in all_settables:
|
709
698
|
raise CommandSetRegistrationError(f'Duplicate settable: {key}')
|
710
699
|
|
711
700
|
else:
|
712
|
-
for key in cmdset.settables
|
701
|
+
for key in cmdset.settables:
|
713
702
|
if key in all_settables:
|
714
703
|
raise CommandSetRegistrationError(f'Duplicate settable {key} is already registered')
|
715
704
|
|
716
705
|
cmdset.on_register(self)
|
717
706
|
methods = cast(
|
718
|
-
|
707
|
+
list[tuple[str, Callable[..., Any]]],
|
719
708
|
inspect.getmembers(
|
720
709
|
cmdset,
|
721
710
|
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
@@ -790,8 +779,7 @@ class Cmd(cmd.Cmd):
|
|
790
779
|
return parser
|
791
780
|
|
792
781
|
def _install_command_function(self, command_func_name: str, command_method: CommandFunc, context: str = '') -> None:
|
793
|
-
"""
|
794
|
-
Install a new command function into the CLI.
|
782
|
+
"""Install a new command function into the CLI.
|
795
783
|
|
796
784
|
:param command_func_name: name of command function to add
|
797
785
|
This points to the command method and may differ from the method's
|
@@ -800,7 +788,6 @@ class Cmd(cmd.Cmd):
|
|
800
788
|
:param context: optional info to provide in error message. (e.g. class this function belongs to)
|
801
789
|
:raises CommandSetRegistrationError: if the command function fails to install
|
802
790
|
"""
|
803
|
-
|
804
791
|
# command_func_name must begin with COMMAND_FUNC_PREFIX to be identified as a command by cmd2.
|
805
792
|
if not command_func_name.startswith(COMMAND_FUNC_PREFIX):
|
806
793
|
raise CommandSetRegistrationError(f"{command_func_name} does not begin with '{COMMAND_FUNC_PREFIX}'")
|
@@ -847,8 +834,7 @@ class Cmd(cmd.Cmd):
|
|
847
834
|
setattr(self, help_func_name, cmd_help)
|
848
835
|
|
849
836
|
def unregister_command_set(self, cmdset: CommandSet) -> None:
|
850
|
-
"""
|
851
|
-
Uninstalls a CommandSet and unloads all associated commands
|
837
|
+
"""Uninstalls a CommandSet and unloads all associated commands.
|
852
838
|
|
853
839
|
:param cmdset: CommandSet to uninstall
|
854
840
|
"""
|
@@ -857,7 +843,7 @@ class Cmd(cmd.Cmd):
|
|
857
843
|
cmdset.on_unregister()
|
858
844
|
self._unregister_subcommands(cmdset)
|
859
845
|
|
860
|
-
methods:
|
846
|
+
methods: list[tuple[str, Callable[..., Any]]] = inspect.getmembers(
|
861
847
|
cmdset,
|
862
848
|
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
863
849
|
and hasattr(meth, '__name__')
|
@@ -903,7 +889,7 @@ class Cmd(cmd.Cmd):
|
|
903
889
|
check_parser_uninstallable(subparser)
|
904
890
|
break
|
905
891
|
|
906
|
-
methods:
|
892
|
+
methods: list[tuple[str, Callable[..., Any]]] = inspect.getmembers(
|
907
893
|
cmdset,
|
908
894
|
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
909
895
|
and hasattr(meth, '__name__')
|
@@ -919,8 +905,7 @@ class Cmd(cmd.Cmd):
|
|
919
905
|
check_parser_uninstallable(command_parser)
|
920
906
|
|
921
907
|
def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
|
922
|
-
"""
|
923
|
-
Register subcommands with their base command
|
908
|
+
"""Register subcommands with their base command.
|
924
909
|
|
925
910
|
:param cmdset: CommandSet or cmd2.Cmd subclass containing subcommands
|
926
911
|
"""
|
@@ -937,14 +922,14 @@ class Cmd(cmd.Cmd):
|
|
937
922
|
)
|
938
923
|
|
939
924
|
# iterate through all matching methods
|
940
|
-
for
|
925
|
+
for _method_name, method in methods:
|
941
926
|
subcommand_name: str = getattr(method, constants.SUBCMD_ATTR_NAME)
|
942
927
|
full_command_name: str = getattr(method, constants.SUBCMD_ATTR_COMMAND)
|
943
928
|
subcmd_parser_builder = getattr(method, constants.CMD_ATTR_ARGPARSER)
|
944
929
|
|
945
930
|
subcommand_valid, errmsg = self.statement_parser.is_valid_command(subcommand_name, is_subcommand=True)
|
946
931
|
if not subcommand_valid:
|
947
|
-
raise CommandSetRegistrationError(f'Subcommand {
|
932
|
+
raise CommandSetRegistrationError(f'Subcommand {subcommand_name!s} is not valid: {errmsg}')
|
948
933
|
|
949
934
|
command_tokens = full_command_name.split()
|
950
935
|
command_name = command_tokens[0]
|
@@ -957,16 +942,14 @@ class Cmd(cmd.Cmd):
|
|
957
942
|
command_func = self.cmd_func(command_name)
|
958
943
|
|
959
944
|
if command_func is None:
|
960
|
-
raise CommandSetRegistrationError(
|
961
|
-
f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
|
962
|
-
)
|
945
|
+
raise CommandSetRegistrationError(f"Could not find command '{command_name}' needed by subcommand: {method!s}")
|
963
946
|
command_parser = self._command_parsers.get(command_func)
|
964
947
|
if command_parser is None:
|
965
948
|
raise CommandSetRegistrationError(
|
966
|
-
f"Could not find argparser for command '{command_name}' needed by subcommand: {
|
949
|
+
f"Could not find argparser for command '{command_name}' needed by subcommand: {method!s}"
|
967
950
|
)
|
968
951
|
|
969
|
-
def find_subcommand(action: argparse.ArgumentParser, subcmd_names:
|
952
|
+
def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> argparse.ArgumentParser:
|
970
953
|
if not subcmd_names:
|
971
954
|
return action
|
972
955
|
cur_subcmd = subcmd_names.pop(0)
|
@@ -976,7 +959,7 @@ class Cmd(cmd.Cmd):
|
|
976
959
|
if choice_name == cur_subcmd:
|
977
960
|
return find_subcommand(choice, subcmd_names)
|
978
961
|
break
|
979
|
-
raise CommandSetRegistrationError(f"Could not find subcommand '{
|
962
|
+
raise CommandSetRegistrationError(f"Could not find subcommand '{action}'")
|
980
963
|
|
981
964
|
target_parser = find_subcommand(command_parser, subcommand_names)
|
982
965
|
|
@@ -1028,8 +1011,7 @@ class Cmd(cmd.Cmd):
|
|
1028
1011
|
break
|
1029
1012
|
|
1030
1013
|
def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
|
1031
|
-
"""
|
1032
|
-
Unregister subcommands from their base command
|
1014
|
+
"""Unregister subcommands from their base command.
|
1033
1015
|
|
1034
1016
|
:param cmdset: CommandSet containing subcommands
|
1035
1017
|
"""
|
@@ -1046,7 +1028,7 @@ class Cmd(cmd.Cmd):
|
|
1046
1028
|
)
|
1047
1029
|
|
1048
1030
|
# iterate through all matching methods
|
1049
|
-
for
|
1031
|
+
for _method_name, method in methods:
|
1050
1032
|
subcommand_name = getattr(method, constants.SUBCMD_ATTR_NAME)
|
1051
1033
|
command_name = getattr(method, constants.SUBCMD_ATTR_COMMAND)
|
1052
1034
|
|
@@ -1059,15 +1041,13 @@ class Cmd(cmd.Cmd):
|
|
1059
1041
|
if command_func is None: # pragma: no cover
|
1060
1042
|
# This really shouldn't be possible since _register_subcommands would prevent this from happening
|
1061
1043
|
# but keeping in case it does for some strange reason
|
1062
|
-
raise CommandSetRegistrationError(
|
1063
|
-
f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
|
1064
|
-
)
|
1044
|
+
raise CommandSetRegistrationError(f"Could not find command '{command_name}' needed by subcommand: {method!s}")
|
1065
1045
|
command_parser = self._command_parsers.get(command_func)
|
1066
1046
|
if command_parser is None: # pragma: no cover
|
1067
1047
|
# This really shouldn't be possible since _register_subcommands would prevent this from happening
|
1068
1048
|
# but keeping in case it does for some strange reason
|
1069
1049
|
raise CommandSetRegistrationError(
|
1070
|
-
f"Could not find argparser for command '{command_name}' needed by subcommand: {
|
1050
|
+
f"Could not find argparser for command '{command_name}' needed by subcommand: {method!s}"
|
1071
1051
|
)
|
1072
1052
|
|
1073
1053
|
for action in command_parser._actions:
|
@@ -1077,8 +1057,7 @@ class Cmd(cmd.Cmd):
|
|
1077
1057
|
|
1078
1058
|
@property
|
1079
1059
|
def always_prefix_settables(self) -> bool:
|
1080
|
-
"""
|
1081
|
-
Flags whether CommandSet settable values should always be prefixed
|
1060
|
+
"""Flags whether CommandSet settable values should always be prefixed.
|
1082
1061
|
|
1083
1062
|
:return: True if CommandSet settable values will always be prefixed. False if not.
|
1084
1063
|
"""
|
@@ -1086,8 +1065,7 @@ class Cmd(cmd.Cmd):
|
|
1086
1065
|
|
1087
1066
|
@always_prefix_settables.setter
|
1088
1067
|
def always_prefix_settables(self, new_value: bool) -> None:
|
1089
|
-
"""
|
1090
|
-
Set whether CommandSet settable values should always be prefixed.
|
1068
|
+
"""Set whether CommandSet settable values should always be prefixed.
|
1091
1069
|
|
1092
1070
|
:param new_value: True if CommandSet settable values should always be prefixed. False if not.
|
1093
1071
|
:raises ValueError: If a registered CommandSet does not have a defined prefix
|
@@ -1103,8 +1081,7 @@ class Cmd(cmd.Cmd):
|
|
1103
1081
|
|
1104
1082
|
@property
|
1105
1083
|
def settables(self) -> Mapping[str, Settable]:
|
1106
|
-
"""
|
1107
|
-
Get all available user-settable attributes. This includes settables defined in installed CommandSets
|
1084
|
+
"""Get all available user-settable attributes. This includes settables defined in installed CommandSets.
|
1108
1085
|
|
1109
1086
|
:return: Mapping from attribute-name to Settable of all user-settable attributes from
|
1110
1087
|
"""
|
@@ -1119,44 +1096,41 @@ class Cmd(cmd.Cmd):
|
|
1119
1096
|
return all_settables
|
1120
1097
|
|
1121
1098
|
def add_settable(self, settable: Settable) -> None:
|
1122
|
-
"""
|
1123
|
-
Add a settable parameter to ``self.settables``
|
1099
|
+
"""Add a settable parameter to ``self.settables``.
|
1124
1100
|
|
1125
1101
|
:param settable: Settable object being added
|
1126
1102
|
"""
|
1127
|
-
if not self.always_prefix_settables:
|
1128
|
-
|
1129
|
-
raise KeyError(f'Duplicate settable: {settable.name}')
|
1103
|
+
if not self.always_prefix_settables and settable.name in self.settables and settable.name not in self._settables:
|
1104
|
+
raise KeyError(f'Duplicate settable: {settable.name}')
|
1130
1105
|
self._settables[settable.name] = settable
|
1131
1106
|
|
1132
1107
|
def remove_settable(self, name: str) -> None:
|
1133
|
-
"""
|
1134
|
-
Convenience method for removing a settable parameter from ``self.settables``
|
1108
|
+
"""Remove a settable parameter from ``self.settables``.
|
1135
1109
|
|
1136
1110
|
:param name: name of the settable being removed
|
1137
1111
|
:raises KeyError: if the Settable matches this name
|
1138
1112
|
"""
|
1139
1113
|
try:
|
1140
1114
|
del self._settables[name]
|
1141
|
-
except KeyError:
|
1142
|
-
raise KeyError(name + " is not a settable parameter")
|
1115
|
+
except KeyError as exc:
|
1116
|
+
raise KeyError(name + " is not a settable parameter") from exc
|
1143
1117
|
|
1144
1118
|
def build_settables(self) -> None:
|
1145
|
-
"""Create the dictionary of user-settable parameters"""
|
1119
|
+
"""Create the dictionary of user-settable parameters."""
|
1146
1120
|
|
1147
|
-
def get_allow_style_choices(
|
1148
|
-
"""
|
1121
|
+
def get_allow_style_choices(_cli_self: Cmd) -> list[str]:
|
1122
|
+
"""Tab complete allow_style values."""
|
1149
1123
|
return [val.name.lower() for val in ansi.AllowStyle]
|
1150
1124
|
|
1151
1125
|
def allow_style_type(value: str) -> ansi.AllowStyle:
|
1152
|
-
"""
|
1126
|
+
"""Convert a string value into an ansi.AllowStyle."""
|
1153
1127
|
try:
|
1154
1128
|
return ansi.AllowStyle[value.upper()]
|
1155
|
-
except KeyError:
|
1129
|
+
except KeyError as esc:
|
1156
1130
|
raise ValueError(
|
1157
1131
|
f"must be {ansi.AllowStyle.ALWAYS}, {ansi.AllowStyle.NEVER}, or "
|
1158
1132
|
f"{ansi.AllowStyle.TERMINAL} (case-insensitive)"
|
1159
|
-
)
|
1133
|
+
) from esc
|
1160
1134
|
|
1161
1135
|
self.add_settable(
|
1162
1136
|
Settable(
|
@@ -1187,16 +1161,16 @@ class Cmd(cmd.Cmd):
|
|
1187
1161
|
|
1188
1162
|
@property
|
1189
1163
|
def allow_style(self) -> ansi.AllowStyle:
|
1190
|
-
"""Read-only property needed to support do_set when it reads allow_style"""
|
1164
|
+
"""Read-only property needed to support do_set when it reads allow_style."""
|
1191
1165
|
return ansi.allow_style
|
1192
1166
|
|
1193
1167
|
@allow_style.setter
|
1194
1168
|
def allow_style(self, new_val: ansi.AllowStyle) -> None:
|
1195
|
-
"""Setter property needed to support do_set when it updates allow_style"""
|
1169
|
+
"""Setter property needed to support do_set when it updates allow_style."""
|
1196
1170
|
ansi.allow_style = new_val
|
1197
1171
|
|
1198
1172
|
def _completion_supported(self) -> bool:
|
1199
|
-
"""Return whether tab completion is supported"""
|
1173
|
+
"""Return whether tab completion is supported."""
|
1200
1174
|
return self.use_rawinput and bool(self.completekey) and rl_type != RlType.NONE
|
1201
1175
|
|
1202
1176
|
@property
|
@@ -1218,8 +1192,7 @@ class Cmd(cmd.Cmd):
|
|
1218
1192
|
end: str = '\n',
|
1219
1193
|
style: Optional[Callable[[str], str]] = None,
|
1220
1194
|
) -> None:
|
1221
|
-
"""
|
1222
|
-
Print message to a given file object.
|
1195
|
+
"""Print message to a given file object.
|
1223
1196
|
|
1224
1197
|
:param dest: the file object being written to
|
1225
1198
|
:param msg: object to print
|
@@ -1239,7 +1212,7 @@ class Cmd(cmd.Cmd):
|
|
1239
1212
|
sys.stderr.write(self.broken_pipe_warning)
|
1240
1213
|
|
1241
1214
|
def poutput(self, msg: Any = '', *, end: str = '\n') -> None:
|
1242
|
-
"""Print message to self.stdout and appends a newline by default
|
1215
|
+
"""Print message to self.stdout and appends a newline by default.
|
1243
1216
|
|
1244
1217
|
:param msg: object to print
|
1245
1218
|
:param end: string appended after the end of the message, default a newline
|
@@ -1247,7 +1220,7 @@ class Cmd(cmd.Cmd):
|
|
1247
1220
|
self.print_to(self.stdout, msg, end=end)
|
1248
1221
|
|
1249
1222
|
def perror(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> None:
|
1250
|
-
"""Print message to sys.stderr
|
1223
|
+
"""Print message to sys.stderr.
|
1251
1224
|
|
1252
1225
|
:param msg: object to print
|
1253
1226
|
:param end: string appended after the end of the message, default a newline
|
@@ -1257,7 +1230,7 @@ class Cmd(cmd.Cmd):
|
|
1257
1230
|
self.print_to(sys.stderr, msg, end=end, style=ansi.style_error if apply_style else None)
|
1258
1231
|
|
1259
1232
|
def psuccess(self, msg: Any = '', *, end: str = '\n') -> None:
|
1260
|
-
"""
|
1233
|
+
"""Wrap poutput, but applies ansi.style_success by default.
|
1261
1234
|
|
1262
1235
|
:param msg: object to print
|
1263
1236
|
:param end: string appended after the end of the message, default a newline
|
@@ -1266,7 +1239,7 @@ class Cmd(cmd.Cmd):
|
|
1266
1239
|
self.poutput(msg, end=end)
|
1267
1240
|
|
1268
1241
|
def pwarning(self, msg: Any = '', *, end: str = '\n') -> None:
|
1269
|
-
"""
|
1242
|
+
"""Wrap perror, but applies ansi.style_warning by default.
|
1270
1243
|
|
1271
1244
|
:param msg: object to print
|
1272
1245
|
:param end: string appended after the end of the message, default a newline
|
@@ -1302,7 +1275,8 @@ class Cmd(cmd.Cmd):
|
|
1302
1275
|
self.perror(final_msg, end=end, apply_style=False)
|
1303
1276
|
|
1304
1277
|
def pfeedback(self, msg: Any, *, end: str = '\n') -> None:
|
1305
|
-
"""
|
1278
|
+
"""Print nonessential feedback. Can be silenced with `quiet`.
|
1279
|
+
|
1306
1280
|
Inclusion in redirected output is controlled by `feedback_to_output`.
|
1307
1281
|
|
1308
1282
|
:param msg: object to print
|
@@ -1334,7 +1308,7 @@ class Cmd(cmd.Cmd):
|
|
1334
1308
|
# Don't try to use the pager when being run by a continuous integration system like Jenkins + pexpect.
|
1335
1309
|
functional_terminal = False
|
1336
1310
|
|
1337
|
-
if self.stdin.isatty() and self.stdout.isatty():
|
1311
|
+
if self.stdin.isatty() and self.stdout.isatty(): # noqa: SIM102
|
1338
1312
|
if sys.platform.startswith('win') or os.environ.get('TERM') is not None:
|
1339
1313
|
functional_terminal = True
|
1340
1314
|
|
@@ -1355,7 +1329,7 @@ class Cmd(cmd.Cmd):
|
|
1355
1329
|
with self.sigint_protection:
|
1356
1330
|
import subprocess
|
1357
1331
|
|
1358
|
-
pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE, stdout=self.stdout)
|
1332
|
+
pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE, stdout=self.stdout) # noqa: S602
|
1359
1333
|
pipe_proc.communicate(final_msg.encode('utf-8', 'replace'))
|
1360
1334
|
except BrokenPipeError:
|
1361
1335
|
# This occurs if a command's output is being piped to another process and that process closes before the
|
@@ -1366,12 +1340,23 @@ class Cmd(cmd.Cmd):
|
|
1366
1340
|
else:
|
1367
1341
|
self.poutput(msg, end=end)
|
1368
1342
|
|
1343
|
+
def ppretty(self, data: Any, *, indent: int = 2, width: int = 80, depth: Optional[int] = None, end: str = '\n') -> None:
|
1344
|
+
"""Pretty print arbitrary Python data structures to self.stdout and appends a newline by default.
|
1345
|
+
|
1346
|
+
:param data: object to print
|
1347
|
+
:param indent: the amount of indentation added for each nesting level
|
1348
|
+
:param width: the desired maximum number of characters per line in the output, a best effort will be made for long data
|
1349
|
+
:param depth: the number of nesting levels which may be printed, if data is too deep, the next level replaced by ...
|
1350
|
+
:param end: string appended after the end of the message, default a newline
|
1351
|
+
"""
|
1352
|
+
self.print_to(self.stdout, pprint.pformat(data, indent, width, depth), end=end)
|
1353
|
+
|
1369
1354
|
# ----- Methods related to tab completion -----
|
1370
1355
|
|
1371
1356
|
def _reset_completion_defaults(self) -> None:
|
1372
|
-
"""
|
1373
|
-
|
1374
|
-
Needs to be called each time readline runs tab completion
|
1357
|
+
"""Reset tab completion settings.
|
1358
|
+
|
1359
|
+
Needs to be called each time readline runs tab completion.
|
1375
1360
|
"""
|
1376
1361
|
self.allow_appended_space = True
|
1377
1362
|
self.allow_closing_quote = True
|
@@ -1387,8 +1372,8 @@ class Cmd(cmd.Cmd):
|
|
1387
1372
|
elif rl_type == RlType.PYREADLINE:
|
1388
1373
|
readline.rl.mode._display_completions = self._display_matches_pyreadline
|
1389
1374
|
|
1390
|
-
def tokens_for_completion(self, line: str, begidx: int, endidx: int) ->
|
1391
|
-
"""
|
1375
|
+
def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> tuple[list[str], list[str]]:
|
1376
|
+
"""Get all tokens through the one being completed, used by tab completion functions.
|
1392
1377
|
|
1393
1378
|
:param line: the current input line with leading whitespace removed
|
1394
1379
|
:param begidx: the beginning index of the prefix text
|
@@ -1454,14 +1439,14 @@ class Cmd(cmd.Cmd):
|
|
1454
1439
|
def basic_complete(
|
1455
1440
|
self,
|
1456
1441
|
text: str,
|
1457
|
-
line: str,
|
1458
|
-
begidx: int,
|
1459
|
-
endidx: int,
|
1442
|
+
line: str, # noqa: ARG002
|
1443
|
+
begidx: int, # noqa: ARG002
|
1444
|
+
endidx: int, # noqa: ARG002
|
1460
1445
|
match_against: Iterable[str],
|
1461
|
-
) ->
|
1462
|
-
"""
|
1463
|
-
|
1464
|
-
|
1446
|
+
) -> list[str]:
|
1447
|
+
"""Tab completion function that matches against a list of strings without considering line contents or cursor position.
|
1448
|
+
|
1449
|
+
The args required by this function are defined in the header of Python's cmd.py.
|
1465
1450
|
|
1466
1451
|
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
1467
1452
|
:param line: the current input line with leading whitespace removed
|
@@ -1480,10 +1465,10 @@ class Cmd(cmd.Cmd):
|
|
1480
1465
|
endidx: int,
|
1481
1466
|
match_against: Iterable[str],
|
1482
1467
|
delimiter: str,
|
1483
|
-
) ->
|
1484
|
-
"""
|
1485
|
-
|
1486
|
-
the portion of the match being tab completed is shown as the completion suggestions.
|
1468
|
+
) -> list[str]:
|
1469
|
+
"""Perform tab completion against a list but each match is split on a delimiter.
|
1470
|
+
|
1471
|
+
Only the portion of the match being tab completed is shown as the completion suggestions.
|
1487
1472
|
This is useful if you match against strings that are hierarchical in nature and have a
|
1488
1473
|
common delimiter.
|
1489
1474
|
|
@@ -1546,10 +1531,10 @@ class Cmd(cmd.Cmd):
|
|
1546
1531
|
line: str,
|
1547
1532
|
begidx: int,
|
1548
1533
|
endidx: int,
|
1549
|
-
flag_dict:
|
1534
|
+
flag_dict: dict[str, Union[Iterable[str], CompleterFunc]],
|
1550
1535
|
*,
|
1551
1536
|
all_else: Union[None, Iterable[str], CompleterFunc] = None,
|
1552
|
-
) ->
|
1537
|
+
) -> list[str]:
|
1553
1538
|
"""Tab completes based on a particular flag preceding the token being completed.
|
1554
1539
|
|
1555
1540
|
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
@@ -1598,7 +1583,7 @@ class Cmd(cmd.Cmd):
|
|
1598
1583
|
index_dict: Mapping[int, Union[Iterable[str], CompleterFunc]],
|
1599
1584
|
*,
|
1600
1585
|
all_else: Optional[Union[Iterable[str], CompleterFunc]] = None,
|
1601
|
-
) ->
|
1586
|
+
) -> list[str]:
|
1602
1587
|
"""Tab completes based on a fixed position in the input string.
|
1603
1588
|
|
1604
1589
|
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
@@ -1626,10 +1611,7 @@ class Cmd(cmd.Cmd):
|
|
1626
1611
|
|
1627
1612
|
# Check if token is at an index in the dictionary
|
1628
1613
|
match_against: Optional[Union[Iterable[str], CompleterFunc]]
|
1629
|
-
|
1630
|
-
match_against = index_dict[index]
|
1631
|
-
else:
|
1632
|
-
match_against = all_else
|
1614
|
+
match_against = index_dict.get(index, all_else)
|
1633
1615
|
|
1634
1616
|
# Perform tab completion using a Iterable
|
1635
1617
|
if isinstance(match_against, Iterable):
|
@@ -1642,9 +1624,15 @@ class Cmd(cmd.Cmd):
|
|
1642
1624
|
return matches
|
1643
1625
|
|
1644
1626
|
def path_complete(
|
1645
|
-
self,
|
1646
|
-
|
1647
|
-
|
1627
|
+
self,
|
1628
|
+
text: str,
|
1629
|
+
line: str,
|
1630
|
+
begidx: int, # noqa: ARG002
|
1631
|
+
endidx: int,
|
1632
|
+
*,
|
1633
|
+
path_filter: Optional[Callable[[str], bool]] = None,
|
1634
|
+
) -> list[str]:
|
1635
|
+
"""Perform completion of local file system paths.
|
1648
1636
|
|
1649
1637
|
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
1650
1638
|
:param line: the current input line with leading whitespace removed
|
@@ -1657,7 +1645,7 @@ class Cmd(cmd.Cmd):
|
|
1657
1645
|
"""
|
1658
1646
|
|
1659
1647
|
# Used to complete ~ and ~user strings
|
1660
|
-
def complete_users() ->
|
1648
|
+
def complete_users() -> list[str]:
|
1661
1649
|
users = []
|
1662
1650
|
|
1663
1651
|
# Windows lacks the pwd module so we can't get a list of users.
|
@@ -1728,12 +1716,11 @@ class Cmd(cmd.Cmd):
|
|
1728
1716
|
return complete_users()
|
1729
1717
|
|
1730
1718
|
# Otherwise expand the user dir
|
1731
|
-
|
1732
|
-
search_str = os.path.expanduser(search_str)
|
1719
|
+
search_str = os.path.expanduser(search_str)
|
1733
1720
|
|
1734
|
-
|
1735
|
-
|
1736
|
-
|
1721
|
+
# Get what we need to restore the original tilde path later
|
1722
|
+
orig_tilde_path = text[:sep_index]
|
1723
|
+
expanded_tilde_path = os.path.expanduser(orig_tilde_path)
|
1737
1724
|
|
1738
1725
|
# If the search text does not have a directory, then use the cwd
|
1739
1726
|
elif not os.path.dirname(text):
|
@@ -1772,10 +1759,7 @@ class Cmd(cmd.Cmd):
|
|
1772
1759
|
|
1773
1760
|
# Remove cwd if it was added to match the text readline expects
|
1774
1761
|
if cwd_added:
|
1775
|
-
if cwd == os.path.sep
|
1776
|
-
to_replace = cwd
|
1777
|
-
else:
|
1778
|
-
to_replace = cwd + os.path.sep
|
1762
|
+
to_replace = cwd if cwd == os.path.sep else cwd + os.path.sep
|
1779
1763
|
matches = [cur_path.replace(to_replace, '', 1) for cur_path in matches]
|
1780
1764
|
|
1781
1765
|
# Restore the tilde string if we expanded one to match the text readline expects
|
@@ -1784,8 +1768,8 @@ class Cmd(cmd.Cmd):
|
|
1784
1768
|
|
1785
1769
|
return matches
|
1786
1770
|
|
1787
|
-
def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *, complete_blank: bool = False) ->
|
1788
|
-
"""
|
1771
|
+
def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *, complete_blank: bool = False) -> list[str]:
|
1772
|
+
"""Perform completion of executables either in a user's path or a given path.
|
1789
1773
|
|
1790
1774
|
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
1791
1775
|
:param line: the current input line with leading whitespace removed
|
@@ -1804,15 +1788,15 @@ class Cmd(cmd.Cmd):
|
|
1804
1788
|
return utils.get_exes_in_path(text)
|
1805
1789
|
|
1806
1790
|
# Otherwise look for executables in the given path
|
1807
|
-
|
1808
|
-
|
1809
|
-
|
1810
|
-
|
1791
|
+
return self.path_complete(
|
1792
|
+
text, line, begidx, endidx, path_filter=lambda path: os.path.isdir(path) or os.access(path, os.X_OK)
|
1793
|
+
)
|
1794
|
+
|
1795
|
+
def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, compfunc: CompleterFunc) -> list[str]:
|
1796
|
+
"""First tab completion function for all commands, called by complete().
|
1811
1797
|
|
1812
|
-
def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, compfunc: CompleterFunc) -> List[str]:
|
1813
|
-
"""Called by complete() as the first tab completion function for all commands
|
1814
1798
|
It determines if it should tab complete for redirection (|, >, >>) or use the
|
1815
|
-
completer function for the current command
|
1799
|
+
completer function for the current command.
|
1816
1800
|
|
1817
1801
|
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
1818
1802
|
:param line: the current input line with leading whitespace removed
|
@@ -1878,20 +1862,21 @@ class Cmd(cmd.Cmd):
|
|
1878
1862
|
if do_shell_completion:
|
1879
1863
|
return self.shell_cmd_complete(text, line, begidx, endidx)
|
1880
1864
|
|
1881
|
-
|
1865
|
+
if do_path_completion:
|
1882
1866
|
return self.path_complete(text, line, begidx, endidx)
|
1883
1867
|
|
1884
1868
|
# If there were redirection strings anywhere on the command line, then we
|
1885
1869
|
# are no longer tab completing for the current command
|
1886
|
-
|
1870
|
+
if has_redirection:
|
1887
1871
|
return []
|
1888
1872
|
|
1889
1873
|
# Call the command's completer function
|
1890
1874
|
return compfunc(text, line, begidx, endidx)
|
1891
1875
|
|
1892
1876
|
@staticmethod
|
1893
|
-
def _pad_matches_to_display(matches_to_display:
|
1894
|
-
"""
|
1877
|
+
def _pad_matches_to_display(matches_to_display: list[str]) -> tuple[list[str], int]: # pragma: no cover
|
1878
|
+
"""Add padding to the matches being displayed as tab completion suggestions.
|
1879
|
+
|
1895
1880
|
The default padding of readline/pyreadine is small and not visually appealing
|
1896
1881
|
especially if matches have spaces. It appears very squished together.
|
1897
1882
|
|
@@ -1912,9 +1897,9 @@ class Cmd(cmd.Cmd):
|
|
1912
1897
|
return [cur_match + padding for cur_match in matches_to_display], len(padding)
|
1913
1898
|
|
1914
1899
|
def _display_matches_gnu_readline(
|
1915
|
-
self, substitution: str, matches:
|
1900
|
+
self, substitution: str, matches: list[str], longest_match_length: int
|
1916
1901
|
) -> None: # pragma: no cover
|
1917
|
-
"""
|
1902
|
+
"""Print a match list using GNU readline's rl_display_match_list().
|
1918
1903
|
|
1919
1904
|
:param substitution: the substitution written to the command line
|
1920
1905
|
:param matches: the tab completion matches to display
|
@@ -1944,8 +1929,7 @@ class Cmd(cmd.Cmd):
|
|
1944
1929
|
|
1945
1930
|
for cur_match in matches_to_display:
|
1946
1931
|
cur_length = ansi.style_aware_wcswidth(cur_match)
|
1947
|
-
|
1948
|
-
longest_match_length = cur_length
|
1932
|
+
longest_match_length = max(longest_match_length, cur_length)
|
1949
1933
|
else:
|
1950
1934
|
matches_to_display = matches
|
1951
1935
|
|
@@ -1960,7 +1944,7 @@ class Cmd(cmd.Cmd):
|
|
1960
1944
|
|
1961
1945
|
# rl_display_match_list() expects matches to be in argv format where
|
1962
1946
|
# substitution is the first element, followed by the matches, and then a NULL.
|
1963
|
-
strings_array = cast(
|
1947
|
+
strings_array = cast(list[Optional[bytes]], (ctypes.c_char_p * (1 + len(encoded_matches) + 1))())
|
1964
1948
|
|
1965
1949
|
# Copy in the encoded strings and add a NULL to the end
|
1966
1950
|
strings_array[0] = encoded_substitution
|
@@ -1973,8 +1957,8 @@ class Cmd(cmd.Cmd):
|
|
1973
1957
|
# Redraw prompt and input line
|
1974
1958
|
rl_force_redisplay()
|
1975
1959
|
|
1976
|
-
def _display_matches_pyreadline(self, matches:
|
1977
|
-
"""
|
1960
|
+
def _display_matches_pyreadline(self, matches: list[str]) -> None: # pragma: no cover
|
1961
|
+
"""Print a match list using pyreadline3's _display_completions().
|
1978
1962
|
|
1979
1963
|
:param matches: the tab completion matches to display
|
1980
1964
|
"""
|
@@ -1997,10 +1981,7 @@ class Cmd(cmd.Cmd):
|
|
1997
1981
|
# Otherwise use pyreadline3's formatter
|
1998
1982
|
else:
|
1999
1983
|
# Check if we should show display_matches
|
2000
|
-
if self.display_matches
|
2001
|
-
matches_to_display = self.display_matches
|
2002
|
-
else:
|
2003
|
-
matches_to_display = matches
|
1984
|
+
matches_to_display = self.display_matches if self.display_matches else matches
|
2004
1985
|
|
2005
1986
|
# Add padding for visual appeal
|
2006
1987
|
matches_to_display, _ = self._pad_matches_to_display(matches_to_display)
|
@@ -2009,15 +1990,15 @@ class Cmd(cmd.Cmd):
|
|
2009
1990
|
orig_pyreadline_display(matches_to_display)
|
2010
1991
|
|
2011
1992
|
@staticmethod
|
2012
|
-
def _determine_ap_completer_type(parser: argparse.ArgumentParser) ->
|
2013
|
-
"""
|
2014
|
-
|
2015
|
-
set, then use argparse_completer.DEFAULT_AP_COMPLETER.
|
1993
|
+
def _determine_ap_completer_type(parser: argparse.ArgumentParser) -> type[argparse_completer.ArgparseCompleter]:
|
1994
|
+
"""Determine what type of ArgparseCompleter to use on a given parser.
|
1995
|
+
|
1996
|
+
If the parser does not have one set, then use argparse_completer.DEFAULT_AP_COMPLETER.
|
2016
1997
|
|
2017
1998
|
:param parser: the parser to examine
|
2018
1999
|
:return: type of ArgparseCompleter
|
2019
2000
|
"""
|
2020
|
-
Completer = Optional[
|
2001
|
+
Completer = Optional[type[argparse_completer.ArgparseCompleter]] # noqa: N806
|
2021
2002
|
completer_type: Completer = parser.get_ap_completer_type() # type: ignore[attr-defined]
|
2022
2003
|
|
2023
2004
|
if completer_type is None:
|
@@ -2027,8 +2008,7 @@ class Cmd(cmd.Cmd):
|
|
2027
2008
|
def _perform_completion(
|
2028
2009
|
self, text: str, line: str, begidx: int, endidx: int, custom_settings: Optional[utils.CustomCompletionSettings] = None
|
2029
2010
|
) -> None:
|
2030
|
-
"""
|
2031
|
-
Helper function for complete() that performs the actual completion
|
2011
|
+
"""Perform the actual completion, helper function for complete().
|
2032
2012
|
|
2033
2013
|
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
2034
2014
|
:param line: the current input line with leading whitespace removed
|
@@ -2106,12 +2086,11 @@ class Cmd(cmd.Cmd):
|
|
2106
2086
|
completer_func = self.completedefault # type: ignore[assignment]
|
2107
2087
|
|
2108
2088
|
# Not a recognized macro or command
|
2089
|
+
# Check if this command should be run as a shell command
|
2090
|
+
elif self.default_to_shell and command in utils.get_exes_in_path(command):
|
2091
|
+
completer_func = self.path_complete
|
2109
2092
|
else:
|
2110
|
-
|
2111
|
-
if self.default_to_shell and command in utils.get_exes_in_path(command):
|
2112
|
-
completer_func = self.path_complete
|
2113
|
-
else:
|
2114
|
-
completer_func = self.completedefault # type: ignore[assignment]
|
2093
|
+
completer_func = self.completedefault # type: ignore[assignment]
|
2115
2094
|
|
2116
2095
|
# Otherwise we are completing the command token or performing custom completion
|
2117
2096
|
else:
|
@@ -2192,10 +2171,7 @@ class Cmd(cmd.Cmd):
|
|
2192
2171
|
|
2193
2172
|
if add_quote:
|
2194
2173
|
# Figure out what kind of quote to add and save it as the unclosed_quote
|
2195
|
-
if any('"' in match for match in self.completion_matches)
|
2196
|
-
completion_token_quote = "'"
|
2197
|
-
else:
|
2198
|
-
completion_token_quote = '"'
|
2174
|
+
completion_token_quote = "'" if any('"' in match for match in self.completion_matches) else '"'
|
2199
2175
|
|
2200
2176
|
self.completion_matches = [completion_token_quote + match for match in self.completion_matches]
|
2201
2177
|
|
@@ -2210,7 +2186,7 @@ class Cmd(cmd.Cmd):
|
|
2210
2186
|
def complete( # type: ignore[override]
|
2211
2187
|
self, text: str, state: int, custom_settings: Optional[utils.CustomCompletionSettings] = None
|
2212
2188
|
) -> Optional[str]:
|
2213
|
-
"""Override of cmd's complete method which returns the next possible completion for 'text'
|
2189
|
+
"""Override of cmd's complete method which returns the next possible completion for 'text'.
|
2214
2190
|
|
2215
2191
|
This completer function is called by readline as complete(text, state), for state in 0, 1, 2, …,
|
2216
2192
|
until it returns a non-string value. It should return the next possible completion starting with text.
|
@@ -2304,7 +2280,7 @@ class Cmd(cmd.Cmd):
|
|
2304
2280
|
ansi.style_aware_write(sys.stdout, '\n' + err_str + '\n')
|
2305
2281
|
rl_force_redisplay()
|
2306
2282
|
return None
|
2307
|
-
except Exception as ex:
|
2283
|
+
except Exception as ex: # noqa: BLE001
|
2308
2284
|
# Insert a newline so the exception doesn't print in the middle of the command line being tab completed
|
2309
2285
|
self.perror()
|
2310
2286
|
self.pexcept(ex)
|
@@ -2312,32 +2288,32 @@ class Cmd(cmd.Cmd):
|
|
2312
2288
|
return None
|
2313
2289
|
|
2314
2290
|
def in_script(self) -> bool:
|
2315
|
-
"""Return whether a text script is running"""
|
2291
|
+
"""Return whether a text script is running."""
|
2316
2292
|
return self._current_script_dir is not None
|
2317
2293
|
|
2318
2294
|
def in_pyscript(self) -> bool:
|
2319
|
-
"""Return whether running inside a Python shell or pyscript"""
|
2295
|
+
"""Return whether running inside a Python shell or pyscript."""
|
2320
2296
|
return self._in_py
|
2321
2297
|
|
2322
2298
|
@property
|
2323
|
-
def aliases(self) ->
|
2324
|
-
"""Read-only property to access the aliases stored in the StatementParser"""
|
2299
|
+
def aliases(self) -> dict[str, str]:
|
2300
|
+
"""Read-only property to access the aliases stored in the StatementParser."""
|
2325
2301
|
return self.statement_parser.aliases
|
2326
2302
|
|
2327
|
-
def get_names(self) ->
|
2303
|
+
def get_names(self) -> list[str]:
|
2328
2304
|
"""Return an alphabetized list of names comprising the attributes of the cmd2 class instance."""
|
2329
2305
|
return dir(self)
|
2330
2306
|
|
2331
|
-
def get_all_commands(self) ->
|
2332
|
-
"""Return a list of all commands"""
|
2307
|
+
def get_all_commands(self) -> list[str]:
|
2308
|
+
"""Return a list of all commands."""
|
2333
2309
|
return [
|
2334
2310
|
name[len(constants.COMMAND_FUNC_PREFIX) :]
|
2335
2311
|
for name in self.get_names()
|
2336
2312
|
if name.startswith(constants.COMMAND_FUNC_PREFIX) and callable(getattr(self, name))
|
2337
2313
|
]
|
2338
2314
|
|
2339
|
-
def get_visible_commands(self) ->
|
2340
|
-
"""Return a list of commands that have not been hidden or disabled"""
|
2315
|
+
def get_visible_commands(self) -> list[str]:
|
2316
|
+
"""Return a list of commands that have not been hidden or disabled."""
|
2341
2317
|
return [
|
2342
2318
|
command
|
2343
2319
|
for command in self.get_all_commands()
|
@@ -2347,9 +2323,9 @@ class Cmd(cmd.Cmd):
|
|
2347
2323
|
# Table displayed when tab completing aliases
|
2348
2324
|
_alias_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None)
|
2349
2325
|
|
2350
|
-
def _get_alias_completion_items(self) ->
|
2351
|
-
"""Return list of alias names and values as CompletionItems"""
|
2352
|
-
results:
|
2326
|
+
def _get_alias_completion_items(self) -> list[CompletionItem]:
|
2327
|
+
"""Return list of alias names and values as CompletionItems."""
|
2328
|
+
results: list[CompletionItem] = []
|
2353
2329
|
|
2354
2330
|
for cur_key in self.aliases:
|
2355
2331
|
row_data = [self.aliases[cur_key]]
|
@@ -2360,9 +2336,9 @@ class Cmd(cmd.Cmd):
|
|
2360
2336
|
# Table displayed when tab completing macros
|
2361
2337
|
_macro_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None)
|
2362
2338
|
|
2363
|
-
def _get_macro_completion_items(self) ->
|
2364
|
-
"""Return list of macro names and values as CompletionItems"""
|
2365
|
-
results:
|
2339
|
+
def _get_macro_completion_items(self) -> list[CompletionItem]:
|
2340
|
+
"""Return list of macro names and values as CompletionItems."""
|
2341
|
+
results: list[CompletionItem] = []
|
2366
2342
|
|
2367
2343
|
for cur_key in self.macros:
|
2368
2344
|
row_data = [self.macros[cur_key].value]
|
@@ -2373,9 +2349,9 @@ class Cmd(cmd.Cmd):
|
|
2373
2349
|
# Table displayed when tab completing Settables
|
2374
2350
|
_settable_completion_table = SimpleTable([Column('Value', width=30), Column('Description', width=60)], divider_char=None)
|
2375
2351
|
|
2376
|
-
def _get_settable_completion_items(self) ->
|
2377
|
-
"""Return list of Settable names, values, and descriptions as CompletionItems"""
|
2378
|
-
results:
|
2352
|
+
def _get_settable_completion_items(self) -> list[CompletionItem]:
|
2353
|
+
"""Return list of Settable names, values, and descriptions as CompletionItems."""
|
2354
|
+
results: list[CompletionItem] = []
|
2379
2355
|
|
2380
2356
|
for cur_key in self.settables:
|
2381
2357
|
row_data = [self.settables[cur_key].get_value(), self.settables[cur_key].description]
|
@@ -2383,15 +2359,15 @@ class Cmd(cmd.Cmd):
|
|
2383
2359
|
|
2384
2360
|
return results
|
2385
2361
|
|
2386
|
-
def _get_commands_aliases_and_macros_for_completion(self) ->
|
2387
|
-
"""Return a list of visible commands, aliases, and macros for tab completion"""
|
2362
|
+
def _get_commands_aliases_and_macros_for_completion(self) -> list[str]:
|
2363
|
+
"""Return a list of visible commands, aliases, and macros for tab completion."""
|
2388
2364
|
visible_commands = set(self.get_visible_commands())
|
2389
2365
|
alias_names = set(self.aliases)
|
2390
2366
|
macro_names = set(self.macros)
|
2391
2367
|
return list(visible_commands | alias_names | macro_names)
|
2392
2368
|
|
2393
|
-
def get_help_topics(self) ->
|
2394
|
-
"""Return a list of help topics"""
|
2369
|
+
def get_help_topics(self) -> list[str]:
|
2370
|
+
"""Return a list of help topics."""
|
2395
2371
|
all_topics = [
|
2396
2372
|
name[len(constants.HELP_FUNC_PREFIX) :]
|
2397
2373
|
for name in self.get_names()
|
@@ -2401,7 +2377,7 @@ class Cmd(cmd.Cmd):
|
|
2401
2377
|
# Filter out hidden and disabled commands
|
2402
2378
|
return [topic for topic in all_topics if topic not in self.hidden_commands and topic not in self.disabled_commands]
|
2403
2379
|
|
2404
|
-
def sigint_handler(self, signum: int, _: Optional[FrameType]) -> None:
|
2380
|
+
def sigint_handler(self, signum: int, _: Optional[FrameType]) -> None: # noqa: ARG002
|
2405
2381
|
"""Signal handler for SIGINTs which typically come from Ctrl-C events.
|
2406
2382
|
|
2407
2383
|
If you need custom SIGINT behavior, then override this method.
|
@@ -2424,8 +2400,7 @@ class Cmd(cmd.Cmd):
|
|
2424
2400
|
self._raise_keyboard_interrupt()
|
2425
2401
|
|
2426
2402
|
def termination_signal_handler(self, signum: int, _: Optional[FrameType]) -> None:
|
2427
|
-
"""
|
2428
|
-
Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac.
|
2403
|
+
"""Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac.
|
2429
2404
|
|
2430
2405
|
SIGHUP - received when terminal window is closed
|
2431
2406
|
SIGTERM - received when this app has been requested to terminate
|
@@ -2441,56 +2416,50 @@ class Cmd(cmd.Cmd):
|
|
2441
2416
|
sys.exit(128 + signum)
|
2442
2417
|
|
2443
2418
|
def _raise_keyboard_interrupt(self) -> None:
|
2444
|
-
"""
|
2419
|
+
"""Raise a KeyboardInterrupt."""
|
2445
2420
|
raise KeyboardInterrupt("Got a keyboard interrupt")
|
2446
2421
|
|
2447
2422
|
def precmd(self, statement: Union[Statement, str]) -> Statement:
|
2448
|
-
"""
|
2449
|
-
[cmd2.Cmd.onecmd][] and after adding it to history.
|
2423
|
+
"""Ran just before the command is executed by [cmd2.Cmd.onecmd][] and after adding it to history (cmd Hook method).
|
2450
2424
|
|
2451
2425
|
:param statement: subclass of str which also contains the parsed input
|
2452
2426
|
:return: a potentially modified version of the input Statement object
|
2453
2427
|
|
2454
|
-
See [cmd2.Cmd.register_postparsing_hook][] and
|
2455
|
-
|
2456
|
-
to run hooks before the command is executed. See
|
2457
|
-
[Hooks](../features/hooks.md) for more information.
|
2428
|
+
See [cmd2.Cmd.register_postparsing_hook][] and [cmd2.Cmd.register_precmd_hook][] for more robust ways
|
2429
|
+
to run hooks before the command is executed. See [Hooks](../features/hooks.md) for more information.
|
2458
2430
|
"""
|
2459
2431
|
return Statement(statement) if not isinstance(statement, Statement) else statement
|
2460
2432
|
|
2461
|
-
def postcmd(self, stop: bool, statement: Union[Statement, str]) -> bool:
|
2462
|
-
"""
|
2463
|
-
[cmd2.Cmd.onecmd][].
|
2433
|
+
def postcmd(self, stop: bool, statement: Union[Statement, str]) -> bool: # noqa: ARG002
|
2434
|
+
"""Ran just after a command is executed by [cmd2.Cmd.onecmd][] (cmd inherited Hook method).
|
2464
2435
|
|
2465
2436
|
:param stop: return `True` to request the command loop terminate
|
2466
2437
|
:param statement: subclass of str which also contains the parsed input
|
2467
2438
|
|
2468
2439
|
See [cmd2.Cmd.register_postcmd_hook][] and [cmd2.Cmd.register_cmdfinalization_hook][] for more robust ways
|
2469
|
-
to run hooks after the command is executed. See
|
2470
|
-
[Hooks](../features/hooks.md) for more information.
|
2440
|
+
to run hooks after the command is executed. See [Hooks](../features/hooks.md) for more information.
|
2471
2441
|
"""
|
2472
2442
|
return stop
|
2473
2443
|
|
2474
2444
|
def preloop(self) -> None:
|
2475
|
-
"""
|
2476
|
-
|
2445
|
+
"""Ran once when the [cmd2.Cmd.cmdloop][] method is called (cmd inherited Hook method).
|
2446
|
+
|
2447
|
+
This method is a stub that does nothing and exists to be overridden by subclasses.
|
2477
2448
|
|
2478
|
-
See [cmd2.Cmd.register_preloop_hook][] for a more robust
|
2479
|
-
|
2480
|
-
[Hooks](../features/hooks.md) for more information.
|
2449
|
+
See [cmd2.Cmd.register_preloop_hook][] for a more robust wayto run hooks before the command loop begins.
|
2450
|
+
See [Hooks](../features/hooks.md) for more information.
|
2481
2451
|
"""
|
2482
|
-
pass
|
2483
2452
|
|
2484
2453
|
def postloop(self) -> None:
|
2485
|
-
"""
|
2454
|
+
"""Ran once when the [cmd2.Cmd.cmdloop][] method is about to return (cmd inherited Hook Method).
|
2486
2455
|
|
2487
|
-
|
2488
|
-
|
2489
|
-
[
|
2456
|
+
This method is a stub that does nothing and exists to be overridden by subclasses.
|
2457
|
+
|
2458
|
+
See [cmd2.Cmd.register_postloop_hook][] for a more robust way to run hooks after the command loop completes.
|
2459
|
+
See [Hooks](../features/hooks.md) for more information.
|
2490
2460
|
"""
|
2491
|
-
pass
|
2492
2461
|
|
2493
|
-
def parseline(self, line: str) ->
|
2462
|
+
def parseline(self, line: str) -> tuple[str, str, str]:
|
2494
2463
|
"""Parse the line into a command name and a string containing the arguments.
|
2495
2464
|
|
2496
2465
|
NOTE: This is an override of a parent class method. It is only used by other parent class methods.
|
@@ -2549,7 +2518,7 @@ class Cmd(cmd.Cmd):
|
|
2549
2518
|
if stop:
|
2550
2519
|
# we should not run the command, but
|
2551
2520
|
# we need to run the finalization hooks
|
2552
|
-
raise EmptyStatement
|
2521
|
+
raise EmptyStatement # noqa: TRY301
|
2553
2522
|
|
2554
2523
|
redir_saved_state: Optional[utils.RedirectionSavedState] = None
|
2555
2524
|
|
@@ -2562,7 +2531,7 @@ class Cmd(cmd.Cmd):
|
|
2562
2531
|
|
2563
2532
|
redir_saved_state = self._redirect_output(statement)
|
2564
2533
|
|
2565
|
-
timestart = datetime.datetime.now()
|
2534
|
+
timestart = datetime.datetime.now(tz=datetime.timezone.utc)
|
2566
2535
|
|
2567
2536
|
# precommand hooks
|
2568
2537
|
precmd_data = plugin.PrecommandData(statement)
|
@@ -2588,7 +2557,7 @@ class Cmd(cmd.Cmd):
|
|
2588
2557
|
stop = self.postcmd(stop, statement)
|
2589
2558
|
|
2590
2559
|
if self.timing:
|
2591
|
-
self.pfeedback(f'Elapsed: {datetime.datetime.now() - timestart}')
|
2560
|
+
self.pfeedback(f'Elapsed: {datetime.datetime.now(tz=datetime.timezone.utc) - timestart}')
|
2592
2561
|
finally:
|
2593
2562
|
# Get sigint protection while we restore stuff
|
2594
2563
|
with self.sigint_protection:
|
@@ -2605,43 +2574,43 @@ class Cmd(cmd.Cmd):
|
|
2605
2574
|
self.perror(f"Invalid syntax: {ex}")
|
2606
2575
|
except RedirectionError as ex:
|
2607
2576
|
self.perror(ex)
|
2608
|
-
except KeyboardInterrupt
|
2577
|
+
except KeyboardInterrupt:
|
2609
2578
|
if raise_keyboard_interrupt and not stop:
|
2610
|
-
raise
|
2579
|
+
raise
|
2611
2580
|
except SystemExit as ex:
|
2612
2581
|
if isinstance(ex.code, int):
|
2613
2582
|
self.exit_code = ex.code
|
2614
2583
|
stop = True
|
2615
2584
|
except PassThroughException as ex:
|
2616
|
-
raise ex.wrapped_ex
|
2617
|
-
except Exception as ex:
|
2585
|
+
raise ex.wrapped_ex from None
|
2586
|
+
except Exception as ex: # noqa: BLE001
|
2618
2587
|
self.pexcept(ex)
|
2619
2588
|
finally:
|
2620
2589
|
try:
|
2621
2590
|
stop = self._run_cmdfinalization_hooks(stop, statement)
|
2622
|
-
except KeyboardInterrupt
|
2591
|
+
except KeyboardInterrupt:
|
2623
2592
|
if raise_keyboard_interrupt and not stop:
|
2624
|
-
raise
|
2593
|
+
raise
|
2625
2594
|
except SystemExit as ex:
|
2626
2595
|
if isinstance(ex.code, int):
|
2627
2596
|
self.exit_code = ex.code
|
2628
2597
|
stop = True
|
2629
2598
|
except PassThroughException as ex:
|
2630
|
-
raise ex.wrapped_ex
|
2631
|
-
except Exception as ex:
|
2599
|
+
raise ex.wrapped_ex from None
|
2600
|
+
except Exception as ex: # noqa: BLE001
|
2632
2601
|
self.pexcept(ex)
|
2633
2602
|
|
2634
2603
|
return stop
|
2635
2604
|
|
2636
2605
|
def _run_cmdfinalization_hooks(self, stop: bool, statement: Optional[Statement]) -> bool:
|
2637
|
-
"""Run the command finalization hooks"""
|
2606
|
+
"""Run the command finalization hooks."""
|
2638
2607
|
with self.sigint_protection:
|
2639
2608
|
if not sys.platform.startswith('win') and self.stdin.isatty():
|
2640
2609
|
# Before the next command runs, fix any terminal problems like those
|
2641
2610
|
# caused by certain binary characters having been printed to it.
|
2642
2611
|
import subprocess
|
2643
2612
|
|
2644
|
-
proc = subprocess.Popen(['stty', 'sane'])
|
2613
|
+
proc = subprocess.Popen(['stty', 'sane']) # noqa: S603, S607
|
2645
2614
|
proc.communicate()
|
2646
2615
|
|
2647
2616
|
data = plugin.CommandFinalizationData(stop, statement)
|
@@ -2653,13 +2622,13 @@ class Cmd(cmd.Cmd):
|
|
2653
2622
|
|
2654
2623
|
def runcmds_plus_hooks(
|
2655
2624
|
self,
|
2656
|
-
cmds: Union[
|
2625
|
+
cmds: Union[list[HistoryItem], list[str]],
|
2657
2626
|
*,
|
2658
2627
|
add_to_history: bool = True,
|
2659
2628
|
stop_on_keyboard_interrupt: bool = False,
|
2660
2629
|
) -> bool:
|
2661
|
-
"""
|
2662
|
-
|
2630
|
+
"""Run commands in an automated fashion from sources like text scripts or history replays.
|
2631
|
+
|
2663
2632
|
The prompt and command line for each command will be printed if echo is True.
|
2664
2633
|
|
2665
2634
|
:param cmds: commands to run
|
@@ -2671,7 +2640,7 @@ class Cmd(cmd.Cmd):
|
|
2671
2640
|
"""
|
2672
2641
|
for line in cmds:
|
2673
2642
|
if isinstance(line, HistoryItem):
|
2674
|
-
line = line.raw
|
2643
|
+
line = line.raw # noqa: PLW2901
|
2675
2644
|
|
2676
2645
|
if self.echo:
|
2677
2646
|
self.poutput(f'{self.prompt}{line}')
|
@@ -2706,7 +2675,7 @@ class Cmd(cmd.Cmd):
|
|
2706
2675
|
"""
|
2707
2676
|
|
2708
2677
|
def combine_rl_history(statement: Statement) -> None:
|
2709
|
-
"""Combine all lines of a multiline command into a single readline history entry"""
|
2678
|
+
"""Combine all lines of a multiline command into a single readline history entry."""
|
2710
2679
|
if orig_rl_history_length is None or not statement.multiline_command:
|
2711
2680
|
return
|
2712
2681
|
|
@@ -2773,15 +2742,13 @@ class Cmd(cmd.Cmd):
|
|
2773
2742
|
|
2774
2743
|
if not statement.command:
|
2775
2744
|
raise EmptyStatement
|
2776
|
-
|
2777
|
-
|
2778
|
-
combine_rl_history(statement)
|
2745
|
+
# If necessary, update history with completed multiline command.
|
2746
|
+
combine_rl_history(statement)
|
2779
2747
|
|
2780
2748
|
return statement
|
2781
2749
|
|
2782
2750
|
def _input_line_to_statement(self, line: str, *, orig_rl_history_length: Optional[int] = None) -> Statement:
|
2783
|
-
"""
|
2784
|
-
Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
|
2751
|
+
"""Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved.
|
2785
2752
|
|
2786
2753
|
:param line: the line being parsed
|
2787
2754
|
:param orig_rl_history_length: Optional length of the readline history before the current command was typed.
|
@@ -2806,7 +2773,7 @@ class Cmd(cmd.Cmd):
|
|
2806
2773
|
orig_rl_history_length = None
|
2807
2774
|
|
2808
2775
|
# Check if this command matches a macro and wasn't already processed to avoid an infinite loop
|
2809
|
-
if statement.command in self.macros
|
2776
|
+
if statement.command in self.macros and statement.command not in used_macros:
|
2810
2777
|
used_macros.append(statement.command)
|
2811
2778
|
resolve_result = self._resolve_macro(statement)
|
2812
2779
|
if resolve_result is None:
|
@@ -2834,13 +2801,12 @@ class Cmd(cmd.Cmd):
|
|
2834
2801
|
return statement
|
2835
2802
|
|
2836
2803
|
def _resolve_macro(self, statement: Statement) -> Optional[str]:
|
2837
|
-
"""
|
2838
|
-
Resolve a macro and return the resulting string
|
2804
|
+
"""Resolve a macro and return the resulting string.
|
2839
2805
|
|
2840
2806
|
:param statement: the parsed statement from the command line
|
2841
2807
|
:return: the resolved macro or None on error
|
2842
2808
|
"""
|
2843
|
-
if statement.command not in self.macros
|
2809
|
+
if statement.command not in self.macros:
|
2844
2810
|
raise KeyError(f"{statement.command} is not a macro")
|
2845
2811
|
|
2846
2812
|
macro = self.macros[statement.command]
|
@@ -2881,7 +2847,6 @@ class Cmd(cmd.Cmd):
|
|
2881
2847
|
:return: A bool telling if an error occurred and a utils.RedirectionSavedState object
|
2882
2848
|
:raises RedirectionError: if an error occurs trying to pipe or redirect
|
2883
2849
|
"""
|
2884
|
-
import io
|
2885
2850
|
import subprocess
|
2886
2851
|
|
2887
2852
|
# Initialize the redirection saved state
|
@@ -2901,13 +2866,13 @@ class Cmd(cmd.Cmd):
|
|
2901
2866
|
read_fd, write_fd = os.pipe()
|
2902
2867
|
|
2903
2868
|
# Open each side of the pipe
|
2904
|
-
subproc_stdin =
|
2905
|
-
new_stdout: TextIO = cast(TextIO,
|
2869
|
+
subproc_stdin = open(read_fd) # noqa: SIM115
|
2870
|
+
new_stdout: TextIO = cast(TextIO, open(write_fd, 'w')) # noqa: SIM115
|
2906
2871
|
|
2907
2872
|
# Create pipe process in a separate group to isolate our signals from it. If a Ctrl-C event occurs,
|
2908
2873
|
# our sigint handler will forward it only to the most recent pipe process. This makes sure pipe
|
2909
2874
|
# processes close in the right order (most recent first).
|
2910
|
-
kwargs:
|
2875
|
+
kwargs: dict[str, Any] = {}
|
2911
2876
|
if sys.platform == 'win32':
|
2912
2877
|
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
|
2913
2878
|
else:
|
@@ -2919,7 +2884,7 @@ class Cmd(cmd.Cmd):
|
|
2919
2884
|
kwargs['executable'] = shell
|
2920
2885
|
|
2921
2886
|
# For any stream that is a StdSim, we will use a pipe so we can capture its output
|
2922
|
-
proc = subprocess.Popen( # type: ignore[call-overload]
|
2887
|
+
proc = subprocess.Popen( # type: ignore[call-overload] # noqa: S602
|
2923
2888
|
statement.pipe_to,
|
2924
2889
|
stdin=subproc_stdin,
|
2925
2890
|
stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, # type: ignore[unreachable]
|
@@ -2932,20 +2897,17 @@ class Cmd(cmd.Cmd):
|
|
2932
2897
|
# like: !ls -l | grep user | wc -l > out.txt. But this makes it difficult to know if the pipe process
|
2933
2898
|
# started OK, since the shell itself always starts. Therefore, we will wait a short time and check
|
2934
2899
|
# if the pipe process is still running.
|
2935
|
-
|
2900
|
+
with contextlib.suppress(subprocess.TimeoutExpired):
|
2936
2901
|
proc.wait(0.2)
|
2937
|
-
except subprocess.TimeoutExpired:
|
2938
|
-
pass
|
2939
2902
|
|
2940
2903
|
# Check if the pipe process already exited
|
2941
2904
|
if proc.returncode is not None:
|
2942
2905
|
subproc_stdin.close()
|
2943
2906
|
new_stdout.close()
|
2944
2907
|
raise RedirectionError(f'Pipe process exited with code {proc.returncode} before command could run')
|
2945
|
-
|
2946
|
-
|
2947
|
-
|
2948
|
-
sys.stdout = self.stdout = new_stdout
|
2908
|
+
redir_saved_state.redirecting = True # type: ignore[unreachable]
|
2909
|
+
cmd_pipe_proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr)
|
2910
|
+
sys.stdout = self.stdout = new_stdout
|
2949
2911
|
|
2950
2912
|
elif statement.output:
|
2951
2913
|
if statement.output_to:
|
@@ -2954,9 +2916,9 @@ class Cmd(cmd.Cmd):
|
|
2954
2916
|
mode = 'a' if statement.output == constants.REDIRECTION_APPEND else 'w'
|
2955
2917
|
try:
|
2956
2918
|
# Use line buffering
|
2957
|
-
new_stdout = cast(TextIO, open(utils.strip_quotes(statement.output_to), mode=mode, buffering=1))
|
2919
|
+
new_stdout = cast(TextIO, open(utils.strip_quotes(statement.output_to), mode=mode, buffering=1)) # noqa: SIM115
|
2958
2920
|
except OSError as ex:
|
2959
|
-
raise RedirectionError(
|
2921
|
+
raise RedirectionError('Failed to redirect output') from ex
|
2960
2922
|
|
2961
2923
|
redir_saved_state.redirecting = True
|
2962
2924
|
sys.stdout = self.stdout = new_stdout
|
@@ -2975,7 +2937,7 @@ class Cmd(cmd.Cmd):
|
|
2975
2937
|
# no point opening up the temporary file
|
2976
2938
|
current_paste_buffer = get_paste_buffer()
|
2977
2939
|
# create a temporary file to store output
|
2978
|
-
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+"))
|
2940
|
+
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+")) # noqa: SIM115
|
2979
2941
|
redir_saved_state.redirecting = True
|
2980
2942
|
sys.stdout = self.stdout = new_stdout
|
2981
2943
|
|
@@ -2990,7 +2952,7 @@ class Cmd(cmd.Cmd):
|
|
2990
2952
|
return redir_saved_state
|
2991
2953
|
|
2992
2954
|
def _restore_output(self, statement: Statement, saved_redir_state: utils.RedirectionSavedState) -> None:
|
2993
|
-
"""
|
2955
|
+
"""Handle restoring state after output redirection.
|
2994
2956
|
|
2995
2957
|
:param statement: Statement object which contains the parsed input from the user
|
2996
2958
|
:param saved_redir_state: contains information needed to restore state data
|
@@ -3001,11 +2963,9 @@ class Cmd(cmd.Cmd):
|
|
3001
2963
|
self.stdout.seek(0)
|
3002
2964
|
write_to_paste_buffer(self.stdout.read())
|
3003
2965
|
|
3004
|
-
|
2966
|
+
with contextlib.suppress(BrokenPipeError):
|
3005
2967
|
# Close the file or pipe that stdout was redirected to
|
3006
2968
|
self.stdout.close()
|
3007
|
-
except BrokenPipeError:
|
3008
|
-
pass
|
3009
2969
|
|
3010
2970
|
# Restore the stdout values
|
3011
2971
|
self.stdout = cast(TextIO, saved_redir_state.saved_self_stdout)
|
@@ -3020,25 +2980,24 @@ class Cmd(cmd.Cmd):
|
|
3020
2980
|
self._redirecting = saved_redir_state.saved_redirecting
|
3021
2981
|
|
3022
2982
|
def cmd_func(self, command: str) -> Optional[CommandFunc]:
|
3023
|
-
"""
|
3024
|
-
Get the function for a command
|
2983
|
+
"""Get the function for a command.
|
3025
2984
|
|
3026
2985
|
:param command: the name of the command
|
3027
2986
|
|
3028
2987
|
Example:
|
3029
|
-
|
3030
2988
|
```py
|
3031
2989
|
helpfunc = self.cmd_func('help')
|
3032
2990
|
```
|
3033
2991
|
|
3034
2992
|
helpfunc now contains a reference to the ``do_help`` method
|
2993
|
+
|
3035
2994
|
"""
|
3036
2995
|
func_name = constants.COMMAND_FUNC_PREFIX + command
|
3037
2996
|
func = getattr(self, func_name, None)
|
3038
2997
|
return cast(CommandFunc, func) if callable(func) else None
|
3039
2998
|
|
3040
2999
|
def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = True) -> bool:
|
3041
|
-
"""
|
3000
|
+
"""Execute the actual do_* method for a command.
|
3042
3001
|
|
3043
3002
|
If the command provided doesn't exist, then it executes default() instead.
|
3044
3003
|
|
@@ -3073,7 +3032,7 @@ class Cmd(cmd.Cmd):
|
|
3073
3032
|
return stop if stop is not None else False
|
3074
3033
|
|
3075
3034
|
def default(self, statement: Statement) -> Optional[bool]: # type: ignore[override]
|
3076
|
-
"""
|
3035
|
+
"""Execute when the command given isn't a recognized command implemented by a do_* method.
|
3077
3036
|
|
3078
3037
|
:param statement: Statement object with parsed input
|
3079
3038
|
"""
|
@@ -3082,14 +3041,13 @@ class Cmd(cmd.Cmd):
|
|
3082
3041
|
self.history.append(statement)
|
3083
3042
|
|
3084
3043
|
return self.do_shell(statement.command_and_args)
|
3085
|
-
|
3086
|
-
|
3087
|
-
|
3088
|
-
err_msg += f"\n{self.default_suggestion_message.format(suggested_command)}"
|
3044
|
+
err_msg = self.default_error.format(statement.command)
|
3045
|
+
if self.suggest_similar_command and (suggested_command := self._suggest_similar_command(statement.command)):
|
3046
|
+
err_msg += f"\n{self.default_suggestion_message.format(suggested_command)}"
|
3089
3047
|
|
3090
|
-
|
3091
|
-
|
3092
|
-
|
3048
|
+
# Set apply_style to False so styles for default_error and default_suggestion_message are not overridden
|
3049
|
+
self.perror(err_msg, apply_style=False)
|
3050
|
+
return None
|
3093
3051
|
|
3094
3052
|
def _suggest_similar_command(self, command: str) -> Optional[str]:
|
3095
3053
|
return suggest_similar(command, self.get_visible_commands())
|
@@ -3098,7 +3056,7 @@ class Cmd(cmd.Cmd):
|
|
3098
3056
|
self,
|
3099
3057
|
prompt: str,
|
3100
3058
|
*,
|
3101
|
-
history: Optional[
|
3059
|
+
history: Optional[list[str]] = None,
|
3102
3060
|
completion_mode: utils.CompletionMode = utils.CompletionMode.NONE,
|
3103
3061
|
preserve_quotes: bool = False,
|
3104
3062
|
choices: Optional[Iterable[Any]] = None,
|
@@ -3106,9 +3064,9 @@ class Cmd(cmd.Cmd):
|
|
3106
3064
|
completer: Optional[CompleterFunc] = None,
|
3107
3065
|
parser: Optional[argparse.ArgumentParser] = None,
|
3108
3066
|
) -> str:
|
3109
|
-
"""
|
3110
|
-
|
3111
|
-
input is being entered.
|
3067
|
+
"""Read input from appropriate stdin value.
|
3068
|
+
|
3069
|
+
Also supports tab completion and up-arrow history while input is being entered.
|
3112
3070
|
|
3113
3071
|
:param prompt: prompt to display to user
|
3114
3072
|
:param history: optional list of strings to use for up-arrow history. If completion_mode is
|
@@ -3139,10 +3097,10 @@ class Cmd(cmd.Cmd):
|
|
3139
3097
|
"""
|
3140
3098
|
readline_configured = False
|
3141
3099
|
saved_completer: Optional[CompleterFunc] = None
|
3142
|
-
saved_history: Optional[
|
3100
|
+
saved_history: Optional[list[str]] = None
|
3143
3101
|
|
3144
3102
|
def configure_readline() -> None:
|
3145
|
-
"""Configure readline tab completion and history"""
|
3103
|
+
"""Configure readline tab completion and history."""
|
3146
3104
|
nonlocal readline_configured
|
3147
3105
|
nonlocal saved_completer
|
3148
3106
|
nonlocal saved_history
|
@@ -3158,7 +3116,7 @@ class Cmd(cmd.Cmd):
|
|
3158
3116
|
# Disable completion
|
3159
3117
|
if completion_mode == utils.CompletionMode.NONE:
|
3160
3118
|
|
3161
|
-
def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover
|
3119
|
+
def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover # noqa: ARG001
|
3162
3120
|
return None
|
3163
3121
|
|
3164
3122
|
complete_func = complete_none
|
@@ -3198,7 +3156,7 @@ class Cmd(cmd.Cmd):
|
|
3198
3156
|
readline_configured = True
|
3199
3157
|
|
3200
3158
|
def restore_readline() -> None:
|
3201
|
-
"""Restore readline tab completion and history"""
|
3159
|
+
"""Restore readline tab completion and history."""
|
3202
3160
|
nonlocal readline_configured
|
3203
3161
|
if not readline_configured or rl_type == RlType.NONE: # pragma: no cover
|
3204
3162
|
return
|
@@ -3232,31 +3190,29 @@ class Cmd(cmd.Cmd):
|
|
3232
3190
|
sys.stdout.write(f'{prompt}{line}\n')
|
3233
3191
|
|
3234
3192
|
# Otherwise read from self.stdin
|
3193
|
+
elif self.stdin.isatty():
|
3194
|
+
# on a tty, print the prompt first, then read the line
|
3195
|
+
self.poutput(prompt, end='')
|
3196
|
+
self.stdout.flush()
|
3197
|
+
line = self.stdin.readline()
|
3198
|
+
if len(line) == 0:
|
3199
|
+
line = 'eof'
|
3235
3200
|
else:
|
3236
|
-
if
|
3237
|
-
|
3238
|
-
|
3239
|
-
|
3240
|
-
|
3241
|
-
|
3242
|
-
|
3201
|
+
# we are reading from a pipe, read the line to see if there is
|
3202
|
+
# anything there, if so, then decide whether to print the
|
3203
|
+
# prompt or not
|
3204
|
+
line = self.stdin.readline()
|
3205
|
+
if len(line):
|
3206
|
+
# we read something, output the prompt and the something
|
3207
|
+
if self.echo:
|
3208
|
+
self.poutput(f'{prompt}{line}')
|
3243
3209
|
else:
|
3244
|
-
|
3245
|
-
# anything there, if so, then decide whether to print the
|
3246
|
-
# prompt or not
|
3247
|
-
line = self.stdin.readline()
|
3248
|
-
if len(line):
|
3249
|
-
# we read something, output the prompt and the something
|
3250
|
-
if self.echo:
|
3251
|
-
self.poutput(f'{prompt}{line}')
|
3252
|
-
else:
|
3253
|
-
line = 'eof'
|
3210
|
+
line = 'eof'
|
3254
3211
|
|
3255
3212
|
return line.rstrip('\r\n')
|
3256
3213
|
|
3257
3214
|
def _read_command_line(self, prompt: str) -> str:
|
3258
|
-
"""
|
3259
|
-
Read command line from appropriate stdin
|
3215
|
+
"""Read command line from appropriate stdin.
|
3260
3216
|
|
3261
3217
|
:param prompt: prompt to display to user
|
3262
3218
|
:return: command line text of 'eof' if an EOFError was caught
|
@@ -3264,11 +3220,9 @@ class Cmd(cmd.Cmd):
|
|
3264
3220
|
"""
|
3265
3221
|
try:
|
3266
3222
|
# Wrap in try since terminal_lock may not be locked
|
3267
|
-
|
3223
|
+
with contextlib.suppress(RuntimeError):
|
3268
3224
|
# Command line is about to be drawn. Allow asynchronous changes to the terminal.
|
3269
3225
|
self.terminal_lock.release()
|
3270
|
-
except RuntimeError:
|
3271
|
-
pass
|
3272
3226
|
return self.read_input(prompt, completion_mode=utils.CompletionMode.COMMANDS)
|
3273
3227
|
except EOFError:
|
3274
3228
|
return 'eof'
|
@@ -3277,8 +3231,7 @@ class Cmd(cmd.Cmd):
|
|
3277
3231
|
self.terminal_lock.acquire()
|
3278
3232
|
|
3279
3233
|
def _set_up_cmd2_readline(self) -> _SavedReadlineSettings:
|
3280
|
-
"""
|
3281
|
-
Called at beginning of command loop to set up readline with cmd2-specific settings
|
3234
|
+
"""Set up readline with cmd2-specific settings, called at beginning of command loop.
|
3282
3235
|
|
3283
3236
|
:return: Class containing saved readline settings
|
3284
3237
|
"""
|
@@ -3318,8 +3271,7 @@ class Cmd(cmd.Cmd):
|
|
3318
3271
|
return readline_settings
|
3319
3272
|
|
3320
3273
|
def _restore_readline(self, readline_settings: _SavedReadlineSettings) -> None:
|
3321
|
-
"""
|
3322
|
-
Called at end of command loop to restore saved readline settings
|
3274
|
+
"""Restore saved readline settings, called at end of command loop.
|
3323
3275
|
|
3324
3276
|
:param readline_settings: the readline settings to restore
|
3325
3277
|
"""
|
@@ -3335,8 +3287,9 @@ class Cmd(cmd.Cmd):
|
|
3335
3287
|
readline.rl.mode._display_completions = orig_pyreadline_display
|
3336
3288
|
|
3337
3289
|
def _cmdloop(self) -> None:
|
3338
|
-
"""Repeatedly issue a prompt, accept input, parse
|
3339
|
-
|
3290
|
+
"""Repeatedly issue a prompt, accept input, parse it, and dispatch to apporpriate commands.
|
3291
|
+
|
3292
|
+
Parse an initial prefix off the received input and dispatch to action methods, passing them
|
3340
3293
|
the remainder of the line as argument.
|
3341
3294
|
|
3342
3295
|
This serves the same role as cmd.cmdloop().
|
@@ -3388,7 +3341,7 @@ class Cmd(cmd.Cmd):
|
|
3388
3341
|
# Preserve quotes since we are passing strings to other commands
|
3389
3342
|
@with_argparser(alias_parser, preserve_quotes=True)
|
3390
3343
|
def do_alias(self, args: argparse.Namespace) -> None:
|
3391
|
-
"""Manage aliases"""
|
3344
|
+
"""Manage aliases."""
|
3392
3345
|
# Call handler for whatever subcommand was selected
|
3393
3346
|
handler = args.cmd2_handler.get()
|
3394
3347
|
handler(args)
|
@@ -3423,7 +3376,7 @@ class Cmd(cmd.Cmd):
|
|
3423
3376
|
|
3424
3377
|
@as_subcommand_to('alias', 'create', alias_create_parser, help=alias_create_description.lower())
|
3425
3378
|
def _alias_create(self, args: argparse.Namespace) -> None:
|
3426
|
-
"""Create or overwrite an alias"""
|
3379
|
+
"""Create or overwrite an alias."""
|
3427
3380
|
self.last_result = False
|
3428
3381
|
|
3429
3382
|
# Validate the alias name
|
@@ -3473,7 +3426,7 @@ class Cmd(cmd.Cmd):
|
|
3473
3426
|
|
3474
3427
|
@as_subcommand_to('alias', 'delete', alias_delete_parser, help=alias_delete_help)
|
3475
3428
|
def _alias_delete(self, args: argparse.Namespace) -> None:
|
3476
|
-
"""Delete aliases"""
|
3429
|
+
"""Delete aliases."""
|
3477
3430
|
self.last_result = True
|
3478
3431
|
|
3479
3432
|
if args.all:
|
@@ -3510,18 +3463,15 @@ class Cmd(cmd.Cmd):
|
|
3510
3463
|
|
3511
3464
|
@as_subcommand_to('alias', 'list', alias_list_parser, help=alias_list_help)
|
3512
3465
|
def _alias_list(self, args: argparse.Namespace) -> None:
|
3513
|
-
"""List some or all aliases as 'alias create' commands"""
|
3514
|
-
self.last_result = {} #
|
3466
|
+
"""List some or all aliases as 'alias create' commands."""
|
3467
|
+
self.last_result = {} # dict[alias_name, alias_value]
|
3515
3468
|
|
3516
3469
|
tokens_to_quote = constants.REDIRECTION_TOKENS
|
3517
3470
|
tokens_to_quote.extend(self.statement_parser.terminators)
|
3518
3471
|
|
3519
|
-
if args.names
|
3520
|
-
to_list = utils.remove_duplicates(args.names)
|
3521
|
-
else:
|
3522
|
-
to_list = sorted(self.aliases, key=self.default_sort_key)
|
3472
|
+
to_list = utils.remove_duplicates(args.names) if args.names else sorted(self.aliases, key=self.default_sort_key)
|
3523
3473
|
|
3524
|
-
not_found:
|
3474
|
+
not_found: list[str] = []
|
3525
3475
|
for name in to_list:
|
3526
3476
|
if name not in self.aliases:
|
3527
3477
|
not_found.append(name)
|
@@ -3556,7 +3506,7 @@ class Cmd(cmd.Cmd):
|
|
3556
3506
|
# Preserve quotes since we are passing strings to other commands
|
3557
3507
|
@with_argparser(macro_parser, preserve_quotes=True)
|
3558
3508
|
def do_macro(self, args: argparse.Namespace) -> None:
|
3559
|
-
"""Manage macros"""
|
3509
|
+
"""Manage macros."""
|
3560
3510
|
# Call handler for whatever subcommand was selected
|
3561
3511
|
handler = args.cmd2_handler.get()
|
3562
3512
|
handler(args)
|
@@ -3615,7 +3565,7 @@ class Cmd(cmd.Cmd):
|
|
3615
3565
|
|
3616
3566
|
@as_subcommand_to('macro', 'create', macro_create_parser, help=macro_create_help)
|
3617
3567
|
def _macro_create(self, args: argparse.Namespace) -> None:
|
3618
|
-
"""Create or overwrite a macro"""
|
3568
|
+
"""Create or overwrite a macro."""
|
3619
3569
|
self.last_result = False
|
3620
3570
|
|
3621
3571
|
# Validate the macro name
|
@@ -3648,8 +3598,8 @@ class Cmd(cmd.Cmd):
|
|
3648
3598
|
max_arg_num = 0
|
3649
3599
|
arg_nums = set()
|
3650
3600
|
|
3651
|
-
|
3652
|
-
|
3601
|
+
try:
|
3602
|
+
while True:
|
3653
3603
|
cur_match = normal_matches.__next__()
|
3654
3604
|
|
3655
3605
|
# Get the number string between the braces
|
@@ -3660,13 +3610,11 @@ class Cmd(cmd.Cmd):
|
|
3660
3610
|
return
|
3661
3611
|
|
3662
3612
|
arg_nums.add(cur_num)
|
3663
|
-
|
3664
|
-
max_arg_num = cur_num
|
3613
|
+
max_arg_num = max(max_arg_num, cur_num)
|
3665
3614
|
|
3666
3615
|
arg_list.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=False))
|
3667
|
-
|
3668
|
-
|
3669
|
-
break
|
3616
|
+
except StopIteration:
|
3617
|
+
pass
|
3670
3618
|
|
3671
3619
|
# Make sure the argument numbers are continuous
|
3672
3620
|
if len(arg_nums) != max_arg_num:
|
@@ -3676,16 +3624,16 @@ class Cmd(cmd.Cmd):
|
|
3676
3624
|
# Find all escaped arguments
|
3677
3625
|
escaped_matches = re.finditer(MacroArg.macro_escaped_arg_pattern, value)
|
3678
3626
|
|
3679
|
-
|
3680
|
-
|
3627
|
+
try:
|
3628
|
+
while True:
|
3681
3629
|
cur_match = escaped_matches.__next__()
|
3682
3630
|
|
3683
3631
|
# Get the number string between the braces
|
3684
3632
|
cur_num_str = re.findall(MacroArg.digit_pattern, cur_match.group())[0]
|
3685
3633
|
|
3686
3634
|
arg_list.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=True))
|
3687
|
-
|
3688
|
-
|
3635
|
+
except StopIteration:
|
3636
|
+
pass
|
3689
3637
|
|
3690
3638
|
# Set the macro
|
3691
3639
|
result = "overwritten" if args.name in self.macros else "created"
|
@@ -3709,7 +3657,7 @@ class Cmd(cmd.Cmd):
|
|
3709
3657
|
|
3710
3658
|
@as_subcommand_to('macro', 'delete', macro_delete_parser, help=macro_delete_help)
|
3711
3659
|
def _macro_delete(self, args: argparse.Namespace) -> None:
|
3712
|
-
"""Delete macros"""
|
3660
|
+
"""Delete macros."""
|
3713
3661
|
self.last_result = True
|
3714
3662
|
|
3715
3663
|
if args.all:
|
@@ -3746,18 +3694,15 @@ class Cmd(cmd.Cmd):
|
|
3746
3694
|
|
3747
3695
|
@as_subcommand_to('macro', 'list', macro_list_parser, help=macro_list_help)
|
3748
3696
|
def _macro_list(self, args: argparse.Namespace) -> None:
|
3749
|
-
"""List some or all macros as 'macro create' commands"""
|
3750
|
-
self.last_result = {} #
|
3697
|
+
"""List some or all macros as 'macro create' commands."""
|
3698
|
+
self.last_result = {} # dict[macro_name, macro_value]
|
3751
3699
|
|
3752
3700
|
tokens_to_quote = constants.REDIRECTION_TOKENS
|
3753
3701
|
tokens_to_quote.extend(self.statement_parser.terminators)
|
3754
3702
|
|
3755
|
-
if args.names
|
3756
|
-
to_list = utils.remove_duplicates(args.names)
|
3757
|
-
else:
|
3758
|
-
to_list = sorted(self.macros, key=self.default_sort_key)
|
3703
|
+
to_list = utils.remove_duplicates(args.names) if args.names else sorted(self.macros, key=self.default_sort_key)
|
3759
3704
|
|
3760
|
-
not_found:
|
3705
|
+
not_found: list[str] = []
|
3761
3706
|
for name in to_list:
|
3762
3707
|
if name not in self.macros:
|
3763
3708
|
not_found.append(name)
|
@@ -3779,9 +3724,8 @@ class Cmd(cmd.Cmd):
|
|
3779
3724
|
for name in not_found:
|
3780
3725
|
self.perror(f"Macro '{name}' not found")
|
3781
3726
|
|
3782
|
-
def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) ->
|
3783
|
-
"""Completes the command argument of help"""
|
3784
|
-
|
3727
|
+
def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
|
3728
|
+
"""Completes the command argument of help."""
|
3785
3729
|
# Complete token against topics and visible commands
|
3786
3730
|
topics = set(self.get_help_topics())
|
3787
3731
|
visible_commands = set(self.get_visible_commands())
|
@@ -3789,10 +3733,9 @@ class Cmd(cmd.Cmd):
|
|
3789
3733
|
return self.basic_complete(text, line, begidx, endidx, strs_to_match)
|
3790
3734
|
|
3791
3735
|
def complete_help_subcommands(
|
3792
|
-
self, text: str, line: str, begidx: int, endidx: int, arg_tokens:
|
3793
|
-
) ->
|
3794
|
-
"""Completes the subcommands argument of help"""
|
3795
|
-
|
3736
|
+
self, text: str, line: str, begidx: int, endidx: int, arg_tokens: dict[str, list[str]]
|
3737
|
+
) -> list[str]:
|
3738
|
+
"""Completes the subcommands argument of help."""
|
3796
3739
|
# Make sure we have a command whose subcommands we will complete
|
3797
3740
|
command = arg_tokens['command'][0]
|
3798
3741
|
if not command:
|
@@ -3824,7 +3767,7 @@ class Cmd(cmd.Cmd):
|
|
3824
3767
|
|
3825
3768
|
@with_argparser(help_parser)
|
3826
3769
|
def do_help(self, args: argparse.Namespace) -> None:
|
3827
|
-
"""List available commands or provide detailed help for a specific command"""
|
3770
|
+
"""List available commands or provide detailed help for a specific command."""
|
3828
3771
|
self.last_result = True
|
3829
3772
|
|
3830
3773
|
if not args.command or args.verbose:
|
@@ -3859,10 +3802,10 @@ class Cmd(cmd.Cmd):
|
|
3859
3802
|
self.perror(err_msg, apply_style=False)
|
3860
3803
|
self.last_result = False
|
3861
3804
|
|
3862
|
-
def print_topics(self, header: str, cmds: Optional[
|
3863
|
-
"""
|
3864
|
-
|
3865
|
-
Override of cmd's print_topics() to handle headers with newlines, ANSI style sequences, and wide characters
|
3805
|
+
def print_topics(self, header: str, cmds: Optional[list[str]], cmdlen: int, maxcol: int) -> None: # noqa: ARG002
|
3806
|
+
"""Print groups of commands and topics in columns and an optional header.
|
3807
|
+
|
3808
|
+
Override of cmd's print_topics() to handle headers with newlines, ANSI style sequences, and wide characters.
|
3866
3809
|
|
3867
3810
|
:param header: string to print above commands being printed
|
3868
3811
|
:param cmds: list of topics to print
|
@@ -3877,9 +3820,10 @@ class Cmd(cmd.Cmd):
|
|
3877
3820
|
self.columnize(cmds, maxcol - 1)
|
3878
3821
|
self.poutput()
|
3879
3822
|
|
3880
|
-
def columnize(self, str_list: Optional[
|
3823
|
+
def columnize(self, str_list: Optional[list[str]], display_width: int = 80) -> None:
|
3881
3824
|
"""Display a list of single-line strings as a compact set of columns.
|
3882
|
-
|
3825
|
+
|
3826
|
+
Override of cmd's columnize() to handle strings with ANSI style sequences and wide characters.
|
3883
3827
|
|
3884
3828
|
Each column is only as wide as necessary.
|
3885
3829
|
Columns are separated by two spaces (one was not legible enough).
|
@@ -3923,10 +3867,7 @@ class Cmd(cmd.Cmd):
|
|
3923
3867
|
texts = []
|
3924
3868
|
for col in range(ncols):
|
3925
3869
|
i = row + nrows * col
|
3926
|
-
if i >= size
|
3927
|
-
x = ""
|
3928
|
-
else:
|
3929
|
-
x = str_list[i]
|
3870
|
+
x = "" if i >= size else str_list[i]
|
3930
3871
|
texts.append(x)
|
3931
3872
|
while texts and not texts[-1]:
|
3932
3873
|
del texts[-1]
|
@@ -3935,7 +3876,7 @@ class Cmd(cmd.Cmd):
|
|
3935
3876
|
self.poutput(" ".join(texts))
|
3936
3877
|
|
3937
3878
|
def _help_menu(self, verbose: bool = False) -> None:
|
3938
|
-
"""Show a list of commands which help can be displayed for"""
|
3879
|
+
"""Show a list of commands which help can be displayed for."""
|
3939
3880
|
cmds_cats, cmds_doc, cmds_undoc, help_topics = self._build_command_info()
|
3940
3881
|
|
3941
3882
|
if not cmds_cats:
|
@@ -3953,15 +3894,15 @@ class Cmd(cmd.Cmd):
|
|
3953
3894
|
self.print_topics(self.misc_header, help_topics, 15, 80)
|
3954
3895
|
self.print_topics(self.undoc_header, cmds_undoc, 15, 80)
|
3955
3896
|
|
3956
|
-
def _build_command_info(self) ->
|
3897
|
+
def _build_command_info(self) -> tuple[dict[str, list[str]], list[str], list[str], list[str]]:
|
3957
3898
|
# Get a sorted list of help topics
|
3958
3899
|
help_topics = sorted(self.get_help_topics(), key=self.default_sort_key)
|
3959
3900
|
|
3960
3901
|
# Get a sorted list of visible command names
|
3961
3902
|
visible_commands = sorted(self.get_visible_commands(), key=self.default_sort_key)
|
3962
|
-
cmds_doc:
|
3963
|
-
cmds_undoc:
|
3964
|
-
cmds_cats:
|
3903
|
+
cmds_doc: list[str] = []
|
3904
|
+
cmds_undoc: list[str] = []
|
3905
|
+
cmds_cats: dict[str, list[str]] = {}
|
3965
3906
|
for command in visible_commands:
|
3966
3907
|
func = cast(CommandFunc, self.cmd_func(command))
|
3967
3908
|
has_help_func = False
|
@@ -3984,8 +3925,8 @@ class Cmd(cmd.Cmd):
|
|
3984
3925
|
cmds_undoc.append(command)
|
3985
3926
|
return cmds_cats, cmds_doc, cmds_undoc, help_topics
|
3986
3927
|
|
3987
|
-
def _print_topics(self, header: str, cmds:
|
3988
|
-
"""
|
3928
|
+
def _print_topics(self, header: str, cmds: list[str], verbose: bool) -> None:
|
3929
|
+
"""Print topics, switching between verbose or traditional output."""
|
3989
3930
|
import io
|
3990
3931
|
|
3991
3932
|
if cmds:
|
@@ -4028,7 +3969,7 @@ class Cmd(cmd.Cmd):
|
|
4028
3969
|
result = io.StringIO()
|
4029
3970
|
|
4030
3971
|
# try to redirect system stdout
|
4031
|
-
with redirect_stdout(result):
|
3972
|
+
with contextlib.redirect_stdout(result):
|
4032
3973
|
# save our internal stdout
|
4033
3974
|
stdout_orig = self.stdout
|
4034
3975
|
try:
|
@@ -4056,10 +3997,10 @@ class Cmd(cmd.Cmd):
|
|
4056
3997
|
|
4057
3998
|
@with_argparser(shortcuts_parser)
|
4058
3999
|
def do_shortcuts(self, _: argparse.Namespace) -> None:
|
4059
|
-
"""List available shortcuts"""
|
4000
|
+
"""List available shortcuts."""
|
4060
4001
|
# Sort the shortcut tuples by name
|
4061
4002
|
sorted_shortcuts = sorted(self.statement_parser.shortcuts, key=lambda x: self.default_sort_key(x[0]))
|
4062
|
-
result = "\n".join('{
|
4003
|
+
result = "\n".join(f'{sc[0]}: {sc[1]}' for sc in sorted_shortcuts)
|
4063
4004
|
self.poutput(f"Shortcuts for other commands:\n{result}")
|
4064
4005
|
self.last_result = True
|
4065
4006
|
|
@@ -4069,8 +4010,8 @@ class Cmd(cmd.Cmd):
|
|
4069
4010
|
|
4070
4011
|
@with_argparser(eof_parser)
|
4071
4012
|
def do_eof(self, _: argparse.Namespace) -> Optional[bool]:
|
4072
|
-
"""
|
4073
|
-
|
4013
|
+
"""Quit with no arguments, called when Ctrl-D is pressed.
|
4014
|
+
|
4074
4015
|
This can be overridden if quit should be called differently.
|
4075
4016
|
"""
|
4076
4017
|
self.poutput()
|
@@ -4082,14 +4023,15 @@ class Cmd(cmd.Cmd):
|
|
4082
4023
|
|
4083
4024
|
@with_argparser(quit_parser)
|
4084
4025
|
def do_quit(self, _: argparse.Namespace) -> Optional[bool]:
|
4085
|
-
"""Exit this application"""
|
4026
|
+
"""Exit this application."""
|
4086
4027
|
# Return True to stop the command loop
|
4087
4028
|
self.last_result = True
|
4088
4029
|
return True
|
4089
4030
|
|
4090
|
-
def select(self, opts: Union[str,
|
4091
|
-
"""
|
4092
|
-
|
4031
|
+
def select(self, opts: Union[str, list[str], list[tuple[Any, Optional[str]]]], prompt: str = 'Your choice? ') -> Any:
|
4032
|
+
"""Present a numbered menu to the user.
|
4033
|
+
|
4034
|
+
Modeled after the bash shell's SELECT. Returns the item chosen.
|
4093
4035
|
|
4094
4036
|
Argument ``opts`` can be:
|
4095
4037
|
|
@@ -4097,13 +4039,14 @@ class Cmd(cmd.Cmd):
|
|
4097
4039
|
| a list of strings -> will be offered as options
|
4098
4040
|
| a list of tuples -> interpreted as (value, text), so
|
4099
4041
|
that the return value can differ from
|
4100
|
-
the text advertised to the user
|
4101
|
-
|
4042
|
+
the text advertised to the user
|
4043
|
+
"""
|
4044
|
+
local_opts: Union[list[str], list[tuple[Any, Optional[str]]]]
|
4102
4045
|
if isinstance(opts, str):
|
4103
|
-
local_opts = cast(
|
4046
|
+
local_opts = cast(list[tuple[Any, Optional[str]]], list(zip(opts.split(), opts.split())))
|
4104
4047
|
else:
|
4105
4048
|
local_opts = opts
|
4106
|
-
fulloptions:
|
4049
|
+
fulloptions: list[tuple[Any, Optional[str]]] = []
|
4107
4050
|
for opt in local_opts:
|
4108
4051
|
if isinstance(opt, str):
|
4109
4052
|
fulloptions.append((opt, opt))
|
@@ -4113,7 +4056,7 @@ class Cmd(cmd.Cmd):
|
|
4113
4056
|
except IndexError:
|
4114
4057
|
fulloptions.append((opt[0], opt[0]))
|
4115
4058
|
for idx, (_, text) in enumerate(fulloptions):
|
4116
|
-
self.poutput(' %2d. %s' % (idx + 1, text))
|
4059
|
+
self.poutput(' %2d. %s' % (idx + 1, text)) # noqa: UP031
|
4117
4060
|
|
4118
4061
|
while True:
|
4119
4062
|
try:
|
@@ -4121,9 +4064,9 @@ class Cmd(cmd.Cmd):
|
|
4121
4064
|
except EOFError:
|
4122
4065
|
response = ''
|
4123
4066
|
self.poutput()
|
4124
|
-
except KeyboardInterrupt
|
4067
|
+
except KeyboardInterrupt:
|
4125
4068
|
self.poutput('^C')
|
4126
|
-
raise
|
4069
|
+
raise
|
4127
4070
|
|
4128
4071
|
if not response:
|
4129
4072
|
continue
|
@@ -4131,20 +4074,20 @@ class Cmd(cmd.Cmd):
|
|
4131
4074
|
try:
|
4132
4075
|
choice = int(response)
|
4133
4076
|
if choice < 1:
|
4134
|
-
raise IndexError
|
4077
|
+
raise IndexError # noqa: TRY301
|
4135
4078
|
return fulloptions[choice - 1][0]
|
4136
4079
|
except (ValueError, IndexError):
|
4137
4080
|
self.poutput(f"'{response}' isn't a valid choice. Pick a number between 1 and {len(fulloptions)}:")
|
4138
4081
|
|
4139
4082
|
def complete_set_value(
|
4140
|
-
self, text: str, line: str, begidx: int, endidx: int, arg_tokens:
|
4141
|
-
) ->
|
4142
|
-
"""Completes the value argument of set"""
|
4083
|
+
self, text: str, line: str, begidx: int, endidx: int, arg_tokens: dict[str, list[str]]
|
4084
|
+
) -> list[str]:
|
4085
|
+
"""Completes the value argument of set."""
|
4143
4086
|
param = arg_tokens['param'][0]
|
4144
4087
|
try:
|
4145
4088
|
settable = self.settables[param]
|
4146
|
-
except KeyError:
|
4147
|
-
raise CompletionError(param + " is not a settable parameter")
|
4089
|
+
except KeyError as exc:
|
4090
|
+
raise CompletionError(param + " is not a settable parameter") from exc
|
4148
4091
|
|
4149
4092
|
# Create a parser with a value field based on this settable
|
4150
4093
|
settable_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(parents=[Cmd.set_parser_parent])
|
@@ -4192,7 +4135,7 @@ class Cmd(cmd.Cmd):
|
|
4192
4135
|
# Preserve quotes so users can pass in quoted empty strings and flags (e.g. -h) as the value
|
4193
4136
|
@with_argparser(set_parser, preserve_quotes=True)
|
4194
4137
|
def do_set(self, args: argparse.Namespace) -> None:
|
4195
|
-
"""Set a settable parameter or show current settings of parameters"""
|
4138
|
+
"""Set a settable parameter or show current settings of parameters."""
|
4196
4139
|
self.last_result = False
|
4197
4140
|
|
4198
4141
|
if not self.settables:
|
@@ -4211,7 +4154,7 @@ class Cmd(cmd.Cmd):
|
|
4211
4154
|
try:
|
4212
4155
|
orig_value = settable.get_value()
|
4213
4156
|
settable.set_value(utils.strip_quotes(args.value))
|
4214
|
-
except
|
4157
|
+
except ValueError as ex:
|
4215
4158
|
self.perror(f"Error setting {args.param}: {ex}")
|
4216
4159
|
else:
|
4217
4160
|
self.poutput(f"{args.param} - was: {orig_value!r}\nnow: {settable.get_value()!r}")
|
@@ -4229,7 +4172,7 @@ class Cmd(cmd.Cmd):
|
|
4229
4172
|
max_name_width = max([ansi.style_aware_wcswidth(param) for param in to_show])
|
4230
4173
|
max_name_width = max(max_name_width, ansi.style_aware_wcswidth(name_label))
|
4231
4174
|
|
4232
|
-
cols:
|
4175
|
+
cols: list[Column] = [
|
4233
4176
|
Column(name_label, width=max_name_width),
|
4234
4177
|
Column('Value', width=30),
|
4235
4178
|
Column('Description', width=60),
|
@@ -4239,7 +4182,7 @@ class Cmd(cmd.Cmd):
|
|
4239
4182
|
self.poutput(table.generate_header())
|
4240
4183
|
|
4241
4184
|
# Build the table and populate self.last_result
|
4242
|
-
self.last_result = {} #
|
4185
|
+
self.last_result = {} # dict[settable_name, settable_value]
|
4243
4186
|
|
4244
4187
|
for param in sorted(to_show, key=self.default_sort_key):
|
4245
4188
|
settable = self.settables[param]
|
@@ -4256,11 +4199,11 @@ class Cmd(cmd.Cmd):
|
|
4256
4199
|
# Preserve quotes since we are passing these strings to the shell
|
4257
4200
|
@with_argparser(shell_parser, preserve_quotes=True)
|
4258
4201
|
def do_shell(self, args: argparse.Namespace) -> None:
|
4259
|
-
"""Execute a command as if at the OS prompt"""
|
4202
|
+
"""Execute a command as if at the OS prompt."""
|
4260
4203
|
import signal
|
4261
4204
|
import subprocess
|
4262
4205
|
|
4263
|
-
kwargs:
|
4206
|
+
kwargs: dict[str, Any] = {}
|
4264
4207
|
|
4265
4208
|
# Set OS-specific parameters
|
4266
4209
|
if sys.platform.startswith('win'):
|
@@ -4282,7 +4225,7 @@ class Cmd(cmd.Cmd):
|
|
4282
4225
|
kwargs['executable'] = shell
|
4283
4226
|
|
4284
4227
|
# Create a list of arguments to shell
|
4285
|
-
tokens = [args.command
|
4228
|
+
tokens = [args.command, *args.command_args]
|
4286
4229
|
|
4287
4230
|
# Expand ~ where needed
|
4288
4231
|
utils.expand_user_in_tokens(tokens)
|
@@ -4292,7 +4235,7 @@ class Cmd(cmd.Cmd):
|
|
4292
4235
|
# still receive the SIGINT since it is in the same process group as us.
|
4293
4236
|
with self.sigint_protection:
|
4294
4237
|
# For any stream that is a StdSim, we will use a pipe so we can capture its output
|
4295
|
-
proc = subprocess.Popen( # type: ignore[call-overload]
|
4238
|
+
proc = subprocess.Popen( # type: ignore[call-overload] # noqa: S602
|
4296
4239
|
expanded_command,
|
4297
4240
|
stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, # type: ignore[unreachable]
|
4298
4241
|
stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, # type: ignore[unreachable]
|
@@ -4313,8 +4256,8 @@ class Cmd(cmd.Cmd):
|
|
4313
4256
|
|
4314
4257
|
@staticmethod
|
4315
4258
|
def _reset_py_display() -> None:
|
4316
|
-
"""
|
4317
|
-
|
4259
|
+
"""Reset the dynamic objects in the sys module that the py and ipy consoles fight over.
|
4260
|
+
|
4318
4261
|
When a Python console starts it adopts certain display settings if they've already been set.
|
4319
4262
|
If an ipy console has previously been run, then py uses its settings and ends up looking
|
4320
4263
|
like an ipy console in terms of prompt and exception text. This method forces the Python
|
@@ -4326,19 +4269,17 @@ class Cmd(cmd.Cmd):
|
|
4326
4269
|
# Delete any prompts that have been set
|
4327
4270
|
attributes = ['ps1', 'ps2', 'ps3']
|
4328
4271
|
for cur_attr in attributes:
|
4329
|
-
|
4272
|
+
with contextlib.suppress(KeyError):
|
4330
4273
|
del sys.__dict__[cur_attr]
|
4331
|
-
except KeyError:
|
4332
|
-
pass
|
4333
4274
|
|
4334
4275
|
# Reset functions
|
4335
4276
|
sys.displayhook = sys.__displayhook__
|
4336
4277
|
sys.excepthook = sys.__excepthook__
|
4337
4278
|
|
4338
4279
|
def _set_up_py_shell_env(self, interp: InteractiveConsole) -> _SavedCmd2Env:
|
4339
|
-
"""
|
4340
|
-
|
4341
|
-
:return: Class containing saved up cmd2 environment
|
4280
|
+
"""Set up interactive Python shell environment.
|
4281
|
+
|
4282
|
+
:return: Class containing saved up cmd2 environment.
|
4342
4283
|
"""
|
4343
4284
|
cmd2_env = _SavedCmd2Env()
|
4344
4285
|
|
@@ -4400,8 +4341,7 @@ class Cmd(cmd.Cmd):
|
|
4400
4341
|
return cmd2_env
|
4401
4342
|
|
4402
4343
|
def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None:
|
4403
|
-
"""
|
4404
|
-
Restore cmd2 environment after exiting an interactive Python shell
|
4344
|
+
"""Restore cmd2 environment after exiting an interactive Python shell.
|
4405
4345
|
|
4406
4346
|
:param cmd2_env: the environment settings to restore
|
4407
4347
|
"""
|
@@ -4437,8 +4377,10 @@ class Cmd(cmd.Cmd):
|
|
4437
4377
|
sys.modules['readline'] = cmd2_env.readline_module
|
4438
4378
|
|
4439
4379
|
def _run_python(self, *, pyscript: Optional[str] = None) -> Optional[bool]:
|
4440
|
-
"""
|
4380
|
+
"""Run an interactive Python shell or execute a pyscript file.
|
4381
|
+
|
4441
4382
|
Called by do_py() and do_run_pyscript().
|
4383
|
+
|
4442
4384
|
If pyscript is None, then this function runs an interactive Python shell.
|
4443
4385
|
Otherwise, it runs the pyscript file.
|
4444
4386
|
|
@@ -4449,7 +4391,7 @@ class Cmd(cmd.Cmd):
|
|
4449
4391
|
self.last_result = False
|
4450
4392
|
|
4451
4393
|
def py_quit() -> None:
|
4452
|
-
"""
|
4394
|
+
"""Exit an interactive Python environment, callable from the interactive Python console."""
|
4453
4395
|
raise EmbeddedConsoleExit
|
4454
4396
|
|
4455
4397
|
from .py_bridge import (
|
@@ -4509,9 +4451,9 @@ class Cmd(cmd.Cmd):
|
|
4509
4451
|
|
4510
4452
|
# Check if we are running Python code
|
4511
4453
|
if py_code_to_run:
|
4512
|
-
try:
|
4454
|
+
try: # noqa: SIM105
|
4513
4455
|
interp.runcode(py_code_to_run) # type: ignore[arg-type]
|
4514
|
-
except BaseException:
|
4456
|
+
except BaseException: # noqa: BLE001, S110
|
4515
4457
|
# We don't care about any exception that happened in the Python code
|
4516
4458
|
pass
|
4517
4459
|
|
@@ -4534,7 +4476,7 @@ class Cmd(cmd.Cmd):
|
|
4534
4476
|
# Since quit() or exit() raise an EmbeddedConsoleExit, interact() exits before printing
|
4535
4477
|
# the exitmsg. Therefore, we will not provide it one and print it manually later.
|
4536
4478
|
interp.interact(banner=banner, exitmsg='')
|
4537
|
-
except BaseException:
|
4479
|
+
except BaseException: # noqa: BLE001, S110
|
4538
4480
|
# We don't care about any exception that happened in the interactive console
|
4539
4481
|
pass
|
4540
4482
|
finally:
|
@@ -4556,11 +4498,11 @@ class Cmd(cmd.Cmd):
|
|
4556
4498
|
|
4557
4499
|
@with_argparser(py_parser)
|
4558
4500
|
def do_py(self, _: argparse.Namespace) -> Optional[bool]:
|
4501
|
+
"""Run an interactive Python shell.
|
4502
|
+
|
4503
|
+
:return: True if running of commands should stop.
|
4559
4504
|
"""
|
4560
|
-
|
4561
|
-
:return: True if running of commands should stop
|
4562
|
-
"""
|
4563
|
-
# self.last_resort will be set by _run_python()
|
4505
|
+
# self.last_result will be set by _run_python()
|
4564
4506
|
return self._run_python()
|
4565
4507
|
|
4566
4508
|
run_pyscript_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run a Python script file inside the console")
|
@@ -4571,8 +4513,7 @@ class Cmd(cmd.Cmd):
|
|
4571
4513
|
|
4572
4514
|
@with_argparser(run_pyscript_parser)
|
4573
4515
|
def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]:
|
4574
|
-
"""
|
4575
|
-
Run a Python script file inside the console
|
4516
|
+
"""Run a Python script file inside the console.
|
4576
4517
|
|
4577
4518
|
:return: True if running of commands should stop
|
4578
4519
|
"""
|
@@ -4594,9 +4535,9 @@ class Cmd(cmd.Cmd):
|
|
4594
4535
|
|
4595
4536
|
try:
|
4596
4537
|
# Overwrite sys.argv to allow the script to take command line arguments
|
4597
|
-
sys.argv = [args.script_path
|
4538
|
+
sys.argv = [args.script_path, *args.script_arguments]
|
4598
4539
|
|
4599
|
-
# self.
|
4540
|
+
# self.last_result will be set by _run_python()
|
4600
4541
|
py_return = self._run_python(pyscript=args.script_path)
|
4601
4542
|
finally:
|
4602
4543
|
# Restore command line arguments to original state
|
@@ -4608,8 +4549,7 @@ class Cmd(cmd.Cmd):
|
|
4608
4549
|
|
4609
4550
|
@with_argparser(ipython_parser)
|
4610
4551
|
def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover
|
4611
|
-
"""
|
4612
|
-
Enter an interactive IPython shell
|
4552
|
+
"""Enter an interactive IPython shell.
|
4613
4553
|
|
4614
4554
|
:return: True if running of commands should stop
|
4615
4555
|
"""
|
@@ -4617,11 +4557,11 @@ class Cmd(cmd.Cmd):
|
|
4617
4557
|
|
4618
4558
|
# Detect whether IPython is installed
|
4619
4559
|
try:
|
4620
|
-
import traitlets.config.loader as
|
4560
|
+
import traitlets.config.loader as traitlets_loader # type: ignore[import]
|
4621
4561
|
|
4622
4562
|
# Allow users to install ipython from a cmd2 prompt when needed and still have ipy command work
|
4623
4563
|
try:
|
4624
|
-
start_ipython # noqa F823
|
4564
|
+
_dummy = start_ipython # noqa: F823
|
4625
4565
|
except NameError:
|
4626
4566
|
from IPython import start_ipython # type: ignore[import]
|
4627
4567
|
|
@@ -4658,7 +4598,7 @@ class Cmd(cmd.Cmd):
|
|
4658
4598
|
local_vars['self'] = self
|
4659
4599
|
|
4660
4600
|
# Configure IPython
|
4661
|
-
config =
|
4601
|
+
config = traitlets_loader.Config()
|
4662
4602
|
config.InteractiveShell.banner2 = (
|
4663
4603
|
'Entering an IPython shell. Type exit, quit, or Ctrl-D to exit.\n'
|
4664
4604
|
f'Run CLI commands with: {self.py_bridge_name}("command ...")\n'
|
@@ -4692,7 +4632,7 @@ class Cmd(cmd.Cmd):
|
|
4692
4632
|
'-t',
|
4693
4633
|
'--transcript',
|
4694
4634
|
metavar='TRANSCRIPT_FILE',
|
4695
|
-
help='
|
4635
|
+
help='create a transcript file by re-running the commands,\nimplies both -r and -s',
|
4696
4636
|
completer=path_complete,
|
4697
4637
|
)
|
4698
4638
|
history_action_group.add_argument('-c', '--clear', action='store_true', help='clear all history')
|
@@ -4728,15 +4668,14 @@ class Cmd(cmd.Cmd):
|
|
4728
4668
|
|
4729
4669
|
@with_argparser(history_parser)
|
4730
4670
|
def do_history(self, args: argparse.Namespace) -> Optional[bool]:
|
4731
|
-
"""
|
4732
|
-
View, run, edit, save, or clear previously entered commands
|
4671
|
+
"""View, run, edit, save, or clear previously entered commands.
|
4733
4672
|
|
4734
4673
|
:return: True if running of commands should stop
|
4735
4674
|
"""
|
4736
4675
|
self.last_result = False
|
4737
4676
|
|
4738
4677
|
# -v must be used alone with no other options
|
4739
|
-
if args.verbose:
|
4678
|
+
if args.verbose: # noqa: SIM102
|
4740
4679
|
if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script:
|
4741
4680
|
self.poutput("-v cannot be used with any other options")
|
4742
4681
|
self.poutput(self.history_parser.format_usage())
|
@@ -4791,7 +4730,7 @@ class Cmd(cmd.Cmd):
|
|
4791
4730
|
try:
|
4792
4731
|
self.run_editor(fname)
|
4793
4732
|
|
4794
|
-
# self.
|
4733
|
+
# self.last_result will be set by do_run_script()
|
4795
4734
|
return self.do_run_script(utils.quote_string(fname))
|
4796
4735
|
finally:
|
4797
4736
|
os.remove(fname)
|
@@ -4811,7 +4750,7 @@ class Cmd(cmd.Cmd):
|
|
4811
4750
|
self.pfeedback(f"{len(history)} command{plural} saved to {full_path}")
|
4812
4751
|
self.last_result = True
|
4813
4752
|
elif args.transcript:
|
4814
|
-
# self.
|
4753
|
+
# self.last_result will be set by _generate_transcript()
|
4815
4754
|
self._generate_transcript(list(history.values()), args.transcript)
|
4816
4755
|
else:
|
4817
4756
|
# Display the history items retrieved
|
@@ -4845,7 +4784,7 @@ class Cmd(cmd.Cmd):
|
|
4845
4784
|
return history
|
4846
4785
|
|
4847
4786
|
def _initialize_history(self, hist_file: str) -> None:
|
4848
|
-
"""Initialize history using history related attributes
|
4787
|
+
"""Initialize history using history related attributes.
|
4849
4788
|
|
4850
4789
|
:param hist_file: optional path to persistent history file. If specified, then history from
|
4851
4790
|
previous sessions will be included. Additionally, all history will be written
|
@@ -4879,7 +4818,7 @@ class Cmd(cmd.Cmd):
|
|
4879
4818
|
with open(hist_file, 'rb') as fobj:
|
4880
4819
|
compressed_bytes = fobj.read()
|
4881
4820
|
except FileNotFoundError:
|
4882
|
-
compressed_bytes =
|
4821
|
+
compressed_bytes = b""
|
4883
4822
|
except OSError as ex:
|
4884
4823
|
self.perror(f"Cannot read persistent history file '{hist_file}': {ex}")
|
4885
4824
|
return
|
@@ -4898,11 +4837,11 @@ class Cmd(cmd.Cmd):
|
|
4898
4837
|
try:
|
4899
4838
|
import lzma as decompress_lib
|
4900
4839
|
|
4901
|
-
decompress_exceptions:
|
4840
|
+
decompress_exceptions: tuple[type[Exception]] = (decompress_lib.LZMAError,)
|
4902
4841
|
except ModuleNotFoundError: # pragma: no cover
|
4903
4842
|
import bz2 as decompress_lib # type: ignore[no-redef]
|
4904
4843
|
|
4905
|
-
decompress_exceptions:
|
4844
|
+
decompress_exceptions: tuple[type[Exception]] = (OSError, ValueError) # type: ignore[no-redef]
|
4906
4845
|
|
4907
4846
|
try:
|
4908
4847
|
history_json = decompress_lib.decompress(compressed_bytes).decode(encoding='utf-8')
|
@@ -4938,7 +4877,7 @@ class Cmd(cmd.Cmd):
|
|
4938
4877
|
readline.add_history(formatted_command)
|
4939
4878
|
|
4940
4879
|
def _persist_history(self) -> None:
|
4941
|
-
"""Write history out to the persistent history file as compressed JSON"""
|
4880
|
+
"""Write history out to the persistent history file as compressed JSON."""
|
4942
4881
|
if not self.persistent_history_file:
|
4943
4882
|
return
|
4944
4883
|
|
@@ -4959,12 +4898,12 @@ class Cmd(cmd.Cmd):
|
|
4959
4898
|
|
4960
4899
|
def _generate_transcript(
|
4961
4900
|
self,
|
4962
|
-
history: Union[
|
4901
|
+
history: Union[list[HistoryItem], list[str]],
|
4963
4902
|
transcript_file: str,
|
4964
4903
|
*,
|
4965
4904
|
add_to_history: bool = True,
|
4966
4905
|
) -> None:
|
4967
|
-
"""Generate a transcript file from a given history of commands"""
|
4906
|
+
"""Generate a transcript file from a given history of commands."""
|
4968
4907
|
self.last_result = False
|
4969
4908
|
|
4970
4909
|
# Validate the transcript file path to make sure directory exists and write access is available
|
@@ -4998,7 +4937,7 @@ class Cmd(cmd.Cmd):
|
|
4998
4937
|
first = True
|
4999
4938
|
command = ''
|
5000
4939
|
if isinstance(history_item, HistoryItem):
|
5001
|
-
history_item = history_item.raw
|
4940
|
+
history_item = history_item.raw # noqa: PLW2901
|
5002
4941
|
for line in history_item.splitlines():
|
5003
4942
|
if first:
|
5004
4943
|
command += f"{self.prompt}{line}\n"
|
@@ -5048,10 +4987,7 @@ class Cmd(cmd.Cmd):
|
|
5048
4987
|
self.perror(f"Error saving transcript file '{transcript_path}': {ex}")
|
5049
4988
|
else:
|
5050
4989
|
# and let the user know what we did
|
5051
|
-
if commands_run == 1
|
5052
|
-
plural = 'command and its output'
|
5053
|
-
else:
|
5054
|
-
plural = 'commands and their outputs'
|
4990
|
+
plural = 'command and its output' if commands_run == 1 else 'commands and their outputs'
|
5055
4991
|
self.pfeedback(f"{commands_run} {plural} saved to transcript file '{transcript_path}'")
|
5056
4992
|
self.last_result = True
|
5057
4993
|
|
@@ -5070,20 +5006,18 @@ class Cmd(cmd.Cmd):
|
|
5070
5006
|
|
5071
5007
|
@with_argparser(edit_parser)
|
5072
5008
|
def do_edit(self, args: argparse.Namespace) -> None:
|
5073
|
-
"""Run a text editor and optionally open a file with it"""
|
5074
|
-
|
5009
|
+
"""Run a text editor and optionally open a file with it."""
|
5075
5010
|
# self.last_result will be set by do_shell() which is called by run_editor()
|
5076
5011
|
self.run_editor(args.file_path)
|
5077
5012
|
|
5078
5013
|
def run_editor(self, file_path: Optional[str] = None) -> None:
|
5079
|
-
"""
|
5080
|
-
Run a text editor and optionally open a file with it
|
5014
|
+
"""Run a text editor and optionally open a file with it.
|
5081
5015
|
|
5082
5016
|
:param file_path: optional path of the file to edit. Defaults to None.
|
5083
5017
|
:raises EnvironmentError: if self.editor is not set
|
5084
5018
|
"""
|
5085
5019
|
if not self.editor:
|
5086
|
-
raise
|
5020
|
+
raise OSError("Please use 'set editor' to specify your text editing program of choice.")
|
5087
5021
|
|
5088
5022
|
command = utils.quote_string(os.path.expanduser(self.editor))
|
5089
5023
|
if file_path:
|
@@ -5096,8 +5030,7 @@ class Cmd(cmd.Cmd):
|
|
5096
5030
|
"""Accessor to get the current script directory from the _script_dir LIFO queue."""
|
5097
5031
|
if self._script_dir:
|
5098
5032
|
return self._script_dir[-1]
|
5099
|
-
|
5100
|
-
return None
|
5033
|
+
return None
|
5101
5034
|
|
5102
5035
|
run_script_description = (
|
5103
5036
|
"Run commands in script file that is encoded as either ASCII or UTF-8 text\n"
|
@@ -5160,7 +5093,7 @@ class Cmd(cmd.Cmd):
|
|
5160
5093
|
self._script_dir.append(os.path.dirname(expanded_path))
|
5161
5094
|
|
5162
5095
|
if args.transcript:
|
5163
|
-
# self.
|
5096
|
+
# self.last_result will be set by _generate_transcript()
|
5164
5097
|
self._generate_transcript(
|
5165
5098
|
script_commands,
|
5166
5099
|
os.path.expanduser(args.transcript),
|
@@ -5198,8 +5131,7 @@ class Cmd(cmd.Cmd):
|
|
5198
5131
|
|
5199
5132
|
@with_argparser(relative_run_script_parser)
|
5200
5133
|
def do__relative_run_script(self, args: argparse.Namespace) -> Optional[bool]:
|
5201
|
-
"""
|
5202
|
-
Run commands in script file that is encoded as either ASCII or UTF-8 text
|
5134
|
+
"""Run commands in script file that is encoded as either ASCII or UTF-8 text.
|
5203
5135
|
|
5204
5136
|
:return: True if running of commands should stop
|
5205
5137
|
"""
|
@@ -5210,8 +5142,8 @@ class Cmd(cmd.Cmd):
|
|
5210
5142
|
# self.last_result will be set by do_run_script()
|
5211
5143
|
return self.do_run_script(utils.quote_string(relative_path))
|
5212
5144
|
|
5213
|
-
def _run_transcript_tests(self, transcript_paths:
|
5214
|
-
"""
|
5145
|
+
def _run_transcript_tests(self, transcript_paths: list[str]) -> None:
|
5146
|
+
"""Run transcript tests for provided file(s).
|
5215
5147
|
|
5216
5148
|
This is called when either -t is provided on the command line or the transcript_files argument is provided
|
5217
5149
|
during construction of the cmd2.Cmd instance.
|
@@ -5246,7 +5178,7 @@ class Cmd(cmd.Cmd):
|
|
5246
5178
|
self.poutput(f'cmd2 app: {sys.argv[0]}')
|
5247
5179
|
self.poutput(ansi.style(f'collected {num_transcripts} transcript{plural}', bold=True))
|
5248
5180
|
|
5249
|
-
|
5181
|
+
self.__class__.testfiles = transcripts_expanded
|
5250
5182
|
sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main()
|
5251
5183
|
testcase = TestMyAppCase()
|
5252
5184
|
stream = cast(TextIO, utils.StdSim(sys.stderr))
|
@@ -5273,8 +5205,8 @@ class Cmd(cmd.Cmd):
|
|
5273
5205
|
self.exit_code = 1
|
5274
5206
|
|
5275
5207
|
def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover
|
5276
|
-
"""
|
5277
|
-
|
5208
|
+
"""Display an important message to the user while they are at a command line prompt.
|
5209
|
+
|
5278
5210
|
To the user it appears as if an alert message is printed above the prompt and their
|
5279
5211
|
current input text and cursor location is left alone.
|
5280
5212
|
|
@@ -5345,8 +5277,7 @@ class Cmd(cmd.Cmd):
|
|
5345
5277
|
raise RuntimeError("another thread holds terminal_lock")
|
5346
5278
|
|
5347
5279
|
def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover
|
5348
|
-
"""
|
5349
|
-
Update the command line prompt while the user is still typing at it.
|
5280
|
+
"""Update the command line prompt while the user is still typing at it.
|
5350
5281
|
|
5351
5282
|
This is good for alerting the user to system changes dynamically in between commands.
|
5352
5283
|
For instance you could alter the color of the prompt to indicate a system status or increase a
|
@@ -5365,8 +5296,7 @@ class Cmd(cmd.Cmd):
|
|
5365
5296
|
self.async_alert('', new_prompt)
|
5366
5297
|
|
5367
5298
|
def async_refresh_prompt(self) -> None: # pragma: no cover
|
5368
|
-
"""
|
5369
|
-
Refresh the oncreen prompt to match self.prompt.
|
5299
|
+
"""Refresh the oncreen prompt to match self.prompt.
|
5370
5300
|
|
5371
5301
|
One case where the onscreen prompt and self.prompt can get out of sync is
|
5372
5302
|
when async_alert() is called while a user is in search mode (e.g. Ctrl-r).
|
@@ -5392,8 +5322,7 @@ class Cmd(cmd.Cmd):
|
|
5392
5322
|
|
5393
5323
|
@staticmethod
|
5394
5324
|
def set_window_title(title: str) -> None: # pragma: no cover
|
5395
|
-
"""
|
5396
|
-
Set the terminal window title.
|
5325
|
+
"""Set the terminal window title.
|
5397
5326
|
|
5398
5327
|
NOTE: This function writes to stderr. Therefore, if you call this during a command run by a pyscript,
|
5399
5328
|
the string which updates the title will appear in that command's CommandResult.stderr data.
|
@@ -5411,8 +5340,7 @@ class Cmd(cmd.Cmd):
|
|
5411
5340
|
pass
|
5412
5341
|
|
5413
5342
|
def enable_command(self, command: str) -> None:
|
5414
|
-
"""
|
5415
|
-
Enable a command by restoring its functions
|
5343
|
+
"""Enable a command by restoring its functions.
|
5416
5344
|
|
5417
5345
|
:param command: the command being enabled
|
5418
5346
|
"""
|
@@ -5444,8 +5372,7 @@ class Cmd(cmd.Cmd):
|
|
5444
5372
|
del self.disabled_commands[command]
|
5445
5373
|
|
5446
5374
|
def enable_category(self, category: str) -> None:
|
5447
|
-
"""
|
5448
|
-
Enable an entire category of commands
|
5375
|
+
"""Enable an entire category of commands.
|
5449
5376
|
|
5450
5377
|
:param category: the category to enable
|
5451
5378
|
"""
|
@@ -5455,8 +5382,7 @@ class Cmd(cmd.Cmd):
|
|
5455
5382
|
self.enable_command(cmd_name)
|
5456
5383
|
|
5457
5384
|
def disable_command(self, command: str, message_to_print: str) -> None:
|
5458
|
-
"""
|
5459
|
-
Disable a command and overwrite its functions
|
5385
|
+
"""Disable a command and overwrite its functions.
|
5460
5386
|
|
5461
5387
|
:param command: the command being disabled
|
5462
5388
|
:param message_to_print: what to print when this command is run or help is called on it while disabled
|
@@ -5493,7 +5419,7 @@ class Cmd(cmd.Cmd):
|
|
5493
5419
|
setattr(self, help_func_name, new_func)
|
5494
5420
|
|
5495
5421
|
# Set the completer to a function that returns a blank list
|
5496
|
-
setattr(self, completer_func_name, lambda *
|
5422
|
+
setattr(self, completer_func_name, lambda *_args, **_kwargs: [])
|
5497
5423
|
|
5498
5424
|
def disable_category(self, category: str, message_to_print: str) -> None:
|
5499
5425
|
"""Disable an entire category of commands.
|
@@ -5512,8 +5438,7 @@ class Cmd(cmd.Cmd):
|
|
5512
5438
|
self.disable_command(cmd_name, message_to_print)
|
5513
5439
|
|
5514
5440
|
def _report_disabled_command_usage(self, *_args: Any, message_to_print: str, **_kwargs: Any) -> None:
|
5515
|
-
"""
|
5516
|
-
Report when a disabled command has been run or had help called on it
|
5441
|
+
"""Report when a disabled command has been run or had help called on it.
|
5517
5442
|
|
5518
5443
|
:param _args: not used
|
5519
5444
|
:param message_to_print: the message reporting that the command is disabled
|
@@ -5523,7 +5448,7 @@ class Cmd(cmd.Cmd):
|
|
5523
5448
|
self.perror(message_to_print, apply_style=False)
|
5524
5449
|
|
5525
5450
|
def cmdloop(self, intro: Optional[str] = None) -> int: # type: ignore[override]
|
5526
|
-
"""
|
5451
|
+
"""Deal with extra features provided by cmd2, this is an outer wrapper around _cmdloop().
|
5527
5452
|
|
5528
5453
|
_cmdloop() provides the main loop equivalent to cmd.cmdloop(). This is a wrapper around that which deals with
|
5529
5454
|
the following extra features provided by cmd2:
|
@@ -5598,13 +5523,13 @@ class Cmd(cmd.Cmd):
|
|
5598
5523
|
#
|
5599
5524
|
###
|
5600
5525
|
def _initialize_plugin_system(self) -> None:
|
5601
|
-
"""Initialize the plugin system"""
|
5602
|
-
self._preloop_hooks:
|
5603
|
-
self._postloop_hooks:
|
5604
|
-
self._postparsing_hooks:
|
5605
|
-
self._precmd_hooks:
|
5606
|
-
self._postcmd_hooks:
|
5607
|
-
self._cmdfinalization_hooks:
|
5526
|
+
"""Initialize the plugin system."""
|
5527
|
+
self._preloop_hooks: list[Callable[[], None]] = []
|
5528
|
+
self._postloop_hooks: list[Callable[[], None]] = []
|
5529
|
+
self._postparsing_hooks: list[Callable[[plugin.PostparsingData], plugin.PostparsingData]] = []
|
5530
|
+
self._precmd_hooks: list[Callable[[plugin.PrecommandData], plugin.PrecommandData]] = []
|
5531
|
+
self._postcmd_hooks: list[Callable[[plugin.PostcommandData], plugin.PostcommandData]] = []
|
5532
|
+
self._cmdfinalization_hooks: list[Callable[[plugin.CommandFinalizationData], plugin.CommandFinalizationData]] = []
|
5608
5533
|
|
5609
5534
|
@classmethod
|
5610
5535
|
def _validate_callable_param_count(cls, func: Callable[..., Any], count: int) -> None:
|
@@ -5620,10 +5545,10 @@ class Cmd(cmd.Cmd):
|
|
5620
5545
|
def _validate_prepostloop_callable(cls, func: Callable[[], None]) -> None:
|
5621
5546
|
"""Check parameter and return types for preloop and postloop hooks."""
|
5622
5547
|
cls._validate_callable_param_count(func, 0)
|
5623
|
-
# make sure there is no return
|
5624
|
-
|
5625
|
-
if
|
5626
|
-
raise TypeError(f"{func.__name__} must
|
5548
|
+
# make sure there is no return annotation or the return is specified as None
|
5549
|
+
_, ret_ann = get_types(func)
|
5550
|
+
if ret_ann is not None:
|
5551
|
+
raise TypeError(f"{func.__name__} must have a return type of 'None', got: {ret_ann}")
|
5627
5552
|
|
5628
5553
|
def register_preloop_hook(self, func: Callable[[], None]) -> None:
|
5629
5554
|
"""Register a function to be called at the beginning of the command loop."""
|
@@ -5637,17 +5562,19 @@ class Cmd(cmd.Cmd):
|
|
5637
5562
|
|
5638
5563
|
@classmethod
|
5639
5564
|
def _validate_postparsing_callable(cls, func: Callable[[plugin.PostparsingData], plugin.PostparsingData]) -> None:
|
5640
|
-
"""Check parameter and return types for postparsing hooks"""
|
5565
|
+
"""Check parameter and return types for postparsing hooks."""
|
5641
5566
|
cls._validate_callable_param_count(cast(Callable[..., Any], func), 1)
|
5642
|
-
|
5643
|
-
|
5644
|
-
|
5567
|
+
type_hints, ret_ann = get_types(func)
|
5568
|
+
if not type_hints:
|
5569
|
+
raise TypeError(f"{func.__name__} parameter is missing a type hint, expected: 'cmd2.plugin.PostparsingData'")
|
5570
|
+
par_ann = next(iter(type_hints.values()))
|
5571
|
+
if par_ann != plugin.PostparsingData:
|
5645
5572
|
raise TypeError(f"{func.__name__} must have one parameter declared with type 'cmd2.plugin.PostparsingData'")
|
5646
|
-
if
|
5573
|
+
if ret_ann != plugin.PostparsingData:
|
5647
5574
|
raise TypeError(f"{func.__name__} must declare return a return type of 'cmd2.plugin.PostparsingData'")
|
5648
5575
|
|
5649
5576
|
def register_postparsing_hook(self, func: Callable[[plugin.PostparsingData], plugin.PostparsingData]) -> None:
|
5650
|
-
"""Register a function to be called after parsing user input but before running the command"""
|
5577
|
+
"""Register a function to be called after parsing user input but before running the command."""
|
5651
5578
|
self._validate_postparsing_callable(func)
|
5652
5579
|
self._postparsing_hooks.append(func)
|
5653
5580
|
|
@@ -5655,24 +5582,24 @@ class Cmd(cmd.Cmd):
|
|
5655
5582
|
|
5656
5583
|
@classmethod
|
5657
5584
|
def _validate_prepostcmd_hook(
|
5658
|
-
cls, func: Callable[[CommandDataType], CommandDataType], data_type:
|
5585
|
+
cls, func: Callable[[CommandDataType], CommandDataType], data_type: type[CommandDataType]
|
5659
5586
|
) -> None:
|
5660
5587
|
"""Check parameter and return types for pre and post command hooks."""
|
5661
|
-
signature = inspect.signature(func)
|
5662
5588
|
# validate that the callable has the right number of parameters
|
5663
5589
|
cls._validate_callable_param_count(cast(Callable[..., Any], func), 1)
|
5590
|
+
|
5591
|
+
type_hints, ret_ann = get_types(func)
|
5592
|
+
if not type_hints:
|
5593
|
+
raise TypeError(f"{func.__name__} parameter is missing a type hint, expected: {data_type}")
|
5594
|
+
param_name, par_ann = next(iter(type_hints.items()))
|
5664
5595
|
# validate the parameter has the right annotation
|
5665
|
-
|
5666
|
-
|
5667
|
-
if param.annotation != data_type:
|
5668
|
-
raise TypeError(f'argument 1 of {func.__name__} has incompatible type {param.annotation}, expected {data_type}')
|
5596
|
+
if par_ann != data_type:
|
5597
|
+
raise TypeError(f'argument 1 of {func.__name__} has incompatible type {par_ann}, expected {data_type}')
|
5669
5598
|
# validate the return value has the right annotation
|
5670
|
-
if
|
5599
|
+
if ret_ann is None:
|
5671
5600
|
raise TypeError(f'{func.__name__} does not have a declared return type, expected {data_type}')
|
5672
|
-
if
|
5673
|
-
raise TypeError(
|
5674
|
-
f'{func.__name__} has incompatible return type {signature.return_annotation}, expected {data_type}'
|
5675
|
-
)
|
5601
|
+
if ret_ann != data_type:
|
5602
|
+
raise TypeError(f'{func.__name__} has incompatible return type {ret_ann}, expected {data_type}')
|
5676
5603
|
|
5677
5604
|
def register_precmd_hook(self, func: Callable[[plugin.PrecommandData], plugin.PrecommandData]) -> None:
|
5678
5605
|
"""Register a hook to be called before the command function."""
|
@@ -5690,12 +5617,16 @@ class Cmd(cmd.Cmd):
|
|
5690
5617
|
) -> None:
|
5691
5618
|
"""Check parameter and return types for command finalization hooks."""
|
5692
5619
|
cls._validate_callable_param_count(func, 1)
|
5693
|
-
|
5694
|
-
|
5695
|
-
|
5696
|
-
|
5697
|
-
if
|
5698
|
-
raise TypeError(
|
5620
|
+
type_hints, ret_ann = get_types(func)
|
5621
|
+
if not type_hints:
|
5622
|
+
raise TypeError(f"{func.__name__} parameter is missing a type hint, expected: {plugin.CommandFinalizationData}")
|
5623
|
+
_, par_ann = next(iter(type_hints.items()))
|
5624
|
+
if par_ann != plugin.CommandFinalizationData:
|
5625
|
+
raise TypeError(
|
5626
|
+
f"{func.__name__} must have one parameter declared with type {plugin.CommandFinalizationData}, got: {par_ann}"
|
5627
|
+
)
|
5628
|
+
if ret_ann != plugin.CommandFinalizationData:
|
5629
|
+
raise TypeError(f"{func.__name__} must declare return a return type of {plugin.CommandFinalizationData}")
|
5699
5630
|
|
5700
5631
|
def register_cmdfinalization_hook(
|
5701
5632
|
self, func: Callable[[plugin.CommandFinalizationData], plugin.CommandFinalizationData]
|
@@ -5709,16 +5640,18 @@ class Cmd(cmd.Cmd):
|
|
5709
5640
|
cmd_support_func: Callable[..., Any],
|
5710
5641
|
cmd_self: Union[CommandSet, 'Cmd', None],
|
5711
5642
|
) -> Optional[object]:
|
5712
|
-
"""
|
5713
|
-
|
5714
|
-
used when defining command's argparse object.
|
5715
|
-
|
5643
|
+
"""Attempt to resolve a candidate instance to pass as 'self'.
|
5644
|
+
|
5645
|
+
Used for an unbound class method that was used when defining command's argparse object.
|
5646
|
+
|
5647
|
+
Since we restrict registration to only a single CommandSet
|
5648
|
+
instance of each type, using type is a reasonably safe way to resolve the correct object instance.
|
5716
5649
|
|
5717
5650
|
:param cmd_support_func: command support function. This could be a completer or namespace provider
|
5718
5651
|
:param cmd_self: The `self` associated with the command or subcommand
|
5719
5652
|
"""
|
5720
5653
|
# figure out what class the command support function was defined in
|
5721
|
-
func_class: Optional[
|
5654
|
+
func_class: Optional[type[Any]] = get_defining_class(cmd_support_func)
|
5722
5655
|
|
5723
5656
|
# Was there a defining class identified? If so, is it a sub-class of CommandSet?
|
5724
5657
|
if func_class is not None and issubclass(func_class, CommandSet):
|
@@ -5729,7 +5662,7 @@ class Cmd(cmd.Cmd):
|
|
5729
5662
|
# 2. Do any of the registered CommandSets in the Cmd2 application exactly match the type?
|
5730
5663
|
# 3. Is there a registered CommandSet that is is the only matching subclass?
|
5731
5664
|
|
5732
|
-
func_self: Optional[Union[CommandSet,
|
5665
|
+
func_self: Optional[Union[CommandSet, Cmd]]
|
5733
5666
|
|
5734
5667
|
# check if the command's CommandSet is a sub-class of the support function's defining class
|
5735
5668
|
if isinstance(cmd_self, func_class):
|
@@ -5738,7 +5671,7 @@ class Cmd(cmd.Cmd):
|
|
5738
5671
|
else:
|
5739
5672
|
# Search all registered CommandSets
|
5740
5673
|
func_self = None
|
5741
|
-
candidate_sets:
|
5674
|
+
candidate_sets: list[CommandSet] = []
|
5742
5675
|
for installed_cmd_set in self._installed_command_sets:
|
5743
5676
|
if type(installed_cmd_set) == func_class: # noqa: E721
|
5744
5677
|
# Case 2: CommandSet is an exact type match for the function's CommandSet
|
@@ -5752,5 +5685,4 @@ class Cmd(cmd.Cmd):
|
|
5752
5685
|
# Case 3: There exists exactly 1 CommandSet that is a sub-class match of the function's CommandSet
|
5753
5686
|
func_self = candidate_sets[0]
|
5754
5687
|
return func_self
|
5755
|
-
|
5756
|
-
return self
|
5688
|
+
return self
|