summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJouke Witteveen <j.witteveen@gmail.com>2022-03-27 13:49:28 (GMT)
committerGitHub <noreply@github.com>2022-03-27 13:49:28 (GMT)
commit5ca6d7469be53960843df39bb900e9c3359f127f (patch)
tree0a0b96d2c1f2e68f993941833fe87da5f5f2274a
parent5fd8c574e016aec85725ddc5ced8742267b0e1b3 (diff)
downloadcpython-5ca6d7469be53960843df39bb900e9c3359f127f.zip
cpython-5ca6d7469be53960843df39bb900e9c3359f127f.tar.gz
cpython-5ca6d7469be53960843df39bb900e9c3359f127f.tar.bz2
bpo-45171: Fix stacklevel handling in logging. (GH-28287)
-rw-r--r--Lib/logging/__init__.py67
-rw-r--r--Lib/test/test_logging.py13
-rw-r--r--Misc/NEWS.d/next/Library/2021-09-11-16-06-54.bpo-45171.ec597j.rst4
3 files changed, 51 insertions, 33 deletions
diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py
index 160b1af..e8054fb 100644
--- a/Lib/logging/__init__.py
+++ b/Lib/logging/__init__.py
@@ -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."""
@@ -184,13 +184,18 @@ else: #pragma: no cover
_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):
@@ -1558,33 +1563,31 @@ class Logger(Filterer):
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"):
- 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")
+ 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):
diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py
index 5f72a6d..00e3541 100644
--- a/Lib/test/test_logging.py
+++ b/Lib/test/test_logging.py
@@ -5050,9 +5050,10 @@ class LoggerTest(BaseTest, AssertErrorMessage):
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()
@@ -5074,6 +5075,16 @@ class LoggerTest(BaseTest, AssertErrorMessage):
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')
diff --git a/Misc/NEWS.d/next/Library/2021-09-11-16-06-54.bpo-45171.ec597j.rst b/Misc/NEWS.d/next/Library/2021-09-11-16-06-54.bpo-45171.ec597j.rst
new file mode 100644
index 0000000..eaa3fb2
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-09-11-16-06-54.bpo-45171.ec597j.rst
@@ -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`.