Thanks to visit codestin.com
Credit goes to github.com

Skip to content

bpo-45171: Fix stacklevel handling in logging. (GH-28287) #28287

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 1 commit into from
Mar 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
67 changes: 35 additions & 32 deletions Lib/logging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ def addLevelName(level, levelName):
finally:
_releaseLock()

if hasattr(sys, '_getframe'):
currentframe = lambda: sys._getframe(3)
if hasattr(sys, "_getframe"):
currentframe = lambda: sys._getframe(1)
else: #pragma: no cover
def currentframe():
"""Return the frame object for the caller's stack frame."""
Expand All @@ -184,13 +184,18 @@ def currentframe():
_srcfile = os.path.normcase(addLevelName.__code__.co_filename)

# _srcfile is only used in conjunction with sys._getframe().
# To provide compatibility with older versions of Python, set _srcfile
# to None if _getframe() is not available; this value will prevent
# findCaller() from being called. You can also do this if you want to avoid
# the overhead of fetching caller information, even when _getframe() is
# available.
#if not hasattr(sys, '_getframe'):
# _srcfile = None
# Setting _srcfile to None will prevent findCaller() from being called. This
# way, you can avoid the overhead of fetching caller information.

# The following is based on warnings._is_internal_frame. It makes sure that
# frames of the import mechanism are skipped when logging at module level and
# using a stacklevel value greater than one.
def _is_internal_frame(frame):
"""Signal whether the frame is a CPython or logging module internal."""
filename = os.path.normcase(frame.f_code.co_filename)
return filename == _srcfile or (
"importlib" in filename and "_bootstrap" in filename
)


def _checkLevel(level):
Expand Down Expand Up @@ -1558,33 +1563,31 @@ def findCaller(self, stack_info=False, stacklevel=1):
f = currentframe()
#On some versions of IronPython, currentframe() returns None if
#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"):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that the new code doesn't verify that frame object contain an f_code attribute. It does verify that the frame object is not None. The f_code attribute is prescribed by the CPython data model and the warnings module doesn't check for it's presence either (it does check for stack exhaustion/None, like the new code here).

co = f.f_code
filename = os.path.normcase(co.co_filename)
if filename == _srcfile:
f = f.f_back
continue
sinfo = None
if stack_info:
sio = io.StringIO()
sio.write('Stack (most recent call last):\n')
if f is None:
return "(unknown file)", 0, "(unknown function)", None
while stacklevel > 0:
next_f = f.f_back
if next_f is None:
##TODO: We've got options here
## If we want to use the last (deepest) frame:
break
## If we want to mimic the warnings module:
#return ("sys", 1, "(unknown function)", None)
## If we want to be pedantic:
#raise ValueError("call stack is not deep enough")

Choose a reason for hiding this comment

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

So far, findCaller did not throw, so to keep the more backward-compatible I think it would be better if it did not throw.

Comment on lines +1570 to +1577
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@vsajip Thanks for the approval and merge! Note that there was a TODO here which I summarized in the original PR description as:

Regarding the last point, I did not know what to do in case more frames are to be skipped than the height of the stack. I listed three possibilities in the code which I think would all be sensible.

Having it merged like this is not wrong per se (it will use the deepest available frame if the stack is exhausted), but it would be good to at least remove the TODO: part on line 1571. Maybe all of the above could even be replaced with

            if next_f is None:
                # Not enough stack frames. We use the last (deepest) one.
                break

Copy link
Member

Choose a reason for hiding this comment

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

Or maybe leave it as a reminder in case this needs revisiting for any reason?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, but in that case I would still remove the word TODO:, since nothing needs to be done.

Anyway, I really appreciate that you took a look at this PR. Thanks!

f = next_f
if not _is_internal_frame(f):
stacklevel -= 1
co = f.f_code
sinfo = None
if stack_info:
with io.StringIO() as sio:
sio.write("Stack (most recent call last):\n")
traceback.print_stack(f, file=sio)
sinfo = sio.getvalue()
if sinfo[-1] == '\n':
sinfo = sinfo[:-1]
sio.close()
rv = (co.co_filename, f.f_lineno, co.co_name, sinfo)
break
return rv
return co.co_filename, f.f_lineno, co.co_name, sinfo

def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
func=None, extra=None, sinfo=None):
Expand Down
13 changes: 12 additions & 1 deletion Lib/test/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -4997,9 +4997,10 @@ def test_find_caller_with_stack_info(self):

def test_find_caller_with_stacklevel(self):
the_level = 1
trigger = self.logger.warning

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

def inner():
innermost()
Expand All @@ -5021,6 +5022,16 @@ def outer():
self.assertEqual(records[-1].funcName, 'outer')
self.assertGreater(records[-1].lineno, lineno)
lineno = records[-1].lineno
trigger = self.logger.warn
outer()
self.assertEqual(records[-1].funcName, 'outer')
root_logger = logging.getLogger()
root_logger.addHandler(self.recording)
trigger = logging.warning
outer()
self.assertEqual(records[-1].funcName, 'outer')
root_logger.removeHandler(self.recording)
trigger = self.logger.warning
the_level += 1
outer()
self.assertEqual(records[-1].funcName, 'test_find_caller_with_stacklevel')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix handling of the ``stacklevel`` argument to logging functions in the
:mod:`logging` module so that it is consistent accross all logging functions
and, as advertised, similar to the ``stacklevel`` argument used in
:meth:`~warnings.warn`.