2

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.

asked May 17, 2022 at 11:23
4
  • 1
    What do you expect to extract for that value? The result of calling the function with the provided arguments? How does that coexist with your desire to not run the file? Commented May 17, 2022 at 15:52
  • @rici I expect to see the result of calling the function. All of the functions in the config dict are os.path calls and I expect to get the path string so I can assert it in the tests. I could just import the config dict to the tests and assert the fields but I don't want to install all of the module dependencies that's why I've used the ast.parse to get the dict from the file Commented May 18, 2022 at 8:57
  • So you want to evaluate the call. It's already parsed. But to evaluate it, you will need all of the variables which might be referred to in the expression. If it's just __file__, that's easy, but I suspect it's more complicated than that. Commented May 18, 2022 at 15:35
  • @rici no they are all just os.path calls similar to this one 'console_config': os.path.join(os.path.dirname(os.path.realpath(__file__)), '../cdn_config.json') Commented May 19, 2022 at 8:28

1 Answer 1

1
+50

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

answered May 24, 2022 at 8:54
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.