From 867017c1cc4bcdcaab552fb9f912b8f1c2df2660 Mon Sep 17 00:00:00 2001 From: Denis Kovalev Date: Fri, 8 Nov 2019 11:18:54 +0100 Subject: [PATCH] FIX: Add get_caller_fqn function --- fqn_decorators/decorators.py | 59 ++++++++++++++++++++++++++++++++ tests/conftest.py | 8 +++++ tests/test_fqn_decorators.py | 66 ++++++++++++++++++++++++++++++++++++ tox.ini | 6 ---- 4 files changed, 133 insertions(+), 6 deletions(-) diff --git a/fqn_decorators/decorators.py b/fqn_decorators/decorators.py index ff64c7b..2910d00 100755 --- a/fqn_decorators/decorators.py +++ b/fqn_decorators/decorators.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import functools +import inspect import sys @@ -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. diff --git a/tests/conftest.py b/tests/conftest.py index b910715..8c2a8fe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 diff --git a/tests/test_fqn_decorators.py b/tests/test_fqn_decorators.py index 0141982..8cb583d 100644 --- a/tests/test_fqn_decorators.py +++ b/tests/test_fqn_decorators.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- import functools +import sys import fqn_decorators import mock @@ -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): diff --git a/tox.ini b/tox.ini index ad65e7f..f1b0187 100644 --- a/tox.ini +++ b/tox.ini @@ -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