Ivan Semochkin
Ivan Semochkin

Reputation: 8897

How to get the function result from ast.Call object?

I need to test the python dictionary in the python module without running the file. So I've decided to parse it with the ast.parse. I've almost figure out how to build the original dictionary except I can't find a way to get function values working.

config.py
...
config = {
    # theese None, booleans and strings I got parsed
    user_info: None,
    debug: False,
    cert_pass: "test",

    # the function values I have problem with
    project_directory: os.path.join(__file__, 'project')
}
...


test.py
...
# Skiping the AST parsing part, going straight to parsing config dict 

def parse_ast_dict(ast_dict):
   #ast_dict is instance of ast.Dict
   result_dict = {}
   # iterating over keys and values
   for item in zip(ast_dict.keys, ast_dict.values):
       if isinstance(item[1], ast.NameConstant):
           result_dict[item[0].s] = item[1].value
       elif isinstance(item[1], ast.Str):
           result_dict[item[0].s] = item[1].s
       elif isinstance(item[1], ast.Num):
           result_dict[item[0].s] = item[1].n

       # I got stuck here parsing the ast.Call which in my case is os.path.join calls
       elif isinstance(item[0].s, ast.Call):
           pass
   return result_dict

I can't move the config object to a different file so that I can test it in isolation because it's a vendor provided code piece and can't import it to the tests either because it contains a lot of library imports so I stuck with ast.

Upvotes: 2

Views: 2395

Answers (1)

blhsing
blhsing

Reputation: 106588

To evaluate a function call, you need to actually execute it (unless you want to implement a Python interpreter yourself).

To avoid executing the entire config.py, however, you should focus on extracting the desired dictionary node from the AST for an isolated evaluation.

To do that, first find the assignment node where the assignment target is 'config' (since that's the name of the variable receiving the dictionary). Then, extract the value of the assignment node, which in this case is the dict you want. Build an Expression node from the value, adjust the line numbers and code offsets with ast.fix_missing_locations, compile it in 'eval' mode, and finally, evaluate the compiled code object with eval and it will return the dictionary you're looking for. Remember to pass to eval a global dict with appropriate values of necessary names such as os and __file__ so that the functions within can be called properly.

import ast
import os

config_path = 'config.py'
with open(config_path) as config_file:
    for node in ast.walk(ast.parse(config_file.read())):
        if isinstance(node, ast.Assign) and node.targets[0].id == 'config':
            expr = ast.Expression(body=node.value)
            ast.fix_missing_locations(expr)
            config = eval(
                compile(expr, '', 'eval'),
                {'os': os, '__file__': os.path.realpath(config_path)}
            )
            break

print(config)

Demo: https://replit.com/@blhsing/BluevioletMeaslyFlash

Upvotes: 1

Related Questions