Skip to content

Commit dde9fdb

Browse files
authoredJun 5, 2018
bpo-33165: Added stacklevel parameter to logging APIs. (GH-7424)
1 parent 9ef1b06 commit dde9fdb

File tree

4 files changed

+77
-19
lines changed

4 files changed

+77
-19
lines changed
 

‎Doc/library/logging.rst

+34-16
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ is the module's name in the Python package namespace.
160160
*msg* using the string formatting operator. (Note that this means that you can
161161
use keywords in the format string, together with a single dictionary argument.)
162162

163-
There are three keyword arguments in *kwargs* which are inspected:
164-
*exc_info*, *stack_info*, and *extra*.
163+
There are four keyword arguments in *kwargs* which are inspected:
164+
*exc_info*, *stack_info*, *stacklevel* and *extra*.
165165

166166
If *exc_info* does not evaluate as false, it causes exception information to be
167167
added to the logging message. If an exception tuple (in the format returned by
@@ -188,11 +188,19 @@ is the module's name in the Python package namespace.
188188
This mimics the ``Traceback (most recent call last):`` which is used when
189189
displaying exception frames.
190190

191-
The third keyword argument is *extra* which can be used to pass a
192-
dictionary which is used to populate the __dict__ of the LogRecord created for
193-
the logging event with user-defined attributes. These custom attributes can then
194-
be used as you like. For example, they could be incorporated into logged
195-
messages. For example::
191+
The third optional keyword argument is *stacklevel*, which defaults to ``1``.
192+
If greater than 1, the corresponding number of stack frames are skipped
193+
when computing the line number and function name set in the :class:`LogRecord`
194+
created for the logging event. This can be used in logging helpers so that
195+
the function name, filename and line number recorded are not the information
196+
for the helper function/method, but rather its caller. The name of this
197+
parameter mirrors the equivalent one in the :mod:`warnings` module.
198+
199+
The fourth keyword argument is *extra* which can be used to pass a
200+
dictionary which is used to populate the __dict__ of the :class:`LogRecord`
201+
created for the logging event with user-defined attributes. These custom
202+
attributes can then be used as you like. For example, they could be
203+
incorporated into logged messages. For example::
196204

197205
FORMAT = '%(asctime)-15s %(clientip)s %(user)-8s %(message)s'
198206
logging.basicConfig(format=FORMAT)
@@ -213,9 +221,9 @@ is the module's name in the Python package namespace.
213221
If you choose to use these attributes in logged messages, you need to exercise
214222
some care. In the above example, for instance, the :class:`Formatter` has been
215223
set up with a format string which expects 'clientip' and 'user' in the attribute
216-
dictionary of the LogRecord. If these are missing, the message will not be
217-
logged because a string formatting exception will occur. So in this case, you
218-
always need to pass the *extra* dictionary with these keys.
224+
dictionary of the :class:`LogRecord`. If these are missing, the message will
225+
not be logged because a string formatting exception will occur. So in this case,
226+
you always need to pass the *extra* dictionary with these keys.
219227

220228
While this might be annoying, this feature is intended for use in specialized
221229
circumstances, such as multi-threaded servers where the same code executes in
@@ -230,6 +238,9 @@ is the module's name in the Python package namespace.
230238
.. versionchanged:: 3.5
231239
The *exc_info* parameter can now accept exception instances.
232240

241+
.. versionadded:: 3.8
242+
The *stacklevel* parameter was added.
243+
233244

234245
.. method:: Logger.info(msg, *args, **kwargs)
235246

@@ -300,12 +311,19 @@ is the module's name in the Python package namespace.
300311
Removes the specified handler *hdlr* from this logger.
301312

302313

303-
.. method:: Logger.findCaller(stack_info=False)
314+
.. method:: Logger.findCaller(stack_info=False, stacklevel=1)
304315

305316
Finds the caller's source filename and line number. Returns the filename, line
306317
number, function name and stack information as a 4-element tuple. The stack
307318
information is returned as ``None`` unless *stack_info* is ``True``.
308319

320+
The *stacklevel* parameter is passed from code calling the :meth:`debug`
321+
and other APIs. If greater than 1, the excess is used to skip stack frames
322+
before determining the values to be returned. This will generally be useful
323+
when calling logging APIs from helper/wrapper code, so that the information
324+
in the event log refers not to the helper/wrapper code, but to the code that
325+
calls it.
326+
309327

310328
.. method:: Logger.handle(record)
311329

@@ -646,9 +664,9 @@ sophisticated criteria than levels, they get to see every record which is
646664
processed by the handler or logger they're attached to: this can be useful if
647665
you want to do things like counting how many records were processed by a
648666
particular logger or handler, or adding, changing or removing attributes in
649-
the LogRecord being processed. Obviously changing the LogRecord needs to be
650-
done with some care, but it does allow the injection of contextual information
651-
into logs (see :ref:`filters-contextual`).
667+
the :class:`LogRecord` being processed. Obviously changing the LogRecord needs
668+
to be done with some care, but it does allow the injection of contextual
669+
information into logs (see :ref:`filters-contextual`).
652670

653671
.. _log-record:
654672

@@ -702,13 +720,13 @@ wire).
702720
be used.
703721

704722
.. versionchanged:: 3.2
705-
The creation of a ``LogRecord`` has been made more configurable by
723+
The creation of a :class:`LogRecord` has been made more configurable by
706724
providing a factory which is used to create the record. The factory can be
707725
set using :func:`getLogRecordFactory` and :func:`setLogRecordFactory`
708726
(see this for the factory's signature).
709727

710728
This functionality can be used to inject your own values into a
711-
LogRecord at creation time. You can use the following pattern::
729+
:class:`LogRecord` at creation time. You can use the following pattern::
712730

713731
old_factory = logging.getLogRecordFactory()
714732

‎Lib/logging/__init__.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -1397,7 +1397,7 @@ def log(self, level, msg, *args, **kwargs):
13971397
if self.isEnabledFor(level):
13981398
self._log(level, msg, args, **kwargs)
13991399

1400-
def findCaller(self, stack_info=False):
1400+
def findCaller(self, stack_info=False, stacklevel=1):
14011401
"""
14021402
Find the stack frame of the caller so that we can note the source
14031403
file name, line number and function name.
@@ -1407,6 +1407,12 @@ def findCaller(self, stack_info=False):
14071407
#IronPython isn't run with -X:Frames.
14081408
if f is not None:
14091409
f = f.f_back
1410+
orig_f = f
1411+
while f and stacklevel > 1:
1412+
f = f.f_back
1413+
stacklevel -= 1
Has a comment. Original line has a comment.
1414+
if not f:
1415+
f = orig_f
14101416
rv = "(unknown file)", 0, "(unknown function)", None
14111417
while hasattr(f, "f_code"):
14121418
co = f.f_code
@@ -1442,7 +1448,8 @@ def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
14421448
rv.__dict__[key] = extra[key]
14431449
return rv
14441450

1445-
def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False):
1451+
def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False,
1452+
stacklevel=1):
14461453
"""
14471454
Low-level logging routine which creates a LogRecord and then calls
14481455
all the handlers of this logger to handle the record.
@@ -1453,7 +1460,7 @@ def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False):
14531460
#exception on some versions of IronPython. We trap it here so that
14541461
#IronPython can use logging.
14551462
try:
1456-
fn, lno, func, sinfo = self.findCaller(stack_info)
1463+
fn, lno, func, sinfo = self.findCaller(stack_info, stacklevel)
14571464
except ValueError: # pragma: no cover
14581465
fn, lno, func = "(unknown file)", 0, "(unknown function)"
14591466
else: # pragma: no cover

‎Lib/test/test_logging.py

+31
Original file line numberDiff line numberDiff line change
@@ -4057,6 +4057,37 @@ def test_find_caller_with_stack_info(self):
40574057
self.assertEqual(len(called), 1)
40584058
self.assertEqual('Stack (most recent call last):\n', called[0])
40594059

4060+
def test_find_caller_with_stacklevel(self):
4061+
the_level = 1
4062+
4063+
def innermost():
4064+
self.logger.warning('test', stacklevel=the_level)
4065+
4066+
def inner():
4067+
innermost()
4068+
4069+
def outer():
4070+
inner()
4071+
4072+
records = self.recording.records
4073+
outer()
4074+
self.assertEqual(records[-1].funcName, 'innermost')
4075+
lineno = records[-1].lineno
4076+
the_level += 1
4077+
outer()
4078+
self.assertEqual(records[-1].funcName, 'inner')
4079+
self.assertGreater(records[-1].lineno, lineno)
4080+
lineno = records[-1].lineno
4081+
the_level += 1
4082+
outer()
4083+
self.assertEqual(records[-1].funcName, 'outer')
4084+
self.assertGreater(records[-1].lineno, lineno)
4085+
lineno = records[-1].lineno
4086+
the_level += 1
4087+
outer()
4088+
self.assertEqual(records[-1].funcName, 'test_find_caller_with_stacklevel')
4089+
self.assertGreater(records[-1].lineno, lineno)
4090+
40604091
def test_make_record_with_extra_overwrite(self):
40614092
name = 'my record'
40624093
level = 13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added a stacklevel parameter to logging calls to allow use of wrapper/helper
2+
functions for logging APIs.

0 commit comments

Comments
 (0)
Please sign in to comment.