1

I want to call a module function with getattr (or maybe something else?) from a string like this:

import bar
funcStr = "myFunc(\"strParam\", 123, bar.myenum.val1, kwarg1=\"someString\", kwarg2=456, kwarg3=bar.myenum.val2)"
[function, args, kwargs] = someParsingFunction(funcStr)
# call module function
getattr(bar, function)(*args, **kwargs)

How can I extract the args and kwargs from the string, so I can pass them to getattr? I tried a literal_eval approach with pythons ast module. But ast is not able to evaluate the enums of the module bar. And all other examples on SO pass a kwargs map with only strings in it. And they especially never parse the arguments from a string. Or is there another way to directly call the function from the string?

EDIT: A python script reads the function string from file. So using eval here is not advised.

EDIT2: Using python 3.6.3

EDIT3: Thanks to the first two answers I came up with two ideas. After parsing the args and kwargs out of the input string there are two possibilities for getting the right type of the arguments.

  1. We could use ast.literal_eval(<value of the argument>). For arguments with standard type like in kwarg2 it will return the needed value. If this excepts, which will happen for the enums, then we we will use getattr on the bar module and get the enums. If this excepts as well, then the string is not valid.
  2. We could use the inspect module and iterate through the parameters of myFunc. Then for every arg and kwarg we will check if the value is an instance of a myFunc parameter (type). If so, we will cast the arg/kwarg to the myFunc parameter type. Otherwise we raise an exception because the given arg/kwarg is not an instance of a myFunc parameter. This solution is more flexible than the first one.

Both solutions feel more like a workaround. First tests seem to work. I will post my results later here.

stackomatiker
  • 324
  • 3
  • 14

2 Answers2

0

Does this help?

funcStr = r"""myFunc(\"strParam\", 123, bar.myenum.val1, kwarg1=\"someString\", kwarg2=456, kwarg3=bar.myenum.val2)"""

def someParsingFunction(s):
    func, s1 = s.split('(', 1)
    l = s1.replace('\\','').strip(')').split(', ')
    arg_ = [x.strip('"') for x in l if '=' not in x]
    kwarg_ = {x.split('=')[0]:x.split('=')[-1] for x in l if '=' in x}
    return func, arg_, kwarg_

class bar:
    def myFunc(self, *args, **kwargs):
        print(*args)
        print(kwargs)

[function, args, kwargs] = someParsingFunction(funcStr)
getattr(bar, function)(*args, **kwargs)

# 123 bar.myenum.val1
# {'kwarg1': '"someString"', 'kwarg2': '456', 'kwarg3': 'bar.myenum.val2'}

Alternatively

funcStr = r"""myFunc(\"strParam\", 123, bar.val1, kwarg1=\"someString\", kwarg2=456, kwarg3=bar.val2)"""

def someParsingFunction(s):
    func, s1 = s.split('(', 1)
    l = s1.replace('\\','').strip(')').split(', ')
    arg_ = [x.strip('"') for x in l if '=' not in x]
    kwarg_ = {x.split('=')[0]:x.split('=')[-1] for x in l if '=' in x}
    return func, arg_, kwarg_

class Bar:
    def __init__(self):
        self.val1 = '111'

[function, args, kwargs] = someParsingFunction(funcStr)

bar = Bar()
obj_name = 'bar' + '.'
args = [bar.__getattribute__(x.split(obj_name)[-1]) if x.startswith(obj_name) else x for x in args]

print(args)
Andreas
  • 8,694
  • 3
  • 14
  • 38
  • Thank you. But unfortunately this is the solution I started with and it does not work. As you can see all your ```kwargs``` values are of type string and they will not match the function signature types. – stackomatiker Jun 02 '21 at 10:29
  • @stackomatiker Ah I think I better understand your problem now, how about the alternative I posted? It is rather an idea than a full solution, – Andreas Jun 02 '21 at 11:02
  • The expected result should be: ```args=["strParam", 123, bar.myenum.val1]``` and ```kwargs={kwarg1: "someString", kwarg2: 456, kwarg3: bar.val2}```. Unfortunately this is not the case in your solution. But your solution gave me an idea. It is psossible to get the enum by splitting the input and execute ```val=getattr(bar.myenum, "val1")```. But this is not the solution for the other kwargs, like ```kwarg2```. – stackomatiker Jun 02 '21 at 12:18
0
def get_bar_args(arg_str):
    """
        example:
        arg_str='bar.abc.def'
        assumess 'bar' module is imported
    """
    from functools import reduce
    reduce(getattr, arg_str.split('.')[1:], bar)

def parseFuncString(func_str):
    '''
        example: func_str = "myFunc(\"strParam\", 123, bar.myenum.val1, kwarg1=\"someString\", kwarg2=456, kwarg3=bar.myenum.val2)"
    '''
    import re
    all_args_str = re.search("(.*)\((.*)\)", func_str)
    all_args = all_args_str.group(2).split(',')
    all_args = [x.strip() for x in all_args]
    kwargs = {kw.group(1): kw.group(2) for x in all_args if (kw:=re.search('(^\w+)=(.*)$', x))}
    pargs = [x for x in all_args if not re.search('(^\w+)=(.*)$', x)]
    pargs = [get_bar_args(x) if x.startswith('bar.') else x for x in pargs]
    kwargs = {k: get_bar_args(v) if v.startswith('bar.') else v for k, v in kwargs.items()}
    print(f'{all_args=}\n{kwargs=}\n{pargs=}')
    func_name = func_str.split("(")[0]
    return func_name, pargs, kwargs
marke
  • 1,024
  • 7
  • 20
  • 1
    There is a little syntax error in the print statement and you used the Walrus operator which is not available in my python version (3.6.3). But I rewrote it and it was not a problem. The reduce with ```getattr``` works for the enums in bar. But the other parameters like ```123``` and ```kwarg2``` will be still returned as strings. And they won't match the ```myFunc``` signature. The expected result should be: ```args=["strParam", 123, bar.myenum.val1]``` and ```kwargs={kwarg1: "someString", kwarg2: 456, kwarg3: bar.myenum.val2}```. – stackomatiker Jun 02 '21 at 14:20