diff --git a/cmd2/__init__.py b/cmd2/__init__.py index 80d686af..09962e79 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -1,4 +1,4 @@ -"""This simply imports certain things for backwards compatibility.""" +"""Import certain things for backwards compatibility.""" import argparse import contextlib diff --git a/cmd2/ansi.py b/cmd2/ansi.py index 22497f4e..b9c3d338 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -1,5 +1,6 @@ -"""Support for ANSI escape sequences which are used for things like applying style to text, -setting the window title, and asynchronous alerts. +"""Support for ANSI escape sequences. + +These are used for things like applying style to text, setting the window title, and asynchronous alerts. """ import functools @@ -92,6 +93,7 @@ def strip_style(text: str) -> str: def style_aware_wcswidth(text: str) -> int: """Wrap wcswidth to make it compatible with strings that contain ANSI style sequences. + This is intended for single line strings. If text contains a newline, this function will return -1. For multiline strings, call widest_line() instead. @@ -105,8 +107,10 @@ def style_aware_wcswidth(text: str) -> int: def widest_line(text: str) -> int: - """Return the width of the widest line in a multiline string. This wraps style_aware_wcswidth() - so it handles ANSI style sequences and has the same restrictions on non-printable characters. + """Return the width of the widest line in a multiline string. + + This wraps style_aware_wcswidth() so it handles ANSI style sequences and has the same + restrictions on non-printable characters. :param text: the string being measured :return: The width of the string when printed to the terminal if no errors occur. @@ -186,14 +190,16 @@ class AnsiSequence: """Base class to create ANSI sequence strings.""" def __add__(self, other: Any) -> str: - """Support building an ANSI sequence string when self is the left operand - e.g. Fg.LIGHT_MAGENTA + "hello". + """Support building an ANSI sequence string when self is the left operand. + + e.g. Fg.LIGHT_MAGENTA + "hello" """ return str(self) + str(other) def __radd__(self, other: Any) -> str: - """Support building an ANSI sequence string when self is the right operand - e.g. "hello" + Fg.RESET. + """Support building an ANSI sequence string when self is the right operand. + + e.g. "hello" + Fg.RESET """ return str(other) + str(self) @@ -262,7 +268,8 @@ class TextStyle(AnsiSequence, Enum): UNDERLINE_DISABLE = 24 def __str__(self) -> str: - """Return ANSI text style sequence instead of enum name + """Return ANSI text style sequence instead of enum name. + This is helpful when using a TextStyle in an f-string or format() call e.g. my_str = f"{TextStyle.UNDERLINE_ENABLE}hello{TextStyle.UNDERLINE_DISABLE}". """ @@ -271,6 +278,7 @@ def __str__(self) -> str: class Fg(FgColor, Enum): """Create ANSI sequences for the 16 standard terminal foreground text colors. + A terminal's color settings affect how these colors appear. To reset any foreground color, use Fg.RESET. """ @@ -295,7 +303,8 @@ class Fg(FgColor, Enum): RESET = 39 def __str__(self) -> str: - """Return ANSI color sequence instead of enum name + """Return ANSI color sequence instead of enum name. + This is helpful when using an Fg in an f-string or format() call e.g. my_str = f"{Fg.BLUE}hello{Fg.RESET}". """ @@ -304,6 +313,7 @@ def __str__(self) -> str: class Bg(BgColor, Enum): """Create ANSI sequences for the 16 standard terminal background text colors. + A terminal's color settings affect how these colors appear. To reset any background color, use Bg.RESET. """ @@ -328,7 +338,8 @@ class Bg(BgColor, Enum): RESET = 49 def __str__(self) -> str: - """Return ANSI color sequence instead of enum name + """Return ANSI color sequence instead of enum name. + This is helpful when using a Bg in an f-string or format() call e.g. my_str = f"{Bg.BLACK}hello{Bg.RESET}". """ @@ -337,6 +348,7 @@ def __str__(self) -> str: class EightBitFg(FgColor, Enum): """Create ANSI sequences for 8-bit terminal foreground text colors. Most terminals support 8-bit/256-color mode. + The first 16 colors correspond to the 16 colors from Fg and behave the same way. To reset any foreground color, including 8-bit, use Fg.RESET. """ @@ -599,7 +611,8 @@ class EightBitFg(FgColor, Enum): GRAY_93 = 255 def __str__(self) -> str: - """Return ANSI color sequence instead of enum name + """Return ANSI color sequence instead of enum name. + This is helpful when using an EightBitFg in an f-string or format() call e.g. my_str = f"{EightBitFg.SLATE_BLUE_1}hello{Fg.RESET}". """ @@ -608,6 +621,7 @@ def __str__(self) -> str: class EightBitBg(BgColor, Enum): """Create ANSI sequences for 8-bit terminal background text colors. Most terminals support 8-bit/256-color mode. + The first 16 colors correspond to the 16 colors from Bg and behave the same way. To reset any background color, including 8-bit, use Bg.RESET. """ @@ -870,7 +884,8 @@ class EightBitBg(BgColor, Enum): GRAY_93 = 255 def __str__(self) -> str: - """Return ANSI color sequence instead of enum name + """Return ANSI color sequence instead of enum name. + This is helpful when using an EightBitBg in an f-string or format() call e.g. my_str = f"{EightBitBg.KHAKI_3}hello{Bg.RESET}". """ @@ -879,6 +894,7 @@ def __str__(self) -> str: class RgbFg(FgColor): """Create ANSI sequences for 24-bit (RGB) terminal foreground text colors. The terminal must support 24-bit/true-color. + To reset any foreground color, including 24-bit, use Fg.RESET. """ @@ -896,7 +912,8 @@ def __init__(self, r: int, g: int, b: int) -> None: self._sequence = f"{CSI}38;2;{r};{g};{b}m" def __str__(self) -> str: - """Return ANSI color sequence instead of enum name + """Return ANSI color sequence instead of enum name. + This is helpful when using an RgbFg in an f-string or format() call e.g. my_str = f"{RgbFg(0, 55, 100)}hello{Fg.RESET}". """ @@ -905,6 +922,7 @@ def __str__(self) -> str: class RgbBg(BgColor): """Create ANSI sequences for 24-bit (RGB) terminal background text colors. The terminal must support 24-bit/true-color. + To reset any background color, including 24-bit, use Bg.RESET. """ @@ -922,7 +940,8 @@ def __init__(self, r: int, g: int, b: int) -> None: self._sequence = f"{CSI}48;2;{r};{g};{b}m" def __str__(self) -> str: - """Return ANSI color sequence instead of enum name + """Return ANSI color sequence instead of enum name. + This is helpful when using an RgbBg in an f-string or format() call e.g. my_str = f"{RgbBg(100, 255, 27)}hello{Bg.RESET}". """ @@ -942,6 +961,7 @@ def style( underline: Optional[bool] = None, ) -> str: """Apply ANSI colors and/or styles to a string and return it. + The styling is self contained which means that at the end of the string reset code(s) are issued to undo whatever styling was done at the beginning. diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 9bbaeed4..44f64ee1 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -1,4 +1,5 @@ -"""This module defines the ArgparseCompleter class which provides argparse-based tab completion to cmd2 apps. +"""Module efines the ArgparseCompleter class which provides argparse-based tab completion to cmd2 apps. + See the header of argparse_custom.py for instructions on how to use these features. """ @@ -70,13 +71,16 @@ def _build_hint(parser: argparse.ArgumentParser, arg_action: argparse.Action) -> def _single_prefix_char(token: str, parser: argparse.ArgumentParser) -> bool: - """Returns if a token is just a single flag prefix character.""" + """Is a token just a single flag prefix character.""" return len(token) == 1 and token[0] in parser.prefix_chars def _looks_like_flag(token: str, parser: argparse.ArgumentParser) -> bool: - """Determine if a token looks like a flag. Unless an argument has nargs set to argparse.REMAINDER, - then anything that looks like a flag can't be consumed as a value for it. + """Determine if a token looks like a flag. + + Unless an argument has nargs set to argparse.REMAINDER, then anything that looks like a flag + can't be consumed as a value for it. + Based on argparse._parse_optional(). """ # Flags have to be at least characters @@ -131,7 +135,8 @@ def __init__(self, arg_action: argparse.Action) -> None: class _UnfinishedFlagError(CompletionError): def __init__(self, flag_arg_state: _ArgumentState) -> None: - """CompletionError which occurs when the user has not finished the current flag + """CompletionError which occurs when the user has not finished the current flag. + :param flag_arg_state: information about the unfinished flag action. """ arg = f'{argparse._get_action_name(flag_arg_state.action)}' @@ -142,8 +147,10 @@ def __init__(self, flag_arg_state: _ArgumentState) -> None: class _NoResultsError(CompletionError): def __init__(self, parser: argparse.ArgumentParser, arg_action: argparse.Action) -> None: - """CompletionError which occurs when there are no results. If hinting is allowed, then its message will - be a hint about the argument being tab completed. + """CompletionError which occurs when there are no results. + + If hinting is allowed, then its message will be a hint about the argument being tab completed. + :param parser: ArgumentParser instance which owns the action being tab completed :param arg_action: action being tab completed. """ @@ -241,8 +248,10 @@ def consume_argument(arg_state: _ArgumentState) -> None: consumed_arg_values[arg_state.action.dest].append(token) def update_mutex_groups(arg_action: argparse.Action) -> None: - """Check if an argument belongs to a mutually exclusive group and either mark that group - as complete or print an error if the group has already been completed + """Check if an argument belongs to a mutually exclusive group potenitally mark that group complete. + + Either mark the group as complete or print an error if the group has already been completed. + :param arg_action: the action of the argument :raises CompletionError: if the group is already completed. """ @@ -590,7 +599,8 @@ def _format_completions(self, arg_state: _ArgumentState, completions: Union[list return cast(list[str], completions) def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: list[str]) -> list[str]: - """Supports cmd2's help command in the completion of subcommand names + """Supports cmd2's help command in the completion of subcommand names. + :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed :param begidx: the beginning index of the prefix text @@ -615,7 +625,8 @@ def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: in return [] def format_help(self, tokens: list[str]) -> str: - """Supports cmd2's help command in the retrieval of help text + """Supports cmd2's help command in the retrieval of help text. + :param tokens: arguments passed to help command :return: help text of the command being queried. """ @@ -643,7 +654,8 @@ def _complete_arg( *, cmd_set: Optional[CommandSet] = None, ) -> list[str]: - """Tab completion routine for an argparse argument + """Tab completion routine for an argparse argument. + :return: list of completions :raises CompletionError: if the completer or choices function this calls raises one. """ diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 95c06bfd..58bc0176 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -1,4 +1,5 @@ -"""This module adds capabilities to argparse by patching a few of its functions. +"""Module adds capabilities to argparse by patching a few of its functions. + It also defines a parser class called Cmd2ArgumentParser which improves error and help output over normal argparse. All cmd2 code uses this parser and it is recommended that developers of cmd2-based apps either use it or write their own @@ -340,9 +341,7 @@ def __call__( @runtime_checkable class CompleterFuncWithTokens(Protocol): - """Function to support tab completion with the provided state of the user prompt and accepts a dictionary of prior - arguments. - """ + """Function to support tab completion with the provided state of the user prompt, accepts a dictionary of prior args.""" def __call__( self, @@ -360,6 +359,7 @@ def __call__( class ChoicesCallable: """Enables using a callable as the choices provider for an argparse argument. + While argparse has the built-in choices attribute, it is limited to an iterable. """ @@ -368,7 +368,8 @@ def __init__( is_completer: bool, to_call: Union[CompleterFunc, ChoicesProviderFunc], ) -> None: - """Initializer + """Initialize the ChoiceCallable instance. + :param is_completer: True if to_call is a tab completion routine which expects the args: text, line, begidx, endidx :param to_call: the callable object that will be called to provide choices for the argument. @@ -621,7 +622,7 @@ def _action_set_suppress_tab_hint(self: argparse.Action, suppress_tab_hint: bool def register_argparse_argument_parameter(param_name: str, param_type: Optional[type[Any]]) -> None: - """Registers a custom argparse argument parameter. + """Register a custom argparse argument parameter. The registered name will then be a recognized keyword parameter to the parser's `add_argument()` function. @@ -693,7 +694,7 @@ def _add_argument_wrapper( descriptive_header: Optional[str] = None, **kwargs: Any, ) -> argparse.Action: - """Wrapper around _ActionsContainer.add_argument() which supports more settings used by cmd2. + """Wrap ActionsContainer.add_argument() which supports more settings used by cmd2. # Args from original function :param self: instance of the _ActionsContainer being added to @@ -913,7 +914,8 @@ def _ArgumentParser_set_ap_completer_type(self: argparse.ArgumentParser, ap_comp # Patch ArgumentParser._check_value to support CompletionItems as choices ############################################################################################################ def _ArgumentParser_check_value(_self: argparse.ArgumentParser, action: argparse.Action, value: Any) -> None: # noqa: N802 - """Custom override of ArgumentParser._check_value that supports CompletionItems as choices. + """Check_value that supports CompletionItems as choices (Custom override of ArgumentParser._check_value). + When evaluating choices, input is compared to CompletionItem.orig_value instead of the CompletionItem instance. @@ -945,7 +947,7 @@ def _ArgumentParser_check_value(_self: argparse.ArgumentParser, action: argparse def _SubParsersAction_remove_parser(self: argparse._SubParsersAction, name: str) -> None: # type: ignore[type-arg] # noqa: N802 - """Removes a sub-parser from a sub-parsers group. Used to remove subcommands from a parser. + """Remove a sub-parser from a sub-parsers group. Used to remove subcommands from a parser. This function is added by cmd2 as a method called ``remove_parser()`` to ``argparse._SubParsersAction`` class. @@ -1126,7 +1128,7 @@ def _determine_metavar( action: argparse.Action, default_metavar: Union[str, tuple[str, ...]], ) -> Union[str, tuple[str, ...]]: - """Custom method to determine what to use as the metavar value of an action.""" + """Determine what to use as the metavar value of an action.""" if action.metavar is not None: result = action.metavar elif action.choices is not None: @@ -1153,7 +1155,7 @@ def format_tuple(tuple_size: int) -> tuple[str, ...]: return format_tuple def _format_args(self, action: argparse.Action, default_metavar: Union[str, tuple[str, ...]]) -> str: - """Customized to handle ranged nargs and make other output less verbose.""" + """Handle ranged nargs and make other output less verbose.""" metavar = self._determine_metavar(action, default_metavar) metavar_formatter = self._metavar_formatter(action, default_metavar) @@ -1200,7 +1202,7 @@ def __init__( *, ap_completer_type: Optional[type['ArgparseCompleter']] = None, ) -> None: - """# Custom parameter added by cmd2. + """Initialize the Cmd2ArgumentParser instance, a custom ArgumentParser added by cmd2. :param ap_completer_type: optional parameter which specifies a subclass of ArgparseCompleter for custom tab completion behavior on this parser. If this is None or not present, then cmd2 will use @@ -1221,9 +1223,9 @@ def __init__( conflict_handler=conflict_handler, add_help=add_help, allow_abbrev=allow_abbrev, - exit_on_error=exit_on_error, - suggest_on_error=suggest_on_error, - color=color, + exit_on_error=exit_on_error, # added in Python 3.9 + suggest_on_error=suggest_on_error, # added in Python 3.14 + color=color, # added in Python 3.14 ) else: # Python < 3.14, so don't pass new arguments to parent argparse.ArgumentParser class @@ -1240,13 +1242,15 @@ def __init__( conflict_handler=conflict_handler, add_help=add_help, allow_abbrev=allow_abbrev, - exit_on_error=exit_on_error, + exit_on_error=exit_on_error, # added in Python 3.9 ) self.set_ap_completer_type(ap_completer_type) # type: ignore[attr-defined] def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction: # type: ignore[type-arg] - """Custom override. Sets a default title if one was not given. + """Add a subcommand parser. + + Set a default title if one was not given.f :param kwargs: additional keyword arguments :return: argparse Subparser Action @@ -1257,7 +1261,10 @@ def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction: # type: return super().add_subparsers(**kwargs) def error(self, message: str) -> NoReturn: - """Custom override that applies custom formatting to the error message.""" + """Print a usage message, including the message, to sys.stderr and terminates the program with a status code of 2. + + Custom override that applies custom formatting to the error message. + """ lines = message.split('\n') formatted_message = '' for linum, line in enumerate(lines): @@ -1271,7 +1278,10 @@ def error(self, message: str) -> NoReturn: self.exit(2, f'{formatted_message}\n\n') def format_help(self) -> str: - """Copy of format_help() from argparse.ArgumentParser with tweaks to separately display required parameters.""" + """Return a string containing a help message, including the program usage and information about the arguments. + + Copy of format_help() from argparse.ArgumentParser with tweaks to separately display required parameters. + """ formatter = self._get_formatter() # usage @@ -1334,6 +1344,7 @@ def _print_message(self, message: str, file: Optional[IO[str]] = None) -> None: class Cmd2AttributeWrapper: """Wraps a cmd2-specific attribute added to an argparse Namespace. + This makes it easy to know which attributes in a Namespace are arguments from a parser and which were added by cmd2. """ @@ -1355,8 +1366,10 @@ def set(self, new_val: Any) -> None: def set_default_argument_parser_type(parser_type: type[argparse.ArgumentParser]) -> None: - """Set the default ArgumentParser class for a cmd2 app. This must be called prior to loading cmd2.py if - you want to override the parser for cmd2's built-in commands. See examples/override_parser.py. + """Set the default ArgumentParser class for a cmd2 app. + + This must be called prior to loading cmd2.py if you want to override the parser for cmd2's built-in commands. + See examples/override_parser.py. """ global DEFAULT_ARGUMENT_PARSER # noqa: PLW0603 DEFAULT_ARGUMENT_PARSER = parser_type diff --git a/cmd2/clipboard.py b/cmd2/clipboard.py index 9b6cfc77..284d57df 100644 --- a/cmd2/clipboard.py +++ b/cmd2/clipboard.py @@ -1,4 +1,4 @@ -"""This module provides basic ability to copy from and paste to the clipboard/pastebuffer.""" +"""Module provides basic ability to copy from and paste to the clipboard/pastebuffer.""" import typing diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index f41e3912..f4f00d68 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -324,8 +324,7 @@ def __init__( allow_clipboard: bool = True, suggest_similar_command: bool = False, ) -> None: - """An easy but powerful framework for writing line-oriented command - interpreters. Extends Python's cmd package. + """Easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package. :param completekey: readline name of a completion key, default to Tab :param stdin: alternate input file object, if not specified, sys.stdin is used @@ -633,11 +632,12 @@ def __init__( def find_commandsets(self, commandset_type: type[CommandSet], *, subclass_match: bool = False) -> list[CommandSet]: """Find all CommandSets that match the provided CommandSet type. + By default, locates a CommandSet that is an exact type match but may optionally return all CommandSets that are sub-classes of the provided type :param commandset_type: CommandSet sub-class type to search for :param subclass_match: If True, return all sub-classes of provided type, otherwise only search for exact match - :return: Matching CommandSets. + :return: Matching CommandSets """ return [ cmdset @@ -646,9 +646,10 @@ def find_commandsets(self, commandset_type: type[CommandSet], *, subclass_match: ] def find_commandset_for_command(self, command_name: str) -> Optional[CommandSet]: - """Finds the CommandSet that registered the command name + """Find the CommandSet that registered the command name. + :param command_name: command name to search - :return: CommandSet that provided the command. + :return: CommandSet that provided the command """ return self._cmd_to_command_sets.get(command_name) @@ -1102,7 +1103,7 @@ def add_settable(self, settable: Settable) -> None: self._settables[settable.name] = settable def remove_settable(self, name: str) -> None: - """Convenience method for removing a settable parameter from ``self.settables``. + """Remove a settable parameter from ``self.settables``. :param name: name of the settable being removed :raises KeyError: if the Settable matches this name @@ -1116,11 +1117,11 @@ def build_settables(self) -> None: """Create the dictionary of user-settable parameters.""" def get_allow_style_choices(_cli_self: Cmd) -> list[str]: - """Used to tab complete allow_style values.""" + """Tab complete allow_style values.""" return [val.name.lower() for val in ansi.AllowStyle] def allow_style_type(value: str) -> ansi.AllowStyle: - """Converts a string value into an ansi.AllowStyle.""" + """Convert a string value into an ansi.AllowStyle.""" try: return ansi.AllowStyle[value.upper()] except KeyError as esc: @@ -1227,7 +1228,7 @@ def perror(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> self.print_to(sys.stderr, msg, end=end, style=ansi.style_error if apply_style else None) def psuccess(self, msg: Any = '', *, end: str = '\n') -> None: - """Wraps poutput, but applies ansi.style_success by default. + """Wrap poutput, but applies ansi.style_success by default. :param msg: object to print :param end: string appended after the end of the message, default a newline @@ -1236,7 +1237,7 @@ def psuccess(self, msg: Any = '', *, end: str = '\n') -> None: self.poutput(msg, end=end) def pwarning(self, msg: Any = '', *, end: str = '\n') -> None: - """Wraps perror, but applies ansi.style_warning by default. + """Wrap perror, but applies ansi.style_warning by default. :param msg: object to print :param end: string appended after the end of the message, default a newline @@ -1272,7 +1273,8 @@ def pexcept(self, msg: Any, *, end: str = '\n', apply_style: bool = True) -> Non self.perror(final_msg, end=end, apply_style=False) def pfeedback(self, msg: Any, *, end: str = '\n') -> None: - """For printing nonessential feedback. Can be silenced with `quiet`. + """Print nonessential feedback. Can be silenced with `quiet`. + Inclusion in redirected output is controlled by `feedback_to_output`. :param msg: object to print @@ -1357,7 +1359,7 @@ def _reset_completion_defaults(self) -> None: readline.rl.mode._display_completions = self._display_matches_pyreadline def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> tuple[list[str], list[str]]: - """Used by tab completion functions to get all tokens through the one being completed. + """Get all tokens through the one being completed, used by tab completion functions. :param line: the current input line with leading whitespace removed :param begidx: the beginning index of the prefix text @@ -1428,8 +1430,9 @@ def basic_complete( endidx: int, # noqa: ARG002 match_against: Iterable[str], ) -> list[str]: - """Basic tab completion function that matches against a list of strings without considering line contents - or cursor position. The args required by this function are defined in the header of Python's cmd.py. + """Tab completion function that matches against a list of strings without considering line contents or cursor position. + + The args required by this function are defined in the header of Python's cmd.py. :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed @@ -1449,8 +1452,9 @@ def delimiter_complete( match_against: Iterable[str], delimiter: str, ) -> list[str]: - """Performs tab completion against a list but each match is split on a delimiter and only - the portion of the match being tab completed is shown as the completion suggestions. + """Perform tab completion against a list but each match is split on a delimiter. + + Only the portion of the match being tab completed is shown as the completion suggestions. This is useful if you match against strings that are hierarchical in nature and have a common delimiter. @@ -1614,7 +1618,7 @@ def path_complete( *, path_filter: Optional[Callable[[str], bool]] = None, ) -> list[str]: - """Performs completion of local file system paths. + """Perform completion of local file system paths. :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed @@ -1751,7 +1755,7 @@ def complete_users() -> list[str]: return matches def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *, complete_blank: bool = False) -> list[str]: - """Performs completion of executables either in a user's path or a given path. + """Perform completion of executables either in a user's path or a given path. :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed @@ -1775,7 +1779,8 @@ def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *, ) def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, compfunc: CompleterFunc) -> list[str]: - """Called by complete() as the first tab completion function for all commands + """First tab completion function for all commands, called by complete(). + It determines if it should tab complete for redirection (|, >, >>) or use the completer function for the current command. @@ -1856,7 +1861,8 @@ def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, com @staticmethod def _pad_matches_to_display(matches_to_display: list[str]) -> tuple[list[str], int]: # pragma: no cover - """Adds padding to the matches being displayed as tab completion suggestions. + """Add padding to the matches being displayed as tab completion suggestions. + The default padding of readline/pyreadine is small and not visually appealing especially if matches have spaces. It appears very squished together. @@ -1879,7 +1885,7 @@ def _pad_matches_to_display(matches_to_display: list[str]) -> tuple[list[str], i def _display_matches_gnu_readline( self, substitution: str, matches: list[str], longest_match_length: int ) -> None: # pragma: no cover - """Prints a match list using GNU readline's rl_display_match_list(). + """Print a match list using GNU readline's rl_display_match_list(). :param substitution: the substitution written to the command line :param matches: the tab completion matches to display @@ -1938,7 +1944,7 @@ def _display_matches_gnu_readline( rl_force_redisplay() def _display_matches_pyreadline(self, matches: list[str]) -> None: # pragma: no cover - """Prints a match list using pyreadline3's _display_completions(). + """Print a match list using pyreadline3's _display_completions(). :param matches: the tab completion matches to display """ @@ -1971,8 +1977,9 @@ def _display_matches_pyreadline(self, matches: list[str]) -> None: # pragma: no @staticmethod def _determine_ap_completer_type(parser: argparse.ArgumentParser) -> type[argparse_completer.ArgparseCompleter]: - """Determine what type of ArgparseCompleter to use on a given parser. If the parser does not have one - set, then use argparse_completer.DEFAULT_AP_COMPLETER. + """Determine what type of ArgparseCompleter to use on a given parser. + + If the parser does not have one set, then use argparse_completer.DEFAULT_AP_COMPLETER. :param parser: the parser to examine :return: type of ArgparseCompleter @@ -1987,7 +1994,7 @@ def _determine_ap_completer_type(parser: argparse.ArgumentParser) -> type[argpar def _perform_completion( self, text: str, line: str, begidx: int, endidx: int, custom_settings: Optional[utils.CustomCompletionSettings] = None ) -> None: - """Helper function for complete() that performs the actual completion. + """Perform the actual completion, helper function for complete(). :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed @@ -2395,7 +2402,7 @@ def termination_signal_handler(self, signum: int, _: Optional[FrameType]) -> Non sys.exit(128 + signum) def _raise_keyboard_interrupt(self) -> None: - """Helper function to raise a KeyboardInterrupt.""" + """Raise a KeyboardInterrupt.""" raise KeyboardInterrupt("Got a keyboard interrupt") def precmd(self, statement: Union[Statement, str]) -> Statement: @@ -2610,7 +2617,8 @@ def runcmds_plus_hooks( add_to_history: bool = True, stop_on_keyboard_interrupt: bool = False, ) -> bool: - """Used when commands are being run in an automated fashion like text scripts or history replays. + """Run commands in an automated fashion from sources like text scripts or history replays. + The prompt and command line for each command will be printed if echo is True. :param cmds: commands to run @@ -2934,7 +2942,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: return redir_saved_state def _restore_output(self, statement: Statement, saved_redir_state: utils.RedirectionSavedState) -> None: - """Handles restoring state after output redirection. + """Handle restoring state after output redirection. :param statement: Statement object which contains the parsed input from the user :param saved_redir_state: contains information needed to restore state data @@ -2979,7 +2987,7 @@ def cmd_func(self, command: str) -> Optional[CommandFunc]: return cast(CommandFunc, func) if callable(func) else None def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = True) -> bool: - """This executes the actual do_* method for a command. + """Execute the actual do_* method for a command. If the command provided doesn't exist, then it executes default() instead. @@ -3014,7 +3022,7 @@ def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = Tru return stop if stop is not None else False def default(self, statement: Statement) -> Optional[bool]: # type: ignore[override] - """Executed when the command given isn't a recognized command implemented by a do_* method. + """Execute when the command given isn't a recognized command implemented by a do_* method. :param statement: Statement object with parsed input """ @@ -3046,8 +3054,9 @@ def read_input( completer: Optional[CompleterFunc] = None, parser: Optional[argparse.ArgumentParser] = None, ) -> str: - """Read input from appropriate stdin value. Also supports tab completion and up-arrow history while - input is being entered. + """Read input from appropriate stdin value. + + Also supports tab completion and up-arrow history while input is being entered. :param prompt: prompt to display to user :param history: optional list of strings to use for up-arrow history. If completion_mode is @@ -3212,7 +3221,7 @@ def _read_command_line(self, prompt: str) -> str: self.terminal_lock.acquire() def _set_up_cmd2_readline(self) -> _SavedReadlineSettings: - """Called at beginning of command loop to set up readline with cmd2-specific settings. + """Set up readline with cmd2-specific settings, called at beginning of command loop. :return: Class containing saved readline settings """ @@ -3252,7 +3261,7 @@ def _set_up_cmd2_readline(self) -> _SavedReadlineSettings: return readline_settings def _restore_readline(self, readline_settings: _SavedReadlineSettings) -> None: - """Called at end of command loop to restore saved readline settings. + """Restore saved readline settings, called at end of command loop. :param readline_settings: the readline settings to restore """ @@ -3268,8 +3277,9 @@ def _restore_readline(self, readline_settings: _SavedReadlineSettings) -> None: readline.rl.mode._display_completions = orig_pyreadline_display def _cmdloop(self) -> None: - """Repeatedly issue a prompt, accept input, parse an initial prefix - off the received input, and dispatch to action methods, passing them + """Repeatedly issue a prompt, accept input, parse it, and dispatch to apporpriate commands. + + Parse an initial prefix off the received input and dispatch to action methods, passing them the remainder of the line as argument. This serves the same role as cmd.cmdloop(). @@ -3784,7 +3794,8 @@ def do_help(self, args: argparse.Namespace) -> None: self.last_result = False def print_topics(self, header: str, cmds: Optional[list[str]], cmdlen: int, maxcol: int) -> None: # noqa: ARG002 - """Print groups of commands and topics in columns and an optional header + """Print groups of commands and topics in columns and an optional header. + Override of cmd's print_topics() to handle headers with newlines, ANSI style sequences, and wide characters. :param header: string to print above commands being printed @@ -3802,6 +3813,7 @@ def print_topics(self, header: str, cmds: Optional[list[str]], cmdlen: int, maxc def columnize(self, str_list: Optional[list[str]], display_width: int = 80) -> None: """Display a list of single-line strings as a compact set of columns. + Override of cmd's columnize() to handle strings with ANSI style sequences and wide characters. Each column is only as wide as necessary. @@ -3905,7 +3917,7 @@ def _build_command_info(self) -> tuple[dict[str, list[str]], list[str], list[str return cmds_cats, cmds_doc, cmds_undoc, help_topics def _print_topics(self, header: str, cmds: list[str], verbose: bool) -> None: - """Customized version of print_topics that can switch between verbose or traditional output.""" + """Print topics, switching between verbose or traditional output.""" import io if cmds: @@ -3989,7 +4001,8 @@ def do_shortcuts(self, _: argparse.Namespace) -> None: @with_argparser(eof_parser) def do_eof(self, _: argparse.Namespace) -> Optional[bool]: - """Called when Ctrl-D is pressed and calls quit with no arguments. + """Quit with no arguments, called when Ctrl-D is pressed. + This can be overridden if quit should be called differently. """ self.poutput() @@ -4007,8 +4020,9 @@ def do_quit(self, _: argparse.Namespace) -> Optional[bool]: return True def select(self, opts: Union[str, list[str], list[tuple[Any, Optional[str]]]], prompt: str = 'Your choice? ') -> Any: - """Presents a numbered menu to the user. Modeled after - the bash shell's SELECT. Returns the item chosen. + """Present a numbered menu to the user. + + Modeled after the bash shell's SELECT. Returns the item chosen. Argument ``opts`` can be: @@ -4233,7 +4247,8 @@ def do_shell(self, args: argparse.Namespace) -> None: @staticmethod def _reset_py_display() -> None: - """Resets the dynamic objects in the sys module that the py and ipy consoles fight over. + """Reset the dynamic objects in the sys module that the py and ipy consoles fight over. + When a Python console starts it adopts certain display settings if they've already been set. If an ipy console has previously been run, then py uses its settings and ends up looking like an ipy console in terms of prompt and exception text. This method forces the Python @@ -4253,7 +4268,8 @@ def _reset_py_display() -> None: sys.excepthook = sys.__excepthook__ def _set_up_py_shell_env(self, interp: InteractiveConsole) -> _SavedCmd2Env: - """Set up interactive Python shell environment + """Set up interactive Python shell environment. + :return: Class containing saved up cmd2 environment. """ cmd2_env = _SavedCmd2Env() @@ -4352,7 +4368,10 @@ def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None: sys.modules['readline'] = cmd2_env.readline_module def _run_python(self, *, pyscript: Optional[str] = None) -> Optional[bool]: - """Called by do_py() and do_run_pyscript(). + """Run an interactive Python shell or execute a pyscript file. + + Called by do_py() and do_run_pyscript(). + If pyscript is None, then this function runs an interactive Python shell. Otherwise, it runs the pyscript file. @@ -4363,7 +4382,7 @@ def _run_python(self, *, pyscript: Optional[str] = None) -> Optional[bool]: self.last_result = False def py_quit() -> None: - """Function callable from the interactive Python console to exit that environment.""" + """Exit an interactive Python environment, callable from the interactive Python console.""" raise EmbeddedConsoleExit from .py_bridge import ( @@ -4470,7 +4489,8 @@ def py_quit() -> None: @with_argparser(py_parser) def do_py(self, _: argparse.Namespace) -> Optional[bool]: - """Run an interactive Python shell + """Run an interactive Python shell. + :return: True if running of commands should stop. """ # self.last_resort will be set by _run_python() @@ -5114,7 +5134,7 @@ def do__relative_run_script(self, args: argparse.Namespace) -> Optional[bool]: return self.do_run_script(utils.quote_string(relative_path)) def _run_transcript_tests(self, transcript_paths: list[str]) -> None: - """Runs transcript tests for provided file(s). + """Run transcript tests for provided file(s). This is called when either -t is provided on the command line or the transcript_files argument is provided during construction of the cmd2.Cmd instance. @@ -5177,6 +5197,7 @@ class TestMyAppCase(Cmd2TestCase): def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover """Display an important message to the user while they are at a command line prompt. + To the user it appears as if an alert message is printed above the prompt and their current input text and cursor location is left alone. @@ -5418,7 +5439,7 @@ def _report_disabled_command_usage(self, *_args: Any, message_to_print: str, **_ self.perror(message_to_print, apply_style=False) def cmdloop(self, intro: Optional[str] = None) -> int: # type: ignore[override] - """This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2. + """Deal with extra features provided by cmd2, this is an outer wrapper around _cmdloop(). _cmdloop() provides the main loop equivalent to cmd.cmdloop(). This is a wrapper around that which deals with the following extra features provided by cmd2: @@ -5604,8 +5625,11 @@ def _resolve_func_self( cmd_support_func: Callable[..., Any], cmd_self: Union[CommandSet, 'Cmd', None], ) -> Optional[object]: - """Attempt to resolve a candidate instance to pass as 'self' for an unbound class method that was - used when defining command's argparse object. Since we restrict registration to only a single CommandSet + """Attempt to resolve a candidate instance to pass as 'self'. + + Used for an unbound class method that was used when defining command's argparse object. + + Since we restrict registration to only a single CommandSet instance of each type, using type is a reasonably safe way to resolve the correct object instance. :param cmd_support_func: command support function. This could be a completer or namespace provider diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py index 2d99bbc5..97f740b4 100644 --- a/cmd2/command_definition.py +++ b/cmd2/command_definition.py @@ -29,8 +29,7 @@ def with_default_category(category: str, *, heritable: bool = True) -> Callable[[CommandSetType], CommandSetType]: - """Decorator that applies a category to all ``do_*`` command methods in a class that do not already - have a category specified. + """Apply a category to all ``do_*`` command methods in a class that do not already have a category specified (Decorator). CommandSets that are decorated by this with `heritable` set to True (default) will set a class attribute that is inherited by all subclasses unless overridden. All commands of this CommandSet and all subclasses of this CommandSet @@ -112,9 +111,11 @@ def _cmd(self) -> 'cmd2.Cmd': return self.__cmd_internal def on_register(self, cmd: 'cmd2.Cmd') -> None: - """Called by cmd2.Cmd as the first step to registering a CommandSet. The commands defined in this class have - not been added to the CLI object at this point. Subclasses can override this to perform any initialization - requiring access to the Cmd object (e.g. configure commands and their parsers based on CLI state data). + """First step to registering a CommandSet, called by cmd2.Cmd. + + The commands defined in this class have not been added to the CLI object at this point. + Subclasses can override this to perform any initialization requiring access to the Cmd object + (e.g. configure commands and their parsers based on CLI state data). :param cmd: The cmd2 main application :raises CommandSetRegistrationError: if CommandSet is already registered. @@ -125,18 +126,21 @@ def on_register(self, cmd: 'cmd2.Cmd') -> None: raise CommandSetRegistrationError('This CommandSet has already been registered') def on_registered(self) -> None: - """Called by cmd2.Cmd after a CommandSet is registered and all its commands have been added to the CLI. + """2nd step to registering, called by cmd2.Cmd after a CommandSet is registered and all its commands have been added. + Subclasses can override this to perform custom steps related to the newly added commands (e.g. setting them to a disabled state). """ def on_unregister(self) -> None: - """Called by ``cmd2.Cmd`` as the first step to unregistering a CommandSet. Subclasses can override this to - perform any cleanup steps which require their commands being registered in the CLI. + """First step to unregistering a CommandSet, called by ``cmd2.Cmd``. + + Subclasses can override this to perform any cleanup steps which require their commands being registered in the CLI. """ def on_unregistered(self) -> None: - """Called by ``cmd2.Cmd`` after a CommandSet has been unregistered and all its commands removed from the CLI. + """2nd step to unregistering, called by ``cmd2.Cmd`` after a CommandSet is unregistered and all its commands removed. + Subclasses can override this to perform remaining cleanup steps. """ self.__cmd_internal = None @@ -150,7 +154,7 @@ def settables(self) -> Mapping[str, Settable]: return self._settables def add_settable(self, settable: Settable) -> None: - """Convenience method to add a settable parameter to the CommandSet. + """Add a settable parameter to the CommandSet. :param settable: Settable object being added """ @@ -165,7 +169,7 @@ def add_settable(self, settable: Settable) -> None: self._settables[settable.name] = settable def remove_settable(self, name: str) -> None: - """Convenience method for removing a settable parameter from the CommandSet. + """Remove a settable parameter from the CommandSet. :param name: name of the settable being removed :raises KeyError: if the Settable matches this name diff --git a/cmd2/constants.py b/cmd2/constants.py index 3306f6fc..c82b3ca1 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -1,4 +1,4 @@ -"""This module contains constants used throughout ``cmd2``.""" +"""Constants used throughout ``cmd2``.""" # Unless documented in https://cmd2.readthedocs.io/en/latest/api/index.html # nothing here should be considered part of the public API of this module diff --git a/cmd2/decorators.py b/cmd2/decorators.py index da5ae0de..3ab1c33b 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -74,8 +74,10 @@ def cat_decorator(func: CommandFunc) -> CommandFunc: # found we can swap out the statement with each decorator's specific parameters ########################## def _parse_positionals(args: tuple[Any, ...]) -> tuple['cmd2.Cmd', Union[Statement, str]]: - """Helper function for cmd2 decorators to inspect the positional arguments until the cmd2.Cmd argument is found + """Inspect the positional arguments until the cmd2.Cmd argument is found. + Assumes that we will find cmd2.Cmd followed by the command statement object or string. + :arg args: The positional arguments to inspect :return: The cmd2.Cmd reference and the command line statement. """ @@ -97,7 +99,7 @@ def _parse_positionals(args: tuple[Any, ...]) -> tuple['cmd2.Cmd', Union[Stateme def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) -> list[Any]: - """Helper function for cmd2 decorators to swap the Statement parameter with one or more decorator-specific parameters. + """Swap the Statement parameter with one or more decorator-specific parameters. :param args: The original positional arguments :param search_arg: The argument to search for (usually the Statement) @@ -136,8 +138,9 @@ def with_argument_list( RawCommandFuncOptionalBoolReturn[CommandParent], Callable[[ArgListCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]], ]: - """A decorator to alter the arguments passed to a ``do_*`` method. Default - passes a string of whatever the user typed. With this decorator, the + """A decorator to alter the arguments passed to a ``do_*`` method. + + Default passes a string of whatever the user typed. With this decorator, the decorated method will receive a list of arguments parsed from user input. :param func_arg: Single-element positional argument list containing ``doi_*`` method @@ -189,8 +192,9 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> Optional[bool]: def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: - """Recursively set prog attribute of a parser and all of its subparsers so that the root command - is a command name and not sys.argv[0]. + """Recursively set prog attribute of a parser and all of its subparsers. + + Does so that the root command is a command name and not sys.argv[0]. :param parser: the parser being edited :param prog: new value for the parser's prog attribute @@ -332,8 +336,7 @@ def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> RawCommandFuncOpt @functools.wraps(func) def cmd_wrapper(*args: Any, **kwargs: dict[str, Any]) -> Optional[bool]: - """Command function wrapper which translates command line into argparse Namespace and calls actual - command function. + """Command function wrapper which translates command line into argparse Namespace and call actual command function. :param args: All positional arguments to this function. We're expecting there to be: cmd2_app, statement: Union[Statement, str] diff --git a/cmd2/exceptions.py b/cmd2/exceptions.py index 0e9e9ce4..5d0cd190 100644 --- a/cmd2/exceptions.py +++ b/cmd2/exceptions.py @@ -8,13 +8,12 @@ class SkipPostcommandHooks(Exception): # noqa: N818 - """Custom exception class for when a command has a failure bad enough to skip post command - hooks, but not bad enough to print the exception to the user. - """ + """For when a command has a failed bad enough to skip post command hooks, but not bad enough to print to the user.""" class Cmd2ArgparseError(SkipPostcommandHooks): """A ``SkipPostcommandHooks`` exception for when a command fails to parse its arguments. + Normally argparse raises a SystemExit exception in these cases. To avoid stopping the command loop, catch the SystemExit and raise this instead. If you still need to run post command hooks after parsing fails, just return instead of raising an exception. @@ -22,17 +21,16 @@ class Cmd2ArgparseError(SkipPostcommandHooks): class CommandSetRegistrationError(Exception): - """Exception that can be thrown when an error occurs while a CommandSet is being added or removed - from a cmd2 application. - """ + """For when an error occurs while a CommandSet is being added or removed from a cmd2 application.""" class CompletionError(Exception): - """Raised during tab completion operations to report any sort of error you want printed. This can also be used - just to display a message, even if it's not an error. For instance, ArgparseCompleter raises CompletionErrors - to display tab completion hints and sets apply_style to False so hints aren't colored like error text. + """Raised during tab completion operations to report any sort of error you want printed. + + This can also be used just to display a message, even if it's not an error. For instance, ArgparseCompleter raises + CompletionErrors to display tab completion hints and sets apply_style to False so hints aren't colored like error text. - Example use cases + Example use cases: - Reading a database to retrieve a tab completion data set failed - A previous command line argument that determines the data set being completed is invalid @@ -40,7 +38,8 @@ class CompletionError(Exception): """ def __init__(self, *args: Any, apply_style: bool = True) -> None: - """Initializer for CompletionError + """Initialize CompletionError instance. + :param apply_style: If True, then ansi.style_error will be applied to the message text when printed. Set to False in cases where the message text already has the desired style. Defaults to True. @@ -52,11 +51,13 @@ def __init__(self, *args: Any, apply_style: bool = True) -> None: class PassThroughException(Exception): # noqa: N818 """Normally all unhandled exceptions raised during commands get printed to the user. + This class is used to wrap an exception that should be raised instead of printed. """ def __init__(self, *args: Any, wrapped_ex: BaseException) -> None: - """Initializer for PassThroughException + """Initialize PassThroughException instance. + :param wrapped_ex: the exception that will be raised. """ self.wrapped_ex = wrapped_ex diff --git a/cmd2/history.py b/cmd2/history.py index 05d79a40..621b9416 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -75,7 +75,7 @@ class HistoryItem: statement: Statement def __str__(self) -> str: - """A convenient human-readable representation of the history item.""" + """Human-readable representation of the history item.""" return self.statement.raw @property @@ -88,8 +88,7 @@ def raw(self) -> str: @property def expanded(self) -> str: - """Return the command as run which includes shortcuts and aliases resolved - plus any changes made in hooks. + """Return the command as run which includes shortcuts and aliases resolved plus any changes made in hooks. Proxy property for ``self.statement.expanded_command_line`` """ @@ -123,12 +122,12 @@ def pr(self, idx: int, script: bool = False, expanded: bool = False, verbose: bo return ret_str def to_dict(self) -> dict[str, Any]: - """Utility method to convert this HistoryItem into a dictionary for use in persistent JSON history files.""" + """Convert this HistoryItem into a dictionary for use in persistent JSON history files.""" return {HistoryItem._statement_field: self.statement.to_dict()} @staticmethod def from_dict(source_dict: dict[str, Any]) -> 'HistoryItem': - """Utility method to restore a HistoryItem from a dictionary. + """Restore a HistoryItem from a dictionary. :param source_dict: source data dictionary (generated using to_dict()) :return: HistoryItem object @@ -139,8 +138,7 @@ def from_dict(source_dict: dict[str, Any]) -> 'HistoryItem': class History(list[HistoryItem]): - """A list of [HistoryItem][cmd2.history.HistoryItem] objects with additional methods - for searching and managing the list. + """A list of [HistoryItem][cmd2.history.HistoryItem] objects with additional methods for searching and managing the list. [cmd2.Cmd][] instantiates this class into the `cmd2.Cmd.history` attribute, and adds commands to it as a user enters them. @@ -335,7 +333,8 @@ def truncate(self, max_length: int) -> None: def _build_result_dictionary( self, start: int, end: int, filter_func: Optional[Callable[[HistoryItem], bool]] = None ) -> 'OrderedDict[int, HistoryItem]': - """Build history search results + """Build history search results. + :param start: start index to search from :param end: end index to stop searching (exclusive). """ @@ -346,7 +345,7 @@ def _build_result_dictionary( return results def to_json(self) -> str: - """Utility method to convert this History into a JSON string for use in persistent history files.""" + """Convert this History into a JSON string for use in persistent history files.""" json_dict = { History._history_version_field: History._history_version, History._history_items_field: [hi.to_dict() for hi in self], @@ -355,7 +354,7 @@ def to_json(self) -> str: @staticmethod def from_json(history_json: str) -> 'History': - """Utility method to restore History from a JSON string. + """Restore History from a JSON string. :param history_json: history data as JSON string (generated using to_json()) :return: History object diff --git a/cmd2/parsing.py b/cmd2/parsing.py index 29bb7da0..e12f799c 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -23,7 +23,10 @@ def shlex_split(str_to_split: str) -> list[str]: - """A wrapper around shlex.split() that uses cmd2's preferred arguments. + """Split the string *str_to_split* using shell-like syntax. + + A wrapper around shlex.split() that uses cmd2's preferred arguments. + This allows other classes to easily call split() the same way StatementParser does. :param str_to_split: the string being split @@ -34,7 +37,8 @@ def shlex_split(str_to_split: str) -> list[str]: @dataclass(frozen=True) class MacroArg: - """Information used to replace or unescape arguments in a macro value when the macro is resolved + """Information used to replace or unescape arguments in a macro value when the macro is resolved. + Normal argument syntax: {5} Escaped argument syntax: {{5}}. """ @@ -217,12 +221,12 @@ def argv(self) -> list[str]: return rtn def to_dict(self) -> dict[str, Any]: - """Utility method to convert this Statement into a dictionary for use in persistent JSON history files.""" + """Convert this Statement into a dictionary for use in persistent JSON history files.""" return self.__dict__.copy() @staticmethod def from_dict(source_dict: dict[str, Any]) -> 'Statement': - """Utility method to restore a Statement from a dictionary. + """Restore a Statement from a dictionary. :param source_dict: source data dictionary (generated using to_dict()) :return: Statement object @@ -359,8 +363,7 @@ def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> tuple[b return valid, errmsg def tokenize(self, line: str) -> list[str]: - """Lex a string into a list of tokens. Shortcuts and aliases are expanded and - comments are removed. + """Lex a string into a list of tokens. Shortcuts and aliases are expanded and comments are removed. :param line: the command line being lexed :return: A list of tokens @@ -383,9 +386,9 @@ def tokenize(self, line: str) -> list[str]: return self.split_on_punctuation(tokens) def parse(self, line: str) -> Statement: - """Tokenize the input and parse it into a [cmd2.parsing.Statement][] object, - stripping comments, expanding aliases and shortcuts, and extracting output - redirection directives. + """Tokenize the input and parse it into a [cmd2.parsing.Statement][] object. + + Stripping comments, expanding aliases and shortcuts, and extracting output redirection directives. :param line: the command line being parsed :return: a new [cmd2.parsing.Statement][] object @@ -523,7 +526,7 @@ def parse(self, line: str) -> Statement: ) def parse_command_only(self, rawinput: str) -> Statement: - """Partially parse input into a [cmd2.Statement][] object. + """Parse input into a [cmd2.Statement][] object (partially). The command is identified, and shortcuts and aliases are expanded. Multiline commands are identified, but terminators and output @@ -584,9 +587,9 @@ def parse_command_only(self, rawinput: str) -> Statement: def get_command_arg_list( self, command_name: str, to_parse: Union[Statement, str], preserve_quotes: bool ) -> tuple[Statement, list[str]]: - """Convenience method used by the argument parsing decorators. + """Retrieve just the arguments being passed to their ``do_*`` methods as a list. - Retrieves just the arguments being passed to their ``do_*`` methods as a list. + Convenience method used by the argument parsing decorators. :param command_name: name of the command being run :param to_parse: what is being passed to the ``do_*`` method. It can be one of two types: @@ -651,9 +654,7 @@ def _expand(self, line: str) -> str: @staticmethod def _command_and_args(tokens: list[str]) -> tuple[str, str]: - """Given a list of tokens, return a tuple of the command - and the args as a string. - """ + """Given a list of tokens, return a tuple of the command and the args as a string.""" command = '' args = '' diff --git a/cmd2/py_bridge.py b/cmd2/py_bridge.py index 1cccbaed..aecf04f9 100644 --- a/cmd2/py_bridge.py +++ b/cmd2/py_bridge.py @@ -1,5 +1,6 @@ -"""Bridges calls made inside of a Python environment to the Cmd2 host app -while maintaining a reasonable degree of isolation between the two. +"""Bridges calls made inside of a Python environment to the Cmd2 host app. + +Maintains a reasonable degree of isolation between the two. """ import sys @@ -68,7 +69,7 @@ class CommandResult(NamedTuple): data: Any = None def __bool__(self) -> bool: - """Returns True if the command succeeded, otherwise False.""" + """Return True if the command succeeded, otherwise False.""" # If data was set, then use it to determine success if self.data is not None: return bool(self.data) @@ -100,11 +101,12 @@ def __dir__(self) -> list[str]: return attributes def __call__(self, command: str, *, echo: Optional[bool] = None) -> CommandResult: - """Provide functionality to call application commands by calling PyBridge + """Provide functionality to call application commands by calling PyBridge. + ex: app('help') :param command: command line being run :param echo: If provided, this temporarily overrides the value of self.cmd_echo while the - command runs. If True, output will be echoed to stdout/stderr. (Defaults to None). + command runs. If True, output will be echoed to stdout/stderr. (Defaults to None) """ if echo is None: diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py index 0d39ffb9..a07479c7 100644 --- a/cmd2/rl_utils.py +++ b/cmd2/rl_utils.py @@ -66,7 +66,8 @@ class RlType(Enum): if sys.stdout is not None and sys.stdout.isatty(): # pragma: no cover def enable_win_vt100(handle: HANDLE) -> bool: - """Enables VT100 character sequences in a Windows console + """Enable VT100 character sequences in a Windows console. + This only works on Windows 10 and up :param handle: the handle on which to enable vt100 :return: True if vt100 characters are enabled for the handle. @@ -104,7 +105,10 @@ def enable_win_vt100(handle: HANDLE) -> bool: if not hasattr(readline, 'remove_history_item'): def pyreadline_remove_history_item(pos: int) -> None: - """An implementation of remove_history_item() for pyreadline3 + """Remove the specified item number from the pyreadline3 history. + + An implementation of remove_history_item() for pyreadline3. + :param pos: The 0-based position in history to remove. """ # Save of the current location of the history cursor @@ -149,9 +153,9 @@ def pyreadline_remove_history_item(pos: int) -> None: def rl_force_redisplay() -> None: # pragma: no cover - """Causes readline to display the prompt and input text wherever the cursor is and start - reading input from this location. This is the proper way to restore the input line after - printing to the screen. + """Causes readline to display the prompt and input text wherever the cursor is and start reading input from this location. + + This is the proper way to restore the input line after printing to the screen. """ if not sys.stdout.isatty(): return @@ -170,7 +174,7 @@ def rl_force_redisplay() -> None: # pragma: no cover def rl_get_point() -> int: # pragma: no cover - """Returns the offset of the current cursor position in rl_line_buffer.""" + """Return the offset of the current cursor position in rl_line_buffer.""" if rl_type == RlType.GNU: return ctypes.c_int.in_dll(readline_lib, "rl_point").value @@ -210,7 +214,8 @@ def rl_get_display_prompt() -> str: # pragma: no cover def rl_set_prompt(prompt: str) -> None: # pragma: no cover - """Sets Readline's prompt + """Set Readline's prompt. + :param prompt: the new prompt value. """ escaped_prompt = rl_escape_prompt(prompt) diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py index 5cd64466..35c89e10 100644 --- a/cmd2/table_creator.py +++ b/cmd2/table_creator.py @@ -1,4 +1,5 @@ -"""cmd2 table creation API +"""cmd2 table creation API. + This API is built upon two core classes: Column and TableCreator The general use case is to inherit from TableCreator to create a table class with custom formatting options. There are already implemented and ready-to-use examples of this below TableCreator's code. @@ -109,7 +110,9 @@ def __init__( class TableCreator: - """Base table creation class. This class handles ANSI style sequences and characters with display widths greater than 1 + """Base table creation class. + + This class handles ANSI style sequences and characters with display widths greater than 1 when performing width calculations. It was designed with the ability to build tables one row at a time. This helps when you have large data sets that you don't want to hold in memory or when you receive portions of the data set incrementally. @@ -147,7 +150,7 @@ def __init__(self, cols: Sequence[Column], *, tab_width: int = 4) -> None: @staticmethod def _wrap_long_word(word: str, max_width: int, max_lines: float, is_last_word: bool) -> tuple[str, int, int]: - """Used by _wrap_text() to wrap a long word over multiple lines. + """Wrap a long word over multiple lines, used by _wrap_text(). :param word: word being wrapped :param max_width: maximum display width of a line @@ -210,9 +213,10 @@ def _wrap_long_word(word: str, max_width: int, max_lines: float, is_last_word: b @staticmethod def _wrap_text(text: str, max_width: int, max_lines: float) -> str: - """Wrap text into lines with a display width no longer than max_width. This function breaks words on whitespace - boundaries. If a word is longer than the space remaining on a line, then it will start on a new line. - ANSI escape sequences do not count toward the width of a line. + """Wrap text into lines with a display width no longer than max_width. + + This function breaks words on whitespace boundaries. If a word is longer than the space remaining on a line, + then it will start on a new line. ANSI escape sequences do not count toward the width of a line. :param text: text to be wrapped :param max_width: maximum display width of a line @@ -224,7 +228,7 @@ def _wrap_text(text: str, max_width: int, max_lines: float) -> str: total_lines = 0 def add_word(word_to_add: str, is_last_word: bool) -> None: - """Called from loop to add a word to the wrapped text. + """Aadd a word to the wrapped text, called from loop. :param word_to_add: the word being added :param is_last_word: True if this is the last word of the total text being wrapped @@ -529,6 +533,7 @@ def __init__(self) -> None: ############################################################################################################ class SimpleTable(TableCreator): """Implementation of TableCreator which generates a borderless table with an optional divider row after the header. + This class can be used to create the whole table at once or one row at a time. """ @@ -580,7 +585,8 @@ def __init__( self.data_bg = data_bg def apply_header_bg(self, value: Any) -> str: - """If defined, apply the header background color to header text + """If defined, apply the header background color to header text. + :param value: object whose text is to be colored :return: formatted text. """ @@ -589,7 +595,8 @@ def apply_header_bg(self, value: Any) -> str: return ansi.style(value, bg=self.header_bg) def apply_data_bg(self, value: Any) -> str: - """If defined, apply the data background color to data text + """If defined, apply the data background color to data text. + :param value: object whose text is to be colored :return: formatted data string. """ @@ -599,7 +606,8 @@ def apply_data_bg(self, value: Any) -> str: @classmethod def base_width(cls, num_cols: int, *, column_spacing: int = 2) -> int: - """Utility method to calculate the display width required for a table before data is added to it. + """Calculate the display width required for a table before data is added to it. + This is useful when determining how wide to make your columns to have a table be a specific width. :param num_cols: how many columns the table will have @@ -717,8 +725,9 @@ def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header: class BorderedTable(TableCreator): - """Implementation of TableCreator which generates a table with borders around the table and between rows. Borders - between columns can also be toggled. This class can be used to create the whole table at once or one row at a time. + """Implementation of TableCreator which generates a table with borders around the table and between rows. + + Borders between columns can also be toggled. This class can be used to create the whole table at once or one row at a time. """ def __init__( @@ -763,7 +772,8 @@ def __init__( self.data_bg = data_bg def apply_border_color(self, value: Any) -> str: - """If defined, apply the border foreground and background colors + """If defined, apply the border foreground and background colors. + :param value: object whose text is to be colored :return: formatted text. """ @@ -772,7 +782,8 @@ def apply_border_color(self, value: Any) -> str: return ansi.style(value, fg=self.border_fg, bg=self.border_bg) def apply_header_bg(self, value: Any) -> str: - """If defined, apply the header background color to header text + """If defined, apply the header background color to header text. + :param value: object whose text is to be colored :return: formatted text. """ @@ -781,7 +792,8 @@ def apply_header_bg(self, value: Any) -> str: return ansi.style(value, bg=self.header_bg) def apply_data_bg(self, value: Any) -> str: - """If defined, apply the data background color to data text + """If defined, apply the data background color to data text. + :param value: object whose text is to be colored :return: formatted data string. """ @@ -791,7 +803,8 @@ def apply_data_bg(self, value: Any) -> str: @classmethod def base_width(cls, num_cols: int, *, column_borders: bool = True, padding: int = 1) -> int: - """Utility method to calculate the display width required for a table before data is added to it. + """Calculate the display width required for a table before data is added to it. + This is useful when determining how wide to make your columns to have a table be a specific width. :param num_cols: how many columns the table will have @@ -1006,8 +1019,9 @@ def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header: class AlternatingTable(BorderedTable): - """Implementation of BorderedTable which uses background colors to distinguish between rows instead of row border - lines. This class can be used to create the whole table at once or one row at a time. + """Implementation of BorderedTable which uses background colors to distinguish between rows instead of row border lines. + + This class can be used to create the whole table at once or one row at a time. To nest an AlternatingTable within another AlternatingTable, set style_data_text to False on the Column which contains the nested table. That will prevent the current row's background color from affecting the colors @@ -1060,7 +1074,8 @@ def __init__( self.even_bg = even_bg def apply_data_bg(self, value: Any) -> str: - """Apply background color to data text based on what row is being generated and whether a color has been defined + """Apply background color to data text based on what row is being generated and whether a color has been defined. + :param value: object whose text is to be colored :return: formatted data string. """ diff --git a/cmd2/utils.py b/cmd2/utils.py index 825eae81..7a90120e 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -49,7 +49,7 @@ def is_quoted(arg: str) -> bool: - """Checks if a string is quoted. + """Check if a string is quoted. :param arg: the string being checked for quotes :return: True if a string is quoted @@ -86,7 +86,7 @@ def strip_quotes(arg: str) -> str: def to_bool(val: Any) -> bool: - """Converts anything to a boolean based on its value. + """Convert anything to a boolean based on its value. Strings like "True", "true", "False", and "false" return True, True, False, and False respectively. All other values are converted using bool() @@ -151,7 +151,7 @@ def __init__( if val_type is bool: def get_bool_choices(_: str) -> list[str]: - """Used to tab complete lowercase boolean values.""" + """Tab complete lowercase boolean values.""" return ['true', 'false'] val_type = to_bool @@ -194,7 +194,7 @@ def set_value(self, value: Any) -> None: def is_text_file(file_path: str) -> bool: - """Returns if a file contains only ASCII or UTF-8 encoded text and isn't empty. + """Return if a file contains only ASCII or UTF-8 encoded text and isn't empty. :param file_path: path to the file being checked :return: True if the file is a non-empty text file, otherwise False @@ -221,7 +221,7 @@ def is_text_file(file_path: str) -> bool: def remove_duplicates(list_to_prune: list[_T]) -> list[_T]: - """Removes duplicates from a list while preserving order of the items. + """Remove duplicates from a list while preserving order of the items. :param list_to_prune: the list being pruned of duplicates :return: The pruned list @@ -258,7 +258,8 @@ def alphabetical_sort(list_to_sort: Iterable[str]) -> list[str]: def try_int_or_force_to_lower_case(input_str: str) -> Union[int, str]: - """Tries to convert the passed-in string to an integer. If that fails, it converts it to lower case using norm_fold. + """Try to convert the passed-in string to an integer. If that fails, it converts it to lower case using norm_fold. + :param input_str: string to convert :return: the string as an integer or a lower case version of the string. """ @@ -269,7 +270,7 @@ def try_int_or_force_to_lower_case(input_str: str) -> Union[int, str]: def natural_keys(input_str: str) -> list[Union[int, str]]: - """Converts a string into a list of integers and strings to support natural sorting (see natural_sort). + """Convert a string into a list of integers and strings to support natural sorting (see natural_sort). For example: natural_keys('abc123def') -> ['abc', '123', 'def'] :param input_str: string to convert @@ -317,8 +318,9 @@ def unquote_specific_tokens(tokens: list[str], tokens_to_unquote: list[str]) -> def expand_user(token: str) -> str: - """Wrap os.expanduser() to support expanding ~ in quoted strings - :param token: the string to expand. + """Wrap os.expanduser() to support expanding ~ in quoted strings. + + :param token: the string to expand """ if token: if is_quoted(token): @@ -337,7 +339,8 @@ def expand_user(token: str) -> str: def expand_user_in_tokens(tokens: list[str]) -> None: - """Call expand_user() on all tokens in a list of strings + """Call expand_user() on all tokens in a list of strings. + :param tokens: tokens to expand. """ for index, _ in enumerate(tokens): @@ -345,7 +348,8 @@ def expand_user_in_tokens(tokens: list[str]) -> None: def find_editor() -> Optional[str]: - """Used to set cmd2.Cmd.DEFAULT_EDITOR. If EDITOR env variable is set, that will be used. + """Set cmd2.Cmd.DEFAULT_EDITOR. If EDITOR env variable is set, that will be used. + Otherwise the function will look for a known editor in directories specified by PATH env variable. :return: Default editor or None. """ @@ -404,7 +408,7 @@ def files_from_glob_patterns(patterns: list[str], access: int = os.F_OK) -> list def get_exes_in_path(starts_with: str) -> list[str]: - """Returns names of executables in a user's path. + """Return names of executables in a user's path. :param starts_with: what the exes should start with. leave blank for all exes in path. :return: a list of matching exe names @@ -435,6 +439,7 @@ def get_exes_in_path(starts_with: str) -> list[str]: class StdSim: """Class to simulate behavior of sys.stdout or sys.stderr. + Stores contents in internal buffer and optionally echos to the inner stream it is simulating. """ @@ -513,8 +518,9 @@ def isatty(self) -> bool: @property def line_buffering(self) -> bool: - """Handle when the inner stream doesn't have a line_buffering attribute which is the case - when running unit tests because pytest sets stdout to a pytest EncodedFile object. + """Handle when the inner stream doesn't have a line_buffering attribute. + + Which is the case when running unit tests because pytest sets stdout to a pytest EncodedFile object. """ try: return bool(self.inner_stream.line_buffering) @@ -556,11 +562,13 @@ def write(self, b: bytes) -> None: class ProcReader: """Used to capture stdout and stderr from a Popen process if any of those were set to subprocess.PIPE. + If neither are pipes, then the process will run normally and no output will be captured. """ def __init__(self, proc: PopenTextIO, stdout: Union[StdSim, TextIO], stderr: Union[StdSim, TextIO]) -> None: - """ProcReader initializer + """ProcReader initializer. + :param proc: the Popen process being read from :param stdout: the stream to write captured stdout :param stderr: the stream to write captured stderr. @@ -617,7 +625,8 @@ def wait(self) -> None: self._write_bytes(self._stderr, err) def _reader_thread_func(self, read_stdout: bool) -> None: - """Thread function that reads a stream from the process + """Thread function that reads a stream from the process. + :param read_stdout: if True, then this thread deals with stdout. Otherwise it deals with stderr. """ if read_stdout: @@ -640,7 +649,8 @@ def _reader_thread_func(self, read_stdout: bool) -> None: @staticmethod def _write_bytes(stream: Union[StdSim, TextIO], to_write: Union[bytes, str]) -> None: - """Write bytes to a stream + """Write bytes to a stream. + :param stream: the stream being written to :param to_write: the bytes being written. """ @@ -687,7 +697,8 @@ def __init__( pipe_proc_reader: Optional[ProcReader], saved_redirecting: bool, ) -> None: - """RedirectionSavedState initializer + """RedirectionSavedState initializer. + :param self_stdout: saved value of Cmd.stdout :param sys_stdout: saved value of sys.stdout :param pipe_proc_reader: saved value of Cmd._cur_pipe_proc_reader @@ -706,8 +717,9 @@ def __init__( def _remove_overridden_styles(styles_to_parse: list[str]) -> list[str]: - """Utility function for align_text() / truncate_line() which filters a style list down - to only those which would still be in effect if all were processed in order. + """Filter a style list down to only those which would still be in effect if all were processed in order. + + Utility function for align_text() / truncate_line(). This is mainly used to reduce how many style strings are stored in memory when building large multiline strings with ANSI styles. We only need to carry over @@ -803,6 +815,7 @@ def align_text( truncate: bool = False, ) -> str: """Align text for display within a given width. Supports characters with display widths greater than 1. + ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned independently. @@ -922,6 +935,7 @@ def align_left( text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False ) -> str: """Left align text for display within a given width. Supports characters with display widths greater than 1. + ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned independently. @@ -944,6 +958,7 @@ def align_center( text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False ) -> str: """Center text for display within a given width. Supports characters with display widths greater than 1. + ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned independently. @@ -966,6 +981,7 @@ def align_right( text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False ) -> str: """Right align text for display within a given width. Supports characters with display widths greater than 1. + ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned independently. @@ -985,9 +1001,10 @@ def align_right( def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str: - """Truncate a single line to fit within a given display width. Any portion of the string that is truncated - is replaced by a '…' character. Supports characters with display widths greater than 1. ANSI style sequences - do not count toward the display width. + """Truncate a single line to fit within a given display width. + + Any portion of the string that is truncated is replaced by a '…' character. Supports characters with display widths greater + than 1. ANSI style sequences do not count toward the display width. If there are ANSI style sequences in the string after where truncation occurs, this function will append them to the returned string. @@ -1122,7 +1139,7 @@ def do_echo(self, arglist): def get_defining_class(meth: Callable[..., Any]) -> Optional[type[Any]]: - """Attempts to resolve the class that defined a method. + """Attempt to resolve the class that defined a method. Inspired by implementation published here: https://stackoverflow.com/a/25959545/1956611 @@ -1168,7 +1185,7 @@ class CustomCompletionSettings: """Used by cmd2.Cmd.complete() to tab complete strings other than command arguments.""" def __init__(self, parser: argparse.ArgumentParser, *, preserve_quotes: bool = False) -> None: - """Initializer. + """CustomCompletionSettings initializer. :param parser: arg parser defining format of string being tab completed :param preserve_quotes: if True, then quoted tokens will keep their quotes when processed by diff --git a/pyproject.toml b/pyproject.toml index 6b036662..17a5ba91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -227,7 +227,9 @@ ignore = [ "B905", # zip() without an explicit strict= parameter (strict added in Python 3.10+) "COM812", # Conflicts with ruff format (see https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules) "COM819", # Conflicts with ruff format + "D203", # 1 blank line required before class docstring (conflicts with D211) "D206", # Conflicts with ruff format + "D213", # Multi-line docstring summary should start at 2nd line (conflicts with D212 which starts at 1st line) "D300", # Conflicts with ruff format "E111", # Conflicts with ruff format "E114", # Conflicts with ruff format