[Python] Wrapping function calls?

Say I have a function with some positional and keyword arguments.
I wanna pass the function call to an object that will handle some pre- and post- operations (logging mostly).

How would I go about passing in the function and args to an object that assembles those inputs into a command that is executed? In MEL you can use EVAL and python has something similar, although most of the online advice is to avoid EVAL in python…

For example:

def my_func(stuff, my_arg=None):
    # stuff

class my_handler(object):
    def __init__(self, functionName, *args, **kwargs):
        # pre-execution log event
        try:
            # assemble and execute inputs... not sure best way to do this?
        except Exception as e:
            # log exception
        finally:
            # post-execution log event

Decorators! Decorators are awesome.

You can create them as classes or function closures, depending on the required features and level of complexity, but the basic concept is the same.

def my_handler(func):
    def wrapper(*args, **kwargs):
        print 'pre function call'
        results = func(*args, **kwargs)
        print 'post function call'
        return results
    return wrapper

@my_handler
def my_func(arg, kwarg='yes'):
    print 'in my func'
    print arg, kwarg

The @my_handler tells python that the following function should be decorated by my_handler. It’s just syntactic sugar for:

def my_func(arg, kwarg='yes'):
    print 'in my func'
    print arg, kwarg

my_func = my_handler(my_func)

my_handler is passed the function object that it is decorating as an argument, and my_handler then returns a function (wrapper, in this case) to be run in it’s place.

Class-based decorators return an instance of the decorator class and thus use the call method instead of returning a decorated function. The same decorator as a class:

class my_handler(object):
    def __init__(self, func):
        self.func = func
        
    def __call__(self, *args, **kwargs):
        print 'pre function call'
        results = self.func(*args, **kwargs)
        print 'post function call'
        return results

There are a lot of resources on decorators, but I found this (2-part article) be a good explanation of them: Decorators I: Introduction to Python Decorators
Part II, which goes over creating decorators that accept arguments: Python Decorators II: Decorator Arguments

Also Since a different function is being returned in place of the original, the function will lose all of it’s original metadata, so if you want to retain things like name and doc you have a add them in the decorator:

class my_handler(object):
    def __init__(self, func):
        self.func = func
        self.__doc__ = func.__doc__
        self.__name__ = func.__name__
        self.__module__ = func.__module__
        
    def __call__(self, *args, **kwargs):
        # ...
1 Like

agreed, though try to keep those simple and really easy to read. Debugging decorators is kind of a pain.

Thanks for the awesome info!
A question, what if my function is stored in a separate module from the decorator?

I already have a large package structure containing dozens of files, I was hoping to create a simple wrapper that could be passed any function… can i make one decorator works for an abritrary number of functions?

Yes. Decorators generally won’t be function specific, and will work just the same when imported as a module.

As an example we have a module of maya decorators for simple things like restoring original selection, or setting the viewport to wireframe, or hiding viewports, etc. Those all get imported as lib.maya.decorators and applied to functions in the primary module. I use them all the time when writing maya code that needs a setting/environment to be changed prior to a function call, and restored afterwards.

If you want to decorate an imported function, just pass the imported function to the decorator and either call the result or store it to call later.

import lib.maya.decorators

@lib.maya.decorators.restore_selection
def selection_stealer():
    pass
    
import function_to_decorate

decorated_function = lib.maya.decorators.restore_selection(function_to_decorate)

thanks again for the help, but since i am quite dense, i am still having trouble grasping the concept. :?:

I would like to avoid decorating individual modules and functions, and only decorate the functions in a specific context.

your example looks close, but i’m confused why you have defined selection_stealer?

Edit:
Since I don’t want the decorated code to run everytime that function is called, just when it is called from a certain context, couldn’t I use forego the “@” syntax and just wrap the function?

i.e.

import repairMeshes as rm
import maya.cmds as cmds

def my_decorator(func):
    def logging_wrapper(*args, **kwargs):
        print 'pre function call'
        results = func(*args, **kwargs)
        print 'post function call'
        return results
    return logging_wrapper

repairMeshesLogged = my_decorator(rm.repairMeshes)
repairMeshesLogged(cmds.ls(sl=True))

Basically I want to log tools whever they are called from a Maya Menu or Shelf, but not when someone is running the commands directly from the script editor.

Sorry I should have been more clear. That code snippet shows two examples: using an imported function to decorate selection_stealer and decorating an imported function.

And yes your methodology is fine, and is the correct (and only, afaik) way of decorating a function in that context. The “@” is just for function definitions and is syntactic sugar; using it doesn’t change the behaviors.

thanks for the help, everything is much clearer now! :slight_smile: