Skip to content

Commit ae0a316

Browse files
Abseil Teamcopybara-github
Abseil Team
authored andcommitted
Add support for stacklevel in the logging module as in the standard library.
PiperOrigin-RevId: 704822441
1 parent d3cb234 commit ae0a316

File tree

2 files changed

+96
-11
lines changed

2 files changed

+96
-11
lines changed

absl/logging/__init__.py

+26-11
Original file line numberDiff line numberDiff line change
@@ -1118,10 +1118,11 @@ def findCaller(self, stack_info=False, stacklevel=1):
11181118
11191119
Args:
11201120
stack_info: bool, when True, include the stack trace as a fourth item
1121-
returned. On Python 3 there are always four items returned - the
1122-
fourth will be None when this is False. On Python 2 the stdlib
1123-
base class API only returns three items. We do the same when this
1124-
new parameter is unspecified or False for compatibility.
1121+
returned. On Python 3 there are always four items returned - the fourth
1122+
will be None when this is False. On Python 2 the stdlib base class API
1123+
only returns three items. We do the same when this new parameter is
1124+
unspecified or False for compatibility.
1125+
stacklevel: int, if greater than 1, that number of frames will be skipped.
11251126
11261127
Returns:
11271128
(filename, lineno, methodname[, sinfo]) of the calling method.
@@ -1130,22 +1131,36 @@ def findCaller(self, stack_info=False, stacklevel=1):
11301131
# Use sys._getframe(2) instead of logging.currentframe(), it's slightly
11311132
# faster because there is one less frame to traverse.
11321133
frame = sys._getframe(2) # pylint: disable=protected-access
1134+
frame_to_return = None
11331135

11341136
while frame:
11351137
code = frame.f_code
11361138
if (_LOGGING_FILE_PREFIX not in code.co_filename and
11371139
(code.co_filename, code.co_name,
11381140
code.co_firstlineno) not in f_to_skip and
11391141
(code.co_filename, code.co_name) not in f_to_skip):
1140-
sinfo = None
1141-
if stack_info:
1142-
out = io.StringIO()
1143-
out.write('Stack (most recent call last):\n')
1144-
traceback.print_stack(frame, file=out)
1145-
sinfo = out.getvalue().rstrip('\n')
1146-
return (code.co_filename, frame.f_lineno, code.co_name, sinfo)
1142+
frame_to_return = frame
1143+
stacklevel -= 1
1144+
if stacklevel <= 0:
1145+
break
11471146
frame = frame.f_back
11481147

1148+
if frame_to_return is not None:
1149+
sinfo = None
1150+
if stack_info:
1151+
out = io.StringIO()
1152+
out.write('Stack (most recent call last):\n')
1153+
traceback.print_stack(frame, file=out)
1154+
sinfo = out.getvalue().rstrip('\n')
1155+
return (
1156+
frame_to_return.f_code.co_filename,
1157+
frame_to_return.f_lineno,
1158+
frame_to_return.f_code.co_name,
1159+
sinfo,
1160+
)
1161+
1162+
return None
1163+
11491164
def critical(self, msg, *args, **kwargs):
11501165
"""Logs ``msg % args`` with severity ``CRITICAL``."""
11511166
self.log(logging.CRITICAL, msg, *args, **kwargs)

absl/logging/tests/logging_test.py

+70
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,76 @@ def test_logger_cannot_be_disabled(self):
593593
self.logger.handle(record)
594594
mock_call_handlers.assert_called_once()
595595

596+
def test_find_caller_respects_stacklevel(self):
597+
self.set_up_mock_frames()
598+
self.logger.register_frame_to_skip('myfile.py', 'Method1')
599+
# Frame 0 is filtered out due to being in the logging file.
600+
# Frame 1 is filtered out due to being registered to be skipped.
601+
# Frame 2 is the topmost unfiltered frame on the stack.
602+
self.assertEqual(
603+
self.logger.findCaller(stacklevel=1),
604+
('myfile.py', 125, 'Method2', None),
605+
)
606+
self.assertEqual(
607+
self.logger.findCaller(stacklevel=2),
608+
('myfile.py', 125, 'Method3', None),
609+
)
610+
self.assertEqual(
611+
self.logger.findCaller(stacklevel=3),
612+
('myfile.py', 249, 'Method2', None),
613+
)
614+
615+
def test_find_caller_handles_skipped_bottom_of_stack(self):
616+
self.set_up_mock_frames()
617+
self.logger.register_frame_to_skip('myfile.py', 'Method2')
618+
# Frame 0 is filtered out due to being in the logging file.
619+
# Frame 1 is unfiltered.
620+
# Frame 2 is filtered due to regiser_frame_to_skip.
621+
# Frame 3 is unfiltered.
622+
# Frame 4 is filtered due to regiser_frame_to_skip.
623+
self.assertEqual(
624+
self.logger.findCaller(stacklevel=1),
625+
('myfile.py', 125, 'Method1', None),
626+
)
627+
self.assertEqual(
628+
self.logger.findCaller(stacklevel=2),
629+
('myfile.py', 125, 'Method3', None),
630+
)
631+
# 3 exceeds the unfiltered stack depth, so it returns the bottom unfiltered frame.
632+
self.assertEqual(
633+
self.logger.findCaller(stacklevel=3),
634+
('myfile.py', 125, 'Method3', None),
635+
)
636+
637+
def test_log_respects_stacklevel(self):
638+
self.set_up_mock_frames()
639+
original_logger_class = std_logging.getLoggerClass()
640+
std_logging.setLoggerClass(logging.ABSLLogger)
641+
absl_logger = std_logging.getLogger('absl')
642+
with self.assertLogs() as cm:
643+
absl_logger.info('lol', stacklevel=2)
644+
std_logging.setLoggerClass(original_logger_class)
645+
self.assertLen(cm.records, 1)
646+
self.assertEqual(cm.records[0].lineno, 125)
647+
self.assertEqual(cm.records[0].funcName, 'Method2')
648+
self.assertEqual(cm.records[0].filename, 'myfile.py')
649+
650+
def test_handles_negative_stacklevel(self):
651+
"""If stacklevel is negative, return the top unfiltered frame."""
652+
self.set_up_mock_frames()
653+
self.assertEqual(
654+
self.logger.findCaller(stacklevel=-1),
655+
('myfile.py', 125, 'Method1', None),
656+
)
657+
658+
def test_handles_excess_stacklevel(self):
659+
"""If stacklevel exceeds the stack depth, return the bottom frame."""
660+
self.set_up_mock_frames()
661+
self.assertEqual(
662+
self.logger.findCaller(stacklevel=1000),
663+
('myfile.py', 249, 'Method2', None),
664+
)
665+
596666

597667
class ABSLLogPrefixTest(parameterized.TestCase):
598668

0 commit comments

Comments
 (0)