From bf45ff7fb8a37e0d459cefb0c123930d0989ecdd Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sun, 25 May 2025 00:42:54 -0400 Subject: [PATCH 1/4] Start of enabling ruff B ruleset --- cmd2/argparse_custom.py | 18 +++---- cmd2/cmd2.py | 52 ++++++++++--------- cmd2/command_definition.py | 4 +- cmd2/decorators.py | 8 +-- cmd2/parsing.py | 4 +- cmd2/rl_utils.py | 4 +- cmd2/table_creator.py | 4 +- cmd2/utils.py | 2 +- examples/async_printing.py | 2 +- examples/cmd_as_argument.py | 4 +- examples/decorator_example.py | 2 +- pyproject.toml | 2 +- tasks.py | 2 +- tests/pyscript/raises_exception.py | 2 +- tests/test_argparse.py | 6 +-- tests/test_cmd2.py | 6 +-- tests/test_completion.py | 2 +- tests/test_utils.py | 2 +- .../test_argparse_subcommands.py | 2 +- .../test_commandset/test_commandset.py | 2 +- 20 files changed, 66 insertions(+), 64 deletions(-) diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 6b2a0c22..cd18e7ab 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -640,15 +640,15 @@ def register_argparse_argument_parameter(param_name: str, param_type: Optional[t getter_name = f'get_{param_name}' def _action_get_custom_parameter(self: argparse.Action) -> Any: - f""" - Get the custom {param_name} attribute of an argparse Action. + """ + Get the custom attribute of an argparse Action. - This function is added by cmd2 as a method called ``{getter_name}()`` to ``argparse.Action`` class. + This function is added by cmd2 as a method called ``get_()`` to ``argparse.Action`` class. - To call: ``action.{getter_name}()`` + To call: ``action.get_()`` :param self: argparse Action being queried - :return: The value of {param_name} or None if attribute does not exist + :return: The value of the custom attribute or None if attribute does not exist """ return getattr(self, attr_name, None) @@ -657,12 +657,12 @@ def _action_get_custom_parameter(self: argparse.Action) -> Any: setter_name = f'set_{param_name}' def _action_set_custom_parameter(self: argparse.Action, value: Any) -> None: - f""" - Set the custom {param_name} attribute of an argparse Action. + """ + Set the custom attribute of an argparse Action. - This function is added by cmd2 as a method called ``{setter_name}()`` to ``argparse.Action`` class. + This function is added by cmd2 as a method called ``set_()`` to ``argparse.Action`` class. - To call: ``action.{setter_name}({param_name})`` + To call: ``action.set_()`` :param self: argparse Action being updated :param value: value being assigned diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 986b0e4e..8e230a0b 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -57,6 +57,7 @@ IO, TYPE_CHECKING, Any, + ClassVar, Optional, TextIO, TypeVar, @@ -297,6 +298,9 @@ class Cmd(cmd.Cmd): ALPHABETICAL_SORT_KEY = utils.norm_fold NATURAL_SORT_KEY = utils.natural_keys + # List for storing transcript test file names + testfiles: ClassVar[list[str]] = [] + def __init__( self, completekey: str = 'tab', @@ -401,7 +405,7 @@ def __init__( # The maximum number of CompletionItems to display during tab completion. If the number of completion # suggestions exceeds this number, they will be displayed in the typical columnized format and will # not include the description value of the CompletionItems. - self.max_completion_items = 50 + self.max_completion_items: int = 50 # A dictionary mapping settable names to their Settable instance self._settables: dict[str, Settable] = {} @@ -414,7 +418,7 @@ def __init__( self.build_settables() # Use as prompt for multiline commands on the 2nd+ line of input - self.continuation_prompt = '> ' + self.continuation_prompt: str = '> ' # Allow access to your application in embedded Python shells and scripts py via self self.self_in_py = False @@ -445,7 +449,7 @@ def __init__( # True if running inside a Python shell or pyscript, False otherwise self._in_py = False - self.statement_parser = StatementParser( + self.statement_parser: StatementParser = StatementParser( terminators=terminators, multiline_commands=multiline_commands, shortcuts=shortcuts ) @@ -456,7 +460,7 @@ def __init__( self._script_dir: list[str] = [] # Context manager used to protect critical sections in the main thread from stopping due to a KeyboardInterrupt - self.sigint_protection = utils.ContextFlag() + self.sigint_protection: utils.ContextFlag = utils.ContextFlag() # If the current command created a process to pipe to, then this will be a ProcReader object. # Otherwise it will be None. It's used to know when a pipe process can be killed and/or waited upon. @@ -551,7 +555,7 @@ def __init__( # command and category names # alias, macro, settable, and shortcut names # tab completion results when self.matches_sorted is False - self.default_sort_key = Cmd.ALPHABETICAL_SORT_KEY + self.default_sort_key: Callable[[str], str] = Cmd.ALPHABETICAL_SORT_KEY ############################################################################################################ # The following variables are used by tab completion functions. They are reset each time complete() is run @@ -567,14 +571,14 @@ def __init__( self.allow_closing_quote = True # An optional hint which prints above tab completion suggestions - self.completion_hint = '' + self.completion_hint: str = '' # Normally cmd2 uses readline's formatter to columnize the list of completion suggestions. # If a custom format is preferred, write the formatted completions to this string. cmd2 will # then print it instead of the readline format. ANSI style sequences and newlines are supported # when using this value. Even when using formatted_completions, the full matches must still be returned # from your completer function. ArgparseCompleter writes its tab completion tables to this string. - self.formatted_completions = '' + self.formatted_completions: str = '' # Used by complete() for readline tab completion self.completion_matches: list[str] = [] @@ -594,10 +598,10 @@ def __init__( # Set to True before returning matches to complete() in cases where matches have already been sorted. # If False, then complete() will sort the matches using self.default_sort_key before they are displayed. # This does not affect self.formatted_completions. - self.matches_sorted = False + self.matches_sorted: bool = False # Command parsers for this Cmd instance. - self._command_parsers = _CommandParsers(self) + self._command_parsers: _CommandParsers = _CommandParsers(self) # Add functions decorated to be subcommands self._register_subcommands(self) @@ -915,7 +919,7 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: ) # iterate through all matching methods - for method_name, method in methods: + for _method_name, method in methods: subcommand_name: str = getattr(method, constants.SUBCMD_ATTR_NAME) full_command_name: str = getattr(method, constants.SUBCMD_ATTR_COMMAND) subcmd_parser_builder = getattr(method, constants.CMD_ATTR_ARGPARSER) @@ -952,7 +956,7 @@ def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> if choice_name == cur_subcmd: return find_subcommand(choice, subcmd_names) break - raise CommandSetRegistrationError(f"Could not find subcommand '{full_command_name}'") + raise CommandSetRegistrationError(f"Could not find subcommand '{action}'") target_parser = find_subcommand(command_parser, subcommand_names) @@ -1021,7 +1025,7 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: ) # iterate through all matching methods - for method_name, method in methods: + for _method_name, method in methods: subcommand_name = getattr(method, constants.SUBCMD_ATTR_NAME) command_name = getattr(method, constants.SUBCMD_ATTR_COMMAND) @@ -1105,8 +1109,8 @@ def remove_settable(self, name: str) -> None: """ try: del self._settables[name] - except KeyError: - raise KeyError(name + " is not a settable parameter") + except KeyError as exc: + raise KeyError(name + " is not a settable parameter") from exc def build_settables(self) -> None: """Create the dictionary of user-settable parameters.""" @@ -1119,11 +1123,11 @@ def allow_style_type(value: str) -> ansi.AllowStyle: """Converts a string value into an ansi.AllowStyle.""" try: return ansi.AllowStyle[value.upper()] - except KeyError: + except KeyError as esc: raise ValueError( f"must be {ansi.AllowStyle.ALWAYS}, {ansi.AllowStyle.NEVER}, or " f"{ansi.AllowStyle.TERMINAL} (case-insensitive)" - ) + ) from esc self.add_settable( Settable( @@ -2561,7 +2565,7 @@ def onecmd_plus_hooks( self.exit_code = ex.code stop = True except PassThroughException as ex: - raise ex.wrapped_ex + raise ex.wrapped_ex from None except Exception as ex: # noqa: BLE001 self.pexcept(ex) finally: @@ -2575,7 +2579,7 @@ def onecmd_plus_hooks( self.exit_code = ex.code stop = True except PassThroughException as ex: - raise ex.wrapped_ex + raise ex.wrapped_ex from None except Exception as ex: # noqa: BLE001 self.pexcept(ex) @@ -2896,7 +2900,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: # Use line buffering 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}') + raise RedirectionError('Failed to redirect output') from ex redir_saved_state.redirecting = True sys.stdout = self.stdout = new_stdout @@ -4016,7 +4020,7 @@ def select(self, opts: Union[str, list[str], list[tuple[Any, Optional[str]]]], p """ local_opts: Union[list[str], list[tuple[Any, Optional[str]]]] if isinstance(opts, str): - local_opts = cast(list[tuple[Any, Optional[str]]], list(zip(opts.split(), opts.split()))) + local_opts = cast(list[tuple[Any, Optional[str]]], list(zip(opts.split(), opts.split(), strict=False))) else: local_opts = opts fulloptions: list[tuple[Any, Optional[str]]] = [] @@ -4059,8 +4063,8 @@ def complete_set_value( param = arg_tokens['param'][0] try: settable = self.settables[param] - except KeyError: - raise CompletionError(param + " is not a settable parameter") + except KeyError as exc: + raise CompletionError(param + " is not a settable parameter") from exc # Create a parser with a value field based on this settable settable_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(parents=[Cmd.set_parser_parent]) @@ -4528,7 +4532,7 @@ def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover # Allow users to install ipython from a cmd2 prompt when needed and still have ipy command work try: - start_ipython # noqa: F823 + _dummy = start_ipython # noqa: F823 except NameError: from IPython import start_ipython # type: ignore[import] @@ -5145,7 +5149,7 @@ class TestMyAppCase(Cmd2TestCase): self.poutput(f'cmd2 app: {sys.argv[0]}') self.poutput(ansi.style(f'collected {num_transcripts} transcript{plural}', bold=True)) - setattr(self.__class__, 'testfiles', transcripts_expanded) + self.__class__.testfiles = transcripts_expanded sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main() testcase = TestMyAppCase() stream = cast(TextIO, utils.StdSim(sys.stderr)) diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py index 941fe033..2d99bbc5 100644 --- a/cmd2/command_definition.py +++ b/cmd2/command_definition.py @@ -172,8 +172,8 @@ def remove_settable(self, name: str) -> None: """ try: del self._settables[name] - except KeyError: - raise KeyError(name + " is not a settable parameter") + except KeyError as exc: + raise KeyError(name + " is not a settable parameter") from exc def sigint_handler(self) -> bool: """Handle a SIGINT that occurred for a command in this CommandSet. diff --git a/cmd2/decorators.py b/cmd2/decorators.py index ca9fdba2..da5ae0de 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -369,15 +369,15 @@ def cmd_wrapper(*args: Any, **kwargs: dict[str, Any]) -> Optional[bool]: else: new_args = (arg_parser.parse_args(parsed_arglist, namespace),) ns = new_args[0] - except SystemExit: - raise Cmd2ArgparseError + except SystemExit as exc: + raise Cmd2ArgparseError from exc else: # Add wrapped statement to Namespace as cmd2_statement - setattr(ns, 'cmd2_statement', Cmd2AttributeWrapper(statement)) + ns.cmd2_statement = Cmd2AttributeWrapper(statement) # Add wrapped subcmd handler (which can be None) to Namespace as cmd2_handler handler = getattr(ns, constants.NS_ATTR_SUBCMD_HANDLER, None) - setattr(ns, 'cmd2_handler', Cmd2AttributeWrapper(handler)) + ns.cmd2_handler = Cmd2AttributeWrapper(handler) # Remove the subcmd handler attribute from the Namespace # since cmd2_handler is how a developer accesses it. diff --git a/cmd2/parsing.py b/cmd2/parsing.py index cc18da1d..29bb7da0 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -232,7 +232,7 @@ def from_dict(source_dict: dict[str, Any]) -> 'Statement': try: value = source_dict[Statement._args_field] except KeyError as ex: - raise KeyError(f"Statement dictionary is missing {ex} field") + raise KeyError(f"Statement dictionary is missing {ex} field") from None # Pass the rest at kwargs (minus args) kwargs = source_dict.copy() @@ -377,7 +377,7 @@ def tokenize(self, line: str) -> list[str]: try: tokens = shlex_split(line) except ValueError as ex: - raise Cmd2ShlexError(ex) + raise Cmd2ShlexError(ex) from None # custom lexing return self.split_on_punctuation(tokens) diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py index 47395bb1..0d39ffb9 100644 --- a/cmd2/rl_utils.py +++ b/cmd2/rl_utils.py @@ -101,9 +101,7 @@ def enable_win_vt100(handle: HANDLE) -> bool: # pyreadline3 is incomplete in terms of the Python readline API. Add the missing functions we need. ############################################################################################################ # Add missing `readline.remove_history_item()` - try: - getattr(readline, 'remove_history_item') - except AttributeError: + if not hasattr(readline, 'remove_history_item'): def pyreadline_remove_history_item(pos: int) -> None: """An implementation of remove_history_item() for pyreadline3 diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py index 6f8d0d80..5cd64466 100644 --- a/cmd2/table_creator.py +++ b/cmd2/table_creator.py @@ -498,9 +498,9 @@ def __init__(self) -> None: to_top = line_diff to_bottom = 0 - for i in range(to_top): + for _ in range(to_top): cell.lines.appendleft(padding_line) - for i in range(to_bottom): + for _ in range(to_bottom): cell.lines.append(padding_line) # Build this row one line at a time diff --git a/cmd2/utils.py b/cmd2/utils.py index a520b417..825eae81 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -1133,7 +1133,7 @@ def get_defining_class(meth: Callable[..., Any]) -> Optional[type[Any]]: if isinstance(meth, functools.partial): return get_defining_class(meth.func) if inspect.ismethod(meth) or ( - inspect.isbuiltin(meth) and getattr(meth, '__self__') is not None and getattr(meth.__self__, '__class__') + inspect.isbuiltin(meth) and hasattr(meth, '__self__') and hasattr(meth.__self__, '__class__') ): for cls in inspect.getmro(meth.__self__.__class__): # type: ignore[attr-defined] if meth.__name__ in cls.__dict__: diff --git a/examples/async_printing.py b/examples/async_printing.py index 5399c2f7..5655a62f 100755 --- a/examples/async_printing.py +++ b/examples/async_printing.py @@ -103,7 +103,7 @@ def _get_alerts(self) -> list[str]: if rand_num > 2: return [] - for i in range(rand_num): + for _ in range(rand_num): self._alert_count += 1 alerts.append(f"Alert {self._alert_count}") diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py index 92bab995..dd265074 100755 --- a/examples/cmd_as_argument.py +++ b/examples/cmd_as_argument.py @@ -53,7 +53,7 @@ def do_speak(self, args) -> None: word = word.upper() words.append(word) repetitions = args.repeat or 1 - for i in range(min(repetitions, self.maxrepeats)): + for _ in range(min(repetitions, self.maxrepeats)): # .poutput handles newlines, and accommodates output redirection too self.poutput(' '.join(words)) @@ -68,7 +68,7 @@ def do_speak(self, args) -> None: def do_mumble(self, args) -> None: """Mumbles what you tell me to.""" repetitions = args.repeat or 1 - for i in range(min(repetitions, self.maxrepeats)): + for _ in range(min(repetitions, self.maxrepeats)): output = [] if random.random() < 0.33: output.append(random.choice(self.MUMBLE_FIRST)) diff --git a/examples/decorator_example.py b/examples/decorator_example.py index 10b04471..736c729e 100755 --- a/examples/decorator_example.py +++ b/examples/decorator_example.py @@ -51,7 +51,7 @@ def do_speak(self, args: argparse.Namespace) -> None: word = word.upper() words.append(word) repetitions = args.repeat or 1 - for i in range(min(repetitions, self.maxrepeats)): + for _ in range(min(repetitions, self.maxrepeats)): self.poutput(' '.join(words)) do_say = do_speak # now "say" is a synonym for "speak" diff --git a/pyproject.toml b/pyproject.toml index 36130304..f948ae6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -161,7 +161,7 @@ select = [ "ANN", # flake8-annotations (missing type annotations for arguments or return types) "ARG", # flake8-unused-arguments (functions or methods with arguments that are never used) "ASYNC", # flake8-async (async await bugs) - # "B", # flake8-bugbear (various likely bugs and design issues) + # "B", # flake8-bugbear (various likely bugs and design issues) "BLE", # flake8-blind-except (force more specific exception types than just Exception) "C4", # flake8-comprehensions (warn about things that could be written as a comprehensions but aren't) "C90", # McCabe cyclomatic complexity (warn about functions that are too complex) diff --git a/tasks.py b/tasks.py index bbcf0a72..f6b9d7ff 100644 --- a/tasks.py +++ b/tasks.py @@ -71,7 +71,7 @@ def pytest(context: Context, junit: bool = False, pty: bool = True, base: bool = tests_cmd = command_str + ' tests' context.run(tests_cmd, pty=pty) if isolated: - for root, dirnames, _ in os.walk(str(TASK_ROOT / 'tests_isolated')): + for _root, dirnames, _ in os.walk(str(TASK_ROOT / 'tests_isolated')): for dir_name in dirnames: if dir_name.startswith('test_'): context.run(command_str + ' tests_isolated/' + dir_name) diff --git a/tests/pyscript/raises_exception.py b/tests/pyscript/raises_exception.py index 5595472b..9883a2b8 100644 --- a/tests/pyscript/raises_exception.py +++ b/tests/pyscript/raises_exception.py @@ -1,3 +1,3 @@ """Example demonstrating what happens when a Python script raises an exception""" -1 + 'blue' +x = 1 + 'blue' diff --git a/tests/test_argparse.py b/tests/test_argparse.py index 892ca86b..e03edb37 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -50,7 +50,7 @@ def do_say(self, args, *, keyword_arg: Optional[str] = None) -> None: modified_word = word.upper() words.append(modified_word) repetitions = args.repeat or 1 - for i in range(min(repetitions, self.maxrepeats)): + for _ in range(min(repetitions, self.maxrepeats)): self.stdout.write(' '.join(words)) self.stdout.write('\n') @@ -106,7 +106,7 @@ def do_speak(self, args, extra, *, keyword_arg: Optional[str] = None) -> None: modified_word = word.upper() words.append(modified_word) repetitions = args.repeat or 1 - for i in range(min(repetitions, self.maxrepeats)): + for _ in range(min(repetitions, self.maxrepeats)): self.stdout.write(' '.join(words)) self.stdout.write('\n') @@ -297,7 +297,7 @@ def base_helpless(self, args) -> None: def do_base(self, args) -> None: """Base command help""" # Call whatever subcommand function was selected - func = getattr(args, 'func') + func = args.func func(self, args) # Add subcommands using as_subcommand_to decorator diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 468b1026..730f9030 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -512,7 +512,7 @@ def test_runcmds_plus_hooks_ctrl_c(base_app, capsys) -> None: def do_keyboard_interrupt(self, _) -> NoReturn: raise KeyboardInterrupt('Interrupting this command') - setattr(base_app, 'do_keyboard_interrupt', types.MethodType(do_keyboard_interrupt, base_app)) + base_app.do_keyboard_interrupt = types.MethodType(do_keyboard_interrupt, base_app) # Default behavior is to not stop runcmds_plus_hooks() on Ctrl-C base_app.history.clear() @@ -603,7 +603,7 @@ def test_system_exit_in_command(base_app, capsys) -> None: def do_system_exit(self, _) -> NoReturn: raise SystemExit(exit_code) - setattr(base_app, 'do_system_exit', types.MethodType(do_system_exit, base_app)) + base_app.do_system_exit = types.MethodType(do_system_exit, base_app) stop = base_app.onecmd_plus_hooks('system_exit') assert stop @@ -620,7 +620,7 @@ def do_passthrough(self, _) -> NoReturn: wrapped_ex = OSError(expected_err) raise exceptions.PassThroughException(wrapped_ex=wrapped_ex) - setattr(base_app, 'do_passthrough', types.MethodType(do_passthrough, base_app)) + base_app.do_passthrough = types.MethodType(do_passthrough, base_app) with pytest.raises(OSError, match=expected_err): base_app.onecmd_plus_hooks('passthrough') diff --git a/tests/test_completion.py b/tests/test_completion.py index 2deb6a80..1d9e9256 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -1031,7 +1031,7 @@ class RedirCompType(enum.Enum): def test_redirect_complete(cmd2_app, monkeypatch, line, comp_type) -> None: # Test both cases of allow_redirection cmd2_app.allow_redirection = True - for count in range(2): + for _ in range(2): shell_cmd_complete_mock = mock.MagicMock(name='shell_cmd_complete') monkeypatch.setattr("cmd2.Cmd.shell_cmd_complete", shell_cmd_complete_mock) diff --git a/tests/test_utils.py b/tests/test_utils.py index 259e4f11..efd5aa56 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -208,7 +208,7 @@ def test_stdsim_getattr_exist(stdout_sim) -> None: # Here the StdSim getattr is allowing us to access methods within StdSim my_str = 'Hello World' stdout_sim.write(my_str) - val_func = getattr(stdout_sim, 'getvalue') + val_func = stdout_sim.getvalue assert val_func() == my_str diff --git a/tests_isolated/test_commandset/test_argparse_subcommands.py b/tests_isolated/test_commandset/test_argparse_subcommands.py index f3a0067c..5f4645d5 100644 --- a/tests_isolated/test_commandset/test_argparse_subcommands.py +++ b/tests_isolated/test_commandset/test_argparse_subcommands.py @@ -56,7 +56,7 @@ def base_helpless(self, args) -> None: def do_base(self, args) -> None: """Base command help""" # Call whatever subcommand function was selected - func = getattr(args, 'func') + func = args.func func(self, args) diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py index d26d3c39..7498e145 100644 --- a/tests_isolated/test_commandset/test_commandset.py +++ b/tests_isolated/test_commandset/test_commandset.py @@ -1083,7 +1083,7 @@ def __init__(self) -> None: cmdset = WithSettablesA() arbitrary2 = Arbitrary() app = cmd2.Cmd(command_sets=[cmdset], auto_load_commands=False) - setattr(app, 'str_value', '') + app.str_value = '' app.add_settable(Settable('always_prefix_settables', bool, 'Prefix settables', app)) app._settables['str_value'] = Settable('str_value', str, 'String value', app) From cdc9c353b0dfdc3dc51c698ac130d89d71156327 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sun, 25 May 2025 00:55:21 -0400 Subject: [PATCH 2/4] Finish enabling ruff B rulset for flake8-bugbear Disabled B010 rule for not calling setattr with constant attribute value in cmd2/argparse_custom.py file due to type checking going nuts if we tried to apply it. --- cmd2/argparse_custom.py | 4 ++-- cmd2/cmd2.py | 4 ++-- pyproject.toml | 15 ++++++++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index cd18e7ab..b0a50ab2 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -319,7 +319,7 @@ def __call__(self) -> list[str]: ... # pragma: no cover class ChoicesProviderFuncWithTokens(Protocol): """Function that returns a list of choices in support of tab completion and accepts a dictionary of prior arguments.""" - def __call__(self, *, arg_tokens: dict[str, list[str]] = {}) -> list[str]: ... # pragma: no cover + def __call__(self, *, arg_tokens: dict[str, list[str]]) -> list[str]: ... # pragma: no cover ChoicesProviderFunc = Union[ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens] @@ -351,7 +351,7 @@ def __call__( begidx: int, endidx: int, *, - arg_tokens: dict[str, list[str]] = {}, + arg_tokens: dict[str, list[str]] = {}, # noqa: B006 ) -> list[str]: ... # pragma: no cover diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 8e230a0b..e6a9c9aa 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -377,9 +377,9 @@ def __init__( """ # Check if py or ipy need to be disabled in this instance if not include_py: - setattr(self, 'do_py', None) + setattr(self, 'do_py', None) # noqa: B010 if not include_ipy: - setattr(self, 'do_ipy', None) + setattr(self, 'do_ipy', None) # noqa: B010 # initialize plugin system # needs to be done before we call __init__(0) diff --git a/pyproject.toml b/pyproject.toml index f948ae6b..f0d622cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -161,11 +161,11 @@ select = [ "ANN", # flake8-annotations (missing type annotations for arguments or return types) "ARG", # flake8-unused-arguments (functions or methods with arguments that are never used) "ASYNC", # flake8-async (async await bugs) - # "B", # flake8-bugbear (various likely bugs and design issues) - "BLE", # flake8-blind-except (force more specific exception types than just Exception) - "C4", # flake8-comprehensions (warn about things that could be written as a comprehensions but aren't) - "C90", # McCabe cyclomatic complexity (warn about functions that are too complex) - "COM", # flake8-commas (forces commas at the end of every type of iterable/container + "B", # flake8-bugbear (various likely bugs and design issues) + "BLE", # flake8-blind-except (force more specific exception types than just Exception) + "C4", # flake8-comprehensions (warn about things that could be written as a comprehensions but aren't) + "C90", # McCabe cyclomatic complexity (warn about functions that are too complex) + "COM", # flake8-commas (forces commas at the end of every type of iterable/container # "CPY", # flake8-copyright (warn about missing copyright notice at top of file - currently in preview) # "D", # pydocstyle (warn about things like missing docstrings) # "DOC", # pydoclint (docstring warnings - currently in preview) @@ -258,6 +258,11 @@ per-file-ignores."cmd2/__init__.py" = [ "F401", # Unused import ] +per-file-ignores."cmd2/argparse_custom.py" = [ + "B010", # Do not call setattr with a constant attribute value +] + + per-file-ignores."examples/*.py" = [ "ANN", # Ignore all type annotation rules in examples folder "D", # Ignore all pydocstyle rules in examples folder From fbf411756e760c3659cd6f73d5cb14a2b78cd3a9 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sun, 25 May 2025 01:20:05 -0400 Subject: [PATCH 3/4] Removed zip parameter added in Python 3.10 and disabled rule that auto-added it --- cmd2/cmd2.py | 2 +- pyproject.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index e6a9c9aa..f41e3912 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -4020,7 +4020,7 @@ def select(self, opts: Union[str, list[str], list[tuple[Any, Optional[str]]]], p """ local_opts: Union[list[str], list[tuple[Any, Optional[str]]]] if isinstance(opts, str): - local_opts = cast(list[tuple[Any, Optional[str]]], list(zip(opts.split(), opts.split(), strict=False))) + local_opts = cast(list[tuple[Any, Optional[str]]], list(zip(opts.split(), opts.split()))) else: local_opts = opts fulloptions: list[tuple[Any, Optional[str]]] = [] diff --git a/pyproject.toml b/pyproject.toml index f0d622cd..6b036662 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dev = [ "black>=24", "codecov>=2", "invoke>=2", + "ipython>=8", "mkdocs-git-revision-date-localized-plugin>=1.3", "mkdocs-include-markdown-plugin>=6", "mkdocs-macros-plugin>=1", @@ -223,6 +224,7 @@ select = [ ignore = [ # `uv run ruff rule E501` for a description of that rule "ANN401", # Dynamically typed expressions (typing.Any) are disallowed (would be good to enable this later) + "B905", # zip() without an explicit strict= parameter (strict added in Python 3.10+) "COM812", # Conflicts with ruff format (see https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules) "COM819", # Conflicts with ruff format "D206", # Conflicts with ruff format From 9b765d4679ad9e2740f3ffbba54d887d61e3a728 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sun, 25 May 2025 01:24:48 -0400 Subject: [PATCH 4/4] Restoring one line I don't think I should have changed --- cmd2/argparse_custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index b0a50ab2..9fefbe5e 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -319,7 +319,7 @@ def __call__(self) -> list[str]: ... # pragma: no cover class ChoicesProviderFuncWithTokens(Protocol): """Function that returns a list of choices in support of tab completion and accepts a dictionary of prior arguments.""" - def __call__(self, *, arg_tokens: dict[str, list[str]]) -> list[str]: ... # pragma: no cover + def __call__(self, *, arg_tokens: dict[str, list[str]] = {}) -> list[str]: ... # pragma: no cover # noqa: B006 ChoicesProviderFunc = Union[ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens]