summaryrefslogtreecommitdiff
path: root/cmd2
diff options
context:
space:
mode:
Diffstat (limited to 'cmd2')
-rw-r--r--cmd2/__init__.py5
-rw-r--r--cmd2/argparse_completer.py45
-rw-r--r--cmd2/exceptions.py4
-rw-r--r--cmd2/utils.py31
4 files changed, 75 insertions, 10 deletions
diff --git a/cmd2/__init__.py b/cmd2/__init__.py
index 19e620be..9f0bb176 100644
--- a/cmd2/__init__.py
+++ b/cmd2/__init__.py
@@ -30,8 +30,9 @@ from .argparse_custom import DEFAULT_ARGUMENT_PARSER
from .cmd2 import Cmd
from .command_definition import CommandSet, with_default_category
from .constants import COMMAND_NAME, DEFAULT_SHORTCUTS
-from .decorators import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category, as_subcommand_to
-from .exceptions import Cmd2ArgparseError, SkipPostcommandHooks
+from .decorators import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category, \
+ as_subcommand_to
+from .exceptions import Cmd2ArgparseError, SkipPostcommandHooks, CommandSetRegistrationError
from . import plugin
from .parsing import Statement
from .py_bridge import CommandResult
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index 0225d22f..f14e83fd 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -25,7 +25,7 @@ from .argparse_custom import (
)
from .command_definition import CommandSet
from .table_creator import Column, SimpleTable
-from .utils import CompletionError, basic_complete
+from .utils import CompletionError, basic_complete, get_defining_class
# If no descriptive header is supplied, then this will be used instead
DEFAULT_DESCRIPTIVE_HEADER = 'Description'
@@ -569,12 +569,43 @@ class ArgparseCompleter:
kwargs = {}
if isinstance(arg_choices, ChoicesCallable):
if arg_choices.is_method:
- cmd_set = getattr(self._parser, constants.PARSER_ATTR_COMMANDSET, cmd_set)
- if cmd_set is not None:
- if isinstance(cmd_set, CommandSet):
- # If command is part of a CommandSet, `self` should be the CommandSet and Cmd will be next
- if cmd_set is not None:
- args.append(cmd_set)
+ # figure out what class the completer was defined in
+ completer_class = get_defining_class(arg_choices.to_call)
+
+ # Was there a defining class identified? If so, is it a sub-class of CommandSet?
+ if completer_class is not None and issubclass(completer_class, CommandSet):
+ # Since the completer function is provided as an unbound function, we need to locate the instance
+ # of the CommandSet to pass in as `self` to emulate a bound method call.
+ # We're searching for candidates that match the completer function's parent type in this order:
+ # 1. Does the CommandSet registered with the command's argparser match as a subclass?
+ # 2. Do any of the registered CommandSets in the Cmd2 application exactly match the type?
+ # 3. Is there a registered CommandSet that is is the only matching subclass?
+
+ # Now get the CommandSet associated with the current command/subcommand argparser
+ parser_cmd_set = getattr(self._parser, constants.PARSER_ATTR_COMMANDSET, cmd_set)
+ if isinstance(parser_cmd_set, completer_class):
+ # Case 1: Parser's CommandSet is a sub-class of the completer function's CommandSet
+ cmd_set = parser_cmd_set
+ else:
+ # Search all registered CommandSets
+ cmd_set = None
+ candidate_sets = [] # type: List[CommandSet]
+ for installed_cmd_set in self._cmd2_app._installed_command_sets:
+ if type(installed_cmd_set) == completer_class:
+ # Case 2: CommandSet is an exact type match for the completer's CommandSet
+ cmd_set = installed_cmd_set
+ break
+
+ # Add candidate for Case 3:
+ if isinstance(installed_cmd_set, completer_class):
+ candidate_sets.append(installed_cmd_set)
+ if cmd_set is None and len(candidate_sets) == 1:
+ # Case 3: There exists exactly 1 CommandSet that is a subclass of the completer's CommandSet
+ cmd_set = candidate_sets[0]
+ if cmd_set is None:
+ # No cases matched, raise an error
+ raise CompletionError('Could not find CommandSet instance matching defining type for completer')
+ args.append(cmd_set)
args.append(self._cmd2_app)
# Check if arg_choices.to_call expects arg_tokens
diff --git a/cmd2/exceptions.py b/cmd2/exceptions.py
index b928f293..d253985a 100644
--- a/cmd2/exceptions.py
+++ b/cmd2/exceptions.py
@@ -25,6 +25,10 @@ class Cmd2ArgparseError(SkipPostcommandHooks):
class CommandSetRegistrationError(Exception):
+ """
+ Exception that can be thrown when an error occurs while a CommandSet is being added or removed
+ from a cmd2 application.
+ """
pass
############################################################################################################
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 5a4fdbf7..39dc6e2b 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -3,7 +3,9 @@
import collections
import collections.abc as collections_abc
+import functools
import glob
+import inspect
import os
import re
import subprocess
@@ -11,7 +13,7 @@ import sys
import threading
import unicodedata
from enum import Enum
-from typing import Any, Callable, Dict, Iterable, List, Optional, TextIO, Union
+from typing import Any, Callable, Dict, Iterable, List, Optional, TextIO, Type, Union
from . import constants
@@ -1037,3 +1039,30 @@ def categorize(func: Union[Callable, Iterable[Callable]], category: str) -> None
setattr(item, constants.CMD_ATTR_HELP_CATEGORY, category)
else:
setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category)
+
+
+def get_defining_class(meth: Callable) -> Optional[Type]:
+ """
+ Attempts to resolve the class that defined a method.
+
+ Inspired by implementation published here:
+ https://stackoverflow.com/a/25959545/1956611
+
+ :param meth: method to inspect
+ :return: class type in which the supplied method was defined. None if it couldn't be resolved.
+ """
+ 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__')):
+ for cls in inspect.getmro(meth.__self__.__class__):
+ if meth.__name__ in cls.__dict__:
+ return cls
+ meth = getattr(meth, '__func__', meth) # fallback to __qualname__ parsing
+ if inspect.isfunction(meth):
+ cls = getattr(inspect.getmodule(meth),
+ meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0])
+ if isinstance(cls, type):
+ return cls
+ return getattr(meth, '__objclass__', None) # handle special descriptor objects