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

This commit is contained in:
Jouke Witteveen 2022-03-27 15:49:28 +02:00 committed by GitHub
parent 5fd8c574e0
commit 5ca6d7469b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 51 additions and 33 deletions

View file

@ -159,8 +159,8 @@ def addLevelName(level, levelName):
finally: finally:
_releaseLock() _releaseLock()
if hasattr(sys, '_getframe'): if hasattr(sys, "_getframe"):
currentframe = lambda: sys._getframe(3) currentframe = lambda: sys._getframe(1)
else: #pragma: no cover else: #pragma: no cover
def currentframe(): def currentframe():
"""Return the frame object for the caller's stack frame.""" """Return the frame object for the caller's stack frame."""
@ -184,13 +184,18 @@ else: #pragma: no cover
_srcfile = os.path.normcase(addLevelName.__code__.co_filename) _srcfile = os.path.normcase(addLevelName.__code__.co_filename)
# _srcfile is only used in conjunction with sys._getframe(). # _srcfile is only used in conjunction with sys._getframe().
# To provide compatibility with older versions of Python, set _srcfile # Setting _srcfile to None will prevent findCaller() from being called. This
# to None if _getframe() is not available; this value will prevent # way, you can avoid the overhead of fetching caller information.
# findCaller() from being called. You can also do this if you want to avoid
# the overhead of fetching caller information, even when _getframe() is # The following is based on warnings._is_internal_frame. It makes sure that
# available. # frames of the import mechanism are skipped when logging at module level and
#if not hasattr(sys, '_getframe'): # using a stacklevel value greater than one.
# _srcfile = None 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): def _checkLevel(level):
@ -1558,33 +1563,31 @@ class Logger(Filterer):
f = currentframe() f = currentframe()
#On some versions of IronPython, currentframe() returns None if #On some versions of IronPython, currentframe() returns None if
#IronPython isn't run with -X:Frames. #IronPython isn't run with -X:Frames.
if f is not None: if f is None:
f = f.f_back return "(unknown file)", 0, "(unknown function)", None
orig_f = f while stacklevel > 0:
while f and stacklevel > 1: next_f = f.f_back
f = f.f_back if next_f is None:
stacklevel -= 1 ##TODO: We've got options here
if not f: ## If we want to use the last (deepest) frame:
f = orig_f break
rv = "(unknown file)", 0, "(unknown function)", None ## If we want to mimic the warnings module:
while hasattr(f, "f_code"): #return ("sys", 1, "(unknown function)", None)
co = f.f_code ## If we want to be pedantic:
filename = os.path.normcase(co.co_filename) #raise ValueError("call stack is not deep enough")
if filename == _srcfile: f = next_f
f = f.f_back if not _is_internal_frame(f):
continue stacklevel -= 1
sinfo = None co = f.f_code
if stack_info: sinfo = None
sio = io.StringIO() if stack_info:
sio.write('Stack (most recent call last):\n') with io.StringIO() as sio:
sio.write("Stack (most recent call last):\n")
traceback.print_stack(f, file=sio) traceback.print_stack(f, file=sio)
sinfo = sio.getvalue() sinfo = sio.getvalue()
if sinfo[-1] == '\n': if sinfo[-1] == '\n':
sinfo = sinfo[:-1] sinfo = sinfo[:-1]
sio.close() return co.co_filename, f.f_lineno, co.co_name, sinfo
rv = (co.co_filename, f.f_lineno, co.co_name, sinfo)
break
return rv
def makeRecord(self, name, level, fn, lno, msg, args, exc_info, def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
func=None, extra=None, sinfo=None): func=None, extra=None, sinfo=None):

View file

@ -5050,9 +5050,10 @@ class LoggerTest(BaseTest, AssertErrorMessage):
def test_find_caller_with_stacklevel(self): def test_find_caller_with_stacklevel(self):
the_level = 1 the_level = 1
trigger = self.logger.warning
def innermost(): def innermost():
self.logger.warning('test', stacklevel=the_level) trigger('test', stacklevel=the_level)
def inner(): def inner():
innermost() innermost()
@ -5074,6 +5075,16 @@ class LoggerTest(BaseTest, AssertErrorMessage):
self.assertEqual(records[-1].funcName, 'outer') self.assertEqual(records[-1].funcName, 'outer')
self.assertGreater(records[-1].lineno, lineno) self.assertGreater(records[-1].lineno, lineno)
lineno = records[-1].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 the_level += 1
outer() outer()
self.assertEqual(records[-1].funcName, 'test_find_caller_with_stacklevel') self.assertEqual(records[-1].funcName, 'test_find_caller_with_stacklevel')

View file

@ -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`.