Commits

larry committed 551473b

Greatly improved help usage printing.

Comments (0)

Files changed (2)

 
 
 
-def column_wrapper(prefix, options, *,
+def column_wrapper(prefix, words, *,
     min_column=12, max_column=40, right_margin=79):
     """
-    Formats text in a pleasing format:
-    ##
-    prefix here         text here which wraps to
-                        multiple lines in a pleasing way.
-    ##
+    Formats text in a pleasing format.
 
-    prefix can be either a string or an iterable of strings.
+    "prefix" can be either a string or an iterable of strings.
 
-    options should be an iterable of strings that have
+    "words" should be an iterable of strings that have
     already been broken up at word boundaries.
-    an empty string in the options iterable inserts
-    a blank line in the output.
+    An empty string in the words iterable inserts
+    a newline in the output.  ()
+
+    The output looks something like this:
+
+        prefix here         words here which are wrapped to
+                            multiple lines in a pleasing way.
     """
 
     lines = []
 
     empty_prefixes = itertools.repeat(' ' * column)
     prefixes = itertools.chain(line_prefixes, empty_prefixes)
-    options = list(reversed(options))
+    words = list(reversed(words))
 
-    while options:
-        # print("options", len(options), options)
+    if not words:
+        for prefix in line_prefixes:
+            lines.append(prefix)
+
+    while words:
         test = line = next(prefixes)
-        is_first_option = True
-        while options:
-            o = options.pop()
+        is_first_word = True
+        while words:
+            o = words.pop()
             if not o:
-                lines.append(line)
-                line = ''
                 break
-            test = line + ' ' + o
+            spacer = '  ' if line.endswith('.') else ' '
+            test = line + spacer + o
             if len(test) > right_margin:
-                if is_first_option:
+                if is_first_word:
                     line = test
                 else:
-                    options.append(o)
+                    words.append(o)
                 break
             else:
                 line = test
-                is_first_option = False
+                is_first_word = False
         lines.append(line)
 
     return '\n'.join(lines).rstrip()
 
 
+class _column_wrapper_splitter:
+
+    def __init__(self, tab_width):
+        self.next(self.state_initial)
+        self.words = []
+        self.hopper = []
+        self.emit = self.hopper.append
+        self.col = self.next_col = 0
+        self.line = self.next_line = 0
+
+    def newline(self):
+        assert not self.hopper, "Emitting newline while hopper is not empty!"
+        self.words.append('')
+
+    def empty_hopper(self):
+        if self.hopper:
+            self.words.append(''.join(self.hopper))
+            self.hopper.clear()
+
+    def next(self, state, c=None):
+        self.state = state
+        if c is not None:
+            self.state(c)
+
+    def write(self, c):
+        if c in '\t\n':
+            if c == '\t':
+                self.next_col = col + tab_width - (col % tab_width)
+            else:
+                self.next_col = 0
+                self.next_line = self.line + 1
+        else:
+            self.next_col = self.col + 1
+
+        self.state(c)
+
+        self.col = self.next_col
+        self.line = self.next_line
+
+    def close(self):
+        self.empty_hopper()
+
+    def state_paragraph_start(self, c):
+        if c.isspace():
+            return
+        if self.col >= 4:
+            next = self.state_code_line_start
+        else:
+            next = self.state_in_paragraph
+        self.next(next, c)
+
+    state_initial = state_paragraph_start
+
+    def state_code_line_start(self, c):
+        if c.isspace():
+            if c == '\n':
+                self.newline()
+                self.next(self.state_paragraph_start)
+            return
+        if self.col < 4:
+            raise ValueError("Can't outdent past 4 in a code paragraph! (line " + str(self.line) + " col " + str(self.col) + ")")
+        self.emit(' ' * self.col)
+        self.next(self.state_in_code, c)
+
+    def state_in_code(self, c):
+        if c.isspace():
+            if c == '\n':
+                self.empty_hopper()
+                self.newline()
+                self.next(self.state_code_line_start)
+            else:
+                self.emit(' ' * (self.next_col - self.col))
+        else:
+            self.emit(c)
+
+    def state_paragraph_line_start(self, c):
+        if not c.isspace():
+            return self.next(self.state_in_paragraph, c)
+        if c == '\n':
+            self.newline()
+            self.newline()
+            self.next(self.state_paragraph_start)
+
+    def state_in_paragraph(self, c):
+        if not c.isspace():
+            self.emit(c)
+            return
+
+        self.empty_hopper()
+        if c == '\n':
+            self.next(self.state_paragraph_line_start)
+
+
+def column_wrapper_split(s, *, tab_width=8):
+    """
+    Splits up a string into individual words, suitable
+    for feeding into column_wrapper().
+
+    Paragraphs indented by four spaces or more preserve
+    whitespace; internal whitespace is preserved, and the
+    newline is preserved.  (This is for code examples.)
+
+    Paragraphs indented by less than four spaces will be
+    broken up into individual words.
+    """
+    cws = _column_wrapper_splitter(tab_width)
+    for c in s:
+        cws.write(c)
+    cws.close()
+    return cws.words
+
+
 @all
 class DryArgument:
     type = None
             default = kwonlydefaults.get(name, unspecified)
             add(name, default, i, is_option=True)
 
-    def _usage_first_line(self):
+    def _usage_first_line(self, global_handler):
         """
         Everything after the command name.
         """
 
-        seen = set()
+        def format_options_for_first_line(command):
+            if not command:
+                return []
 
-        short_options = []
-        long_options = []
-        for name in sorted(self.options):
-            value = self.options[name]
-            if value in seen:
-                continue
-            if (len(value.options) == 1 and
-                value.type is bool and
-                len(name) == 1):
-                short_options.append(name)
-            else:
-                long_options.append(value.usage(first_line=True))
-            seen.add(value)
+            seen = set()
+
+            short_options = []
+            long_options = []
+            for name in sorted(command.options):
+                value = command.options[name]
+                if value in seen:
+                    continue
+                if (len(value.options) == 1 and
+                    value.type is bool and
+                    len(name) == 1):
+                    short_options.append(name)
+                else:
+                    long_options.append(value.usage(first_line=True))
+                seen.add(value)
+
+            if short_options:
+                long_options.insert(0, '[-' + ''.join(short_options) + ']')
+            return long_options
+
+        global_options = format_options_for_first_line(global_handler)
+        options = format_options_for_first_line(self)
 
         arguments = []
         unwind = 0
 
         arguments.append(']' * unwind)
 
-        final = []
-        if short_options:
-            final.append('[-' + ''.join(short_options) + ']')
-        final.extend(long_options)
-        final.extend(arguments)
+        prefix = ["Usage:", sys.argv[0]]
+        prefix.extend(global_options)
+        prefix.append(self.name)
 
-        return column_wrapper(["usage:", sys.argv[0], self.name], final)
+        words = []
+        words.extend(options)
+        words.extend(arguments)
 
-    def _usage(self, *, short=False, error=None):
+        return column_wrapper(prefix, words)
+
+    def _usage(self, *, global_handler=None, short=False, error=None):
         short = bool(short or error)
         output = []
         def print(*a):
                 del lines[0]
                 while lines and not lines[0]:
                     del lines[0]
-                while lines and not lines[-1]:
+                while lines and not lines[-1].lstrip():
                     lines.pop()
 
         if error:
             print(error)
 
-        print(self._usage_first_line())
+        print(self._usage_first_line(global_handler))
 
         if error or short:
             print('Try "' + sys.argv[0] + ' help ' + self.name + '" for more information.')
             return '\n'.join(output)
 
+        print()
         print(summary_line)
 
-        if lines:
+        # global options
+        if global_handler:
+            options = {}
+            longest_option = 0
+            for argument in global_handler.all_arguments:
+                if argument.options:
+                    dashed_options = [ '-' + ('-' if len(option) > 1 else '') + option for option in argument.options]
+                    all_options = ', '.join(sorted(dashed_options))
+                    longest_option = max(longest_option, len(all_options))
+                    options[all_options] = argument
             print()
-            for line in lines:
-                print(" ", line)
-
-        print()
-
-        print(self._usage_first_line())
+            print("Global options:")
+            min_column = longest_option + 4
+            for all_options in sorted(options):
+                argument = options[all_options]
+                prefix = "  " + all_options
+                split_doc = column_wrapper_split(argument.doc)
+                formatted = column_wrapper(prefix, split_doc, min_column=min_column)
+                print(formatted)
 
         options = {}
         longest_option = longest_positional = 0
         if abs(longest_positional - longest_option) < 3:
             longest_positional = longest_option = max(longest_positional, longest_option)
         if options:
+            print()
             print("Options:")
+            min_column = longest_option + 4
             for all_options in sorted(options):
                 argument = options[all_options]
-                print(" ", all_options.ljust(longest_option), "", argument.doc)
+                prefix = "  " + all_options
+                split_doc = column_wrapper_split(argument.doc)
+                formatted = column_wrapper(prefix, split_doc, min_column=min_column)
+                print(formatted)
+        if self.arguments:
             print()
-        if self.arguments:
             print("Arguments:")
+            min_column = longest_positional + 2
             for argument in self.arguments:
-                print(" ", argument.name.ljust(longest_positional), "", argument.doc)
+                prefix = "  " + argument.name
+                split_doc = column_wrapper_split(argument.doc or '')
+                formatted = column_wrapper(prefix, split_doc, min_column=min_column)
+                print(formatted)
             if self.star_args:
-                print(" ", '[' + self.star_args.name + '...]' )
+                print("  [" + self.star_args.name + "...]")
 
-        print()
+        if lines:
+            print()
+            for line in lines:
+                print(" ", line)
+
         return '\n'.join(output)
 
     def usage(self, *, error=None):
 
         #       * no if it starts and ends with '__'
         name = name or o.__name__
-        if name.startswith('__') and name.endswith('__'):
+        if name.startswith('_'):
             return False
 
         return None
                 return self.usage()
             return self.commands[command]._usage()
 
-        for name, value in self.commands.items():
-            print("cmd ", name, value)
         have_global_options = self.global_handler and self.global_handler.options
         have_commands = self.commands
         have_global_arguments = (not have_commands) and self.global_handler and self.global_handler.have_arguments
         if self.doc:
             print(self.doc)
             print()
-        a = ["usage:", sys.argv[0]]
+        a = ["Usage:", sys.argv[0]]
         add = a.append
         if have_global_options:
-            add("[global-options]")
+            add("[options]")
         if have_global_arguments:
-            add("global-arguments")
+            add("arguments")
         if have_commands:
             add("command")
             if have_options:
                 add("[options]")
             if have_arguments:
-                add("[arguments]")
+                add("[arguments ...]")
         print(' '.join(a))
         print()
 
         if have_commands:
-            print("Supported commands:")
-            commands = sorted(self.commands)
-            longest = len('help')
-            for name in commands:
-                longest = max(longest, len(name))
-            for name in commands:
-                if name == 'help':
-                    print(" ", "help".ljust(longest), " help on command usage")
-                    continue
-                command = self.commands[name]
-                first_line = command.doc.strip().split('\n')[0].strip()
-                print(" ", name.ljust(longest), "", first_line)
+            self.print_commands()
 
-        print()
         return '\n'.join(output)
 
-    def help(self, *args):
-        self.usage(argv=args)
+    def print_commands(self):
+        print("Supported commands:")
+        commands = sorted(self.commands)
+        longest = len('help')
+        for name in commands:
+            longest = max(longest, len(name))
+        for name in commands:
+            if name == 'help':
+                print(" ", "help".ljust(longest), " List all commands, or show usage on a specific command.")
+                continue
+            command = self.commands[name]
+            first_line = command.doc.strip().split('\n')[0].strip()
+            print(" ", name.ljust(longest), "", first_line)
+
+    def help(self, command=''):
+        if command:
+            return self.commands[command]._usage(global_handler=self.global_handler)
+        self.print_commands()
+        print()
+        print("Use 'help command' for help on a specific command.")
 
     def main(self, argv=None):
         return_value = None
 		Adds two or more numbers together.
 		"""
 		result = a + b + sum(args)
+		if self.right:
+			result = str(result).rjust(80)
 		print(result)
 
 	def divide(self, numerator:{int},
 			result = numerator // denominator
 		else:
 			result = numerator / denominator
+		if self.right:
+			result = str(result).rjust(80)
 		print(result)
 
+	def _global_handler(self, *, right:{'-R', 'Right-justify output.'}=False):
+		self.right = right
+
 if __name__ == '__main__':
 	c = Calculator()
 	dp = dryparse.DryParse()
+	dp.global_handler = c._global_handler
 	dp.update(c)
 	sys.exit(dp.main())