cmd2 2.6.2__py3-none-any.whl → 3.0.0b1__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/parsing.py CHANGED
@@ -7,19 +7,14 @@ from dataclasses import (
7
7
  dataclass,
8
8
  field,
9
9
  )
10
- from typing import (
11
- Any,
12
- Optional,
13
- Union,
14
- )
10
+ from typing import Any
15
11
 
16
12
  from . import (
17
13
  constants,
18
14
  utils,
19
15
  )
20
- from .exceptions import (
21
- Cmd2ShlexError,
22
- )
16
+ from . import string_utils as su
17
+ from .exceptions import Cmd2ShlexError
23
18
 
24
19
 
25
20
  def shlex_split(str_to_split: str) -> list[str]:
@@ -86,7 +81,7 @@ class Macro:
86
81
 
87
82
 
88
83
  @dataclass(frozen=True)
89
- class Statement(str): # type: ignore[override] # noqa: SLOT000
84
+ class Statement(str): # noqa: SLOT000
90
85
  """String subclass with additional attributes to store the results of parsing.
91
86
 
92
87
  The ``cmd`` module in the standard library passes commands around as a
@@ -213,8 +208,8 @@ class Statement(str): # type: ignore[override] # noqa: SLOT000
213
208
  If you want to strip quotes from the input, you can use ``argv[1:]``.
214
209
  """
215
210
  if self.command:
216
- rtn = [utils.strip_quotes(self.command)]
217
- rtn.extend(utils.strip_quotes(cur_token) for cur_token in self.arg_list)
211
+ rtn = [su.strip_quotes(self.command)]
212
+ rtn.extend(su.strip_quotes(cur_token) for cur_token in self.arg_list)
218
213
  else:
219
214
  rtn = []
220
215
 
@@ -250,10 +245,10 @@ class StatementParser:
250
245
 
251
246
  def __init__(
252
247
  self,
253
- terminators: Optional[Iterable[str]] = None,
254
- multiline_commands: Optional[Iterable[str]] = None,
255
- aliases: Optional[dict[str, str]] = None,
256
- shortcuts: Optional[dict[str, str]] = None,
248
+ terminators: Iterable[str] | None = None,
249
+ multiline_commands: Iterable[str] | None = None,
250
+ aliases: dict[str, str] | None = None,
251
+ shortcuts: dict[str, str] | None = None,
257
252
  ) -> None:
258
253
  """Initialize an instance of StatementParser.
259
254
 
@@ -490,7 +485,7 @@ class StatementParser:
490
485
 
491
486
  # Check if we are redirecting to a file
492
487
  if len(tokens) > output_index + 1:
493
- unquoted_path = utils.strip_quotes(tokens[output_index + 1])
488
+ unquoted_path = su.strip_quotes(tokens[output_index + 1])
494
489
  if unquoted_path:
495
490
  output_to = utils.expand_user(tokens[output_index + 1])
496
491
 
@@ -585,7 +580,7 @@ class StatementParser:
585
580
  return Statement(args, raw=rawinput, command=command, multiline_command=multiline_command)
586
581
 
587
582
  def get_command_arg_list(
588
- self, command_name: str, to_parse: Union[Statement, str], preserve_quotes: bool
583
+ self, command_name: str, to_parse: Statement | str, preserve_quotes: bool
589
584
  ) -> tuple[Statement, list[str]]:
590
585
  """Retrieve just the arguments being passed to their ``do_*`` methods as a list.
591
586
 
cmd2/plugin.py CHANGED
@@ -3,7 +3,6 @@
3
3
  from dataclasses import (
4
4
  dataclass,
5
5
  )
6
- from typing import Optional
7
6
 
8
7
  from .parsing import (
9
8
  Statement,
@@ -38,4 +37,4 @@ class CommandFinalizationData:
38
37
  """Data class containing information passed to command finalization hook methods."""
39
38
 
40
39
  stop: bool
41
- statement: Optional[Statement]
40
+ statement: Statement | None
cmd2/py_bridge.py CHANGED
@@ -4,18 +4,13 @@ Maintains a reasonable degree of isolation between the two.
4
4
  """
5
5
 
6
6
  import sys
7
- from contextlib import (
8
- redirect_stderr,
9
- redirect_stdout,
10
- )
7
+ from contextlib import redirect_stderr
11
8
  from typing import (
12
9
  IO,
13
10
  TYPE_CHECKING,
14
11
  Any,
15
12
  NamedTuple,
16
- Optional,
17
13
  TextIO,
18
- Union,
19
14
  cast,
20
15
  )
21
16
 
@@ -101,7 +96,7 @@ class PyBridge:
101
96
  attributes.insert(0, 'cmd_echo')
102
97
  return attributes
103
98
 
104
- def __call__(self, command: str, *, echo: Optional[bool] = None) -> CommandResult:
99
+ def __call__(self, command: str, *, echo: bool | None = None) -> CommandResult:
105
100
  """Provide functionality to call application commands by calling PyBridge.
106
101
 
107
102
  ex: app('help')
@@ -113,8 +108,11 @@ class PyBridge:
113
108
  if echo is None:
114
109
  echo = self.cmd_echo
115
110
 
111
+ # Only capture sys.stdout if it's the same stream as self.stdout
112
+ stdouts_match = self._cmd2_app.stdout == sys.stdout
113
+
116
114
  # This will be used to capture _cmd2_app.stdout and sys.stdout
117
- copy_cmd_stdout = StdSim(cast(Union[TextIO, StdSim], self._cmd2_app.stdout), echo=echo)
115
+ copy_cmd_stdout = StdSim(cast(TextIO | StdSim, self._cmd2_app.stdout), echo=echo)
118
116
 
119
117
  # Pause the storing of stdout until onecmd_plus_hooks enables it
120
118
  copy_cmd_stdout.pause_storage = True
@@ -126,8 +124,12 @@ class PyBridge:
126
124
 
127
125
  stop = False
128
126
  try:
129
- self._cmd2_app.stdout = cast(TextIO, copy_cmd_stdout)
130
- with redirect_stdout(cast(IO[str], copy_cmd_stdout)), redirect_stderr(cast(IO[str], copy_stderr)):
127
+ with self._cmd2_app.sigint_protection:
128
+ self._cmd2_app.stdout = cast(TextIO, copy_cmd_stdout)
129
+ if stdouts_match:
130
+ sys.stdout = self._cmd2_app.stdout
131
+
132
+ with redirect_stderr(cast(IO[str], copy_stderr)):
131
133
  stop = self._cmd2_app.onecmd_plus_hooks(
132
134
  command,
133
135
  add_to_history=self._add_to_history,
@@ -136,6 +138,9 @@ class PyBridge:
136
138
  finally:
137
139
  with self._cmd2_app.sigint_protection:
138
140
  self._cmd2_app.stdout = cast(IO[str], copy_cmd_stdout.inner_stream)
141
+ if stdouts_match:
142
+ sys.stdout = self._cmd2_app.stdout
143
+
139
144
  self.stop = stop or self.stop
140
145
 
141
146
  # Save the result
cmd2/rich_utils.py ADDED
@@ -0,0 +1,451 @@
1
+ """Provides common utilities to support Rich in cmd2-based applications."""
2
+
3
+ import re
4
+ from collections.abc import (
5
+ Iterable,
6
+ Mapping,
7
+ )
8
+ from enum import Enum
9
+ from typing import (
10
+ IO,
11
+ Any,
12
+ TypedDict,
13
+ )
14
+
15
+ from rich.console import (
16
+ Console,
17
+ ConsoleRenderable,
18
+ JustifyMethod,
19
+ OverflowMethod,
20
+ RenderableType,
21
+ )
22
+ from rich.padding import Padding
23
+ from rich.pretty import is_expandable
24
+ from rich.protocol import rich_cast
25
+ from rich.segment import Segment
26
+ from rich.style import StyleType
27
+ from rich.table import (
28
+ Column,
29
+ Table,
30
+ )
31
+ from rich.text import Text
32
+ from rich.theme import Theme
33
+ from rich_argparse import RichHelpFormatter
34
+
35
+ from .styles import DEFAULT_CMD2_STYLES
36
+
37
+ # A compiled regular expression to detect ANSI style sequences.
38
+ ANSI_STYLE_SEQUENCE_RE = re.compile(r"\x1b\[[0-9;?]*m")
39
+
40
+
41
+ class AllowStyle(Enum):
42
+ """Values for ``cmd2.rich_utils.ALLOW_STYLE``."""
43
+
44
+ ALWAYS = "Always" # Always output ANSI style sequences
45
+ NEVER = "Never" # Remove ANSI style sequences from all output
46
+ TERMINAL = "Terminal" # Remove ANSI style sequences if the output is not going to the terminal
47
+
48
+ def __str__(self) -> str:
49
+ """Return value instead of enum name for printing in cmd2's set command."""
50
+ return str(self.value)
51
+
52
+ def __repr__(self) -> str:
53
+ """Return quoted value instead of enum description for printing in cmd2's set command."""
54
+ return repr(self.value)
55
+
56
+
57
+ # Controls when ANSI style sequences are allowed in output
58
+ ALLOW_STYLE = AllowStyle.TERMINAL
59
+
60
+
61
+ def _create_default_theme() -> Theme:
62
+ """Create a default theme for the application.
63
+
64
+ This theme combines the default styles from cmd2, rich-argparse, and Rich.
65
+ """
66
+ app_styles = DEFAULT_CMD2_STYLES.copy()
67
+ app_styles.update(RichHelpFormatter.styles.copy())
68
+ return Theme(app_styles, inherit=True)
69
+
70
+
71
+ def set_theme(styles: Mapping[str, StyleType] | None = None) -> None:
72
+ """Set the Rich theme used by cmd2.
73
+
74
+ Call set_theme() with no arguments to reset to the default theme.
75
+ This will clear any custom styles that were previously applied.
76
+
77
+ :param styles: optional mapping of style names to styles
78
+ """
79
+ global APP_THEME # noqa: PLW0603
80
+
81
+ # Start with a fresh copy of the default styles.
82
+ app_styles: dict[str, StyleType] = {}
83
+ app_styles.update(_create_default_theme().styles)
84
+
85
+ # Incorporate custom styles.
86
+ if styles is not None:
87
+ app_styles.update(styles)
88
+
89
+ APP_THEME = Theme(app_styles)
90
+
91
+ # Synchronize rich-argparse styles with the main application theme.
92
+ for name in RichHelpFormatter.styles.keys() & APP_THEME.styles.keys():
93
+ RichHelpFormatter.styles[name] = APP_THEME.styles[name]
94
+
95
+
96
+ # The application-wide theme. You can change it with set_theme().
97
+ APP_THEME = _create_default_theme()
98
+
99
+
100
+ class RichPrintKwargs(TypedDict, total=False):
101
+ """Keyword arguments that can be passed to rich.console.Console.print() via cmd2's print methods.
102
+
103
+ See Rich's Console.print() documentation for full details on these parameters.
104
+ https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console.print
105
+
106
+ Note: All fields are optional (total=False). If a key is not present in the
107
+ dictionary, Rich's default behavior for that argument will apply.
108
+ """
109
+
110
+ justify: JustifyMethod | None
111
+ overflow: OverflowMethod | None
112
+ no_wrap: bool | None
113
+ width: int | None
114
+ height: int | None
115
+ crop: bool
116
+ new_line_start: bool
117
+
118
+
119
+ class Cmd2BaseConsole(Console):
120
+ """Base class for all cmd2 Rich consoles.
121
+
122
+ This class handles the core logic for managing Rich behavior based on
123
+ cmd2's global settings, such as `ALLOW_STYLE` and `APP_THEME`.
124
+ """
125
+
126
+ def __init__(
127
+ self,
128
+ file: IO[str] | None = None,
129
+ **kwargs: Any,
130
+ ) -> None:
131
+ """Cmd2BaseConsole initializer.
132
+
133
+ :param file: optional file object where the console should write to.
134
+ Defaults to sys.stdout.
135
+ :param kwargs: keyword arguments passed to the parent Console class.
136
+ :raises TypeError: if disallowed keyword argument is passed in.
137
+ """
138
+ # Don't allow force_terminal or force_interactive to be passed in, as their
139
+ # behavior is controlled by the ALLOW_STYLE setting.
140
+ if "force_terminal" in kwargs:
141
+ raise TypeError(
142
+ "Passing 'force_terminal' is not allowed. Its behavior is controlled by the 'ALLOW_STYLE' setting."
143
+ )
144
+ if "force_interactive" in kwargs:
145
+ raise TypeError(
146
+ "Passing 'force_interactive' is not allowed. Its behavior is controlled by the 'ALLOW_STYLE' setting."
147
+ )
148
+
149
+ # Don't allow a theme to be passed in, as it is controlled by the global APP_THEME.
150
+ # Use cmd2.rich_utils.set_theme() to set the global theme or use a temporary
151
+ # theme with console.use_theme().
152
+ if "theme" in kwargs:
153
+ raise TypeError(
154
+ "Passing 'theme' is not allowed. Its behavior is controlled by the global APP_THEME and set_theme()."
155
+ )
156
+
157
+ force_terminal: bool | None = None
158
+ force_interactive: bool | None = None
159
+
160
+ if ALLOW_STYLE == AllowStyle.ALWAYS:
161
+ force_terminal = True
162
+
163
+ # Turn off interactive mode if dest is not actually a terminal which supports it
164
+ tmp_console = Console(file=file)
165
+ force_interactive = tmp_console.is_interactive
166
+ elif ALLOW_STYLE == AllowStyle.NEVER:
167
+ force_terminal = False
168
+
169
+ super().__init__(
170
+ file=file,
171
+ force_terminal=force_terminal,
172
+ force_interactive=force_interactive,
173
+ theme=APP_THEME,
174
+ **kwargs,
175
+ )
176
+
177
+ def on_broken_pipe(self) -> None:
178
+ """Override which raises BrokenPipeError instead of SystemExit."""
179
+ self.quiet = True
180
+ raise BrokenPipeError
181
+
182
+
183
+ class Cmd2GeneralConsole(Cmd2BaseConsole):
184
+ """Rich console for general-purpose printing."""
185
+
186
+ def __init__(self, file: IO[str] | None = None) -> None:
187
+ """Cmd2GeneralConsole initializer.
188
+
189
+ :param file: optional file object where the console should write to.
190
+ Defaults to sys.stdout.
191
+ """
192
+ # This console is configured for general-purpose printing. It enables soft wrap
193
+ # and disables Rich's automatic detection for markup, emoji, and highlighting.
194
+ # These defaults can be overridden in calls to the console's or cmd2's print methods.
195
+ super().__init__(
196
+ file=file,
197
+ soft_wrap=True,
198
+ markup=False,
199
+ emoji=False,
200
+ highlight=False,
201
+ )
202
+
203
+
204
+ class Cmd2RichArgparseConsole(Cmd2BaseConsole):
205
+ """Rich console for rich-argparse output.
206
+
207
+ This class ensures long lines in help text are not truncated by avoiding soft_wrap,
208
+ which conflicts with rich-argparse's explicit no_wrap and overflow settings.
209
+ """
210
+
211
+ def __init__(self, file: IO[str] | None = None) -> None:
212
+ """Cmd2RichArgparseConsole initializer.
213
+
214
+ :param file: optional file object where the console should write to.
215
+ Defaults to sys.stdout.
216
+ """
217
+ # Since this console is used to print error messages which may not have
218
+ # been pre-formatted by rich-argparse, disable Rich's automatic detection
219
+ # for markup, emoji, and highlighting. rich-argparse does markup and
220
+ # highlighting without involving the console so these won't affect its
221
+ # internal functionality.
222
+ super().__init__(
223
+ file=file,
224
+ markup=False,
225
+ emoji=False,
226
+ highlight=False,
227
+ )
228
+
229
+
230
+ class Cmd2ExceptionConsole(Cmd2BaseConsole):
231
+ """Rich console for printing exceptions.
232
+
233
+ Ensures that long exception messages word wrap for readability by keeping soft_wrap disabled.
234
+ """
235
+
236
+
237
+ def console_width() -> int:
238
+ """Return the width of the console."""
239
+ return Console().width
240
+
241
+
242
+ def rich_text_to_string(text: Text) -> str:
243
+ """Convert a Rich Text object to a string.
244
+
245
+ This function's purpose is to render a Rich Text object, including any styles (e.g., color, bold),
246
+ to a plain Python string with ANSI style sequences. It differs from `text.plain`, which strips
247
+ all formatting.
248
+
249
+ :param text: the text object to convert
250
+ :return: the resulting string with ANSI styles preserved.
251
+ """
252
+ console = Console(
253
+ force_terminal=True,
254
+ soft_wrap=True,
255
+ no_color=False,
256
+ markup=False,
257
+ emoji=False,
258
+ highlight=False,
259
+ theme=APP_THEME,
260
+ )
261
+ with console.capture() as capture:
262
+ console.print(text, end="")
263
+ return capture.get()
264
+
265
+
266
+ def indent(renderable: RenderableType, level: int) -> Padding:
267
+ """Indent a Rich renderable.
268
+
269
+ When soft-wrapping is enabled, a Rich console is unable to properly print a
270
+ Padding object of indented text, as it truncates long strings instead of wrapping
271
+ them. This function provides a workaround for this issue, ensuring that indented
272
+ text is printed correctly regardless of the soft-wrap setting.
273
+
274
+ For non-text objects, this function merely serves as a convenience
275
+ wrapper around Padding.indent().
276
+
277
+ :param renderable: a Rich renderable to indent.
278
+ :param level: number of characters to indent.
279
+ :return: a Padding object containing the indented content.
280
+ """
281
+ if isinstance(renderable, (str, Text)):
282
+ # Wrap text in a grid to handle the wrapping.
283
+ text_grid = Table.grid(Column(overflow="fold"))
284
+ text_grid.add_row(renderable)
285
+ renderable = text_grid
286
+
287
+ return Padding.indent(renderable, level)
288
+
289
+
290
+ def prepare_objects_for_rendering(*objects: Any) -> tuple[Any, ...]:
291
+ """Prepare a tuple of objects for printing by Rich's Console.print().
292
+
293
+ This function processes objects to ensure they are rendered correctly by Rich.
294
+ It inspects each object and, if its string representation contains ANSI style
295
+ sequences, it converts the object to a Rich Text object. This ensures Rich can
296
+ properly parse the non-printing codes for accurate display width calculation.
297
+
298
+ Objects that already implement the Rich console protocol or are expandable
299
+ by its pretty printer are left untouched, as they can be handled directly by
300
+ Rich's native renderers.
301
+
302
+ :param objects: objects to prepare
303
+ :return: a tuple containing the processed objects.
304
+ """
305
+ object_list = list(objects)
306
+
307
+ for i, obj in enumerate(object_list):
308
+ # Resolve the object's final renderable form, including those
309
+ # with a __rich__ method that might return a string.
310
+ renderable = rich_cast(obj)
311
+
312
+ # No preprocessing is needed for Rich-compatible or expandable objects.
313
+ if isinstance(renderable, ConsoleRenderable) or is_expandable(renderable):
314
+ continue
315
+
316
+ # Check for ANSI style sequences in its string representation.
317
+ renderable_as_str = str(renderable)
318
+ if ANSI_STYLE_SEQUENCE_RE.search(renderable_as_str):
319
+ object_list[i] = Text.from_ansi(renderable_as_str)
320
+
321
+ return tuple(object_list)
322
+
323
+
324
+ ###################################################################################
325
+ # Rich Library Monkey Patches
326
+ #
327
+ # These patches fix specific bugs in the Rich library. They are conditional and
328
+ # will only be applied if the bug is detected. When the bugs are fixed in a
329
+ # future Rich release, these patches and their corresponding tests should be
330
+ # removed.
331
+ ###################################################################################
332
+
333
+ ###################################################################################
334
+ # Text.from_ansi() monkey patch
335
+ ###################################################################################
336
+
337
+ # Save original Text.from_ansi() so we can call it in our wrapper
338
+ _orig_text_from_ansi = Text.from_ansi
339
+
340
+
341
+ @classmethod # type: ignore[misc]
342
+ def _from_ansi_wrapper(cls: type[Text], text: str, *args: Any, **kwargs: Any) -> Text: # noqa: ARG001
343
+ r"""Wrap Text.from_ansi() to fix its trailing newline bug.
344
+
345
+ This wrapper handles an issue where Text.from_ansi() removes the
346
+ trailing line break from a string (e.g. "Hello\n" becomes "Hello").
347
+
348
+ There is currently a pull request on Rich to fix this.
349
+ https://github.com/Textualize/rich/pull/3793
350
+ """
351
+ result = _orig_text_from_ansi(text, *args, **kwargs)
352
+
353
+ # If the original string ends with a recognized line break character,
354
+ # then restore the missing newline. We use "\n" because Text.from_ansi()
355
+ # converts all line breaks into newlines.
356
+ # Source: https://docs.python.org/3/library/stdtypes.html#str.splitlines
357
+ line_break_chars = {
358
+ "\n", # Line Feed
359
+ "\r", # Carriage Return
360
+ "\v", # Vertical Tab
361
+ "\f", # Form Feed
362
+ "\x1c", # File Separator
363
+ "\x1d", # Group Separator
364
+ "\x1e", # Record Separator
365
+ "\x85", # Next Line (NEL)
366
+ "\u2028", # Line Separator
367
+ "\u2029", # Paragraph Separator
368
+ }
369
+ if text and text[-1] in line_break_chars:
370
+ result.append("\n")
371
+
372
+ return result
373
+
374
+
375
+ def _from_ansi_has_newline_bug() -> bool:
376
+ """Check if Test.from_ansi() strips the trailing line break from a string."""
377
+ return Text.from_ansi("\n") == Text.from_ansi("")
378
+
379
+
380
+ # Only apply the monkey patch if the bug is present
381
+ if _from_ansi_has_newline_bug():
382
+ Text.from_ansi = _from_ansi_wrapper # type: ignore[assignment]
383
+
384
+
385
+ ###################################################################################
386
+ # Segment.apply_style() monkey patch
387
+ ###################################################################################
388
+
389
+ # Save original Segment.apply_style() so we can call it in our wrapper
390
+ _orig_segment_apply_style = Segment.apply_style
391
+
392
+
393
+ @classmethod # type: ignore[misc]
394
+ def _apply_style_wrapper(cls: type[Segment], *args: Any, **kwargs: Any) -> Iterable["Segment"]:
395
+ r"""Wrap Segment.apply_style() to fix bug with styling newlines.
396
+
397
+ This wrapper handles an issue where Segment.apply_style() includes newlines
398
+ within styled Segments. As a result, when printing text using a background color
399
+ and soft wrapping, the background color incorrectly carries over onto the following line.
400
+
401
+ You can reproduce this behavior by calling console.print() using a background color
402
+ and soft wrapping.
403
+
404
+ For example:
405
+ console.print("line_1", style="blue on white", soft_wrap=True)
406
+
407
+ When soft wrapping is disabled, console.print() splits Segments into their individual
408
+ lines, which separates the newlines from the styled text. Therefore, the background color
409
+ issue does not occur in that mode.
410
+
411
+ This function copies that behavior to fix this the issue even when soft wrapping is enabled.
412
+
413
+ There is currently a pull request on Rich to fix this.
414
+ https://github.com/Textualize/rich/pull/3839
415
+ """
416
+ styled_segments = list(_orig_segment_apply_style(*args, **kwargs))
417
+ newline_segment = cls.line()
418
+
419
+ # If the final segment ends in a newline, that newline will be stripped by Segment.split_lines().
420
+ # Save an unstyled newline to restore later.
421
+ end_segment = newline_segment if styled_segments and styled_segments[-1].text.endswith("\n") else None
422
+
423
+ # Use Segment.split_lines() to separate the styled text from the newlines.
424
+ # This way the ANSI reset code will appear before any newline.
425
+ sanitized_segments: list[Segment] = []
426
+
427
+ lines = list(Segment.split_lines(styled_segments))
428
+ for index, line in enumerate(lines):
429
+ sanitized_segments.extend(line)
430
+ if index < len(lines) - 1:
431
+ sanitized_segments.append(newline_segment)
432
+
433
+ if end_segment is not None:
434
+ sanitized_segments.append(end_segment)
435
+
436
+ return sanitized_segments
437
+
438
+
439
+ def _rich_has_styled_newline_bug() -> bool:
440
+ """Check if newlines are styled when soft wrapping."""
441
+ console = Console(force_terminal=True)
442
+ with console.capture() as capture:
443
+ console.print("line_1", style="blue on white", soft_wrap=True)
444
+
445
+ # Check if we see a styled newline in the output
446
+ return "\x1b[34;47m\n\x1b[0m" in capture.get()
447
+
448
+
449
+ # Only apply the monkey patch if the bug is present
450
+ if _rich_has_styled_newline_bug():
451
+ Segment.apply_style = _apply_style_wrapper # type: ignore[assignment]
cmd2/rl_utils.py CHANGED
@@ -5,7 +5,6 @@ import sys
5
5
  from enum import (
6
6
  Enum,
7
7
  )
8
- from typing import Union
9
8
 
10
9
  #########################################################################################################################
11
10
  # NOTE ON LIBEDIT:
@@ -25,11 +24,11 @@ from typing import Union
25
24
 
26
25
  # Prefer statically linked gnureadline if installed due to compatibility issues with libedit
27
26
  try:
28
- import gnureadline as readline # type: ignore[import]
27
+ import gnureadline as readline # type: ignore[import-not-found]
29
28
  except ImportError:
30
29
  # Note: If this actually fails, you should install gnureadline on Linux/Mac or pyreadline3 on Windows.
31
30
  with contextlib.suppress(ImportError):
32
- import readline # type: ignore[no-redef]
31
+ import readline
33
32
 
34
33
 
35
34
  class RlType(Enum):
@@ -133,7 +132,7 @@ elif 'gnureadline' in sys.modules or 'readline' in sys.modules:
133
132
  readline_lib = ctypes.CDLL(readline.__file__)
134
133
  except (AttributeError, OSError): # pragma: no cover
135
134
  _rl_warn_reason = (
136
- "this application is running in a non-standard Python environment in\n"
135
+ "this application is running in a non-standard Python environment in "
137
136
  "which GNU readline is not loaded dynamically from a shared library file."
138
137
  )
139
138
  else:
@@ -144,10 +143,10 @@ elif 'gnureadline' in sys.modules or 'readline' in sys.modules:
144
143
  if rl_type == RlType.NONE: # pragma: no cover
145
144
  if not _rl_warn_reason:
146
145
  _rl_warn_reason = (
147
- "no supported version of readline was found. To resolve this, install\n"
146
+ "no supported version of readline was found. To resolve this, install "
148
147
  "pyreadline3 on Windows or gnureadline on Linux/Mac."
149
148
  )
150
- rl_warning = "Readline features including tab completion have been disabled because\n" + _rl_warn_reason + '\n\n'
149
+ rl_warning = f"Readline features including tab completion have been disabled because {_rl_warn_reason}\n\n"
151
150
  else:
152
151
  rl_warning = ''
153
152
 
@@ -191,7 +190,7 @@ def rl_get_prompt() -> str: # pragma: no cover
191
190
  prompt = '' if encoded_prompt is None else encoded_prompt.decode(encoding='utf-8')
192
191
 
193
192
  elif rl_type == RlType.PYREADLINE:
194
- prompt_data: Union[str, bytes] = readline.rl.prompt
193
+ prompt_data: str | bytes = readline.rl.prompt
195
194
  prompt = prompt_data.decode(encoding='utf-8') if isinstance(prompt_data, bytes) else prompt_data
196
195
 
197
196
  else:
@@ -288,10 +287,15 @@ def rl_in_search_mode() -> bool: # pragma: no cover
288
287
  if not isinstance(readline.rl.mode, EmacsMode):
289
288
  return False
290
289
 
291
- # While in search mode, the current keyevent function is set one of the following.
290
+ # While in search mode, the current keyevent function is set to one of the following.
292
291
  search_funcs = (
293
292
  readline.rl.mode._process_incremental_search_keyevent,
294
293
  readline.rl.mode._process_non_incremental_search_keyevent,
295
294
  )
296
295
  return readline.rl.mode.process_keyevent_queue[-1] in search_funcs
297
296
  return False
297
+
298
+
299
+ __all__ = [
300
+ 'readline',
301
+ ]