Source code for pedal.source.source

"""

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

    "substitutions", "List[Substitution]", "[]", "The history of previous source codes before the latest substitutions."
    "success", "bool", "None", "Whether the current file has been parsed successfully."
    "ast", "ast.Ast", "None", "The root node of the latest successfully parsed chunk of code."

"""
import sys
import ast

from pedal.core.feedback import CompositeFeedbackFunction
from pedal.core.report import Report, MAIN_REPORT
from pedal.core.submission import Submission
from pedal.source.constants import TOOL_NAME, DEFAULT_STUDENT_FILENAME
from pedal.source.sections import separate_into_sections
from pedal.source.feedbacks import blank_source, syntax_error, source_file_not_found, indentation_error
from pedal.source.substitutions import Substitution
from pedal.utilities.files import find_possible_filenames


[docs] def reset(report=MAIN_REPORT): """ Resets the Source tool back to its initial configuration, removing any previously stored code/parses/section info. Args: report: The report object to be reseting. Returns: dict: The data associated with the Source tool. """ report[TOOL_NAME] = { 'substitutions': [], 'success': None, 'ast': None, 'independent': None, 'sections': None, 'section': None, 'section_pattern': None, } return report[TOOL_NAME]
Report.register_tool(TOOL_NAME, reset)
[docs] def set_source(code, filename=DEFAULT_STUDENT_FILENAME, sections=False, independent=False, report=MAIN_REPORT): """ Sets the contents of the Source to be the given code. Can also be optionally given a filename. If there is no existing Submission, this contextualizes the Report with a new Submission containing the given code. Otherwise, it creates a Substitution and temporarily replaces the Submission's current main code. Args: code (str): The contents of the source file. filename (str): The filename of the students' code. Defaults to `answer.py`. sections (str or bool): Whether the file should be divided into sections. If a str, then it should be a Python regular expression for how the sections are separated. If False, there will be no sections. If True, then the default pattern will be used: '^##### Part (\\d+)$' independent (bool): Whether the separate sections should be considered separate or all existing in an accumulating namespace. report (Report): The report object to store data and feedback in. If left None, defaults to the global MAIN_REPORT. """ if report.submission is None: report.contextualize(Submission({filename: code}, filename, code)) else: backup = Substitution(report.submission.main_code, report.submission.main_file) report[TOOL_NAME]['substitutions'].append(backup) report.submission.replace_main(code, filename) report[TOOL_NAME]['independent'] = independent report[TOOL_NAME]['success'] = True if not sections: report[TOOL_NAME]['sections'] = None report[TOOL_NAME]['section'] = None verify(code, report=report) else: separate_into_sections(report=report)
# TODO: source_prepend and source_append
[docs] def restore_code(report=MAIN_REPORT): """ Args: report: """ if TOOL_NAME in report: old_submission = report[TOOL_NAME]['substitutions'].pop() report.submission.replace_main(old_submission.code, old_submission.filename) verify(report=report)
[docs] @CompositeFeedbackFunction(blank_source, syntax_error, indentation_error, source_file_not_found) def verify(code=None, filename=DEFAULT_STUDENT_FILENAME, report=MAIN_REPORT, muted=False, enhance=True): """ Parses the given source code and checks for syntax errors; if no code is given, defaults to the current Main file of the submission. Args: enhance (bool): Whether to use native Python error messages or the Pedal enhanced ones. muted: Whether this Feedback should be considered for showing to the student. code (str): Some code to parse and syntax check. filename (str): An optional filename to use report: Returns: """ if code is None: code = report.submission.main_code filename = report.submission.main_file if report.submission.load_error: source_file_not_found(filename, None, enhance=enhance, report=report, muted=muted) report[TOOL_NAME]['success'] = False return False if code.strip() == '': blank_source(enhance=enhance, report=report, muted=muted) report[TOOL_NAME]['success'] = False try: parsed = ast.parse(code, filename) report[TOOL_NAME]['ast'] = parsed except IndentationError as e: indentation_error(e.lineno, e.filename, code, e.offset, e, sys.exc_info(), report=report, muted=muted, enhance=enhance) report[TOOL_NAME]['success'] = False report[TOOL_NAME]['ast'] = ast.parse("") except SyntaxError as e: syntax_error(e.lineno, e.filename, code, e.offset, e, sys.exc_info(), report=report, muted=muted, enhance=enhance) report[TOOL_NAME]['success'] = False report[TOOL_NAME]['ast'] = ast.parse("") else: report[TOOL_NAME]['success'] = True return report[TOOL_NAME]['success']
# Legacy verify_section; now done by verify since its aware of sections verify_section = verify
[docs] def get_program(report=MAIN_REPORT) -> str: """ Retrieves the current main file's code. """ return report.submission.main_code
[docs] def get_original_program(report=MAIN_REPORT) -> str: """ Retrieves the original version of the Submission's main code, ignoring all subsitutions. Args: report: Returns: """ return report[TOOL_NAME]['substitutions'][0].code
[docs] @CompositeFeedbackFunction(source_file_not_found) def set_source_file(filename: str, sections=False, independent=False, report=MAIN_REPORT): """ Uses the given `filename` on the filesystem as the new main file. Args: filename (str or list[str]): Checks the files, in the given order, to be loaded. If a single string is given, we check if there is a ";" and separate it as multiple options. If a file isn't found, then the next option in the list is tried. If "*.py" is the ending of a given option, then the first Python file found will be used, respecting any directories given (e.g., `"*.py"` finds any in this directory, while `"source/*.py"` finds the first Python file in the `source/` directory). If no files are found, the `source_file_not_found` feedback will be delivered. sections: independent: report: Returns: """ for a_filename in find_possible_filenames(filename): try: with open(a_filename, 'r') as student_file: set_source(student_file.read(), filename=a_filename, sections=sections, independent=independent, report=report) return except IOError: continue else: source_file_not_found(filename, sections, report=report) report[TOOL_NAME]['success'] = False