Skip to content

Commit cecb59e

Browse files
committed
Simplified categorizing commands.
1 parent 6fbf084 commit cecb59e

20 files changed

Lines changed: 356 additions & 353 deletions

cmd2/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@
1919
)
2020
from .cmd2 import Cmd
2121
from .colors import Color
22-
from .command_definition import (
23-
CommandSet,
24-
with_default_category,
25-
)
22+
from .command_definition import CommandSet
2623
from .completion import (
2724
Choices,
2825
CompletionItem,
@@ -80,7 +77,6 @@
8077
'with_argument_list',
8178
'with_argparser',
8279
'with_category',
83-
'with_default_category',
8480
'as_subcommand_to',
8581
# Exceptions
8682
'Cmd2ArgparseError',

cmd2/cmd2.py

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import time
4343
from code import InteractiveConsole
4444
from collections import (
45+
defaultdict,
4546
deque,
4647
namedtuple,
4748
)
@@ -60,6 +61,7 @@
6061
IO,
6162
TYPE_CHECKING,
6263
Any,
64+
ClassVar,
6365
TextIO,
6466
TypeVar,
6567
Union,
@@ -123,7 +125,6 @@
123125
Completions,
124126
)
125127
from .constants import (
126-
CMDSET_ATTR_DEFAULT_HELP_CATEGORY,
127128
COMMAND_FUNC_PREFIX,
128129
COMPLETER_FUNC_PREFIX,
129130
HELP_FUNC_PREFIX,
@@ -328,9 +329,22 @@ class Cmd:
328329
Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes.
329330
"""
330331

331-
DEFAULT_COMPLETEKEY = 'tab'
332-
DEFAULT_EDITOR = utils.find_editor()
333-
DEFAULT_PROMPT = '(Cmd) '
332+
DEFAULT_COMPLETEKEY: ClassVar[str] = "tab"
333+
DEFAULT_EDITOR: ClassVar[str | None] = utils.find_editor()
334+
DEFAULT_PROMPT: ClassVar[str] = "(Cmd) "
335+
336+
# Default category used for documented commands (those with a docstring,
337+
# help function, or argparse decorator) defined in this class that have
338+
# not been explicitly categorized with the @with_category decorator.
339+
# This value is inherited by subclasses but they can set their own
340+
# DEFAULT_CATEGORY to place their commands into a custom category.
341+
DEFAULT_CATEGORY: ClassVar[str] = "Cmd2 Commands"
342+
343+
# Header for table listing help topics not related to a command.
344+
MISC_HEADER: ClassVar[str] = "Miscellaneous Help Topics"
345+
346+
# Header for table listing commands that have no help info.
347+
UNDOC_HEADER: ClassVar[str] = "Undocumented Commands"
334348

335349
def __init__(
336350
self,
@@ -537,19 +551,6 @@ def __init__(
537551
# Set text which prints right before all of the help tables are listed.
538552
self.doc_leader = ""
539553

540-
# Set header for table listing documented commands.
541-
self.doc_header = "Documented Commands"
542-
543-
# Set header for table listing help topics not related to a command.
544-
self.misc_header = "Miscellaneous Help Topics"
545-
546-
# Set header for table listing commands that have no help info.
547-
self.undoc_header = "Undocumented Commands"
548-
549-
# If any command has been categorized, then all other documented commands that
550-
# haven't been categorized will display under this section in the help output.
551-
self.default_category = "Uncategorized Commands"
552-
553554
# The error that prints when no help information can be found
554555
self.help_error = "No help on {}"
555556

@@ -840,8 +841,6 @@ def register_command_set(self, cmdset: CommandSet) -> None:
840841
),
841842
)
842843

843-
default_category = getattr(cmdset, CMDSET_ATTR_DEFAULT_HELP_CATEGORY, None)
844-
845844
installed_attributes = []
846845
try:
847846
for cmd_func_name, command_method in methods:
@@ -864,9 +863,6 @@ def register_command_set(self, cmdset: CommandSet) -> None:
864863

865864
self._cmd_to_command_sets[command] = cmdset
866865

867-
if default_category and not hasattr(command_method, constants.CMD_ATTR_HELP_CATEGORY):
868-
utils.categorize(command_method, default_category)
869-
870866
# If this command is in a disabled category, then disable it
871867
command_category = getattr(command_method, constants.CMD_ATTR_HELP_CATEGORY, None)
872868
if command_category in self.disabled_categories:
@@ -4214,12 +4210,11 @@ def complete_help_subcommands(
42144210
completer = argparse_completer.DEFAULT_AP_COMPLETER(argparser, self)
42154211
return completer.complete_subcommand_help(text, line, begidx, endidx, arg_tokens['subcommands'])
42164212

4217-
def _build_command_info(self) -> tuple[dict[str, list[str]], list[str], list[str], list[str]]:
4213+
def _build_command_info(self) -> tuple[dict[str, list[str]], list[str], list[str]]:
42184214
"""Categorizes and sorts visible commands and help topics for display.
42194215
42204216
:return: tuple containing:
42214217
- dictionary mapping category names to lists of command names
4222-
- list of documented command names
42234218
- list of undocumented command names
42244219
- list of help topic names that are not also commands
42254220
"""
@@ -4228,9 +4223,9 @@ def _build_command_info(self) -> tuple[dict[str, list[str]], list[str], list[str
42284223

42294224
# Get a sorted list of visible command names
42304225
visible_commands = sorted(self.get_visible_commands(), key=utils.DEFAULT_STR_SORT_KEY)
4231-
cmds_doc: list[str] = []
4226+
cmds_cats: dict[str, list[str]] = defaultdict(list)
42324227
cmds_undoc: list[str] = []
4233-
cmds_cats: dict[str, list[str]] = {}
4228+
42344229
for command in visible_commands:
42354230
func = cast(CommandFunc, self.cmd_func(command))
42364231
has_help_func = False
@@ -4245,13 +4240,15 @@ def _build_command_info(self) -> tuple[dict[str, list[str]], list[str], list[str
42454240

42464241
if hasattr(func, constants.CMD_ATTR_HELP_CATEGORY):
42474242
category: str = getattr(func, constants.CMD_ATTR_HELP_CATEGORY)
4248-
cmds_cats.setdefault(category, [])
42494243
cmds_cats[category].append(command)
42504244
elif func.__doc__ or has_help_func or has_parser:
4251-
cmds_doc.append(command)
4245+
# Determine the category based on the defining class
4246+
defining_cls = get_defining_class(func)
4247+
category = getattr(defining_cls, 'DEFAULT_CATEGORY', self.DEFAULT_CATEGORY)
4248+
cmds_cats[category].append(command)
42524249
else:
42534250
cmds_undoc.append(command)
4254-
return cmds_cats, cmds_doc, cmds_undoc, help_topics
4251+
return cmds_cats, cmds_undoc, help_topics
42554252

42564253
@classmethod
42574254
def _build_help_parser(cls) -> Cmd2ArgumentParser:
@@ -4284,7 +4281,7 @@ def do_help(self, args: argparse.Namespace) -> None:
42844281
self.last_result = True
42854282

42864283
if not args.command or args.verbose:
4287-
cmds_cats, cmds_doc, cmds_undoc, help_topics = self._build_command_info()
4284+
cmds_cats, cmds_undoc, help_topics = self._build_command_info()
42884285

42894286
if self.doc_leader:
42904287
self.poutput()
@@ -4294,10 +4291,6 @@ def do_help(self, args: argparse.Namespace) -> None:
42944291
# Print any categories first and then the remaining documented commands.
42954292
sorted_categories = sorted(cmds_cats.keys(), key=utils.DEFAULT_STR_SORT_KEY)
42964293
all_cmds = {category: cmds_cats[category] for category in sorted_categories}
4297-
if all_cmds:
4298-
all_cmds[self.default_category] = cmds_doc
4299-
else:
4300-
all_cmds[self.doc_header] = cmds_doc
43014294

43024295
# Used to provide verbose table separation for better readability.
43034296
previous_table_printed = False
@@ -4312,8 +4305,8 @@ def do_help(self, args: argparse.Namespace) -> None:
43124305
if previous_table_printed and (help_topics or cmds_undoc):
43134306
self.poutput()
43144307

4315-
self.print_topics(self.misc_header, help_topics, 15, 80)
4316-
self.print_topics(self.undoc_header, cmds_undoc, 15, 80)
4308+
self.print_topics(self.MISC_HEADER, help_topics, 15, 80)
4309+
self.print_topics(self.UNDOC_HEADER, cmds_undoc, 15, 80)
43174310

43184311
else:
43194312
# Getting help for a specific command

cmd2/command_definition.py

Lines changed: 10 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -6,82 +6,34 @@
66
)
77
from typing import (
88
TYPE_CHECKING,
9+
ClassVar,
910
TypeAlias,
10-
TypeVar,
1111
)
1212

13-
from .constants import (
14-
CMDSET_ATTR_DEFAULT_HELP_CATEGORY,
15-
COMMAND_FUNC_PREFIX,
16-
)
1713
from .exceptions import CommandSetRegistrationError
1814
from .utils import Settable
1915

2016
if TYPE_CHECKING: # pragma: no cover
2117
from .cmd2 import Cmd
2218

23-
#: Callable signature for a basic command function
24-
#: Further refinements are needed to define the input parameters
19+
# Callable signature for a basic command function
20+
# Further refinements are needed to define the input parameters
2521
CommandFunc: TypeAlias = Callable[..., bool | None]
2622

27-
CommandSetType = TypeVar('CommandSetType', bound=type['CommandSet'])
28-
29-
30-
def with_default_category(category: str, *, heritable: bool = True) -> Callable[[CommandSetType], CommandSetType]:
31-
"""Apply a category to all ``do_*`` command methods in a class that do not already have a category specified (Decorator).
32-
33-
CommandSets that are decorated by this with `heritable` set to True (default) will set a class attribute that is
34-
inherited by all subclasses unless overridden. All commands of this CommandSet and all subclasses of this CommandSet
35-
that do not declare an explicit category will be placed in this category. Subclasses may use this decorator to
36-
override the default category.
37-
38-
If `heritable` is set to False, then only the commands declared locally to this CommandSet will be placed in the
39-
specified category. Dynamically created commands and commands declared in sub-classes will not receive this
40-
category.
41-
42-
:param category: category to put all uncategorized commands in
43-
:param heritable: Flag whether this default category should apply to sub-classes. Defaults to True
44-
:return: decorator function
45-
"""
46-
47-
def decorate_class(cls: CommandSetType) -> CommandSetType:
48-
if heritable:
49-
setattr(cls, CMDSET_ATTR_DEFAULT_HELP_CATEGORY, category)
50-
51-
import inspect
52-
53-
from .constants import CMD_ATTR_HELP_CATEGORY
54-
from .decorators import with_category
55-
56-
# get members of the class that meet the following criteria:
57-
# 1. Must be a function
58-
# 2. Must start with COMMAND_FUNC_PREFIX (do_)
59-
# 3. Must be a member of the class being decorated and not one inherited from a parent declaration
60-
methods = inspect.getmembers(
61-
cls,
62-
predicate=lambda meth: (
63-
inspect.isfunction(meth)
64-
and meth.__name__.startswith(COMMAND_FUNC_PREFIX)
65-
and meth in inspect.getmro(cls)[0].__dict__.values()
66-
),
67-
)
68-
category_decorator = with_category(category)
69-
for method in methods:
70-
if not hasattr(method[1], CMD_ATTR_HELP_CATEGORY):
71-
setattr(cls, method[0], category_decorator(method[1]))
72-
return cls
73-
74-
return decorate_class
75-
7623

7724
class CommandSet:
7825
"""Base class for defining sets of commands to load in cmd2.
7926
80-
``with_default_category`` can be used to apply a default category to all commands in the CommandSet.
81-
8227
``do_``, ``help_``, and ``complete_`` functions differ only in that self is the CommandSet instead of the cmd2 app
8328
"""
8429

30+
# Default category used for documented commands (those with a docstring,
31+
# help function, or argparse decorator) defined in this CommandSet that have
32+
# not been explicitly categorized with the @with_category decorator.
33+
# This value is inherited by subclasses but they can set their own
34+
# DEFAULT_CATEGORY to place their commands into a custom category.
35+
DEFAULT_CATEGORY: ClassVar[str] = "CommandSet Commands"
36+
8537
def __init__(self) -> None:
8638
"""Private reference to the CLI instance in which this CommandSet running.
8739

cmd2/constants.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,6 @@ def cmd2_public_attr_name(name: str) -> str:
7878
# Attached to a command function; defines whether tokens are unquoted before reaching argparse
7979
CMD_ATTR_PRESERVE_QUOTES = cmd2_private_attr_name('preserve_quotes')
8080

81-
# Attached to a CommandSet class; defines a default help category for its member functions
82-
CMDSET_ATTR_DEFAULT_HELP_CATEGORY = cmd2_private_attr_name('default_help_category')
83-
8481
# Attached to a subcommand function; defines the full command path to the parent (e.g., "foo" or "foo bar")
8582
SUBCMD_ATTR_COMMAND = cmd2_private_attr_name('parent_command')
8683

cmd2/decorators.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,17 @@ def _arg_swap(args: Sequence[Any], search_arg: Any, *replace_arg: Any) -> list[A
104104
return args_list
105105

106106

107-
#: Function signature for a command function that accepts a pre-processed argument list from user input
108-
#: and optionally returns a boolean
107+
# Function signature for a command function that accepts a pre-processed argument list from user input
108+
# and optionally returns a boolean
109109
ArgListCommandFuncOptionalBoolReturn: TypeAlias = Callable[[CmdOrSet, list[str]], bool | None]
110-
#: Function signature for a command function that accepts a pre-processed argument list from user input
111-
#: and returns a boolean
110+
# Function signature for a command function that accepts a pre-processed argument list from user input
111+
# and returns a boolean
112112
ArgListCommandFuncBoolReturn: TypeAlias = Callable[[CmdOrSet, list[str]], bool]
113-
#: Function signature for a command function that accepts a pre-processed argument list from user input
114-
#: and returns Nothing
113+
# Function signature for a command function that accepts a pre-processed argument list from user input
114+
# and returns Nothing
115115
ArgListCommandFuncNoneReturn: TypeAlias = Callable[[CmdOrSet, list[str]], None]
116116

117-
#: Aggregate of all accepted function signatures for command functions that accept a pre-processed argument list
117+
# Aggregate of all accepted function signatures for command functions that accept a pre-processed argument list
118118
ArgListCommandFunc: TypeAlias = (
119119
ArgListCommandFuncOptionalBoolReturn[CmdOrSet]
120120
| ArgListCommandFuncBoolReturn[CmdOrSet]
@@ -184,24 +184,24 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
184184
return arg_decorator
185185

186186

187-
#: Function signatures for command functions that use a Cmd2ArgumentParser to process user input
188-
#: and optionally return a boolean
187+
# Function signatures for command functions that use a Cmd2ArgumentParser to process user input
188+
# and optionally return a boolean
189189
ArgparseCommandFuncOptionalBoolReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace], bool | None]
190190
ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn: TypeAlias = Callable[
191191
[CmdOrSet, argparse.Namespace, list[str]], bool | None
192192
]
193193

194-
#: Function signatures for command functions that use a Cmd2ArgumentParser to process user input
195-
#: and return a boolean
194+
# Function signatures for command functions that use a Cmd2ArgumentParser to process user input
195+
# and return a boolean
196196
ArgparseCommandFuncBoolReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace], bool]
197197
ArgparseCommandFuncWithUnknownArgsBoolReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace, list[str]], bool]
198198

199-
#: Function signatures for command functions that use a Cmd2ArgumentParser to process user input
200-
#: and return nothing
199+
# Function signatures for command functions that use a Cmd2ArgumentParser to process user input
200+
# and return nothing
201201
ArgparseCommandFuncNoneReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace], None]
202202
ArgparseCommandFuncWithUnknownArgsNoneReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace, list[str]], None]
203203

204-
#: Aggregate of all accepted function signatures for an argparse command function
204+
# Aggregate of all accepted function signatures for an argparse command function
205205
ArgparseCommandFunc: TypeAlias = (
206206
ArgparseCommandFuncOptionalBoolReturn[CmdOrSet]
207207
| ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn[CmdOrSet]

0 commit comments

Comments
 (0)