From 5a779a33790d24ab2ee9bb5dd7624d98d91578bc Mon Sep 17 00:00:00 2001
From: Athanasius <github@miggy.org>
Date: Sat, 25 Jul 2020 15:22:22 +0100
Subject: [PATCH] Cleanups and docstrings

* Added/fleshed out docstrings on file, classes and functions.
* No need to use a function for the stack frame getting.
* Check if LogRecord has class or qualname before setting, allowing
 upstream to implement them.
* Use setattr()/getattr() rather than __dict__ fiddling.
* Force an error string into class/qualname if we have issues finding
 them, rather than failing silently to ''.
---
 EDMCLogging.py | 76 +++++++++++++++++++++++++++++++++-----------------
 1 file changed, 50 insertions(+), 26 deletions(-)

diff --git a/EDMCLogging.py b/EDMCLogging.py
index 24edac32..f522864e 100644
--- a/EDMCLogging.py
+++ b/EDMCLogging.py
@@ -1,5 +1,8 @@
 """
-TODO: blurb
+This module provides for a common logging-powered log facility.
+Mostly it implements a logging.Filter() in order to get two extra
+members on the logging.LogRecord instance for use in logging.Formatter()
+strings.
 """
 
 import sys
@@ -8,8 +11,14 @@ from typing import Tuple
 
 class logger(object):
     """
-    TODO: desc
     Wrapper class for all logging configuration and code.
+
+    Class instantiation requires the 'logger name' and optional loglevel.
+    It is intended that this 'logger name' be re-used in all files/modules
+    that need to log.
+
+    Users of this class should then call getLogger() to get the
+    logging.Logger instance.
     """
     def __init__(self, logger_name: str, loglevel: int=logging.DEBUG):
         """
@@ -36,48 +45,61 @@ class logger(object):
         self.logger.addHandler(self.logger_channel)
 
     def getLogger(self) -> logging.Logger:
+        """
+        :return: The logging.Logger instance.
+        """
         return self.logger
 
 
 class EDMCContextFilter(logging.Filter):
     """
-    TODO: Update this
-    logging.Filter sub-class to place the calling __class__ in the record.
+    logging.Filter sub-class to place extra attributes of the calling site
+    into the record.
     """
     def filter(self, record: logging.LogRecord) -> bool:
         """
+        Attempt to set the following in the LogRecord:
 
-        :param record:
-        :return: bool - True for this record to be logged.
+            1. class = class name of the call site, if applicable
+            2. qualname = __qualname__ of the call site.  This simplifies
+             logging.Formatter() as you can use just this no matter if there is
+             a class involved or not, so you get a nice clean:
+                 <file/module>.<classA>[.classB....].<function>
+
+        :param record: The LogRecord we're "filtering"
+        :return: bool - Always true in order for this record to be logged.
         """
-        # TODO: Only set these if they're not already, in case upstream
-        #       adds them.
-        # TODO: Try setattr(record, 'class', ...
-        (class_name, qualname) = self.caller_class_and_qualname()
-        record.__dict__['class'] = class_name
-        record.__dict__['qualname'] = qualname
+        class_name = qualname = ''
+        # Don't even call in if both already set.
+        if not getattr(record, 'class', None) or not getattr(record, 'qualname', None):
+            (class_name, qualname) = self.caller_class_and_qualname()
+
+        # Only set if not already provided by logging itself
+        if getattr(record, 'class', None) is None:
+            setattr(record, 'class', class_name)
+
+        # Only set if not already provided by logging itself
+        if getattr(record, 'qualname', None) is None:
+            setattr(record, 'qualname', qualname)
+
         return True
 
-    def caller_class_and_qualname(self, skip=4) -> Tuple[str, str]:
+    def caller_class_and_qualname(self) -> Tuple[str, str]:
         """
-        Figure out our caller's qualname
+        Figure out our caller's class name and qualname
 
         Ref: <https://gist.github.com/techtonik/2151726#gistcomment-2333747>
 
-        :param skip: How many stack frames above to look.
-        :return: str: The caller's qualname
+        :return: Tuple[str, str]: The caller's class name and qualname
         """
-        import inspect
+        # TODO: we might as well just walk this below.
+        # Build the stack of frames from here upwards
+        stack = []
+        frame = sys._getframe(0)
+        while frame:
+            stack.append(frame)
+            frame = frame.f_back
 
-        # TODO: Fold this into caller_class()
-        def stack_(frame):
-            framelist = []
-            while frame:
-                framelist.append(frame)
-                frame = frame.f_back
-            return framelist
-
-        stack = stack_(sys._getframe(0))
         # Go up through stack frames until we find the first with a
         # type(f_locals.self) of logging.Logger.  This should be the start
         # of the frames internal to logging.
@@ -117,8 +139,10 @@ class EDMCContextFilter(logging.Filter):
 
         if caller_qualname == '':
             print('ALERT!  Something went wrong with finding caller qualname for logging!')
+            caller_qualname = '<ERROR in EDMCLogging.caller_class_and_qualname()>'
 
         if caller_class_name == '':
             print('ALERT!  Something went wrong with finding caller class name for logging!')
+            caller_class_name = '<ERROR in EDMCLogging.caller_class_and_qualname()>'
 
         return (caller_class_name, caller_qualname)