Skip to content

Commit

Permalink
FIX: Add get_caller_fqn function
Browse files Browse the repository at this point in the history
  • Loading branch information
aikikode committed Nov 11, 2019
1 parent 406582a commit 867017c
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 6 deletions.
59 changes: 59 additions & 0 deletions fqn_decorators/decorators.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import functools
import inspect
import sys


Expand Down Expand Up @@ -31,6 +32,64 @@ def get_fqn(obj):
return '.'.join(filter(None, path))


def get_caller_fqn():
"""
Use this method inside the function/method to get the fully qualified name (FQN) of its caller.
Works for methods and functions including inner ones.
E.g. consider module 'src.check_fqn':
def parent():
internal()
def internal():
get_caller_fqn()
parent() # Returns FQN of the method that called 'internal()': 'src.check_fqn.parent'
For complex nested inner functions FQN will represent a stack trace of calls (only for Python3):
def inside():
def child_method():
return get_caller_fqn()
def outer_method():
return child_method()
class Caller:
def parent_with_inner(self):
def inner_parent():
return outer_method()
return inner_parent()
return Caller().parent_with_inner()
inside() # Returns 'src.check_fqn.inside.Caller.parent_with_inner.inner_parent.outer_method'
"""
stack_depth = len(inspect.stack())
depth = 2 # 0: current method, 1: method that uses this function, 2: actual caller
function_name = None
while depth < stack_depth:
caller_frame = inspect.stack()[depth]
try:
frame, caller_function = caller_frame.frame, caller_frame.function
except AttributeError: # old Python versions
frame, caller_function = caller_frame[0], caller_frame[3]
# If caller is a function
try:
return get_fqn(frame.f_globals[caller_function])
except KeyError:
pass
# If caller is a class method
function_name = '.'.join(filter(None, [caller_function, function_name])) # Handle nested inner calls
try:
return '{}.{}'.format(get_fqn(frame.f_locals['self'].__class__), function_name)
except KeyError:
pass
# We are called from internal method we can't obtain reference to, e.g. list comprehension or nested inner
# functions -> try to go one level up the stack
depth += 1
return ''


class Decorator(object):
"""
A base class to easily create decorators.
Expand Down
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
import sys


collect_ignore = ["setup.py"]
if sys.version_info[0] < 3:
collect_ignore.append("test_fqn_decorators_asynchronous.py")


def pytest_configure():
pass
66 changes: 66 additions & 0 deletions tests/test_fqn_decorators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import functools
import sys

import fqn_decorators
import mock
Expand Down Expand Up @@ -43,6 +44,71 @@ def test_decorated_class(self):
assert fqn_decorators.get_fqn(examples.A) == 'tests.examples.A'


class TestGetCallerFqn:
def test_function(self):
def inline_method():
return fqn_decorators.get_caller_fqn()

assert inline_method() == 'tests.test_fqn_decorators.TestGetCallerFqn.test_function'

def test_method(self):
class Inline:
def inline(self):
return fqn_decorators.get_caller_fqn()

assert Inline().inline() == 'tests.test_fqn_decorators.TestGetCallerFqn.test_method'

@pytest.mark.skipif(sys.version_info < (3, 0), reason='requires python3')
def test_caller_is_method(self):
def inline_method():
return fqn_decorators.get_caller_fqn()

class Caller:
def parent(self):
return inline_method()

assert Caller().parent() == (
'tests.test_fqn_decorators.TestGetCallerFqn.test_caller_is_method.Caller.parent'
)

def test_caller_is_inner(self):
def child_method():
return fqn_decorators.get_caller_fqn()

def parent_method():
return child_method()

assert parent_method() == (
'tests.test_fqn_decorators.TestGetCallerFqn.test_caller_is_inner.parent_method'
)

@pytest.mark.skipif(sys.version_info < (3, 0), reason='requires python3')
def test_caller_is_complex(self):
def child_method():
return fqn_decorators.get_caller_fqn()

def outer_method():
return child_method()

class Caller:
def parent(self):
return outer_method()

def parent_with_inner(self):
def inner_parent():
return outer_method()
return inner_parent()

assert Caller().parent() == (
'tests.test_fqn_decorators.TestGetCallerFqn.test_caller_is_complex.Caller.parent.outer_method'
)

assert Caller().parent_with_inner() == (
'tests.test_fqn_decorators.TestGetCallerFqn.test_caller_is_complex.'
'Caller.parent_with_inner.inner_parent.outer_method'
)


class TestDecorator(object):

def test_getattr(self):
Expand Down
6 changes: 0 additions & 6 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@ deps =
-rrequirements/requirements-base.txt
-rrequirements/requirements-testing.txt

[testenv:py27]
setenv =
PYTEST_ADDOPTS = --ignore tests/test_fqn_decorators_asynchronous.py
deps =
{[testenv]deps}

[testenv:lint]
basepython = python3.5
commands = flake8 decorators tests --exclude=fqn_decorators/__init__.py
Expand Down

0 comments on commit 867017c

Please sign in to comment.