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.

    :param text: the string being measured
    :return: The width of the string when printed to the terminal if no errors occur.
             If text contains characters with no absolute width (i.e. tabs),
             then this function returns -1. Replace tabs with spaces before calling this.
    """
    # Strip ANSI style sequences since they cause wcswidth to return -1
    return cast(int, wcswidth(strip_style(text))) 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.

    :param text: the string being measured
    :return: The width of the string when printed to the terminal if no errors occur.
             If text contains characters with no absolute width (i.e. tabs),
             then this function returns -1. Replace tabs with spaces before calling this.
    """
    if not text:
        return 0

    lines_widths = [style_aware_wcswidth(line) for line in text.splitlines()]
    if -1 in lines_widths:
        return -1

    return max(lines_widths) INFINITY = float('inf') 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.
    """

    def __init__(
        self,
        is_completer: bool,
        to_call: Union[CompleterFunc, ChoicesProviderFunc],
    ) -> None:
        """
        Initializer
        :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
        """
        self.is_completer = is_completer
        if is_completer:
            if not isinstance(to_call, (CompleterFuncBase, CompleterFuncWithTokens)):  # pragma: no cover
                # runtime checking of Protocols do not currently check the parameters of a function.
                raise ValueError(
                    'With is_completer set to true, to_call must be either CompleterFunc, CompleterFuncWithTokens'
                )
        else:
            if not isinstance(to_call, (ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens)):  # pragma: no cover
                # runtime checking of Protocols do not currently check the parameters of a function.
                raise ValueError(
                    'With is_completer set to false, to_call must be either: '
                    'ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens'
                )
        self.to_call = to_call

    @property
    def completer(self) -> CompleterFunc:
        if not isinstance(self.to_call, (CompleterFuncBase, CompleterFuncWithTokens)):  # pragma: no cover
            # this should've been caught in the constructor, just a backup check
            raise ValueError('Function is not a CompleterFunc')
        return self.to_call

    @property
    def choices_provider(self) -> ChoicesProviderFunc:
        if not isinstance(self.to_call, (ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens)):  # pragma: no cover
            # this should've been caught in the constructor, just a backup check
            raise ValueError('Function is not a ChoicesProviderFunc')
        return self.to_call class ChoicesProviderFuncWithTokens(Protocol):
    """
    Function that returns a list of choices in support of tab completion and accepts a dictionary of prior arguments.
    """

    def __call__(self, *, arg_tokens: Dict[str, List[str]] = {}) -> List[str]:
        ...  # pragma: no cover class CompletionItem(str):
    """
    Completion item with descriptive text attached

    See header of this file for more information
    """

    def __new__(cls, value: object, *args: Any, **kwargs: Any) -> 'CompletionItem':
        return super(CompletionItem, cls).__new__(cls, value)

    # noinspection PyUnusedLocal
    def __init__(self, value: object, description: str = '', *args: Any) -> None:
        """
        CompletionItem Initializer

        :param value: the value being tab completed
        :param description: description text to display
        :param args: args for str __init__
        :param kwargs: kwargs for str __init__
        """
        super().__init__(*args)
        self.description = description

        # Save the original value to support CompletionItems as argparse choices.
        # cmd2 has patched argparse so input is compared to this value instead of the CompletionItem instance.
        self._orig_value = value

    @property
    def orig_value(self) -> Any:
        """Read-only property for _orig_value"""
        return self._orig_value def generate_range_error(range_min: int, range_max: Union[int, float]) -> str:
    """Generate an error message when the the number of arguments provided is not within the expected range"""
    err_str = "expected "

    if range_max == constants.INFINITY:
        plural = '' if range_min == 1 else 's'
        err_str += f"at least {range_min}"
    else:
        plural = '' if range_max == 1 else 's'
        if range_min == range_max:
            err_str += f"{range_min}"
        else:
            err_str += f"{range_min} to {range_max}"

    err_str += f" argument{plural}"

    return err_str class CommandSet(object):
    """
    Base class for defining sets of commands to load in cmd2.

    ``with_default_category`` can be used to apply a default category to all commands in the CommandSet.

    ``do_``, ``help_``, and ``complete_`` functions differ only in that self is the CommandSet instead of the cmd2 app
    """

    def __init__(self) -> None:
        self._cmd: Optional[cmd2.Cmd] = None
        self._settables: Dict[str, Settable] = {}
        self._settable_prefix = self.__class__.__name__

    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).

        :param cmd: The cmd2 main application
        """
        if self._cmd is None:
            self._cmd = cmd
        else:
            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.
        Subclasses can override this to perform custom steps related to the newly added commands (e.g. setting
        them to a disabled state).
        """
        pass

    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.
        """
        pass

    def on_unregistered(self) -> None:
        """
        Called by ``cmd2.Cmd`` after a CommandSet has been unregistered and all its commands removed from the CLI.
        Subclasses can override this to perform remaining cleanup steps.
        """
        self._cmd = None

    @property
    def settable_prefix(self) -> str:
        return self._settable_prefix

    @property
    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

        :param settable: Settable object being added
        """
        if self._cmd:
            if not self._cmd.always_prefix_settables:
                if settable.name in self._cmd.settables.keys() and settable.name not in self._settables.keys():
                    raise KeyError(f'Duplicate settable: {settable.name}')
            else:
                prefixed_name = f'{self._settable_prefix}.{settable.name}'
                if prefixed_name in self._cmd.settables.keys() and settable.name not in self._settables.keys():
                    raise KeyError(f'Duplicate settable: {settable.name}')
        self._settables[settable.name] = settable

    def remove_settable(self, name: str) -> None:
        """
        Convenience method for removing a settable parameter from the CommandSet

        :param name: name of the settable being removed
        :raises: KeyError if the Settable matches this name
        """
        try:
            del self._settables[name]
        except KeyError:
            raise KeyError(name + " is not a settable parameter") 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.

    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
    - Tab completion hints
    """

    def __init__(self, *args: Any, apply_style: bool = True) -> None:
        """
        Initializer for CompletionError
        :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.
        """
        self.apply_style = apply_style

        # noinspection PyArgumentList
        super().__init__(*args) class Column:
    """Table column configuration"""

    def __init__(
        self,
        header: str,
        *,
        width: Optional[int] = None,
        header_horiz_align: HorizontalAlignment = HorizontalAlignment.LEFT,
        header_vert_align: VerticalAlignment = VerticalAlignment.BOTTOM,
        style_header_text: bool = True,
        data_horiz_align: HorizontalAlignment = HorizontalAlignment.LEFT,
        data_vert_align: VerticalAlignment = VerticalAlignment.TOP,
        style_data_text: bool = True,
        max_data_lines: Union[int, float] = constants.INFINITY,
    ) -> None:
        """
        Column initializer

        :param header: label for column header
        :param width: display width of column. This does not account for any borders or padding which
                      may be added (e.g pre_line, inter_cell, and post_line). Header and data text wrap within
                      this width using word-based wrapping (defaults to actual width of header or 1 if header is blank)
        :param header_horiz_align: horizontal alignment of header cells (defaults to left)
        :param header_vert_align: vertical alignment of header cells (defaults to bottom)
        :param style_header_text: if True, then the table is allowed to apply styles to the header text, which may
                                  conflict with any styles the header already has. If False, the header is printed as is.
                                  Table classes which apply style to headers must account for the value of this flag.
                                  (defaults to True)
        :param data_horiz_align: horizontal alignment of data cells (defaults to left)
        :param data_vert_align: vertical alignment of data cells (defaults to top)
        :param style_data_text: if True, then the table is allowed to apply styles to the data text, which may
                                conflict with any styles the data already has. If False, the data is printed as is.
                                Table classes which apply style to data must account for the value of this flag.
                                (defaults to True)
        :param max_data_lines: maximum lines allowed in a data cell. If line count exceeds this, then the final
                               line displayed will be truncated with an ellipsis. (defaults to INFINITY)
        :raises: ValueError if width is less than 1
        :raises: ValueError if max_data_lines is less than 1
        """
        self.header = header

        if width is not None and width < 1:
            raise ValueError("Column width cannot be less than 1")
        else:
            self.width: int = width if width is not None else -1

        self.header_horiz_align = header_horiz_align
        self.header_vert_align = header_vert_align
        self.style_header_text = style_header_text

        self.data_horiz_align = data_horiz_align
        self.data_vert_align = data_vert_align
        self.style_data_text = style_data_text

        if max_data_lines < 1:
            raise ValueError("Max data lines cannot be less than 1")

        self.max_data_lines = max_data_lines class HorizontalAlignment(Enum):
    """Horizontal alignment of text in a cell"""

    LEFT = 1
    CENTER = 2
    RIGHT = 3 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.
    """

    def __init__(
        self,
        cols: Sequence[Column],
        *,
        column_spacing: int = 2,
        tab_width: int = 4,
        divider_char: Optional[str] = '-',
        header_bg: Optional[ansi.BgColor] = None,
        data_bg: Optional[ansi.BgColor] = None,
    ) -> None:
        """
        SimpleTable initializer

        :param cols: column definitions for this table
        :param column_spacing: how many spaces to place between columns. Defaults to 2.
        :param tab_width: all tabs will be replaced with this many spaces. If a row's fill_char is a tab,
                          then it will be converted to one space.
        :param divider_char: optional character used to build the header divider row. Set this to blank or None if you don't
                             want a divider row. Defaults to dash. (Cannot be a line breaking character)
        :param header_bg: optional background color for header cells (defaults to None)
        :param data_bg: optional background color for data cells (defaults to None)
        :raises: ValueError if tab_width is less than 1
        :raises: ValueError if column_spacing is less than 0
        :raises: TypeError if divider_char is longer than one character
        :raises: ValueError if divider_char is an unprintable character
        """
        super().__init__(cols, tab_width=tab_width)

        if column_spacing < 0:
            raise ValueError("Column spacing cannot be less than 0")

        self.column_spacing = column_spacing

        if divider_char == '':
            divider_char = None

        if divider_char is not None:
            if len(ansi.strip_style(divider_char)) != 1:
                raise TypeError("Divider character must be exactly one character long")

            divider_char_width = ansi.style_aware_wcswidth(divider_char)
            if divider_char_width == -1:
                raise ValueError("Divider character is an unprintable character")

        self.divider_char = divider_char
        self.header_bg = header_bg
        self.data_bg = data_bg

    def apply_header_bg(self, value: Any) -> str:
        """
        If defined, apply the header background color to header text
        :param value: object whose text is to be colored
        :return: formatted text
        """
        if self.header_bg is None:
            return str(value)
        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
        :param value: object whose text is to be colored
        :return: formatted data string
        """
        if self.data_bg is None:
            return str(value)
        return ansi.style(value, bg=self.data_bg)

    @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.
        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
        :param column_spacing: how many spaces to place between columns. Defaults to 2.
        :return: base width
        :raises: ValueError if column_spacing is less than 0
        :raises: ValueError if num_cols is less than 1
        """
        if num_cols < 1:
            raise ValueError("Column count cannot be less than 1")

        data_str = SPACE
        data_width = ansi.style_aware_wcswidth(data_str) * num_cols

        tbl = cls([Column(data_str)] * num_cols, column_spacing=column_spacing)
        data_row = tbl.generate_data_row([data_str] * num_cols)

        return ansi.style_aware_wcswidth(data_row) - data_width

    def total_width(self) -> int:
        """Calculate the total display width of this table"""
        base_width = self.base_width(len(self.cols), column_spacing=self.column_spacing)
        data_width = sum(col.width for col in self.cols)
        return base_width + data_width

    def generate_header(self) -> str:
        """Generate table header with an optional divider row"""
        header_buf = io.StringIO()

        fill_char = self.apply_header_bg(SPACE)
        inter_cell = self.apply_header_bg(self.column_spacing * SPACE)

        # Apply background color to header text in Columns which allow it
        to_display: List[Any] = []
        for col in self.cols:
            if col.style_header_text:
                to_display.append(self.apply_header_bg(col.header))
            else:
                to_display.append(col.header)

        # Create the header labels
        header_labels = self.generate_row(to_display, is_header=True, fill_char=fill_char, inter_cell=inter_cell)
        header_buf.write(header_labels)

        # Add the divider if necessary
        divider = self.generate_divider()
        if divider:
            header_buf.write('\n' + divider)

        return header_buf.getvalue()

    def generate_divider(self) -> str:
        """Generate divider row"""
        if self.divider_char is None:
            return ''

        return utils.align_left('', fill_char=self.divider_char, width=self.total_width())

    def generate_data_row(self, row_data: Sequence[Any]) -> str:
        """
        Generate a data row

        :param row_data: data with an entry for each column in the row
        :return: data row string
        :raises: ValueError if row_data isn't the same length as self.cols
        """
        if len(row_data) != len(self.cols):
            raise ValueError("Length of row_data must match length of cols")

        fill_char = self.apply_data_bg(SPACE)
        inter_cell = self.apply_data_bg(self.column_spacing * SPACE)

        # Apply background color to data text in Columns which allow it
        to_display: List[Any] = []
        for index, col in enumerate(self.cols):
            if col.style_data_text:
                to_display.append(self.apply_data_bg(row_data[index]))
            else:
                to_display.append(row_data[index])

        return self.generate_row(to_display, is_header=False, fill_char=fill_char, inter_cell=inter_cell)

    def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header: bool = True, row_spacing: int = 1) -> str:
        """
        Generate a table from a data set

        :param table_data: Data with an entry for each data row of the table. Each entry should have data for
                           each column in the row.
        :param include_header: If True, then a header will be included at top of table. (Defaults to True)
        :param row_spacing: A number 0 or greater specifying how many blank lines to place between
                            each row (Defaults to 1)
        :raises: ValueError if row_spacing is less than 0
        """
        if row_spacing < 0:
            raise ValueError("Row spacing cannot be less than 0")

        table_buf = io.StringIO()

        if include_header:
            header = self.generate_header()
            table_buf.write(header)
            if len(table_data) > 0:
                table_buf.write('\n')

        row_divider = utils.align_left('', fill_char=self.apply_data_bg(SPACE), width=self.total_width()) + '\n'

        for index, row_data in enumerate(table_data):
            if index > 0 and row_spacing > 0:
                table_buf.write(row_spacing * row_divider)

            row = self.generate_data_row(row_data)
            table_buf.write(row)
            if index < len(table_data) - 1:
                table_buf.write('\n')

        return table_buf.getvalue()
import argparse
import inspect
import numbers
from collections import (
    deque,
)
from typing import (
    TYPE_CHECKING,
    Dict,
    List,
    Optional,
    Type,
    Union,
    cast,
)
from .ansi import (
    style_aware_wcswidth,
    widest_line,
)
from .constants import (
    INFINITY,
)
    from .cmd2 import (
        Cmd,
    )
from .argparse_custom import (
    ChoicesCallable,
    ChoicesProviderFuncWithTokens,
    CompletionItem,
    generate_range_error,
)
from .command_definition import (
    CommandSet,
)
from .exceptions import (
    CompletionError,
)
from .table_creator import (
    Column,
    HorizontalAlignment,
    SimpleTable,
)

            # Otherwise, print a hint if text isn't possibly the start of a flag
            elif not _single_prefix_char(text, self._parser) or skip_remaining_flags:
                raise _NoResultsError(self._parser, pos_arg_state.action)

        # If we aren't skipping remaining flags, then complete flag names if either is True:
        #   1. text is a single flag prefix character that didn't complete against any argument values
        #   2. there are no more positionals to complete
        if not skip_remaining_flags and (_single_prefix_char(text, self._parser) or not remaining_positionals):
            # Reset any completion settings that may have been set by functions which actually had no matches.
            # Otherwise, those settings could alter how the flags are displayed.
            self._cmd2_app._reset_completion_defaults()
            return self._complete_flags(text, line, begidx, endidx, matched_flags)

        return completion_results

    def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: List[str]) -> List[str]:
        """Tab completion routine for a parsers unused flags"""

        # Build a list of flags that can be tab completed
        match_against = []

        for flag in self._flags:
            # Make sure this flag hasn't already been used
            if flag not in matched_flags:
                # Make sure this flag isn't considered hidden
                action = self._flag_to_action[flag]
                if action.help != argparse.SUPPRESS:
                    match_against.append(flag)

        matches = self._cmd2_app.basic_complete(text, line, begidx, endidx, match_against)

        # Build a dictionary linking actions with their matched flag names
        matched_actions: Dict[argparse.Action, List[str]] = dict()
        for flag in matches:
            action = self._flag_to_action[flag]
            matched_actions.setdefault(action, [])
            matched_actions[action].append(flag)

        # For tab completion suggestions, group matched flags by action
        for action, option_strings in matched_actions.items():
            flag_text = ', '.join(option_strings)

            # Mark optional flags with brackets
            if not action.required:
                flag_text = '[' + flag_text + ']'
            self._cmd2_app.display_matches.append(flag_text)

        return matches

    def _format_completions(self, arg_state: _ArgumentState, completions: Union[List[str], List[CompletionItem]]) -> List[str]:
        """Format CompletionItems into hint table"""

        # Nothing to do if we don't have at least 2 completions which are all CompletionItems
        if len(completions) < 2 or not all(isinstance(c, CompletionItem) for c in completions):
            return cast(List[str], completions)

        completion_items = cast(List[CompletionItem], completions)

        # Check if the data being completed have a numerical type
        all_nums = all(isinstance(c.orig_value, numbers.Number) for c in completion_items)

        # Sort CompletionItems before building the hint table
        if not self._cmd2_app.matches_sorted:
            # If all orig_value types are numbers, then sort by that value
            if all_nums:
                completion_items.sort(key=lambda c: c.orig_value)  # type: ignore[no-any-return]

            # Otherwise sort as strings
            else:
                completion_items.sort(key=self._cmd2_app.default_sort_key)

            self._cmd2_app.matches_sorted = True

        # Check if there are too many CompletionItems to display as a table
        if len(completions) <= self._cmd2_app.max_completion_items:
            four_spaces = 4 * ' '

            # If a metavar was defined, use that instead of the dest field
            destination = arg_state.action.metavar if arg_state.action.metavar else arg_state.action.dest

            # Handle case where metavar was a tuple
            if isinstance(destination, tuple):
                # Figure out what string in the tuple to use based on how many of the arguments have been completed.
                # Use min() to avoid going passed the end of the tuple to support nargs being ZERO_OR_MORE and
                # ONE_OR_MORE. In those cases, argparse limits metavar tuple to 2 elements but we may be completing
                # the 3rd or more argument here.
                tuple_index = min(len(destination) - 1, arg_state.count)
                destination = destination[tuple_index]

            desc_header = arg_state.action.get_descriptive_header()  # type: ignore[attr-defined]
            if desc_header is None:
                desc_header = DEFAULT_DESCRIPTIVE_HEADER

            # Replace tabs with 4 spaces so we can calculate width
            desc_header = desc_header.replace('\t', four_spaces)

            # Calculate needed widths for the token and description columns of the table
            token_width = style_aware_wcswidth(destination)
            desc_width = widest_line(desc_header)

            for item in completion_items:
                token_width = max(style_aware_wcswidth(item), token_width)

                # Replace tabs with 4 spaces so we can calculate width
                item.description = item.description.replace('\t', four_spaces)
                desc_width = max(widest_line(item.description), desc_width)

            cols = list()
            dest_alignment = HorizontalAlignment.RIGHT if all_nums else HorizontalAlignment.LEFT
            cols.append(
                Column(
                    destination.upper(),
                    width=token_width,
                    header_horiz_align=dest_alignment,
                    data_horiz_align=dest_alignment,
                )
            )
            cols.append(Column(desc_header, width=desc_width))


            hint_table = SimpleTable(cols, divider_char=self._cmd2_app.ruler)