Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions cmd2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -63,7 +62,6 @@
'DEFAULT_SHORTCUTS',
# Argparse Exports
'Cmd2ArgumentParser',
'Cmd2AttributeWrapper',
'register_argparse_argument_parameter',
'set_default_ap_completer_type',
'set_default_argument_parser_type',
Expand Down
22 changes: 1 addition & 21 deletions cmd2/argparse_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_<param_name>)
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")

Expand Down Expand Up @@ -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
Expand Down
10 changes: 4 additions & 6 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions cmd2/command_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
)

from .constants import (
CLASS_ATTR_DEFAULT_HELP_CATEGORY,
CMDSET_ATTR_DEFAULT_HELP_CATEGORY,
COMMAND_FUNC_PREFIX,
)
from .exceptions import CommandSetRegistrationError
Expand Down Expand Up @@ -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

Expand Down
75 changes: 56 additions & 19 deletions cmd2/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,35 +32,72 @@
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'{PRIVATE_ATTR_PREFIX}{name}'


def cmd2_public_attr_name(name: str) -> str:
Comment thread
tleonhardt marked this conversation as resolved.
"""Build a public 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'{PUBLIC_ATTR_PREFIX}{name}'


##################################################################################################
# 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.
##################################################################################################

# --- Private Internal Attributes ---

# Attached to a command function; defines its argument parser
CMD_ATTR_ARGPARSER = cmd2_private_attr_name('argparser')

# 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')

# 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')

# 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')
# Attached to a subcommand function; specifies kwargs passed to add_parser()
SUBCMD_ATTR_ADD_PARSER_KWARGS = cmd2_private_attr_name('subcommand_add_parser_kwargs')

# The argparse parser for the command
CMD_ATTR_ARGPARSER = cmd2_attr_name('argparser')
# Attached to an argparse parser; identifies the CommandSet instance it belongs to
PARSER_ATTR_COMMANDSET_ID = cmd2_private_attr_name('command_set_id')

# Whether or not tokens are unquoted before sending to argparse
CMD_ATTR_PRESERVE_QUOTES = cmd2_attr_name('preserve_quotes')

# 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 Developer Attributes ---

# argparse attribute uniquely identifying the command set instance
PARSER_ATTR_COMMANDSET_ID = cmd2_attr_name('command_set_id')
# Attached to an argparse Namespace; contains the Statement object created during parsing
NS_ATTR_STATEMENT = cmd2_public_attr_name('statement')

# custom attributes added to argparse Namespaces
NS_ATTR_SUBCMD_HANDLER = cmd2_attr_name('subcmd_handler')
# Attached to an argparse Namespace; the function to handle the subcommand (or None)
NS_ATTR_SUBCMD_HANDLER = cmd2_public_attr_name('subcmd_handler')
28 changes: 10 additions & 18 deletions cmd2/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@
)

from . import constants
from .argparse_custom import (
Cmd2ArgumentParser,
Cmd2AttributeWrapper,
)
from .argparse_custom import Cmd2ArgumentParser
from .command_definition import (
CommandFunc,
CommandSet,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 6 additions & 10 deletions docs/features/argument_processing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
2 changes: 1 addition & 1 deletion docs/features/modular_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions examples/argparse_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__':
Expand Down
2 changes: 1 addition & 1 deletion examples/command_sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 1 addition & 2 deletions tests/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
3 changes: 1 addition & 2 deletions tests/test_argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
12 changes: 1 addition & 11 deletions tests/test_argparse_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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):
Expand Down
Loading
Loading