Package: pure-eval
pure_evalThis is a Python package that lets you safely evaluate certain AST nodes without triggering arbitrary code that may have unwanted side effects.It can be installed from PyPI:pip install pure_evalTo demonstrate usage, suppose we have an object defined as follows:classRectangle:def__init__(self,width,height):self.width=widthself.height=height@propertydefarea(self):print("Calculating area...")returnself.width*self.heightrect=Rectangle(3,5)Given therectobject, we want to evaluate whatever expressions we can in this source code:source="(rect.width, rect.height, rect.area)"This library works with the AST, so let's parse the source code and peek inside:importasttree=ast.parse(source)the_tuple=tree.body[0].valuefornodeinthe_tuple.elts:print(ast.dump(node))Output:Attribute(value=Name(id='rect',ctx=Load()),attr='width',ctx=Load())Attribute(value=Name(id='rect',ctx=Load()),attr='height',ctx=Load())Attribute(value=Name(id='rect',ctx=Load()),attr='area',ctx=Load())Now to actually use the library. First construct an Evaluator:frompure_evalimportEvaluatorevaluator=Evaluator({"rect":rect})The argument toEvaluatorshould be a mapping from variable names to their values. Or if you have access to the stack frame whererectis defined, you can instead use:evaluator=Evaluator.from_frame(frame)Now to evaluate some nodes, usingevaluator[node]:print("rect.width:",evaluator[the_tuple.elts[0]])print("rect:",evaluator[the_tuple.elts[0].value])Output:rect.width: 3
rect: <__main__.Rectangle object at 0x105b0dd30>OK, but you could have done the same thing witheval. The useful part is that it will refuse to evaluate the propertyrect.areabecause that would trigger unknown code. If we try, it'll raise aCannotEvalexception.frompure_evalimportCannotEvaltry:print("rect.area:",evaluator[the_tuple.elts[2]])# failsexceptCannotEvalase:print(e)# prints CannotEvalTo find all the expressions that can be evaluated in a tree:fornode,valueinevaluator.find_expressions(tree):print(ast.dump(node),value)Output:Attribute(value=Name(id='rect',ctx=Load()),attr='width',ctx=Load())3Attribute(value=Name(id='rect',ctx=Load()),attr='height',ctx=Load())5Name(id='rect',ctx=Load())<__main__.Rectangleobjectat0x105568d30>Name(id='rect',ctx=Load())<__main__.Rectangleobjectat0x105568d30>Name(id='rect',ctx=Load())<__main__.Rectangleobjectat0x105568d30>Note that this includesrectthree times, once for each appearance in the source code. Since all these nodes are equivalent, we can group them together:frompure_evalimportgroup_expressionsfornodes,valuesingroup_expressions(evaluator.find_expressions(tree)):print(len(nodes),"nodes with value:",values)Output:1 nodes with value: 3
1 nodes with value: 5
3 nodes with value: <__main__.Rectangle object at 0x10d374d30>If we want to list all the expressions in a tree, we may want to filter out certain expressions whose values are obvious. For example, suppose we have a functionfoo:deffoo():passIf we refer tofooby its name as usual, then that's not interesting:frompure_evalimportis_expression_interestingnode=ast.parse('foo').body[0].valueprint(ast.dump(node))print(is_expression_interesting(node,foo))Output:Name(id='foo',ctx=Load())FalseBut if we refer to it by a different name, then it's interesting:node=ast.parse('bar').body[0].valueprint(ast.dump(node))print(is_expression_interesting(node,foo))Output:Name(id='bar',ctx=Load())TrueIn generalis_expression_interestingreturns False for the following values:Literals (e.g.123,'abc',[1, 2, 3],{'a': (), 'b': ([1, 2], [3])})Variables or attributes whose name is equal to the value's__name__, such asfooabove orself.fooif it was a method.Builtins (e.g.len) referred to by their usual name.To make things easier, you can combine finding expressions, grouping them, and filtering out the obvious ones with:evaluator.interesting_expressions_grouped(root)To get the source code of an AST node, I recommendasttokens.Here's a complete example that brings it all together:fromasttokensimportASTTokensfrompure_evalimportEvaluatorsource="""x = 1d ={x: 2}y = d[x]"""names={}exec(source,names)atok=ASTTokens(source,parse=True)fornodes,valueinEvaluator(names).interesting_expressions_grouped(atok.tree):print(atok.get_text(nodes[0]),"=",value)Output:x=1d={1:2}y=2d[x]=2