Help Page

This is a module to help with creating help-pages.

Font Constants

ASCII codes are used to change the text sent to standard output. The available codes are:

  • BLUE
  • RED
  • RESET
  • BOLD
  • RESET gets rid of the previously set codes (it turns off bold and makes the color black).
  • These codes are used in the TEMPLATE so you can use them but it is not required.

Python TextWrapper

TextWrapper is being used to make the output (hopefully) a little nicer. These are some notes generated while I figure out how to use it. TextWrapper has many options and two methods but I anticipate only use the width, and subsequent_indent attributes and the fill(text) method.

textwrap.TextWrapper([width, ...]) Object for wrapping/filling text.
textwrap.TextWrapper.width
textwrap.TextWrapper.drop_whitespace
textwrap.TextWrapper.subsequent_indent
textwrap.TextWrapper.fill(...) Reformat the single paragraph in ‘text’ to fit in lines of no more than ‘self.width’ columns, and return a new string containing the entire wrapped paragraph.
if output_documentation:
    indent = ' ' * 5
    tw = textwrap.TextWrapper(width=80, subsequent_indent=indent,
                              drop_whitespace=False)
    text = """
A Little Text:

Now is the winter of our discontent;
Made glorious summer by this Son of York;
And all the clouds that lour'd upon our house;
In the deep bosom of the ocean buried.
"""
    wrapped_text = tw.fill(text)
    print wrapped_text
A Little Text:  Now is the winter of our discontent; Made glorious summer by
    this Son of York; And all the clouds that lour'd upon our house; In the
    deep bosom of the ocean buried.

That was not really what I wanted – I thought that the empty line before the paragraph would be preserved. It looks like in order to do what I wanted you would need to do it as two separate print calls (and and initial indent added so that the body will be entirely indented).

textwrap.TextWrapper.initial_indent
if output_documentation:
    tw.initial_indent = indent
    header = 'A Little Text:\n'
    text = """
Now is the winter of our discontent;
Made glorious summer by this Son of York;
And all the clouds that lour'd upon our house;
In the deep bosom of the ocean buried.
"""
    print header
    headerless_wrapped_text = tw.fill(text)
    print headerless_wrapped_text
A Little Text:

      Now is the winter of our discontent; Made glorious summer by this Son of
     York; And all the clouds that lour'd upon our house; In the deep bosom of
     the ocean buried.

Okay, but now the output is all blocky. Try again with the drop_whitespace set to True.

if output_documentation:
    tw.drop_whitespace = True
    print header
    headerless_wrapped_text = tw.fill(text)
    print headerless_wrapped_text
A Little Text:

      Now is the winter of our discontent; Made glorious summer by this Son of
     York; And all the clouds that lour'd upon our house; In the deep bosom of
     the ocean buried.

Well, it is closer, but that first line in the body is still mysteriously off.

if output_documentation:
    tw.initial_indent = ' ' * 4
    headerless_wrapped_text = tw.fill(text)
    print header
    print headerless_wrapped_text
A Little Text:

     Now is the winter of our discontent; Made glorious summer by this Son of
     York; And all the clouds that lour'd upon our house; In the deep bosom of
     the ocean buried.

Warning

I initially misspelled initial_indent and it just silently did not change the output behavior – use the constructor instead of assigning the values like I do here.

Less is More

In addition to using TextWrap I will use the less command to keep the text from rolling off the screen.

This will break if the computer does not have less, which seems unlikely since I do not run Windows, but just in case I will use check_call so that it can fall back to printing.

subprocess.check_call(*popenargs, **kwargs) Run command with arguments.
shlex.split(s[, comments, posix])
if output_documentation:
    long_text = text * 100
    long_wrapped_text = tw.fill(long_text)
    command = shlex.split('echo "{0}" | less -R'.format(long_wrapped_text))

    subprocess.check_call(command)
    try:
        subprocess.check_call('ummagumma')
    except OSError as error:
        print wrapped_text
A Little Text:  Now is the winter of our discontent; Made glorious summer by
    this Son of York; And all the clouds that lour'd upon our house; In the
    deep bosom of the ocean buried.

The output from the first call will not show up in the documentation output, so it might not be obvious that it does not work. Apparently to do a pipe you need to use Popen and communicate.

Popen(args[, bufsize, executable, stdin, ...]) Create new Popen instance.
Popen.communicate([input]) Interact with process: Send data to stdin.
PIPE int(x=0) -> int or long

So you need to do something like this:

command = 'less -R'.split()
subprocess.Popen(command, stdin=subprocess.PIPE).communicate(input=long_wrapped_text)

The HelpPage

With that background material to the aside, here is the actual pager.

HelpPage -|> BaseClass
HelpPage o- subprocess.Popen
HelpPage o- textwrap.TextWrapper

HelpPage
HelpPage.__call__
HelpPage.text

Warning

The HelpPage is using the `string.format <http://docs.python.org/2/library/stdtypes.html#str.format`_ method so putting curly braces (‘{}’) in the help-strings is probably a bad idea. If you need them, make sure to double them – (‘{{like this}}’).

If the headers argument is not set then the keys from the sections argument will be used (this way an ordered dict can be used to make things simpler).

Suggested Sections

My original idea was to put suggested sections in the HelpPage but it made the constructor-signature too big (and seemed too inflexible). So here are what the sections were going to be (taken from man pages):

Suggested Sections
Header Contents
NAME The topic of the help and a short description.
SYNOPSIS Comprehensive listing of usage options.
CONFIGURATION Setting up.
DESCRIPTION The main body of the help describing the topic.
OPTIONS Description of the options.
RETURN VALUE Values returned on exit (if any).
ERRORS Possible errors raised.
FILES Paths to relevant files.
NOTES Miscellaneous notes.
BUGS Known bugs.
EXAMPLE Usage examples.
AUTHORS Code authors.
SEE ALSO Related topics.

The man-pages have other sections but these were the ones I thought might be the most relevant for the way I am planning to use this.

Example Use

It is getting too hard to test this thing, so I will just do a plain old usage check.

if __name__ == '__main__':
    headers = 'title poem author source'.split()
    poem = """
    They fuck you up, your mum and dad.
        They may not mean to, but they do.
    They fill you with the faults they had
        And add some extra, just for you.

    But they were fucked up in their turn
        By fools in old-style hats and coats,
    Who half the time were soppy-stern
        And half at one another's throats.

    Man hands on misery to man.
        It deepens like a coastal shelf.
    Get out as early as you can,
        And don't have any kids yourself.
    """
    poem = textwrap.dedent(poem)
    title='{red}This Be The Verse{reset}'
    author = '{blue}Philip Larkin{reset}'
    source = 'http://www.poetryfoundation.org/poem/178055'
    sections = dict(zip(headers, (title, poem, author, source)))
    pager = HelpPage(headers = headers,
                     sections = sections)
    pager()