Search code examples
pythonvariableslocalself

How to get self into a Python method without explicitly accepting it


I'm developing a documentation testing framework -- basically unit tests for PDFs. Tests are (decorated) methods of instances of classes defined by the framework, and these are located and instantiated at runtime and the methods are invoked to execute the tests.

My goal is to cut down on the amount of quirky Python syntax that the people who will write tests need to be concerned about, as these people may or may not be Python programmers, or even very much programmers at all. So I would like them to be able to write "def foo():" instead of "def foo(self):" for methods, but still be able to use "self" to access members.

In an ordinary program I would consider this a horrible idea, but in a domain-specific-languagey kind of program like this one, it seems worth a try.

I have successfully eliminated the self from the method signature by using a decorator (actually, since I am using a decorator already for the test cases, I would just roll it into that), but "self" does not then refer to anything in the test case method.

I have considered using a global for self, and even come up with an implementation that more or less works, but I'd rather pollute the smallest namespace possible, which is why I would prefer to inject the variable directly into the test case method's local namespace. Any thoughts?


Solution

  • Here's a one line method decorator that seems to do the job without modifying any Special attributes of Callable types* marked Read-only:

    # method decorator -- makes undeclared 'self' argument available to method
    injectself = lambda f: lambda self: eval(f.func_code, dict(self=self))
    
    class TestClass:
        def __init__(self, thing):
            self.attr = thing
    
        @injectself
        def method():
            print 'in TestClass::method(): self.attr = %r' % self.attr
            return 42
    
    test = TestClass("attribute's value")
    ret = test.method()
    print 'return value:', ret
    
    # output:
    # in TestClass::method(): self.attr = "attribute's value"
    # return value: 42
    

    Note that unless you take precautions to prevent it, a side-effect of the eval() function may be it adding a few entries -- such as a reference to the __builtin__ module under the key __builtins__ -- automatically to the dict passed to it.

    @kendall: Per your comment about how you're using this with methods being in container classes (but ignoring the injection of additional variables for the moment) -- is the following something like what you're doing? It's difficult for me to understand how things are split up between the framework and what the users write. It sounds like an interesting design pattern to me.

    # method decorator -- makes undeclared 'self' argument available to method
    injectself = lambda f: lambda self: eval(f.func_code, dict(self=self))
    
    class methodclass:
        def __call__():
            print 'in methodclass::__call__(): self.attr = %r' % self.attr
            return 42
    
    class TestClass:
        def __init__(self, thing):
            self.attr = thing
    
        method = injectself(methodclass.__call__)
    
    test = TestClass("attribute's value")
    ret = test.method()
    print 'return value:', ret
    
    # output
    # in methodclass::__call__(): self.attr = "attribute's value"
    # return value: 42