Repository: bttner/kanelbulle

Issue:
Implement a simple logger
To track events that happen when the software runs, a simple logger should be implemented. Requirements:
- Can load settings via a _json_ file.
- Saves the output in a _log_ file.
- Can handle different loggers (one logger per file).

Sufficient tests should be included as well.

Comments:

Patch:
diff --git a/kanelbulle/utils/log.py b/kanelbulle/utils/log.py
new file mode 100644
index 0000000..47e1c69
--- /dev/null
+++ b/kanelbulle/utils/log.py
@@ -0,0 +1,75 @@
+"""Utilities related to logging."""
+
+import logging
+from kanelbulle.config import config
+
+# Log format to use.
+FORMATTER = logging.Formatter('%(asctime)s | %(name)-10s | '
+                              '%(levelname)-8s | %(message)s', '%Y-%m-%d %H:%M')
+
+# Log levels to use.
+LOG_LEVELS = {
+    'CRITICAL': logging.CRITICAL,
+    'ERROR': logging.ERROR,
+    'WARNING': logging.WARNING,
+    'INFO': logging.INFO,
+    'DEBUG': logging.DEBUG,
+}
+
+# Load configurations from file. If it fails, take predefined values.
+ERROR = False
+FILE_LEVEL = logging.DEBUG
+CONSOLE_LEVEL = logging.WARNING
+
+try:
+    CONFIG = config.var.data['logger']
+    FILE_LEVEL = LOG_LEVELS[CONFIG['file']['level'].upper()]
+    CONSOLE_LEVEL = LOG_LEVELS[CONFIG['console']['level'].upper()]
+except (TypeError, KeyError):
+    ERROR = True
+
+# Initialize handlers.
+FILE = logging.FileHandler('kanelbulle/utils/log.log')
+FILE.setFormatter(FORMATTER)
+FILE.setLevel(FILE_LEVEL)
+
+CONSOLE = logging.StreamHandler()
+CONSOLE.setFormatter(FORMATTER)
+CONSOLE.setLevel(CONSOLE_LEVEL)
+
+
+def __init__():
+    """Reset log file or create a new one in case the respective file does not
+    exist.
+    """
+    wfile = logging.FileHandler('kanelbulle/utils/log.log', mode='w')
+    wfile.setLevel(logging.DEBUG)
+
+    logger = logging.getLogger(__name__)
+    logger.setLevel(logging.DEBUG)
+    logger.addHandler(wfile)
+    logger.info('Log file')
+    logger.info(90*'-')
+
+
+def get_logger(name):
+    """Set up a new logger.
+
+    Args:
+        name: name of the new logger (string).
+
+    Return:
+        Logger with specified name.
+    """
+    logger = logging.getLogger(name)
+    logger.setLevel(logging.DEBUG)
+    logger.addHandler(FILE)
+    logger.addHandler(CONSOLE)
+
+    return logger
+
+
+# The different loggers used (alphabetical order).
+app = get_logger('app')
+config = get_logger('config')
+log = get_logger('log')


Test patch:
diff --git a/tests/unit/test_log.py b/tests/unit/test_log.py
new file mode 100644
index 0000000..0217c0d
--- /dev/null
+++ b/tests/unit/test_log.py
@@ -0,0 +1,45 @@
+"""Tests for kanelbulle.utils.log."""
+
+import importlib
+import pytest
+from unittest import mock
+from kanelbulle.utils import log
+from kanelbulle.config import config
+
+config_none = None
+config_empty = {}
+config_no_logger = {"color": "blue"}
+config_no_file = {"logger": {"color": {"level": "wrong"}, "console": {
+    "level": "warning"}}}
+config_no_level = {"logger": {"file": {"color": "blue"}, "console": {
+    "color": "red"}}}
+config_wrong_logger = {"logger": "wrong"}
+config_wrong_file = {"logger": {"file": "wrong", "console": {
+    "level": "warning"}}}
+config_wrong_level = {"logger": {"file": {"level": "wrong"}, "console": {
+    "level": "warning"}}}
+@pytest.mark.parametrize('config_data', [config_none, config_empty,
+                                         config_no_logger, config_no_file,
+                                         config_no_level, config_wrong_logger,
+                                         config_wrong_file, config_wrong_level])
+def test_config_error(config_data):
+    """IF the configuration file does not include an entry for
+    logging or is incomplete, THEN a corresponding error value should be set to
+    True and predefined values should be used instead.
+    """
+    with mock.patch.object(config.var, 'data', config_data):
+        pre_file_level = log.FILE_LEVEL
+        pre_console_level = log.CONSOLE_LEVEL
+        importlib.reload(log)
+        assert log.ERROR == True
+        assert log.FILE_LEVEL == pre_file_level
+        assert log.CONSOLE_LEVEL == pre_console_level
+
+
+@mock.patch.object(log, 'logging')
+def test_init_(mock_logging):
+    """The initialization of the log module shall overwrite the existing log
+    file or shall create a new one if a respective file does not exist.
+    """
+    log.__init__()
+    mock_logging.FileHandler.assert_called_with(mock.ANY, mode='w')