diff --git a/cmd2/__init__.py b/cmd2/__init__.py index e9c9a5c6..bdb72bd2 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -1,14 +1,12 @@ """This simply imports certain things for backwards compatibility.""" import argparse +import contextlib import importlib.metadata as importlib_metadata import sys -try: +with contextlib.suppress(importlib_metadata.PackageNotFoundError): __version__ = importlib_metadata.version(__name__) -except importlib_metadata.PackageNotFoundError: # pragma: no cover - # package is not installed - pass from .ansi import ( Bg, diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 420fd644..9bbaeed4 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -92,11 +92,7 @@ def _looks_like_flag(token: str, parser: argparse.ArgumentParser) -> bool: return False # Flags can't have a space - if ' ' in token: - return False - - # Starts like a flag - return True + return ' ' not in token class _ArgumentState: @@ -368,34 +364,30 @@ def update_mutex_groups(arg_action: argparse.Action) -> None: # Otherwise treat as a positional argument else: # If we aren't current tracking a positional, then get the next positional arg to handle this token - if pos_arg_state is None: - # Make sure we are still have positional arguments to parse - if remaining_positionals: - action = remaining_positionals.popleft() - - # Are we at a subcommand? If so, forward to the matching completer - if action == self._subcommand_action: - if token in self._subcommand_action.choices: - # Merge self._parent_tokens and consumed_arg_values - parent_tokens = {**self._parent_tokens, **consumed_arg_values} - - # Include the subcommand name if its destination was set - if action.dest != argparse.SUPPRESS: - parent_tokens[action.dest] = [token] - - parser: argparse.ArgumentParser = self._subcommand_action.choices[token] - completer_type = self._cmd2_app._determine_ap_completer_type(parser) - - completer = completer_type(parser, self._cmd2_app, parent_tokens=parent_tokens) - - return completer.complete( - text, line, begidx, endidx, tokens[token_index + 1 :], cmd_set=cmd_set - ) - # Invalid subcommand entered, so no way to complete remaining tokens - return [] - - # Otherwise keep track of the argument - pos_arg_state = _ArgumentState(action) + if pos_arg_state is None and remaining_positionals: + action = remaining_positionals.popleft() + + # Are we at a subcommand? If so, forward to the matching completer + if action == self._subcommand_action: + if token in self._subcommand_action.choices: + # Merge self._parent_tokens and consumed_arg_values + parent_tokens = {**self._parent_tokens, **consumed_arg_values} + + # Include the subcommand name if its destination was set + if action.dest != argparse.SUPPRESS: + parent_tokens[action.dest] = [token] + + parser: argparse.ArgumentParser = self._subcommand_action.choices[token] + completer_type = self._cmd2_app._determine_ap_completer_type(parser) + + completer = completer_type(parser, self._cmd2_app, parent_tokens=parent_tokens) + + return completer.complete(text, line, begidx, endidx, tokens[token_index + 1 :], cmd_set=cmd_set) + # Invalid subcommand entered, so no way to complete remaining tokens + return [] + + # Otherwise keep track of the argument + pos_arg_state = _ArgumentState(action) # Check if we have a positional to consume this token if pos_arg_state is not None: diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 67599537..a670d97e 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -828,12 +828,8 @@ def _add_argument_wrapper( def _get_nargs_pattern_wrapper(self: argparse.ArgumentParser, action: argparse.Action) -> str: # Wrapper around ArgumentParser._get_nargs_pattern behavior to support nargs ranges nargs_range = action.get_nargs_range() # type: ignore[attr-defined] - if nargs_range is not None: - if nargs_range[1] == constants.INFINITY: - range_max = '' - else: - range_max = nargs_range[1] # type: ignore[assignment] - + if nargs_range: + range_max = '' if nargs_range[1] == constants.INFINITY else nargs_range[1] nargs_pattern = f'(-*A{{{nargs_range[0]},{range_max}}}-*)' # if this is an optional action, -- is not allowed @@ -1265,14 +1261,12 @@ def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction: # type: def error(self, message: str) -> NoReturn: """Custom override that applies custom formatting to the error message.""" lines = message.split('\n') - linum = 0 formatted_message = '' - for line in lines: + for linum, line in enumerate(lines): if linum == 0: formatted_message = 'Error: ' + line else: formatted_message += '\n ' + line - linum += 1 self.print_usage(sys.stderr) formatted_message = ansi.style_error(formatted_message) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 3d760436..6f965c4b 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -30,6 +30,7 @@ # setting is True import argparse import cmd +import contextlib import copy import functools import glob @@ -48,10 +49,6 @@ namedtuple, ) from collections.abc import Callable, Iterable, Mapping -from contextlib import ( - redirect_stdout, - suppress, -) from types import ( FrameType, ModuleType, @@ -123,10 +120,8 @@ ) # NOTE: When using gnureadline with Python 3.13, start_ipython needs to be imported before any readline-related stuff -try: +with contextlib.suppress(ImportError): from IPython import start_ipython # type: ignore[import] -except ImportError: - pass from .rl_utils import ( RlType, @@ -1098,9 +1093,8 @@ def add_settable(self, settable: Settable) -> None: :param settable: Settable object being added """ - if not self.always_prefix_settables: - if settable.name in self.settables and settable.name not in self._settables: - raise KeyError(f'Duplicate settable: {settable.name}') + if not self.always_prefix_settables and settable.name in self.settables and settable.name not in self._settables: + raise KeyError(f'Duplicate settable: {settable.name}') self._settables[settable.name] = settable def remove_settable(self, name: str) -> None: @@ -1306,7 +1300,7 @@ def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False) -> None: # Don't try to use the pager when being run by a continuous integration system like Jenkins + pexpect. functional_terminal = False - if self.stdin.isatty() and self.stdout.isatty(): + if self.stdin.isatty() and self.stdout.isatty(): # noqa: SIM102 if sys.platform.startswith('win') or os.environ.get('TERM') is not None: functional_terminal = True @@ -2844,8 +2838,8 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: read_fd, write_fd = os.pipe() # Open each side of the pipe - subproc_stdin = open(read_fd) - new_stdout: TextIO = cast(TextIO, open(write_fd, 'w')) + subproc_stdin = open(read_fd) # noqa: SIM115 + new_stdout: TextIO = cast(TextIO, open(write_fd, 'w')) # noqa: SIM115 # Create pipe process in a separate group to isolate our signals from it. If a Ctrl-C event occurs, # our sigint handler will forward it only to the most recent pipe process. This makes sure pipe @@ -2875,7 +2869,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: # like: !ls -l | grep user | wc -l > out.txt. But this makes it difficult to know if the pipe process # started OK, since the shell itself always starts. Therefore, we will wait a short time and check # if the pipe process is still running. - with suppress(subprocess.TimeoutExpired): + with contextlib.suppress(subprocess.TimeoutExpired): proc.wait(0.2) # Check if the pipe process already exited @@ -2894,7 +2888,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: mode = 'a' if statement.output == constants.REDIRECTION_APPEND else 'w' try: # Use line buffering - new_stdout = cast(TextIO, open(utils.strip_quotes(statement.output_to), mode=mode, buffering=1)) + new_stdout = cast(TextIO, open(utils.strip_quotes(statement.output_to), mode=mode, buffering=1)) # noqa: SIM115 except OSError as ex: raise RedirectionError(f'Failed to redirect because: {ex}') @@ -2915,7 +2909,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: # no point opening up the temporary file current_paste_buffer = get_paste_buffer() # create a temporary file to store output - new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+")) + new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+")) # noqa: SIM115 redir_saved_state.redirecting = True sys.stdout = self.stdout = new_stdout @@ -2941,11 +2935,9 @@ def _restore_output(self, statement: Statement, saved_redir_state: utils.Redirec self.stdout.seek(0) write_to_paste_buffer(self.stdout.read()) - try: + with contextlib.suppress(BrokenPipeError): # Close the file or pipe that stdout was redirected to self.stdout.close() - except BrokenPipeError: - pass # Restore the stdout values self.stdout = cast(TextIO, saved_redir_state.saved_self_stdout) @@ -3199,11 +3191,9 @@ def _read_command_line(self, prompt: str) -> str: """ try: # Wrap in try since terminal_lock may not be locked - try: + with contextlib.suppress(RuntimeError): # Command line is about to be drawn. Allow asynchronous changes to the terminal. self.terminal_lock.release() - except RuntimeError: - pass return self.read_input(prompt, completion_mode=utils.CompletionMode.COMMANDS) except EOFError: return 'eof' @@ -3948,7 +3938,7 @@ def _print_topics(self, header: str, cmds: list[str], verbose: bool) -> None: result = io.StringIO() # try to redirect system stdout - with redirect_stdout(result): + with contextlib.redirect_stdout(result): # save our internal stdout stdout_orig = self.stdout try: @@ -4245,7 +4235,7 @@ def _reset_py_display() -> None: # Delete any prompts that have been set attributes = ['ps1', 'ps2', 'ps3'] for cur_attr in attributes: - with suppress(KeyError): + with contextlib.suppress(KeyError): del sys.__dict__[cur_attr] # Reset functions @@ -4423,7 +4413,7 @@ def py_quit() -> None: # Check if we are running Python code if py_code_to_run: - try: + try: # noqa: SIM105 interp.runcode(py_code_to_run) # type: ignore[arg-type] except BaseException: # noqa: BLE001, S110 # We don't care about any exception that happened in the Python code @@ -4646,7 +4636,7 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]: self.last_result = False # -v must be used alone with no other options - if args.verbose: + if args.verbose: # noqa: SIM102 if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script: self.poutput("-v cannot be used with any other options") self.poutput(self.history_parser.format_usage()) diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py index e765f28a..47395bb1 100644 --- a/cmd2/rl_utils.py +++ b/cmd2/rl_utils.py @@ -1,5 +1,6 @@ """Imports the proper Readline for the platform and provides utility functions for it.""" +import contextlib import sys from enum import ( Enum, @@ -27,10 +28,8 @@ import gnureadline as readline # type: ignore[import] except ImportError: # Note: If this actually fails, you should install gnureadline on Linux/Mac or pyreadline3 on Windows. - try: + with contextlib.suppress(ImportError): import readline # type: ignore[no-redef] - except ImportError: # pragma: no cover - pass class RlType(Enum): diff --git a/cmd2/transcript.py b/cmd2/transcript.py index 73e65f98..9d376d8e 100644 --- a/cmd2/transcript.py +++ b/cmd2/transcript.py @@ -64,9 +64,8 @@ def _fetch_transcripts(self) -> None: self.transcripts = {} testfiles = cast(list[str], getattr(self.cmdapp, 'testfiles', [])) for fname in testfiles: - tfile = open(fname) - self.transcripts[fname] = iter(tfile.readlines()) - tfile.close() + with open(fname) as tfile: + self.transcripts[fname] = iter(tfile.readlines()) def _test_transcript(self, fname: str, transcript: Iterator[str]) -> None: if self.cmdapp is None: diff --git a/cmd2/utils.py b/cmd2/utils.py index d58a2b53..86df8e01 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -2,6 +2,7 @@ import argparse import collections +import contextlib import functools import glob import inspect @@ -646,11 +647,9 @@ def _write_bytes(stream: Union[StdSim, TextIO], to_write: Union[bytes, str]) -> if isinstance(to_write, str): to_write = to_write.encode() - try: + # BrokenPipeError can occur if output is being piped to a process that closed + with contextlib.suppress(BrokenPipeError): stream.buffer.write(to_write) - except BrokenPipeError: - # This occurs if output is being piped to a process that closed - pass class ContextFlag: @@ -877,12 +876,8 @@ def align_text( line_styles = list(get_styles_dict(line).values()) # Calculate how wide each side of filling needs to be - if line_width >= width: - # Don't return here even though the line needs no fill chars. - # There may be styles sequences to restore. - total_fill_width = 0 - else: - total_fill_width = width - line_width + total_fill_width = 0 if line_width >= width else width - line_width + # Even if the line needs no fill chars, there may be styles sequences to restore if alignment == TextAlignment.LEFT: left_fill_width = 0 diff --git a/examples/custom_parser.py b/examples/custom_parser.py index c814a299..a79a65b8 100644 --- a/examples/custom_parser.py +++ b/examples/custom_parser.py @@ -19,14 +19,12 @@ def __init__(self, *args, **kwargs) -> None: def error(self, message: str) -> None: """Custom override that applies custom formatting to the error message.""" lines = message.split('\n') - linum = 0 formatted_message = '' - for line in lines: + for linum, line in enumerate(lines): if linum == 0: formatted_message = 'Error: ' + line else: formatted_message += '\n ' + line - linum += 1 self.print_usage(sys.stderr) diff --git a/examples/scripts/save_help_text.py b/examples/scripts/save_help_text.py index a1e2cdd2..41636b08 100644 --- a/examples/scripts/save_help_text.py +++ b/examples/scripts/save_help_text.py @@ -60,7 +60,8 @@ def main() -> None: # Open the output file outfile_path = os.path.expanduser(sys.argv[1]) try: - outfile = open(outfile_path, 'w') + with open(outfile_path, 'w') as outfile: + pass except OSError as e: print(f"Error opening {outfile_path} because: {e}") return diff --git a/plugins/ext_test/tasks.py b/plugins/ext_test/tasks.py index 31fc3f4f..b7f36937 100644 --- a/plugins/ext_test/tasks.py +++ b/plugins/ext_test/tasks.py @@ -6,6 +6,7 @@ - setuptools >= 39.1.0 """ +import contextlib import os import pathlib import shutil @@ -27,10 +28,8 @@ def rmrf(items, verbose=True): print(f"Removing {item}") shutil.rmtree(item, ignore_errors=True) # rmtree doesn't remove bare files - try: + with contextlib.suppress(FileNotFoundError): os.remove(item) - except FileNotFoundError: - pass # create namespaces diff --git a/pyproject.toml b/pyproject.toml index 0ff6ffa3..e25d1562 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -206,8 +206,8 @@ select = [ "RET", # flake8-return (various warnings related to implicit vs explicit return statements) "RSE", # flake8-raise (warn about unnecessary parentheses on raised exceptions) # "RUF", # Ruff-specific rules (miscellaneous grab bag of lint checks specific to Ruff) - "S", # flake8-bandit (security oriented checks, but extremely pedantic - do not attempt to apply to unit test files) - # "SIM", # flake8-simplify (rules to attempt to simplify code) + "S", # flake8-bandit (security oriented checks, but extremely pedantic - do not attempt to apply to unit test files) + "SIM", # flake8-simplify (rules to attempt to simplify code) # "SLF", # flake8-self (warn when protected members are accessed outside of a class or file) "SLOT", # flake8-slots (warn about subclasses that should define __slots__) "T10", # flake8-debugger (check for pdb traces left in Python code) diff --git a/tests/conftest.py b/tests/conftest.py index 026b62ef..d9b48e4e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -178,9 +178,12 @@ def get_endidx(): return endidx # Run the readline tab completion function with readline mocks in place - with mock.patch.object(readline, 'get_line_buffer', get_line), mock.patch.object(readline, 'get_begidx', get_begidx): - with mock.patch.object(readline, 'get_endidx', get_endidx): - return app.complete(text, 0) + with ( + mock.patch.object(readline, 'get_line_buffer', get_line), + mock.patch.object(readline, 'get_begidx', get_begidx), + mock.patch.object(readline, 'get_endidx', get_endidx), + ): + return app.complete(text, 0) def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> argparse.ArgumentParser: diff --git a/tests/test_utils.py b/tests/test_utils.py index bb05093e..259e4f11 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -247,20 +247,20 @@ def test_stdsim_line_buffering(base_app) -> None: import os import tempfile - file = tempfile.NamedTemporaryFile(mode='wt') - file.line_buffering = True + with tempfile.NamedTemporaryFile(mode='wt') as file: + file.line_buffering = True - stdsim = cu.StdSim(file, echo=True) - saved_size = os.path.getsize(file.name) + stdsim = cu.StdSim(file, echo=True) + saved_size = os.path.getsize(file.name) - bytes_to_write = b'hello\n' - stdsim.buffer.write(bytes_to_write) - assert os.path.getsize(file.name) == saved_size + len(bytes_to_write) - saved_size = os.path.getsize(file.name) + bytes_to_write = b'hello\n' + stdsim.buffer.write(bytes_to_write) + assert os.path.getsize(file.name) == saved_size + len(bytes_to_write) + saved_size = os.path.getsize(file.name) - bytes_to_write = b'hello\r' - stdsim.buffer.write(bytes_to_write) - assert os.path.getsize(file.name) == saved_size + len(bytes_to_write) + bytes_to_write = b'hello\r' + stdsim.buffer.write(bytes_to_write) + assert os.path.getsize(file.name) == saved_size + len(bytes_to_write) @pytest.fixture diff --git a/tests_isolated/test_commandset/conftest.py b/tests_isolated/test_commandset/conftest.py index d5853de4..171f4a29 100644 --- a/tests_isolated/test_commandset/conftest.py +++ b/tests_isolated/test_commandset/conftest.py @@ -161,9 +161,12 @@ def get_endidx(): return endidx # Run the readline tab completion function with readline mocks in place - with mock.patch.object(readline, 'get_line_buffer', get_line), mock.patch.object(readline, 'get_begidx', get_begidx): - with mock.patch.object(readline, 'get_endidx', get_endidx): - return app.complete(text, 0) + with ( + mock.patch.object(readline, 'get_line_buffer', get_line), + mock.patch.object(readline, 'get_begidx', get_begidx), + mock.patch.object(readline, 'get_endidx', get_endidx), + ): + return app.complete(text, 0) class WithCommandSets(ExternalTestMixin, cmd2.Cmd):