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/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
- ansi,
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 example.py
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 = ansi.strip_style(next(transcript))
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 = ansi.strip_style(next(transcript))
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 ansi.strip_style(line).startswith(self.cmdapp.visible_prompt):
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 ansi.strip_style(line).startswith(self.cmdapp.visible_prompt):
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)