Skip to content

bpo-33165: Added stacklevel parameter to logging APIs. #7424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 5, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions Doc/library/logging.rst
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ is the module's name in the Python package namespace.
*msg* using the string formatting operator. (Note that this means that you can
use keywords in the format string, together with a single dictionary argument.)

There are three keyword arguments in *kwargs* which are inspected:
*exc_info*, *stack_info*, and *extra*.
There are four keyword arguments in *kwargs* which are inspected:
*exc_info*, *stack_info*, *stacklevel* and *extra*.

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

The third keyword argument is *extra* which can be used to pass a
The third optional keyword argument is *stacklevel*, which defaults to ``1``.
If greater than 1, the corresponding number of stack frames are skipped
when computing the line number and function name set in the LogRecord
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should LogRecord be a link, i.e. :class:`LogRecord`?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I will update the PR. Thanks for spotting it!

created for the logging event. This can be used in logging helpers so that
the function name, filename and line number recorded are not the information
for the helper function/method, but rather its caller. The name of this
parameter mirrors the equivalent one in the :mod:`warnings` module.

The fourth keyword argument is *extra* which can be used to pass a
dictionary which is used to populate the __dict__ of the LogRecord created for
the logging event with user-defined attributes. These custom attributes can then
be used as you like. For example, they could be incorporated into logged
Expand Down Expand Up @@ -230,6 +238,9 @@ is the module's name in the Python package namespace.
.. versionchanged:: 3.5
The *exc_info* parameter can now accept exception instances.

.. versionadded:: 3.8
The *stacklevel* parameter was added.


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

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


.. method:: Logger.findCaller(stack_info=False)
.. method:: Logger.findCaller(stack_info=False, stacklevel=1)

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

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


.. method:: Logger.handle(record)

Expand Down
13 changes: 10 additions & 3 deletions Lib/logging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1397,7 +1397,7 @@ def log(self, level, msg, *args, **kwargs):
if self.isEnabledFor(level):
self._log(level, msg, args, **kwargs)

def findCaller(self, stack_info=False):
def findCaller(self, stack_info=False, stacklevel=1):
"""
Find the stack frame of the caller so that we can note the source
file name, line number and function name.
Expand All @@ -1407,6 +1407,12 @@ def findCaller(self, stack_info=False):
#IronPython isn't run with -X:Frames.
if f is not None:
f = f.f_back
orig_f = f
while f and stacklevel > 1:
f = f.f_back
stacklevel -= 1
if not f:
f = orig_f
rv = "(unknown file)", 0, "(unknown function)", None
while hasattr(f, "f_code"):
co = f.f_code
Expand Down Expand Up @@ -1442,7 +1448,8 @@ def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
rv.__dict__[key] = extra[key]
return rv

def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False):
def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False,
stacklevel=1):
"""
Low-level logging routine which creates a LogRecord and then calls
all the handlers of this logger to handle the record.
Expand All @@ -1453,7 +1460,7 @@ def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False):
#exception on some versions of IronPython. We trap it here so that
#IronPython can use logging.
try:
fn, lno, func, sinfo = self.findCaller(stack_info)
fn, lno, func, sinfo = self.findCaller(stack_info, stacklevel)
except ValueError: # pragma: no cover
fn, lno, func = "(unknown file)", 0, "(unknown function)"
else: # pragma: no cover
Expand Down
31 changes: 31 additions & 0 deletions Lib/test/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -4057,6 +4057,37 @@ def test_find_caller_with_stack_info(self):
self.assertEqual(len(called), 1)
self.assertEqual('Stack (most recent call last):\n', called[0])

def test_find_caller_with_stacklevel(self):
the_level = 1

def innermost():
self.logger.warning('test', stacklevel=the_level)

def inner():
innermost()

def outer():
inner()

records = self.recording.records
outer()
self.assertEqual(records[-1].funcName, 'innermost')
lineno = records[-1].lineno
the_level += 1
outer()
self.assertEqual(records[-1].funcName, 'inner')
self.assertGreater(records[-1].lineno, lineno)
lineno = records[-1].lineno
the_level += 1
outer()
self.assertEqual(records[-1].funcName, 'outer')
self.assertGreater(records[-1].lineno, lineno)
lineno = records[-1].lineno
the_level += 1
outer()
self.assertEqual(records[-1].funcName, 'test_find_caller_with_stacklevel')
self.assertGreater(records[-1].lineno, lineno)

def test_make_record_with_extra_overwrite(self):
name = 'my record'
level = 13
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added a stacklevel parameter to logging calls to allow use of wrapper/helper
functions for logging APIs.