Skip to content

Commit 5ca6d74

Browse files
bpo-45171: Fix stacklevel handling in logging. (GH-28287)
1 parent 5fd8c57 commit 5ca6d74

File tree

3 files changed

+51
-33
lines changed

3 files changed

+51
-33
lines changed

Lib/logging/__init__.py

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ def addLevelName(level, levelName):
159159
finally:
160160
_releaseLock()
161161

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

186186
# _srcfile is only used in conjunction with sys._getframe().
187-
# To provide compatibility with older versions of Python, set _srcfile
188-
# to None if _getframe() is not available; this value will prevent
189-
# findCaller() from being called. You can also do this if you want to avoid
190-
# the overhead of fetching caller information, even when _getframe() is
191-
# available.
192-
#if not hasattr(sys, '_getframe'):
193-
# _srcfile = None
187+
# Setting _srcfile to None will prevent findCaller() from being called. This
188+
# way, you can avoid the overhead of fetching caller information.
189+
190+
# The following is based on warnings._is_internal_frame. It makes sure that
191+
# frames of the import mechanism are skipped when logging at module level and
192+
# using a stacklevel value greater than one.
193+
def _is_internal_frame(frame):
194+
"""Signal whether the frame is a CPython or logging module internal."""
195+
filename = os.path.normcase(frame.f_code.co_filename)
196+
return filename == _srcfile or (
197+
"importlib" in filename and "_bootstrap" in filename
198+
)
194199

195200

196201
def _checkLevel(level):
@@ -1558,33 +1563,31 @@ def findCaller(self, stack_info=False, stacklevel=1):
15581563
f = currentframe()
15591564
#On some versions of IronPython, currentframe() returns None if
15601565
#IronPython isn't run with -X:Frames.
1561-
if f is not None:
1562-
f = f.f_back
1563-
orig_f = f
1564-
while f and stacklevel > 1:
1565-
f = f.f_back
1566-
stacklevel -= 1
1567-
if not f:
1568-
f = orig_f
1569-
rv = "(unknown file)", 0, "(unknown function)", None
1570-
while hasattr(f, "f_code"):
1571-
co = f.f_code
1572-
filename = os.path.normcase(co.co_filename)
1573-
if filename == _srcfile:
1574-
f = f.f_back
1575-
continue
1576-
sinfo = None
1577-
if stack_info:
1578-
sio = io.StringIO()
1579-
sio.write('Stack (most recent call last):\n')
1566+
if f is None:
1567+
return "(unknown file)", 0, "(unknown function)", None
1568+
while stacklevel > 0:
1569+
next_f = f.f_back
1570+
if next_f is None:
1571+
##TODO: We've got options here
1572+
## If we want to use the last (deepest) frame:
1573+
break
1574+
## If we want to mimic the warnings module:
1575+
#return ("sys", 1, "(unknown function)", None)
1576+
## If we want to be pedantic:
1577+
#raise ValueError("call stack is not deep enough")
1578+
f = next_f
1579+
if not _is_internal_frame(f):
1580+
stacklevel -= 1
1581+
co = f.f_code
1582+
sinfo = None
1583+
if stack_info:
1584+
with io.StringIO() as sio:
1585+
sio.write("Stack (most recent call last):\n")
15801586
traceback.print_stack(f, file=sio)
15811587
sinfo = sio.getvalue()
15821588
if sinfo[-1] == '\n':
15831589
sinfo = sinfo[:-1]
1584-
sio.close()
1585-
rv = (co.co_filename, f.f_lineno, co.co_name, sinfo)
1586-
break
1587-
return rv
1590+
return co.co_filename, f.f_lineno, co.co_name, sinfo
15881591

15891592
def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
15901593
func=None, extra=None, sinfo=None):

Lib/test/test_logging.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5050,9 +5050,10 @@ def test_find_caller_with_stack_info(self):
50505050

50515051
def test_find_caller_with_stacklevel(self):
50525052
the_level = 1
5053+
trigger = self.logger.warning
50535054

50545055
def innermost():
5055-
self.logger.warning('test', stacklevel=the_level)
5056+
trigger('test', stacklevel=the_level)
50565057

50575058
def inner():
50585059
innermost()
@@ -5074,6 +5075,16 @@ def outer():
50745075
self.assertEqual(records[-1].funcName, 'outer')
50755076
self.assertGreater(records[-1].lineno, lineno)
50765077
lineno = records[-1].lineno
5078+
trigger = self.logger.warn
5079+
outer()
5080+
self.assertEqual(records[-1].funcName, 'outer')
5081+
root_logger = logging.getLogger()
5082+
root_logger.addHandler(self.recording)
5083+
trigger = logging.warning
5084+
outer()
5085+
self.assertEqual(records[-1].funcName, 'outer')
5086+
root_logger.removeHandler(self.recording)
5087+
trigger = self.logger.warning
50775088
the_level += 1
50785089
outer()
50795090
self.assertEqual(records[-1].funcName, 'test_find_caller_with_stacklevel')
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix handling of the ``stacklevel`` argument to logging functions in the
2+
:mod:`logging` module so that it is consistent accross all logging functions
3+
and, as advertised, similar to the ``stacklevel`` argument used in
4+
:meth:`~warnings.warn`.

0 commit comments

Comments
 (0)