diff options
| author | Todd Leonhardt <todd.leonhardt@gmail.com> | 2023-01-31 15:01:12 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-01-31 15:01:12 -0500 |
| commit | ee7599f9ac0dbb6ce3793f6b665ba1200d3ef9a3 (patch) | |
| tree | 36a957ba9028cbc0cecfc9f9310dfe8db139ed0a /cmd2 | |
| parent | 031832a76b7a9e25d708153085d18d5366ff318d (diff) | |
| download | cmd2-git-ee7599f9ac0dbb6ce3793f6b665ba1200d3ef9a3.tar.gz | |
Deprecate support for Python 3.6 and remove dependency on attrs (#1257)
* Start deprecation of Python 3.6
* Removed dependency on attrs and replaced with dataclasses
* Fix typing
* Added comments to assist with dropping support of Python versions in the future.
---------
Co-authored-by: Kevin Van Brunt <kmvanbrunt@gmail.com>
Diffstat (limited to 'cmd2')
| -rw-r--r-- | cmd2/argparse_completer.py | 5 | ||||
| -rw-r--r-- | cmd2/argparse_custom.py | 4 | ||||
| -rw-r--r-- | cmd2/cmd2.py | 19 | ||||
| -rw-r--r-- | cmd2/history.py | 13 | ||||
| -rwxr-xr-x | cmd2/parsing.py | 55 | ||||
| -rw-r--r-- | cmd2/plugin.py | 13 | ||||
| -rw-r--r-- | cmd2/transcript.py | 2 |
7 files changed, 46 insertions, 65 deletions
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 09ec2255..88be8b52 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -273,10 +273,8 @@ class ArgparseCompleter: # Check if this action is in a mutually exclusive group for group in self._parser._mutually_exclusive_groups: if arg_action in group._group_actions: - # Check if the group this action belongs to has already been completed if group in completed_mutex_groups: - # If this is the action that completed the group, then there is no error # since it's allowed to appear on the command line more than once. completer_action = completed_mutex_groups[group] @@ -307,7 +305,6 @@ class ArgparseCompleter: # Parse all but the last token ############################################################################################# for token_index, token in enumerate(tokens[:-1]): - # If we're in a positional REMAINDER arg, force all future tokens to go to that if pos_arg_state is not None and pos_arg_state.is_remainder: consume_argument(pos_arg_state) @@ -339,7 +336,6 @@ class ArgparseCompleter: # Check the format of the current token to see if it can be an argument's value if _looks_like_flag(token, self._parser) and not skip_remaining_flags: - # Check if there is an unfinished flag if ( flag_arg_state is not None @@ -484,7 +480,6 @@ class ArgparseCompleter: # Otherwise check if we have a positional to complete elif pos_arg_state is not None or remaining_positionals: - # If we aren't current tracking a positional, then get the next positional arg to handle this token if pos_arg_state is None: action = remaining_positionals.popleft() diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index a8ad1ebd..c2e98b75 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -276,6 +276,7 @@ try: runtime_checkable, ) except ImportError: + # Remove these imports when we no longer support Python 3.7 from typing_extensions import ( # type: ignore[assignment] Protocol, runtime_checkable, @@ -807,7 +808,6 @@ def _add_argument_wrapper( nargs_adjusted: Union[int, str, Tuple[int], Tuple[int, int], Tuple[int, float], None] # Check if nargs was given as a range if isinstance(nargs, tuple): - # Handle 1-item tuple by setting max to INFINITY if len(nargs) == 1: nargs = (nargs[0], constants.INFINITY) @@ -1032,6 +1032,7 @@ setattr(argparse.ArgumentParser, '_check_value', _ArgumentParser_check_value) # Patch argparse._SubParsersAction to add remove_parser function ############################################################################################################ + # noinspection PyPep8Naming,PyProtectedMember def _SubParsersAction_remove_parser(self: argparse._SubParsersAction, name: str) -> None: # type: ignore """ @@ -1123,7 +1124,6 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter): # wrap the usage parts if it's too long text_width = self._width - self._current_indent if len(prefix) + len(usage) > text_width: - # Begin cmd2 customization # break usage into wrappable parts diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 6cd8b950..97cfb77d 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -155,13 +155,11 @@ else: orig_rl_delims = readline.get_completer_delims() if rl_type == RlType.PYREADLINE: - # Save the original pyreadline3 display completion function since we need to override it and restore it # noinspection PyProtectedMember,PyUnresolvedReferences orig_pyreadline_display = readline.rl.mode._display_completions elif rl_type == RlType.GNU: - # Get the readline lib so we can make changes to it import ctypes @@ -1498,7 +1496,6 @@ class Cmd(cmd.Cmd): # Used to complete ~ and ~user strings def complete_users() -> List[str]: - users = [] # Windows lacks the pwd module so we can't get a list of users. @@ -1516,10 +1513,8 @@ class Cmd(cmd.Cmd): # Iterate through a list of users from the password database for cur_pw in pwd.getpwall(): - # Check if the user has an existing home dir if os.path.isdir(cur_pw.pw_dir): - # Add a ~ to the user to match against text cur_user = '~' + cur_pw.pw_name if cur_user.startswith(text): @@ -1605,7 +1600,6 @@ class Cmd(cmd.Cmd): # Build display_matches and add a slash to directories for index, cur_match in enumerate(matches): - # Display only the basename of this path in the tab completion suggestions self.display_matches.append(os.path.basename(cur_match)) @@ -1674,7 +1668,6 @@ class Cmd(cmd.Cmd): # Must at least have the command if len(raw_tokens) > 1: - # True when command line contains any redirection tokens has_redirection = False @@ -1766,7 +1759,6 @@ class Cmd(cmd.Cmd): :param longest_match_length: longest printed length of the matches """ if rl_type == RlType.GNU: - # Print hint if one exists and we are supposed to display it hint_printed = False if self.always_show_hint and self.completion_hint: @@ -1826,7 +1818,6 @@ class Cmd(cmd.Cmd): :param matches: the tab completion matches to display """ if rl_type == RlType.PYREADLINE: - # Print hint if one exists and we are supposed to display it hint_printed = False if self.always_show_hint and self.completion_hint: @@ -1980,7 +1971,6 @@ class Cmd(cmd.Cmd): # Check if the token being completed has an opening quote if raw_completion_token and raw_completion_token[0] in constants.QUOTES: - # Since the token is still being completed, we know the opening quote is unclosed. # Save the quote so we can add a matching closing quote later. completion_token_quote = raw_completion_token[0] @@ -2005,7 +1995,6 @@ class Cmd(cmd.Cmd): self.completion_matches = self._redirect_complete(text, line, begidx, endidx, completer_func) if self.completion_matches: - # Eliminate duplicates self.completion_matches = utils.remove_duplicates(self.completion_matches) self.display_matches = utils.remove_duplicates(self.display_matches) @@ -2020,7 +2009,6 @@ class Cmd(cmd.Cmd): # Check if we need to add an opening quote if not completion_token_quote: - add_quote = False # This is the tab completion text that will appear on the command line. @@ -2103,7 +2091,7 @@ class Cmd(cmd.Cmd): # from text and update the indexes. This only applies if we are at the beginning of the command line. shortcut_to_restore = '' if begidx == 0 and custom_settings is None: - for (shortcut, _) in self.statement_parser.shortcuts: + for shortcut, _ in self.statement_parser.shortcuts: if text.startswith(shortcut): # Save the shortcut to restore later shortcut_to_restore = shortcut @@ -3066,7 +3054,6 @@ class Cmd(cmd.Cmd): readline_settings = _SavedReadlineSettings() if self._completion_supported(): - # Set up readline for our tab completion needs if rl_type == RlType.GNU: # GNU readline automatically adds a closing quote if the text being completed has an opening quote. @@ -3100,7 +3087,6 @@ class Cmd(cmd.Cmd): :param readline_settings: the readline settings to restore """ if self._completion_supported(): - # Restore what we changed in readline readline.set_completer(readline_settings.completer) readline.set_completer_delims(readline_settings.delims) @@ -3881,7 +3867,7 @@ class Cmd(cmd.Cmd): fulloptions.append((opt[0], opt[1])) except IndexError: fulloptions.append((opt[0], opt[0])) - for (idx, (_, text)) in enumerate(fulloptions): + for idx, (_, text) in enumerate(fulloptions): self.poutput(' %2d. %s' % (idx + 1, text)) while True: @@ -5035,7 +5021,6 @@ class Cmd(cmd.Cmd): # Sanity check that can't fail if self.terminal_lock was acquired before calling this function if self.terminal_lock.acquire(blocking=False): - # Windows terminals tend to flicker when we redraw the prompt and input lines. # To reduce how often this occurs, only update terminal if there are changes. update_terminal = False diff --git a/cmd2/history.py b/cmd2/history.py index df3c1255..a7d6baff 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -8,6 +8,9 @@ import re from collections import ( OrderedDict, ) +from dataclasses import ( + dataclass, +) from typing import ( Any, Callable, @@ -19,8 +22,6 @@ from typing import ( overload, ) -import attr - from . import ( utils, ) @@ -29,7 +30,7 @@ from .parsing import ( ) -@attr.s(auto_attribs=True, frozen=True) +@dataclass(frozen=True) class HistoryItem: """Class used to represent one command in the history list""" @@ -39,10 +40,10 @@ class HistoryItem: # Used in JSON dictionaries _statement_field = 'statement' - statement: Statement = attr.ib(default=None, validator=attr.validators.instance_of(Statement)) + statement: Statement def __str__(self) -> str: - """A convenient human readable representation of the history item""" + """A convenient human-readable representation of the history item""" return self.statement.raw @property @@ -90,7 +91,7 @@ class HistoryItem: if self.statement.multiline_command: # This is an approximation and not meant to be a perfect piecing together of lines. # All newlines will be converted to spaces, including the ones in quoted strings that - # are considered literals. Also if the final line starts with a terminator, then the + # are considered literals. Also, if the final line starts with a terminator, then the # terminator will have an extra space before it in the 1 line version. ret_str = ret_str.replace('\n', ' ') diff --git a/cmd2/parsing.py b/cmd2/parsing.py index 9069cea2..fc28b634 100755 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -4,6 +4,10 @@ import re import shlex +from dataclasses import ( + dataclass, + field, +) from typing import ( Any, Dict, @@ -14,8 +18,6 @@ from typing import ( Union, ) -import attr - from . import ( constants, utils, @@ -36,7 +38,7 @@ def shlex_split(str_to_split: str) -> List[str]: return shlex.split(str_to_split, comments=False, posix=False) -@attr.s(auto_attribs=True, frozen=True) +@dataclass(frozen=True) class MacroArg: """ Information used to replace or unescape arguments in a macro value when the macro is resolved @@ -45,15 +47,15 @@ class MacroArg: """ # The starting index of this argument in the macro value - start_index: int = attr.ib(validator=attr.validators.instance_of(int)) + start_index: int # The number string that appears between the braces # This is a string instead of an int because we support unicode digits and must be able # to reproduce this string later - number_str: str = attr.ib(validator=attr.validators.instance_of(str)) + number_str: str # Tells if this argument is escaped and therefore needs to be unescaped - is_escaped: bool = attr.ib(validator=attr.validators.instance_of(bool)) + 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 @@ -69,24 +71,24 @@ class MacroArg: digit_pattern = re.compile(r'\d+') -@attr.s(auto_attribs=True, frozen=True) +@dataclass(frozen=True) class Macro: """Defines a cmd2 macro""" # Name of the macro - name: str = attr.ib(validator=attr.validators.instance_of(str)) + name: str # The string the macro resolves to - value: str = attr.ib(validator=attr.validators.instance_of(str)) + value: str # The minimum number of args the user has to pass to this macro - minimum_arg_count: int = attr.ib(validator=attr.validators.instance_of(int)) + minimum_arg_count: int # Used to fill in argument placeholders in the macro - arg_list: List[MacroArg] = attr.ib(default=attr.Factory(list), validator=attr.validators.instance_of(list)) + arg_list: List[MacroArg] = field(default_factory=list) -@attr.s(auto_attribs=True, frozen=True) +@dataclass(frozen=True) class Statement(str): # type: ignore[override] """String subclass with additional attributes to store the results of parsing. @@ -118,34 +120,34 @@ class Statement(str): # type: ignore[override] """ # the arguments, but not the command, nor the output redirection clauses. - args: str = attr.ib(default='', validator=attr.validators.instance_of(str)) + args: str = '' # string containing exactly what we input by the user - raw: str = attr.ib(default='', validator=attr.validators.instance_of(str)) + raw: str = '' # the command, i.e. the first whitespace delimited word - command: str = attr.ib(default='', validator=attr.validators.instance_of(str)) + command: str = '' # list of arguments to the command, not including any output redirection or terminators; quoted args remain quoted - arg_list: List[str] = attr.ib(default=attr.Factory(list), validator=attr.validators.instance_of(list)) + arg_list: List[str] = field(default_factory=list) # if the command is a multiline command, the name of the command, otherwise empty - multiline_command: str = attr.ib(default='', validator=attr.validators.instance_of(str)) + multiline_command: str = '' # the character which terminated the multiline command, if there was one - terminator: str = attr.ib(default='', validator=attr.validators.instance_of(str)) + terminator: str = '' # characters appearing after the terminator but before output redirection, if any - suffix: str = attr.ib(default='', validator=attr.validators.instance_of(str)) + suffix: str = '' # if output was piped to a shell command, the shell command as a string - pipe_to: str = attr.ib(default='', validator=attr.validators.instance_of(str)) + pipe_to: str = '' # if output was redirected, the redirection token, i.e. '>>' - output: str = attr.ib(default='', validator=attr.validators.instance_of(str)) + output: str = '' # if output was redirected, the destination file token (quotes preserved) - output_to: str = attr.ib(default='', validator=attr.validators.instance_of(str)) + output_to: str = '' # Used in JSON dictionaries _args_field = 'args' @@ -156,7 +158,7 @@ class Statement(str): # type: ignore[override] We must override __new__ because we are subclassing `str` which is immutable and takes a different number of arguments as Statement. - NOTE: attrs takes care of initializing other members in the __init__ it + NOTE: @dataclass takes care of initializing other members in the __init__ it generates. """ stmt = super().__new__(cls, value) @@ -348,7 +350,7 @@ class StatementParser: return False, 'cannot start with the comment character' if not is_subcommand: - for (shortcut, _) in self.shortcuts: + for shortcut, _ in self.shortcuts: if word.startswith(shortcut): # Build an error string with all shortcuts listed errmsg = 'cannot start with a shortcut: ' @@ -481,7 +483,6 @@ class StatementParser: # Check if output should be piped to a shell command if pipe_index < redir_index and pipe_index < append_index: - # Get the tokens for the pipe command and expand ~ where needed pipe_to_tokens = tokens[pipe_index + 1 :] utils.expand_user_in_tokens(pipe_to_tokens) @@ -656,7 +657,7 @@ class StatementParser: keep_expanding = bool(remaining_aliases) # expand shortcuts - for (shortcut, expansion) in self.shortcuts: + for shortcut, expansion in self.shortcuts: if line.startswith(shortcut): # If the next character after the shortcut isn't a space, then insert one shortcut_len = len(shortcut) @@ -701,7 +702,6 @@ class StatementParser: punctuated_tokens = [] for cur_initial_token in tokens: - # Save tokens up to 1 character in length or quoted tokens. No need to parse these. if len(cur_initial_token) <= 1 or cur_initial_token[0] in constants.QUOTES: punctuated_tokens.append(cur_initial_token) @@ -716,7 +716,6 @@ class StatementParser: while True: if cur_char not in punctuation: - # Keep appending to new_token until we hit a punctuation char while cur_char not in punctuation: new_token += cur_char diff --git a/cmd2/plugin.py b/cmd2/plugin.py index f9f5c573..affe2421 100644 --- a/cmd2/plugin.py +++ b/cmd2/plugin.py @@ -1,18 +1,19 @@ # # coding=utf-8 """Classes for the cmd2 plugin system""" +from dataclasses import ( + dataclass, +) from typing import ( Optional, ) -import attr - from .parsing import ( Statement, ) -@attr.s(auto_attribs=True) +@dataclass class PostparsingData: """Data class containing information passed to postparsing hook methods""" @@ -20,14 +21,14 @@ class PostparsingData: statement: Statement -@attr.s(auto_attribs=True) +@dataclass class PrecommandData: """Data class containing information passed to precommand hook methods""" statement: Statement -@attr.s(auto_attribs=True) +@dataclass class PostcommandData: """Data class containing information passed to postcommand hook methods""" @@ -35,7 +36,7 @@ class PostcommandData: statement: Statement -@attr.s(auto_attribs=True) +@dataclass class CommandFinalizationData: """Data class containing information passed to command finalization hook methods""" diff --git a/cmd2/transcript.py b/cmd2/transcript.py index 83ede932..a38c1902 100644 --- a/cmd2/transcript.py +++ b/cmd2/transcript.py @@ -60,7 +60,7 @@ class Cmd2TestCase(unittest.TestCase): def runTest(self) -> None: # was testall if self.cmdapp: its = sorted(self.transcripts.items()) - for (fname, transcript) in its: + for fname, transcript in its: self._test_transcript(fname, transcript) def _fetchTranscripts(self) -> None: |
