summaryrefslogtreecommitdiff
path: root/cmd2
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2023-01-31 15:01:12 -0500
committerGitHub <noreply@github.com>2023-01-31 15:01:12 -0500
commitee7599f9ac0dbb6ce3793f6b665ba1200d3ef9a3 (patch)
tree36a957ba9028cbc0cecfc9f9310dfe8db139ed0a /cmd2
parent031832a76b7a9e25d708153085d18d5366ff318d (diff)
downloadcmd2-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.py5
-rw-r--r--cmd2/argparse_custom.py4
-rw-r--r--cmd2/cmd2.py19
-rw-r--r--cmd2/history.py13
-rwxr-xr-xcmd2/parsing.py55
-rw-r--r--cmd2/plugin.py13
-rw-r--r--cmd2/transcript.py2
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: