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/utils.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
|
2
|
-
"""Shared utility functions"""
|
1
|
+
"""Shared utility functions."""
|
3
2
|
|
4
3
|
import argparse
|
5
4
|
import collections
|
5
|
+
import contextlib
|
6
6
|
import functools
|
7
7
|
import glob
|
8
8
|
import inspect
|
@@ -13,6 +13,7 @@ import subprocess
|
|
13
13
|
import sys
|
14
14
|
import threading
|
15
15
|
import unicodedata
|
16
|
+
from collections.abc import Callable, Iterable
|
16
17
|
from difflib import (
|
17
18
|
SequenceMatcher,
|
18
19
|
)
|
@@ -22,13 +23,8 @@ from enum import (
|
|
22
23
|
from typing import (
|
23
24
|
TYPE_CHECKING,
|
24
25
|
Any,
|
25
|
-
Callable,
|
26
|
-
Dict,
|
27
|
-
Iterable,
|
28
|
-
List,
|
29
26
|
Optional,
|
30
27
|
TextIO,
|
31
|
-
Type,
|
32
28
|
TypeVar,
|
33
29
|
Union,
|
34
30
|
cast,
|
@@ -53,8 +49,7 @@ _T = TypeVar('_T')
|
|
53
49
|
|
54
50
|
|
55
51
|
def is_quoted(arg: str) -> bool:
|
56
|
-
"""
|
57
|
-
Checks if a string is quoted
|
52
|
+
"""Check if a string is quoted.
|
58
53
|
|
59
54
|
:param arg: the string being checked for quotes
|
60
55
|
:return: True if a string is quoted
|
@@ -63,17 +58,14 @@ def is_quoted(arg: str) -> bool:
|
|
63
58
|
|
64
59
|
|
65
60
|
def quote_string(arg: str) -> str:
|
66
|
-
"""Quote a string"""
|
67
|
-
if '"' in arg
|
68
|
-
quote = "'"
|
69
|
-
else:
|
70
|
-
quote = '"'
|
61
|
+
"""Quote a string."""
|
62
|
+
quote = "'" if '"' in arg else '"'
|
71
63
|
|
72
64
|
return quote + arg + quote
|
73
65
|
|
74
66
|
|
75
67
|
def quote_string_if_needed(arg: str) -> str:
|
76
|
-
"""Quote a string if it contains spaces and isn't already quoted"""
|
68
|
+
"""Quote a string if it contains spaces and isn't already quoted."""
|
77
69
|
if is_quoted(arg) or ' ' not in arg:
|
78
70
|
return arg
|
79
71
|
|
@@ -94,7 +86,7 @@ def strip_quotes(arg: str) -> str:
|
|
94
86
|
|
95
87
|
|
96
88
|
def to_bool(val: Any) -> bool:
|
97
|
-
"""
|
89
|
+
"""Convert anything to a boolean based on its value.
|
98
90
|
|
99
91
|
Strings like "True", "true", "False", and "false" return True, True, False, and False
|
100
92
|
respectively. All other values are converted using bool()
|
@@ -106,22 +98,21 @@ def to_bool(val: Any) -> bool:
|
|
106
98
|
if isinstance(val, str):
|
107
99
|
if val.capitalize() == str(True):
|
108
100
|
return True
|
109
|
-
|
101
|
+
if val.capitalize() == str(False):
|
110
102
|
return False
|
111
103
|
raise ValueError("must be True or False (case-insensitive)")
|
112
|
-
|
104
|
+
if isinstance(val, bool):
|
113
105
|
return val
|
114
|
-
|
115
|
-
return bool(val)
|
106
|
+
return bool(val)
|
116
107
|
|
117
108
|
|
118
109
|
class Settable:
|
119
|
-
"""Used to configure an attribute to be settable via the set command in the CLI"""
|
110
|
+
"""Used to configure an attribute to be settable via the set command in the CLI."""
|
120
111
|
|
121
112
|
def __init__(
|
122
113
|
self,
|
123
114
|
name: str,
|
124
|
-
val_type: Union[
|
115
|
+
val_type: Union[type[Any], Callable[[Any], Any]],
|
125
116
|
description: str,
|
126
117
|
settable_object: object,
|
127
118
|
*,
|
@@ -131,8 +122,7 @@ class Settable:
|
|
131
122
|
choices_provider: Optional[ChoicesProviderFunc] = None,
|
132
123
|
completer: Optional[CompleterFunc] = None,
|
133
124
|
) -> None:
|
134
|
-
"""
|
135
|
-
Settable Initializer
|
125
|
+
"""Settable Initializer.
|
136
126
|
|
137
127
|
:param name: name of the instance attribute being made settable
|
138
128
|
:param val_type: callable used to cast the string value from the command line into its proper type and
|
@@ -160,8 +150,8 @@ class Settable:
|
|
160
150
|
"""
|
161
151
|
if val_type is bool:
|
162
152
|
|
163
|
-
def get_bool_choices(_) ->
|
164
|
-
"""
|
153
|
+
def get_bool_choices(_: str) -> list[str]:
|
154
|
+
"""Tab complete lowercase boolean values."""
|
165
155
|
return ['true', 'false']
|
166
156
|
|
167
157
|
val_type = to_bool
|
@@ -182,8 +172,7 @@ class Settable:
|
|
182
172
|
return getattr(self.settable_obj, self.settable_attrib_name)
|
183
173
|
|
184
174
|
def set_value(self, value: Any) -> None:
|
185
|
-
"""
|
186
|
-
Set the settable attribute on the specified destination object.
|
175
|
+
"""Set the settable attribute on the specified destination object.
|
187
176
|
|
188
177
|
:param value: new value to set
|
189
178
|
"""
|
@@ -205,7 +194,7 @@ class Settable:
|
|
205
194
|
|
206
195
|
|
207
196
|
def is_text_file(file_path: str) -> bool:
|
208
|
-
"""
|
197
|
+
"""Return if a file contains only ASCII or UTF-8 encoded text and isn't empty.
|
209
198
|
|
210
199
|
:param file_path: path to the file being checked
|
211
200
|
:return: True if the file is a non-empty text file, otherwise False
|
@@ -231,8 +220,8 @@ def is_text_file(file_path: str) -> bool:
|
|
231
220
|
return valid_text_file
|
232
221
|
|
233
222
|
|
234
|
-
def remove_duplicates(list_to_prune:
|
235
|
-
"""
|
223
|
+
def remove_duplicates(list_to_prune: list[_T]) -> list[_T]:
|
224
|
+
"""Remove duplicates from a list while preserving order of the items.
|
236
225
|
|
237
226
|
:param list_to_prune: the list being pruned of duplicates
|
238
227
|
:return: The pruned list
|
@@ -253,7 +242,7 @@ def norm_fold(astr: str) -> str:
|
|
253
242
|
return unicodedata.normalize('NFC', astr).casefold()
|
254
243
|
|
255
244
|
|
256
|
-
def alphabetical_sort(list_to_sort: Iterable[str]) ->
|
245
|
+
def alphabetical_sort(list_to_sort: Iterable[str]) -> list[str]:
|
257
246
|
"""Sorts a list of strings alphabetically.
|
258
247
|
|
259
248
|
For example: ['a1', 'A11', 'A2', 'a22', 'a3']
|
@@ -269,10 +258,10 @@ def alphabetical_sort(list_to_sort: Iterable[str]) -> List[str]:
|
|
269
258
|
|
270
259
|
|
271
260
|
def try_int_or_force_to_lower_case(input_str: str) -> Union[int, str]:
|
272
|
-
"""
|
273
|
-
|
261
|
+
"""Try to convert the passed-in string to an integer. If that fails, it converts it to lower case using norm_fold.
|
262
|
+
|
274
263
|
:param input_str: string to convert
|
275
|
-
:return: the string as an integer or a lower case version of the string
|
264
|
+
:return: the string as an integer or a lower case version of the string.
|
276
265
|
"""
|
277
266
|
try:
|
278
267
|
return int(input_str)
|
@@ -280,9 +269,8 @@ def try_int_or_force_to_lower_case(input_str: str) -> Union[int, str]:
|
|
280
269
|
return norm_fold(input_str)
|
281
270
|
|
282
271
|
|
283
|
-
def natural_keys(input_str: str) ->
|
284
|
-
"""
|
285
|
-
Converts a string into a list of integers and strings to support natural sorting (see natural_sort).
|
272
|
+
def natural_keys(input_str: str) -> list[Union[int, str]]:
|
273
|
+
"""Convert a string into a list of integers and strings to support natural sorting (see natural_sort).
|
286
274
|
|
287
275
|
For example: natural_keys('abc123def') -> ['abc', '123', 'def']
|
288
276
|
:param input_str: string to convert
|
@@ -291,9 +279,8 @@ def natural_keys(input_str: str) -> List[Union[int, str]]:
|
|
291
279
|
return [try_int_or_force_to_lower_case(substr) for substr in re.split(r'(\d+)', input_str)]
|
292
280
|
|
293
281
|
|
294
|
-
def natural_sort(list_to_sort: Iterable[str]) ->
|
295
|
-
"""
|
296
|
-
Sorts a list of strings case insensitively as well as numerically.
|
282
|
+
def natural_sort(list_to_sort: Iterable[str]) -> list[str]:
|
283
|
+
"""Sorts a list of strings case insensitively as well as numerically.
|
297
284
|
|
298
285
|
For example: ['a1', 'A2', 'a3', 'A11', 'a22']
|
299
286
|
|
@@ -307,9 +294,8 @@ def natural_sort(list_to_sort: Iterable[str]) -> List[str]:
|
|
307
294
|
return sorted(list_to_sort, key=natural_keys)
|
308
295
|
|
309
296
|
|
310
|
-
def quote_specific_tokens(tokens:
|
311
|
-
"""
|
312
|
-
Quote specific tokens in a list
|
297
|
+
def quote_specific_tokens(tokens: list[str], tokens_to_quote: list[str]) -> None:
|
298
|
+
"""Quote specific tokens in a list.
|
313
299
|
|
314
300
|
:param tokens: token list being edited
|
315
301
|
:param tokens_to_quote: the tokens, which if present in tokens, to quote
|
@@ -319,9 +305,8 @@ def quote_specific_tokens(tokens: List[str], tokens_to_quote: List[str]) -> None
|
|
319
305
|
tokens[i] = quote_string(token)
|
320
306
|
|
321
307
|
|
322
|
-
def unquote_specific_tokens(tokens:
|
323
|
-
"""
|
324
|
-
Unquote specific tokens in a list
|
308
|
+
def unquote_specific_tokens(tokens: list[str], tokens_to_unquote: list[str]) -> None:
|
309
|
+
"""Unquote specific tokens in a list.
|
325
310
|
|
326
311
|
:param tokens: token list being edited
|
327
312
|
:param tokens_to_unquote: the tokens, which if present in tokens, to unquote
|
@@ -333,8 +318,8 @@ def unquote_specific_tokens(tokens: List[str], tokens_to_unquote: List[str]) ->
|
|
333
318
|
|
334
319
|
|
335
320
|
def expand_user(token: str) -> str:
|
336
|
-
"""
|
337
|
-
|
321
|
+
"""Wrap os.expanduser() to support expanding ~ in quoted strings.
|
322
|
+
|
338
323
|
:param token: the string to expand
|
339
324
|
"""
|
340
325
|
if token:
|
@@ -353,20 +338,20 @@ def expand_user(token: str) -> str:
|
|
353
338
|
return token
|
354
339
|
|
355
340
|
|
356
|
-
def expand_user_in_tokens(tokens:
|
357
|
-
"""
|
358
|
-
|
359
|
-
:param tokens: tokens to expand
|
341
|
+
def expand_user_in_tokens(tokens: list[str]) -> None:
|
342
|
+
"""Call expand_user() on all tokens in a list of strings.
|
343
|
+
|
344
|
+
:param tokens: tokens to expand.
|
360
345
|
"""
|
361
346
|
for index, _ in enumerate(tokens):
|
362
347
|
tokens[index] = expand_user(tokens[index])
|
363
348
|
|
364
349
|
|
365
350
|
def find_editor() -> Optional[str]:
|
366
|
-
"""
|
367
|
-
|
351
|
+
"""Set cmd2.Cmd.DEFAULT_EDITOR. If EDITOR env variable is set, that will be used.
|
352
|
+
|
368
353
|
Otherwise the function will look for a known editor in directories specified by PATH env variable.
|
369
|
-
:return: Default editor or None
|
354
|
+
:return: Default editor or None.
|
370
355
|
"""
|
371
356
|
editor = os.environ.get('EDITOR')
|
372
357
|
if not editor:
|
@@ -377,17 +362,16 @@ def find_editor() -> Optional[str]:
|
|
377
362
|
|
378
363
|
# Get a list of every directory in the PATH environment variable and ignore symbolic links
|
379
364
|
env_path = os.getenv('PATH')
|
380
|
-
if env_path is None
|
381
|
-
paths = []
|
382
|
-
else:
|
383
|
-
paths = [p for p in env_path.split(os.path.pathsep) if not os.path.islink(p)]
|
365
|
+
paths = [] if env_path is None else [p for p in env_path.split(os.path.pathsep) if not os.path.islink(p)]
|
384
366
|
|
385
|
-
for
|
386
|
-
editor_path = os.path.join(path,
|
367
|
+
for possible_editor, path in itertools.product(editors, paths):
|
368
|
+
editor_path = os.path.join(path, possible_editor)
|
387
369
|
if os.path.isfile(editor_path) and os.access(editor_path, os.X_OK):
|
388
370
|
if sys.platform[:3] == 'win':
|
389
371
|
# Remove extension from Windows file names
|
390
|
-
editor = os.path.splitext(
|
372
|
+
editor = os.path.splitext(possible_editor)[0]
|
373
|
+
else:
|
374
|
+
editor = possible_editor
|
391
375
|
break
|
392
376
|
else:
|
393
377
|
editor = None
|
@@ -395,7 +379,7 @@ def find_editor() -> Optional[str]:
|
|
395
379
|
return editor
|
396
380
|
|
397
381
|
|
398
|
-
def files_from_glob_pattern(pattern: str, access: int = os.F_OK) ->
|
382
|
+
def files_from_glob_pattern(pattern: str, access: int = os.F_OK) -> list[str]:
|
399
383
|
"""Return a list of file paths based on a glob pattern.
|
400
384
|
|
401
385
|
Only files are returned, not directories, and optionally only files for which the user has a specified access to.
|
@@ -407,7 +391,7 @@ def files_from_glob_pattern(pattern: str, access: int = os.F_OK) -> List[str]:
|
|
407
391
|
return [f for f in glob.glob(pattern) if os.path.isfile(f) and os.access(f, access)]
|
408
392
|
|
409
393
|
|
410
|
-
def files_from_glob_patterns(patterns:
|
394
|
+
def files_from_glob_patterns(patterns: list[str], access: int = os.F_OK) -> list[str]:
|
411
395
|
"""Return a list of file paths based on a list of glob patterns.
|
412
396
|
|
413
397
|
Only files are returned, not directories, and optionally only files for which the user has a specified access to.
|
@@ -423,8 +407,8 @@ def files_from_glob_patterns(patterns: List[str], access: int = os.F_OK) -> List
|
|
423
407
|
return files
|
424
408
|
|
425
409
|
|
426
|
-
def get_exes_in_path(starts_with: str) ->
|
427
|
-
"""
|
410
|
+
def get_exes_in_path(starts_with: str) -> list[str]:
|
411
|
+
"""Return names of executables in a user's path.
|
428
412
|
|
429
413
|
:param starts_with: what the exes should start with. leave blank for all exes in path.
|
430
414
|
:return: a list of matching exe names
|
@@ -437,10 +421,7 @@ def get_exes_in_path(starts_with: str) -> List[str]:
|
|
437
421
|
|
438
422
|
# Get a list of every directory in the PATH environment variable and ignore symbolic links
|
439
423
|
env_path = os.getenv('PATH')
|
440
|
-
if env_path is None
|
441
|
-
paths = []
|
442
|
-
else:
|
443
|
-
paths = [p for p in env_path.split(os.path.pathsep) if not os.path.islink(p)]
|
424
|
+
paths = [] if env_path is None else [p for p in env_path.split(os.path.pathsep) if not os.path.islink(p)]
|
444
425
|
|
445
426
|
# Use a set to store exe names since there can be duplicates
|
446
427
|
exes_set = set()
|
@@ -457,8 +438,8 @@ def get_exes_in_path(starts_with: str) -> List[str]:
|
|
457
438
|
|
458
439
|
|
459
440
|
class StdSim:
|
460
|
-
"""
|
461
|
-
|
441
|
+
"""Class to simulate behavior of sys.stdout or sys.stderr.
|
442
|
+
|
462
443
|
Stores contents in internal buffer and optionally echos to the inner stream it is simulating.
|
463
444
|
"""
|
464
445
|
|
@@ -470,8 +451,7 @@ class StdSim:
|
|
470
451
|
encoding: str = 'utf-8',
|
471
452
|
errors: str = 'replace',
|
472
453
|
) -> None:
|
473
|
-
"""
|
474
|
-
StdSim Initializer
|
454
|
+
"""StdSim Initializer.
|
475
455
|
|
476
456
|
:param inner_stream: the wrapped stream. Should be a TextIO or StdSim instance.
|
477
457
|
:param echo: if True, then all input will be echoed to inner_stream
|
@@ -486,8 +466,7 @@ class StdSim:
|
|
486
466
|
self.buffer = ByteBuf(self)
|
487
467
|
|
488
468
|
def write(self, s: str) -> None:
|
489
|
-
"""
|
490
|
-
Add str to internal bytes buffer and if echo is True, echo contents to inner stream
|
469
|
+
"""Add str to internal bytes buffer and if echo is True, echo contents to inner stream.
|
491
470
|
|
492
471
|
:param s: String to write to the stream
|
493
472
|
"""
|
@@ -500,16 +479,15 @@ class StdSim:
|
|
500
479
|
self.inner_stream.write(s)
|
501
480
|
|
502
481
|
def getvalue(self) -> str:
|
503
|
-
"""Get the internal contents as a str"""
|
482
|
+
"""Get the internal contents as a str."""
|
504
483
|
return self.buffer.byte_buf.decode(encoding=self.encoding, errors=self.errors)
|
505
484
|
|
506
485
|
def getbytes(self) -> bytes:
|
507
|
-
"""Get the internal contents as bytes"""
|
486
|
+
"""Get the internal contents as bytes."""
|
508
487
|
return bytes(self.buffer.byte_buf)
|
509
488
|
|
510
489
|
def read(self, size: Optional[int] = -1) -> str:
|
511
|
-
"""
|
512
|
-
Read from the internal contents as a str and then clear them out
|
490
|
+
"""Read from the internal contents as a str and then clear them out.
|
513
491
|
|
514
492
|
:param size: Number of bytes to read from the stream
|
515
493
|
"""
|
@@ -523,27 +501,26 @@ class StdSim:
|
|
523
501
|
return result
|
524
502
|
|
525
503
|
def readbytes(self) -> bytes:
|
526
|
-
"""Read from the internal contents as bytes and then clear them out"""
|
504
|
+
"""Read from the internal contents as bytes and then clear them out."""
|
527
505
|
result = self.getbytes()
|
528
506
|
self.clear()
|
529
507
|
return result
|
530
508
|
|
531
509
|
def clear(self) -> None:
|
532
|
-
"""Clear the internal contents"""
|
510
|
+
"""Clear the internal contents."""
|
533
511
|
self.buffer.byte_buf.clear()
|
534
512
|
|
535
513
|
def isatty(self) -> bool:
|
536
514
|
"""StdSim only considered an interactive stream if `echo` is True and `inner_stream` is a tty."""
|
537
515
|
if self.echo:
|
538
516
|
return self.inner_stream.isatty()
|
539
|
-
|
540
|
-
return False
|
517
|
+
return False
|
541
518
|
|
542
519
|
@property
|
543
520
|
def line_buffering(self) -> bool:
|
544
|
-
"""
|
545
|
-
|
546
|
-
when running unit tests because pytest sets stdout to a pytest EncodedFile object.
|
521
|
+
"""Handle when the inner stream doesn't have a line_buffering attribute.
|
522
|
+
|
523
|
+
Which is the case when running unit tests because pytest sets stdout to a pytest EncodedFile object.
|
547
524
|
"""
|
548
525
|
try:
|
549
526
|
return bool(self.inner_stream.line_buffering)
|
@@ -551,21 +528,20 @@ class StdSim:
|
|
551
528
|
return False
|
552
529
|
|
553
530
|
def __getattr__(self, item: str) -> Any:
|
531
|
+
"""When an attribute lookup fails to find the attribute in the usual places, this special method is called."""
|
554
532
|
if item in self.__dict__:
|
555
533
|
return self.__dict__[item]
|
556
|
-
|
557
|
-
return getattr(self.inner_stream, item)
|
534
|
+
return getattr(self.inner_stream, item)
|
558
535
|
|
559
536
|
|
560
537
|
class ByteBuf:
|
561
|
-
"""
|
562
|
-
Used by StdSim to write binary data and stores the actual bytes written
|
563
|
-
"""
|
538
|
+
"""Used by StdSim to write binary data and stores the actual bytes written."""
|
564
539
|
|
565
540
|
# Used to know when to flush the StdSim
|
566
|
-
NEWLINES =
|
541
|
+
NEWLINES = (b'\n', b'\r')
|
567
542
|
|
568
543
|
def __init__(self, std_sim_instance: StdSim) -> None:
|
544
|
+
"""Initialize the ByteBuf instance."""
|
569
545
|
self.byte_buf = bytearray()
|
570
546
|
self.std_sim_instance = std_sim_instance
|
571
547
|
|
@@ -582,23 +558,22 @@ class ByteBuf:
|
|
582
558
|
# and the bytes being written contain a new line character. This is helpful when StdSim
|
583
559
|
# is being used to capture output of a shell command because it causes the output to print
|
584
560
|
# to the screen more often than if we waited for the stream to flush its buffer.
|
585
|
-
if self.std_sim_instance.line_buffering:
|
586
|
-
|
587
|
-
self.std_sim_instance.flush()
|
561
|
+
if self.std_sim_instance.line_buffering and any(newline in b for newline in ByteBuf.NEWLINES):
|
562
|
+
self.std_sim_instance.flush()
|
588
563
|
|
589
564
|
|
590
565
|
class ProcReader:
|
591
|
-
"""
|
592
|
-
|
566
|
+
"""Used to capture stdout and stderr from a Popen process if any of those were set to subprocess.PIPE.
|
567
|
+
|
593
568
|
If neither are pipes, then the process will run normally and no output will be captured.
|
594
569
|
"""
|
595
570
|
|
596
571
|
def __init__(self, proc: PopenTextIO, stdout: Union[StdSim, TextIO], stderr: Union[StdSim, TextIO]) -> None:
|
597
|
-
"""
|
598
|
-
|
572
|
+
"""ProcReader initializer.
|
573
|
+
|
599
574
|
:param proc: the Popen process being read from
|
600
575
|
:param stdout: the stream to write captured stdout
|
601
|
-
:param stderr: the stream to write captured stderr
|
576
|
+
:param stderr: the stream to write captured stderr.
|
602
577
|
"""
|
603
578
|
self._proc = proc
|
604
579
|
self._stdout = stdout
|
@@ -615,7 +590,7 @@ class ProcReader:
|
|
615
590
|
self._err_thread.start()
|
616
591
|
|
617
592
|
def send_sigint(self) -> None:
|
618
|
-
"""Send a SIGINT to the process similar to if <Ctrl>+C were pressed"""
|
593
|
+
"""Send a SIGINT to the process similar to if <Ctrl>+C were pressed."""
|
619
594
|
import signal
|
620
595
|
|
621
596
|
if sys.platform.startswith('win'):
|
@@ -632,11 +607,11 @@ class ProcReader:
|
|
632
607
|
return
|
633
608
|
|
634
609
|
def terminate(self) -> None:
|
635
|
-
"""Terminate the process"""
|
610
|
+
"""Terminate the process."""
|
636
611
|
self._proc.terminate()
|
637
612
|
|
638
613
|
def wait(self) -> None:
|
639
|
-
"""Wait for the process to finish"""
|
614
|
+
"""Wait for the process to finish."""
|
640
615
|
if self._out_thread.is_alive():
|
641
616
|
self._out_thread.join()
|
642
617
|
if self._err_thread.is_alive():
|
@@ -652,8 +627,8 @@ class ProcReader:
|
|
652
627
|
self._write_bytes(self._stderr, err)
|
653
628
|
|
654
629
|
def _reader_thread_func(self, read_stdout: bool) -> None:
|
655
|
-
"""
|
656
|
-
|
630
|
+
"""Thread function that reads a stream from the process.
|
631
|
+
|
657
632
|
:param read_stdout: if True, then this thread deals with stdout. Otherwise it deals with stderr.
|
658
633
|
"""
|
659
634
|
if read_stdout:
|
@@ -664,7 +639,8 @@ class ProcReader:
|
|
664
639
|
write_stream = self._stderr
|
665
640
|
|
666
641
|
# The thread should have been started only if this stream was a pipe
|
667
|
-
|
642
|
+
if read_stream is None:
|
643
|
+
raise ValueError("read_stream is None")
|
668
644
|
|
669
645
|
# Run until process completes
|
670
646
|
while self._proc.poll() is None:
|
@@ -675,19 +651,17 @@ class ProcReader:
|
|
675
651
|
|
676
652
|
@staticmethod
|
677
653
|
def _write_bytes(stream: Union[StdSim, TextIO], to_write: Union[bytes, str]) -> None:
|
678
|
-
"""
|
679
|
-
|
654
|
+
"""Write bytes to a stream.
|
655
|
+
|
680
656
|
:param stream: the stream being written to
|
681
|
-
:param to_write: the bytes being written
|
657
|
+
:param to_write: the bytes being written.
|
682
658
|
"""
|
683
659
|
if isinstance(to_write, str):
|
684
660
|
to_write = to_write.encode()
|
685
661
|
|
686
|
-
|
662
|
+
# BrokenPipeError can occur if output is being piped to a process that closed
|
663
|
+
with contextlib.suppress(BrokenPipeError):
|
687
664
|
stream.buffer.write(to_write)
|
688
|
-
except BrokenPipeError:
|
689
|
-
# This occurs if output is being piped to a process that closed
|
690
|
-
pass
|
691
665
|
|
692
666
|
|
693
667
|
class ContextFlag:
|
@@ -699,24 +673,29 @@ class ContextFlag:
|
|
699
673
|
"""
|
700
674
|
|
701
675
|
def __init__(self) -> None:
|
702
|
-
|
703
|
-
|
676
|
+
"""When this flag has a positive value, it is considered set. When it is 0, it is not set.
|
677
|
+
|
678
|
+
It should never go below 0.
|
679
|
+
"""
|
704
680
|
self.__count = 0
|
705
681
|
|
706
682
|
def __bool__(self) -> bool:
|
683
|
+
"""Define the truth value of an object when it is used in a boolean context."""
|
707
684
|
return self.__count > 0
|
708
685
|
|
709
686
|
def __enter__(self) -> None:
|
687
|
+
"""When a with block is entered, the __enter__ method of the context manager is called."""
|
710
688
|
self.__count += 1
|
711
689
|
|
712
|
-
def __exit__(self, *args:
|
690
|
+
def __exit__(self, *args: object) -> None:
|
691
|
+
"""When the execution flow exits a with statement block this is called, regardless of whether an exception occurred."""
|
713
692
|
self.__count -= 1
|
714
693
|
if self.__count < 0:
|
715
694
|
raise ValueError("count has gone below 0")
|
716
695
|
|
717
696
|
|
718
697
|
class RedirectionSavedState:
|
719
|
-
"""Created by each command to store information required to restore state after redirection"""
|
698
|
+
"""Created by each command to store information required to restore state after redirection."""
|
720
699
|
|
721
700
|
def __init__(
|
722
701
|
self,
|
@@ -725,12 +704,12 @@ class RedirectionSavedState:
|
|
725
704
|
pipe_proc_reader: Optional[ProcReader],
|
726
705
|
saved_redirecting: bool,
|
727
706
|
) -> None:
|
728
|
-
"""
|
729
|
-
|
707
|
+
"""RedirectionSavedState initializer.
|
708
|
+
|
730
709
|
:param self_stdout: saved value of Cmd.stdout
|
731
710
|
:param sys_stdout: saved value of sys.stdout
|
732
711
|
:param pipe_proc_reader: saved value of Cmd._cur_pipe_proc_reader
|
733
|
-
:param saved_redirecting: saved value of Cmd._redirecting
|
712
|
+
:param saved_redirecting: saved value of Cmd._redirecting.
|
734
713
|
"""
|
735
714
|
# Tells if command is redirecting
|
736
715
|
self.redirecting = False
|
@@ -744,10 +723,10 @@ class RedirectionSavedState:
|
|
744
723
|
self.saved_redirecting = saved_redirecting
|
745
724
|
|
746
725
|
|
747
|
-
def _remove_overridden_styles(styles_to_parse:
|
748
|
-
"""
|
749
|
-
|
750
|
-
|
726
|
+
def _remove_overridden_styles(styles_to_parse: list[str]) -> list[str]:
|
727
|
+
"""Filter a style list down to only those which would still be in effect if all were processed in order.
|
728
|
+
|
729
|
+
Utility function for align_text() / truncate_line().
|
751
730
|
|
752
731
|
This is mainly used to reduce how many style strings are stored in memory when
|
753
732
|
building large multiline strings with ANSI styles. We only need to carry over
|
@@ -761,11 +740,11 @@ def _remove_overridden_styles(styles_to_parse: List[str]) -> List[str]:
|
|
761
740
|
)
|
762
741
|
|
763
742
|
class StyleState:
|
764
|
-
"""Keeps track of what text styles are enabled"""
|
743
|
+
"""Keeps track of what text styles are enabled."""
|
765
744
|
|
766
745
|
def __init__(self) -> None:
|
767
746
|
# Contains styles still in effect, keyed by their index in styles_to_parse
|
768
|
-
self.style_dict:
|
747
|
+
self.style_dict: dict[int, str] = {}
|
769
748
|
|
770
749
|
# Indexes into style_dict
|
771
750
|
self.reset_all: Optional[int] = None
|
@@ -826,7 +805,7 @@ def _remove_overridden_styles(styles_to_parse: List[str]) -> List[str]:
|
|
826
805
|
|
827
806
|
|
828
807
|
class TextAlignment(Enum):
|
829
|
-
"""Horizontal text alignment"""
|
808
|
+
"""Horizontal text alignment."""
|
830
809
|
|
831
810
|
LEFT = 1
|
832
811
|
CENTER = 2
|
@@ -842,8 +821,8 @@ def align_text(
|
|
842
821
|
tab_width: int = 4,
|
843
822
|
truncate: bool = False,
|
844
823
|
) -> str:
|
845
|
-
"""
|
846
|
-
|
824
|
+
"""Align text for display within a given width. Supports characters with display widths greater than 1.
|
825
|
+
|
847
826
|
ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned
|
848
827
|
independently.
|
849
828
|
|
@@ -893,24 +872,21 @@ def align_text(
|
|
893
872
|
# fill characters. Instead of repeating the style characters for each fill character, we'll wrap each sequence.
|
894
873
|
fill_char_style_begin, fill_char_style_end = fill_char.split(stripped_fill_char)
|
895
874
|
|
896
|
-
if text
|
897
|
-
lines = text.splitlines()
|
898
|
-
else:
|
899
|
-
lines = ['']
|
875
|
+
lines = text.splitlines() if text else ['']
|
900
876
|
|
901
877
|
text_buf = io.StringIO()
|
902
878
|
|
903
879
|
# ANSI style sequences that may affect subsequent lines will be cancelled by the fill_char's style.
|
904
880
|
# To avoid this, we save styles which are still in effect so we can restore them when beginning the next line.
|
905
881
|
# This also allows lines to be used independently and still have their style. TableCreator does this.
|
906
|
-
previous_styles:
|
882
|
+
previous_styles: list[str] = []
|
907
883
|
|
908
884
|
for index, line in enumerate(lines):
|
909
885
|
if index > 0:
|
910
886
|
text_buf.write('\n')
|
911
887
|
|
912
888
|
if truncate:
|
913
|
-
line = truncate_line(line, width)
|
889
|
+
line = truncate_line(line, width) # noqa: PLW2901
|
914
890
|
|
915
891
|
line_width = ansi.style_aware_wcswidth(line)
|
916
892
|
if line_width == -1:
|
@@ -920,12 +896,8 @@ def align_text(
|
|
920
896
|
line_styles = list(get_styles_dict(line).values())
|
921
897
|
|
922
898
|
# Calculate how wide each side of filling needs to be
|
923
|
-
if line_width >= width
|
924
|
-
|
925
|
-
# There may be styles sequences to restore.
|
926
|
-
total_fill_width = 0
|
927
|
-
else:
|
928
|
-
total_fill_width = width - line_width
|
899
|
+
total_fill_width = 0 if line_width >= width else width - line_width
|
900
|
+
# Even if the line needs no fill chars, there may be styles sequences to restore
|
929
901
|
|
930
902
|
if alignment == TextAlignment.LEFT:
|
931
903
|
left_fill_width = 0
|
@@ -969,8 +941,8 @@ def align_text(
|
|
969
941
|
def align_left(
|
970
942
|
text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False
|
971
943
|
) -> str:
|
972
|
-
"""
|
973
|
-
|
944
|
+
"""Left align text for display within a given width. Supports characters with display widths greater than 1.
|
945
|
+
|
974
946
|
ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned
|
975
947
|
independently.
|
976
948
|
|
@@ -992,8 +964,8 @@ def align_left(
|
|
992
964
|
def align_center(
|
993
965
|
text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False
|
994
966
|
) -> str:
|
995
|
-
"""
|
996
|
-
|
967
|
+
"""Center text for display within a given width. Supports characters with display widths greater than 1.
|
968
|
+
|
997
969
|
ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned
|
998
970
|
independently.
|
999
971
|
|
@@ -1015,8 +987,8 @@ def align_center(
|
|
1015
987
|
def align_right(
|
1016
988
|
text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False
|
1017
989
|
) -> str:
|
1018
|
-
"""
|
1019
|
-
|
990
|
+
"""Right align text for display within a given width. Supports characters with display widths greater than 1.
|
991
|
+
|
1020
992
|
ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned
|
1021
993
|
independently.
|
1022
994
|
|
@@ -1036,10 +1008,10 @@ def align_right(
|
|
1036
1008
|
|
1037
1009
|
|
1038
1010
|
def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str:
|
1039
|
-
"""
|
1040
|
-
|
1041
|
-
is replaced by a '…' character. Supports characters with display widths greater
|
1042
|
-
do not count toward the display width.
|
1011
|
+
"""Truncate a single line to fit within a given display width.
|
1012
|
+
|
1013
|
+
Any portion of the string that is truncated is replaced by a '…' character. Supports characters with display widths greater
|
1014
|
+
than 1. ANSI style sequences do not count toward the display width.
|
1043
1015
|
|
1044
1016
|
If there are ANSI style sequences in the string after where truncation occurs, this function will append them
|
1045
1017
|
to the returned string.
|
@@ -1114,9 +1086,8 @@ def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str:
|
|
1114
1086
|
return truncated_buf.getvalue()
|
1115
1087
|
|
1116
1088
|
|
1117
|
-
def get_styles_dict(text: str) ->
|
1118
|
-
"""
|
1119
|
-
Return an OrderedDict containing all ANSI style sequences found in a string
|
1089
|
+
def get_styles_dict(text: str) -> dict[int, str]:
|
1090
|
+
"""Return an OrderedDict containing all ANSI style sequences found in a string.
|
1120
1091
|
|
1121
1092
|
The structure of the dictionary is:
|
1122
1093
|
key: index where sequences begins
|
@@ -1153,7 +1124,6 @@ def categorize(func: Union[Callable[..., Any], Iterable[Callable[..., Any]]], ca
|
|
1153
1124
|
:param category: category to put it in
|
1154
1125
|
|
1155
1126
|
Example:
|
1156
|
-
|
1157
1127
|
```py
|
1158
1128
|
import cmd2
|
1159
1129
|
class MyApp(cmd2.Cmd):
|
@@ -1164,20 +1134,19 @@ def categorize(func: Union[Callable[..., Any], Iterable[Callable[..., Any]]], ca
|
|
1164
1134
|
```
|
1165
1135
|
|
1166
1136
|
For an alternative approach to categorizing commands using a decorator, see [cmd2.decorators.with_category][]
|
1137
|
+
|
1167
1138
|
"""
|
1168
1139
|
if isinstance(func, Iterable):
|
1169
1140
|
for item in func:
|
1170
1141
|
setattr(item, constants.CMD_ATTR_HELP_CATEGORY, category)
|
1142
|
+
elif inspect.ismethod(func):
|
1143
|
+
setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category) # type: ignore[attr-defined]
|
1171
1144
|
else:
|
1172
|
-
|
1173
|
-
setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category) # type: ignore[attr-defined]
|
1174
|
-
else:
|
1175
|
-
setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category)
|
1145
|
+
setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category)
|
1176
1146
|
|
1177
1147
|
|
1178
|
-
def get_defining_class(meth: Callable[..., Any]) -> Optional[
|
1179
|
-
"""
|
1180
|
-
Attempts to resolve the class that defined a method.
|
1148
|
+
def get_defining_class(meth: Callable[..., Any]) -> Optional[type[Any]]:
|
1149
|
+
"""Attempt to resolve the class that defined a method.
|
1181
1150
|
|
1182
1151
|
Inspired by implementation published here:
|
1183
1152
|
https://stackoverflow.com/a/25959545/1956611
|
@@ -1188,7 +1157,7 @@ def get_defining_class(meth: Callable[..., Any]) -> Optional[Type[Any]]:
|
|
1188
1157
|
if isinstance(meth, functools.partial):
|
1189
1158
|
return get_defining_class(meth.func)
|
1190
1159
|
if inspect.ismethod(meth) or (
|
1191
|
-
inspect.isbuiltin(meth) and
|
1160
|
+
inspect.isbuiltin(meth) and hasattr(meth, '__self__') and hasattr(meth.__self__, '__class__')
|
1192
1161
|
):
|
1193
1162
|
for cls in inspect.getmro(meth.__self__.__class__): # type: ignore[attr-defined]
|
1194
1163
|
if meth.__name__ in cls.__dict__:
|
@@ -1202,7 +1171,7 @@ def get_defining_class(meth: Callable[..., Any]) -> Optional[Type[Any]]:
|
|
1202
1171
|
|
1203
1172
|
|
1204
1173
|
class CompletionMode(Enum):
|
1205
|
-
"""Enum for what type of tab completion to perform in cmd2.Cmd.read_input()"""
|
1174
|
+
"""Enum for what type of tab completion to perform in cmd2.Cmd.read_input()."""
|
1206
1175
|
|
1207
1176
|
# Tab completion will be disabled during read_input() call
|
1208
1177
|
# Use of custom up-arrow history supported
|
@@ -1220,11 +1189,10 @@ class CompletionMode(Enum):
|
|
1220
1189
|
|
1221
1190
|
|
1222
1191
|
class CustomCompletionSettings:
|
1223
|
-
"""Used by cmd2.Cmd.complete() to tab complete strings other than command arguments"""
|
1192
|
+
"""Used by cmd2.Cmd.complete() to tab complete strings other than command arguments."""
|
1224
1193
|
|
1225
1194
|
def __init__(self, parser: argparse.ArgumentParser, *, preserve_quotes: bool = False) -> None:
|
1226
|
-
"""
|
1227
|
-
Initializer
|
1195
|
+
"""CustomCompletionSettings initializer.
|
1228
1196
|
|
1229
1197
|
:param parser: arg parser defining format of string being tab completed
|
1230
1198
|
:param preserve_quotes: if True, then quoted tokens will keep their quotes when processed by
|
@@ -1238,8 +1206,7 @@ class CustomCompletionSettings:
|
|
1238
1206
|
|
1239
1207
|
|
1240
1208
|
def strip_doc_annotations(doc: str) -> str:
|
1241
|
-
"""
|
1242
|
-
Strip annotations from a docstring leaving only the text description
|
1209
|
+
"""Strip annotations from a docstring leaving only the text description.
|
1243
1210
|
|
1244
1211
|
:param doc: documentation string
|
1245
1212
|
"""
|
@@ -1264,8 +1231,10 @@ def strip_doc_annotations(doc: str) -> str:
|
|
1264
1231
|
|
1265
1232
|
|
1266
1233
|
def similarity_function(s1: str, s2: str) -> float:
|
1267
|
-
|
1268
|
-
|
1234
|
+
"""Ratio from s1,s2 may be different to s2,s1. We keep the max.
|
1235
|
+
|
1236
|
+
See https://docs.python.org/3/library/difflib.html#difflib.SequenceMatcher.ratio
|
1237
|
+
"""
|
1269
1238
|
return max(SequenceMatcher(None, s1, s2).ratio(), SequenceMatcher(None, s2, s1).ratio())
|
1270
1239
|
|
1271
1240
|
|
@@ -1275,15 +1244,13 @@ MIN_SIMIL_TO_CONSIDER = 0.7
|
|
1275
1244
|
def suggest_similar(
|
1276
1245
|
requested_command: str, options: Iterable[str], similarity_function_to_use: Optional[Callable[[str, str], float]] = None
|
1277
1246
|
) -> Optional[str]:
|
1278
|
-
"""
|
1279
|
-
Given a requested command and an iterable of possible options returns the most similar (if any is similar)
|
1247
|
+
"""Given a requested command and an iterable of possible options returns the most similar (if any is similar).
|
1280
1248
|
|
1281
1249
|
:param requested_command: The command entered by the user
|
1282
1250
|
:param options: The list of available commands to search for the most similar
|
1283
1251
|
:param similarity_function_to_use: An optional callable to use to compare commands
|
1284
1252
|
:return: The most similar command or None if no one is similar
|
1285
1253
|
"""
|
1286
|
-
|
1287
1254
|
proposed_command = None
|
1288
1255
|
best_simil = MIN_SIMIL_TO_CONSIDER
|
1289
1256
|
requested_command_to_compare = requested_command.lower()
|