-
Notifications
You must be signed in to change notification settings - Fork 124
Help categories #348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Help categories #348
Changes from all commits
5d1e981
ef23633
62d8c23
5e349dc
4f68eb2
da28564
f4f2b9e
2ffd342
52bf16c
33decb4
e4ca6c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,7 +56,7 @@ | |
| # Collection is a container that is sizable and iterable | ||
| # It was introduced in Python 3.6. We will try to import it, otherwise use our implementation | ||
| try: | ||
| from collections.abc import Collection | ||
| from collections.abc import Collection, Iterable | ||
| except ImportError: | ||
|
|
||
| if six.PY3: | ||
|
|
@@ -79,7 +79,6 @@ def __subclasshook__(cls, C): | |
| return True | ||
| return NotImplemented | ||
|
|
||
|
|
||
| # Newer versions of pyperclip are released as a single file, but older versions had a more complicated structure | ||
| try: | ||
| from pyperclip.exceptions import PyperclipException | ||
|
|
@@ -113,6 +112,11 @@ def __subclasshook__(cls, C): | |
| else: | ||
| from contextlib import redirect_stdout, redirect_stderr | ||
|
|
||
| if sys.version_info > (3, 0): | ||
| from io import StringIO # Python3 | ||
| else: | ||
| from io import BytesIO as StringIO # Python2 | ||
|
|
||
| # Detect whether IPython is installed to determine if the built-in "ipy" command should be included | ||
| ipython_available = True | ||
| try: | ||
|
|
@@ -183,6 +187,7 @@ class RlType(Enum): | |
| except ImportError: | ||
| pass | ||
|
|
||
|
|
||
| __version__ = '0.8.4' | ||
|
|
||
| # Pyparsing enablePackrat() can greatly speed up parsing, but problems have been seen in Python 3 in the past | ||
|
|
@@ -210,6 +215,25 @@ class RlType(Enum): | |
| QUOTES = ['"', "'"] | ||
| REDIRECTION_CHARS = ['|', '<', '>'] | ||
|
|
||
| # optional attribute, when tagged on a function, allows cmd2 to categorize commands | ||
| HELP_CATEGORY = 'help_category' | ||
| HELP_SUMMARY = 'help_summary' | ||
|
|
||
|
|
||
| def categorize(func, category): | ||
| """Categorize a function. | ||
|
|
||
| The help command output will group this function under the specified category heading | ||
|
|
||
| :param func: Union[Callable, Iterable] - function to categorize | ||
| :param category: str - category to put it in | ||
| """ | ||
| if isinstance(func, Iterable): | ||
| for item in func: | ||
| setattr(item, HELP_CATEGORY, category) | ||
| else: | ||
| setattr(func, HELP_CATEGORY, category) | ||
|
|
||
|
|
||
| def set_posix_shlex(val): | ||
| """ Allows user of cmd2 to choose between POSIX and non-POSIX splitting of args for decorated commands. | ||
|
|
@@ -340,6 +364,14 @@ def parse_quoted_string(cmdline): | |
| return lexed_arglist | ||
|
|
||
|
|
||
| def with_category(category): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that you added a decorator, that should make the code clean and easy to read. I strongly encourage you to make one simple change to your decorator to help avoid order-of-operations dependencies when multiple decorators are involved. You should apply the "@functools.wraps(func)" decorator to your internal cat_decorator function - see how we do it in other decorators below such as with_argument_list: def with_category(category):
"""A decorator to apply a category to a command function"""
@functools.wraps(func)
def cat_decorator(func):
categorize(func, category)
return func
return cat_decorator
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that's necessary in this situation because this decorator function is truly a decorator function rather than a wrapping function. This adds an attribute the the passed-in function and returns it.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You make a good point, your decorator is mutating and returning the original function. I think you are fine as-is. |
||
| """A decorator to apply a category to a command function""" | ||
| def cat_decorator(func): | ||
| categorize(func, category) | ||
| return func | ||
| return cat_decorator | ||
|
|
||
|
|
||
| def with_argument_list(func): | ||
| """A decorator to alter the arguments passed to a do_* cmd2 | ||
| method. Default passes a string of whatever the user typed. | ||
|
|
@@ -378,6 +410,9 @@ def cmd_wrapper(instance, cmdline): | |
| if argparser.description is None and func.__doc__: | ||
| argparser.description = func.__doc__ | ||
|
|
||
| if func.__doc__: | ||
| setattr(cmd_wrapper, HELP_SUMMARY, func.__doc__) | ||
|
|
||
| cmd_wrapper.__doc__ = argparser.format_help() | ||
|
|
||
| # Mark this function as having an argparse ArgumentParser (used by do_help) | ||
|
|
@@ -417,6 +452,9 @@ def cmd_wrapper(instance, cmdline): | |
| if argparser.description is None and func.__doc__: | ||
| argparser.description = func.__doc__ | ||
|
|
||
| if func.__doc__: | ||
| setattr(cmd_wrapper, HELP_SUMMARY, func.__doc__) | ||
|
|
||
| cmd_wrapper.__doc__ = argparser.format_help() | ||
|
|
||
| # Mark this function as having an argparse ArgumentParser (used by do_help) | ||
|
|
@@ -2875,7 +2913,10 @@ def complete_unalias(self, text, line, begidx, endidx): | |
| @with_argument_list | ||
| def do_help(self, arglist): | ||
| """List available commands with "help" or detailed help with "help cmd".""" | ||
| if arglist: | ||
| if not arglist or (len(arglist) == 1 and arglist[0] in ('--verbose', '-v')): | ||
| verbose = len(arglist) == 1 and arglist[0] in ('--verbose', '-v') | ||
| self._help_menu(verbose) | ||
| else: | ||
| # Getting help for a specific command | ||
| funcname = self._func_named(arglist[0]) | ||
| if funcname: | ||
|
|
@@ -2896,11 +2937,8 @@ def do_help(self, arglist): | |
| else: | ||
| # This could be a help topic | ||
| cmd.Cmd.do_help(self, arglist[0]) | ||
| else: | ||
| # Show a menu of what commands help can be gotten for | ||
| self._help_menu() | ||
|
|
||
| def _help_menu(self): | ||
| def _help_menu(self, verbose=False): | ||
| """Show a list of commands which help can be displayed for. | ||
| """ | ||
| # Get a sorted list of help topics | ||
|
|
@@ -2913,21 +2951,107 @@ def _help_menu(self): | |
|
|
||
| cmds_doc = [] | ||
| cmds_undoc = [] | ||
| cmds_cats = {} | ||
|
|
||
| for command in visible_commands: | ||
| if command in help_topics: | ||
| cmds_doc.append(command) | ||
| help_topics.remove(command) | ||
| elif getattr(self, self._func_named(command)).__doc__: | ||
| cmds_doc.append(command) | ||
| if command in help_topics or getattr(self, self._func_named(command)).__doc__: | ||
| if command in help_topics: | ||
| help_topics.remove(command) | ||
| if hasattr(getattr(self, self._func_named(command)), HELP_CATEGORY): | ||
| category = getattr(getattr(self, self._func_named(command)), HELP_CATEGORY) | ||
| cmds_cats.setdefault(category, []) | ||
| cmds_cats[category].append(command) | ||
| else: | ||
| cmds_doc.append(command) | ||
| else: | ||
| cmds_undoc.append(command) | ||
|
|
||
| self.poutput("%s\n" % str(self.doc_leader)) | ||
| self.print_topics(self.doc_header, cmds_doc, 15, 80) | ||
| if len(cmds_cats) == 0: | ||
| # No categories found, fall back to standard behavior | ||
| self.poutput("{}\n".format(str(self.doc_leader))) | ||
| self._print_topics(self.doc_header, cmds_doc, verbose) | ||
| else: | ||
| # Categories found, Organize all commands by category | ||
| self.poutput('{}\n'.format(str(self.doc_leader))) | ||
| self.poutput('{}\n\n'.format(str(self.doc_header))) | ||
| for category in sorted(cmds_cats.keys()): | ||
| self._print_topics(category, cmds_cats[category], verbose) | ||
| self._print_topics('Other', cmds_doc, verbose) | ||
|
|
||
| self.print_topics(self.misc_header, help_topics, 15, 80) | ||
| self.print_topics(self.undoc_header, cmds_undoc, 15, 80) | ||
|
|
||
| def _print_topics(self, header, cmds, verbose): | ||
| """Customized version of print_topics that can switch between verbose or traditional output""" | ||
| if cmds: | ||
| if not verbose: | ||
| self.print_topics(header, cmds, 15, 80) | ||
| else: | ||
| self.stdout.write('{}\n'.format(str(header))) | ||
| widest = 0 | ||
| # measure the commands | ||
| for command in cmds: | ||
| width = len(command) | ||
| if width > widest: | ||
| widest = width | ||
| # add a 4-space pad | ||
| widest += 4 | ||
| if widest < 20: | ||
| widest = 20 | ||
|
|
||
| if self.ruler: | ||
| self.stdout.write('{:{ruler}<{width}}\n'.format('', ruler=self.ruler, width=80)) | ||
|
|
||
| help_topics = self.get_help_topics() | ||
| for command in cmds: | ||
| doc = '' | ||
| # Try to get the documentation string | ||
| try: | ||
| # first see if there's a help function implemented | ||
| func = getattr(self, 'help_' + command) | ||
| except AttributeError: | ||
| # Couldn't find a help function | ||
| try: | ||
| # Now see if help_summary has been set | ||
| doc = getattr(self, self._func_named(command)).help_summary | ||
| except AttributeError: | ||
| # Last, try to directly ac cess the function's doc-string | ||
| doc = getattr(self, self._func_named(command)).__doc__ | ||
| else: | ||
| # we found the help function | ||
| result = StringIO() | ||
| # try to redirect system stdout | ||
| with redirect_stdout(result): | ||
| # save our internal stdout | ||
| stdout_orig = self.stdout | ||
| try: | ||
| # redirect our internal stdout | ||
| self.stdout = result | ||
| func() | ||
| finally: | ||
| # restore internal stdout | ||
| self.stdout = stdout_orig | ||
| doc = result.getvalue() | ||
|
|
||
| # Attempt to locate the first documentation block | ||
| doc_block = [] | ||
| found_first = False | ||
| for doc_line in doc.splitlines(): | ||
| str(doc_line).strip() | ||
| if len(doc_line.strip()) > 0: | ||
| doc_block.append(doc_line.strip()) | ||
| found_first = True | ||
| else: | ||
| if found_first: | ||
| break | ||
|
|
||
| for doc_line in doc_block: | ||
| self.stdout.write('{: <{col_width}}{doc}\n'.format(command, | ||
| col_width=widest, | ||
| doc=doc_line)) | ||
| command = '' | ||
| self.stdout.write("\n") | ||
|
|
||
| def do_shortcuts(self, _): | ||
| """Lists shortcuts (aliases) available.""" | ||
| result = "\n".join('%s: %s' % (sc[0], sc[1]) for sc in sorted(self.shortcuts)) | ||
|
|
@@ -3025,7 +3149,7 @@ def show(self, args, parameter): | |
| else: | ||
| raise LookupError("Parameter '%s' not supported (type 'show' for list of parameters)." % param) | ||
|
|
||
| set_parser = argparse.ArgumentParser() | ||
| set_parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) | ||
| set_parser.add_argument('-a', '--all', action='store_true', help='display read-only settings as well') | ||
| set_parser.add_argument('-l', '--long', action='store_true', help='describe function of parameter') | ||
| set_parser.add_argument('settable', nargs='*', help='[param_name] [value]') | ||
|
|
@@ -3194,6 +3318,8 @@ def cmd_with_subs_completer(self, text, line, begidx, endidx): | |
| # noinspection PyBroadException | ||
| def do_py(self, arg): | ||
| """ | ||
| Invoke python command, shell, or script | ||
|
|
||
| py <command>: Executes a Python command. | ||
| py: Enters interactive Python mode. | ||
| End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, using the
sixmodule can help avoid Python 2 vs 3 compatibility gymnastics such as this.sixis a Python 2 vs 3 compatibility module (6 == 2 * 3 == 3 * 2).