Search code examples
pythonpython-3.xgetterchainingmethod-chaining

Python - Execute getter chains from string


getter_string = 'getName().attr.full_name()[0]'

How to apply the above given getter string to any object?

I need a function f such that f(obj, getter_string) would return f.getName().attr.full_name()[0]

I had a look at Python Chain getattr as a string but it seems to be only for chaining attributes. I wish to chain methods as well as indices.

I know this can be done by writing a parser which would handle all the cases carefully, but is there a more compact way of doing this?

Its safe to assume that the methods in the getter string won't have any parameters for now to prevent this from becoming unecessarily complex.


Solution

  • Let's use as example the attribute-tree getName().attr.full_name()[0].

    First, we need to create a dummy object that has this tree:

    class Dummy:
        def __init__(self, value):
            self.value = value
                
        class A:
            def __init__(self, value):
                self.value = value
            
            def full_name(self):
                return [self.value]
                
        class B:
            def __init__(self, value):
                self.value = value
                
            @property
            def attr(self):
                return Dummy.A(self.value)
                
        def getName(self):
            return Dummy.B(self.value)
    

    To create a Dummy object, you must pass a value to its constructor. This value will be returnied when acessing the attribute-tree:

    obj = Dummy(3.14)
    print(obj.getName().attr.full_name()[0])
    # Outputs 3.14
    

    We'll only use Dummy to demonstrate that our code is working. I'm assuming you already have an object with this attribute-tree.

    Now, you can use the ast module to parse the getter-string. In this case, I'm considering that the getter-string only contains properties, methods and indexes:

    import ast
    
    def parse(obj, getter_str):
        # Store the current object for each iteration. For example,
        #    - in the 1st iteration, current_obj = obj
        #    - in the 2nd iteration, current_obj = obj.getName()
        #    - in the 3rd iteration, current_obj = obj.getName().attr
        current_obj = obj
    
        # Store the current attribute name. The ast.parse returns a tree that yields
        #    - a ast.Subscript node when finding a index access
        #    - a ast.Attribute node when finding a attribute (either property or method)
        #    - a ast.Attribute and a ast.Call nodes (one after another) when finding a method
        #
        # Therefore, it's not easy to distinguish between a method and a property.
        # We'll use the following approach for each node:
        #    1. if a node is a ast.Attribute, save its name in current_attr
        #    2. if the next node is a ast.Attribute, the current_attr is an attribute
        #    3. otherwise, if the next node is a ast.Call, the current_attr is a method
        current_attr = None
    
        # Parse the getter-string and return only
        #    - the attributes (properties and methods)
        #    - the callables (only methods)
        #    - the subscripts (index accessing)
        tree = reversed([node 
                for node in ast.walk(ast.parse('obj.' + getter_str))
                if isinstance(node, (ast.Attribute, ast.Call, ast.Subscript))])
                
        for node in tree:
            if isinstance(node, ast.Call):
                # Method accessing
                if current_attr is not None:
                    current_obj = getattr(current_obj, current_attr)()
                    current_attr = None
            
            elif isinstance(node, ast.Attribute):
                # Property or method accessing
                if current_attr is not None:
                    current_obj = getattr(current_obj, current_attr)
                    
                current_attr = node.attr
            
            elif isinstance(node, ast.Subscript):
                # Index accessing
                current_obj = current_obj[node.slice.value.value]
                
        return current_obj
    

    Now, let's create a Dummy object and see if, when calling parse with the attribute-tree given, it'll return the value passed in its constructor:

    obj = Dummy(2.71)
    print(parse(obj, 'getName().attr.full_name()[0]'))
    # Outputs 2.71
    

    So the parse function is able to correctly parse the given attribute-tree.

    I'm not familiar with ast so there may be an easier way to do that.