summaryrefslogtreecommitdiff
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
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>
-rw-r--r--.github/CONTRIBUTING.md5
-rw-r--r--.github/workflows/doc.yml2
-rw-r--r--.github/workflows/format.yml2
-rw-r--r--.github/workflows/lint.yml2
-rw-r--r--.github/workflows/mypy.yml2
-rw-r--r--.gitignore5
-rw-r--r--CHANGELOG.md6
-rw-r--r--Pipfile1
-rwxr-xr-xREADME.md2
-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
-rw-r--r--docs/features/transcripts.rst8
-rw-r--r--docs/overview/installation.rst4
-rwxr-xr-xexamples/async_printing.py1
-rw-r--r--examples/scripts/save_help_text.py1
-rw-r--r--noxfile.py4
-rw-r--r--plugins/ext_test/build-pyenvs.sh2
-rw-r--r--plugins/ext_test/cmd2_ext_test/__init__.py2
-rw-r--r--plugins/ext_test/noxfile.py2
-rw-r--r--plugins/ext_test/setup.py4
-rw-r--r--plugins/ext_test/tasks.py1
-rw-r--r--plugins/template/README.md10
-rw-r--r--plugins/template/cmd2_myplugin/__init__.py2
-rw-r--r--plugins/template/setup.py3
-rwxr-xr-xsetup.py5
-rw-r--r--tests/test_argparse_custom.py1
-rwxr-xr-xtests/test_parsing.py7
-rw-r--r--tests/test_transcript.py1
-rw-r--r--tests/test_utils.py11
-rw-r--r--tests_isolated/test_commandset/test_commandset.py2
35 files changed, 89 insertions, 120 deletions
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 8e3596bc..abd05ccd 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -46,9 +46,8 @@ The tables below list all prerequisites along with the minimum required version
#### Prerequisites to run cmd2 applications
| Prerequisite | Minimum Version |
-| --------------------------------------------------- | --------------- |
-| [python](https://www.python.org/downloads/) | `3.6` |
-| [attrs](https://github.com/python-attrs/attrs) | `16.3` |
+| --------------------------------------------------- |-----------------|
+| [python](https://www.python.org/downloads/) | `3.7` |
| [pyperclip](https://github.com/asweigart/pyperclip) | `1.6` |
| [setuptools](https://pypi.org/project/setuptools/) | `34.4` |
| [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.1.7` |
diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml
index 70bd661b..48e6b4a7 100644
--- a/.github/workflows/doc.yml
+++ b/.github/workflows/doc.yml
@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
- python-version: ["3.10"]
+ python-version: ["3.11"]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
index 6f0454d8..8c8ecb98 100644
--- a/.github/workflows/format.yml
+++ b/.github/workflows/format.yml
@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
- python-version: ["3.10"]
+ python-version: ["3.11"]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 6b567238..e36f05d7 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
- python-version: ["3.10"]
+ python-version: ["3.11"]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml
index 6a4d0533..474e1137 100644
--- a/.github/workflows/mypy.yml
+++ b/.github/workflows/mypy.yml
@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
- python-version: ["3.10"]
+ python-version: ["3.11"]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
diff --git a/.gitignore b/.gitignore
index 1fdfc6a5..11b71aa3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,4 +39,7 @@ Pipfile.lock
.venv
# Commitizen configuration
-.cz.toml \ No newline at end of file
+.cz.toml
+
+# pyenv version file
+.python-version
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7d52a49..57e04405 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 2.5.0 (TBD)
+* Breaking Change
+ * `cmd2` 2.5 supports Python 3.7+ (removed support for Python 3.6)
+* Enhancements
+ * Removed dependency on `attrs` and replaced with [dataclasses](https://docs.python.org/3/library/dataclasses.html)
+
## 2.4.3 (January 27, 2023)
* Bug Fixes
* Fixed ValueError caused when passing `Cmd.columnize()` strings wider than `display_width`.
diff --git a/Pipfile b/Pipfile
index f0e94a8b..fbf399ad 100644
--- a/Pipfile
+++ b/Pipfile
@@ -4,7 +4,6 @@ url = "https://pypi.org/simple"
verify_ssl = true
[packages]
-attrs = ">=16.3.0"
pyperclip = ">=1.6"
setuptools = ">=34.4"
wcwidth = ">=0.1.7"
diff --git a/README.md b/README.md
index dbf7af1c..09e083df 100755
--- a/README.md
+++ b/README.md
@@ -78,7 +78,7 @@ On all operating systems, the latest stable version of `cmd2` can be installed u
pip install -U cmd2
```
-cmd2 works with Python 3.6+ on Windows, macOS, and Linux. It is pure Python code with few 3rd-party dependencies.
+cmd2 works with Python 3.7+ on Windows, macOS, and Linux. It is pure Python code with few 3rd-party dependencies.
For information on other installation options, see
[Installation Instructions](https://cmd2.readthedocs.io/en/latest/overview/installation.html) in the cmd2
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:
diff --git a/docs/features/transcripts.rst b/docs/features/transcripts.rst
index fa6d9cb3..9bab8996 100644
--- a/docs/features/transcripts.rst
+++ b/docs/features/transcripts.rst
@@ -127,8 +127,8 @@ If your output has slashes in it, you will need to escape those slashes so the
stuff between them is not interpred as a regular expression. In this
transcript::
- (Cmd) say cd /usr/local/lib/python3.6/site-packages
- /usr/local/lib/python3.6/site-packages
+ (Cmd) say cd /usr/local/lib/python3.11/site-packages
+ /usr/local/lib/python3.11/site-packages
the output contains slashes. The text between the first slash and the second
slash, will be interpreted as a regular expression, and those two slashes will
@@ -136,8 +136,8 @@ not be included in the comparison. When replayed, this transcript would
therefore fail. To fix it, we could either write a regular expression to match
the path instead of specifying it verbatim, or we can escape the slashes::
- (Cmd) say cd /usr/local/lib/python3.6/site-packages
- \/usr\/local\/lib\/python3.6\/site-packages
+ (Cmd) say cd /usr/local/lib/python3.11/site-packages
+ \/usr\/local\/lib\/python3.11\/site-packages
.. warning::
diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst
index a98fef52..743e0aca 100644
--- a/docs/overview/installation.rst
+++ b/docs/overview/installation.rst
@@ -7,7 +7,7 @@ Installation Instructions
.. _setuptools: https://pypi.org/project/setuptools
.. _PyPI: https://pypi.org
-``cmd2`` works on Linux, macOS, and Windows. It requires Python 3.6 or
+``cmd2`` works on Linux, macOS, and Windows. It requires Python 3.7 or
higher, pip_, and setuptools_. If you've got all that, then you can just:
.. code-block:: shell
@@ -30,7 +30,7 @@ higher, pip_, and setuptools_. If you've got all that, then you can just:
Prerequisites
-------------
-If you have Python 3 >=3.6 installed from `python.org
+If you have Python 3 >=3.7 installed from `python.org
<https://www.python.org>`_, you will already have pip_ and setuptools_, but may
need to upgrade to the latest versions:
diff --git a/examples/async_printing.py b/examples/async_printing.py
index 15a4445e..6ff3a262 100755
--- a/examples/async_printing.py
+++ b/examples/async_printing.py
@@ -176,7 +176,6 @@ class AlerterApp(cmd2.Cmd):
# Always acquire terminal_lock before printing alerts or updating the prompt
# To keep the app responsive, do not block on this call
if self.terminal_lock.acquire(blocking=False):
-
# Get any alerts that need to be printed
alert_str = self._generate_alert_str()
diff --git a/examples/scripts/save_help_text.py b/examples/scripts/save_help_text.py
index ad028395..b8ba9624 100644
--- a/examples/scripts/save_help_text.py
+++ b/examples/scripts/save_help_text.py
@@ -22,7 +22,6 @@ def get_sub_commands(parser: argparse.ArgumentParser) -> List[str]:
# Check if this is parser has subcommands
if parser is not None and parser._subparsers is not None:
-
# Find the _SubParsersAction for the subcommands of this parser
for action in parser._subparsers._actions:
if isinstance(action, argparse._SubParsersAction):
diff --git a/noxfile.py b/noxfile.py
index 14b4d5a0..f7a705fb 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -1,7 +1,7 @@
import nox
-@nox.session(python=['3.10'])
+@nox.session(python=['3.11'])
def docs(session):
session.install(
'sphinx',
@@ -41,7 +41,7 @@ def tests(session, plugin):
)
-@nox.session(python=['3.8', '3.9', '3.10'])
+@nox.session(python=['3.8', '3.9', '3.10', '3.11'])
@nox.parametrize('step', ['mypy', 'flake8'])
def validate(session, step):
session.install('invoke', './[validate]')
diff --git a/plugins/ext_test/build-pyenvs.sh b/plugins/ext_test/build-pyenvs.sh
index d64e11bd..572db568 100644
--- a/plugins/ext_test/build-pyenvs.sh
+++ b/plugins/ext_test/build-pyenvs.sh
@@ -23,7 +23,7 @@
# virtualenvs will be added to '.python-version'. Feel free to modify
# this list, but note that this script intentionally won't install
# dev, rc, or beta python releases
-declare -a pythons=("3.7" "3.6" "3.8" "3.9")
+declare -a pythons=("3.7" "3.8" "3.9", "3.10", "3.11")
# function to find the latest patch of a minor version of python
function find_latest_version {
diff --git a/plugins/ext_test/cmd2_ext_test/__init__.py b/plugins/ext_test/cmd2_ext_test/__init__.py
index c17a329a..b30c949d 100644
--- a/plugins/ext_test/cmd2_ext_test/__init__.py
+++ b/plugins/ext_test/cmd2_ext_test/__init__.py
@@ -9,7 +9,7 @@ try:
# For python 3.8 and later
import importlib.metadata as importlib_metadata
except ImportError: # pragma: no cover
- # For everyone else
+ # Remove this import when we no longer support Python 3.7
# MyPy Issue # 1153 causes a spurious error that must be ignored
import importlib_metadata # type: ignore
diff --git a/plugins/ext_test/noxfile.py b/plugins/ext_test/noxfile.py
index 75eab841..9872e193 100644
--- a/plugins/ext_test/noxfile.py
+++ b/plugins/ext_test/noxfile.py
@@ -1,7 +1,7 @@
import nox
-@nox.session(python=['3.6', '3.7', '3.8', '3.9'])
+@nox.session(python=['3.7', '3.8', '3.9', '3.10', '3.11'])
def tests(session):
session.install('invoke', './[test]')
session.run('invoke', 'pytest', '--junit', '--no-pty')
diff --git a/plugins/ext_test/setup.py b/plugins/ext_test/setup.py
index 06e7827d..644d2677 100644
--- a/plugins/ext_test/setup.py
+++ b/plugins/ext_test/setup.py
@@ -33,7 +33,7 @@ setuptools.setup(
license='MIT',
package_data=PACKAGE_DATA,
packages=['cmd2_ext_test'],
- python_requires='>=3.6',
+ python_requires='>=3.7',
install_requires=['cmd2 >= 2, <3'],
setup_requires=['setuptools >= 42', 'setuptools_scm >= 3.4'],
classifiers=[
@@ -43,11 +43,11 @@ setuptools.setup(
'Topic :: Software Development :: Libraries :: Python Modules',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
- 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
+ 'Programming Language :: Python :: 3.11',
],
# dependencies for development and testing
# $ pip install -e .[dev]
diff --git a/plugins/ext_test/tasks.py b/plugins/ext_test/tasks.py
index 757dfe79..6370af0c 100644
--- a/plugins/ext_test/tasks.py
+++ b/plugins/ext_test/tasks.py
@@ -17,6 +17,7 @@ import invoke
TASK_ROOT = pathlib.Path(__file__).resolve().parent
TASK_ROOT_STR = str(TASK_ROOT)
+
# shared function
def rmrf(items, verbose=True):
"""Silently remove a list of directories or files"""
diff --git a/plugins/template/README.md b/plugins/template/README.md
index 509fa0c0..f7f6d057 100644
--- a/plugins/template/README.md
+++ b/plugins/template/README.md
@@ -233,8 +233,6 @@ If you prefer to create these virtualenvs by hand, do the following:
$ cd cmd2_abbrev
$ pyenv install 3.7.0
$ pyenv virtualenv -p python3.7 3.7.0 cmd2-3.7
-$ pyenv install 3.6.5
-$ pyenv virtualenv -p python3.6 3.6.5 cmd2-3.6
$ pyenv install 3.8.5
$ pyenv virtualenv -p python3.8 3.8.5 cmd2-3.8
$ pyenv install 3.9.0
@@ -243,7 +241,7 @@ $ pyenv virtualenv -p python3.9 3.9.0 cmd2-3.9
Now set pyenv to make all three of those available at the same time:
```
-$ pyenv local cmd2-3.7 cmd2-3.6 cmd2-3.8 cmd2-3.9
+$ pyenv local cmd2-3.7 cmd2-3.8 cmd2-3.9
```
Whether you ran the script, or did it by hand, you now have isolated virtualenvs
@@ -253,16 +251,10 @@ utilize.
| Command | python | virtualenv |
| ----------- | ------ | ---------- |
-| `python` | 3.7.0 | cmd2-3.6 |
-| `python3` | 3.7.0 | cmd2-3.6 |
| `python3.7` | 3.7.0 | cmd2-3.7 |
-| `python3.6` | 3.6.5 | cmd2-3.6 |
| `python3.8` | 3.8.5 | cmd2-3.8 |
| `python3.9` | 3.9.0 | cmd2-3.9 |
-| `pip` | 3.7.0 | cmd2-3.6 |
-| `pip3` | 3.7.0 | cmd2-3.6 |
| `pip3.7` | 3.7.0 | cmd2-3.7 |
-| `pip3.6` | 3.6.5 | cmd2-3.6 |
| `pip3.8` | 3.8.5 | cmd2-3.8 |
| `pip3.9` | 3.9.0 | cmd2-3.9 |
diff --git a/plugins/template/cmd2_myplugin/__init__.py b/plugins/template/cmd2_myplugin/__init__.py
index daa20e71..394b219b 100644
--- a/plugins/template/cmd2_myplugin/__init__.py
+++ b/plugins/template/cmd2_myplugin/__init__.py
@@ -14,7 +14,7 @@ try:
# For python 3.8 and later
import importlib.metadata as importlib_metadata
except ImportError: # pragma: no cover
- # For everyone else
+ # Remove this import when we no longer support Python 3.7
import importlib_metadata
try:
__version__ = importlib_metadata.version(__name__)
diff --git a/plugins/template/setup.py b/plugins/template/setup.py
index 5fab9f06..47cc72c2 100644
--- a/plugins/template/setup.py
+++ b/plugins/template/setup.py
@@ -24,7 +24,7 @@ setuptools.setup(
url='https://github.com/python-cmd2/cmd2-plugin-template',
license='MIT',
packages=['cmd2_myplugin'],
- python_requires='>=3.6',
+ python_requires='>=3.7',
install_requires=['cmd2 >= 2, <3'],
setup_requires=['setuptools_scm'],
classifiers=[
@@ -34,7 +34,6 @@ setuptools.setup(
'Topic :: Software Development :: Libraries :: Python Modules',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
- 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
diff --git a/setup.py b/setup.py
index 591ba9f8..b5724dd4 100755
--- a/setup.py
+++ b/setup.py
@@ -28,11 +28,11 @@ Intended Audience :: System Administrators
License :: OSI Approved :: MIT License
Programming Language :: Python
Programming Language :: Python :: 3
-Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
+Programming Language :: Python :: 3.11
Programming Language :: Python :: Implementation :: CPython
Topic :: Software Development :: Libraries :: Python Modules
""".splitlines(),
@@ -43,7 +43,6 @@ Topic :: Software Development :: Libraries :: Python Modules
SETUP_REQUIRES = ['setuptools >= 34.4', 'setuptools_scm >= 3.0']
INSTALL_REQUIRES = [
- 'attrs >= 16.3.0',
'importlib_metadata>=1.6.0;python_version<"3.8"',
'pyperclip >= 1.6',
'typing_extensions; python_version<"3.8"',
@@ -104,7 +103,7 @@ setup(
package_data=PACKAGE_DATA,
packages=['cmd2'],
keywords='command prompt console cmd',
- python_requires='>=3.6',
+ python_requires='>=3.7',
setup_requires=SETUP_REQUIRES,
install_requires=INSTALL_REQUIRES,
extras_require=EXTRAS_REQUIRE,
diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py
index d425baa8..36f2fce5 100644
--- a/tests/test_argparse_custom.py
+++ b/tests/test_argparse_custom.py
@@ -184,7 +184,6 @@ def test_apcustom_narg_tuple_one_base():
# noinspection PyUnresolvedReferences
def test_apcustom_narg_tuple_other_ranges():
-
# Test range with no upper bound on max
parser = Cmd2ArgumentParser()
arg = parser.add_argument('arg', nargs=(2,))
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index 59b8905b..6fe67d20 100755
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -3,7 +3,8 @@
"""
Test the parsing logic in parsing.py
"""
-import attr
+import dataclasses
+
import pytest
import cmd2
@@ -939,9 +940,9 @@ def test_statement_is_immutable():
assert string == statement
assert statement.args == statement
assert statement.raw == ''
- with pytest.raises(attr.exceptions.FrozenInstanceError):
+ with pytest.raises(dataclasses.FrozenInstanceError):
statement.args = 'bar'
- with pytest.raises(attr.exceptions.FrozenInstanceError):
+ with pytest.raises(dataclasses.FrozenInstanceError):
statement.raw = 'baz'
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index f5ca653e..39c87532 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -30,7 +30,6 @@ from .conftest import (
class CmdLineApp(cmd2.Cmd):
-
MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh']
MUMBLE_FIRST = ['so', 'like', 'well']
MUMBLE_LAST = ['right?']
diff --git a/tests/test_utils.py b/tests/test_utils.py
index cfdf07b0..806cfc7d 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -7,6 +7,9 @@ import os
import signal
import sys
import time
+from unittest import (
+ mock,
+)
import pytest
@@ -18,14 +21,6 @@ from cmd2.constants import (
HORIZONTAL_ELLIPSIS,
)
-try:
- import mock
-except ImportError:
- from unittest import (
- mock,
- )
-
-
HELLO_WORLD = 'Hello, world!'
diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py
index 522a99e1..cfe8090d 100644
--- a/tests_isolated/test_commandset/test_commandset.py
+++ b/tests_isolated/test_commandset/test_commandset.py
@@ -186,7 +186,6 @@ def test_custom_construct_commandsets():
def test_load_commands(command_sets_manual, capsys):
-
# now install a command set and verify the commands are now present
cmd_set = CommandSetA()
@@ -454,7 +453,6 @@ class LoadableVegetables(cmd2.CommandSet):
def test_subcommands(command_sets_manual):
-
base_cmds = LoadableBase(1)
badbase_cmds = LoadableBadBase(1)
fruit_cmds = LoadableFruits(1)