Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIX: Add get_caller_fqn() function #14

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
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