"""
.. 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