Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Doc/library/logging.config.rst
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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.