diff --git a/CHANGELOG.md b/CHANGELOG.md index ff22a040..1091bd69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ shell, and the option for a persistent bottom bar that can display realtime stat - Removed `Statement.pipe_to` since it can be handled by `Statement.redirector` and `Statement.redirect_to`. - Changed `StatementParser.parse_command_only()` to return a `PartialStatement` object. + - Renamed `Macro.arg_list` to `Macro.args`. - Enhancements - New `cmd2.Cmd` parameters - **auto_suggest**: (boolean) if `True`, provide fish shell style auto-suggestions. These diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index d18b5aca..cfd29a34 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -41,10 +41,7 @@ import tempfile import threading from code import InteractiveConsole -from collections import ( - OrderedDict, - namedtuple, -) +from collections import namedtuple from collections.abc import ( Callable, Iterable, @@ -2951,7 +2948,7 @@ def _resolve_macro(self, statement: Statement) -> str | None: # Resolve the arguments in reverse and read their values from statement.argv since those # are unquoted. Macro args should have been quoted when the macro was created. resolved = macro.value - reverse_arg_list = sorted(macro.arg_list, key=lambda ma: ma.start_index, reverse=True) + reverse_arg_list = sorted(macro.args, key=lambda ma: ma.start_index, reverse=True) for macro_arg in reverse_arg_list: if macro_arg.is_escaped: @@ -3743,7 +3740,7 @@ def _macro_create(self, args: argparse.Namespace) -> None: value += ' ' + ' '.join(args.command_args) # Find all normal arguments - arg_list = [] + macro_args = [] normal_matches = re.finditer(MacroArg.macro_normal_arg_pattern, value) max_arg_num = 0 arg_nums = set() @@ -3762,7 +3759,7 @@ def _macro_create(self, args: argparse.Namespace) -> None: arg_nums.add(cur_num) max_arg_num = max(max_arg_num, cur_num) - arg_list.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=False)) + macro_args.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=False)) except StopIteration: pass @@ -3781,7 +3778,7 @@ def _macro_create(self, args: argparse.Namespace) -> None: # Get the number string between the braces cur_num_str = re.findall(MacroArg.digit_pattern, cur_match.group())[0] - arg_list.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=True)) + macro_args.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=True)) except StopIteration: pass @@ -3789,7 +3786,7 @@ def _macro_create(self, args: argparse.Namespace) -> None: result = "overwritten" if args.name in self.macros else "created" self.poutput(f"Macro '{args.name}' {result}") - self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, arg_list=arg_list) + self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, args=macro_args) self.last_result = True # macro -> delete @@ -4961,7 +4958,7 @@ def do_history(self, args: argparse.Namespace) -> bool | None: self.last_result = history return None - def _get_history(self, args: argparse.Namespace) -> 'OrderedDict[int, HistoryItem]': + def _get_history(self, args: argparse.Namespace) -> dict[int, HistoryItem]: """If an argument was supplied, then retrieve partial contents of the history; otherwise retrieve entire history. This function returns a dictionary with history items keyed by their 1-based index in ascending order. @@ -4969,7 +4966,7 @@ def _get_history(self, args: argparse.Namespace) -> 'OrderedDict[int, HistoryIte if args.arg: try: int_arg = int(args.arg) - return OrderedDict({int_arg: self.history.get(int_arg)}) + return {int_arg: self.history.get(int_arg)} except ValueError: pass diff --git a/cmd2/history.py b/cmd2/history.py index c2b1e2ca..599bd13f 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -2,7 +2,6 @@ import json import re -from collections import OrderedDict from collections.abc import ( Callable, Iterable, @@ -224,7 +223,7 @@ def get(self, index: int) -> HistoryItem: # spanpattern = re.compile(r'^\s*(?P-?[1-9]\d*)?(?P:|(\.{2,}))(?P-?[1-9]\d*)?\s*$') - def span(self, span: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]': + def span(self, span: str, include_persisted: bool = False) -> dict[int, 'HistoryItem']: """Return a slice of the History list. :param span: string containing an index or a slice @@ -273,7 +272,7 @@ def span(self, span: str, include_persisted: bool = False) -> 'OrderedDict[int, return self._build_result_dictionary(start, end) - def str_search(self, search: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]': + def str_search(self, search: str, include_persisted: bool = False) -> dict[int, 'HistoryItem']: """Find history items which contain a given string. :param search: the string to search for @@ -292,7 +291,7 @@ def isin(history_item: HistoryItem) -> bool: start = 0 if include_persisted else self.session_start_index return self._build_result_dictionary(start, len(self), isin) - def regex_search(self, regex: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]': + def regex_search(self, regex: str, include_persisted: bool = False) -> dict[int, 'HistoryItem']: """Find history items which match a given regular expression. :param regex: the regular expression to search for. @@ -328,13 +327,13 @@ def truncate(self, max_length: int) -> None: def _build_result_dictionary( self, start: int, end: int, filter_func: Callable[[HistoryItem], bool] | None = None - ) -> 'OrderedDict[int, HistoryItem]': + ) -> dict[int, 'HistoryItem']: """Build history search results. :param start: start index to search from :param end: end index to stop searching (exclusive). """ - results: OrderedDict[int, HistoryItem] = OrderedDict() + results: dict[int, HistoryItem] = {} for index in range(start, end): if filter_func is None or filter_func(self[index]): results[index + 1] = self[index] diff --git a/cmd2/parsing.py b/cmd2/parsing.py index 543c9d29..e1095529 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -3,13 +3,19 @@ import re import shlex import sys -from collections.abc import Iterable +from collections.abc import ( + Iterable, + Sequence, +) from dataclasses import ( asdict, dataclass, field, ) -from typing import Any +from typing import ( + Any, + ClassVar, +) if sys.version_info >= (3, 11): from typing import Self @@ -37,13 +43,9 @@ def shlex_split(str_to_split: str) -> list[str]: return shlex.split(str_to_split, comments=False, posix=False) -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class MacroArg: - """Information used to replace or unescape arguments in a macro value when the macro is resolved. - - Normal argument syntax: {5} - Escaped argument syntax: {{5}}. - """ + """Information used to resolve or unescape macro arguments.""" # The starting index of this argument in the macro value start_index: int @@ -56,21 +58,22 @@ class MacroArg: # Tells if this argument is escaped and therefore needs to be unescaped is_escaped: bool - # Pattern used to find normal argument - # Digits surrounded by exactly 1 brace on a side and 1 or more braces on the opposite side - # Match strings like: {5}, {{{{{4}, {2}}}}} - macro_normal_arg_pattern = re.compile(r'(? Match '{' not preceded by '{' + # \d+ -> Match digits + # }(?!}) -> Match '}' not followed by '}' + macro_normal_arg_pattern: ClassVar[re.Pattern[str]] = re.compile(r'(? None: + """Finalize the object after initialization.""" + # Convert args to an immutable tuple. + if not isinstance(self.args, tuple): + object.__setattr__(self, 'args', tuple(self.args)) @dataclass(frozen=True)