From 56457835283d8a5882051e100ce4db61cc64525d Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 5 Apr 2026 02:21:18 -0400 Subject: [PATCH 1/3] Removed Cmd2AttributeWrapper class. --- CHANGELOG.md | 3 ++ cmd2/__init__.py | 2 - cmd2/argparse_custom.py | 22 +---------- cmd2/cmd2.py | 6 +-- cmd2/constants.py | 57 +++++++++++++++++++--------- cmd2/decorators.py | 28 +++++--------- docs/features/argument_processing.md | 16 +++----- docs/features/modular_commands.md | 2 +- examples/argparse_example.py | 3 +- examples/command_sets.py | 2 +- tests/test_argparse.py | 3 +- tests/test_argparse_completer.py | 3 +- tests/test_argparse_custom.py | 12 +----- tests/test_commandset.py | 13 +++---- tests/test_plugin.py | 2 +- 15 files changed, 74 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7d3d380..51e4c8d58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,9 @@ prompt is displayed. - Removed `set_ap_completer_type()` and `get_ap_completer_type()` since `ap_completer_type` is now a public member of `Cmd2ArgumentParser`. - Moved `set_parser_prog()` to `Cmd2ArgumentParser.update_prog()`. + - Renamed `cmd2_handler` to `cmd2_subcmd_handler` in the `argparse.Namespace` for clarity. + - Removed `Cmd2AttributeWrapper` class. `argparse.Namespace` objects passed to command functions + now contain direct attributes for `cmd2_statement` and `cmd2_subcmd_handler`. - Enhancements - New `cmd2.Cmd` parameters - **auto_suggest**: (boolean) if `True`, provide fish shell style auto-suggestions. These diff --git a/cmd2/__init__.py b/cmd2/__init__.py index dbfb5faa0..001d031b1 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -14,7 +14,6 @@ from .argparse_completer import set_default_ap_completer_type from .argparse_custom import ( Cmd2ArgumentParser, - Cmd2AttributeWrapper, register_argparse_argument_parameter, set_default_argument_parser_type, ) @@ -63,7 +62,6 @@ 'DEFAULT_SHORTCUTS', # Argparse Exports 'Cmd2ArgumentParser', - 'Cmd2AttributeWrapper', 'register_argparse_argument_parameter', 'set_default_ap_completer_type', 'set_default_argument_parser_type', diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 23b6ad2ba..88ef9202f 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -329,7 +329,7 @@ def register_argparse_argument_parameter( raise KeyError(f"Accessor methods for '{param_name}' already exist on argparse.Action") # Check for the prefixed internal attribute name collision (e.g., _cmd2_) - attr_name = constants.cmd2_attr_name(param_name) + attr_name = constants.cmd2_private_attr_name(param_name) if hasattr(argparse.Action, attr_name): raise KeyError(f"The internal attribute '{attr_name}' already exists on argparse.Action") @@ -1047,26 +1047,6 @@ def _check_value(self, action: argparse.Action, value: Any) -> None: raise ArgumentError(action, msg % args) -class Cmd2AttributeWrapper: - """Wraps a cmd2-specific attribute added to an argparse Namespace. - - This makes it easy to know which attributes in a Namespace are - arguments from a parser and which were added by cmd2. - """ - - def __init__(self, attribute: Any) -> None: - """Initialize Cmd2AttributeWrapper instances.""" - self.__attribute = attribute - - def get(self) -> Any: - """Get the value of the attribute.""" - return self.__attribute - - def set(self, new_val: Any) -> None: - """Set the value of the attribute.""" - self.__attribute = new_val - - # Parser type used by cmd2's built-in commands. # Set it using cmd2.set_default_argument_parser_type(). DEFAULT_ARGUMENT_PARSER: type[Cmd2ArgumentParser] = Cmd2ArgumentParser diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index d661509d7..424763d12 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3729,8 +3729,7 @@ def _build_alias_parser() -> Cmd2ArgumentParser: def do_alias(self, args: argparse.Namespace) -> None: """Manage aliases.""" # Call handler for whatever subcommand was selected - handler = args.cmd2_handler.get() - handler(args) + args.cmd2_subcmd_handler(args) # alias -> create @classmethod @@ -3946,8 +3945,7 @@ def _build_macro_parser() -> Cmd2ArgumentParser: def do_macro(self, args: argparse.Namespace) -> None: """Manage macros.""" # Call handler for whatever subcommand was selected - handler = args.cmd2_handler.get() - handler(args) + args.cmd2_subcmd_handler(args) # macro -> create @classmethod diff --git a/cmd2/constants.py b/cmd2/constants.py index 91497d86b..d2df9192b 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -32,35 +32,56 @@ COMPLETER_FUNC_PREFIX = 'complete_' # Prefix for private attributes injected by cmd2 -CMD2_ATTR_PREFIX = '_cmd2_' +PRIVATE_ATTR_PREFIX = '_cmd2_' +# Prefix for public attributes injected by cmd2 +PUBLIC_ATTR_PREFIX = 'cmd2_' -def cmd2_attr_name(name: str) -> str: - """Build an attribute name with the cmd2 prefix. + +def cmd2_private_attr_name(name: str) -> str: + """Build a private attribute name with the _cmd2_ prefix. :param name: the name of the attribute :return: the prefixed attribute name """ - return f'{CMD2_ATTR_PREFIX}{name}' + return f'{PRIVATE_ATTR_PREFIX}{name}' + +def cmd2_public_attr_name(name: str) -> str: + """Build a public attribute name with the cmd2_ prefix. -# The custom help category a command belongs to -CMD_ATTR_HELP_CATEGORY = cmd2_attr_name('help_category') -CLASS_ATTR_DEFAULT_HELP_CATEGORY = cmd2_attr_name('default_help_category') + :param name: the name of the attribute + :return: the prefixed attribute name + """ + return f'{PUBLIC_ATTR_PREFIX}{name}' -# The argparse parser for the command -CMD_ATTR_ARGPARSER = cmd2_attr_name('argparser') + +################################################################################################## +# Private cmd2-specific attributes for internal use +################################################################################################## +CMD_ATTR_HELP_CATEGORY = cmd2_private_attr_name('help_category') +CLASS_ATTR_DEFAULT_HELP_CATEGORY = cmd2_private_attr_name('default_help_category') + +# The parser for a command +CMD_ATTR_ARGPARSER = cmd2_private_attr_name('argparser') # Whether or not tokens are unquoted before sending to argparse -CMD_ATTR_PRESERVE_QUOTES = cmd2_attr_name('preserve_quotes') +CMD_ATTR_PRESERVE_QUOTES = cmd2_private_attr_name('preserve_quotes') + +# Subcommand attributes for the base command name and the subcommand name +SUBCMD_ATTR_COMMAND = cmd2_private_attr_name('parent_command') +SUBCMD_ATTR_NAME = cmd2_private_attr_name('subcommand_name') +SUBCMD_ATTR_ADD_PARSER_KWARGS = cmd2_private_attr_name('subcommand_add_parser_kwargs') + +# Attribute added to a parser which uniquely identifies its command set instance +PARSER_ATTR_COMMANDSET_ID = cmd2_private_attr_name('command_set_id') -# subcommand attributes for the base command name and the subcommand name -SUBCMD_ATTR_COMMAND = cmd2_attr_name('parent_command') -SUBCMD_ATTR_NAME = cmd2_attr_name('subcommand_name') -SUBCMD_ATTR_ADD_PARSER_KWARGS = cmd2_attr_name('subcommand_add_parser_kwargs') +################################################################################################## +# Public cmd2-specific attributes for use by developers +################################################################################################## -# argparse attribute uniquely identifying the command set instance -PARSER_ATTR_COMMANDSET_ID = cmd2_attr_name('command_set_id') +# Namespace attribute: Statement object that was created when parsing the command line +NS_ATTR_STATEMENT = cmd2_public_attr_name('statement') -# custom attributes added to argparse Namespaces -NS_ATTR_SUBCMD_HANDLER = cmd2_attr_name('subcmd_handler') +# Namespace attribute: subcommand handler function or None if one was not set +NS_ATTR_SUBCMD_HANDLER = cmd2_public_attr_name('subcmd_handler') diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 9ca81d0d4..7ebc6745b 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -13,10 +13,7 @@ ) from . import constants -from .argparse_custom import ( - Cmd2ArgumentParser, - Cmd2AttributeWrapper, -) +from .argparse_custom import Cmd2ArgumentParser from .command_definition import ( CommandFunc, CommandSet, @@ -233,9 +230,9 @@ def with_argparser( :param preserve_quotes: if ``True``, then arguments passed to argparse maintain their quotes :param with_unknown_args: if true, then capture unknown args :return: function that gets passed argparse-parsed args in a ``Namespace`` - A [cmd2.argparse_custom.Cmd2AttributeWrapper][] called ``cmd2_statement`` is included - in the ``Namespace`` to provide access to the [cmd2.Statement][] object that was created when - parsing the command line. This can be useful if the command function needs to know the command line. + A ``cmd2_statement`` attribute is included in the ``Namespace`` to provide access to the + [cmd2.Statement][] object that was created when parsing the command line. This can be useful + if the command function needs to know the command line. Example: ```py @@ -320,20 +317,15 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None: except SystemExit as exc: raise Cmd2ArgparseError from exc - # Add cmd2-specific metadata to the Namespace + # Add cmd2-specific attributes to the Namespace parsed_namespace = parsing_results[0] - # Add wrapped statement to Namespace as cmd2_statement - parsed_namespace.cmd2_statement = Cmd2AttributeWrapper(statement) - - # Add wrapped subcmd handler (which can be None) to Namespace as cmd2_handler - handler = getattr(parsed_namespace, constants.NS_ATTR_SUBCMD_HANDLER, None) - parsed_namespace.cmd2_handler = Cmd2AttributeWrapper(handler) + # Include the Statement object created from the command line + setattr(parsed_namespace, constants.NS_ATTR_STATEMENT, statement) - # Remove the subcmd handler attribute from the Namespace - # since cmd2_handler is how a developer accesses it. - if hasattr(parsed_namespace, constants.NS_ATTR_SUBCMD_HANDLER): - delattr(parsed_namespace, constants.NS_ATTR_SUBCMD_HANDLER) + # Ensure NS_ATTR_SUBCMD_HANDLER is always present. + if not hasattr(parsed_namespace, constants.NS_ATTR_SUBCMD_HANDLER): + setattr(parsed_namespace, constants.NS_ATTR_SUBCMD_HANDLER, None) func_arg_list = _arg_swap(args, statement_arg, *parsing_results) return func(*func_arg_list, **kwargs) diff --git a/docs/features/argument_processing.md b/docs/features/argument_processing.md index 8f9b3ccb4..18f093848 100644 --- a/docs/features/argument_processing.md +++ b/docs/features/argument_processing.md @@ -11,8 +11,8 @@ following for you: 1. Passes the resulting [argparse.Namespace](https://docs.python.org/3/library/argparse.html#argparse.Namespace) object to your command function. The `Namespace` includes the [Statement][cmd2.Statement] object that - was created when parsing the command line. It can be retrieved by calling `cmd2_statement.get()` - on the `Namespace`. + was created when parsing the command line. It is accessible via the `cmd2_statement` attribute on + the `Namespace`. 1. Adds the usage message from the argument parser to your command's help. 1. Checks if the `-h/--help` option is present, and if so, displays the help message for the command @@ -397,11 +397,7 @@ example demonstrates both above cases in a concrete fashion. ## Reserved Argument Names `cmd2`'s `@with_argparser` decorator adds the following attributes to argparse Namespaces. To avoid -naming collisions, do not use any of the names for your argparse arguments. - -- `cmd2_statement` - `cmd2.Cmd2AttributeWrapper` object containing the `cmd2.Statement` object that - was created when parsing the command line. -- `cmd2_handler` - `cmd2.Cmd2AttributeWrapper` object containing a subcommand handler function or - `None` if one was not set. -- `__subcmd_handler__` - used by cmd2 to identify the handler for a subcommand created with the - `@cmd2.as_subcommand_to` decorator. +naming collisions, do not use any of these names for your argparse arguments. + +- `cmd2_statement` - [cmd2.Statement][] object that was created when parsing the command line. +- `cmd2_subcmd_handler` - subcommand handler function or `None` if one was not set. diff --git a/docs/features/modular_commands.md b/docs/features/modular_commands.md index 3ba8e994d..851668836 100644 --- a/docs/features/modular_commands.md +++ b/docs/features/modular_commands.md @@ -337,7 +337,7 @@ class ExampleApp(cmd2.Cmd): @with_argparser(cut_parser) def do_cut(self, ns: argparse.Namespace): - handler = ns.cmd2_handler.get() + handler = ns.cmd2_subcmd_handler if handler is not None: # Call whatever subcommand function was selected handler(ns) diff --git a/examples/argparse_example.py b/examples/argparse_example.py index 564f4be92..5db31d035 100755 --- a/examples/argparse_example.py +++ b/examples/argparse_example.py @@ -139,8 +139,7 @@ def subtract(self, args: argparse.Namespace) -> None: @cmd2.with_category(ARGPARSE_SUBCOMMANDS) def do_calculate(self, args: argparse.Namespace) -> None: """Calculate a simple mathematical operation on two integers.""" - handler = args.cmd2_handler.get() - handler(args) + args.cmd2_subcmd_handler(args) if __name__ == '__main__': diff --git a/examples/command_sets.py b/examples/command_sets.py index a14cb80c8..fb0e3e024 100755 --- a/examples/command_sets.py +++ b/examples/command_sets.py @@ -148,7 +148,7 @@ def do_unload(self, ns: argparse.Namespace) -> None: @with_category(COMMANDSET_SUBCOMMAND) def do_cut(self, ns: argparse.Namespace) -> None: """Intended to be used with dynamically loaded subcommands specifically.""" - handler = ns.cmd2_handler.get() + handler = ns.cmd2_subcmd_handler if handler is not None: handler(ns) else: diff --git a/tests/test_argparse.py b/tests/test_argparse.py index 13d567bf5..4b4fb3772 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -350,8 +350,7 @@ def do_base(self, args) -> None: # Add subcommands using as_subcommand_to decorator @cmd2.with_argparser(_build_has_subcmd_parser) def do_test_subcmd_decorator(self, args: argparse.Namespace) -> None: - handler = args.cmd2_handler.get() - handler(args) + args.cmd2_subcmd_handler(args) subcmd_parser = cmd2.Cmd2ArgumentParser(description="A subcommand") diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py index c94479f91..0bd1a0c5a 100644 --- a/tests/test_argparse_completer.py +++ b/tests/test_argparse_completer.py @@ -1314,8 +1314,7 @@ def do_custom_completer(self, args: argparse.Namespace) -> None: def do_top(self, args: argparse.Namespace) -> None: """Top level command""" # Call handler for whatever subcommand was selected - handler = args.cmd2_handler.get() - handler(args) + args.cmd2_subcmd_handler(args) # Parser for a subcommand with no custom completer type no_custom_completer_parser = Cmd2ArgumentParser(description="No custom completer") diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py index d4f29688e..7a333295b 100644 --- a/tests/test_argparse_custom.py +++ b/tests/test_argparse_custom.py @@ -288,16 +288,6 @@ def test_apcustom_metavar_tuple() -> None: assert '[--aflag foo bar]' in parser.format_help() -def test_cmd2_attribute_wrapper() -> None: - initial_val = 5 - wrapper = cmd2.Cmd2AttributeWrapper(initial_val) - assert wrapper.get() == initial_val - - new_val = 22 - wrapper.set(new_val) - assert wrapper.get() == new_val - - def test_register_argparse_argument_parameter() -> None: # Test successful registration param_name = "test_unique_param" @@ -333,7 +323,7 @@ def test_register_argparse_argument_parameter() -> None: # Test collision with internal attribute try: - attr_name = constants.cmd2_attr_name("internal_collision") + attr_name = constants.cmd2_private_attr_name("internal_collision") setattr(argparse.Action, attr_name, None) expected_err = f"The internal attribute '{attr_name}' already exists on argparse.Action" with pytest.raises(KeyError, match=expected_err): diff --git a/tests/test_commandset.py b/tests/test_commandset.py index 69129106d..067a81215 100644 --- a/tests/test_commandset.py +++ b/tests/test_commandset.py @@ -92,8 +92,7 @@ def do_elderberry(self, ns: argparse.Namespace) -> None: @cmd2.with_argparser(main_parser) def do_main(self, args: argparse.Namespace) -> None: # Call handler for whatever subcommand was selected - handler = args.cmd2_handler.get() - handler(args) + args.cmd2_subcmd_handler(args) # main -> sub subcmd_parser = cmd2.Cmd2ArgumentParser(description="Sub Command") @@ -398,7 +397,7 @@ def namespace_provider(self) -> argparse.Namespace: @cmd2.with_argparser(cut_parser) def do_cut(self, ns: argparse.Namespace) -> None: """Cut something""" - handler = ns.cmd2_handler.get() + handler = ns.cmd2_subcmd_handler if handler is not None: # Call whatever subcommand function was selected handler(ns) @@ -418,7 +417,7 @@ def do_stir(self, ns: argparse.Namespace) -> None: self._cmd.poutput('Need to cut before stirring') return - handler = ns.cmd2_handler.get() + handler = ns.cmd2_subcmd_handler if handler is not None: # Call whatever subcommand function was selected handler(ns) @@ -433,7 +432,7 @@ def do_stir(self, ns: argparse.Namespace) -> None: @cmd2.as_subcommand_to('stir', 'pasta', stir_pasta_parser) def stir_pasta(self, ns: argparse.Namespace) -> None: - handler = ns.cmd2_handler.get() + handler = ns.cmd2_subcmd_handler if handler is not None: # Call whatever subcommand function was selected handler(ns) @@ -448,7 +447,7 @@ def __init__(self, dummy) -> None: def do_cut(self, ns: argparse.Namespace) -> None: """Cut something""" - handler = ns.cmd2_handler.get() + handler = ns.cmd2_subcmd_handler if handler is not None: # Call whatever subcommand function was selected handler(ns) @@ -715,7 +714,7 @@ def __init__(self, *args, **kwargs) -> None: @cmd2.with_argparser(cut_parser) def do_cut(self, ns: argparse.Namespace) -> None: """Cut something""" - handler = ns.cmd2_handler.get() + handler = ns.cmd2_subcmd_handler if handler is not None: # Call whatever subcommand function was selected handler(ns) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 8b1c9da8f..346292a7f 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -281,7 +281,7 @@ def do_skip_postcmd_hooks(self, _) -> NoReturn: @with_argparser(parser) def do_argparse_cmd(self, namespace: argparse.Namespace) -> None: """Repeat back the arguments""" - self.poutput(namespace.cmd2_statement.get()) + self.poutput(namespace.cmd2_statement) ### From ebc28dc2b55931cfe95ccdfefc154319ef6ad006 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 6 Apr 2026 14:14:58 -0400 Subject: [PATCH 2/3] Improved comments in constants.py. --- cmd2/cmd2.py | 4 ++-- cmd2/command_definition.py | 4 ++-- cmd2/constants.py | 40 ++++++++++++++++++++++++++------------ tests/test_categories.py | 12 ++++++------ 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 424763d12..e600d195c 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -123,7 +123,7 @@ Completions, ) from .constants import ( - CLASS_ATTR_DEFAULT_HELP_CATEGORY, + CMDSET_ATTR_DEFAULT_HELP_CATEGORY, COMMAND_FUNC_PREFIX, COMPLETER_FUNC_PREFIX, HELP_FUNC_PREFIX, @@ -840,7 +840,7 @@ def register_command_set(self, cmdset: CommandSet) -> None: ), ) - default_category = getattr(cmdset, CLASS_ATTR_DEFAULT_HELP_CATEGORY, None) + default_category = getattr(cmdset, CMDSET_ATTR_DEFAULT_HELP_CATEGORY, None) installed_attributes = [] try: diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py index f98ab22f5..cb9d49957 100644 --- a/cmd2/command_definition.py +++ b/cmd2/command_definition.py @@ -11,7 +11,7 @@ ) from .constants import ( - CLASS_ATTR_DEFAULT_HELP_CATEGORY, + CMDSET_ATTR_DEFAULT_HELP_CATEGORY, COMMAND_FUNC_PREFIX, ) from .exceptions import CommandSetRegistrationError @@ -46,7 +46,7 @@ def with_default_category(category: str, *, heritable: bool = True) -> Callable[ def decorate_class(cls: CommandSetType) -> CommandSetType: if heritable: - setattr(cls, CLASS_ATTR_DEFAULT_HELP_CATEGORY, category) + setattr(cls, CMDSET_ATTR_DEFAULT_HELP_CATEGORY, category) import inspect diff --git a/cmd2/constants.py b/cmd2/constants.py index d2df9192b..3a0e4077c 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -57,31 +57,47 @@ def cmd2_public_attr_name(name: str) -> str: ################################################################################################## -# Private cmd2-specific attributes for internal use +# Attribute Injection Constants +# +# cmd2 attaches custom attributes to various objects (functions, classes, and parsers) to +# track metadata and manage command state. +# +# Private attributes (_cmd2_ prefix) are for internal framework logic. +# Public attributes (cmd2_ prefix) are available for developer use, typically within +# argparse Namespaces. ################################################################################################## -CMD_ATTR_HELP_CATEGORY = cmd2_private_attr_name('help_category') -CLASS_ATTR_DEFAULT_HELP_CATEGORY = cmd2_private_attr_name('default_help_category') -# The parser for a command +# --- Private Internal Attributes --- + +# Attached to a command function; defines its argument parser CMD_ATTR_ARGPARSER = cmd2_private_attr_name('argparser') -# Whether or not tokens are unquoted before sending to argparse +# Attached to a command function; defines its help section category +CMD_ATTR_HELP_CATEGORY = cmd2_private_attr_name('help_category') + +# Attached to a command function; defines whether tokens are unquoted before reaching argparse CMD_ATTR_PRESERVE_QUOTES = cmd2_private_attr_name('preserve_quotes') -# Subcommand attributes for the base command name and the subcommand name +# Attached to a CommandSet class; defines a default help category for its member functions +CMDSET_ATTR_DEFAULT_HELP_CATEGORY = cmd2_private_attr_name('default_help_category') + +# Attached to a subcommand function; defines the full command path to the parent (e.g., "foo" or "foo bar") SUBCMD_ATTR_COMMAND = cmd2_private_attr_name('parent_command') + +# Attached to a subcommand function; defines the name of this specific subcommand (e.g., "bar") SUBCMD_ATTR_NAME = cmd2_private_attr_name('subcommand_name') + +# Attached to a subcommand function; specifies kwargs passed to add_parser() SUBCMD_ATTR_ADD_PARSER_KWARGS = cmd2_private_attr_name('subcommand_add_parser_kwargs') -# Attribute added to a parser which uniquely identifies its command set instance +# Attached to an argparse parser; identifies the CommandSet instance it belongs to PARSER_ATTR_COMMANDSET_ID = cmd2_private_attr_name('command_set_id') -################################################################################################## -# Public cmd2-specific attributes for use by developers -################################################################################################## -# Namespace attribute: Statement object that was created when parsing the command line +# --- Public Developer Attributes --- + +# Attached to an argparse Namespace; contains the Statement object created during parsing NS_ATTR_STATEMENT = cmd2_public_attr_name('statement') -# Namespace attribute: subcommand handler function or None if one was not set +# Attached to an argparse Namespace; the function to handle the subcommand (or None) NS_ATTR_SUBCMD_HANDLER = cmd2_public_attr_name('subcmd_handler') diff --git a/tests/test_categories.py b/tests/test_categories.py index 8150c5e7d..ee53bb134 100644 --- a/tests/test_categories.py +++ b/tests/test_categories.py @@ -79,31 +79,31 @@ def test_heritable_categories() -> None: app = ExampleApp() base_cs = MyBaseCommandSet(0) - assert getattr(base_cs, cmd2.constants.CLASS_ATTR_DEFAULT_HELP_CATEGORY, None) == 'Default Category' + assert getattr(base_cs, cmd2.constants.CMDSET_ATTR_DEFAULT_HELP_CATEGORY, None) == 'Default Category' child1 = ChildInheritsParentCategories(1) - assert getattr(child1, cmd2.constants.CLASS_ATTR_DEFAULT_HELP_CATEGORY, None) == 'Default Category' + assert getattr(child1, cmd2.constants.CMDSET_ATTR_DEFAULT_HELP_CATEGORY, None) == 'Default Category' app.register_command_set(child1) assert getattr(app.cmd_func('hello').__func__, cmd2.constants.CMD_ATTR_HELP_CATEGORY, None) == 'Default Category' app.unregister_command_set(child1) child_nonheritable = ChildOverridesParentCategoriesNonHeritable(2) - assert getattr(child_nonheritable, cmd2.constants.CLASS_ATTR_DEFAULT_HELP_CATEGORY, None) != 'Non-Heritable Category' + assert getattr(child_nonheritable, cmd2.constants.CMDSET_ATTR_DEFAULT_HELP_CATEGORY, None) != 'Non-Heritable Category' app.register_command_set(child_nonheritable) assert getattr(app.cmd_func('goodbye').__func__, cmd2.constants.CMD_ATTR_HELP_CATEGORY, None) == 'Non-Heritable Category' app.unregister_command_set(child_nonheritable) grandchild1 = GrandchildInheritsGrandparentCategory(3) - assert getattr(grandchild1, cmd2.constants.CLASS_ATTR_DEFAULT_HELP_CATEGORY, None) == 'Default Category' + assert getattr(grandchild1, cmd2.constants.CMDSET_ATTR_DEFAULT_HELP_CATEGORY, None) == 'Default Category' app.register_command_set(grandchild1) assert getattr(app.cmd_func('aloha').__func__, cmd2.constants.CMD_ATTR_HELP_CATEGORY, None) == 'Default Category' app.unregister_command_set(grandchild1) child_overrides = ChildOverridesParentCategories(4) - assert getattr(child_overrides, cmd2.constants.CLASS_ATTR_DEFAULT_HELP_CATEGORY, None) == 'Heritable Category' + assert getattr(child_overrides, cmd2.constants.CMDSET_ATTR_DEFAULT_HELP_CATEGORY, None) == 'Heritable Category' app.register_command_set(child_overrides) assert getattr(app.cmd_func('bonjour').__func__, cmd2.constants.CMD_ATTR_HELP_CATEGORY, None) == 'Heritable Category' app.unregister_command_set(child_overrides) grandchild2 = GrandchildInheritsHeritable(5) - assert getattr(grandchild2, cmd2.constants.CLASS_ATTR_DEFAULT_HELP_CATEGORY, None) == 'Heritable Category' + assert getattr(grandchild2, cmd2.constants.CMDSET_ATTR_DEFAULT_HELP_CATEGORY, None) == 'Heritable Category' From c15121602f0e0028fc98b752ce133a3c742949a3 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 6 Apr 2026 14:23:28 -0400 Subject: [PATCH 3/3] Reformatted import. --- cmd2/command_definition.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py index cb9d49957..b17a10906 100644 --- a/cmd2/command_definition.py +++ b/cmd2/command_definition.py @@ -50,12 +50,8 @@ def decorate_class(cls: CommandSetType) -> CommandSetType: import inspect - from .constants import ( - CMD_ATTR_HELP_CATEGORY, - ) - from .decorators import ( - with_category, - ) + from .constants import CMD_ATTR_HELP_CATEGORY + from .decorators import with_category # get members of the class that meet the following criteria: # 1. Must be a function