diff options
author | kotfu <kotfu@kotfu.net> | 2023-02-01 14:08:49 -0700 |
---|---|---|
committer | kotfu <kotfu@kotfu.net> | 2023-02-01 14:08:49 -0700 |
commit | a95b8b3cbe92d44a0eec9866a5ead892e097514a (patch) | |
tree | aef6be03994e51db9eb3e74efbc1b854fd167826 | |
parent | 95b27c12e849c732c3d05cf839af2076d87e8ed4 (diff) | |
parent | ee7599f9ac0dbb6ce3793f6b665ba1200d3ef9a3 (diff) | |
download | cmd2-git-a95b8b3cbe92d44a0eec9866a5ead892e097514a.tar.gz |
Merge branch 'master' into clipboard
# Conflicts:
# CHANGELOG.md
35 files changed, 86 insertions, 121 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: @@ -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 587237b4..f23931da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## 2.5.0 (TBD) +* Breaking Change + * `cmd2` 2.5 supports Python 3.7+ (removed support for Python 3.6) * Enhancements - * add `allow_clipboard` initialization parameter and attribute to disable ability to + * Removed dependency on `attrs` and replaced with [dataclasses](https://docs.python.org/3/library/dataclasses.html) add output to the operating system clipboard ## 2.4.3 (January 27, 2023) @@ -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" @@ -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 a3cd9045..cf109fa8 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 @@ -1500,7 +1498,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. @@ -1518,10 +1515,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): @@ -1607,7 +1602,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)) @@ -1676,7 +1670,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 @@ -1768,7 +1761,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: @@ -1828,7 +1820,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: @@ -1982,7 +1973,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] @@ -2007,7 +1997,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) @@ -2022,7 +2011,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. @@ -2105,7 +2093,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 @@ -3076,7 +3064,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. @@ -3110,7 +3097,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) @@ -3891,7 +3877,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: @@ -5043,7 +5029,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): @@ -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', @@ -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) |