]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-103357: Add logging.Formatter defaults support to logging.config fileConfig and...
authorBar Harel <bharel@barharel.com>
Wed, 12 Apr 2023 07:35:56 +0000 (08:35 +0100)
committerGitHub <noreply@github.com>
Wed, 12 Apr 2023 07:35:56 +0000 (08:35 +0100)
Doc/library/logging.config.rst
Lib/logging/config.py
Lib/test/test_logging.py
Misc/NEWS.d/next/Library/2023-04-08-01-33-12.gh-issue-103357.vjin28.rst [new file with mode: 0644]

index 2daf2422ebd5b47bbbc9e65531ab6578d86f2ec1..250246b5cd9adcc2a48289cd62449a5cebe4724d 100644 (file)
@@ -253,6 +253,7 @@ otherwise, the context is used to determine what to instantiate.
    * ``datefmt``
    * ``style``
    * ``validate`` (since version >=3.8)
+   * ``defaults`` (since version >=3.12)
 
   An optional ``class`` key indicates the name of the formatter's
   class (as a dotted module and class name).  The instantiation
@@ -953,16 +954,22 @@ Sections which specify formatter configuration are typified by the following.
 .. code-block:: ini
 
    [formatter_form01]
-   format=F1 %(asctime)s %(levelname)s %(message)s
+   format=F1 %(asctime)s %(levelname)s %(message)s %(customfield)s
    datefmt=
    style=%
    validate=True
+   defaults={'customfield': 'defaultvalue'}
    class=logging.Formatter
 
 The arguments for the formatter configuration are the same as the keys
 in the dictionary schema :ref:`formatters section
 <logging-config-dictschema-formatters>`.
 
+The ``defaults`` entry, when :ref:`evaluated <func-eval>` in the context of
+the ``logging`` package's namespace, is a dictionary of default values for
+custom formatting fields. If not provided, it defaults to ``None``.
+
+
 .. note::
 
    Due to the use of :func:`eval` as described above, there are
index 7cd16c643e9dadf606de1732b08787808505710c..16c54a6a4f7a2f24bb16a8c08736da8b236122b8 100644 (file)
@@ -114,11 +114,18 @@ def _create_formatters(cp):
         fs = cp.get(sectname, "format", raw=True, fallback=None)
         dfs = cp.get(sectname, "datefmt", raw=True, fallback=None)
         stl = cp.get(sectname, "style", raw=True, fallback='%')
+        defaults = cp.get(sectname, "defaults", raw=True, fallback=None)
+
         c = logging.Formatter
         class_name = cp[sectname].get("class")
         if class_name:
             c = _resolve(class_name)
-        f = c(fs, dfs, stl)
+
+        if defaults is not None:
+            defaults = eval(defaults, vars(logging))
+            f = c(fs, dfs, stl, defaults=defaults)
+        else:
+            f = c(fs, dfs, stl)
         formatters[form] = f
     return formatters
 
@@ -668,18 +675,27 @@ class DictConfigurator(BaseConfigurator):
             dfmt = config.get('datefmt', None)
             style = config.get('style', '%')
             cname = config.get('class', None)
+            defaults = config.get('defaults', None)
 
             if not cname:
                 c = logging.Formatter
             else:
                 c = _resolve(cname)
 
+            kwargs  = {}
+
+            # Add defaults only if it exists.
+            # Prevents TypeError in custom formatter callables that do not
+            # accept it.
+            if defaults is not None:
+                kwargs['defaults'] = defaults
+
             # A TypeError would be raised if "validate" key is passed in with a formatter callable
             # that does not accept "validate" as a parameter
             if 'validate' in config:  # if user hasn't mentioned it, the default will be fine
-                result = c(fmt, dfmt, style, config['validate'])
+                result = c(fmt, dfmt, style, config['validate'], **kwargs)
             else:
-                result = c(fmt, dfmt, style)
+                result = c(fmt, dfmt, style, **kwargs)
 
         return result
 
index c6de34e9dbdc8ffcd8e9e8eef4ac97f200d4d3c9..9176d8eeb56d013c2e09e242ace1eada1198a991 100644 (file)
@@ -1524,6 +1524,32 @@ class ConfigFileTest(BaseTest):
     kwargs={{"encoding": "utf-8"}}
     """
 
+
+    config9 = """
+    [loggers]
+    keys=root
+
+    [handlers]
+    keys=hand1
+
+    [formatters]
+    keys=form1
+
+    [logger_root]
+    level=WARNING
+    handlers=hand1
+
+    [handler_hand1]
+    class=StreamHandler
+    level=NOTSET
+    formatter=form1
+    args=(sys.stdout,)
+
+    [formatter_form1]
+    format=%(message)s ++ %(customfield)s
+    defaults={"customfield": "defaultvalue"}
+    """
+
     disable_test = """
     [loggers]
     keys=root
@@ -1687,6 +1713,16 @@ class ConfigFileTest(BaseTest):
         handler = logging.root.handlers[0]
         self.addCleanup(closeFileHandler, handler, fn)
 
+    def test_config9_ok(self):
+        self.apply_config(self.config9)
+        formatter = logging.root.handlers[0].formatter
+        result = formatter.format(logging.makeLogRecord({'msg': 'test'}))
+        self.assertEqual(result, 'test ++ defaultvalue')
+        result = formatter.format(logging.makeLogRecord(
+            {'msg': 'test', 'customfield': "customvalue"}))
+        self.assertEqual(result, 'test ++ customvalue')
+
+
     def test_logger_disabling(self):
         self.apply_config(self.disable_test)
         logger = logging.getLogger('some_pristine_logger')
@@ -2909,6 +2945,30 @@ class ConfigDictTest(BaseTest):
         },
     }
 
+    # config0 but with default values for formatter. Skipped 15, it is defined
+    # in the test code.
+    config16 = {
+        'version': 1,
+        'formatters': {
+            'form1' : {
+                'format' : '%(message)s ++ %(customfield)s',
+                'defaults': {"customfield": "defaultvalue"}
+            },
+        },
+        'handlers' : {
+            'hand1' : {
+                'class' : 'logging.StreamHandler',
+                'formatter' : 'form1',
+                'level' : 'NOTSET',
+                'stream'  : 'ext://sys.stdout',
+            },
+        },
+        'root' : {
+            'level' : 'WARNING',
+            'handlers' : ['hand1'],
+        },
+    }
+
     bad_format = {
         "version": 1,
         "formatters": {
@@ -3021,7 +3081,7 @@ class ConfigDictTest(BaseTest):
         }
     }
 
-    # Configuration with custom function and 'validate' set to False
+    # Configuration with custom function, 'validate' set to False and no defaults
     custom_formatter_with_function = {
         'version': 1,
         'formatters': {
@@ -3048,6 +3108,33 @@ class ConfigDictTest(BaseTest):
         }
     }
 
+    # Configuration with custom function, and defaults
+    custom_formatter_with_defaults = {
+        'version': 1,
+        'formatters': {
+            'form1': {
+                '()': formatFunc,
+                'format': '%(levelname)s:%(name)s:%(message)s:%(customfield)s',
+                'defaults': {"customfield": "myvalue"}
+            },
+        },
+        'handlers' : {
+            'hand1' : {
+                'class': 'logging.StreamHandler',
+                'formatter': 'form1',
+                'level': 'NOTSET',
+                'stream': 'ext://sys.stdout',
+            },
+        },
+        "loggers": {
+            "my_test_logger_custom_formatter": {
+                "level": "DEBUG",
+                "handlers": ["hand1"],
+                "propagate": "true"
+            }
+        }
+    }
+
     config_queue_handler = {
         'version': 1,
         'handlers' : {
@@ -3349,6 +3436,22 @@ class ConfigDictTest(BaseTest):
         handler = logging.root.handlers[0]
         self.addCleanup(closeFileHandler, handler, fn)
 
+    def test_config16_ok(self):
+        self.apply_config(self.config16)
+        h = logging._handlers['hand1']
+
+        # Custom value
+        result = h.formatter.format(logging.makeLogRecord(
+            {'msg': 'Hello', 'customfield': 'customvalue'}))
+        self.assertEqual(result, 'Hello ++ customvalue')
+
+        # Default value
+        result = h.formatter.format(logging.makeLogRecord(
+            {'msg': 'Hello'}))
+        self.assertEqual(result, 'Hello ++ defaultvalue')
+
+
+
     def setup_via_listener(self, text, verify=None):
         text = text.encode("utf-8")
         # Ask for a randomly assigned port (by using port 0)
@@ -3516,6 +3619,9 @@ class ConfigDictTest(BaseTest):
     def test_custom_formatter_function_with_validate(self):
         self.assertRaises(ValueError, self.apply_config, self.custom_formatter_with_function)
 
+    def test_custom_formatter_function_with_defaults(self):
+        self.assertRaises(ValueError, self.apply_config, self.custom_formatter_with_defaults)
+
     def test_baseconfig(self):
         d = {
             'atuple': (1, 2, 3),
diff --git a/Misc/NEWS.d/next/Library/2023-04-08-01-33-12.gh-issue-103357.vjin28.rst b/Misc/NEWS.d/next/Library/2023-04-08-01-33-12.gh-issue-103357.vjin28.rst
new file mode 100644 (file)
index 0000000..83dce56
--- /dev/null
@@ -0,0 +1,3 @@
+Added support for :class:`logging.Formatter` ``defaults`` parameter to
+:func:`logging.config.dictConfig` and :func:`logging.config.fileConfig`.
+Patch by Bar Harel.