Skip to content

Commit

Permalink
pythongh-103357: Add logging.Formatter defaults support to logging.co…
Browse files Browse the repository at this point in the history
…nfig fileConfig and dictConfig (pythonGH-103359)
  • Loading branch information
bharel authored and aisk committed Apr 18, 2023
1 parent 73a1cc7 commit 14f6c7c
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 5 deletions.
9 changes: 8 additions & 1 deletion Doc/library/logging.config.rst
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
22 changes: 19 additions & 3 deletions Lib/logging/config.py
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
108 changes: 107 additions & 1 deletion Lib/test/test_logging.py
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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': {
Expand All @@ -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' : {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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),
Expand Down
@@ -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.

0 comments on commit 14f6c7c

Please sign in to comment.