5656# Collection is a container that is sizable and iterable
5757# It was introduced in Python 3.6. We will try to import it, otherwise use our implementation
5858try :
59- from collections .abc import Collection
59+ from collections .abc import Collection , Iterable
6060except ImportError :
6161
6262 if six .PY3 :
@@ -79,7 +79,6 @@ def __subclasshook__(cls, C):
7979 return True
8080 return NotImplemented
8181
82-
8382# Newer versions of pyperclip are released as a single file, but older versions had a more complicated structure
8483try :
8584 from pyperclip .exceptions import PyperclipException
@@ -113,6 +112,11 @@ def __subclasshook__(cls, C):
113112else :
114113 from contextlib import redirect_stdout , redirect_stderr
115114
115+ if sys .version_info > (3 , 0 ):
116+ from io import StringIO # Python3
117+ else :
118+ from io import BytesIO as StringIO # Python2
119+
116120# Detect whether IPython is installed to determine if the built-in "ipy" command should be included
117121ipython_available = True
118122try :
@@ -183,6 +187,7 @@ class RlType(Enum):
183187 except ImportError :
184188 pass
185189
190+
186191__version__ = '0.8.4'
187192
188193# 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):
210215QUOTES = ['"' , "'" ]
211216REDIRECTION_CHARS = ['|' , '<' , '>' ]
212217
218+ # optional attribute, when tagged on a function, allows cmd2 to categorize commands
219+ HELP_CATEGORY = 'help_category'
220+ HELP_SUMMARY = 'help_summary'
221+
222+
223+ def categorize (func , category ):
224+ """Categorize a function.
225+
226+ The help command output will group this function under the specified category heading
227+
228+ :param func: Union[Callable, Iterable] - function to categorize
229+ :param category: str - category to put it in
230+ """
231+ if isinstance (func , Iterable ):
232+ for item in func :
233+ setattr (item , HELP_CATEGORY , category )
234+ else :
235+ setattr (func , HELP_CATEGORY , category )
236+
213237
214238def set_posix_shlex (val ):
215239 """ 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):
340364 return lexed_arglist
341365
342366
367+ def with_category (category ):
368+ """A decorator to apply a category to a command function"""
369+ def cat_decorator (func ):
370+ categorize (func , category )
371+ return func
372+ return cat_decorator
373+
374+
343375def with_argument_list (func ):
344376 """A decorator to alter the arguments passed to a do_* cmd2
345377 method. Default passes a string of whatever the user typed.
@@ -378,6 +410,9 @@ def cmd_wrapper(instance, cmdline):
378410 if argparser .description is None and func .__doc__ :
379411 argparser .description = func .__doc__
380412
413+ if func .__doc__ :
414+ setattr (cmd_wrapper , HELP_SUMMARY , func .__doc__ )
415+
381416 cmd_wrapper .__doc__ = argparser .format_help ()
382417
383418 # Mark this function as having an argparse ArgumentParser (used by do_help)
@@ -417,6 +452,9 @@ def cmd_wrapper(instance, cmdline):
417452 if argparser .description is None and func .__doc__ :
418453 argparser .description = func .__doc__
419454
455+ if func .__doc__ :
456+ setattr (cmd_wrapper , HELP_SUMMARY , func .__doc__ )
457+
420458 cmd_wrapper .__doc__ = argparser .format_help ()
421459
422460 # Mark this function as having an argparse ArgumentParser (used by do_help)
@@ -2875,7 +2913,10 @@ def complete_unalias(self, text, line, begidx, endidx):
28752913 @with_argument_list
28762914 def do_help (self , arglist ):
28772915 """List available commands with "help" or detailed help with "help cmd"."""
2878- if arglist :
2916+ if not arglist or (len (arglist ) == 1 and arglist [0 ] in ('--verbose' , '-v' )):
2917+ verbose = len (arglist ) == 1 and arglist [0 ] in ('--verbose' , '-v' )
2918+ self ._help_menu (verbose )
2919+ else :
28792920 # Getting help for a specific command
28802921 funcname = self ._func_named (arglist [0 ])
28812922 if funcname :
@@ -2896,11 +2937,8 @@ def do_help(self, arglist):
28962937 else :
28972938 # This could be a help topic
28982939 cmd .Cmd .do_help (self , arglist [0 ])
2899- else :
2900- # Show a menu of what commands help can be gotten for
2901- self ._help_menu ()
29022940
2903- def _help_menu (self ):
2941+ def _help_menu (self , verbose = False ):
29042942 """Show a list of commands which help can be displayed for.
29052943 """
29062944 # Get a sorted list of help topics
@@ -2913,21 +2951,107 @@ def _help_menu(self):
29132951
29142952 cmds_doc = []
29152953 cmds_undoc = []
2954+ cmds_cats = {}
29162955
29172956 for command in visible_commands :
2918- if command in help_topics :
2919- cmds_doc .append (command )
2920- help_topics .remove (command )
2921- elif getattr (self , self ._func_named (command )).__doc__ :
2922- cmds_doc .append (command )
2957+ if command in help_topics or getattr (self , self ._func_named (command )).__doc__ :
2958+ if command in help_topics :
2959+ help_topics .remove (command )
2960+ if hasattr (getattr (self , self ._func_named (command )), HELP_CATEGORY ):
2961+ category = getattr (getattr (self , self ._func_named (command )), HELP_CATEGORY )
2962+ cmds_cats .setdefault (category , [])
2963+ cmds_cats [category ].append (command )
2964+ else :
2965+ cmds_doc .append (command )
29232966 else :
29242967 cmds_undoc .append (command )
29252968
2926- self .poutput ("%s\n " % str (self .doc_leader ))
2927- self .print_topics (self .doc_header , cmds_doc , 15 , 80 )
2969+ if len (cmds_cats ) == 0 :
2970+ # No categories found, fall back to standard behavior
2971+ self .poutput ("{}\n " .format (str (self .doc_leader )))
2972+ self ._print_topics (self .doc_header , cmds_doc , verbose )
2973+ else :
2974+ # Categories found, Organize all commands by category
2975+ self .poutput ('{}\n ' .format (str (self .doc_leader )))
2976+ self .poutput ('{}\n \n ' .format (str (self .doc_header )))
2977+ for category in sorted (cmds_cats .keys ()):
2978+ self ._print_topics (category , cmds_cats [category ], verbose )
2979+ self ._print_topics ('Other' , cmds_doc , verbose )
2980+
29282981 self .print_topics (self .misc_header , help_topics , 15 , 80 )
29292982 self .print_topics (self .undoc_header , cmds_undoc , 15 , 80 )
29302983
2984+ def _print_topics (self , header , cmds , verbose ):
2985+ """Customized version of print_topics that can switch between verbose or traditional output"""
2986+ if cmds :
2987+ if not verbose :
2988+ self .print_topics (header , cmds , 15 , 80 )
2989+ else :
2990+ self .stdout .write ('{}\n ' .format (str (header )))
2991+ widest = 0
2992+ # measure the commands
2993+ for command in cmds :
2994+ width = len (command )
2995+ if width > widest :
2996+ widest = width
2997+ # add a 4-space pad
2998+ widest += 4
2999+ if widest < 20 :
3000+ widest = 20
3001+
3002+ if self .ruler :
3003+ self .stdout .write ('{:{ruler}<{width}}\n ' .format ('' , ruler = self .ruler , width = 80 ))
3004+
3005+ help_topics = self .get_help_topics ()
3006+ for command in cmds :
3007+ doc = ''
3008+ # Try to get the documentation string
3009+ try :
3010+ # first see if there's a help function implemented
3011+ func = getattr (self , 'help_' + command )
3012+ except AttributeError :
3013+ # Couldn't find a help function
3014+ try :
3015+ # Now see if help_summary has been set
3016+ doc = getattr (self , self ._func_named (command )).help_summary
3017+ except AttributeError :
3018+ # Last, try to directly ac cess the function's doc-string
3019+ doc = getattr (self , self ._func_named (command )).__doc__
3020+ else :
3021+ # we found the help function
3022+ result = StringIO ()
3023+ # try to redirect system stdout
3024+ with redirect_stdout (result ):
3025+ # save our internal stdout
3026+ stdout_orig = self .stdout
3027+ try :
3028+ # redirect our internal stdout
3029+ self .stdout = result
3030+ func ()
3031+ finally :
3032+ # restore internal stdout
3033+ self .stdout = stdout_orig
3034+ doc = result .getvalue ()
3035+
3036+ # Attempt to locate the first documentation block
3037+ doc_block = []
3038+ found_first = False
3039+ for doc_line in doc .splitlines ():
3040+ str (doc_line ).strip ()
3041+ if len (doc_line .strip ()) > 0 :
3042+ doc_block .append (doc_line .strip ())
3043+ found_first = True
3044+ else :
3045+ if found_first :
3046+ break
3047+
3048+ for doc_line in doc_block :
3049+ self .stdout .write ('{: <{col_width}}{doc}\n ' .format (command ,
3050+ col_width = widest ,
3051+ doc = doc_line ))
3052+ command = ''
3053+ self .stdout .write ("\n " )
3054+
29313055 def do_shortcuts (self , _ ):
29323056 """Lists shortcuts (aliases) available."""
29333057 result = "\n " .join ('%s: %s' % (sc [0 ], sc [1 ]) for sc in sorted (self .shortcuts ))
@@ -3025,7 +3149,7 @@ def show(self, args, parameter):
30253149 else :
30263150 raise LookupError ("Parameter '%s' not supported (type 'show' for list of parameters)." % param )
30273151
3028- set_parser = argparse .ArgumentParser ()
3152+ set_parser = argparse .ArgumentParser (formatter_class = argparse . RawTextHelpFormatter )
30293153 set_parser .add_argument ('-a' , '--all' , action = 'store_true' , help = 'display read-only settings as well' )
30303154 set_parser .add_argument ('-l' , '--long' , action = 'store_true' , help = 'describe function of parameter' )
30313155 set_parser .add_argument ('settable' , nargs = '*' , help = '[param_name] [value]' )
@@ -3194,6 +3318,8 @@ def cmd_with_subs_completer(self, text, line, begidx, endidx):
31943318 # noinspection PyBroadException
31953319 def do_py (self , arg ):
31963320 """
3321+ Invoke python command, shell, or script
3322+
31973323 py <command>: Executes a Python command.
31983324 py: Enters interactive Python mode.
31993325 End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``.
0 commit comments