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