summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Lin <anselor@gmail.com>2021-03-15 15:18:29 -0400
committeranselor <anselor@gmail.com>2021-03-18 18:26:20 -0400
commit9d1b7c7f1068ce9b55ba160ebceeadd665d1bc02 (patch)
tree2add05f8f7f955f2ba6aafaa640afccafe8f514a
parentf30627d5d2d0adc7db45aa26956372ea2cb3dc19 (diff)
downloadcmd2-git-9d1b7c7f1068ce9b55ba160ebceeadd665d1bc02.tar.gz
Some mypy validation fixes
-rw-r--r--cmd2/ansi.py21
-rw-r--r--cmd2/argparse_custom.py70
-rw-r--r--cmd2/clipboard.py8
-rw-r--r--cmd2/exceptions.py7
-rw-r--r--cmd2/plugin.py26
-rw-r--r--cmd2/py_bridge.py12
-rw-r--r--cmd2/utils.py85
-rw-r--r--docs/api/utils.rst2
-rw-r--r--tests_isolated/test_commandset/test_commandset.py2
9 files changed, 121 insertions, 112 deletions
diff --git a/cmd2/ansi.py b/cmd2/ansi.py
index 0f34016d..741d3b8b 100644
--- a/cmd2/ansi.py
+++ b/cmd2/ansi.py
@@ -13,15 +13,16 @@ from typing import (
Any,
List,
Union,
+ cast,
)
-import colorama
+import colorama # type: ignore [import]
from colorama import (
Back,
Fore,
Style,
)
-from wcwidth import (
+from wcwidth import ( # type: ignore [import]
wcswidth,
)
@@ -86,14 +87,14 @@ class ColorBase(Enum):
Support building a color string when self is the left operand
e.g. fg.blue + "hello"
"""
- return str(self) + other
+ return cast(str, str(self) + other)
def __radd__(self, other: Any) -> str:
"""
Support building a color string when self is the right operand
e.g. "hello" + fg.reset
"""
- return other + str(self)
+ return cast(str, other + str(self))
@classmethod
def colors(cls) -> List[str]:
@@ -194,7 +195,7 @@ def style_aware_wcswidth(text: str) -> int:
then this function returns -1. Replace tabs with spaces before calling this.
"""
# Strip ANSI style sequences since they cause wcswidth to return -1
- return wcswidth(strip_style(text))
+ return cast(int, wcswidth(strip_style(text)))
def widest_line(text: str) -> int:
@@ -217,7 +218,7 @@ def widest_line(text: str) -> int:
return max(lines_widths)
-def style_aware_write(fileobj: IO, msg: str) -> None:
+def style_aware_write(fileobj: IO[str], msg: str) -> None:
"""
Write a string to a fileobject and strip its ANSI style sequences if required by allow_style setting
@@ -229,7 +230,7 @@ def style_aware_write(fileobj: IO, msg: str) -> None:
fileobj.write(msg)
-def fg_lookup(fg_name: Union[str, fg]) -> str:
+def fg_lookup(fg_name: Union[str, fg]) -> Fore:
"""
Look up ANSI escape codes based on foreground color name.
@@ -247,7 +248,7 @@ def fg_lookup(fg_name: Union[str, fg]) -> str:
return ansi_escape
-def bg_lookup(bg_name: Union[str, bg]) -> str:
+def bg_lookup(bg_name: Union[str, bg]) -> Back:
"""
Look up ANSI escape codes based on background color name.
@@ -321,7 +322,7 @@ def style(
removals.append(UNDERLINE_DISABLE)
# Combine the ANSI style sequences with the text
- return "".join(additions) + text + "".join(removals)
+ return cast(str, "".join(additions) + text + "".join(removals))
# Default styles for printing strings of various types.
@@ -400,4 +401,4 @@ def set_title_str(title: str) -> str:
:param title: new title for the window
:return: string to write to sys.stderr in order to set the window title to the desired test
"""
- return colorama.ansi.set_title(title)
+ return cast(str, colorama.ansi.set_title(title))
diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py
index 2c71dec3..a9879d31 100644
--- a/cmd2/argparse_custom.py
+++ b/cmd2/argparse_custom.py
@@ -198,16 +198,21 @@ from argparse import (
ONE_OR_MORE,
ZERO_OR_MORE,
ArgumentError,
- _,
+)
+from gettext import (
+ gettext,
)
from typing import (
Any,
Callable,
+ Iterable,
+ List,
NoReturn,
Optional,
Tuple,
Type,
Union,
+ cast,
)
from . import (
@@ -261,11 +266,11 @@ class CompletionItem(str):
See header of this file for more information
"""
- def __new__(cls, value: object, *args, **kwargs) -> str:
- return super().__new__(cls, value)
+ def __new__(cls, value: object, *args: Any, **kwargs: Any) -> 'CompletionItem':
+ return cast(CompletionItem, super(CompletionItem, cls).__new__(cls, value)) # type: ignore [call-arg]
# noinspection PyUnusedLocal
- def __init__(self, value: object, desc: str = '', *args) -> None:
+ def __init__(self, value: object, desc: str = '', *args: Any) -> None:
"""
CompletionItem Initializer
@@ -287,7 +292,11 @@ class ChoicesCallable:
While argparse has the built-in choices attribute, it is limited to an iterable.
"""
- def __init__(self, is_completer: bool, to_call: Callable):
+ def __init__(
+ self,
+ is_completer: bool,
+ to_call: Union[Callable[[], List[str]], Callable[[str, str, int, int], List[str]]],
+ ) -> None:
"""
Initializer
:param is_completer: True if to_call is a tab completion routine which expects
@@ -319,12 +328,12 @@ def _set_choices_callable(action: argparse.Action, choices_callable: ChoicesCall
setattr(action, ATTR_CHOICES_CALLABLE, choices_callable)
-def set_choices_provider(action: argparse.Action, choices_provider: Callable) -> None:
+def set_choices_provider(action: argparse.Action, choices_provider: Callable[[], List[str]]) -> None:
"""Set choices_provider on an argparse action"""
_set_choices_callable(action, ChoicesCallable(is_completer=False, to_call=choices_provider))
-def set_completer(action: argparse.Action, completer: Callable) -> None:
+def set_completer(action: argparse.Action, completer: Callable[[str, str, int, int], List[str]]) -> None:
"""Set completer on an argparse action"""
_set_choices_callable(action, ChoicesCallable(is_completer=True, to_call=completer))
@@ -339,14 +348,14 @@ orig_actions_container_add_argument = argparse._ActionsContainer.add_argument
def _add_argument_wrapper(
- self,
- *args,
+ self: argparse._ActionsContainer,
+ *args: Any,
nargs: Union[int, str, Tuple[int], Tuple[int, int], Tuple[int, float], None] = None,
- choices_provider: Optional[Callable] = None,
- completer: Optional[Callable] = None,
+ choices_provider: Optional[Callable[[], List[str]]] = None,
+ completer: Optional[Callable[[str, str, int, int], List[str]]] = None,
suppress_tab_hint: bool = False,
descriptive_header: Optional[str] = None,
- **kwargs
+ **kwargs: Any
) -> argparse.Action:
"""
Wrapper around _ActionsContainer.add_argument() which supports more settings used by cmd2
@@ -392,6 +401,7 @@ def _add_argument_wrapper(
nargs_range = None
if nargs is not None:
+ nargs_adjusted: Union[int, str, Tuple[int], Tuple[int, int], Tuple[int, float], None]
# Check if nargs was given as a range
if isinstance(nargs, tuple):
@@ -402,11 +412,11 @@ def _add_argument_wrapper(
# Validate nargs tuple
if (
len(nargs) != 2
- or not isinstance(nargs[0], int)
- or not (isinstance(nargs[1], int) or nargs[1] == constants.INFINITY)
+ or not isinstance(nargs[0], int) # type: ignore[unreachable]
+ or not (isinstance(nargs[1], int) or nargs[1] == constants.INFINITY) # type: ignore[misc]
):
raise ValueError('Ranged values for nargs must be a tuple of 1 or 2 integers')
- if nargs[0] >= nargs[1]:
+ if nargs[0] >= nargs[1]: # type: ignore[misc]
raise ValueError('Invalid nargs range. The first value must be less than the second')
if nargs[0] < 0:
raise ValueError('Negative numbers are invalid for nargs range')
@@ -414,7 +424,7 @@ def _add_argument_wrapper(
# Save the nargs tuple as our range setting
nargs_range = nargs
range_min = nargs_range[0]
- range_max = nargs_range[1]
+ range_max = nargs_range[1] # type: ignore[misc]
# Convert nargs into a format argparse recognizes
if range_min == 0:
@@ -460,7 +470,7 @@ def _add_argument_wrapper(
# Overwrite _ActionsContainer.add_argument with our wrapper
# noinspection PyProtectedMember
-argparse._ActionsContainer.add_argument = _add_argument_wrapper
+setattr(argparse._ActionsContainer, 'add_argument', _add_argument_wrapper)
############################################################################################################
# Patch ArgumentParser._get_nargs_pattern with our wrapper to nargs ranges
@@ -472,7 +482,7 @@ orig_argument_parser_get_nargs_pattern = argparse.ArgumentParser._get_nargs_patt
# noinspection PyProtectedMember
-def _get_nargs_pattern_wrapper(self, action) -> str:
+def _get_nargs_pattern_wrapper(self: argparse.ArgumentParser, action: argparse.Action) -> str:
# Wrapper around ArgumentParser._get_nargs_pattern behavior to support nargs ranges
nargs_range = getattr(action, ATTR_NARGS_RANGE, None)
if nargs_range is not None:
@@ -494,7 +504,7 @@ def _get_nargs_pattern_wrapper(self, action) -> str:
# Overwrite ArgumentParser._get_nargs_pattern with our wrapper
# noinspection PyProtectedMember
-argparse.ArgumentParser._get_nargs_pattern = _get_nargs_pattern_wrapper
+setattr(argparse.ArgumentParser, '_get_nargs_pattern', _get_nargs_pattern_wrapper)
############################################################################################################
@@ -505,7 +515,7 @@ orig_argument_parser_match_argument = argparse.ArgumentParser._match_argument
# noinspection PyProtectedMember
-def _match_argument_wrapper(self, action, arg_strings_pattern) -> int:
+def _match_argument_wrapper(self: argparse.ArgumentParser, action: argparse.Action, arg_strings_pattern: str) -> int:
# Wrapper around ArgumentParser._match_argument behavior to support nargs ranges
nargs_pattern = self._get_nargs_pattern(action)
match = re.match(nargs_pattern, arg_strings_pattern)
@@ -521,7 +531,7 @@ def _match_argument_wrapper(self, action, arg_strings_pattern) -> int:
# Overwrite ArgumentParser._match_argument with our wrapper
# noinspection PyProtectedMember
-argparse.ArgumentParser._match_argument = _match_argument_wrapper
+setattr(argparse.ArgumentParser, '_match_argument', _match_argument_wrapper)
############################################################################################################
@@ -529,7 +539,7 @@ argparse.ArgumentParser._match_argument = _match_argument_wrapper
############################################################################################################
# noinspection PyPep8Naming
-def _SubParsersAction_remove_parser(self, name: str):
+def _SubParsersAction_remove_parser(self: argparse._SubParsersAction, name: str) -> None:
"""
Removes a sub-parser from a sub-parsers group
@@ -572,20 +582,26 @@ setattr(argparse._SubParsersAction, 'remove_parser', _SubParsersAction_remove_pa
class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
"""Custom help formatter to configure ordering of help text"""
- def _format_usage(self, usage, actions, groups, prefix) -> str:
+ def _format_usage(
+ self,
+ usage: Optional[str],
+ actions: Iterable[argparse.Action],
+ groups: Iterable[argparse._ArgumentGroup],
+ prefix: Optional[str] = None,
+ ) -> str:
if prefix is None:
- prefix = _('Usage: ')
+ prefix = gettext('Usage: ')
# if usage is specified, use that
if usage is not None:
usage %= dict(prog=self._prog)
# if no optionals or positionals are available, usage is just prog
- elif usage is None and not actions:
+ elif not actions:
usage = '%(prog)s' % dict(prog=self._prog)
# if optionals and positionals are available, calculate usage
- elif usage is None:
+ else:
prog = '%(prog)s' % dict(prog=self._prog)
# split optionals from positionals
@@ -630,7 +646,7 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
# helper for wrapping lines
# noinspection PyMissingOrEmptyDocstring,PyShadowingNames
- def get_lines(parts, indent, prefix=None):
+ def get_lines(parts: List[str], indent: str, prefix: Optional[str] = None):
lines = []
line = []
if prefix is not None:
diff --git a/cmd2/clipboard.py b/cmd2/clipboard.py
index 03931724..f31f5d5a 100644
--- a/cmd2/clipboard.py
+++ b/cmd2/clipboard.py
@@ -2,7 +2,11 @@
"""
This module provides basic ability to copy from and paste to the clipboard/pastebuffer.
"""
-import pyperclip
+from typing import (
+ cast,
+)
+
+import pyperclip # type: ignore [import]
# noinspection PyProtectedMember
from pyperclip import (
@@ -26,7 +30,7 @@ def get_paste_buffer() -> str:
:return: contents of the clipboard
"""
- pb_str = pyperclip.paste()
+ pb_str = cast(str, pyperclip.paste())
return pb_str
diff --git a/cmd2/exceptions.py b/cmd2/exceptions.py
index b45167d1..011f53bd 100644
--- a/cmd2/exceptions.py
+++ b/cmd2/exceptions.py
@@ -1,6 +1,9 @@
# coding=utf-8
"""Custom exceptions for cmd2"""
+from typing import (
+ Any,
+)
############################################################################################################
# The following exceptions are part of the public API
@@ -49,7 +52,7 @@ class CompletionError(Exception):
- Tab completion hints
"""
- def __init__(self, *args, apply_style: bool = True):
+ def __init__(self, *args: Any, apply_style: bool = True) -> None:
"""
Initializer for CompletionError
:param apply_style: If True, then ansi.style_error will be applied to the message text when printed.
@@ -68,7 +71,7 @@ class PassThroughException(Exception):
This class is used to wrap an exception that should be raised instead of printed.
"""
- def __init__(self, *args, wrapped_ex: BaseException):
+ def __init__(self, *args: Any, wrapped_ex: BaseException) -> None:
"""
Initializer for PassThroughException
:param wrapped_ex: the exception that will be raised
diff --git a/cmd2/plugin.py b/cmd2/plugin.py
index e836b9d1..59169050 100644
--- a/cmd2/plugin.py
+++ b/cmd2/plugin.py
@@ -3,33 +3,37 @@
"""Classes for the cmd2 plugin system"""
import attr
+from .parsing import (
+ Statement,
+)
-@attr.s
+
+@attr.s(auto_attribs=True)
class PostparsingData:
"""Data class containing information passed to postparsing hook methods"""
- stop = attr.ib()
- statement = attr.ib()
+ stop: bool
+ statement: Statement
-@attr.s
+@attr.s(auto_attribs=True)
class PrecommandData:
"""Data class containing information passed to precommand hook methods"""
- statement = attr.ib()
+ statement: Statement
-@attr.s
+@attr.s(auto_attribs=True)
class PostcommandData:
"""Data class containing information passed to postcommand hook methods"""
- stop = attr.ib()
- statement = attr.ib()
+ stop: bool
+ statement: Statement
-@attr.s
+@attr.s(auto_attribs=True)
class CommandFinalizationData:
"""Data class containing information passed to command finalization hook methods"""
- stop = attr.ib()
- statement = attr.ib()
+ stop: bool
+ statement: Statement
diff --git a/cmd2/py_bridge.py b/cmd2/py_bridge.py
index fd9b55fb..890363b0 100644
--- a/cmd2/py_bridge.py
+++ b/cmd2/py_bridge.py
@@ -10,16 +10,17 @@ from contextlib import (
redirect_stdout,
)
from typing import (
+ Any,
+ NamedTuple,
Optional,
)
-from .utils import (
+from .utils import ( # namedtuple_with_defaults,
StdSim,
- namedtuple_with_defaults,
)
-class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr', 'stop', 'data'])):
+class CommandResult(NamedTuple):
"""Encapsulates the results from a cmd2 app command
:stdout: str - output captured from stdout while this command is executing
@@ -56,6 +57,11 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr
not for modification.
"""
+ stdout: str = ''
+ stderr: str = ''
+ stop: bool = False
+ data: Any = None
+
def __bool__(self) -> bool:
"""Returns True if the command succeeded, otherwise False"""
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 1008cb86..717d73b4 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -3,7 +3,6 @@
import argparse
import collections
-import collections.abc as collections_abc
import functools
import glob
import inspect
@@ -19,24 +18,27 @@ from enum import (
)
from typing import (
IO,
+ TYPE_CHECKING,
Any,
Callable,
Dict,
Iterable,
List,
- NamedTuple,
Optional,
TextIO,
Type,
- TYPE_CHECKING,
+ TypeVar,
Union,
+ cast,
)
from . import (
constants,
)
+
if TYPE_CHECKING: # pragma: no cover
- import cmd2
+ import cmd2 # noqa: F401
+
def is_quoted(arg: str) -> bool:
"""
@@ -100,15 +102,15 @@ class Settable:
def __init__(
self,
name: str,
- val_type: Callable,
+ val_type: Union[Type[Any], Callable[[Any], Any]],
description: str,
*,
settable_object: Optional[object] = None,
settable_attrib_name: Optional[str] = None,
- onchange_cb: Callable[[str, Any, Any], Any] = None,
- choices: Iterable = None,
- choices_provider: Optional[Callable] = None,
- completer: Optional[Callable] = None
+ onchange_cb: Optional[Callable[[str, Any, Any], Any]] = None,
+ choices: Optional[Iterable[Any]] = None,
+ choices_provider: Optional[Callable[[], List[str]]] = None,
+ completer: Optional[Callable[[str, str, int, int], List[str]]] = None
):
"""
Settable Initializer
@@ -174,36 +176,6 @@ class Settable:
return new_value
-def namedtuple_with_defaults(typename: str, field_names: Union[str, List[str]], default_values: collections_abc.Iterable = ()):
- """
- Convenience function for defining a namedtuple with default values
-
- From: https://stackoverflow.com/questions/11351032/namedtuple-and-default-values-for-optional-keyword-arguments
-
- Examples:
- >>> Node = namedtuple_with_defaults('Node', 'val left right')
- >>> Node()
- Node(val=None, left=None, right=None)
- >>> Node = namedtuple_with_defaults('Node', 'val left right', [1, 2, 3])
- >>> Node()
- Node(val=1, left=2, right=3)
- >>> Node = namedtuple_with_defaults('Node', 'val left right', {'right':7})
- >>> Node()
- Node(val=None, left=None, right=7)
- >>> Node(4)
- Node(val=4, left=None, right=7)
- """
- T: NamedTuple = collections.namedtuple(typename, field_names)
- # noinspection PyProtectedMember,PyUnresolvedReferences
- T.__new__.__defaults__ = (None,) * len(T._fields)
- if isinstance(default_values, collections_abc.Mapping):
- prototype = T(**default_values)
- else:
- prototype = T(*default_values)
- T.__new__.__defaults__ = tuple(prototype)
- return T
-
-
def is_text_file(file_path: str) -> bool:
"""Returns if a file contains only ASCII or UTF-8 encoded text.
@@ -241,13 +213,16 @@ def is_text_file(file_path: str) -> bool:
return valid_text_file
-def remove_duplicates(list_to_prune: List) -> List:
+_T = TypeVar('_T')
+
+
+def remove_duplicates(list_to_prune: List[_T]) -> List[_T]:
"""Removes duplicates from a list while preserving order of the items.
:param list_to_prune: the list being pruned of duplicates
:return: The pruned list
"""
- temp_dict = collections.OrderedDict()
+ temp_dict: collections.OrderedDict[_T, Any] = collections.OrderedDict()
for item in list_to_prune:
temp_dict[item] = None
@@ -405,7 +380,7 @@ def find_editor() -> Optional[str]:
return editor
-def files_from_glob_pattern(pattern: str, access=os.F_OK) -> List[str]:
+def files_from_glob_pattern(pattern: str, access: int = os.F_OK) -> List[str]:
"""Return a list of file paths based on a glob pattern.
Only files are returned, not directories, and optionally only files for which the user has a specified access to.
@@ -417,7 +392,7 @@ def files_from_glob_pattern(pattern: str, access=os.F_OK) -> List[str]:
return [f for f in glob.glob(pattern) if os.path.isfile(f) and os.access(f, access)]
-def files_from_glob_patterns(patterns: List[str], access=os.F_OK) -> List[str]:
+def files_from_glob_patterns(patterns: List[str], access: int = os.F_OK) -> List[str]:
"""Return a list of file paths based on a list of glob patterns.
Only files are returned, not directories, and optionally only files for which the user has a specified access to.
@@ -472,7 +447,7 @@ class StdSim:
Stores contents in internal buffer and optionally echos to the inner stream it is simulating.
"""
- def __init__(self, inner_stream, *, echo: bool = False, encoding: str = 'utf-8', errors: str = 'replace') -> None:
+ def __init__(self, inner_stream: TextIO, *, echo: bool = False, encoding: str = 'utf-8', errors: str = 'replace') -> None:
"""
StdSim Initializer
:param inner_stream: the wrapped stream. Should be a TextIO or StdSim instance.
@@ -540,11 +515,11 @@ class StdSim:
when running unit tests because pytest sets stdout to a pytest EncodedFile object.
"""
try:
- return self.inner_stream.line_buffering
+ return bool(self.inner_stream.line_buffering)
except AttributeError:
return False
- def __getattr__(self, item: str):
+ def __getattr__(self, item: str) -> Any:
if item in self.__dict__:
return self.__dict__[item]
else:
@@ -701,7 +676,7 @@ class ContextFlag:
def __enter__(self) -> None:
self.__count += 1
- def __exit__(self, *args) -> None:
+ def __exit__(self, *args: Any) -> None:
self.__count -= 1
if self.__count < 0:
raise ValueError("count has gone below 0")
@@ -1060,7 +1035,7 @@ def get_styles_in_text(text: str) -> Dict[int, str]:
return styles
-def categorize(func: Union[Callable, Iterable[Callable]], category: str) -> None:
+def categorize(func: Union[Callable[..., Any], Iterable[Callable[..., Any]]], category: str) -> None:
"""Categorize a function.
The help command output will group the passed function under the
@@ -1085,13 +1060,13 @@ def categorize(func: Union[Callable, Iterable[Callable]], category: str) -> None
for item in func:
setattr(item, constants.CMD_ATTR_HELP_CATEGORY, category)
else:
- if inspect.ismethod(func):
- setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category)
+ if inspect.ismethod(func) and hasattr(func, '__func__'):
+ setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category) # type: ignore[attr-defined]
else:
setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category)
-def get_defining_class(meth: Callable) -> Optional[Type]:
+def get_defining_class(meth: Callable[..., Any]) -> Optional[Type[Any]]:
"""
Attempts to resolve the class that defined a method.
@@ -1104,9 +1079,11 @@ def get_defining_class(meth: Callable) -> Optional[Type]:
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__')
+ inspect.isbuiltin(meth)
+ and getattr(meth, '__self__') is not None
+ and getattr(meth.__self__, '__class__') # type: ignore[attr-defined]
):
- for cls in inspect.getmro(meth.__self__.__class__):
+ for cls in inspect.getmro(meth.__self__.__class__): # type: ignore[attr-defined]
if meth.__name__ in cls.__dict__:
return cls
meth = getattr(meth, '__func__', meth) # fallback to __qualname__ parsing
@@ -1114,7 +1091,7 @@ def get_defining_class(meth: Callable) -> Optional[Type]:
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
+ return cast(type, getattr(meth, '__objclass__', None)) # handle special descriptor objects
class CompletionMode(Enum):
diff --git a/docs/api/utils.rst b/docs/api/utils.rst
index 81c978c9..f9092d8d 100644
--- a/docs/api/utils.rst
+++ b/docs/api/utils.rst
@@ -90,8 +90,6 @@ Miscellaneous
.. autofunction:: cmd2.utils.str_to_bool
-.. autofunction:: cmd2.utils.namedtuple_with_defaults
-
.. autofunction:: cmd2.utils.categorize
.. autofunction:: cmd2.utils.remove_duplicates
diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py
index 85608248..5bb68b08 100644
--- a/tests_isolated/test_commandset/test_commandset.py
+++ b/tests_isolated/test_commandset/test_commandset.py
@@ -20,10 +20,10 @@ from cmd2.exceptions import (
)
from .conftest import (
+ WithCommandSets,
complete_tester,
normalize,
run_cmd,
- WithCommandSets,
)