def test_group_expressions(): x = (1, 2) evaluator = Evaluator({'x': x}) tree = ast.parse('x[0] + x[x[0]]').body[0].value expressions = evaluator.find_expressions(tree) grouped = set((frozenset(nodes), value) for nodes, value in group_expressions(expressions)) expected = { (frozenset([tree.left, subscript_item(tree.right)]), x[0]), (frozenset([ tree.left.value, subscript_item(tree.right).value, tree.right.value ]), x), (frozenset([ subscript_item(tree.left), subscript_item(subscript_item(tree.right)) ]), 0), (frozenset([tree.right]), x[x[0]]), (frozenset([tree]), x[0] + x[x[0]]), } assert grouped == expected grouped = set( (frozenset(nodes), value) for nodes, value in evaluator.interesting_expressions_grouped(tree)) expected = set((nodes, value) for nodes, value in expected if value != 0) assert grouped == expected
def check_interesting(source): frame = inspect.currentframe().f_back evaluator = Evaluator.from_frame(frame) root = ast.parse(source) node = root.body[0].value cannot = value = None try: value = evaluator[node] except CannotEval as e: cannot = e expr = ast.Expression(body=node) ast.copy_location(expr, node) code = compile(expr, "<expr>", "eval") try: expected = eval(code, frame.f_globals, frame.f_locals) except Exception: if cannot: return None else: raise else: if cannot: raise cannot else: assert value == expected return is_expression_interesting(node, value)
def calling_env(funtype: str) -> Any: """Checking how the function is called: 1. PIPING_VERB: It is a verb that is piped directed. ie. data >> verb(...) 2. PIPING: It is a function called as (part of) the argument of a piping verb. ie.: >>> data >> verb(func(...)) Note that `func` here could also be a verb. When a function is called inside a lambda body, it should not be counted in this situation: >>> data >> verb(lambda: func(...)) In this case, func should be called as normal function. This function should return `None` 3. FUNC_ARG: It is an argument of any function call 4. None: None of the above situation fits This function should be only called inside register_*.wrapper """ if options.assume_all_piping: return (CallingEnvs.PIPING_VERB if funtype == 'Verb' else CallingEnvs.PIPING) # frame 1: register_*.wrapper # frame 2: func(...) frame = sys._getframe(2) my_node = Source.executing(frame).node if not my_node and options.warn_astnode_failure: warnings.warn( "Failed to fetch the node calling the function, " "please call the verbs regularly instead of `data >> verb(...)`.") return None piping_verb_node = _get_piping_verb_node(my_node) if piping_verb_node is my_node and piping_verb_node is not None: return CallingEnvs.PIPING_VERB if _is_piping_verb_argument_node(my_node, piping_verb_node): return CallingEnvs.PIPING parent_call_node = _argument_node_of(my_node) if parent_call_node is None: return None # check if parent call node is a function registered by # register_verb/register_func evaluator = Evaluator.from_frame(frame) try: func = evaluator[parent_call_node.func] except CannotEval: # pragma: no cover return None if functype(func) != "plain": return CallingEnvs.PIPING return None
def variables(self) -> List[Variable]: """ All Variable objects whose nodes are contained within .scope and whose values could be safely evaluated by pure_eval. """ if not self.scope: return [] evaluator = Evaluator.from_frame(self.frame) scope = self.scope node_values = [ pair for pair in evaluator.find_expressions(scope) if is_expression_interesting(*pair) ] # type: List[Tuple[ast.AST, Any]] if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef)): for node in ast.walk(scope.args): if not isinstance(node, ast.arg): continue name = node.arg try: value = evaluator.names[name] except KeyError: pass else: node_values.append((node, value)) # Group equivalent nodes together def get_text(n): if isinstance(n, ast.arg): return n.arg else: return self.source.asttokens().get_text(n) def normalise_node(n): try: # Add parens to avoid syntax errors for multiline expressions return ast.parse('(' + get_text(n) + ')') except Exception: return n grouped = group_by_key_func( node_values, lambda nv: ast.dump(normalise_node(nv[0])), ) result = [] for group in grouped.values(): nodes, values = zip(*group) value = values[0] text = get_text(nodes[0]) if not text: continue result.append(Variable(text, nodes, value)) return result
def test_cannot_subscript(expr): with pytest.raises(Exception): eval(expr) evaluator = Evaluator({}) tree = ast.parse(expr) node = tree.body[0].value assert isinstance(node, ast.Subscript) with pytest.raises(CannotEval): str(evaluator[node])
def check_interesting(source): frame = inspect.currentframe().f_back evaluator = Evaluator.from_frame(frame) root = ast.parse(source) node = root.body[0].value value = evaluator[node] expr = ast.Expression(body=node) ast.copy_location(expr, node) code = compile(expr, "<expr>", "eval") expected = eval(code, frame.f_globals, frame.f_locals) assert value == expected return is_expression_interesting(node, value)
def check_eval(source, *expected_values, total=True): frame = inspect.currentframe().f_back evaluator = Evaluator.from_frame(frame) root = ast.parse(source) values = [] for node, value in evaluator.find_expressions(root): expr = ast.Expression(body=node) ast.copy_location(expr, node) code = compile(expr, "<expr>", "eval") expected = eval(code, frame.f_globals, frame.f_locals) assert value == expected values.append(value) if total: assert value in expected_values for expected in expected_values: assert expected in values
def get_function_called_argname(frame: FrameType, node: ast.AST) -> Callable: """Get the function who called argname""" # We need node to be ast.Call if not isinstance(node, ast.Call): raise VarnameRetrievingError( f"Expect an 'ast.Call' node, but got {type(node)!r}. " "Are you using 'argname' inside a function?") # variable if isinstance(node.func, ast.Name): func = frame.f_locals.get(node.func.id, frame.f_globals.get(node.func.id)) if func is None: # pragma: no cover # not sure how it would happen but in case raise VarnameRetrievingError( f"Cannot retrieve the function by {node.func.id!r}.") return func # use pure_eval pure_eval_fail_msg = None try: from pure_eval import Evaluator, CannotEval except ImportError: pure_eval_fail_msg = "'pure_eval' is not installed." else: try: evaluator = Evaluator.from_frame(frame) return evaluator[node.func] except CannotEval: pure_eval_fail_msg = ( f"Cannot evaluate node {ast.dump(node.func)} " "using 'pure_eval'.") # try eval warnings.warn( f"{pure_eval_fail_msg} " "Using 'eval' to get the function that calls 'argname'. " "Try calling it using a variable reference to the function, or " "passing the function to 'argname' explicitly.") expr = ast.Expression(node.func) code = compile(expr, '<ast-call>', 'eval') # pylint: disable=eval-used return eval(code, frame.f_globals, frame.f_locals)
def variables(self): if not self.source.tree: return [] evaluator = Evaluator.from_frame(self.frame) get_text = self.source.asttokens().get_text scope = self.scope node_values = evaluator.find_expressions(scope) if isinstance(scope, ast.FunctionDef): for node in ast.walk(scope.args): if not isinstance(node, ast.arg): continue name = node.arg try: value = evaluator.names[name] except KeyError: pass else: node_values.append((node, value)) # TODO use compile(...).co_code instead of ast.dump? # Group equivalent nodes together grouped = group_by_key_func( node_values, # Add parens to avoid syntax errors for multiline expressions lambda nv: ast.dump(ast.parse('(' + get_text(nv[0]) + ')')), ) result = [] for group in grouped.values(): nodes, values = zip(*group) if not all(value is values[0] for value in values): # Distinct values found for same expression # Probably indicates another thread is messing with things # Since no single correct value exists, ignore this expression continue value = values[0] text = get_text(nodes[0]) result.append(Variable(text, nodes, value)) return result
def test_evaluator_wrong_getitem(): evaluator = Evaluator({}) with pytest.raises(TypeError, match="node should be an ast.expr, not 'str'"): # noinspection PyTypeChecker str(evaluator["foo"])
def get_all_objects(line, frame): """Given a (partial) line of code and a frame, obtains a dict containing all the relevant information about objects found on that line so that they can be formatted as part of the answer to "where()" or they can be used during the analysis of the cause of the exception. The dict returned has five keys. The first three, 'locals', 'globals', 'builtins', each containing a list of tuples, each tuple being of the form (name, repr(obj), obj) where name --> obj. The fourth key, 'expressions', contains a list of tuples of the form ('name', obj). It is only occasionally used in helping to make suggestions regarding the cause of some exception. """ objects = { "locals": [], "globals": [], "builtins": [], "expressions": [], "name, obj": [], } scopes = ( ("locals", frame.f_locals), # always have locals before globals ("globals", frame.f_globals), ) names = set() tokens = token_utils.get_significant_tokens(line) for tok in tokens: if tok.is_identifier(): name = tok.string if name in names: continue for scope, scope_dict in scopes: if name in scope_dict: names.add(name) obj = scope_dict[name] objects[scope].append((name, repr(obj), obj)) objects["name, obj"].append((name, obj)) break else: if name in dir(builtins): names.add(name) obj = getattr(builtins, name) objects["builtins"].append((name, repr(obj), obj)) objects["name, obj"].append((name, obj)) try: atok = ASTTokens(line, parse=True) except SyntaxError: # this should not happen return objects if atok is not None: evaluator = Evaluator.from_frame(frame) for nodes, obj in group_expressions( pair for pair in evaluator.find_expressions(atok.tree)): name = atok.get_text(nodes[0]) if name in names: continue names.add(name) objects["name, obj"].append((name, obj)) try: # We're not interested in showing literals in the list of variables ast.literal_eval(name) except Exception: # noqa objects["expressions"].append((name, obj)) return objects
def get_all_objects(line, frame): """Given a (partial) line of code and a frame, obtains a dict containing all the relevant information about objects found on that line so that they can be formatted as part of the answer to "where()" or they can be used during the analysis of the cause of the exception. The dict returned has four keys. The first three, 'locals', 'globals', 'nonlocals', each containing a list of tuples, each tuple being of the form (name, repr(obj), obj) where name --> obj. The fourth key, 'literals', contains a list of tuples of the form ('name', obj). It is only occasionally used in helping to make suggestions regarding the cause of some exception. """ objects = { "locals": [], "globals": [], "literals": [], "builtins": [], "name, obj": [], } scopes = ( ("locals", frame.f_locals), # always have locals before globals ("globals", frame.f_globals), ) names = set([]) try: atok = ASTTokens(line, parse=True) except SyntaxError: # this should not happen atok = None if atok is not None: for scope, scope_dict in scopes: for nodes, obj in Evaluator( scope_dict).interesting_expressions_grouped(atok.tree): name = atok.get_text(nodes[0]) if name in names: continue names.add(name) objects[scope].append((name, repr(obj), obj)) objects["name, obj"].append((name, obj)) Evaluator.literal_expressions_grouped = literal_expressions_grouped for nodes, obj in Evaluator({}).literal_expressions_grouped( atok.tree): # noqa name = atok.get_text(nodes[0]) objects["literals"].append((name, obj)) objects["name, obj"].append((name, obj)) tokens = token_utils.get_significant_tokens(line) for tok in tokens: if tok.is_identifier(): name = tok.string if name in names: continue for scope, scope_dict in scopes: if name in scope_dict: names.add(name) obj = scope_dict[name] objects[scope].append((name, repr(obj), obj)) objects["name, obj"].append((name, obj)) break else: if name in dir(builtins): obj = getattr(builtins, name) objects["builtins"].append((name, repr(obj), obj)) objects["name, obj"].append((name, obj)) dotted_names = get_dotted_names(line) for name in dotted_names: for scope, scope_dict in scopes: if name not in scope_dict: continue obj = scope_dict[name] if (name, obj) not in objects["name, obj"]: objects[scope].append((name, repr(obj), obj)) objects["name, obj"].append((name, obj)) # TODO: check to see if this is still needed objects["nonlocals"] = get_nonlocal_objects(frame) return objects