Skip to content

Commit 867017c

Browse files
committed
FIX: Add get_caller_fqn function
1 parent 406582a commit 867017c

4 files changed

Lines changed: 133 additions & 6 deletions

File tree

fqn_decorators/decorators.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
import functools
3+
import inspect
34
import sys
45

56

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

3334

35+
def get_caller_fqn():
36+
"""
37+
Use this method inside the function/method to get the fully qualified name (FQN) of its caller.
38+
Works for methods and functions including inner ones.
39+
40+
E.g. consider module 'src.check_fqn':
41+
def parent():
42+
internal()
43+
44+
def internal():
45+
get_caller_fqn()
46+
47+
parent() # Returns FQN of the method that called 'internal()': 'src.check_fqn.parent'
48+
49+
For complex nested inner functions FQN will represent a stack trace of calls (only for Python3):
50+
def inside():
51+
def child_method():
52+
return get_caller_fqn()
53+
54+
def outer_method():
55+
return child_method()
56+
57+
class Caller:
58+
def parent_with_inner(self):
59+
def inner_parent():
60+
return outer_method()
61+
return inner_parent()
62+
63+
return Caller().parent_with_inner()
64+
65+
inside() # Returns 'src.check_fqn.inside.Caller.parent_with_inner.inner_parent.outer_method'
66+
"""
67+
stack_depth = len(inspect.stack())
68+
depth = 2 # 0: current method, 1: method that uses this function, 2: actual caller
69+
function_name = None
70+
while depth < stack_depth:
71+
caller_frame = inspect.stack()[depth]
72+
try:
73+
frame, caller_function = caller_frame.frame, caller_frame.function
74+
except AttributeError: # old Python versions
75+
frame, caller_function = caller_frame[0], caller_frame[3]
76+
# If caller is a function
77+
try:
78+
return get_fqn(frame.f_globals[caller_function])
79+
except KeyError:
80+
pass
81+
# If caller is a class method
82+
function_name = '.'.join(filter(None, [caller_function, function_name])) # Handle nested inner calls
83+
try:
84+
return '{}.{}'.format(get_fqn(frame.f_locals['self'].__class__), function_name)
85+
except KeyError:
86+
pass
87+
# We are called from internal method we can't obtain reference to, e.g. list comprehension or nested inner
88+
# functions -> try to go one level up the stack
89+
depth += 1
90+
return ''
91+
92+
3493
class Decorator(object):
3594
"""
3695
A base class to easily create decorators.

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
1+
import sys
2+
3+
4+
collect_ignore = ["setup.py"]
5+
if sys.version_info[0] < 3:
6+
collect_ignore.append("test_fqn_decorators_asynchronous.py")
7+
8+
19
def pytest_configure():
210
pass

tests/test_fqn_decorators.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
33
import functools
4+
import sys
45

56
import fqn_decorators
67
import mock
@@ -43,6 +44,71 @@ def test_decorated_class(self):
4344
assert fqn_decorators.get_fqn(examples.A) == 'tests.examples.A'
4445

4546

47+
class TestGetCallerFqn:
48+
def test_function(self):
49+
def inline_method():
50+
return fqn_decorators.get_caller_fqn()
51+
52+
assert inline_method() == 'tests.test_fqn_decorators.TestGetCallerFqn.test_function'
53+
54+
def test_method(self):
55+
class Inline:
56+
def inline(self):
57+
return fqn_decorators.get_caller_fqn()
58+
59+
assert Inline().inline() == 'tests.test_fqn_decorators.TestGetCallerFqn.test_method'
60+
61+
@pytest.mark.skipif(sys.version_info < (3, 0), reason='requires python3')
62+
def test_caller_is_method(self):
63+
def inline_method():
64+
return fqn_decorators.get_caller_fqn()
65+
66+
class Caller:
67+
def parent(self):
68+
return inline_method()
69+
70+
assert Caller().parent() == (
71+
'tests.test_fqn_decorators.TestGetCallerFqn.test_caller_is_method.Caller.parent'
72+
)
73+
74+
def test_caller_is_inner(self):
75+
def child_method():
76+
return fqn_decorators.get_caller_fqn()
77+
78+
def parent_method():
79+
return child_method()
80+
81+
assert parent_method() == (
82+
'tests.test_fqn_decorators.TestGetCallerFqn.test_caller_is_inner.parent_method'
83+
)
84+
85+
@pytest.mark.skipif(sys.version_info < (3, 0), reason='requires python3')
86+
def test_caller_is_complex(self):
87+
def child_method():
88+
return fqn_decorators.get_caller_fqn()
89+
90+
def outer_method():
91+
return child_method()
92+
93+
class Caller:
94+
def parent(self):
95+
return outer_method()
96+
97+
def parent_with_inner(self):
98+
def inner_parent():
99+
return outer_method()
100+
return inner_parent()
101+
102+
assert Caller().parent() == (
103+
'tests.test_fqn_decorators.TestGetCallerFqn.test_caller_is_complex.Caller.parent.outer_method'
104+
)
105+
106+
assert Caller().parent_with_inner() == (
107+
'tests.test_fqn_decorators.TestGetCallerFqn.test_caller_is_complex.'
108+
'Caller.parent_with_inner.inner_parent.outer_method'
109+
)
110+
111+
46112
class TestDecorator(object):
47113

48114
def test_getattr(self):

tox.ini

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,6 @@ deps =
1313
-rrequirements/requirements-base.txt
1414
-rrequirements/requirements-testing.txt
1515

16-
[testenv:py27]
17-
setenv =
18-
PYTEST_ADDOPTS = --ignore tests/test_fqn_decorators_asynchronous.py
19-
deps =
20-
{[testenv]deps}
21-
2216
[testenv:lint]
2317
basepython = python3.5
2418
commands = flake8 decorators tests --exclude=fqn_decorators/__init__.py

0 commit comments

Comments
 (0)