Reputation: 8897
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
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