0
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.

  • You need to do some parsing to detect `[0]` and turn that into an index instead of attribute getter. – Barmar Jun 24 '21 at 19:23

1 Answers1

1

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.

enzo
  • 9,861
  • 3
  • 15
  • 38