Search code examples
python-3.xrobotframeworkpython-decorators

How to get wrapped func.__code__.co_filename with decorator?


A simple folder structure shown below, and all the functions in test.py have keyword decorator on it.

 lib
  |--- keyword.py
 main
  |--- test.py

Keyword.py

from functools import wraps
def keyword(name=None, tags=(), types=()):

    def _method_wrapper(func):

        @wraps(func)
        def _passargs(self, *args, **kwargs):  
            print(func.__code__.co_filename)  # --> '..\main\test.py'
            return func(self, *args, **kwargs)

        print(_passargs.__code__.co_filename)  # --> '..\lib\keyword.py'
                
        return _passargs

    return _method_wrapper 

notes: print(..) is just an example, I need both _passargs & func to have the same code object, instead of printing another variable :)

As you can see, the _passargs got the wrong co_filename.

This is from robotframework keyword.py, and I modified it for my own purpose. However I failed to figure out how to make both _passargs & func have the correct source file location so the robot.libdoc can generate doc.libspec correctly.

Can anyone help?

The expectation

func.__code__.co_filename = '..\main\test.py'
_passargs.__code__.co_filename = '..\main\test.py'

Python version = 3.8.10


Solution

  • Answering my own question here to help ppl who may struggle with the same issue.

    The original decorator func:

    from functools import wraps
    def keyword(name=None, tags=(), types=()):
    
        def _method_wrapper(func):
    
            @wraps(func)
            def _passargs(self, *args, **kwargs):  
                print(func.__code__.co_filename)  # --> '..\main\test.py'
                return func(self, *args, **kwargs)
    
            print(_passargs.__code__.co_filename)  # --> '..\lib\keyword.py'
                    
            return _passargs
    
        return _method_wrapper 
    

    Most of the time, we only needs the last level of decorator (return func(self, *args, **kwargs) in this example) to return the proper attributes of the wrapped func.

    However, in my case, there's a function which will scan all the functions with this keyword decorator during initializing class. And it won't go into def _passargs level due to we didn't provide any arguments but just trying to get the wrapped func (return _passargs) related attributes.

    To resolve this issue, we can only override the co_filename at RUNTIME.

    Here's the solution to override __code__ object in decorator.

    from types import CodeType
    from functools import wraps
    
    
    def keyword(name=None, tags=(), types=()):
    
        def _method_wrapper(func):
    
            @wraps(func)
            def _passargs(self, *args, **kwargs):  
                print(func.__code__.co_filename)  # --> '..\main\test.py'
                return func(self, *args, **kwargs)
    
            fix_co_filename(_passargs, func.__code__.co_filename)
            print(_passargs.__code__.co_filename)  # --> '..\main\test.py'
                    
            return _passargs
    
        return _method_wrapper 
    
    
    def fix_co_filename(func, co_filename):
        fn_code = func.__code__
        func.__code__ = CodeType(
            fn_code.co_argcount,
            fn_code.co_posonlyargcount,
            fn_code.co_kwonlyargcount,
            fn_code.co_nlocals,
            fn_code.co_stacksize,
            fn_code.co_flags,
            fn_code.co_code,
            fn_code.co_consts,
            fn_code.co_names,
            fn_code.co_varnames,
            co_filename,
            fn_code.co_name,
            fn_code.co_firstlineno,
            fn_code.co_lnotab,
            fn_code.co_freevars,
            fn_code.co_cellvars)