cmd2 2.5.10__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/utils.py CHANGED
@@ -1,8 +1,8 @@
1
- # coding=utf-8
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
- """Converts anything to a boolean based on its value.
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
- elif val.capitalize() == str(False):
101
+ if val.capitalize() == str(False):
110
102
  return False
111
103
  raise ValueError("must be True or False (case-insensitive)")
112
- elif isinstance(val, bool):
104
+ if isinstance(val, bool):
113
105
  return val
114
- else:
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[Type[Any], Callable[[Any], Any]],
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(_) -> List[str]: # type: ignore[no-untyped-def]
164
- """Used to tab complete lowercase boolean values"""
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
- """Returns if a file contains only ASCII or UTF-8 encoded text and isn't empty.
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: List[_T]) -> List[_T]:
235
- """Removes duplicates from a list while preserving order of the items.
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]) -> List[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
- Tries to convert the passed-in string to an integer. If that fails, it converts it to lower case using norm_fold.
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) -> List[Union[int, 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]) -> List[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: List[str], tokens_to_quote: List[str]) -> None:
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: List[str], tokens_to_unquote: List[str]) -> None:
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
- Wrap os.expanduser() to support expanding ~ in quoted strings
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: List[str]) -> None:
357
- """
358
- Call expand_user() on all tokens in a list of strings
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
- Used to set cmd2.Cmd.DEFAULT_EDITOR. If EDITOR env variable is set, that will be used.
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 editor, path in itertools.product(editors, paths):
386
- editor_path = os.path.join(path, editor)
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(editor)[0]
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) -> List[str]:
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: List[str], access: int = os.F_OK) -> List[str]:
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) -> List[str]:
427
- """Returns names of executables in a user's path
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
- Class to simulate behavior of sys.stdout or sys.stderr.
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
- else:
540
- return False
517
+ return False
541
518
 
542
519
  @property
543
520
  def line_buffering(self) -> bool:
544
- """
545
- Handle when the inner stream doesn't have a line_buffering attribute which is the case
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
- else:
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 = [b'\n', b'\r']
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
- if any(newline in b for newline in ByteBuf.NEWLINES):
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
- Used to capture stdout and stderr from a Popen process if any of those were set to subprocess.PIPE.
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
- ProcReader initializer
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
- Thread function that reads a stream from the process
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
- assert read_stream is not None
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
- Write bytes to a stream
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
- try:
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
- # When this flag has a positive value, it is considered set.
703
- # When it is 0, it is not set. It should never go below 0.
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: Any) -> None:
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
- RedirectionSavedState initializer
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: List[str]) -> List[str]:
748
- """
749
- Utility function for align_text() / truncate_line() which filters a style list down
750
- to only those which would still be in effect if all were processed in order.
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: Dict[int, str] = 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
- Align text for display within a given width. Supports characters with display widths greater than 1.
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: List[str] = []
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
- # Don't return here even though the line needs no fill chars.
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
- Left align text for display within a given width. Supports characters with display widths greater than 1.
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
- Center text for display within a given width. Supports characters with display widths greater than 1.
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
- Right align text for display within a given width. Supports characters with display widths greater than 1.
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
- Truncate a single line to fit within a given display width. Any portion of the string that is truncated
1041
- is replaced by a '…' character. Supports characters with display widths greater than 1. ANSI style sequences
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) -> Dict[int, 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
- if inspect.ismethod(func):
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[Type[Any]]:
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 getattr(meth, '__self__') is not None and getattr(meth.__self__, '__class__')
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
- # The ratio from s1,s2 may be different to s2,s1. We keep the max.
1268
- # See https://docs.python.org/3/library/difflib.html#difflib.SequenceMatcher.ratio
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()