From 14f6c7c2af95f96c71207a9c6021854d4bb81b84 Mon Sep 17 00:00:00 2001 From: Bar Harel Date: Wed, 12 Apr 2023 08:35:56 +0100 Subject: [PATCH] gh-103357: Add logging.Formatter defaults support to logging.config fileConfig and dictConfig (GH-103359) --- Doc/library/logging.config.rst | 9 +- Lib/logging/config.py | 22 +++- Lib/test/test_logging.py | 108 +++++++++++++++++- ...-04-08-01-33-12.gh-issue-103357.vjin28.rst | 3 + 4 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-04-08-01-33-12.gh-issue-103357.vjin28.rst diff --git a/Doc/library/logging.config.rst b/Doc/library/logging.config.rst index 2daf2422ebd5b47..250246b5cd9adcc 100644 --- a/Doc/library/logging.config.rst +++ b/Doc/library/logging.config.rst @@ -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 `. +The ``defaults`` entry, when :ref:`evaluated ` 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 diff --git a/Lib/logging/config.py b/Lib/logging/config.py index 7cd16c643e9dadf..16c54a6a4f7a2f2 100644 --- a/Lib/logging/config.py +++ b/Lib/logging/config.py @@ -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 @@ def configure_formatter(self, config): 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 diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index c6de34e9dbdc8ff..9176d8eeb56d013 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -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 @@ def test_config8_ok(self): 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 @@ def test_config15_ok(self): 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 @@ def test_custom_formatter_class_with_validate3(self): 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 index 000000000000000..83dce56ed0b7c5d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-04-08-01-33-12.gh-issue-103357.vjin28.rst @@ -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.