summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBar Harel <bharel@barharel.com>2023-04-12 07:35:56 (GMT)
committerGitHub <noreply@github.com>2023-04-12 07:35:56 (GMT)
commit8f54302ab49a07e857843f1a551db5ddb536ce56 (patch)
treece22dc0d943450b642a69290303c956385b8740a
parent449bf2a76b23b97a38158d506bc30d3ebe006321 (diff)
downloadcpython-8f54302ab49a07e857843f1a551db5ddb536ce56.zip
cpython-8f54302ab49a07e857843f1a551db5ddb536ce56.tar.gz
cpython-8f54302ab49a07e857843f1a551db5ddb536ce56.tar.bz2
gh-103357: Add logging.Formatter defaults support to logging.config fileConfig and dictConfig (GH-103359)
-rw-r--r--Doc/library/logging.config.rst9
-rw-r--r--Lib/logging/config.py22
-rw-r--r--Lib/test/test_logging.py108
-rw-r--r--Misc/NEWS.d/next/Library/2023-04-08-01-33-12.gh-issue-103357.vjin28.rst3
4 files changed, 137 insertions, 5 deletions
diff --git a/Doc/library/logging.config.rst b/Doc/library/logging.config.rst
index 2daf242..250246b 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
<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
diff --git a/Lib/logging/config.py b/Lib/logging/config.py
index 7cd16c6..16c54a6 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 @@ 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
diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py
index c6de34e..9176d8e 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 @@ 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
index 0000000..83dce56
--- /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.