Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 4 additions & 8 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,16 +46,12 @@ 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

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
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:
"""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