Source code for pedal.cait.cait_api

"""

.. csv-table:: Cait Report Data
    :header: "Field", "Type", "Initial", "Description"
    :widths: 20, 20, 20, 40

    "ast", "CaitNode", "None", "The CaitNode tree that was most recently parsed out."
    "cache", "{str: CaitNode}", "{}", "A dictionary mapping previously parsed code to CaitNode trees."
    "success", "bool", "True", "Whether the most recent parsing was successful."
    "error", "Exception", "None", "The most recent exception, or None."

"""
from pedal.cait.constants import TOOL_NAME
from pedal.core.commands import system_error
from pedal.core.report import Report, MAIN_REPORT
from pedal.cait.stretchy_tree_matching import StretchyTreeMatcher
from pedal.cait.cait_node import CaitNode
import ast

# TODO: Decide if we want the imports; seems like overkill?
SOURCE_TOOL_NAME = 'source'
TIFA_TOOL_NAME = 'tifa'


[docs] class CaitException(Exception): pass
def _parse_source(code, report=MAIN_REPORT): """ Parses the given code and returns its Cait representation. If the parse was unsuccessful, it attaches the error to the report. Args: code (str): A string of Python code. report (Report): A Report to store information in. Returns: AstNode: The parsed AST reprensetation, or None """ try: parsed = ast.parse(code) report[TOOL_NAME]['success'] = True report[TOOL_NAME]['error'] = None except SyntaxError as e: system_error(TOOL_NAME, "Could not parse code:" + str(e), report=report) report[TOOL_NAME]['success'] = False report[TOOL_NAME]['error'] = e return ast.parse("") return parsed
[docs] def reparse_if_needed(student_code=None, report=MAIN_REPORT): """ Retrieves the current report for CAIT. If there is no CAIT report, it will generate one. If source code is given, that will be used instead of the report's submission. Args: student_code (str): The code to parse into the a CaitNode tree. If None, then it will use the code in the report's submission. report (Report): The report to attach data to. Returns: dict: Returns the Cait Report """ cait = report[TOOL_NAME] if student_code is not None: if student_code in cait['cache']: cait['ast'] = cait['cache'][student_code] return cait else: student_ast = _parse_source(student_code, report=report) else: student_code = report.submission.main_code # Have we already parsed this code? if student_code in cait['cache']: cait['ast'] = cait['cache'][student_code] return cait # Try to steal parse from Source module, if available if report[SOURCE_TOOL_NAME]['success']: student_ast = report[SOURCE_TOOL_NAME]['ast'] else: student_ast = _parse_source(student_code, report=report) cait['ast'] = cait['cache'][student_code] = CaitNode(student_ast, report=report) return cait
[docs] def require_tifa(self): """ Confirms that TIFA was run successfully, otherwise raises a CaitException. TODO: Is this deprecated? """ if not self.report[TIFA_TOOL_NAME]['success']: system_error(TOOL_NAME, "TIFA was not successfully run prior to CAIT.") raise CaitException("TIFA was not run prior to CAIT.")
# noinspection PyBroadException
[docs] def parse_program(student_code=None, report=MAIN_REPORT): """ Parses student code and produces a CAIT representation. Args: student_code (str): The student source code to parse. If None, defaults to the code within the Source tool of the given Report. report (Report): The report to attach data to. Defaults to MAIN_REPORT. Returns: CaitNode: A CAIT-enhanced representation of the root Node. """ cait_report = reparse_if_needed(student_code, report=report) return cait_report['ast']
[docs] def expire_cait_cache(report=MAIN_REPORT): """ Deletes the most recent CAIT run and any cached CAIT parses. Args: report (Report): The report to attach data to. Defaults to MAIN_REPORT. """ report['cait']['ast'] = None report['cait']['cache'] = {}
[docs] def def_use_error(node, report=MAIN_REPORT): """ Checks if node is a name and has a def_use_error Args: node (str or AstNode or CaitNode): The Name node to look up. report (Report): The report to attach data to. Defaults to MAIN_REPORT. Returns: True if the given name has a def_use_error """ if not isinstance(node, str) and node.ast_name != "Name": raise TypeError from pedal.tifa.commands import get_issues from pedal.tifa.feedbacks import initialization_problem def_use_issues = get_issues(initialization_problem) if not isinstance(node, str): node_id = node.id else: node_id = node has_error = False for issue in def_use_issues: name = issue.fields['name'] if name == node_id: has_error = True break return has_error
# noinspection PyBroadException
[docs] def data_state(node, report=MAIN_REPORT): """ Determines the Tifa State of the given node. Args: node (str or AstNode or CaitNode): The Name node to look up in TIFA. report (Report): The report to attach data to. Defaults to MAIN_REPORT. Returns: The State of the object (Tifa State) or None if it doesn't exist """ if not isinstance(node, str) and node.ast_name != "Name": raise TypeError if isinstance(node, str): node_id = node else: node_id = node.id try: return report[TIFA_TOOL_NAME]["latest"].top_level_variables[node_id] except KeyError: return None
[docs] def data_type(node, report=MAIN_REPORT): """ Looks up the type of the node using Tifa's analysis. Args: node (str or AstNode or CaitNode): The Name node to look up in TIFA. report (Report): The report to attach data to. Defaults to MAIN_REPORT. Returns: The type of the object (Tifa type) or None if a type doesn't exist """ state = data_state(node, report=report) if state is not None: return state.type return None
[docs] def find_match(pattern, student_code=None, report=MAIN_REPORT, cut=False, use_previous=None): """ Apply Tree Inclusion and return the first match of the `pattern` in the `student_code`. Args: pattern (str): The CaitExpression to match against. student_code (str): The string of student code to check against. Defaults to the code of the Source tool in the Report. report (Report): The report to attach data to. cut (bool): Set to true to trim root to first branch use_previous (AstMap): If user wants to continue off of a previously found match Returns: CaitNode or None: The first matching node for the given pattern, or None if nothing was found. """ matches = find_matches(pattern=pattern, student_code=student_code, report=report, cut=cut, use_previous=use_previous) if matches: return matches[0] else: return None
[docs] def find_matches(pattern, student_code=None, cut=False, report=MAIN_REPORT, use_previous=None): """ Apply Tree Inclusion and return all matches of the `pattern` in the `student_code`. Args: pattern (str): The CaitExpression to match against. student_code (str): The string of student code to check against. report (Report): The report to attach data to. cut (bool): Set to true to trim root to first branch use_previous (AstMap): If user wants to continue off of a previously found match Returns: list[pedal.cait.ast_map.AstMap]: All matching nodes for the given pattern. """ cait_report = reparse_if_needed(student_code, report) if not cait_report['success']: return [] student_ast = cait_report['ast'] matcher = StretchyTreeMatcher(pattern, report=report) return matcher.find_matches(student_ast, use_previous=use_previous)
[docs] def find_submatches(pattern, student_code, is_mod=False, report=MAIN_REPORT): """ Incomplete. """ return find_expr_sub_matches(pattern, student_code, is_mod, report=report)
[docs] def find_expr_sub_matches(pattern, student_code, is_mod=False, report=MAIN_REPORT): """ Finds pattern in student_code # TODO: Add code to make pattern accept CaitNodes # TODO: Make this function without so much meta knowledge Args: pattern: the expression to find (str that MUST evaluate to a Module node with a single child or an AstNode) student_code: student subtree is_mod (bool): currently hack for multiline sub matches report: defaults to MAIN_REPORT unless another one exists Returns: a list of matches or False if no matches found """ is_node = isinstance(pattern, CaitNode) if not isinstance(pattern, str) and not is_node: raise TypeError("pattern expected str or CaitNode, found {0}".format(type(pattern))) matcher = StretchyTreeMatcher(pattern, report=report) if (not is_node and not is_mod) and len(matcher.root_node.children) != 1: raise ValueError("pattern does not evaluate to a singular statement") return matcher.find_matches(student_code, check_meta=False)
[docs] def find_asts(node_name: str, student_code=None, report=MAIN_REPORT): """ Find all occurrences of the given AST node, based on the name of the AST node (e.g., `"For"` or `"FunctionDef"`). Args: node_name: the string representing the "type" of node to look for student_code (str): Optionally, different code to parse and search. report: defaults to MAIN_REPORT unless another one is given. Returns: a list of Ast Nodes (cait_nodes) of self that are of the specified type (including self if self meets that criteria) Returns: """ cait_report = reparse_if_needed(student_code, report) if not cait_report['success']: return [] student_ast = cait_report['ast'] return student_ast.find_all(node_name)
[docs] def reset(report=MAIN_REPORT): """ Args: report: Returns: """ report[TOOL_NAME] = { 'success': True, 'error': None, 'ast': None, 'cache': {} } return report[TOOL_NAME]
Report.register_tool(TOOL_NAME, reset)