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/__init__.py +41 -38
- cmd2/argparse_completer.py +80 -81
- cmd2/argparse_custom.py +359 -151
- cmd2/clipboard.py +1 -1
- cmd2/cmd2.py +1272 -845
- cmd2/colors.py +270 -0
- cmd2/command_definition.py +13 -5
- cmd2/constants.py +0 -6
- cmd2/decorators.py +41 -104
- cmd2/exceptions.py +1 -1
- cmd2/history.py +7 -11
- cmd2/parsing.py +12 -17
- cmd2/plugin.py +1 -2
- cmd2/py_bridge.py +15 -10
- cmd2/rich_utils.py +451 -0
- cmd2/rl_utils.py +12 -8
- cmd2/string_utils.py +166 -0
- cmd2/styles.py +72 -0
- cmd2/terminal_utils.py +144 -0
- cmd2/transcript.py +7 -9
- cmd2/utils.py +88 -508
- {cmd2-2.6.2.dist-info → cmd2-3.0.0b1.dist-info}/METADATA +23 -44
- cmd2-3.0.0b1.dist-info/RECORD +27 -0
- cmd2/ansi.py +0 -1093
- cmd2/table_creator.py +0 -1122
- cmd2-2.6.2.dist-info/RECORD +0 -24
- {cmd2-2.6.2.dist-info → cmd2-3.0.0b1.dist-info}/WHEEL +0 -0
- {cmd2-2.6.2.dist-info → cmd2-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
- {cmd2-2.6.2.dist-info → cmd2-3.0.0b1.dist-info}/top_level.txt +0 -0
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 .
|
21
|
-
|
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): #
|
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 = [
|
217
|
-
rtn.extend(
|
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:
|
254
|
-
multiline_commands:
|
255
|
-
aliases:
|
256
|
-
shortcuts:
|
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 =
|
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:
|
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:
|
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:
|
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(
|
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.
|
130
|
-
|
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
|
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
|
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
|
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
|
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:
|
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
|
+
]
|