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/string_utils.py
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
"""Provides string utility functions.
|
2
|
+
|
3
|
+
This module offers a collection of string utility functions built on the Rich library.
|
4
|
+
These utilities are designed to correctly handle strings with ANSI style sequences and
|
5
|
+
full-width characters (like those used in CJK languages).
|
6
|
+
"""
|
7
|
+
|
8
|
+
from rich.align import AlignMethod
|
9
|
+
from rich.style import StyleType
|
10
|
+
from rich.text import Text
|
11
|
+
|
12
|
+
from . import rich_utils as ru
|
13
|
+
|
14
|
+
|
15
|
+
def align(
|
16
|
+
val: str,
|
17
|
+
align: AlignMethod,
|
18
|
+
width: int | None = None,
|
19
|
+
character: str = " ",
|
20
|
+
) -> str:
|
21
|
+
"""Align string to a given width.
|
22
|
+
|
23
|
+
There are convenience wrappers around this function: align_left(), align_center(), and align_right()
|
24
|
+
|
25
|
+
:param val: string to align
|
26
|
+
:param align: one of "left", "center", or "right".
|
27
|
+
:param width: Desired width. Defaults to width of the terminal.
|
28
|
+
:param character: Character to pad with. Defaults to " ".
|
29
|
+
|
30
|
+
"""
|
31
|
+
if width is None:
|
32
|
+
width = ru.console_width()
|
33
|
+
|
34
|
+
text = Text.from_ansi(val)
|
35
|
+
text.align(align, width=width, character=character)
|
36
|
+
return ru.rich_text_to_string(text)
|
37
|
+
|
38
|
+
|
39
|
+
def align_left(
|
40
|
+
val: str,
|
41
|
+
width: int | None = None,
|
42
|
+
character: str = " ",
|
43
|
+
) -> str:
|
44
|
+
"""Left-align string to a given width.
|
45
|
+
|
46
|
+
:param val: string to align
|
47
|
+
:param width: Desired width. Defaults to width of the terminal.
|
48
|
+
:param character: Character to pad with. Defaults to " ".
|
49
|
+
|
50
|
+
"""
|
51
|
+
return align(val, "left", width=width, character=character)
|
52
|
+
|
53
|
+
|
54
|
+
def align_center(
|
55
|
+
val: str,
|
56
|
+
width: int | None = None,
|
57
|
+
character: str = " ",
|
58
|
+
) -> str:
|
59
|
+
"""Center-align string to a given width.
|
60
|
+
|
61
|
+
:param val: string to align
|
62
|
+
:param width: Desired width. Defaults to width of the terminal.
|
63
|
+
:param character: Character to pad with. Defaults to " ".
|
64
|
+
|
65
|
+
"""
|
66
|
+
return align(val, "center", width=width, character=character)
|
67
|
+
|
68
|
+
|
69
|
+
def align_right(
|
70
|
+
val: str,
|
71
|
+
width: int | None = None,
|
72
|
+
character: str = " ",
|
73
|
+
) -> str:
|
74
|
+
"""Right-align string to a given width.
|
75
|
+
|
76
|
+
:param val: string to align
|
77
|
+
:param width: Desired width. Defaults to width of the terminal.
|
78
|
+
:param character: Character to pad with. Defaults to " ".
|
79
|
+
|
80
|
+
"""
|
81
|
+
return align(val, "right", width=width, character=character)
|
82
|
+
|
83
|
+
|
84
|
+
def stylize(val: str, style: StyleType) -> str:
|
85
|
+
"""Apply an ANSI style to a string, preserving any existing styles.
|
86
|
+
|
87
|
+
:param val: string to be styled
|
88
|
+
:param style: style instance or style definition to apply.
|
89
|
+
:return: the stylized string
|
90
|
+
"""
|
91
|
+
# Convert to a Rich Text object to parse and preserve existing ANSI styles.
|
92
|
+
text = Text.from_ansi(val)
|
93
|
+
text.stylize(style)
|
94
|
+
return ru.rich_text_to_string(text)
|
95
|
+
|
96
|
+
|
97
|
+
def strip_style(val: str) -> str:
|
98
|
+
"""Strip all ANSI style sequences from a string.
|
99
|
+
|
100
|
+
:param val: string to be stripped
|
101
|
+
:return: the stripped string
|
102
|
+
"""
|
103
|
+
return ru.ANSI_STYLE_SEQUENCE_RE.sub("", val)
|
104
|
+
|
105
|
+
|
106
|
+
def str_width(val: str) -> int:
|
107
|
+
"""Return the display width of a string.
|
108
|
+
|
109
|
+
This is intended for single-line strings.
|
110
|
+
Replace tabs with spaces before calling this.
|
111
|
+
|
112
|
+
:param val: the string being measured
|
113
|
+
:return: width of the string when printed to the terminal
|
114
|
+
"""
|
115
|
+
text = Text.from_ansi(val)
|
116
|
+
return text.cell_len
|
117
|
+
|
118
|
+
|
119
|
+
def is_quoted(val: str) -> bool:
|
120
|
+
"""Check if a string is quoted.
|
121
|
+
|
122
|
+
:param val: the string being checked for quotes
|
123
|
+
:return: True if a string is quoted
|
124
|
+
"""
|
125
|
+
from . import constants
|
126
|
+
|
127
|
+
return len(val) > 1 and val[0] == val[-1] and val[0] in constants.QUOTES
|
128
|
+
|
129
|
+
|
130
|
+
def quote(val: str) -> str:
|
131
|
+
"""Quote a string."""
|
132
|
+
quote = "'" if '"' in val else '"'
|
133
|
+
|
134
|
+
return quote + val + quote
|
135
|
+
|
136
|
+
|
137
|
+
def quote_if_needed(val: str) -> str:
|
138
|
+
"""Quote a string if it contains spaces and isn't already quoted."""
|
139
|
+
if is_quoted(val) or ' ' not in val:
|
140
|
+
return val
|
141
|
+
|
142
|
+
return quote(val)
|
143
|
+
|
144
|
+
|
145
|
+
def strip_quotes(val: str) -> str:
|
146
|
+
"""Strip outer quotes from a string.
|
147
|
+
|
148
|
+
Applies to both single and double quotes.
|
149
|
+
|
150
|
+
:param val: string to strip outer quotes from
|
151
|
+
:return: same string with potentially outer quotes stripped
|
152
|
+
"""
|
153
|
+
if is_quoted(val):
|
154
|
+
val = val[1:-1]
|
155
|
+
return val
|
156
|
+
|
157
|
+
|
158
|
+
def norm_fold(val: str) -> str:
|
159
|
+
"""Normalize and casefold Unicode strings for saner comparisons.
|
160
|
+
|
161
|
+
:param val: input unicode string
|
162
|
+
:return: a normalized and case-folded version of the input string
|
163
|
+
"""
|
164
|
+
import unicodedata
|
165
|
+
|
166
|
+
return unicodedata.normalize("NFC", val).casefold()
|
cmd2/styles.py
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
"""Defines custom Rich styles and their corresponding names for cmd2.
|
2
|
+
|
3
|
+
This module provides a centralized and discoverable way to manage Rich styles
|
4
|
+
used within the cmd2 framework. It defines a StrEnum for style names and a
|
5
|
+
dictionary that maps these names to their default style objects.
|
6
|
+
|
7
|
+
**Notes**
|
8
|
+
|
9
|
+
Cmd2 uses Rich for its terminal output, and while this module defines a set of
|
10
|
+
cmd2-specific styles, it's important to understand that these aren't the only
|
11
|
+
styles that can appear. Components like Rich tracebacks and the rich-argparse
|
12
|
+
library, which cmd2 uses for its help output, also apply their own built-in
|
13
|
+
styles. Additionally, app developers may use other Rich objects that have
|
14
|
+
their own default styles.
|
15
|
+
|
16
|
+
For a complete theming experience, you can create a custom theme that includes
|
17
|
+
styles from Rich and rich-argparse. The `cmd2.rich_utils.set_theme()` function
|
18
|
+
automatically updates rich-argparse's styles with any custom styles provided in
|
19
|
+
your theme dictionary, so you don't have to modify them directly.
|
20
|
+
|
21
|
+
You can find Rich's default styles in the `rich.default_styles` module.
|
22
|
+
For rich-argparse, the style names are defined in the
|
23
|
+
`rich_argparse.RichHelpFormatter.styles` dictionary.
|
24
|
+
|
25
|
+
"""
|
26
|
+
|
27
|
+
import sys
|
28
|
+
|
29
|
+
from rich.style import (
|
30
|
+
Style,
|
31
|
+
StyleType,
|
32
|
+
)
|
33
|
+
|
34
|
+
if sys.version_info >= (3, 11):
|
35
|
+
from enum import StrEnum
|
36
|
+
else:
|
37
|
+
from backports.strenum import StrEnum
|
38
|
+
|
39
|
+
from .colors import Color
|
40
|
+
|
41
|
+
|
42
|
+
class Cmd2Style(StrEnum):
|
43
|
+
"""An enumeration of the names of custom Rich styles used in cmd2.
|
44
|
+
|
45
|
+
Using this enum allows for autocompletion and prevents typos when
|
46
|
+
referencing cmd2-specific styles.
|
47
|
+
|
48
|
+
This StrEnum is tightly coupled with `DEFAULT_CMD2_STYLES`. Any name
|
49
|
+
added here must have a corresponding style definition there.
|
50
|
+
"""
|
51
|
+
|
52
|
+
COMMAND_LINE = "cmd2.example" # Command line examples in help text
|
53
|
+
ERROR = "cmd2.error" # Error text (used by perror())
|
54
|
+
EXCEPTION_TYPE = "cmd2.exception.type" # Used by pexcept to mark an exception type
|
55
|
+
HELP_HEADER = "cmd2.help.header" # Help table header text
|
56
|
+
HELP_LEADER = "cmd2.help.leader" # Text right before the help tables are listed
|
57
|
+
SUCCESS = "cmd2.success" # Success text (used by psuccess())
|
58
|
+
TABLE_BORDER = "cmd2.table_border" # Applied to cmd2's table borders
|
59
|
+
WARNING = "cmd2.warning" # Warning text (used by pwarning())
|
60
|
+
|
61
|
+
|
62
|
+
# Default styles used by cmd2. Tightly coupled with the Cmd2Style enum.
|
63
|
+
DEFAULT_CMD2_STYLES: dict[str, StyleType] = {
|
64
|
+
Cmd2Style.COMMAND_LINE: Style(color=Color.CYAN, bold=True),
|
65
|
+
Cmd2Style.ERROR: Style(color=Color.BRIGHT_RED),
|
66
|
+
Cmd2Style.EXCEPTION_TYPE: Style(color=Color.DARK_ORANGE, bold=True),
|
67
|
+
Cmd2Style.HELP_HEADER: Style(color=Color.BRIGHT_GREEN, bold=True),
|
68
|
+
Cmd2Style.HELP_LEADER: Style(color=Color.CYAN, bold=True),
|
69
|
+
Cmd2Style.SUCCESS: Style(color=Color.GREEN),
|
70
|
+
Cmd2Style.TABLE_BORDER: Style(color=Color.BRIGHT_GREEN),
|
71
|
+
Cmd2Style.WARNING: Style(color=Color.BRIGHT_YELLOW),
|
72
|
+
}
|
cmd2/terminal_utils.py
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
r"""Support for terminal control escape sequences.
|
2
|
+
|
3
|
+
These are used for things like setting the window title and asynchronous alerts.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from . import string_utils as su
|
7
|
+
|
8
|
+
#######################################################
|
9
|
+
# Common ANSI escape sequence constants
|
10
|
+
#######################################################
|
11
|
+
ESC = '\x1b'
|
12
|
+
CSI = f'{ESC}['
|
13
|
+
OSC = f'{ESC}]'
|
14
|
+
BEL = '\a'
|
15
|
+
|
16
|
+
|
17
|
+
####################################################################################
|
18
|
+
# Utility functions which create various ANSI sequences
|
19
|
+
####################################################################################
|
20
|
+
def set_title_str(title: str) -> str:
|
21
|
+
"""Generate a string that, when printed, sets a terminal's window title.
|
22
|
+
|
23
|
+
:param title: new title for the window
|
24
|
+
:return: the set title string
|
25
|
+
"""
|
26
|
+
return f"{OSC}2;{title}{BEL}"
|
27
|
+
|
28
|
+
|
29
|
+
def clear_screen_str(clear_type: int = 2) -> str:
|
30
|
+
"""Generate a string that, when printed, clears a terminal screen based on value of clear_type.
|
31
|
+
|
32
|
+
:param clear_type: integer which specifies how to clear the screen (Defaults to 2)
|
33
|
+
Possible values:
|
34
|
+
0 - clear from cursor to end of screen
|
35
|
+
1 - clear from cursor to beginning of the screen
|
36
|
+
2 - clear entire screen
|
37
|
+
3 - clear entire screen and delete all lines saved in the scrollback buffer
|
38
|
+
:return: the clear screen string
|
39
|
+
:raises ValueError: if clear_type is not a valid value
|
40
|
+
"""
|
41
|
+
if 0 <= clear_type <= 3:
|
42
|
+
return f"{CSI}{clear_type}J"
|
43
|
+
raise ValueError("clear_type must in an integer from 0 to 3")
|
44
|
+
|
45
|
+
|
46
|
+
def clear_line_str(clear_type: int = 2) -> str:
|
47
|
+
"""Generate a string that, when printed, clears a line based on value of clear_type.
|
48
|
+
|
49
|
+
:param clear_type: integer which specifies how to clear the line (Defaults to 2)
|
50
|
+
Possible values:
|
51
|
+
0 - clear from cursor to the end of the line
|
52
|
+
1 - clear from cursor to beginning of the line
|
53
|
+
2 - clear entire line
|
54
|
+
:return: the clear line string
|
55
|
+
:raises ValueError: if clear_type is not a valid value
|
56
|
+
"""
|
57
|
+
if 0 <= clear_type <= 2:
|
58
|
+
return f"{CSI}{clear_type}K"
|
59
|
+
raise ValueError("clear_type must in an integer from 0 to 2")
|
60
|
+
|
61
|
+
|
62
|
+
####################################################################################
|
63
|
+
# Implementations intended for direct use (do NOT use outside of cmd2)
|
64
|
+
####################################################################################
|
65
|
+
class Cursor:
|
66
|
+
"""Create ANSI sequences to alter the cursor position."""
|
67
|
+
|
68
|
+
@staticmethod
|
69
|
+
def UP(count: int = 1) -> str: # noqa: N802
|
70
|
+
"""Move the cursor up a specified amount of lines (Defaults to 1)."""
|
71
|
+
return f"{CSI}{count}A"
|
72
|
+
|
73
|
+
@staticmethod
|
74
|
+
def DOWN(count: int = 1) -> str: # noqa: N802
|
75
|
+
"""Move the cursor down a specified amount of lines (Defaults to 1)."""
|
76
|
+
return f"{CSI}{count}B"
|
77
|
+
|
78
|
+
@staticmethod
|
79
|
+
def FORWARD(count: int = 1) -> str: # noqa: N802
|
80
|
+
"""Move the cursor forward a specified amount of lines (Defaults to 1)."""
|
81
|
+
return f"{CSI}{count}C"
|
82
|
+
|
83
|
+
@staticmethod
|
84
|
+
def BACK(count: int = 1) -> str: # noqa: N802
|
85
|
+
"""Move the cursor back a specified amount of lines (Defaults to 1)."""
|
86
|
+
return f"{CSI}{count}D"
|
87
|
+
|
88
|
+
@staticmethod
|
89
|
+
def SET_POS(x: int, y: int) -> str: # noqa: N802
|
90
|
+
"""Set the cursor position to coordinates which are 1-based."""
|
91
|
+
return f"{CSI}{y};{x}H"
|
92
|
+
|
93
|
+
|
94
|
+
def async_alert_str(*, terminal_columns: int, prompt: str, line: str, cursor_offset: int, alert_msg: str) -> str:
|
95
|
+
"""Calculate the desired string, including ANSI escape codes, for displaying an asynchronous alert message.
|
96
|
+
|
97
|
+
:param terminal_columns: terminal width (number of columns)
|
98
|
+
:param prompt: current onscreen prompt
|
99
|
+
:param line: current contents of the Readline line buffer
|
100
|
+
:param cursor_offset: the offset of the current cursor position within line
|
101
|
+
:param alert_msg: the message to display to the user
|
102
|
+
:return: the correct string so that the alert message appears to the user to be printed above the current line.
|
103
|
+
"""
|
104
|
+
# Split the prompt lines since it can contain newline characters.
|
105
|
+
prompt_lines = prompt.splitlines() or ['']
|
106
|
+
|
107
|
+
# Calculate how many terminal lines are taken up by all prompt lines except for the last one.
|
108
|
+
# That will be included in the input lines calculations since that is where the cursor is.
|
109
|
+
num_prompt_terminal_lines = 0
|
110
|
+
for prompt_line in prompt_lines[:-1]:
|
111
|
+
prompt_line_width = su.str_width(prompt_line)
|
112
|
+
num_prompt_terminal_lines += int(prompt_line_width / terminal_columns) + 1
|
113
|
+
|
114
|
+
# Now calculate how many terminal lines are take up by the input
|
115
|
+
last_prompt_line = prompt_lines[-1]
|
116
|
+
last_prompt_line_width = su.str_width(last_prompt_line)
|
117
|
+
|
118
|
+
input_width = last_prompt_line_width + su.str_width(line)
|
119
|
+
|
120
|
+
num_input_terminal_lines = int(input_width / terminal_columns) + 1
|
121
|
+
|
122
|
+
# Get the cursor's offset from the beginning of the first input line
|
123
|
+
cursor_input_offset = last_prompt_line_width + cursor_offset
|
124
|
+
|
125
|
+
# Calculate what input line the cursor is on
|
126
|
+
cursor_input_line = int(cursor_input_offset / terminal_columns) + 1
|
127
|
+
|
128
|
+
# Create a string that when printed will clear all input lines and display the alert
|
129
|
+
terminal_str = ''
|
130
|
+
|
131
|
+
# Move the cursor down to the last input line
|
132
|
+
if cursor_input_line != num_input_terminal_lines:
|
133
|
+
terminal_str += Cursor.DOWN(num_input_terminal_lines - cursor_input_line)
|
134
|
+
|
135
|
+
# Clear each line from the bottom up so that the cursor ends up on the first prompt line
|
136
|
+
total_lines = num_prompt_terminal_lines + num_input_terminal_lines
|
137
|
+
terminal_str += (clear_line_str() + Cursor.UP(1)) * (total_lines - 1)
|
138
|
+
|
139
|
+
# Clear the first prompt line
|
140
|
+
terminal_str += clear_line_str()
|
141
|
+
|
142
|
+
# Move the cursor to the beginning of the first prompt line and print the alert
|
143
|
+
terminal_str += '\r' + alert_msg
|
144
|
+
return terminal_str
|
cmd2/transcript.py
CHANGED
@@ -18,10 +18,8 @@ from typing import (
|
|
18
18
|
cast,
|
19
19
|
)
|
20
20
|
|
21
|
-
from . import
|
22
|
-
|
23
|
-
utils,
|
24
|
-
)
|
21
|
+
from . import string_utils as su
|
22
|
+
from . import utils
|
25
23
|
|
26
24
|
if TYPE_CHECKING: # pragma: no cover
|
27
25
|
from cmd2 import (
|
@@ -36,7 +34,7 @@ class Cmd2TestCase(unittest.TestCase):
|
|
36
34
|
that will execute the commands in a transcript file and expect the
|
37
35
|
results shown.
|
38
36
|
|
39
|
-
See
|
37
|
+
See transcript_example.py
|
40
38
|
"""
|
41
39
|
|
42
40
|
cmdapp: Optional['Cmd'] = None
|
@@ -76,13 +74,13 @@ class Cmd2TestCase(unittest.TestCase):
|
|
76
74
|
|
77
75
|
line_num = 0
|
78
76
|
finished = False
|
79
|
-
line =
|
77
|
+
line = su.strip_style(next(transcript))
|
80
78
|
line_num += 1
|
81
79
|
while not finished:
|
82
80
|
# Scroll forward to where actual commands begin
|
83
81
|
while not line.startswith(self.cmdapp.visible_prompt):
|
84
82
|
try:
|
85
|
-
line =
|
83
|
+
line = su.strip_style(next(transcript))
|
86
84
|
except StopIteration:
|
87
85
|
finished = True
|
88
86
|
break
|
@@ -108,14 +106,14 @@ class Cmd2TestCase(unittest.TestCase):
|
|
108
106
|
result = self.cmdapp.stdout.read()
|
109
107
|
stop_msg = 'Command indicated application should quit, but more commands in transcript'
|
110
108
|
# Read the expected result from transcript
|
111
|
-
if
|
109
|
+
if su.strip_style(line).startswith(self.cmdapp.visible_prompt):
|
112
110
|
message = f'\nFile {fname}, line {line_num}\nCommand was:\n{command}\nExpected: (nothing)\nGot:\n{result}\n'
|
113
111
|
assert not result.strip(), message # noqa: S101
|
114
112
|
# If the command signaled the application to quit there should be no more commands
|
115
113
|
assert not stop, stop_msg # noqa: S101
|
116
114
|
continue
|
117
115
|
expected_parts = []
|
118
|
-
while not
|
116
|
+
while not su.strip_style(line).startswith(self.cmdapp.visible_prompt):
|
119
117
|
expected_parts.append(line)
|
120
118
|
try:
|
121
119
|
line = next(transcript)
|