3

I'm trying to figure out how to differentiate between a python module-level function and a method that is not yet bound to an object instance. When the function is an unbound instance method, I need a handle to the class that defined the method. inspect.ismethod() doesn't work because the method is not yet bound to an object instance. type() returns 'function' for both a module-level function and an unbound method.

The use case is a plugin framework that uses method and function decorators. I have a generic 'Model' class that can be subclassed to add functionality. Plugin 'actions' are defined in the subclass using the @register_action( name ) decorator. The decorator function is called on import to register the action. The gui then adds menu items for each ACTION in the dictionary -- but the gui should only add an action if the Model class for the gui is an instance of the class where the function/method was registered.

Some sample code is below. Right now my solution is to parse the text of the fcn descriptor, then check if class_str == str( model.__class__ ).split('.')[-1].strip("'>"). I also tried passing in SpecialModel to the decorator, but SpecialModel is not defined when the decorator function runs so that doesn't work. There must be a better solution for registering class method actions.

Note: since it is not possible to pass SpecialMethod into the register_action decorator because SpecialMethod is not defined, I may need to call get_class_from_function when it comes time to register an action with the gui.

ACTIONS = dict()

### HACK -- best I could do, but want actual Class, not class_name ###
# FIXME : want to return None, Model, SpecialModel, etc. 
def get_class_from_function( fcn ):
    qname = fcn.__qualname__.split( '.' )
    if len( qname ) > 1:     # this is (likely) a class function
        cls_name = qname[0]
        return cls_name
    else:
        return None

def register_action( name ):
    def register_decorator( fcn ):
        global ACTION
        cls = get_class_from_function( fcn )
        ACTION[ name ] = ( fcn, cls )
        return fcn
    return register_decorator

@register_action( "Help/About" )
def help_about_function( model ):
    pass

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

    @register_action( "File/Load" )
    def load( self ):
        pass

class SpecialModel( Model ):
    @register_action( "Special/Custom Action" )
    def specialact( self ):
        pass
swinman
  • 391
  • 3
  • 10
  • 1
    Have you considered checking whether the first argument is `self` (e.g. `args = inspect.getargspec(thing).args; args and args[0] == 'self'`)? It's no guarantee, but a good indication. – jonrsharpe Nov 23 '15 at 20:59
  • 1
    I believe that in Python 3 there is no distinction between a free function and a function defined within a class definition. In fact, you may define a function at the module level and "tack it on" to the class, and it will work just as if it were defined as a method. Perhaps this isn't the best approach... maybe the action of registering a method should be done by the class. That is, whenever a `Model` is defined, that is when its methods should be registered. This can be done with a class decorator, or, alternatively, a metaclass. – jme Nov 23 '15 at 21:06
  • *class method* is a term of art. As far as I can tell, your use of that term is incorrect and just confuses the issue. – 7stud Nov 23 '15 at 21:08
  • @jonrsharpe -- yes, that is one of the only differences we could find using the inspect module. It solves the first problem if we are careful about using 'self' instead of slf, s, etc, but it doesn't solve the second part of the problem where we need to check if our model is a subclass of SpecialModel etc. – swinman Nov 23 '15 at 21:09
  • @swinman you cannot get access to the class from an unbound method in 3.x - see e.g. http://stackoverflow.com/q/3589311/3001761 – jonrsharpe Nov 23 '15 at 21:10
  • The class object doesn't exist at the time the decorator executes. – user2357112 Nov 23 '15 at 21:12
  • @jonrsharpe that is a helpful link. thank you – swinman Nov 23 '15 at 21:43

2 Answers2

3

I think you could accomplish your end-goal with a decorator like this:

def register_action( name ):
    def register_decorator( fcn ):
        fcn._action_name = name
        return fcn
    return register_decorator

Then later on in your gui code when you actually have the object instance, iterate over its instance methods and look for any with _action_name and create the gui-specific code for it:

for method_name, method in inspect.getmembers(my_specific_instance, predicate=inspect.ismethod):
   if hasattr(method, '_action_name'):
       add_to_gui_menu(method._action_name, method)
R Bryan
  • 56
  • 4
  • thanks, this works (after minor syntax fixes). `inspect.getmembers()` returns `(mname, method)` tuple. Also, change `__action_name__` to `_action_name` because trailing `__` are for python special and leading `__` mangles the variable name – swinman Nov 23 '15 at 23:13
2

This should help to distinguish a function defined in a module from a function defined in a class:

import sys
import types

def is_module_func(obj):
    mod = sys.modules[obj.__module__]
    return isinstance(obj, types.FunctionType) and hasattr(mod, obj.__name__)

Now:

class A:
    def meth(self):
        pass

def func():
    pass

>>> is_module_func(func)
True
>>> is_module_func(A.meth)
False
Mike Müller
  • 82,630
  • 20
  • 166
  • 161
  • thanks - unfortunately this doesn't work from within the @register decorator because hasattr( mod, obj.__name__) returns false, and it gets confused when the module and function have the same name – swinman Nov 23 '15 at 22:04
  • to clarify: hasattr returns false because the object has not yet been added to the module when the decorator runs – swinman Nov 23 '15 at 22:13