9

I am trying to dynamically create module level functions from the methods in a class. So for every method in a class, I want to create a function with the same name which creates an instance of the class and then calls the method.

The reason I want to do this is so I can take an object-oriented approach to creating Fabric files. Since Fabric will call module level functions but not methods of a class, this is my work-around.

I have used the following links to get me started

And I have come up with the following code

import inspect
import sys
import types

class TestClass(object):
    def __init__(self):
        pass

    def method1(self, arg1):
        print 'method 1 %s' % arg1

    def method2(self):
        print 'method 2'

def fabric_class_to_function_magic(module_name):
    # get the module as an object
    print module_name
    module_obj = sys.modules[module_name]
    print dir(module_obj)

    # Iterate over the methods of the class and dynamically create a function
    # for each method that calls the method and add it to the current module
    for method in inspect.getmembers(TestClass, predicate=inspect.ismethod):
        print
        print method
        method_name, method_obj = method

        # create a new template function which calls the method
        def newfunc_template(*args, **kwargs):
            tc = TestClass()
            func = getattr(tc, method_name)
            return func(*args, **kwargs)

        # create the actual function
        print 'code: ', newfunc_template.func_code
        print 'method_name: ', method_name
        newfunc = types.FunctionType(newfunc_template.func_code,
                                     {'TestClass': TestClass,
                                      'getattr': getattr,
                                      'method_name': method_name,
                                      },
                                     name=method_name,
                                     argdefs=newfunc_template.func_defaults,
                                     closure=newfunc_template.func_closure,
                                     )

        # add the new function to the current module
        setattr(module_obj, method_name, newfunc)

# test the dynamically created module level function
thismodule = sys.modules[__name__]
print dir(thismodule)
fabric_class_to_function_magic(__name__)
print dir(thismodule)
method1('arg1')
method2()

And I get the following error

['TestClass', '__builtins__', '__doc__', '__file__', '__name__', '__package__', 'fabric_class_to_function_magic', 'inspect', 'sys', 'thismodule', 'types']
__main__
['TestClass', '__builtins__', '__doc__', '__file__', '__name__', '__package__', 'fabric_class_to_function_magic', 'inspect', 'sys', 'thismodule', 'types']

('__init__', <unbound method TestClass.__init__>)
code:  <code object newfunc_template at 0x7f8800a28d50, file "test.py", line 85>
method_name:  __init__

('method1', <unbound method TestClass.method1>)
code:  <code object newfunc_template at 0x7f8800a28d50, file "test.py", line 85>
method_name:  method1

('method2', <unbound method TestClass.method2>)
code:  <code object newfunc_template at 0x7f8800a28d50, file "test.py", line 85>
method_name:  method2
['TestClass', '__builtins__', '__doc__', '__file__', '__init__', '__name__', '__package__', 'fabric_class_to_function_magic', 'inspect', 'method1', 'method2', 'sys', 'thismodule', 'types']
Traceback (most recent call last):
  File "test.py", line 111, in <module>
    method1('arg1')
  File "test.py", line 88, in newfunc_template
    return func(*args, **kwargs)
TypeError: method2() takes exactly 1 argument (2 given)

It seems to be reusing the reference to the function? Any ideas?

UPDATE: Here is the working code with Ned Batchelder's fix

def fabric_class_to_function_magic(module_name):
    # get the module as an object
    module_obj = sys.modules[module_name]

    # Iterate over the methods of the class and dynamically create a function
    # for each method that calls the method and add it to the current module
    for method in inspect.getmembers(TestClass, predicate=inspect.ismethod):
        method_name, method_obj = method

        # get the bound method
        tc = TestClass()
        func = getattr(tc, method_name)

        # add the function to the current module
        setattr(module_obj, method_name, func)

UPDATE 2: Here is my blog post on the subject: http://www.saltycrane.com/blog/2010/09/class-based-fabric-scripts-metaprogramming-hack/

1
  • 1
    oddly enough, you should also set func.__module__ = module_name in order for it to describe itself properly... otherwise it will report its module as the one containing fabric_class_to_function_magic instead of the target module! Commented Dec 1, 2017 at 20:10

3 Answers 3

9

You're over-thinking your solution. Change the end of fabric_class_to_function_magic to be this:

    tc = TestClass()
    func = getattr(tc, method_name)

    # add the new function to the current module
    setattr(module_obj, method_name, func)

and it works fine. No need to make a new function object, you already have one returned by getattr on your object. The bound method returned by getattr is a callable thing. Just assign it to your module attribute, and you are good to go.

Sign up to request clarification or add additional context in comments.

Comments

1

actually your code is right but when return func(*args, **kwargs) executes, args will pass empty tuple like () and there are no parameters in your method2, so it raises such exception,

a quick solution towards your problem would be, like

class TestClass(object):
    def __init__(self):
        pass

    def method1(self, arg1):
        print 'method 1 %s' % arg1

    def method2(self, *args, **kw):
        print 'method 2'

1 Comment

This makes the error go away, but the underlying problem still exists. Now it prints "method 2" twice.
0

This might be useful to someone:

import inspect
import sys

def get_functions_from_class_in_module_and_extract_to_module(module, *args, class_config={}, **kwargs):

    classes = dict(tuple(x) for x in inspect.getmembers(sys.modules[module.__name__], inspect.isclass))

    for cls_name, cls in classes.items():

        class_instance = cls(**class_config[cls_name])

        functions = dict(tuple(x) for x in inspect.getmembers(cls, inspect.isfunction))

        for function_name, func in functions.items():
                
            class_func = lambda *args, **kwargs: func(class_instance, *args, **kwargs)

            setattr(module, function_name, class_func)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.