How do I check if a line is a valid function call in python?

2.3k views Asked by At

Background: I'm currently creating a line-magic for ipython. This magic shall only work for lines, where the return value of a function is assigned to a variable.

I'm looking for a way to make sure, that a line is a valid function-call + assignment in python.

e.g. the following shall be accepted:

a = b()
a,b = c(d,e="f")
a = b(c()+c)

and the following shall be declined:

a = def fun() # no function call
b(a=2) # no assignment
a = b + c # no function call 
a = b() + c() # top-level on right-hand-side must be function call

If the line is no valid python at all, I don't care whether it passes, as this will be handled at another stage.

2

There are 2 answers

1
Kevin On BEST ANSWER

You could use Python's own parser, accessible through the ast module, to directly inspect each statement to see if it's an assignment whose right-hand-side is a call.

import ast

def is_call_assignment(line):
    try:
        node = ast.parse(line)
    except SyntaxError:
        return False
    if not isinstance(node, ast.Module):
        return False
    if len(node.body) != 1 or not isinstance(node.body[0], ast.Assign):
        return False
    statement = node.body[0]
    return isinstance(statement.value, ast.Call)


test_cases = [
    'a = b()',
    'a,b = c(d,e="f")',
    'a = b(c()+c)',
    'a = def fun()',
    'b(a=2)',
    'a = b + c',
    'a = b() + c()'
]

for line in test_cases:
    print(line)
    print(is_call_assignment(line))
    print("")

Result:

a = b()
True

a,b = c(d,e="f")
True

a = b(c()+c)
True

a = def fun()
False

b(a=2)
False

a = b + c
False

a = b() + c()
False
1
wotanii On

The best I have come up with this:

  1. apply this regexp
    [A-z, ]*= *[A-z_]* *\(.*\)
  2. split at the first "="
  3. make sure brackets are balanced, but fail if the bracket-count hits zero before the last bracket

step 3 is needed, to make this case fail :

a = b() + c() # top-level on right-hand-side must be function call

also step 3 will kinda look like this:

def matched(str):
    count = 0
    for i in str.strip():
        if i == "(":
            count += 1
        elif i == ")":
            count -= 1
        if count < 0 or (count < 1 and i>=len(str)-1):
            return False
    return count == 0

(based on this)

This solution is horribly ugly, because I can't figure out how to nicely fail if the top level is no function call.

A much better solution might use python's AST, but I don't know how to access that.