further bugfixing in parsers & clean up formatting.py
[logparse.git] / logparse / formatting.py
index d7a9b4d4d99a31821bba41318691d9558344a9d7..45d2e3fa383583397d158d05d6c83d7a0c5426aa 100644 (file)
@@ -3,8 +3,8 @@
 #   
 #   This file contains global functions for formatting and printing data. This
 #   file should be imported into individual log-parsing scripts located in
-#   logs/*. Data is all formatted in HTML. Writing to disk and/or emailng data
-#   is left to __main__.py.
+#   logs/*. Data is formatted in HTML or plaintext. Writing to disk and/or
+#   emailng data is left to __main__.py.
 #
 
 import os
@@ -13,11 +13,12 @@ import locale
 from string import Template
 
 import logparse
-from . import interface, util, config
+from . import interface, util, config, mail
 
 import logging
 logger = logging.getLogger(__name__)
 
+
 locale.setlocale(locale.LC_ALL, '') # inherit system locale
 #DEG = "°".encode('unicode_escape')
 DEG = u'\N{DEGREE SIGN}'
@@ -28,17 +29,29 @@ CORNERCHARS_DOUBLE = ['╚', '╝', '╗', '╔']
 CORNERCHARS_SINGLE = ['└', '┘', '┐', '┌']
 LINECHARS_DOUBLE = ['║', '═']
 LINECHARS_SINGLE = ['│', '─']
+BULLET = "• "
+INDENT = "  "
+
 
 class Output:
+    """
+    Base class for a data processor. 
+    """
     
     def __init__(self):
         self.content = ""
         self.destination = ""
 
     def append(self, content):
+        """
+        Add a string
+        """
         self.content += content
 
     def write(self, destination=""):
+        """
+        Output contents into a file
+        """
         if destination == "":
             destination = self.destination
         if destination == "":
@@ -50,6 +63,9 @@ class Output:
 
 
 class PlaintextOutput(Output):
+    """
+    Processes & outputs data in a plaintext form which can be read with cat or plaintext email.
+    """
 
     def __init__(self, linewidth=80):
         self.content = ""
@@ -57,24 +73,38 @@ class PlaintextOutput(Output):
         self.linewidth = linewidth;
 
     def append_header(self, template=''):
+        """
+        Print details with some primitive formatting
+        """
         init_varfilter()
         box = PlaintextBox(content=Template("$title $version on $hostname\n\n$time $date").safe_substitute(varsubst), vpadding=2, hpadding="\t\t", linewidth=config.prefs['linewidth'])
         line = PlaintextLine(self.linewidth)
         self.append(box.draw() + line.draw())
 
     def append_footer(self):
+        """
+        Append a horizontal line and some details
+        """
         init_varfilter()
         self.append(PlaintextLine(self.linewidth, vpadding=1).draw())
         self.append(Template("$hostname $time $date").safe_substitute(varsubst))
 
     def append_section(self, section):
+        """
+        Call the appropriate methods to format a section (provided by a parser).
+        This should be run by interface.py after every instance of parse_log().
+        """
         self.append(PlaintextBox(content=section.title, double=False, fullwidth=False, vpadding=0, hpadding=" ").draw())
         self.append('\n'*2)
         for data in section.data:
             self.append(self._fmt_data(data.subtitle, data.items))
-        self.append('\n')
+            self.append('\n')
 
     def _fmt_data(self, subtitle, data = None):   # write title and data
+        """
+        Format the properties of a data object into usable plaintext form with a few fancy symbols/formatting tricks.
+        Subtitle is required, data is not. If only subtitle is supplied or subtitle + one data item, a single line will be printed.
+        """
         if (subtitle == ""):
             logger.warning("No subtitle provided.. skipping section")
             return
@@ -90,26 +120,31 @@ class PlaintextOutput(Output):
             else:
                 itemoutput = subtitle + '\n'
                 for datum in data:
-                    datum = '• ' + datum
-                    if len(datum) > config.prefs['linewidth']:
+                    datum = BULLET + datum
+                    if len(datum) > config.prefs['linewidth'] - 3:
                         words = datum.split()
-                        if max(map(len, words)) > config.prefs['linewidth']:
-                            raise ValueError("Content width is too small")
+                        if max(map(len, words)) > config.prefs['linewidth'] - len(INDENT):
+                            continue
                         res, part, others = [], words[0], words[1:]
                         for word in others:
-                            if len(' ') + len(word) > config.prefs['linewidth'] - len(part):
+                            if 1 + len(word) > config.prefs['linewidth'] - len(part):
                                 res.append(part)
                                 part = word
                             else:
                                 part += ' ' + word
                         if part:
                             res.append(part)
-                        datum = '\n'.join(res)
+                        datum = ('\n    ').join(res)
+                    datum = INDENT + datum
                     itemoutput += datum + '\n'
                 return itemoutput
 
 
 class HtmlOutput(Output):
+    """
+    Process and output data in HTML format.
+    All HTML formatting functions now reside in this class to differentiate them from plaintext.
+    """
 
     def __init__(self):
         self.content = ""
@@ -117,75 +152,157 @@ class HtmlOutput(Output):
         self.css = ""
 
     def embed_css(self, css):
+        """
+        Convert stylesheet to inline tags
+        """
         self.content = mail.mailprep(self.content, css)
+        return self.content
 
-    def append_header(self, template):   # insert variables into header template file
+    def append_header(self, template):
+        """
+        Insert variables into header template file and append HTML tags
+        """
         init_varfilter()
         headercontent = Template(open(template, 'r').read())
         self.append(headercontent.safe_substitute(varsubst))
-        self.append(opentag('div', id='main'))
+        self.append(self.opentag('div', id='main'))
 
     def append_footer(self):
-        self.append(closetag('div') + closetag('body') + closetag('html'))
+        """
+        Close HTML tags that were opened in the template.
+        TODO: add footer template similar to header template.
+        """
+        self.append(self.closetag('div') + self.closetag('body') + self.closetag('html'))
 
     def append_section(self, section):
-        self.append(opentag('div', 1, section.title, 'section'))
+        """
+        Call the appropriate methods to generate HTML tags for a section (provided by a parser).
+        This should be run by interface.py after every instance of parse_log().
+        """
+        self.append(self.opentag('div', 1, section.title, 'section'))
         self.append(self._gen_title(section.title))
         for data in section.data:
             self.append(self._fmt_data(data.subtitle, data.items))
-        self.append(closetag('div', 1))
+        self.append(self.closetag('div', 1))
 
-    def _gen_title(self, title):  # write title for a section
+    def _gen_title(self, title):
+        """
+        Format the title for a section
+        """
         if (title == '' or '\n' in title):
             logger.error("Invalid title")
             raise ValueError 
         logger.debug("Writing title for " + title)
-        return tag('h2', 0, title)
+        return self.tag('h2', False, title)
 
-    def _fmt_data(self, subtitle, data = None):   # write title and data
+    def _fmt_data(self, subtitle, data = None):
+        """
+        Format the properties of a data object into usable HTML tags.
+        Subtitle is required, data is not. If only subtitle is supplied or subtitle + one data item, a single line will be printed.
+        """
         if (subtitle == ""):
             logger.warning("No subtitle provided.. skipping section")
             return
 
         if (data == None or len(data) == 0):
             logger.debug("No data provided.. just printing subtitle")
-            return tag('p', 0, subtitle)
+            return self.tag('p', False, subtitle)
         else:
             logger.debug("Received data " + str(data))
             subtitle += ':'
             if (len(data) == 1):
-                return tag('p', 0, subtitle + ' ' + data[0])
+                return self.tag('p', False, subtitle + ' ' + data[0])
             else:
                 output = ""
-                output += tag('p', 0, subtitle)
-                output += opentag('ul', 1)
+                output += self.tag('p', False, subtitle)
+                output += self.opentag('ul', 1)
                 coderegex = re.compile('`(.*)`')
                 for datum in data:
                     if datum == "" or datum == None:
                         continue
-                    datum = coderegex.sub(r'<code>{\1}</code>', str(datum))
-                    output += tag('li', 0, datum)
-                output += closetag('ul', 1)
+                    datum = coderegex.sub(r"<code>\1</code>", str(datum))
+                    output += self.tag('li', False, datum)
+                output += self.closetag('ul', True)
                 return output
 
+    def opentag(self, tag, block=False, id=None, cl=None):
+        """
+        Write HTML opening tag
+        """
+        output = ""
+        if (block):
+            output += '\n'
+        output += '<' + tag
+        if (id != None):
+            output += " id='" + id + "'"
+        if (cl != None):
+            output += " class='" + cl + "'"
+        output += '>'
+        if (block):
+            output += '\n'
+        return output
+
+    def closetag(self, tag, block=False):
+        """
+        Write HTML closing tag
+        """
+        if block:
+            return "\n</" + tag + ">\n"
+        else:
+            return "</" + tag + ">"
+
+    def tag(self, tag, block=False, content=""):
+        """
+        Write HTML opening tag, content, and closing tag
+        """
+        o = self.opentag(tag, block)
+        c = self.closetag(tag, block)
+        return o + content + c
+
 
 class Section:
+    """
+    Each parser should output a Section() which contains the title and returned data.
+    """
 
     def __init__(self, title):
         self.title = title
         self.data = []
 
-    def add_data(self, data):
+    def append_data(self, data):
         self.data.append(data)
 
+
 class Data:
+    """
+    Each section (parser) can have one or more Data() objects which are essentially glorified lists.
+    """
     
-    def __init__(self, subtitle, items=None):
+    def __init__(self, subtitle="", items=[]):
         self.subtitle = subtitle
         self.items = items 
 
+    def truncl(self, limit):      # truncate list
+        """
+        Truncate self.items to a specified value and state how many items are hidden.
+        """
+        if (len(self.items) > limit):
+            more = str(len(self.items) - limit)
+            self.items = self.items[:limit]
+            self.items.append("+ {0} more".format(more))
+
+    def orderbyfreq(self):
+        """
+        Order a list by frequency of each item, then remove duplicates and append frequency in parentheses.
+        """
+        unsorted = list(self.items)
+        self.items = ["{0} ({1})".format(y, unsorted.count(y)) for y in sorted(set(unsorted), key = lambda x: -unsorted.count(x))]
+
 
 class PlaintextLine:
+    """
+    Draw a horizontal line for plain text format, with optional padding/styling.
+    """
 
     def __init__(self, linewidth=80, double=True, vpadding=1, hpadding=""):
         self.linewidth = linewidth
@@ -197,7 +314,11 @@ class PlaintextLine:
         line = (LINECHARS_DOUBLE[1] if self.double else LINECHARS_SINGLE[1])
         return "\n" * self.vpadding + self.hpadding +  line * (self.linewidth - 2 * len(self.hpadding)) + self.hpadding + "\n" * self.vpadding
 
+
 class PlaintextBox:
+    """
+    Draw a rectangular box around text, with customisable padding/size/style
+    """
 
     def __init__(self, content="", double=True, fullwidth=True, linewidth=80, hpadding="\t", vpadding=1):
         self.content = content
@@ -235,7 +356,7 @@ class PlaintextBox:
             if len(line) > contentwidth:
                 words = line.split()
                 if max(map(len, words)) > contentwidth:
-                    raise ValueError("Content width is too small")
+                    continue
                 res, part, others = [], words[0], words[1:]
                 for word in others:
                     if len(' ') + len(word) > contentwidth - len(part):
@@ -269,6 +390,7 @@ class PlaintextBox:
         contentlines.append(cornerchars[0] + linechars[1] * (contentwidth + len(self.hpadding)*2) + cornerchars[1])
         return ('\n').join(contentlines)
 
+
 def init_varfilter():
     global varfilter
     global varpattern
@@ -278,97 +400,35 @@ def init_varfilter():
     varpattern = re.compile("|".join(varfilter.keys()))
     varsubst = dict(title=config.prefs['title'], date=interface.start.strftime(DATEFMT), time=interface.start.strftime(TIMEFMT), hostname=util.hostname(config.prefs['hostname-path']), version=logparse.__version__, css=os.path.relpath(config.prefs['css'], os.path.dirname(config.prefs['output'])))
 
-def writetitle(title):  # write title for a section
-    if (title == '' or '\n' in title):
-        logger.error("Invalid title")
-        raise ValueError 
-    logger.debug("Writing title for " + title)
-    return tag('h2', 0, title)
-
-def opentag(tag, block = 0, id = None, cl = None):   # write html opening tag
-    output = ""
-    if (block):
-        output += '\n'
-    output += '<' + tag
-    if (id != None):
-        output += " id='" + id + "'"
-    if (cl != None):
-        output += " class='" + cl + "'"
-    output += '>'
-    if (block):
-        output += '\n'
-    return output
-
-def closetag(tag, block = 0):  # write html closing tag
-    if (block == 0):
-        return "</" + tag + ">"
-    else:
-        return "\n</" + tag + ">\n"
-
-def tag(tag, block = 0, content = ""):  # write html opening tag, content, and html closing tag
-    o = opentag(tag, block)
-    c = closetag(tag, block)
-    return o + content + c
-
-def orderbyfreq(l):     # order a list by the frequency of its elements and remove duplicates
-    temp_l = l[:]
-    l = list(set(l))
-    l = [[i, temp_l.count(i)] for i in l]   # add count of each element
-    l.sort(key=lambda x:temp_l.count(x[0])) # sort by count
-    l = [i[0] + ' (' + str(i[1]) + ')' for i in l]  # put element and count into string
-    l = l[::-1]     # reverse
-    return l
-
-def addtag(l, tag):  # add prefix and suffix tags to each item in a list
-    l2 = ['<' + tag + '>' + i + '</' + tag + '>' for i in l]
-    return l2
-
-def truncl(input, limit):      # truncate list
-    if (len(input) > limit):
-        more = str(len(input) - limit)
-        output = input[:limit]
-        output.append("+ " + more + " more")
-        return(output)
-    else:
-        return(input)
+def backticks(l):
+    return ["`" + x + "`" for x in l]
+
 
-def plural(noun, quantity): # return "1 noun" or "n nouns"
+def plural(noun, quantity):
+    """
+    Return "1 noun" or "n nouns"
+    """
     if (quantity == 1):
         return(str(quantity) + " " + noun)
     else:
         return(str(quantity) + " " + noun + "s")
 
-def parsesize(num, suffix='B'):     # return human-readable size from number of bytes
+
+def parsesize(num, suffix='B'):
+    """
+    Return human-readable size from number of bytes
+    """
     for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
         if abs(num) < 1024.0:
             return "%3.1f %s%s" % (num, unit, suffix)
         num /= 1024.0
     return "%.1f%s%s" % (num, 'Yi', suffix)
 
-def fsubject(template): # Replace variables in the title template provided in config
+
+def fsubject(template):
+    """
+    Replace variables in the title template provided in config
+    """
     r = varpattern.sub(lambda m: varfilter[re.escape(m.group(0))], template)
     logger.debug("Returning subject line " + r)
     return r
-
-def writedata(subtitle, data = None):   # write title and data
-    if (subtitle == ""):
-        logger.warning("No subtitle provided.. skipping section")
-        return
-
-    if (data == None or len(data) == 0):
-        logger.debug("No data provided.. just printing subtitle")
-        return tag('p', 0, subtitle)
-    else:
-        logger.debug("Received data " + str(data))
-        subtitle += ':'
-        if (len(data) == 1):
-            return tag('p', 0, subtitle + ' ' + data[0])
-        else:
-            output = ""
-            output += tag('p', 0, subtitle)
-            output += opentag('ul', 1)
-            for datum in data:
-                output += tag('li', 0, datum)
-            output += closetag('ul', 1)
-            return output
-