A nifty module hack

As I was working to rewrite my Sprycle utility, I found myself wishing that I could pass arguments to functions that were referenced by the Python script controller. Through a bit of hackery, I managed to make that possible:


# flexmod.py

class Module:
        
    def __getattr__(self, key):
        fn = key
        
        if fn[0] != '_':
            fn = '_' + fn
            
            if not '(' in fn:
                fn += "(cont)"
            else:
                sp = fn.split('(')
                fn = "(cont, ".join(sp)


            def func(cont):
                id(self) # preserve self in local context
                
                if not sum([s.positive for s in cont.sensors]):
                    return
                
                eval("self." + fn)
                
                for a in cont.actuators:
                    cont.activate(a)
                
            return func
        
        raise AttributeError(key[1:])

With that bit of code, you can write your module as a subclass:


# rand.py 

import sys
import random
import flexmod

class Rand(flexmod.Module):
        
    @staticmethod
    def _vec(cont, rr=1):
        gobj = cont.owner
        v = [random.uniform(-rr, rr) for i in range(3)]
        add = cont.actuators[0]
        add.linearVelocity = v

sys.modules[__name__] = Rand()

And that will enable you to actually call the function, with arguments, via the Python script controller:

Keyboard -----> Python-Module: rand.vec(5) -----> EditObject-AddObject

In this case, the linear velocity of the add object actuator will be set to a vector, with components that are random numbers between -5 and 5. This same approach can be used for a wide variety of different operations (some of which can be seen in the later parts of my new sprycle video), and it could serve in general to extend capabilities within the SCA paradigm (for those who don’t want to venture beyond).

Hope some of you find this interesting, or maybe even useful.

Very nice, though i haven’t figured out how it works yet.
If I’m thinking it throught right:

  • The bge uses getattr to find your function
  • You intercept this and replace it with func
  • func goes and runs your function, passing in the parameters.

I would have moved the activate actuators outside of the flexmod module though.

Conceptually, yes, that’s the basic process: I intercept the request, and I return a function that the controller expects (one that takes only a single argument - cont), but which internally incorporates the provided arguments.

I query the sensors, and activate the actuators in that function, because that’s typically what you want to do, so instead of constantly typing out the same lines, it made sense to just do it once, in that flexmod func.

Of course, you could add code to provide more flexibility, if required.

Really useful. I was wondering if this would be possible. I knew for example ‘bge.logic.endGame’, ‘bge.logic.PrintGLInfo’ or ‘bge.logic.PrintMemInfo’ works.

This is a really cool addition, Goran. Thanks.

Edit: Just playing with it a bit. Thought it would be nice to be able to control variables through game properties. Give the spawner a game property ‘r’ for example, so you could have: rand.vec(‘r’). Then just one line to the code:

    @staticmethod
    def _vec(cont, rr=1):
        gobj = cont.owner
<b>        if isinstance(rr, str): rr = gobj[rr]</b>
        v = [random.uniform(-rr, rr) for i in range(3)]
        add = cont.actuators[0]
        add.linearVelocity = v

class Module:
        
    def __getattr__(self, key):
        func_name, *args_value = key.split("(")
        
        try:
            argument_body = args_value[0]
            
        except IndexError:
            argument_body = ""
        
        else:
            if argument_body.endswith(")"):
                argument_body = argument_body[:-1]
        
        arguments = argument_body.replace(" ", "").split(",")
        
        argument_body = "({},)".format(",".join(arguments)) if arguments[0] else "()"
                     
        func = object.__getattribute__(self, func_name)
        
        def wrapper(cont):            
            if not sum([s.positive for s in cont.sensors]):
                return
            
            own = cont.owner
            
            locals_ = locals().copy()
            
            # Update with properties
            locals_.update({name: own[name] for name in own.getPropertyNames()})
            locals_.update({k.name: k for k in own.sensors})
            locals_.update({k.name: k for k in own.controllers})
            locals_.update({k.name: k for k in own.actuators})
            
            args = eval(argument_body, globals(), locals_)
            func(*args)
                       
            for a in cont.actuators:
                cont.activate(a)
            
        return wrapper

This might be a more generic solution. It populates the locals dictionary with the property names and SCA names of the object.

I think it might be more sensible to use function inspection for this particular request -> reading the function signature and providing correct arguments. Or do you think a more fallback approach would be sensible (protecting locally defined arguments of the same name)


import sys




class Module:
        
    def __getattribute__(self, func_name):                
        func = object.__getattribute__(self, func_name)
        
        def wrapper(cont):            
            if not sum([s.positive for s in cont.sensors]):
                return
            
            own = cont.owner
            
            # Update with properties
            object_data = dict()
            object_data.update({name: own[name] for name in own.getPropertyNames()})
            object_data.update({k.name: k for k in own.sensors})
            object_data.update({k.name: k for k in own.controllers})
            object_data.update({k.name: k for k in own.actuators})
            
            object_data['cont'] = cont
            object_data['own'] = own
            
            globals_ = FallbackDict(object_data)
            globals_.update(globals())
            globals_.update(__builtins__)
            
            exec(func.__code__, globals_)
                       
            for a in cont.actuators:
                cont.activate(a)
            
        return wrapper
       
class FallbackDict(dict):
    
    def __init__(self, fallback):
        self.fallback = fallback
    
    def __getitem__(self, key):
        try:
            return super().__getitem__(key)       
        
        except KeyError:
            return self.fallback[key]
       
class Main(Module):
    
    @staticmethod
    def main():
        print(cont)






sys.modules[__name__] = Main()

With this example, you can declare an empty function, and it will lookup undefined variables from the object. I don’t recommend this, save for rapid prototyping

You can have a naming convention to specify properties as arguments, which would then be automatically replaced with the proper values.

I would do it like this:


# flexmod.py

import re

class Module:
    def __init__(self, globs):
        self.globs = globs
        
    def __getattr__(self, key):
        fn, *argsl = key.split('(', 1)
        args = "(cont" + (',' + argsl[0] if argsl else ')')
        
        def wrapped(cont):
            if not sum([s.positive for s in cont.sensors]):
                return
            
            rex = re.compile("\|([a-zA-Z0-9_]*)\|")
            call = fn + rex.sub(r"cont.owner['\1']", args)
            
            eval(call, self.globs, locals())
                
            for a in cont.actuators:
                cont.activate(a)
                
        return wrapped


# rand.py

import sys
import random
import flexmod

def vec(cont, rr=1):
    v = [random.uniform(-rr, rr) for i in range(3)]
    add = cont.actuators[0]
    add.linearVelocity = v
    
sys.modules[__name__] = flexmod.Module(globals())

Then you can just call: rand.vec(|propname|)

@agoose77

Your code doesn’t work:

When calling a function with a single argument, the derived argument string is “(arg)”, and that will eval to just arg, not a sequence that contains arg; Trying to split that into arguments throws an error.

The target function will also throw errors, because the context specificed via locals_ is not available in the function itself; It’s only available to the expression that eval operates on, which is just the argument sequence in this case.

The second script is unable to handle arguments, so … I don’t see how it can be useful, and I don’t think there’s anything to be gained by simply dumping everything into function context.

Also, both scripts are unable to handle the original reference syntax, and instead require the user to actually call the function, which is inconvenient.

Concerning the first point, yes, I’ve posted an older version. Allow me to replace it, and note that one should post things before 7 am (Worked through the night, last night).

However, it functions as intended -> replacing any undefined variables in the arguments list with game properties.

On point 2, I was responding to Raco. The point being to expose game object data to the function body without explicitly referencing it. Note the disclaimer.
I don’t see any greater benefit to passing arguments in the manner of the original post than declaring them as object properties (besides convenience, an issue which I choose to address through providing variables to the function namespace), given the dependence upon the module which adds a level of obfuscation to call the function, unless you solely call it from other modules.

We cannot argue to technically about this, because there are already serious limitations (due to the fact that the BGE module lookup just splits by period characters, rather than investigating a valid path), such as entering floating point values, or any non-constant (i.e “”.join() method,)

An attempt to reference “cont” in the target function results in a NameError …

I was responding to Raco. The point being to expose game object data to the function body without explicitly referencing it.

He didn’t actually ask for that. Looking at his snippet, it seems pretty clear that he simply wanted a mechanism to reference specific properties, so they could be used as function inputs.

That’s something that can (and should) be done without stuffing function context.

That said: Injecting property names into locals (for the call, not the function context) would be good idea, so my latest revision would be:


# flexmod.py

class Module:
    def __init__(self, globs):
        self.globs = globs
        
    def __getattr__(self, key):
        fn, *argsl = key.split('(', 1)
        args = "(cont" + (',' + argsl[0] if argsl else ')')
        
        def wrapped(cont):
            if not sum([s.positive for s in cont.sensors]):
                return
            
            ls = locals()
            o = cont.owner
            ls.update({p: o[p] for p in o.getPropertyNames()})
            
            eval(fn + args, self.globs, ls)
                
            for a in cont.actuators:
                cont.activate(a)
                
        return wrapped

Call in controller would then be: rand.vec(propname)

Indeed, as aforementioned i wrote this rather late in my day, relatively, so the amended version should correct this. I proposed two alternative versions, the former being injecting locals, properties, sensors, actuators etc, into the dictionary of the evaluation and then a second non-argumentative (pun intended, albeit weak) iteration which serves to just provide the function body with such data.

I was referring to the amended version; It’s still broken.

Thanks guys. As soon as I have the time I’ll study the script further. It opens up some possibilities, so I’m quite happy about it.

Not sure what you’re testing. The first example uses locals() resolution for the arguments, the second for the function body, expecting no arguments.

I’m talking about the first example, used to process the following call: rand.vec(5)

It seems you forgot to inject cont into the argument expression … or maybe you expect users to explicitly reference cont in the call?

What are you testing?

Ah, yes I didn’t feel it necessary to include cont as a default argument. Otherwise, I would assume it is never provided by the arguments list, and do so within the wrapper as an implementation feature.

import sys
import re
from bge import logic


class Interface:
    
    def __init__(self):
        self.get_arguments = re.compile('\((.*)\)\Z')
        self.get_function_name = re.compile('.*?(?=\()')
        
    def __getattr__(self, name):
        match = re.search(self.get_function_name, name)
        
        if match is None:
            return globals()[name]
        
        function_name = match.group(0)
        if not function_name in globals():
            raise AttributeError(function_name)
        
        argument_match = re.search(self.get_arguments, name)
        if argument_match is None:
            arguments = ""
        
        else:
            arguments = argument_match.group(1)
                
        data = globals().copy()
        data["cont"] = logic.getCurrentController()
        return lambda: exec("{}(cont, {})".format(function_name, arguments), data)




sys.modules[__name__] = Interface()

So, by chance, I found a use case for this functionality, but I prefer writing in the module body, for module functions.

Here’s an adapted module design which simply provides a wrapper to the current module. You don’t need to subclass a wrapper class, which is nice.

It will provide the current controller by default, and the rest of your code can be written as usual.

If you don’t “call” the function in the module controller, it will access the function in the usual manner.

Looks interesting, agoose77, but how should it be used? Where do I put the code of the module itself (f.e. rand.py) and should I call this script flexmod.py? I don’t completely understand the code, but if I could have a working example, maybe I could investigate further.

My code also provides those conveniences (note that Module.init takes a “globals” argument).

I don’t see the need for regular expressions, or exec. As you can see in my code, one can simply evaluate the call string.

@Raco


# flexmod.py

class Module:
    def __init__(self, globs):
        self.globs = globs
        
    def __getattr__(self, key):
        fn, *argsl = key.split('(', 1)
        args = "(cont" + (',' + argsl[0] if argsl else ')')
        
        def wrapped(cont):
            if not sum([s.positive for s in cont.sensors]):
                return
            
            ls = locals()
            o = cont.owner
            ls.update({p: o[p] for p in o.getPropertyNames()})
            
            eval(fn + args, self.globs, ls)
                
            for a in cont.actuators:
                cont.activate(a)
                
        return wrapped


# rand.py

import sys
import flexmod

def vec(cont, rr=1):
    v = [random.uniform(-rr, rr) for i in range(3)]
    add = cont.actuators[0]
    add.linearVelocity = v

sys.modules[__name__] = flexmod.Module(globals())


You can then call rand.vec, or rand.vec(5), or rand.vec(property) in the controller.

Late to the game!

I use regex because it is clean and more reliable than string operations, when we’re looking to match a specific pattern. One might argue that in such a narrow context as this it is overkill, but naturally, I disagree :wink:

I didn’t argue that it was “overkill”; I argued that it was unnecessary.

There’s no need to “match a specific pattern” in this case. One just needs to insert “cont” as the first argument, and then simply evaluate the call string, as I demonstrated.

Yet you still must differentiate between a valid function name, for sane error reporting, and use that for lookup. The patterns in question are the function name which forms “xxxx” and the argument body which is nested within parenthesis.