cmd2 2.5.11__py3-none-any.whl → 2.6.0__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 +447 -521
- 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 +143 -176
- {cmd2-2.5.11.dist-info → cmd2-2.6.0.dist-info}/METADATA +34 -17
- cmd2-2.6.0.dist-info/RECORD +24 -0
- {cmd2-2.5.11.dist-info → cmd2-2.6.0.dist-info}/WHEEL +1 -1
- cmd2-2.5.11.dist-info/RECORD +0 -24
- {cmd2-2.5.11.dist-info → cmd2-2.6.0.dist-info/licenses}/LICENSE +0 -0
- {cmd2-2.5.11.dist-info → cmd2-2.6.0.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,
|
@@ -187,7 +177,7 @@ else:
|
|
187
177
|
|
188
178
|
|
189
179
|
class _SavedReadlineSettings:
|
190
|
-
"""readline settings that are backed up when switching between readline environments"""
|
180
|
+
"""readline settings that are backed up when switching between readline environments."""
|
191
181
|
|
192
182
|
def __init__(self) -> None:
|
193
183
|
self.completer = None
|
@@ -196,18 +186,18 @@ class _SavedReadlineSettings:
|
|
196
186
|
|
197
187
|
|
198
188
|
class _SavedCmd2Env:
|
199
|
-
"""cmd2 environment settings that are backed up when entering an interactive Python shell"""
|
189
|
+
"""cmd2 environment settings that are backed up when entering an interactive Python shell."""
|
200
190
|
|
201
191
|
def __init__(self) -> None:
|
202
192
|
self.readline_settings = _SavedReadlineSettings()
|
203
193
|
self.readline_module: Optional[ModuleType] = None
|
204
|
-
self.history:
|
194
|
+
self.history: list[str] = []
|
205
195
|
self.sys_stdout: Optional[TextIO] = None
|
206
196
|
self.sys_stdin: Optional[TextIO] = None
|
207
197
|
|
208
198
|
|
209
199
|
# 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'])
|
200
|
+
DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function', 'completer_function']) # noqa: PYI024
|
211
201
|
|
212
202
|
|
213
203
|
if TYPE_CHECKING: # pragma: no cover
|
@@ -219,8 +209,7 @@ else:
|
|
219
209
|
|
220
210
|
|
221
211
|
class _CommandParsers:
|
222
|
-
"""
|
223
|
-
Create and store all command method argument parsers for a given Cmd instance.
|
212
|
+
"""Create and store all command method argument parsers for a given Cmd instance.
|
224
213
|
|
225
214
|
Parser creation and retrieval are accomplished through the get() method.
|
226
215
|
"""
|
@@ -230,7 +219,7 @@ class _CommandParsers:
|
|
230
219
|
|
231
220
|
# Keyed by the fully qualified method names. This is more reliable than
|
232
221
|
# the methods themselves, since wrapping a method will change its address.
|
233
|
-
self._parsers:
|
222
|
+
self._parsers: dict[str, argparse.ArgumentParser] = {}
|
234
223
|
|
235
224
|
@staticmethod
|
236
225
|
def _fully_qualified_name(command_method: CommandFunc) -> str:
|
@@ -241,8 +230,7 @@ class _CommandParsers:
|
|
241
230
|
return ""
|
242
231
|
|
243
232
|
def __contains__(self, command_method: CommandFunc) -> bool:
|
244
|
-
"""
|
245
|
-
Return whether a given method's parser is in self.
|
233
|
+
"""Return whether a given method's parser is in self.
|
246
234
|
|
247
235
|
If the parser does not yet exist, it will be created if applicable.
|
248
236
|
This is basically for checking if a method is argarse-based.
|
@@ -251,8 +239,7 @@ class _CommandParsers:
|
|
251
239
|
return bool(parser)
|
252
240
|
|
253
241
|
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.
|
242
|
+
"""Return a given method's parser or None if the method is not argparse-based.
|
256
243
|
|
257
244
|
If the parser does not yet exist, it will be created.
|
258
245
|
"""
|
@@ -296,7 +283,7 @@ class _CommandParsers:
|
|
296
283
|
class Cmd(cmd.Cmd):
|
297
284
|
"""An easy but powerful framework for writing line-oriented command interpreters.
|
298
285
|
|
299
|
-
Extends the Python Standard Library
|
286
|
+
Extends the Python Standard Library's cmd package by adding a lot of useful features
|
300
287
|
to the out of the box configuration.
|
301
288
|
|
302
289
|
Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes.
|
@@ -312,6 +299,9 @@ class Cmd(cmd.Cmd):
|
|
312
299
|
ALPHABETICAL_SORT_KEY = utils.norm_fold
|
313
300
|
NATURAL_SORT_KEY = utils.natural_keys
|
314
301
|
|
302
|
+
# List for storing transcript test file names
|
303
|
+
testfiles: ClassVar[list[str]] = []
|
304
|
+
|
315
305
|
def __init__(
|
316
306
|
self,
|
317
307
|
completekey: str = 'tab',
|
@@ -325,18 +315,17 @@ class Cmd(cmd.Cmd):
|
|
325
315
|
include_py: bool = False,
|
326
316
|
include_ipy: bool = False,
|
327
317
|
allow_cli_args: bool = True,
|
328
|
-
transcript_files: Optional[
|
318
|
+
transcript_files: Optional[list[str]] = None,
|
329
319
|
allow_redirection: bool = True,
|
330
|
-
multiline_commands: Optional[
|
331
|
-
terminators: Optional[
|
332
|
-
shortcuts: Optional[
|
320
|
+
multiline_commands: Optional[list[str]] = None,
|
321
|
+
terminators: Optional[list[str]] = None,
|
322
|
+
shortcuts: Optional[dict[str, str]] = None,
|
333
323
|
command_sets: Optional[Iterable[CommandSet]] = None,
|
334
324
|
auto_load_commands: bool = True,
|
335
325
|
allow_clipboard: bool = True,
|
336
326
|
suggest_similar_command: bool = False,
|
337
327
|
) -> None:
|
338
|
-
"""
|
339
|
-
interpreters. Extends Python's cmd package.
|
328
|
+
"""Easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package.
|
340
329
|
|
341
330
|
:param completekey: readline name of a completion key, default to Tab
|
342
331
|
:param stdin: alternate input file object, if not specified, sys.stdin is used
|
@@ -388,9 +377,9 @@ class Cmd(cmd.Cmd):
|
|
388
377
|
"""
|
389
378
|
# Check if py or ipy need to be disabled in this instance
|
390
379
|
if not include_py:
|
391
|
-
setattr(self, 'do_py', None)
|
380
|
+
setattr(self, 'do_py', None) # noqa: B010
|
392
381
|
if not include_ipy:
|
393
|
-
setattr(self, 'do_ipy', None)
|
382
|
+
setattr(self, 'do_ipy', None) # noqa: B010
|
394
383
|
|
395
384
|
# initialize plugin system
|
396
385
|
# needs to be done before we call __init__(0)
|
@@ -416,20 +405,20 @@ class Cmd(cmd.Cmd):
|
|
416
405
|
# The maximum number of CompletionItems to display during tab completion. If the number of completion
|
417
406
|
# suggestions exceeds this number, they will be displayed in the typical columnized format and will
|
418
407
|
# not include the description value of the CompletionItems.
|
419
|
-
self.max_completion_items = 50
|
408
|
+
self.max_completion_items: int = 50
|
420
409
|
|
421
410
|
# A dictionary mapping settable names to their Settable instance
|
422
|
-
self._settables:
|
411
|
+
self._settables: dict[str, Settable] = {}
|
423
412
|
self._always_prefix_settables: bool = False
|
424
413
|
|
425
414
|
# CommandSet containers
|
426
|
-
self._installed_command_sets:
|
427
|
-
self._cmd_to_command_sets:
|
415
|
+
self._installed_command_sets: set[CommandSet] = set()
|
416
|
+
self._cmd_to_command_sets: dict[str, CommandSet] = {}
|
428
417
|
|
429
418
|
self.build_settables()
|
430
419
|
|
431
420
|
# Use as prompt for multiline commands on the 2nd+ line of input
|
432
|
-
self.continuation_prompt = '> '
|
421
|
+
self.continuation_prompt: str = '> '
|
433
422
|
|
434
423
|
# Allow access to your application in embedded Python shells and scripts py via self
|
435
424
|
self.self_in_py = False
|
@@ -446,21 +435,21 @@ class Cmd(cmd.Cmd):
|
|
446
435
|
self.exclude_from_history = ['eof', 'history']
|
447
436
|
|
448
437
|
# Dictionary of macro names and their values
|
449
|
-
self.macros:
|
438
|
+
self.macros: dict[str, Macro] = {}
|
450
439
|
|
451
440
|
# Keeps track of typed command history in the Python shell
|
452
|
-
self._py_history:
|
441
|
+
self._py_history: list[str] = []
|
453
442
|
|
454
443
|
# The name by which Python environments refer to the PyBridge to call app commands
|
455
444
|
self.py_bridge_name = 'app'
|
456
445
|
|
457
446
|
# Defines app-specific variables/functions available in Python shells and pyscripts
|
458
|
-
self.py_locals:
|
447
|
+
self.py_locals: dict[str, Any] = {}
|
459
448
|
|
460
449
|
# True if running inside a Python shell or pyscript, False otherwise
|
461
450
|
self._in_py = False
|
462
451
|
|
463
|
-
self.statement_parser = StatementParser(
|
452
|
+
self.statement_parser: StatementParser = StatementParser(
|
464
453
|
terminators=terminators, multiline_commands=multiline_commands, shortcuts=shortcuts
|
465
454
|
)
|
466
455
|
|
@@ -468,10 +457,10 @@ class Cmd(cmd.Cmd):
|
|
468
457
|
self.last_result: Any = None
|
469
458
|
|
470
459
|
# Used by run_script command to store current script dir as a LIFO queue to support _relative_run_script command
|
471
|
-
self._script_dir:
|
460
|
+
self._script_dir: list[str] = []
|
472
461
|
|
473
462
|
# Context manager used to protect critical sections in the main thread from stopping due to a KeyboardInterrupt
|
474
|
-
self.sigint_protection = utils.ContextFlag()
|
463
|
+
self.sigint_protection: utils.ContextFlag = utils.ContextFlag()
|
475
464
|
|
476
465
|
# If the current command created a process to pipe to, then this will be a ProcReader object.
|
477
466
|
# Otherwise it will be None. It's used to know when a pipe process can be killed and/or waited upon.
|
@@ -499,7 +488,7 @@ class Cmd(cmd.Cmd):
|
|
499
488
|
self.broken_pipe_warning = ''
|
500
489
|
|
501
490
|
# Commands that will run at the beginning of the command loop
|
502
|
-
self._startup_commands:
|
491
|
+
self._startup_commands: list[str] = []
|
503
492
|
|
504
493
|
# If a startup script is provided and exists, then execute it in the startup commands
|
505
494
|
if startup_script:
|
@@ -511,7 +500,7 @@ class Cmd(cmd.Cmd):
|
|
511
500
|
self._startup_commands.append(script_cmd)
|
512
501
|
|
513
502
|
# Transcript files to run instead of interactive command loop
|
514
|
-
self._transcript_files: Optional[
|
503
|
+
self._transcript_files: Optional[list[str]] = None
|
515
504
|
|
516
505
|
# Check for command line args
|
517
506
|
if allow_cli_args:
|
@@ -554,7 +543,7 @@ class Cmd(cmd.Cmd):
|
|
554
543
|
# Commands that have been disabled from use. This is to support commands that are only available
|
555
544
|
# during specific states of the application. This dictionary's keys are the command names and its
|
556
545
|
# values are DisabledCommand objects.
|
557
|
-
self.disabled_commands:
|
546
|
+
self.disabled_commands: dict[str, DisabledCommand] = {}
|
558
547
|
|
559
548
|
# If any command has been categorized, then all other commands that haven't been categorized
|
560
549
|
# will display under this section in the help output.
|
@@ -566,7 +555,7 @@ class Cmd(cmd.Cmd):
|
|
566
555
|
# command and category names
|
567
556
|
# alias, macro, settable, and shortcut names
|
568
557
|
# tab completion results when self.matches_sorted is False
|
569
|
-
self.default_sort_key = Cmd.ALPHABETICAL_SORT_KEY
|
558
|
+
self.default_sort_key: Callable[[str], str] = Cmd.ALPHABETICAL_SORT_KEY
|
570
559
|
|
571
560
|
############################################################################################################
|
572
561
|
# The following variables are used by tab completion functions. They are reset each time complete() is run
|
@@ -582,17 +571,17 @@ class Cmd(cmd.Cmd):
|
|
582
571
|
self.allow_closing_quote = True
|
583
572
|
|
584
573
|
# An optional hint which prints above tab completion suggestions
|
585
|
-
self.completion_hint = ''
|
574
|
+
self.completion_hint: str = ''
|
586
575
|
|
587
576
|
# Normally cmd2 uses readline's formatter to columnize the list of completion suggestions.
|
588
577
|
# If a custom format is preferred, write the formatted completions to this string. cmd2 will
|
589
578
|
# then print it instead of the readline format. ANSI style sequences and newlines are supported
|
590
579
|
# when using this value. Even when using formatted_completions, the full matches must still be returned
|
591
580
|
# from your completer function. ArgparseCompleter writes its tab completion tables to this string.
|
592
|
-
self.formatted_completions = ''
|
581
|
+
self.formatted_completions: str = ''
|
593
582
|
|
594
583
|
# Used by complete() for readline tab completion
|
595
|
-
self.completion_matches:
|
584
|
+
self.completion_matches: list[str] = []
|
596
585
|
|
597
586
|
# Use this list if you need to display tab completion suggestions that are different than the actual text
|
598
587
|
# of the matches. For instance, if you are completing strings that contain a common delimiter and you only
|
@@ -600,7 +589,7 @@ class Cmd(cmd.Cmd):
|
|
600
589
|
# still must be returned from your completer function. For an example, look at path_complete() which
|
601
590
|
# uses this to show only the basename of paths as the suggestions. delimiter_complete() also populates
|
602
591
|
# this list. These are ignored if self.formatted_completions is populated.
|
603
|
-
self.display_matches:
|
592
|
+
self.display_matches: list[str] = []
|
604
593
|
|
605
594
|
# Used by functions like path_complete() and delimiter_complete() to properly
|
606
595
|
# quote matches that are completed in a delimited fashion
|
@@ -609,10 +598,10 @@ class Cmd(cmd.Cmd):
|
|
609
598
|
# Set to True before returning matches to complete() in cases where matches have already been sorted.
|
610
599
|
# If False, then complete() will sort the matches using self.default_sort_key before they are displayed.
|
611
600
|
# This does not affect self.formatted_completions.
|
612
|
-
self.matches_sorted = False
|
601
|
+
self.matches_sorted: bool = False
|
613
602
|
|
614
603
|
# Command parsers for this Cmd instance.
|
615
|
-
self._command_parsers = _CommandParsers(self)
|
604
|
+
self._command_parsers: _CommandParsers = _CommandParsers(self)
|
616
605
|
|
617
606
|
# Add functions decorated to be subcommands
|
618
607
|
self._register_subcommands(self)
|
@@ -642,9 +631,9 @@ class Cmd(cmd.Cmd):
|
|
642
631
|
# the current command being executed
|
643
632
|
self.current_command: Optional[Statement] = None
|
644
633
|
|
645
|
-
def find_commandsets(self, commandset_type:
|
646
|
-
"""
|
647
|
-
|
634
|
+
def find_commandsets(self, commandset_type: type[CommandSet], *, subclass_match: bool = False) -> list[CommandSet]:
|
635
|
+
"""Find all CommandSets that match the provided CommandSet type.
|
636
|
+
|
648
637
|
By default, locates a CommandSet that is an exact type match but may optionally return all CommandSets that
|
649
638
|
are sub-classes of the provided type
|
650
639
|
:param commandset_type: CommandSet sub-class type to search for
|
@@ -658,8 +647,8 @@ class Cmd(cmd.Cmd):
|
|
658
647
|
]
|
659
648
|
|
660
649
|
def find_commandset_for_command(self, command_name: str) -> Optional[CommandSet]:
|
661
|
-
"""
|
662
|
-
|
650
|
+
"""Find the CommandSet that registered the command name.
|
651
|
+
|
663
652
|
:param command_name: command name to search
|
664
653
|
:return: CommandSet that provided the command
|
665
654
|
"""
|
@@ -671,7 +660,7 @@ class Cmd(cmd.Cmd):
|
|
671
660
|
all_commandset_defs = CommandSet.__subclasses__()
|
672
661
|
existing_commandset_types = [type(command_set) for command_set in self._installed_command_sets]
|
673
662
|
|
674
|
-
def load_commandset_by_type(commandset_types:
|
663
|
+
def load_commandset_by_type(commandset_types: list[type[CommandSet]]) -> None:
|
675
664
|
for cmdset_type in commandset_types:
|
676
665
|
# check if the type has sub-classes. We will only auto-load leaf class types.
|
677
666
|
subclasses = cmdset_type.__subclasses__()
|
@@ -690,8 +679,7 @@ class Cmd(cmd.Cmd):
|
|
690
679
|
load_commandset_by_type(all_commandset_defs)
|
691
680
|
|
692
681
|
def register_command_set(self, cmdset: CommandSet) -> None:
|
693
|
-
"""
|
694
|
-
Installs a CommandSet, loading all commands defined in the CommandSet
|
682
|
+
"""Installs a CommandSet, loading all commands defined in the CommandSet.
|
695
683
|
|
696
684
|
:param cmdset: CommandSet to load
|
697
685
|
"""
|
@@ -703,19 +691,19 @@ class Cmd(cmd.Cmd):
|
|
703
691
|
if self.always_prefix_settables:
|
704
692
|
if not cmdset.settable_prefix.strip():
|
705
693
|
raise CommandSetRegistrationError('CommandSet settable prefix must not be empty')
|
706
|
-
for key in cmdset.settables
|
694
|
+
for key in cmdset.settables:
|
707
695
|
prefixed_name = f'{cmdset.settable_prefix}.{key}'
|
708
696
|
if prefixed_name in all_settables:
|
709
697
|
raise CommandSetRegistrationError(f'Duplicate settable: {key}')
|
710
698
|
|
711
699
|
else:
|
712
|
-
for key in cmdset.settables
|
700
|
+
for key in cmdset.settables:
|
713
701
|
if key in all_settables:
|
714
702
|
raise CommandSetRegistrationError(f'Duplicate settable {key} is already registered')
|
715
703
|
|
716
704
|
cmdset.on_register(self)
|
717
705
|
methods = cast(
|
718
|
-
|
706
|
+
list[tuple[str, Callable[..., Any]]],
|
719
707
|
inspect.getmembers(
|
720
708
|
cmdset,
|
721
709
|
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
@@ -790,8 +778,7 @@ class Cmd(cmd.Cmd):
|
|
790
778
|
return parser
|
791
779
|
|
792
780
|
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.
|
781
|
+
"""Install a new command function into the CLI.
|
795
782
|
|
796
783
|
:param command_func_name: name of command function to add
|
797
784
|
This points to the command method and may differ from the method's
|
@@ -800,7 +787,6 @@ class Cmd(cmd.Cmd):
|
|
800
787
|
:param context: optional info to provide in error message. (e.g. class this function belongs to)
|
801
788
|
:raises CommandSetRegistrationError: if the command function fails to install
|
802
789
|
"""
|
803
|
-
|
804
790
|
# command_func_name must begin with COMMAND_FUNC_PREFIX to be identified as a command by cmd2.
|
805
791
|
if not command_func_name.startswith(COMMAND_FUNC_PREFIX):
|
806
792
|
raise CommandSetRegistrationError(f"{command_func_name} does not begin with '{COMMAND_FUNC_PREFIX}'")
|
@@ -847,8 +833,7 @@ class Cmd(cmd.Cmd):
|
|
847
833
|
setattr(self, help_func_name, cmd_help)
|
848
834
|
|
849
835
|
def unregister_command_set(self, cmdset: CommandSet) -> None:
|
850
|
-
"""
|
851
|
-
Uninstalls a CommandSet and unloads all associated commands
|
836
|
+
"""Uninstalls a CommandSet and unloads all associated commands.
|
852
837
|
|
853
838
|
:param cmdset: CommandSet to uninstall
|
854
839
|
"""
|
@@ -857,7 +842,7 @@ class Cmd(cmd.Cmd):
|
|
857
842
|
cmdset.on_unregister()
|
858
843
|
self._unregister_subcommands(cmdset)
|
859
844
|
|
860
|
-
methods:
|
845
|
+
methods: list[tuple[str, Callable[..., Any]]] = inspect.getmembers(
|
861
846
|
cmdset,
|
862
847
|
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
863
848
|
and hasattr(meth, '__name__')
|
@@ -903,7 +888,7 @@ class Cmd(cmd.Cmd):
|
|
903
888
|
check_parser_uninstallable(subparser)
|
904
889
|
break
|
905
890
|
|
906
|
-
methods:
|
891
|
+
methods: list[tuple[str, Callable[..., Any]]] = inspect.getmembers(
|
907
892
|
cmdset,
|
908
893
|
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
|
909
894
|
and hasattr(meth, '__name__')
|
@@ -919,8 +904,7 @@ class Cmd(cmd.Cmd):
|
|
919
904
|
check_parser_uninstallable(command_parser)
|
920
905
|
|
921
906
|
def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
|
922
|
-
"""
|
923
|
-
Register subcommands with their base command
|
907
|
+
"""Register subcommands with their base command.
|
924
908
|
|
925
909
|
:param cmdset: CommandSet or cmd2.Cmd subclass containing subcommands
|
926
910
|
"""
|
@@ -937,14 +921,14 @@ class Cmd(cmd.Cmd):
|
|
937
921
|
)
|
938
922
|
|
939
923
|
# iterate through all matching methods
|
940
|
-
for
|
924
|
+
for _method_name, method in methods:
|
941
925
|
subcommand_name: str = getattr(method, constants.SUBCMD_ATTR_NAME)
|
942
926
|
full_command_name: str = getattr(method, constants.SUBCMD_ATTR_COMMAND)
|
943
927
|
subcmd_parser_builder = getattr(method, constants.CMD_ATTR_ARGPARSER)
|
944
928
|
|
945
929
|
subcommand_valid, errmsg = self.statement_parser.is_valid_command(subcommand_name, is_subcommand=True)
|
946
930
|
if not subcommand_valid:
|
947
|
-
raise CommandSetRegistrationError(f'Subcommand {
|
931
|
+
raise CommandSetRegistrationError(f'Subcommand {subcommand_name!s} is not valid: {errmsg}')
|
948
932
|
|
949
933
|
command_tokens = full_command_name.split()
|
950
934
|
command_name = command_tokens[0]
|
@@ -957,16 +941,14 @@ class Cmd(cmd.Cmd):
|
|
957
941
|
command_func = self.cmd_func(command_name)
|
958
942
|
|
959
943
|
if command_func is None:
|
960
|
-
raise CommandSetRegistrationError(
|
961
|
-
f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
|
962
|
-
)
|
944
|
+
raise CommandSetRegistrationError(f"Could not find command '{command_name}' needed by subcommand: {method!s}")
|
963
945
|
command_parser = self._command_parsers.get(command_func)
|
964
946
|
if command_parser is None:
|
965
947
|
raise CommandSetRegistrationError(
|
966
|
-
f"Could not find argparser for command '{command_name}' needed by subcommand: {
|
948
|
+
f"Could not find argparser for command '{command_name}' needed by subcommand: {method!s}"
|
967
949
|
)
|
968
950
|
|
969
|
-
def find_subcommand(action: argparse.ArgumentParser, subcmd_names:
|
951
|
+
def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> argparse.ArgumentParser:
|
970
952
|
if not subcmd_names:
|
971
953
|
return action
|
972
954
|
cur_subcmd = subcmd_names.pop(0)
|
@@ -976,7 +958,7 @@ class Cmd(cmd.Cmd):
|
|
976
958
|
if choice_name == cur_subcmd:
|
977
959
|
return find_subcommand(choice, subcmd_names)
|
978
960
|
break
|
979
|
-
raise CommandSetRegistrationError(f"Could not find subcommand '{
|
961
|
+
raise CommandSetRegistrationError(f"Could not find subcommand '{action}'")
|
980
962
|
|
981
963
|
target_parser = find_subcommand(command_parser, subcommand_names)
|
982
964
|
|
@@ -1028,8 +1010,7 @@ class Cmd(cmd.Cmd):
|
|
1028
1010
|
break
|
1029
1011
|
|
1030
1012
|
def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
|
1031
|
-
"""
|
1032
|
-
Unregister subcommands from their base command
|
1013
|
+
"""Unregister subcommands from their base command.
|
1033
1014
|
|
1034
1015
|
:param cmdset: CommandSet containing subcommands
|
1035
1016
|
"""
|
@@ -1046,7 +1027,7 @@ class Cmd(cmd.Cmd):
|
|
1046
1027
|
)
|
1047
1028
|
|
1048
1029
|
# iterate through all matching methods
|
1049
|
-
for
|
1030
|
+
for _method_name, method in methods:
|
1050
1031
|
subcommand_name = getattr(method, constants.SUBCMD_ATTR_NAME)
|
1051
1032
|
command_name = getattr(method, constants.SUBCMD_ATTR_COMMAND)
|
1052
1033
|
|
@@ -1059,15 +1040,13 @@ class Cmd(cmd.Cmd):
|
|
1059
1040
|
if command_func is None: # pragma: no cover
|
1060
1041
|
# This really shouldn't be possible since _register_subcommands would prevent this from happening
|
1061
1042
|
# 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
|
-
)
|
1043
|
+
raise CommandSetRegistrationError(f"Could not find command '{command_name}' needed by subcommand: {method!s}")
|
1065
1044
|
command_parser = self._command_parsers.get(command_func)
|
1066
1045
|
if command_parser is None: # pragma: no cover
|
1067
1046
|
# This really shouldn't be possible since _register_subcommands would prevent this from happening
|
1068
1047
|
# but keeping in case it does for some strange reason
|
1069
1048
|
raise CommandSetRegistrationError(
|
1070
|
-
f"Could not find argparser for command '{command_name}' needed by subcommand: {
|
1049
|
+
f"Could not find argparser for command '{command_name}' needed by subcommand: {method!s}"
|
1071
1050
|
)
|
1072
1051
|
|
1073
1052
|
for action in command_parser._actions:
|
@@ -1077,8 +1056,7 @@ class Cmd(cmd.Cmd):
|
|
1077
1056
|
|
1078
1057
|
@property
|
1079
1058
|
def always_prefix_settables(self) -> bool:
|
1080
|
-
"""
|
1081
|
-
Flags whether CommandSet settable values should always be prefixed
|
1059
|
+
"""Flags whether CommandSet settable values should always be prefixed.
|
1082
1060
|
|
1083
1061
|
:return: True if CommandSet settable values will always be prefixed. False if not.
|
1084
1062
|
"""
|
@@ -1086,8 +1064,7 @@ class Cmd(cmd.Cmd):
|
|
1086
1064
|
|
1087
1065
|
@always_prefix_settables.setter
|
1088
1066
|
def always_prefix_settables(self, new_value: bool) -> None:
|
1089
|
-
"""
|
1090
|
-
Set whether CommandSet settable values should always be prefixed.
|
1067
|
+
"""Set whether CommandSet settable values should always be prefixed.
|
1091
1068
|
|
1092
1069
|
:param new_value: True if CommandSet settable values should always be prefixed. False if not.
|
1093
1070
|
:raises ValueError: If a registered CommandSet does not have a defined prefix
|
@@ -1103,8 +1080,7 @@ class Cmd(cmd.Cmd):
|
|
1103
1080
|
|
1104
1081
|
@property
|
1105
1082
|
def settables(self) -> Mapping[str, Settable]:
|
1106
|
-
"""
|
1107
|
-
Get all available user-settable attributes. This includes settables defined in installed CommandSets
|
1083
|
+
"""Get all available user-settable attributes. This includes settables defined in installed CommandSets.
|
1108
1084
|
|
1109
1085
|
:return: Mapping from attribute-name to Settable of all user-settable attributes from
|
1110
1086
|
"""
|
@@ -1119,44 +1095,41 @@ class Cmd(cmd.Cmd):
|
|
1119
1095
|
return all_settables
|
1120
1096
|
|
1121
1097
|
def add_settable(self, settable: Settable) -> None:
|
1122
|
-
"""
|
1123
|
-
Add a settable parameter to ``self.settables``
|
1098
|
+
"""Add a settable parameter to ``self.settables``.
|
1124
1099
|
|
1125
1100
|
:param settable: Settable object being added
|
1126
1101
|
"""
|
1127
|
-
if not self.always_prefix_settables:
|
1128
|
-
|
1129
|
-
raise KeyError(f'Duplicate settable: {settable.name}')
|
1102
|
+
if not self.always_prefix_settables and settable.name in self.settables and settable.name not in self._settables:
|
1103
|
+
raise KeyError(f'Duplicate settable: {settable.name}')
|
1130
1104
|
self._settables[settable.name] = settable
|
1131
1105
|
|
1132
1106
|
def remove_settable(self, name: str) -> None:
|
1133
|
-
"""
|
1134
|
-
Convenience method for removing a settable parameter from ``self.settables``
|
1107
|
+
"""Remove a settable parameter from ``self.settables``.
|
1135
1108
|
|
1136
1109
|
:param name: name of the settable being removed
|
1137
1110
|
:raises KeyError: if the Settable matches this name
|
1138
1111
|
"""
|
1139
1112
|
try:
|
1140
1113
|
del self._settables[name]
|
1141
|
-
except KeyError:
|
1142
|
-
raise KeyError(name + " is not a settable parameter")
|
1114
|
+
except KeyError as exc:
|
1115
|
+
raise KeyError(name + " is not a settable parameter") from exc
|
1143
1116
|
|
1144
1117
|
def build_settables(self) -> None:
|
1145
|
-
"""Create the dictionary of user-settable parameters"""
|
1118
|
+
"""Create the dictionary of user-settable parameters."""
|
1146
1119
|
|
1147
|
-
def get_allow_style_choices(
|
1148
|
-
"""
|
1120
|
+
def get_allow_style_choices(_cli_self: Cmd) -> list[str]:
|
1121
|
+
"""Tab complete allow_style values."""
|
1149
1122
|
return [val.name.lower() for val in ansi.AllowStyle]
|
1150
1123
|
|
1151
1124
|
def allow_style_type(value: str) -> ansi.AllowStyle:
|
1152
|
-
"""
|
1125
|
+
"""Convert a string value into an ansi.AllowStyle."""
|
1153
1126
|
try:
|
1154
1127
|
return ansi.AllowStyle[value.upper()]
|
1155
|
-
except KeyError:
|
1128
|
+
except KeyError as esc:
|
1156
1129
|
raise ValueError(
|
1157
1130
|
f"must be {ansi.AllowStyle.ALWAYS}, {ansi.AllowStyle.NEVER}, or "
|
1158
1131
|
f"{ansi.AllowStyle.TERMINAL} (case-insensitive)"
|
1159
|
-
)
|
1132
|
+
) from esc
|
1160
1133
|
|
1161
1134
|
self.add_settable(
|
1162
1135
|
Settable(
|
@@ -1187,16 +1160,16 @@ class Cmd(cmd.Cmd):
|
|
1187
1160
|
|
1188
1161
|
@property
|
1189
1162
|
def allow_style(self) -> ansi.AllowStyle:
|
1190
|
-
"""Read-only property needed to support do_set when it reads allow_style"""
|
1163
|
+
"""Read-only property needed to support do_set when it reads allow_style."""
|
1191
1164
|
return ansi.allow_style
|
1192
1165
|
|
1193
1166
|
@allow_style.setter
|
1194
1167
|
def allow_style(self, new_val: ansi.AllowStyle) -> None:
|
1195
|
-
"""Setter property needed to support do_set when it updates allow_style"""
|
1168
|
+
"""Setter property needed to support do_set when it updates allow_style."""
|
1196
1169
|
ansi.allow_style = new_val
|
1197
1170
|
|
1198
1171
|
def _completion_supported(self) -> bool:
|
1199
|
-
"""Return whether tab completion is supported"""
|
1172
|
+
"""Return whether tab completion is supported."""
|
1200
1173
|
return self.use_rawinput and bool(self.completekey) and rl_type != RlType.NONE
|
1201
1174
|
|
1202
1175
|
@property
|
@@ -1218,8 +1191,7 @@ class Cmd(cmd.Cmd):
|
|
1218
1191
|
end: str = '\n',
|
1219
1192
|
style: Optional[Callable[[str], str]] = None,
|
1220
1193
|
) -> None:
|
1221
|
-
"""
|
1222
|
-
Print message to a given file object.
|
1194
|
+
"""Print message to a given file object.
|
1223
1195
|
|
1224
1196
|
:param dest: the file object being written to
|
1225
1197
|
:param msg: object to print
|
@@ -1239,7 +1211,7 @@ class Cmd(cmd.Cmd):
|
|
1239
1211
|
sys.stderr.write(self.broken_pipe_warning)
|
1240
1212
|
|
1241
1213
|
def poutput(self, msg: Any = '', *, end: str = '\n') -> None:
|
1242
|
-
"""Print message to self.stdout and appends a newline by default
|
1214
|
+
"""Print message to self.stdout and appends a newline by default.
|
1243
1215
|
|
1244
1216
|
:param msg: object to print
|
1245
1217
|
:param end: string appended after the end of the message, default a newline
|
@@ -1247,7 +1219,7 @@ class Cmd(cmd.Cmd):
|
|
1247
1219
|
self.print_to(self.stdout, msg, end=end)
|
1248
1220
|
|
1249
1221
|
def perror(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> None:
|
1250
|
-
"""Print message to sys.stderr
|
1222
|
+
"""Print message to sys.stderr.
|
1251
1223
|
|
1252
1224
|
:param msg: object to print
|
1253
1225
|
:param end: string appended after the end of the message, default a newline
|
@@ -1257,7 +1229,7 @@ class Cmd(cmd.Cmd):
|
|
1257
1229
|
self.print_to(sys.stderr, msg, end=end, style=ansi.style_error if apply_style else None)
|
1258
1230
|
|
1259
1231
|
def psuccess(self, msg: Any = '', *, end: str = '\n') -> None:
|
1260
|
-
"""
|
1232
|
+
"""Wrap poutput, but applies ansi.style_success by default.
|
1261
1233
|
|
1262
1234
|
:param msg: object to print
|
1263
1235
|
:param end: string appended after the end of the message, default a newline
|
@@ -1266,7 +1238,7 @@ class Cmd(cmd.Cmd):
|
|
1266
1238
|
self.poutput(msg, end=end)
|
1267
1239
|
|
1268
1240
|
def pwarning(self, msg: Any = '', *, end: str = '\n') -> None:
|
1269
|
-
"""
|
1241
|
+
"""Wrap perror, but applies ansi.style_warning by default.
|
1270
1242
|
|
1271
1243
|
:param msg: object to print
|
1272
1244
|
:param end: string appended after the end of the message, default a newline
|
@@ -1302,7 +1274,8 @@ class Cmd(cmd.Cmd):
|
|
1302
1274
|
self.perror(final_msg, end=end, apply_style=False)
|
1303
1275
|
|
1304
1276
|
def pfeedback(self, msg: Any, *, end: str = '\n') -> None:
|
1305
|
-
"""
|
1277
|
+
"""Print nonessential feedback. Can be silenced with `quiet`.
|
1278
|
+
|
1306
1279
|
Inclusion in redirected output is controlled by `feedback_to_output`.
|
1307
1280
|
|
1308
1281
|
:param msg: object to print
|
@@ -1334,7 +1307,7 @@ class Cmd(cmd.Cmd):
|
|
1334
1307
|
# Don't try to use the pager when being run by a continuous integration system like Jenkins + pexpect.
|
1335
1308
|
functional_terminal = False
|
1336
1309
|
|
1337
|
-
if self.stdin.isatty() and self.stdout.isatty():
|
1310
|
+
if self.stdin.isatty() and self.stdout.isatty(): # noqa: SIM102
|
1338
1311
|
if sys.platform.startswith('win') or os.environ.get('TERM') is not None:
|
1339
1312
|
functional_terminal = True
|
1340
1313
|
|
@@ -1355,7 +1328,7 @@ class Cmd(cmd.Cmd):
|
|
1355
1328
|
with self.sigint_protection:
|
1356
1329
|
import subprocess
|
1357
1330
|
|
1358
|
-
pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE, stdout=self.stdout)
|
1331
|
+
pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE, stdout=self.stdout) # noqa: S602
|
1359
1332
|
pipe_proc.communicate(final_msg.encode('utf-8', 'replace'))
|
1360
1333
|
except BrokenPipeError:
|
1361
1334
|
# This occurs if a command's output is being piped to another process and that process closes before the
|
@@ -1366,12 +1339,23 @@ class Cmd(cmd.Cmd):
|
|
1366
1339
|
else:
|
1367
1340
|
self.poutput(msg, end=end)
|
1368
1341
|
|
1342
|
+
def ppretty(self, data: Any, *, indent: int = 2, width: int = 80, depth: Optional[int] = None, end: str = '\n') -> None:
|
1343
|
+
"""Pretty print arbitrary Python data structures to self.stdout and appends a newline by default.
|
1344
|
+
|
1345
|
+
:param data: object to print
|
1346
|
+
:param indent: the amount of indentation added for each nesting level
|
1347
|
+
:param width: the desired maximum number of characters per line in the output, a best effort will be made for long data
|
1348
|
+
:param depth: the number of nesting levels which may be printed, if data is too deep, the next level replaced by ...
|
1349
|
+
:param end: string appended after the end of the message, default a newline
|
1350
|
+
"""
|
1351
|
+
self.print_to(self.stdout, pprint.pformat(data, indent, width, depth), end=end)
|
1352
|
+
|
1369
1353
|
# ----- Methods related to tab completion -----
|
1370
1354
|
|
1371
1355
|
def _reset_completion_defaults(self) -> None:
|
1372
|
-
"""
|
1373
|
-
|
1374
|
-
Needs to be called each time readline runs tab completion
|
1356
|
+
"""Reset tab completion settings.
|
1357
|
+
|
1358
|
+
Needs to be called each time readline runs tab completion.
|
1375
1359
|
"""
|
1376
1360
|
self.allow_appended_space = True
|
1377
1361
|
self.allow_closing_quote = True
|
@@ -1387,8 +1371,8 @@ class Cmd(cmd.Cmd):
|
|
1387
1371
|
elif rl_type == RlType.PYREADLINE:
|
1388
1372
|
readline.rl.mode._display_completions = self._display_matches_pyreadline
|
1389
1373
|
|
1390
|
-
def tokens_for_completion(self, line: str, begidx: int, endidx: int) ->
|
1391
|
-
"""
|
1374
|
+
def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> tuple[list[str], list[str]]:
|
1375
|
+
"""Get all tokens through the one being completed, used by tab completion functions.
|
1392
1376
|
|
1393
1377
|
:param line: the current input line with leading whitespace removed
|
1394
1378
|
:param begidx: the beginning index of the prefix text
|
@@ -1454,14 +1438,14 @@ class Cmd(cmd.Cmd):
|
|
1454
1438
|
def basic_complete(
|
1455
1439
|
self,
|
1456
1440
|
text: str,
|
1457
|
-
line: str,
|
1458
|
-
begidx: int,
|
1459
|
-
endidx: int,
|
1441
|
+
line: str, # noqa: ARG002
|
1442
|
+
begidx: int, # noqa: ARG002
|
1443
|
+
endidx: int, # noqa: ARG002
|
1460
1444
|
match_against: Iterable[str],
|
1461
|
-
) ->
|
1462
|
-
"""
|
1463
|
-
|
1464
|
-
|
1445
|
+
) -> list[str]:
|
1446
|
+
"""Tab completion function that matches against a list of strings without considering line contents or cursor position.
|
1447
|
+
|
1448
|
+
The args required by this function are defined in the header of Python's cmd.py.
|
1465
1449
|
|
1466
1450
|
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
1467
1451
|
:param line: the current input line with leading whitespace removed
|
@@ -1480,10 +1464,10 @@ class Cmd(cmd.Cmd):
|
|
1480
1464
|
endidx: int,
|
1481
1465
|
match_against: Iterable[str],
|
1482
1466
|
delimiter: str,
|
1483
|
-
) ->
|
1484
|
-
"""
|
1485
|
-
|
1486
|
-
the portion of the match being tab completed is shown as the completion suggestions.
|
1467
|
+
) -> list[str]:
|
1468
|
+
"""Perform tab completion against a list but each match is split on a delimiter.
|
1469
|
+
|
1470
|
+
Only the portion of the match being tab completed is shown as the completion suggestions.
|
1487
1471
|
This is useful if you match against strings that are hierarchical in nature and have a
|
1488
1472
|
common delimiter.
|
1489
1473
|
|
@@ -1546,10 +1530,10 @@ class Cmd(cmd.Cmd):
|
|
1546
1530
|
line: str,
|
1547
1531
|
begidx: int,
|
1548
1532
|
endidx: int,
|
1549
|
-
flag_dict:
|
1533
|
+
flag_dict: dict[str, Union[Iterable[str], CompleterFunc]],
|
1550
1534
|
*,
|
1551
1535
|
all_else: Union[None, Iterable[str], CompleterFunc] = None,
|
1552
|
-
) ->
|
1536
|
+
) -> list[str]:
|
1553
1537
|
"""Tab completes based on a particular flag preceding the token being completed.
|
1554
1538
|
|
1555
1539
|
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
@@ -1598,7 +1582,7 @@ class Cmd(cmd.Cmd):
|
|
1598
1582
|
index_dict: Mapping[int, Union[Iterable[str], CompleterFunc]],
|
1599
1583
|
*,
|
1600
1584
|
all_else: Optional[Union[Iterable[str], CompleterFunc]] = None,
|
1601
|
-
) ->
|
1585
|
+
) -> list[str]:
|
1602
1586
|
"""Tab completes based on a fixed position in the input string.
|
1603
1587
|
|
1604
1588
|
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
@@ -1626,10 +1610,7 @@ class Cmd(cmd.Cmd):
|
|
1626
1610
|
|
1627
1611
|
# Check if token is at an index in the dictionary
|
1628
1612
|
match_against: Optional[Union[Iterable[str], CompleterFunc]]
|
1629
|
-
|
1630
|
-
match_against = index_dict[index]
|
1631
|
-
else:
|
1632
|
-
match_against = all_else
|
1613
|
+
match_against = index_dict.get(index, all_else)
|
1633
1614
|
|
1634
1615
|
# Perform tab completion using a Iterable
|
1635
1616
|
if isinstance(match_against, Iterable):
|
@@ -1642,9 +1623,15 @@ class Cmd(cmd.Cmd):
|
|
1642
1623
|
return matches
|
1643
1624
|
|
1644
1625
|
def path_complete(
|
1645
|
-
self,
|
1646
|
-
|
1647
|
-
|
1626
|
+
self,
|
1627
|
+
text: str,
|
1628
|
+
line: str,
|
1629
|
+
begidx: int, # noqa: ARG002
|
1630
|
+
endidx: int,
|
1631
|
+
*,
|
1632
|
+
path_filter: Optional[Callable[[str], bool]] = None,
|
1633
|
+
) -> list[str]:
|
1634
|
+
"""Perform completion of local file system paths.
|
1648
1635
|
|
1649
1636
|
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
1650
1637
|
:param line: the current input line with leading whitespace removed
|
@@ -1657,7 +1644,7 @@ class Cmd(cmd.Cmd):
|
|
1657
1644
|
"""
|
1658
1645
|
|
1659
1646
|
# Used to complete ~ and ~user strings
|
1660
|
-
def complete_users() ->
|
1647
|
+
def complete_users() -> list[str]:
|
1661
1648
|
users = []
|
1662
1649
|
|
1663
1650
|
# Windows lacks the pwd module so we can't get a list of users.
|
@@ -1728,12 +1715,11 @@ class Cmd(cmd.Cmd):
|
|
1728
1715
|
return complete_users()
|
1729
1716
|
|
1730
1717
|
# Otherwise expand the user dir
|
1731
|
-
|
1732
|
-
search_str = os.path.expanduser(search_str)
|
1718
|
+
search_str = os.path.expanduser(search_str)
|
1733
1719
|
|
1734
|
-
|
1735
|
-
|
1736
|
-
|
1720
|
+
# Get what we need to restore the original tilde path later
|
1721
|
+
orig_tilde_path = text[:sep_index]
|
1722
|
+
expanded_tilde_path = os.path.expanduser(orig_tilde_path)
|
1737
1723
|
|
1738
1724
|
# If the search text does not have a directory, then use the cwd
|
1739
1725
|
elif not os.path.dirname(text):
|
@@ -1772,10 +1758,7 @@ class Cmd(cmd.Cmd):
|
|
1772
1758
|
|
1773
1759
|
# Remove cwd if it was added to match the text readline expects
|
1774
1760
|
if cwd_added:
|
1775
|
-
if cwd == os.path.sep
|
1776
|
-
to_replace = cwd
|
1777
|
-
else:
|
1778
|
-
to_replace = cwd + os.path.sep
|
1761
|
+
to_replace = cwd if cwd == os.path.sep else cwd + os.path.sep
|
1779
1762
|
matches = [cur_path.replace(to_replace, '', 1) for cur_path in matches]
|
1780
1763
|
|
1781
1764
|
# Restore the tilde string if we expanded one to match the text readline expects
|
@@ -1784,8 +1767,8 @@ class Cmd(cmd.Cmd):
|
|
1784
1767
|
|
1785
1768
|
return matches
|
1786
1769
|
|
1787
|
-
def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *, complete_blank: bool = False) ->
|
1788
|
-
"""
|
1770
|
+
def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *, complete_blank: bool = False) -> list[str]:
|
1771
|
+
"""Perform completion of executables either in a user's path or a given path.
|
1789
1772
|
|
1790
1773
|
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
1791
1774
|
:param line: the current input line with leading whitespace removed
|
@@ -1804,15 +1787,15 @@ class Cmd(cmd.Cmd):
|
|
1804
1787
|
return utils.get_exes_in_path(text)
|
1805
1788
|
|
1806
1789
|
# Otherwise look for executables in the given path
|
1807
|
-
|
1808
|
-
|
1809
|
-
|
1810
|
-
|
1790
|
+
return self.path_complete(
|
1791
|
+
text, line, begidx, endidx, path_filter=lambda path: os.path.isdir(path) or os.access(path, os.X_OK)
|
1792
|
+
)
|
1793
|
+
|
1794
|
+
def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, compfunc: CompleterFunc) -> list[str]:
|
1795
|
+
"""First tab completion function for all commands, called by complete().
|
1811
1796
|
|
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
1797
|
It determines if it should tab complete for redirection (|, >, >>) or use the
|
1815
|
-
completer function for the current command
|
1798
|
+
completer function for the current command.
|
1816
1799
|
|
1817
1800
|
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
1818
1801
|
:param line: the current input line with leading whitespace removed
|
@@ -1878,20 +1861,21 @@ class Cmd(cmd.Cmd):
|
|
1878
1861
|
if do_shell_completion:
|
1879
1862
|
return self.shell_cmd_complete(text, line, begidx, endidx)
|
1880
1863
|
|
1881
|
-
|
1864
|
+
if do_path_completion:
|
1882
1865
|
return self.path_complete(text, line, begidx, endidx)
|
1883
1866
|
|
1884
1867
|
# If there were redirection strings anywhere on the command line, then we
|
1885
1868
|
# are no longer tab completing for the current command
|
1886
|
-
|
1869
|
+
if has_redirection:
|
1887
1870
|
return []
|
1888
1871
|
|
1889
1872
|
# Call the command's completer function
|
1890
1873
|
return compfunc(text, line, begidx, endidx)
|
1891
1874
|
|
1892
1875
|
@staticmethod
|
1893
|
-
def _pad_matches_to_display(matches_to_display:
|
1894
|
-
"""
|
1876
|
+
def _pad_matches_to_display(matches_to_display: list[str]) -> tuple[list[str], int]: # pragma: no cover
|
1877
|
+
"""Add padding to the matches being displayed as tab completion suggestions.
|
1878
|
+
|
1895
1879
|
The default padding of readline/pyreadine is small and not visually appealing
|
1896
1880
|
especially if matches have spaces. It appears very squished together.
|
1897
1881
|
|
@@ -1912,9 +1896,9 @@ class Cmd(cmd.Cmd):
|
|
1912
1896
|
return [cur_match + padding for cur_match in matches_to_display], len(padding)
|
1913
1897
|
|
1914
1898
|
def _display_matches_gnu_readline(
|
1915
|
-
self, substitution: str, matches:
|
1899
|
+
self, substitution: str, matches: list[str], longest_match_length: int
|
1916
1900
|
) -> None: # pragma: no cover
|
1917
|
-
"""
|
1901
|
+
"""Print a match list using GNU readline's rl_display_match_list().
|
1918
1902
|
|
1919
1903
|
:param substitution: the substitution written to the command line
|
1920
1904
|
:param matches: the tab completion matches to display
|
@@ -1944,8 +1928,7 @@ class Cmd(cmd.Cmd):
|
|
1944
1928
|
|
1945
1929
|
for cur_match in matches_to_display:
|
1946
1930
|
cur_length = ansi.style_aware_wcswidth(cur_match)
|
1947
|
-
|
1948
|
-
longest_match_length = cur_length
|
1931
|
+
longest_match_length = max(longest_match_length, cur_length)
|
1949
1932
|
else:
|
1950
1933
|
matches_to_display = matches
|
1951
1934
|
|
@@ -1960,7 +1943,7 @@ class Cmd(cmd.Cmd):
|
|
1960
1943
|
|
1961
1944
|
# rl_display_match_list() expects matches to be in argv format where
|
1962
1945
|
# substitution is the first element, followed by the matches, and then a NULL.
|
1963
|
-
strings_array = cast(
|
1946
|
+
strings_array = cast(list[Optional[bytes]], (ctypes.c_char_p * (1 + len(encoded_matches) + 1))())
|
1964
1947
|
|
1965
1948
|
# Copy in the encoded strings and add a NULL to the end
|
1966
1949
|
strings_array[0] = encoded_substitution
|
@@ -1973,8 +1956,8 @@ class Cmd(cmd.Cmd):
|
|
1973
1956
|
# Redraw prompt and input line
|
1974
1957
|
rl_force_redisplay()
|
1975
1958
|
|
1976
|
-
def _display_matches_pyreadline(self, matches:
|
1977
|
-
"""
|
1959
|
+
def _display_matches_pyreadline(self, matches: list[str]) -> None: # pragma: no cover
|
1960
|
+
"""Print a match list using pyreadline3's _display_completions().
|
1978
1961
|
|
1979
1962
|
:param matches: the tab completion matches to display
|
1980
1963
|
"""
|
@@ -1997,10 +1980,7 @@ class Cmd(cmd.Cmd):
|
|
1997
1980
|
# Otherwise use pyreadline3's formatter
|
1998
1981
|
else:
|
1999
1982
|
# 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
|
1983
|
+
matches_to_display = self.display_matches if self.display_matches else matches
|
2004
1984
|
|
2005
1985
|
# Add padding for visual appeal
|
2006
1986
|
matches_to_display, _ = self._pad_matches_to_display(matches_to_display)
|
@@ -2009,15 +1989,15 @@ class Cmd(cmd.Cmd):
|
|
2009
1989
|
orig_pyreadline_display(matches_to_display)
|
2010
1990
|
|
2011
1991
|
@staticmethod
|
2012
|
-
def _determine_ap_completer_type(parser: argparse.ArgumentParser) ->
|
2013
|
-
"""
|
2014
|
-
|
2015
|
-
set, then use argparse_completer.DEFAULT_AP_COMPLETER.
|
1992
|
+
def _determine_ap_completer_type(parser: argparse.ArgumentParser) -> type[argparse_completer.ArgparseCompleter]:
|
1993
|
+
"""Determine what type of ArgparseCompleter to use on a given parser.
|
1994
|
+
|
1995
|
+
If the parser does not have one set, then use argparse_completer.DEFAULT_AP_COMPLETER.
|
2016
1996
|
|
2017
1997
|
:param parser: the parser to examine
|
2018
1998
|
:return: type of ArgparseCompleter
|
2019
1999
|
"""
|
2020
|
-
Completer = Optional[
|
2000
|
+
Completer = Optional[type[argparse_completer.ArgparseCompleter]] # noqa: N806
|
2021
2001
|
completer_type: Completer = parser.get_ap_completer_type() # type: ignore[attr-defined]
|
2022
2002
|
|
2023
2003
|
if completer_type is None:
|
@@ -2027,8 +2007,7 @@ class Cmd(cmd.Cmd):
|
|
2027
2007
|
def _perform_completion(
|
2028
2008
|
self, text: str, line: str, begidx: int, endidx: int, custom_settings: Optional[utils.CustomCompletionSettings] = None
|
2029
2009
|
) -> None:
|
2030
|
-
"""
|
2031
|
-
Helper function for complete() that performs the actual completion
|
2010
|
+
"""Perform the actual completion, helper function for complete().
|
2032
2011
|
|
2033
2012
|
:param text: the string prefix we are attempting to match (all matches must begin with it)
|
2034
2013
|
:param line: the current input line with leading whitespace removed
|
@@ -2106,12 +2085,11 @@ class Cmd(cmd.Cmd):
|
|
2106
2085
|
completer_func = self.completedefault # type: ignore[assignment]
|
2107
2086
|
|
2108
2087
|
# Not a recognized macro or command
|
2088
|
+
# Check if this command should be run as a shell command
|
2089
|
+
elif self.default_to_shell and command in utils.get_exes_in_path(command):
|
2090
|
+
completer_func = self.path_complete
|
2109
2091
|
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]
|
2092
|
+
completer_func = self.completedefault # type: ignore[assignment]
|
2115
2093
|
|
2116
2094
|
# Otherwise we are completing the command token or performing custom completion
|
2117
2095
|
else:
|
@@ -2192,10 +2170,7 @@ class Cmd(cmd.Cmd):
|
|
2192
2170
|
|
2193
2171
|
if add_quote:
|
2194
2172
|
# 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 = '"'
|
2173
|
+
completion_token_quote = "'" if any('"' in match for match in self.completion_matches) else '"'
|
2199
2174
|
|
2200
2175
|
self.completion_matches = [completion_token_quote + match for match in self.completion_matches]
|
2201
2176
|
|
@@ -2210,7 +2185,7 @@ class Cmd(cmd.Cmd):
|
|
2210
2185
|
def complete( # type: ignore[override]
|
2211
2186
|
self, text: str, state: int, custom_settings: Optional[utils.CustomCompletionSettings] = None
|
2212
2187
|
) -> Optional[str]:
|
2213
|
-
"""Override of cmd's complete method which returns the next possible completion for 'text'
|
2188
|
+
"""Override of cmd's complete method which returns the next possible completion for 'text'.
|
2214
2189
|
|
2215
2190
|
This completer function is called by readline as complete(text, state), for state in 0, 1, 2, …,
|
2216
2191
|
until it returns a non-string value. It should return the next possible completion starting with text.
|
@@ -2304,7 +2279,7 @@ class Cmd(cmd.Cmd):
|
|
2304
2279
|
ansi.style_aware_write(sys.stdout, '\n' + err_str + '\n')
|
2305
2280
|
rl_force_redisplay()
|
2306
2281
|
return None
|
2307
|
-
except Exception as ex:
|
2282
|
+
except Exception as ex: # noqa: BLE001
|
2308
2283
|
# Insert a newline so the exception doesn't print in the middle of the command line being tab completed
|
2309
2284
|
self.perror()
|
2310
2285
|
self.pexcept(ex)
|
@@ -2312,32 +2287,32 @@ class Cmd(cmd.Cmd):
|
|
2312
2287
|
return None
|
2313
2288
|
|
2314
2289
|
def in_script(self) -> bool:
|
2315
|
-
"""Return whether a text script is running"""
|
2290
|
+
"""Return whether a text script is running."""
|
2316
2291
|
return self._current_script_dir is not None
|
2317
2292
|
|
2318
2293
|
def in_pyscript(self) -> bool:
|
2319
|
-
"""Return whether running inside a Python shell or pyscript"""
|
2294
|
+
"""Return whether running inside a Python shell or pyscript."""
|
2320
2295
|
return self._in_py
|
2321
2296
|
|
2322
2297
|
@property
|
2323
|
-
def aliases(self) ->
|
2324
|
-
"""Read-only property to access the aliases stored in the StatementParser"""
|
2298
|
+
def aliases(self) -> dict[str, str]:
|
2299
|
+
"""Read-only property to access the aliases stored in the StatementParser."""
|
2325
2300
|
return self.statement_parser.aliases
|
2326
2301
|
|
2327
|
-
def get_names(self) ->
|
2302
|
+
def get_names(self) -> list[str]:
|
2328
2303
|
"""Return an alphabetized list of names comprising the attributes of the cmd2 class instance."""
|
2329
2304
|
return dir(self)
|
2330
2305
|
|
2331
|
-
def get_all_commands(self) ->
|
2332
|
-
"""Return a list of all commands"""
|
2306
|
+
def get_all_commands(self) -> list[str]:
|
2307
|
+
"""Return a list of all commands."""
|
2333
2308
|
return [
|
2334
2309
|
name[len(constants.COMMAND_FUNC_PREFIX) :]
|
2335
2310
|
for name in self.get_names()
|
2336
2311
|
if name.startswith(constants.COMMAND_FUNC_PREFIX) and callable(getattr(self, name))
|
2337
2312
|
]
|
2338
2313
|
|
2339
|
-
def get_visible_commands(self) ->
|
2340
|
-
"""Return a list of commands that have not been hidden or disabled"""
|
2314
|
+
def get_visible_commands(self) -> list[str]:
|
2315
|
+
"""Return a list of commands that have not been hidden or disabled."""
|
2341
2316
|
return [
|
2342
2317
|
command
|
2343
2318
|
for command in self.get_all_commands()
|
@@ -2347,9 +2322,9 @@ class Cmd(cmd.Cmd):
|
|
2347
2322
|
# Table displayed when tab completing aliases
|
2348
2323
|
_alias_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None)
|
2349
2324
|
|
2350
|
-
def _get_alias_completion_items(self) ->
|
2351
|
-
"""Return list of alias names and values as CompletionItems"""
|
2352
|
-
results:
|
2325
|
+
def _get_alias_completion_items(self) -> list[CompletionItem]:
|
2326
|
+
"""Return list of alias names and values as CompletionItems."""
|
2327
|
+
results: list[CompletionItem] = []
|
2353
2328
|
|
2354
2329
|
for cur_key in self.aliases:
|
2355
2330
|
row_data = [self.aliases[cur_key]]
|
@@ -2360,9 +2335,9 @@ class Cmd(cmd.Cmd):
|
|
2360
2335
|
# Table displayed when tab completing macros
|
2361
2336
|
_macro_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None)
|
2362
2337
|
|
2363
|
-
def _get_macro_completion_items(self) ->
|
2364
|
-
"""Return list of macro names and values as CompletionItems"""
|
2365
|
-
results:
|
2338
|
+
def _get_macro_completion_items(self) -> list[CompletionItem]:
|
2339
|
+
"""Return list of macro names and values as CompletionItems."""
|
2340
|
+
results: list[CompletionItem] = []
|
2366
2341
|
|
2367
2342
|
for cur_key in self.macros:
|
2368
2343
|
row_data = [self.macros[cur_key].value]
|
@@ -2373,9 +2348,9 @@ class Cmd(cmd.Cmd):
|
|
2373
2348
|
# Table displayed when tab completing Settables
|
2374
2349
|
_settable_completion_table = SimpleTable([Column('Value', width=30), Column('Description', width=60)], divider_char=None)
|
2375
2350
|
|
2376
|
-
def _get_settable_completion_items(self) ->
|
2377
|
-
"""Return list of Settable names, values, and descriptions as CompletionItems"""
|
2378
|
-
results:
|
2351
|
+
def _get_settable_completion_items(self) -> list[CompletionItem]:
|
2352
|
+
"""Return list of Settable names, values, and descriptions as CompletionItems."""
|
2353
|
+
results: list[CompletionItem] = []
|
2379
2354
|
|
2380
2355
|
for cur_key in self.settables:
|
2381
2356
|
row_data = [self.settables[cur_key].get_value(), self.settables[cur_key].description]
|
@@ -2383,15 +2358,15 @@ class Cmd(cmd.Cmd):
|
|
2383
2358
|
|
2384
2359
|
return results
|
2385
2360
|
|
2386
|
-
def _get_commands_aliases_and_macros_for_completion(self) ->
|
2387
|
-
"""Return a list of visible commands, aliases, and macros for tab completion"""
|
2361
|
+
def _get_commands_aliases_and_macros_for_completion(self) -> list[str]:
|
2362
|
+
"""Return a list of visible commands, aliases, and macros for tab completion."""
|
2388
2363
|
visible_commands = set(self.get_visible_commands())
|
2389
2364
|
alias_names = set(self.aliases)
|
2390
2365
|
macro_names = set(self.macros)
|
2391
2366
|
return list(visible_commands | alias_names | macro_names)
|
2392
2367
|
|
2393
|
-
def get_help_topics(self) ->
|
2394
|
-
"""Return a list of help topics"""
|
2368
|
+
def get_help_topics(self) -> list[str]:
|
2369
|
+
"""Return a list of help topics."""
|
2395
2370
|
all_topics = [
|
2396
2371
|
name[len(constants.HELP_FUNC_PREFIX) :]
|
2397
2372
|
for name in self.get_names()
|
@@ -2401,7 +2376,7 @@ class Cmd(cmd.Cmd):
|
|
2401
2376
|
# Filter out hidden and disabled commands
|
2402
2377
|
return [topic for topic in all_topics if topic not in self.hidden_commands and topic not in self.disabled_commands]
|
2403
2378
|
|
2404
|
-
def sigint_handler(self, signum: int, _: Optional[FrameType]) -> None:
|
2379
|
+
def sigint_handler(self, signum: int, _: Optional[FrameType]) -> None: # noqa: ARG002
|
2405
2380
|
"""Signal handler for SIGINTs which typically come from Ctrl-C events.
|
2406
2381
|
|
2407
2382
|
If you need custom SIGINT behavior, then override this method.
|
@@ -2424,8 +2399,7 @@ class Cmd(cmd.Cmd):
|
|
2424
2399
|
self._raise_keyboard_interrupt()
|
2425
2400
|
|
2426
2401
|
def termination_signal_handler(self, signum: int, _: Optional[FrameType]) -> None:
|
2427
|
-
"""
|
2428
|
-
Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac.
|
2402
|
+
"""Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac.
|
2429
2403
|
|
2430
2404
|
SIGHUP - received when terminal window is closed
|
2431
2405
|
SIGTERM - received when this app has been requested to terminate
|
@@ -2441,56 +2415,50 @@ class Cmd(cmd.Cmd):
|
|
2441
2415
|
sys.exit(128 + signum)
|
2442
2416
|
|
2443
2417
|
def _raise_keyboard_interrupt(self) -> None:
|
2444
|
-
"""
|
2418
|
+
"""Raise a KeyboardInterrupt."""
|
2445
2419
|
raise KeyboardInterrupt("Got a keyboard interrupt")
|
2446
2420
|
|
2447
2421
|
def precmd(self, statement: Union[Statement, str]) -> Statement:
|
2448
|
-
"""
|
2449
|
-
[cmd2.Cmd.onecmd][] and after adding it to history.
|
2422
|
+
"""Ran just before the command is executed by [cmd2.Cmd.onecmd][] and after adding it to history (cmd Hook method).
|
2450
2423
|
|
2451
2424
|
:param statement: subclass of str which also contains the parsed input
|
2452
2425
|
:return: a potentially modified version of the input Statement object
|
2453
2426
|
|
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.
|
2427
|
+
See [cmd2.Cmd.register_postparsing_hook][] and [cmd2.Cmd.register_precmd_hook][] for more robust ways
|
2428
|
+
to run hooks before the command is executed. See [Hooks](../features/hooks.md) for more information.
|
2458
2429
|
"""
|
2459
2430
|
return Statement(statement) if not isinstance(statement, Statement) else statement
|
2460
2431
|
|
2461
|
-
def postcmd(self, stop: bool, statement: Union[Statement, str]) -> bool:
|
2462
|
-
"""
|
2463
|
-
[cmd2.Cmd.onecmd][].
|
2432
|
+
def postcmd(self, stop: bool, statement: Union[Statement, str]) -> bool: # noqa: ARG002
|
2433
|
+
"""Ran just after a command is executed by [cmd2.Cmd.onecmd][] (cmd inherited Hook method).
|
2464
2434
|
|
2465
2435
|
:param stop: return `True` to request the command loop terminate
|
2466
2436
|
:param statement: subclass of str which also contains the parsed input
|
2467
2437
|
|
2468
2438
|
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.
|
2439
|
+
to run hooks after the command is executed. See [Hooks](../features/hooks.md) for more information.
|
2471
2440
|
"""
|
2472
2441
|
return stop
|
2473
2442
|
|
2474
2443
|
def preloop(self) -> None:
|
2475
|
-
"""
|
2476
|
-
|
2444
|
+
"""Ran once when the [cmd2.Cmd.cmdloop][] method is called (cmd inherited Hook method).
|
2445
|
+
|
2446
|
+
This method is a stub that does nothing and exists to be overridden by subclasses.
|
2477
2447
|
|
2478
|
-
See [cmd2.Cmd.register_preloop_hook][] for a more robust
|
2479
|
-
|
2480
|
-
[Hooks](../features/hooks.md) for more information.
|
2448
|
+
See [cmd2.Cmd.register_preloop_hook][] for a more robust wayto run hooks before the command loop begins.
|
2449
|
+
See [Hooks](../features/hooks.md) for more information.
|
2481
2450
|
"""
|
2482
|
-
pass
|
2483
2451
|
|
2484
2452
|
def postloop(self) -> None:
|
2485
|
-
"""
|
2453
|
+
"""Ran once when the [cmd2.Cmd.cmdloop][] method is about to return (cmd inherited Hook Method).
|
2486
2454
|
|
2487
|
-
|
2488
|
-
|
2489
|
-
[
|
2455
|
+
This method is a stub that does nothing and exists to be overridden by subclasses.
|
2456
|
+
|
2457
|
+
See [cmd2.Cmd.register_postloop_hook][] for a more robust way to run hooks after the command loop completes.
|
2458
|
+
See [Hooks](../features/hooks.md) for more information.
|
2490
2459
|
"""
|
2491
|
-
pass
|
2492
2460
|
|
2493
|
-
def parseline(self, line: str) ->
|
2461
|
+
def parseline(self, line: str) -> tuple[str, str, str]:
|
2494
2462
|
"""Parse the line into a command name and a string containing the arguments.
|
2495
2463
|
|
2496
2464
|
NOTE: This is an override of a parent class method. It is only used by other parent class methods.
|
@@ -2549,7 +2517,7 @@ class Cmd(cmd.Cmd):
|
|
2549
2517
|
if stop:
|
2550
2518
|
# we should not run the command, but
|
2551
2519
|
# we need to run the finalization hooks
|
2552
|
-
raise EmptyStatement
|
2520
|
+
raise EmptyStatement # noqa: TRY301
|
2553
2521
|
|
2554
2522
|
redir_saved_state: Optional[utils.RedirectionSavedState] = None
|
2555
2523
|
|
@@ -2562,7 +2530,7 @@ class Cmd(cmd.Cmd):
|
|
2562
2530
|
|
2563
2531
|
redir_saved_state = self._redirect_output(statement)
|
2564
2532
|
|
2565
|
-
timestart = datetime.datetime.now()
|
2533
|
+
timestart = datetime.datetime.now(tz=datetime.timezone.utc)
|
2566
2534
|
|
2567
2535
|
# precommand hooks
|
2568
2536
|
precmd_data = plugin.PrecommandData(statement)
|
@@ -2588,7 +2556,7 @@ class Cmd(cmd.Cmd):
|
|
2588
2556
|
stop = self.postcmd(stop, statement)
|
2589
2557
|
|
2590
2558
|
if self.timing:
|
2591
|
-
self.pfeedback(f'Elapsed: {datetime.datetime.now() - timestart}')
|
2559
|
+
self.pfeedback(f'Elapsed: {datetime.datetime.now(tz=datetime.timezone.utc) - timestart}')
|
2592
2560
|
finally:
|
2593
2561
|
# Get sigint protection while we restore stuff
|
2594
2562
|
with self.sigint_protection:
|
@@ -2605,43 +2573,43 @@ class Cmd(cmd.Cmd):
|
|
2605
2573
|
self.perror(f"Invalid syntax: {ex}")
|
2606
2574
|
except RedirectionError as ex:
|
2607
2575
|
self.perror(ex)
|
2608
|
-
except KeyboardInterrupt
|
2576
|
+
except KeyboardInterrupt:
|
2609
2577
|
if raise_keyboard_interrupt and not stop:
|
2610
|
-
raise
|
2578
|
+
raise
|
2611
2579
|
except SystemExit as ex:
|
2612
2580
|
if isinstance(ex.code, int):
|
2613
2581
|
self.exit_code = ex.code
|
2614
2582
|
stop = True
|
2615
2583
|
except PassThroughException as ex:
|
2616
|
-
raise ex.wrapped_ex
|
2617
|
-
except Exception as ex:
|
2584
|
+
raise ex.wrapped_ex from None
|
2585
|
+
except Exception as ex: # noqa: BLE001
|
2618
2586
|
self.pexcept(ex)
|
2619
2587
|
finally:
|
2620
2588
|
try:
|
2621
2589
|
stop = self._run_cmdfinalization_hooks(stop, statement)
|
2622
|
-
except KeyboardInterrupt
|
2590
|
+
except KeyboardInterrupt:
|
2623
2591
|
if raise_keyboard_interrupt and not stop:
|
2624
|
-
raise
|
2592
|
+
raise
|
2625
2593
|
except SystemExit as ex:
|
2626
2594
|
if isinstance(ex.code, int):
|
2627
2595
|
self.exit_code = ex.code
|
2628
2596
|
stop = True
|
2629
2597
|
except PassThroughException as ex:
|
2630
|
-
raise ex.wrapped_ex
|
2631
|
-
except Exception as ex:
|
2598
|
+
raise ex.wrapped_ex from None
|
2599
|
+
except Exception as ex: # noqa: BLE001
|
2632
2600
|
self.pexcept(ex)
|
2633
2601
|
|
2634
2602
|
return stop
|
2635
2603
|
|
2636
2604
|
def _run_cmdfinalization_hooks(self, stop: bool, statement: Optional[Statement]) -> bool:
|
2637
|
-
"""Run the command finalization hooks"""
|
2605
|
+
"""Run the command finalization hooks."""
|
2638
2606
|
with self.sigint_protection:
|
2639
2607
|
if not sys.platform.startswith('win') and self.stdin.isatty():
|
2640
2608
|
# Before the next command runs, fix any terminal problems like those
|
2641
2609
|
# caused by certain binary characters having been printed to it.
|
2642
2610
|
import subprocess
|
2643
2611
|
|
2644
|
-
proc = subprocess.Popen(['stty', 'sane'])
|
2612
|
+
proc = subprocess.Popen(['stty', 'sane']) # noqa: S603, S607
|
2645
2613
|
proc.communicate()
|
2646
2614
|
|
2647
2615
|
data = plugin.CommandFinalizationData(stop, statement)
|
@@ -2653,13 +2621,13 @@ class Cmd(cmd.Cmd):
|
|
2653
2621
|
|
2654
2622
|
def runcmds_plus_hooks(
|
2655
2623
|
self,
|
2656
|
-
cmds: Union[
|
2624
|
+
cmds: Union[list[HistoryItem], list[str]],
|
2657
2625
|
*,
|
2658
2626
|
add_to_history: bool = True,
|
2659
2627
|
stop_on_keyboard_interrupt: bool = False,
|
2660
2628
|
) -> bool:
|
2661
|
-
"""
|
2662
|
-
|
2629
|
+
"""Run commands in an automated fashion from sources like text scripts or history replays.
|
2630
|
+
|
2663
2631
|
The prompt and command line for each command will be printed if echo is True.
|
2664
2632
|
|
2665
2633
|
:param cmds: commands to run
|
@@ -2671,7 +2639,7 @@ class Cmd(cmd.Cmd):
|
|
2671
2639
|
"""
|
2672
2640
|
for line in cmds:
|
2673
2641
|
if isinstance(line, HistoryItem):
|
2674
|
-
line = line.raw
|
2642
|
+
line = line.raw # noqa: PLW2901
|
2675
2643
|
|
2676
2644
|
if self.echo:
|
2677
2645
|
self.poutput(f'{self.prompt}{line}')
|
@@ -2706,7 +2674,7 @@ class Cmd(cmd.Cmd):
|
|
2706
2674
|
"""
|
2707
2675
|
|
2708
2676
|
def combine_rl_history(statement: Statement) -> None:
|
2709
|
-
"""Combine all lines of a multiline command into a single readline history entry"""
|
2677
|
+
"""Combine all lines of a multiline command into a single readline history entry."""
|
2710
2678
|
if orig_rl_history_length is None or not statement.multiline_command:
|
2711
2679
|
return
|
2712
2680
|
|
@@ -2773,15 +2741,13 @@ class Cmd(cmd.Cmd):
|
|
2773
2741
|
|
2774
2742
|
if not statement.command:
|
2775
2743
|
raise EmptyStatement
|
2776
|
-
|
2777
|
-
|
2778
|
-
combine_rl_history(statement)
|
2744
|
+
# If necessary, update history with completed multiline command.
|
2745
|
+
combine_rl_history(statement)
|
2779
2746
|
|
2780
2747
|
return statement
|
2781
2748
|
|
2782
2749
|
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
|
2750
|
+
"""Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved.
|
2785
2751
|
|
2786
2752
|
:param line: the line being parsed
|
2787
2753
|
:param orig_rl_history_length: Optional length of the readline history before the current command was typed.
|
@@ -2806,7 +2772,7 @@ class Cmd(cmd.Cmd):
|
|
2806
2772
|
orig_rl_history_length = None
|
2807
2773
|
|
2808
2774
|
# Check if this command matches a macro and wasn't already processed to avoid an infinite loop
|
2809
|
-
if statement.command in self.macros
|
2775
|
+
if statement.command in self.macros and statement.command not in used_macros:
|
2810
2776
|
used_macros.append(statement.command)
|
2811
2777
|
resolve_result = self._resolve_macro(statement)
|
2812
2778
|
if resolve_result is None:
|
@@ -2834,13 +2800,12 @@ class Cmd(cmd.Cmd):
|
|
2834
2800
|
return statement
|
2835
2801
|
|
2836
2802
|
def _resolve_macro(self, statement: Statement) -> Optional[str]:
|
2837
|
-
"""
|
2838
|
-
Resolve a macro and return the resulting string
|
2803
|
+
"""Resolve a macro and return the resulting string.
|
2839
2804
|
|
2840
2805
|
:param statement: the parsed statement from the command line
|
2841
2806
|
:return: the resolved macro or None on error
|
2842
2807
|
"""
|
2843
|
-
if statement.command not in self.macros
|
2808
|
+
if statement.command not in self.macros:
|
2844
2809
|
raise KeyError(f"{statement.command} is not a macro")
|
2845
2810
|
|
2846
2811
|
macro = self.macros[statement.command]
|
@@ -2881,7 +2846,6 @@ class Cmd(cmd.Cmd):
|
|
2881
2846
|
:return: A bool telling if an error occurred and a utils.RedirectionSavedState object
|
2882
2847
|
:raises RedirectionError: if an error occurs trying to pipe or redirect
|
2883
2848
|
"""
|
2884
|
-
import io
|
2885
2849
|
import subprocess
|
2886
2850
|
|
2887
2851
|
# Initialize the redirection saved state
|
@@ -2901,13 +2865,13 @@ class Cmd(cmd.Cmd):
|
|
2901
2865
|
read_fd, write_fd = os.pipe()
|
2902
2866
|
|
2903
2867
|
# Open each side of the pipe
|
2904
|
-
subproc_stdin =
|
2905
|
-
new_stdout: TextIO = cast(TextIO,
|
2868
|
+
subproc_stdin = open(read_fd) # noqa: SIM115
|
2869
|
+
new_stdout: TextIO = cast(TextIO, open(write_fd, 'w')) # noqa: SIM115
|
2906
2870
|
|
2907
2871
|
# Create pipe process in a separate group to isolate our signals from it. If a Ctrl-C event occurs,
|
2908
2872
|
# our sigint handler will forward it only to the most recent pipe process. This makes sure pipe
|
2909
2873
|
# processes close in the right order (most recent first).
|
2910
|
-
kwargs:
|
2874
|
+
kwargs: dict[str, Any] = {}
|
2911
2875
|
if sys.platform == 'win32':
|
2912
2876
|
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
|
2913
2877
|
else:
|
@@ -2919,7 +2883,7 @@ class Cmd(cmd.Cmd):
|
|
2919
2883
|
kwargs['executable'] = shell
|
2920
2884
|
|
2921
2885
|
# 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]
|
2886
|
+
proc = subprocess.Popen( # type: ignore[call-overload] # noqa: S602
|
2923
2887
|
statement.pipe_to,
|
2924
2888
|
stdin=subproc_stdin,
|
2925
2889
|
stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, # type: ignore[unreachable]
|
@@ -2932,20 +2896,17 @@ class Cmd(cmd.Cmd):
|
|
2932
2896
|
# like: !ls -l | grep user | wc -l > out.txt. But this makes it difficult to know if the pipe process
|
2933
2897
|
# started OK, since the shell itself always starts. Therefore, we will wait a short time and check
|
2934
2898
|
# if the pipe process is still running.
|
2935
|
-
|
2899
|
+
with contextlib.suppress(subprocess.TimeoutExpired):
|
2936
2900
|
proc.wait(0.2)
|
2937
|
-
except subprocess.TimeoutExpired:
|
2938
|
-
pass
|
2939
2901
|
|
2940
2902
|
# Check if the pipe process already exited
|
2941
2903
|
if proc.returncode is not None:
|
2942
2904
|
subproc_stdin.close()
|
2943
2905
|
new_stdout.close()
|
2944
2906
|
raise RedirectionError(f'Pipe process exited with code {proc.returncode} before command could run')
|
2945
|
-
|
2946
|
-
|
2947
|
-
|
2948
|
-
sys.stdout = self.stdout = new_stdout
|
2907
|
+
redir_saved_state.redirecting = True # type: ignore[unreachable]
|
2908
|
+
cmd_pipe_proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr)
|
2909
|
+
sys.stdout = self.stdout = new_stdout
|
2949
2910
|
|
2950
2911
|
elif statement.output:
|
2951
2912
|
if statement.output_to:
|
@@ -2954,9 +2915,9 @@ class Cmd(cmd.Cmd):
|
|
2954
2915
|
mode = 'a' if statement.output == constants.REDIRECTION_APPEND else 'w'
|
2955
2916
|
try:
|
2956
2917
|
# Use line buffering
|
2957
|
-
new_stdout = cast(TextIO, open(utils.strip_quotes(statement.output_to), mode=mode, buffering=1))
|
2918
|
+
new_stdout = cast(TextIO, open(utils.strip_quotes(statement.output_to), mode=mode, buffering=1)) # noqa: SIM115
|
2958
2919
|
except OSError as ex:
|
2959
|
-
raise RedirectionError(
|
2920
|
+
raise RedirectionError('Failed to redirect output') from ex
|
2960
2921
|
|
2961
2922
|
redir_saved_state.redirecting = True
|
2962
2923
|
sys.stdout = self.stdout = new_stdout
|
@@ -2975,7 +2936,7 @@ class Cmd(cmd.Cmd):
|
|
2975
2936
|
# no point opening up the temporary file
|
2976
2937
|
current_paste_buffer = get_paste_buffer()
|
2977
2938
|
# create a temporary file to store output
|
2978
|
-
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+"))
|
2939
|
+
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+")) # noqa: SIM115
|
2979
2940
|
redir_saved_state.redirecting = True
|
2980
2941
|
sys.stdout = self.stdout = new_stdout
|
2981
2942
|
|
@@ -2990,7 +2951,7 @@ class Cmd(cmd.Cmd):
|
|
2990
2951
|
return redir_saved_state
|
2991
2952
|
|
2992
2953
|
def _restore_output(self, statement: Statement, saved_redir_state: utils.RedirectionSavedState) -> None:
|
2993
|
-
"""
|
2954
|
+
"""Handle restoring state after output redirection.
|
2994
2955
|
|
2995
2956
|
:param statement: Statement object which contains the parsed input from the user
|
2996
2957
|
:param saved_redir_state: contains information needed to restore state data
|
@@ -3001,11 +2962,9 @@ class Cmd(cmd.Cmd):
|
|
3001
2962
|
self.stdout.seek(0)
|
3002
2963
|
write_to_paste_buffer(self.stdout.read())
|
3003
2964
|
|
3004
|
-
|
2965
|
+
with contextlib.suppress(BrokenPipeError):
|
3005
2966
|
# Close the file or pipe that stdout was redirected to
|
3006
2967
|
self.stdout.close()
|
3007
|
-
except BrokenPipeError:
|
3008
|
-
pass
|
3009
2968
|
|
3010
2969
|
# Restore the stdout values
|
3011
2970
|
self.stdout = cast(TextIO, saved_redir_state.saved_self_stdout)
|
@@ -3020,25 +2979,24 @@ class Cmd(cmd.Cmd):
|
|
3020
2979
|
self._redirecting = saved_redir_state.saved_redirecting
|
3021
2980
|
|
3022
2981
|
def cmd_func(self, command: str) -> Optional[CommandFunc]:
|
3023
|
-
"""
|
3024
|
-
Get the function for a command
|
2982
|
+
"""Get the function for a command.
|
3025
2983
|
|
3026
2984
|
:param command: the name of the command
|
3027
2985
|
|
3028
2986
|
Example:
|
3029
|
-
|
3030
2987
|
```py
|
3031
2988
|
helpfunc = self.cmd_func('help')
|
3032
2989
|
```
|
3033
2990
|
|
3034
2991
|
helpfunc now contains a reference to the ``do_help`` method
|
2992
|
+
|
3035
2993
|
"""
|
3036
2994
|
func_name = constants.COMMAND_FUNC_PREFIX + command
|
3037
2995
|
func = getattr(self, func_name, None)
|
3038
2996
|
return cast(CommandFunc, func) if callable(func) else None
|
3039
2997
|
|
3040
2998
|
def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = True) -> bool:
|
3041
|
-
"""
|
2999
|
+
"""Execute the actual do_* method for a command.
|
3042
3000
|
|
3043
3001
|
If the command provided doesn't exist, then it executes default() instead.
|
3044
3002
|
|
@@ -3073,7 +3031,7 @@ class Cmd(cmd.Cmd):
|
|
3073
3031
|
return stop if stop is not None else False
|
3074
3032
|
|
3075
3033
|
def default(self, statement: Statement) -> Optional[bool]: # type: ignore[override]
|
3076
|
-
"""
|
3034
|
+
"""Execute when the command given isn't a recognized command implemented by a do_* method.
|
3077
3035
|
|
3078
3036
|
:param statement: Statement object with parsed input
|
3079
3037
|
"""
|
@@ -3082,14 +3040,13 @@ class Cmd(cmd.Cmd):
|
|
3082
3040
|
self.history.append(statement)
|
3083
3041
|
|
3084
3042
|
return self.do_shell(statement.command_and_args)
|
3085
|
-
|
3086
|
-
|
3087
|
-
|
3088
|
-
err_msg += f"\n{self.default_suggestion_message.format(suggested_command)}"
|
3043
|
+
err_msg = self.default_error.format(statement.command)
|
3044
|
+
if self.suggest_similar_command and (suggested_command := self._suggest_similar_command(statement.command)):
|
3045
|
+
err_msg += f"\n{self.default_suggestion_message.format(suggested_command)}"
|
3089
3046
|
|
3090
|
-
|
3091
|
-
|
3092
|
-
|
3047
|
+
# Set apply_style to False so styles for default_error and default_suggestion_message are not overridden
|
3048
|
+
self.perror(err_msg, apply_style=False)
|
3049
|
+
return None
|
3093
3050
|
|
3094
3051
|
def _suggest_similar_command(self, command: str) -> Optional[str]:
|
3095
3052
|
return suggest_similar(command, self.get_visible_commands())
|
@@ -3098,7 +3055,7 @@ class Cmd(cmd.Cmd):
|
|
3098
3055
|
self,
|
3099
3056
|
prompt: str,
|
3100
3057
|
*,
|
3101
|
-
history: Optional[
|
3058
|
+
history: Optional[list[str]] = None,
|
3102
3059
|
completion_mode: utils.CompletionMode = utils.CompletionMode.NONE,
|
3103
3060
|
preserve_quotes: bool = False,
|
3104
3061
|
choices: Optional[Iterable[Any]] = None,
|
@@ -3106,9 +3063,9 @@ class Cmd(cmd.Cmd):
|
|
3106
3063
|
completer: Optional[CompleterFunc] = None,
|
3107
3064
|
parser: Optional[argparse.ArgumentParser] = None,
|
3108
3065
|
) -> str:
|
3109
|
-
"""
|
3110
|
-
|
3111
|
-
input is being entered.
|
3066
|
+
"""Read input from appropriate stdin value.
|
3067
|
+
|
3068
|
+
Also supports tab completion and up-arrow history while input is being entered.
|
3112
3069
|
|
3113
3070
|
:param prompt: prompt to display to user
|
3114
3071
|
:param history: optional list of strings to use for up-arrow history. If completion_mode is
|
@@ -3139,10 +3096,10 @@ class Cmd(cmd.Cmd):
|
|
3139
3096
|
"""
|
3140
3097
|
readline_configured = False
|
3141
3098
|
saved_completer: Optional[CompleterFunc] = None
|
3142
|
-
saved_history: Optional[
|
3099
|
+
saved_history: Optional[list[str]] = None
|
3143
3100
|
|
3144
3101
|
def configure_readline() -> None:
|
3145
|
-
"""Configure readline tab completion and history"""
|
3102
|
+
"""Configure readline tab completion and history."""
|
3146
3103
|
nonlocal readline_configured
|
3147
3104
|
nonlocal saved_completer
|
3148
3105
|
nonlocal saved_history
|
@@ -3158,7 +3115,7 @@ class Cmd(cmd.Cmd):
|
|
3158
3115
|
# Disable completion
|
3159
3116
|
if completion_mode == utils.CompletionMode.NONE:
|
3160
3117
|
|
3161
|
-
def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover
|
3118
|
+
def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover # noqa: ARG001
|
3162
3119
|
return None
|
3163
3120
|
|
3164
3121
|
complete_func = complete_none
|
@@ -3198,7 +3155,7 @@ class Cmd(cmd.Cmd):
|
|
3198
3155
|
readline_configured = True
|
3199
3156
|
|
3200
3157
|
def restore_readline() -> None:
|
3201
|
-
"""Restore readline tab completion and history"""
|
3158
|
+
"""Restore readline tab completion and history."""
|
3202
3159
|
nonlocal readline_configured
|
3203
3160
|
if not readline_configured or rl_type == RlType.NONE: # pragma: no cover
|
3204
3161
|
return
|
@@ -3232,31 +3189,29 @@ class Cmd(cmd.Cmd):
|
|
3232
3189
|
sys.stdout.write(f'{prompt}{line}\n')
|
3233
3190
|
|
3234
3191
|
# Otherwise read from self.stdin
|
3192
|
+
elif self.stdin.isatty():
|
3193
|
+
# on a tty, print the prompt first, then read the line
|
3194
|
+
self.poutput(prompt, end='')
|
3195
|
+
self.stdout.flush()
|
3196
|
+
line = self.stdin.readline()
|
3197
|
+
if len(line) == 0:
|
3198
|
+
line = 'eof'
|
3235
3199
|
else:
|
3236
|
-
if
|
3237
|
-
|
3238
|
-
|
3239
|
-
|
3240
|
-
|
3241
|
-
|
3242
|
-
|
3200
|
+
# we are reading from a pipe, read the line to see if there is
|
3201
|
+
# anything there, if so, then decide whether to print the
|
3202
|
+
# prompt or not
|
3203
|
+
line = self.stdin.readline()
|
3204
|
+
if len(line):
|
3205
|
+
# we read something, output the prompt and the something
|
3206
|
+
if self.echo:
|
3207
|
+
self.poutput(f'{prompt}{line}')
|
3243
3208
|
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'
|
3209
|
+
line = 'eof'
|
3254
3210
|
|
3255
3211
|
return line.rstrip('\r\n')
|
3256
3212
|
|
3257
3213
|
def _read_command_line(self, prompt: str) -> str:
|
3258
|
-
"""
|
3259
|
-
Read command line from appropriate stdin
|
3214
|
+
"""Read command line from appropriate stdin.
|
3260
3215
|
|
3261
3216
|
:param prompt: prompt to display to user
|
3262
3217
|
:return: command line text of 'eof' if an EOFError was caught
|
@@ -3264,11 +3219,9 @@ class Cmd(cmd.Cmd):
|
|
3264
3219
|
"""
|
3265
3220
|
try:
|
3266
3221
|
# Wrap in try since terminal_lock may not be locked
|
3267
|
-
|
3222
|
+
with contextlib.suppress(RuntimeError):
|
3268
3223
|
# Command line is about to be drawn. Allow asynchronous changes to the terminal.
|
3269
3224
|
self.terminal_lock.release()
|
3270
|
-
except RuntimeError:
|
3271
|
-
pass
|
3272
3225
|
return self.read_input(prompt, completion_mode=utils.CompletionMode.COMMANDS)
|
3273
3226
|
except EOFError:
|
3274
3227
|
return 'eof'
|
@@ -3277,8 +3230,7 @@ class Cmd(cmd.Cmd):
|
|
3277
3230
|
self.terminal_lock.acquire()
|
3278
3231
|
|
3279
3232
|
def _set_up_cmd2_readline(self) -> _SavedReadlineSettings:
|
3280
|
-
"""
|
3281
|
-
Called at beginning of command loop to set up readline with cmd2-specific settings
|
3233
|
+
"""Set up readline with cmd2-specific settings, called at beginning of command loop.
|
3282
3234
|
|
3283
3235
|
:return: Class containing saved readline settings
|
3284
3236
|
"""
|
@@ -3318,8 +3270,7 @@ class Cmd(cmd.Cmd):
|
|
3318
3270
|
return readline_settings
|
3319
3271
|
|
3320
3272
|
def _restore_readline(self, readline_settings: _SavedReadlineSettings) -> None:
|
3321
|
-
"""
|
3322
|
-
Called at end of command loop to restore saved readline settings
|
3273
|
+
"""Restore saved readline settings, called at end of command loop.
|
3323
3274
|
|
3324
3275
|
:param readline_settings: the readline settings to restore
|
3325
3276
|
"""
|
@@ -3335,8 +3286,9 @@ class Cmd(cmd.Cmd):
|
|
3335
3286
|
readline.rl.mode._display_completions = orig_pyreadline_display
|
3336
3287
|
|
3337
3288
|
def _cmdloop(self) -> None:
|
3338
|
-
"""Repeatedly issue a prompt, accept input, parse
|
3339
|
-
|
3289
|
+
"""Repeatedly issue a prompt, accept input, parse it, and dispatch to apporpriate commands.
|
3290
|
+
|
3291
|
+
Parse an initial prefix off the received input and dispatch to action methods, passing them
|
3340
3292
|
the remainder of the line as argument.
|
3341
3293
|
|
3342
3294
|
This serves the same role as cmd.cmdloop().
|
@@ -3388,7 +3340,7 @@ class Cmd(cmd.Cmd):
|
|
3388
3340
|
# Preserve quotes since we are passing strings to other commands
|
3389
3341
|
@with_argparser(alias_parser, preserve_quotes=True)
|
3390
3342
|
def do_alias(self, args: argparse.Namespace) -> None:
|
3391
|
-
"""Manage aliases"""
|
3343
|
+
"""Manage aliases."""
|
3392
3344
|
# Call handler for whatever subcommand was selected
|
3393
3345
|
handler = args.cmd2_handler.get()
|
3394
3346
|
handler(args)
|
@@ -3423,7 +3375,7 @@ class Cmd(cmd.Cmd):
|
|
3423
3375
|
|
3424
3376
|
@as_subcommand_to('alias', 'create', alias_create_parser, help=alias_create_description.lower())
|
3425
3377
|
def _alias_create(self, args: argparse.Namespace) -> None:
|
3426
|
-
"""Create or overwrite an alias"""
|
3378
|
+
"""Create or overwrite an alias."""
|
3427
3379
|
self.last_result = False
|
3428
3380
|
|
3429
3381
|
# Validate the alias name
|
@@ -3473,7 +3425,7 @@ class Cmd(cmd.Cmd):
|
|
3473
3425
|
|
3474
3426
|
@as_subcommand_to('alias', 'delete', alias_delete_parser, help=alias_delete_help)
|
3475
3427
|
def _alias_delete(self, args: argparse.Namespace) -> None:
|
3476
|
-
"""Delete aliases"""
|
3428
|
+
"""Delete aliases."""
|
3477
3429
|
self.last_result = True
|
3478
3430
|
|
3479
3431
|
if args.all:
|
@@ -3510,18 +3462,15 @@ class Cmd(cmd.Cmd):
|
|
3510
3462
|
|
3511
3463
|
@as_subcommand_to('alias', 'list', alias_list_parser, help=alias_list_help)
|
3512
3464
|
def _alias_list(self, args: argparse.Namespace) -> None:
|
3513
|
-
"""List some or all aliases as 'alias create' commands"""
|
3514
|
-
self.last_result = {} #
|
3465
|
+
"""List some or all aliases as 'alias create' commands."""
|
3466
|
+
self.last_result = {} # dict[alias_name, alias_value]
|
3515
3467
|
|
3516
3468
|
tokens_to_quote = constants.REDIRECTION_TOKENS
|
3517
3469
|
tokens_to_quote.extend(self.statement_parser.terminators)
|
3518
3470
|
|
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)
|
3471
|
+
to_list = utils.remove_duplicates(args.names) if args.names else sorted(self.aliases, key=self.default_sort_key)
|
3523
3472
|
|
3524
|
-
not_found:
|
3473
|
+
not_found: list[str] = []
|
3525
3474
|
for name in to_list:
|
3526
3475
|
if name not in self.aliases:
|
3527
3476
|
not_found.append(name)
|
@@ -3556,7 +3505,7 @@ class Cmd(cmd.Cmd):
|
|
3556
3505
|
# Preserve quotes since we are passing strings to other commands
|
3557
3506
|
@with_argparser(macro_parser, preserve_quotes=True)
|
3558
3507
|
def do_macro(self, args: argparse.Namespace) -> None:
|
3559
|
-
"""Manage macros"""
|
3508
|
+
"""Manage macros."""
|
3560
3509
|
# Call handler for whatever subcommand was selected
|
3561
3510
|
handler = args.cmd2_handler.get()
|
3562
3511
|
handler(args)
|
@@ -3615,7 +3564,7 @@ class Cmd(cmd.Cmd):
|
|
3615
3564
|
|
3616
3565
|
@as_subcommand_to('macro', 'create', macro_create_parser, help=macro_create_help)
|
3617
3566
|
def _macro_create(self, args: argparse.Namespace) -> None:
|
3618
|
-
"""Create or overwrite a macro"""
|
3567
|
+
"""Create or overwrite a macro."""
|
3619
3568
|
self.last_result = False
|
3620
3569
|
|
3621
3570
|
# Validate the macro name
|
@@ -3660,8 +3609,7 @@ class Cmd(cmd.Cmd):
|
|
3660
3609
|
return
|
3661
3610
|
|
3662
3611
|
arg_nums.add(cur_num)
|
3663
|
-
|
3664
|
-
max_arg_num = cur_num
|
3612
|
+
max_arg_num = max(max_arg_num, cur_num)
|
3665
3613
|
|
3666
3614
|
arg_list.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=False))
|
3667
3615
|
|
@@ -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:
|
@@ -5637,17 +5562,17 @@ 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
5567
|
signature = inspect.signature(func)
|
5643
|
-
_, param =
|
5568
|
+
_, param = next(iter(signature.parameters.items()))
|
5644
5569
|
if param.annotation != plugin.PostparsingData:
|
5645
5570
|
raise TypeError(f"{func.__name__} must have one parameter declared with type 'cmd2.plugin.PostparsingData'")
|
5646
5571
|
if signature.return_annotation != plugin.PostparsingData:
|
5647
5572
|
raise TypeError(f"{func.__name__} must declare return a return type of 'cmd2.plugin.PostparsingData'")
|
5648
5573
|
|
5649
5574
|
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"""
|
5575
|
+
"""Register a function to be called after parsing user input but before running the command."""
|
5651
5576
|
self._validate_postparsing_callable(func)
|
5652
5577
|
self._postparsing_hooks.append(func)
|
5653
5578
|
|
@@ -5655,14 +5580,14 @@ class Cmd(cmd.Cmd):
|
|
5655
5580
|
|
5656
5581
|
@classmethod
|
5657
5582
|
def _validate_prepostcmd_hook(
|
5658
|
-
cls, func: Callable[[CommandDataType], CommandDataType], data_type:
|
5583
|
+
cls, func: Callable[[CommandDataType], CommandDataType], data_type: type[CommandDataType]
|
5659
5584
|
) -> None:
|
5660
5585
|
"""Check parameter and return types for pre and post command hooks."""
|
5661
5586
|
signature = inspect.signature(func)
|
5662
5587
|
# validate that the callable has the right number of parameters
|
5663
5588
|
cls._validate_callable_param_count(cast(Callable[..., Any], func), 1)
|
5664
5589
|
# validate the parameter has the right annotation
|
5665
|
-
paramname =
|
5590
|
+
paramname = next(iter(signature.parameters.keys()))
|
5666
5591
|
param = signature.parameters[paramname]
|
5667
5592
|
if param.annotation != data_type:
|
5668
5593
|
raise TypeError(f'argument 1 of {func.__name__} has incompatible type {param.annotation}, expected {data_type}')
|
@@ -5691,7 +5616,7 @@ class Cmd(cmd.Cmd):
|
|
5691
5616
|
"""Check parameter and return types for command finalization hooks."""
|
5692
5617
|
cls._validate_callable_param_count(func, 1)
|
5693
5618
|
signature = inspect.signature(func)
|
5694
|
-
_, param =
|
5619
|
+
_, param = next(iter(signature.parameters.items()))
|
5695
5620
|
if param.annotation != plugin.CommandFinalizationData:
|
5696
5621
|
raise TypeError(f"{func.__name__} must have one parameter declared with type {plugin.CommandFinalizationData}")
|
5697
5622
|
if signature.return_annotation != plugin.CommandFinalizationData:
|
@@ -5709,16 +5634,18 @@ class Cmd(cmd.Cmd):
|
|
5709
5634
|
cmd_support_func: Callable[..., Any],
|
5710
5635
|
cmd_self: Union[CommandSet, 'Cmd', None],
|
5711
5636
|
) -> Optional[object]:
|
5712
|
-
"""
|
5713
|
-
|
5714
|
-
used when defining command's argparse object.
|
5715
|
-
|
5637
|
+
"""Attempt to resolve a candidate instance to pass as 'self'.
|
5638
|
+
|
5639
|
+
Used for an unbound class method that was used when defining command's argparse object.
|
5640
|
+
|
5641
|
+
Since we restrict registration to only a single CommandSet
|
5642
|
+
instance of each type, using type is a reasonably safe way to resolve the correct object instance.
|
5716
5643
|
|
5717
5644
|
:param cmd_support_func: command support function. This could be a completer or namespace provider
|
5718
5645
|
:param cmd_self: The `self` associated with the command or subcommand
|
5719
5646
|
"""
|
5720
5647
|
# figure out what class the command support function was defined in
|
5721
|
-
func_class: Optional[
|
5648
|
+
func_class: Optional[type[Any]] = get_defining_class(cmd_support_func)
|
5722
5649
|
|
5723
5650
|
# Was there a defining class identified? If so, is it a sub-class of CommandSet?
|
5724
5651
|
if func_class is not None and issubclass(func_class, CommandSet):
|
@@ -5729,7 +5656,7 @@ class Cmd(cmd.Cmd):
|
|
5729
5656
|
# 2. Do any of the registered CommandSets in the Cmd2 application exactly match the type?
|
5730
5657
|
# 3. Is there a registered CommandSet that is is the only matching subclass?
|
5731
5658
|
|
5732
|
-
func_self: Optional[Union[CommandSet,
|
5659
|
+
func_self: Optional[Union[CommandSet, Cmd]]
|
5733
5660
|
|
5734
5661
|
# check if the command's CommandSet is a sub-class of the support function's defining class
|
5735
5662
|
if isinstance(cmd_self, func_class):
|
@@ -5738,7 +5665,7 @@ class Cmd(cmd.Cmd):
|
|
5738
5665
|
else:
|
5739
5666
|
# Search all registered CommandSets
|
5740
5667
|
func_self = None
|
5741
|
-
candidate_sets:
|
5668
|
+
candidate_sets: list[CommandSet] = []
|
5742
5669
|
for installed_cmd_set in self._installed_command_sets:
|
5743
5670
|
if type(installed_cmd_set) == func_class: # noqa: E721
|
5744
5671
|
# Case 2: CommandSet is an exact type match for the function's CommandSet
|
@@ -5752,5 +5679,4 @@ class Cmd(cmd.Cmd):
|
|
5752
5679
|
# Case 3: There exists exactly 1 CommandSet that is a sub-class match of the function's CommandSet
|
5753
5680
|
func_self = candidate_sets[0]
|
5754
5681
|
return func_self
|
5755
|
-
|
5756
|
-
return self
|
5682
|
+
return self
|