#!/usr/bin/env python
# cardinal_pythonlib/debugging.py
"""
===============================================================================
Original code copyright (C) 2009-2022 Rudolf Cardinal (rudolf@pobox.com).
This file is part of cardinal_pythonlib.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
===============================================================================
**Functions for debugging.**
"""
import ctypes
import inspect
import logging
import pdb
import sys
import traceback
from types import FrameType
from typing import Any, Callable, List, Optional, TYPE_CHECKING
from cardinal_pythonlib.logs import get_brace_style_log_with_null_handler
if TYPE_CHECKING:
# noinspection PyUnresolvedReferences
from inspect import FrameInfo
log = get_brace_style_log_with_null_handler(__name__)
# =============================================================================
# Debugging
# =============================================================================
[docs]def pdb_run(func: Callable, *args: Any, **kwargs: Any) -> Any:
"""
Calls ``func(*args, **kwargs)``; if it raises an exception, break into
the ``pdb`` debugger.
"""
# noinspection PyBroadException
try:
return func(*args, **kwargs)
except Exception:
type_, value, tb = sys.exc_info()
traceback.print_exc()
pdb.post_mortem(tb)
[docs]def cause_segfault() -> None:
"""
This function will induce a segmentation fault and CRASH the application.
Method as per https://docs.python.org/3/library/faulthandler.html
"""
ctypes.string_at(0) # will crash!
# =============================================================================
# Name of calling class/function, for status messages
# =============================================================================
[docs]def get_class_name_from_frame(fr: FrameType) -> Optional[str]:
"""
A frame contains information about a specific call in the Python call
stack; see https://docs.python.org/3/library/inspect.html.
If the call was to a member function of a class, this function attempts
to read the class's name. It returns ``None`` otherwise.
"""
# https://stackoverflow.com/questions/2203424/python-how-to-retrieve-class-information-from-a-frame-object # noqa: E501
args, _, _, value_dict = inspect.getargvalues(fr)
# we check the first parameter for the frame function is named 'self'
if len(args) and args[0] == "self":
# in that case, 'self' will be referenced in value_dict
instance = value_dict.get("self", None)
if instance:
# return its class
cls = getattr(instance, "__class__", None)
if cls:
return cls.__name__
return None
# return None otherwise
return None
[docs]def get_caller_name(back: int = 0) -> str:
"""
Return details about the CALLER OF THE CALLER (plus n calls further back)
of this function.
So, if your function calls :func:`get_caller_name`, it will return the
name of the function that called your function! (Or ``back`` calls further
back.)
Example:
.. code-block:: python
from cardinal_pythonlib.debugging import get_caller_name
def who_am_i():
return get_caller_name()
class MyClass(object):
def classfunc(self):
print("I am: " + who_am_i())
print("I was called by: " + get_caller_name())
print("That was called by: " + get_caller_name(back=1))
def f2():
x = MyClass()
x.classfunc()
def f1():
f2()
f1()
will produce:
.. code-block:: none
I am: MyClass.classfunc
I was called by: f2
That was called by: f1
"""
# https://stackoverflow.com/questions/5067604/determine-function-name-from-within-that-function-without-using-traceback # noqa: E501
try:
# noinspection PyProtectedMember
frame = sys._getframe(back + 2)
except ValueError:
# Stack isn't deep enough.
return "?"
function_name = frame.f_code.co_name
class_name = get_class_name_from_frame(frame)
if class_name:
return f"{class_name}.{function_name}"
return function_name
# =============================================================================
# Who called us?
# =============================================================================
[docs]def get_caller_stack_info(start_back: int = 1) -> List[str]:
r"""
Retrieves a textual representation of the call stack.
Args:
start_back: number of calls back in the frame stack (starting
from the frame stack as seen by :func:`get_caller_stack_info`)
to begin with
Returns:
list of descriptions
Example:
.. code-block:: python
from cardinal_pythonlib.debugging import get_caller_stack_info
def who_am_i():
return get_caller_name()
class MyClass(object):
def classfunc(self):
print("Stack info:\n" + "\n".join(get_caller_stack_info()))
def f2():
x = MyClass()
x.classfunc()
def f1():
f2()
f1()
if called from the Python prompt will produce:
.. code-block:: none
Stack info:
<module>()
... defined at <stdin>:1
... line 1 calls next in stack; code is:
f1()
... defined at <stdin>:1
... line 2 calls next in stack; code is:
f2()
... defined at <stdin>:1
... line 3 calls next in stack; code is:
classfunc(self=<__main__.MyClass object at 0x7f86a009c6d8>)
... defined at <stdin>:2
... line 3 calls next in stack; code is:
and if called from a Python file will produce:
.. code-block:: none
Stack info:
<module>()
... defined at /home/rudolf/tmp/stack.py:1
... line 17 calls next in stack; code is:
f1()
f1()
... defined at /home/rudolf/tmp/stack.py:14
... line 15 calls next in stack; code is:
f2()
f2()
... defined at /home/rudolf/tmp/stack.py:10
... line 12 calls next in stack; code is:
x.classfunc()
classfunc(self=<__main__.MyClass object at 0x7fd7a731f358>)
... defined at /home/rudolf/tmp/stack.py:7
... line 8 calls next in stack; code is:
print("Stack info:\n" + "\n".join(get_caller_stack_info()))
"""
# "0 back" is debug_callers, so "1 back" its caller
# https://docs.python.org/3/library/inspect.html
callers = [] # type: List[str]
frameinfolist = inspect.stack() # type: List[FrameInfo]
frameinfolist = frameinfolist[start_back:]
for frameinfo in frameinfolist:
frame = frameinfo.frame
function_defined_at = "... defined at {filename}:{line}".format(
filename=frame.f_code.co_filename, line=frame.f_code.co_firstlineno
)
argvalues = inspect.getargvalues(frame)
formatted_argvalues = inspect.formatargvalues(*argvalues)
function_call = "{funcname}{argvals}".format(
funcname=frame.f_code.co_name, argvals=formatted_argvalues
)
code_context = frameinfo.code_context
code = "".join(code_context) if code_context else ""
onwards = "... line {line} calls next in stack; code is:\n{c}".format(
line=frame.f_lineno, c=code
)
description = "\n".join([function_call, function_defined_at, onwards])
callers.append(description)
return list(reversed(callers))
# =============================================================================
# Show the structure of an object in detail
# =============================================================================
[docs]def debug_object(obj, log_level: int = logging.DEBUG) -> None:
"""
Sends details about a Python to the log, specifically its ``repr()``
representation, and all of its attributes with their name, value, and type.
Args:
obj: object to debug
log_level: log level to use; default is ``logging.DEBUG``
"""
msgs = [f"For {obj!r}:"]
for attrname in dir(obj):
attribute = getattr(obj, attrname)
msgs.append(
f"- {attrname!r}: {attribute!r}, of type {type(attribute)!r}"
)
log.log(log_level, "{}", "\n".join(msgs))