"""
This module contains *somewhat risky* code that uses app introspection e.g. via :mod:`inspect`.
Most functions / classes in this module will **ONLY work on CPython** (the official Python interpreter from python.org),
and their functionality is not guaranteed to be stable as they interact with the interpreter to enable special functionality
such as detecting the function/class/module which called your function/method.
Functions and methods in this module may be updated with breaking API changes at any time, especially if they're needed
to adjust for a change in Python itself. Please ensure that any usage of this module is properly wrapped in a try/catch block,
and avoid relying on functions/methods in this module for critical functionality of your application.
Most useful functions:
* :func:`.calling_function` - Returns the name of the function/method which called your function/method
* :func:`.calling_module` - Returns the name of the module which called your function/method
* :func:`.caller_name` - Returns the fully qualified module path to the function/method/module which called
your function/method
**Copyright**::
+===================================================+
| © 2019 Privex Inc. |
| https://www.privex.io |
+===================================================+
| |
| Originally Developed by Privex Inc. |
| License: X11 / MIT |
| |
| Core Developer(s): |
| |
| (+) Chris (@someguy123) [Privex] |
| (+) Kale (@kryogenic) [Privex] |
| |
+===================================================+
Copyright 2019 Privex Inc. ( https://www.privex.io )
"""
import inspect
from typing import Optional
[docs]def calling_function(skip=2) -> Optional[str]:
"""
Returns the name of the function which called your function/method.
Example::
>>> def x(skip=2): return calling_function(skip=2)
>>>
>>> def y(skip=2): return x(skip)
>>>
>>> def z(skip=2): return y(skip)
>>>
>>> print(y()) # The call to x() returns that 'y' is the function which called it.
y
>>> print(z()) # The call to z() calls y() -> x() - still returning that 'y' is the caller of x()
y
>>> # If we adjust skip to 3 instead of 2, we can see that z() is the function that called y() which called x()
>>> print(z(3))
z
:param int skip: Skip this many frames.
0 = calling_function()
1 = function which called calling_function()
2 = function which called the function that called calling_function() (default)
and so on...
:return str|None function_name: Either a string containing the function name, or ``None`` if you've skipped too many frames.
"""
curframe = inspect.currentframe()
calframe = inspect.getouterframes(curframe, 2)
if len(calframe) <= skip:
return None
return calframe[skip][3]
[docs]def last_frames():
curframe = inspect.currentframe()
calframe = inspect.getouterframes(curframe, 2)
return curframe, calframe
[docs]def last_stack_frame(frame_num=2):
return inspect.stack()[frame_num]
[docs]def calling_module(skip=2) -> Optional[str]:
"""
Returns the name of the module which called your function/method.
:param int skip: Skip this many frames.
0 = module containing calling_function()
1 = module which called calling_function()
2 = module which called the function that called calling_function() (default)
and so on...
:return str|None mod_name: Either a string containing the module name, or ``None`` if you've skipped too many frames.
If called from the main python script, then ``'__main__'`` will be returned instead of a
proper module path.
"""
stk = inspect.stack()
if len(stk) <= skip:
return None
mod = inspect.getmodule(stk[skip][0])
if mod is None:
return '__main__'
return mod.__name__
[docs]def caller_name(skip=2) -> Optional[str]:
"""
Get the fully qualified name of a caller in the format ``some.module.SomeClass.method``
.. Attention:: While class instance methods will be returned correctly, class static methods will not show up as expected.
The static method ``some.module.SomeClass.some_static`` would be returned as ``some.module.some_static``,
as if it were a top-level function in the module.
Original source: https://stackoverflow.com/a/9812105
**Basic Example**
When used within the main program (the script you run ``python3 xxx.py`` on), the module will be reported as ``__main__``.
File ``hello.py``::
>>> from privex.helpers.black_magic import caller_name
>>>
>>> def f2():
... return caller_name()
>>>
>>> def f1():
... return f2()
...
>>> print(f"[{__name__}] f1 result: {f1()}")
[__main__] f1 result: __main__.f1
However, as we can see below, when we create and run ``world.py`` which imports ``hello.py``, it correctly returns the
path ``hello.f1``.
File ``world.py``::
>>> from hello import f1
>>>
>>> print(f"[{__name__}] f1 result: {f1()}")
[hello] f1 result: hello.f1
[__main__] f1 result: hello.f1
**More Complex Example**
File ``some/module/hello.py``::
>>> from privex.helpers.black_magic import caller_name
>>>
>>> class SomeClass:
>>> def example_method(self, skip=2):
... return caller_name(skip)
...
File ``some/module/world.py``::
>>> from some.module.hello import SomeClass
>>>
>>> class OtherClass:
... def call_some(self, skip=2):
... return SomeClass().example_method(skip)
...
File ``test.py``::
>>> from some.module.hello import SomeClass
>>> from some.module.world import OtherClass
>>>
>>> def main_func():
... print('SomeClass (2)', SomeClass().example_method())
... print('OtherClass (1)', OtherClass().call_some(1))
... print('OtherClass (2)', OtherClass().call_some())
... print('OtherClass (3)', OtherClass().call_some(3))
...
>>> main_func()
SomeClass (2) test.main_func
OtherClass (1) some.module.hello.SomeClass.example_method
OtherClass (2) some.module.world.OtherClass.call_some
OtherClass (3) test.main_func
:param int skip: Specifies how many levels of stack to skip while getting caller name.
``skip=1`` means "who called caller_name",
``skip=2`` means "who called this function/method" etc.
:return str caller: A fully qualified module path, e.g. ``some.module.SomeClass.some_method``
``None`` is returned if skipped levels exceed stack height.
"""
stack = inspect.stack()
start = 0 + skip
if len(stack) < start + 1:
return None
parentframe = stack[start][0]
name = []
module = inspect.getmodule(parentframe)
# `modname` can be None when frame is executed directly in console
# TODO(techtonik): consider using __main__
if module:
name.append(module.__name__)
# detect classname
if 'self' in parentframe.f_locals:
# I don't know any way to detect call from the object method
# XXX: there seems to be no way to detect static method call - it will
# be just a function call
name.append(parentframe.f_locals['self'].__class__.__name__)
codename = parentframe.f_code.co_name
if codename != '<module>': # top level usually
name.append(codename) # function or a method
# Avoid circular refs and frame leaks
# https://docs.python.org/2.7/library/inspect.html#the-interpreter-stack
del parentframe, stack
_caller = ".".join(name)
if _caller in ["", None]:
return None
return _caller