onaplogging: Docstrings, refactor, type hinting
[logging-analytics.git] / pylog / onaplogging / colorFormatter.py
index ef713ea..5de0d60 100644 (file)
 # limitations under the License.
 
 import os
-import sys
-from logging import Formatter
 
-from .utils import is_above_python_2_7, is_above_python_3_2
+from logging import Formatter, LogRecord
+from deprecated import deprecated
+from warnings import warn
+from typing import Optional, Union, Dict
 
+from onaplogging.utils.system import is_above_python_2_7, is_above_python_3_2
+from onaplogging.utils.styles import (
+    ATTRIBUTES,
+    HIGHLIGHTS,
+    COLORS,
 
-ATTRIBUTES = {
-    'normal': 0,
-    'bold': 1,
-    'underline': 4,
-    'blink': 5,
-    'invert': 7,
-    'hide': 8,
+    ATTRIBUTE_TAG,
+    HIGHLIGHT_TAG,
+    COLOR_TAG,
 
-}
+    RESET,
+    FMT_STR
+)
 
 
-HIGHLIGHTS = {
-
-    'black': 40,
-    'red':  41,
-    'green': 42,
-    'yellow': 43,
-    'blue': 44,
-    'purple': 45,
-    'cyan': 46,
-    'white': 47,
-}
+class BaseColorFormatter(Formatter):
+    """Text color formatter class.
+
+    Wraps the logging. Uses Git shell coloring codes.  Doesn't support Windows
+    CMD yet. If `fmt` is not suppied, the `style` is used. Eventually converts
+    a LogRecord object to "colored" text.
+
+    TODO:
+        Support for Windows CMD.
+    Extends:
+        logging.Formatter
+    Properties:
+        style       : '%', '{' or '$' formatting.
+        datefrmt    : ISO8601-like (or RFC 3339-like) format.
+    Args:
+        fmt         : human-readable format.                  Defaults to None.
+        datefmt     : ISO8601-like (or RFC 3339-like) format. Defaults to None.
+        colorfmt    : Color schemas for logging levels.       Defaults to None.
+        style       : '%', '{' or '$' formatting.             Defaults to '%'.
+    Methods:
+        format      : formats a LogRecord record.
+        _parseColor : selects colors based on a logging levels.
+    """
+
+    @property
+    def style(self):
+        # type: () -> str
+        return self.__style  # name mangling with __ to avoid accidents
+
+    @property
+    def colorfmt(self):
+        # type: () -> str
+        return self.__colorfmt
+
+    @style.setter
+    def style(self, value):
+        # type: (str) -> None
+        """Assign new style."""
+        self.__style = value
+
+    @colorfmt.setter
+    def colorfmt(self, value):
+        # type: (str) -> None
+        """Assign new color format."""
+        self.__colorfmt = value
+
+    def __init__(self,
+                 fmt=None,          # type: Optional[str]
+                 datefmt=None,      # type: Optional[str]
+                 colorfmt=None,     # type: Optional[Dict]
+                 style="%"):        # type: Optional[str]
 
-COLORS = {
+        if is_above_python_3_2():
+            super(BaseColorFormatter, self). \
+            __init__(fmt=fmt,  # noqa: E122
+                    datefmt=datefmt,
+                    style=style)
 
-    'black': 30,
-    'red': 31,
-    'green': 32,
-    'yellow': 33,
-    'blue': 34,
-    'purple': 35,
-    'cyan': 36,
-    'white': 37,
-}
+        elif is_above_python_2_7():
+            super(BaseColorFormatter, self). \
+            __init__(fmt, datefmt)  # noqa: E122
 
-COLOR_TAG = "color"
-HIGHLIGHT_TAG = "highlight"
-ATTRIBUTE_TAG = "attribute"
+        else:
+            Formatter. \
+            __init__(self, fmt, datefmt)  # noqa: E122
+        self.style = style
+        self.colorfmt = colorfmt
 
-RESET = "\033[0m"
-FMT_STR = "\033[%dm%s"
+    def format(self, record):
+        """Text formatter.
 
+        Connects 2 methods. First it extract a level and a colors
+        assigned to  this level in  the BaseColorFormatter  class.
+        Second it applied the colors to the text.
 
-def colored(text, color=None, on_color=None, attrs=None):
-    # It can't support windows system cmd right now!
-    # TODO: colered output on windows system cmd
-    if os.name in ('nt', 'ce'):
-        return text
+        Args:
+            record   : an instance of a logged event.
+        Returns:
+            str      : "colored" text (formatted text).
+        """
 
-    if isinstance(attrs, str):
-        attrs = [attrs]
+        if is_above_python_2_7():
+            s = super(BaseColorFormatter, self). \
+                format(record)
 
-    if os.getenv('ANSI_COLORS_DISABLED', None) is None:
-        if color is not None and isinstance(color, str):
-            text = FMT_STR % (COLORS.get(color, 0), text)
+        else:
+            s = Formatter. \
+                format(self, record)
 
-        if on_color is not None and isinstance(on_color, str):
-            text = FMT_STR % (HIGHLIGHTS.get(on_color, 0), text)
+        color, highlight, attribute = self._parse_color(record)
 
-        if attrs is not None:
-            for attr in attrs:
-                text = FMT_STR % (ATTRIBUTES.get(attr, 0), text)
+        return apply_color(s, color, highlight, attrs=attribute)
 
-        #  keep origin color for tail spaces
-        text += RESET
-    return text
+    def _parse_color(self, record):
+        # type: (LogRecord) -> (Optional[str], Optional[str], Optional[str])
+        """Color formatter based on the logging level.
 
+        This method formats the  record according to  its  level
+        and a color format  set for that level.  If the level is
+        not found, then this method will eventually return None.
 
-class BaseColorFormatter(Formatter):
+        Args:
+            record  : an instance of a logged event.
+        Returns:
+            str     : Colors.
+            str     : Hightlight tag.
+            str     : Attribute tag.
+        """
+        if  self.colorfmt and \
+            isinstance(self.colorfmt, dict):
 
-    def __init__(self, fmt=None, datefmt=None, colorfmt=None, style="%"):
-        if is_above_python_3_2():
-            super(BaseColorFormatter, self).__init__(
-                fmt=fmt, datefmt=datefmt, style=style)
-        elif is_above_python_2_7():
-            super(BaseColorFormatter, self).__init__(fmt, datefmt)
-        else:
-            Formatter.__init__(self, fmt, datefmt)
+            level = record.levelname
+            colors = self.colorfmt.get(level, None)
 
-        self.style = style
-        self.colorfmt = colorfmt
+            if  colors is not None and \
+                isinstance(colors, dict):
+                return (colors.get(COLOR_TAG, None),        # noqa: E201
+                        colors.get(HIGHLIGHT_TAG, None),
+                        colors.get(ATTRIBUTE_TAG, None))    # noqa: E202
+        return None, None, None
 
+    @deprecated(reason="Will be removed. Use _parse_color(record) instead.")
     def _parseColor(self, record):
         """
-        color formatter for instance:
-        {
-            "logging-levelname":
-                {
-                    "color":"<COLORS>",
-                    "highlight":"<HIGHLIGHTS>",
-                    "attribute":"<ATTRIBUTES>",
-                }
-        }
-        :param record:
-        :return: text color, background color, text attribute
+        Color based on logging level.
+        See method _parse_color(record).
         """
-        if self.colorfmt and isinstance(self.colorfmt, dict):
+        return self._parse_color(record)
+
+
+def apply_color(text,           # type: str
+                color=None,     # type: Optional[str]
+                on_color=None,  # type: Optional[str]
+                attrs=None):    # type: Optional[Union[str, list]]
+    # type: (...) -> str
+    """Applies color codes to the text.
+
+    Args:
+        text      : text to be "colored" (formatted).
+        color     : Color in human-readable format. Defaults to None.
+        highlight : Hightlight color in human-readable format.
+                         Previously called "on_color". Defaults to None.
+        attrs     : Colors for attribute(s). Defaults to None.
+    Returns:
+        str       : "colored" text (formatted text).
+    """
+    warn("`on_color` will be replaced with `highlight`.", DeprecationWarning)
+    highlight = on_color  # replace the parameter and remove
 
-            level = record.levelname
-            colors = self.colorfmt.get(level, None)
+    if os.name in ('nt', 'ce'):
+        return text
 
-            if colors is not None and isinstance(colors, dict):
-                return colors.get(COLOR_TAG, None), \
-                       colors.get(HIGHLIGHT_TAG, None), \
-                       colors.get(ATTRIBUTE_TAG, None)
+    if isinstance(attrs, str):
+        attrs = [attrs]
 
-        return None, None, None
+    ansi_disabled = os.getenv('ANSI_COLORS_DISABLED', None)
 
-    def format(self, record):
+    if ansi_disabled is None:
 
-        if sys.version_info > (2, 7):
-            s = super(BaseColorFormatter, self).format(record)
-        else:
-            s = Formatter.format(self, record)
-        color, on_color, attribute = self._parseColor(record)
-        return colored(s, color, on_color, attrs=attribute)
+        if  color is not None and \
+            isinstance(color, str):
+            text = FMT_STR % (COLORS.get(color, 0), text)
+
+        if  highlight is not None and \
+            isinstance(highlight, str):
+            text = FMT_STR % (HIGHLIGHTS.get(highlight, 0), text)
+
+        if attrs is not None:
+            for attr in attrs:
+                text = FMT_STR % (ATTRIBUTES.get(attr, 0), text)
+
+        text += RESET  # keep origin color for tail spaces
+
+    return text
+
+
+@deprecated(reason="Will be removed. Call apply_color(...) instead.")
+def colored(text, color=None, on_color=None, attrs=None):
+    """
+    Format text with color codes.
+    See method apply_color(text, color, on_color, attrs).
+    """
+    return apply_color(text, color, on_color, attrs)