"""
Representation of a student's submission to pedal. Almost certainly contains their code,
but may also contain other metadata.
TODO: Normalize the concept of evaluations ("<stdin>" or "evaluations").
get_program(filename='<stdin>') => submission.files['<stdin>']
get_evaluation()
"""
__all__ = ['Submission']
from pedal.utilities.files import get_extension
DEFAULT_EXTENSION = "py"
def python_submission_parser(code: str, filename: str) -> (str, str):
return code, filename
def create_ipynb_submission_parser(metadata_filter=None):
def ipynb_submission_parser(code: str, filename: str) -> (str, str):
# TODO: What happens if we have a syntax error in the file?
# TODO: Consider making a fallback JSON parser, since the structure is known.
try:
import nbformat
except ImportError:
raise ImportError("You have attempted to read an IPYNB file, but did not install `nbformat`. Is Jupyter installed?")
nb = nbformat.reads(code, as_version=nbformat.current_nbformat)
result = []
for cell in nb.cells:
if metadata_filter is not None and not metadata_filter(cell, nb.cells):
continue
if cell.cell_type == 'markdown':
result.append(f'"""{cell.source}"""')
elif cell.cell_type == 'code':
result.append(cell.source)
return "\n".join(result), filename
return ipynb_submission_parser
[docs]
class Submission:
"""
Simple class for holding information about the student's submission.
Examples:
A very simple example of creating a Submission with a single file::
>>> Submission({'answer.py': "a = 0"})
Attributes:
files (`dict` mapping `str` to `str`): Dictionary of filenames mapped to their contents, emulating
a file system.
main_file (str): The entry point file that will be considered the main file.
main_code (str): The actual code to run; if None, then defaults to the code of the main file.
Useful for tools that want to change the currently active code (e.g., Source's sections) or
run additional commands (e.g., Sandboxes' call).
user (dict): Additional information about the user.
assignment (dict): Additional information about the assignment.
course (dict): Additional information about the course.
execution (dict): Additional information about the results of executing the students' code.
"""
PARSERS = {
"py": python_submission_parser,
"ipynb": create_ipynb_submission_parser()
}
def __init__(self, files=None, main_file='answer.py', main_code=None,
user=None, assignment=None, course=None, execution=None,
instructor_file='instructor_tests.py', load_error=None):
if files is None:
files = {}
self.files = files
self.main_file = main_file
self.load_error = load_error
if main_code is not None:
self.main_code = main_code
if not self.main_code:
self.main_code = ""
if self.main_file not in self.files:
self.main_code, self.main_file = self._preprocess_file(self.main_code, self.main_file)
self.files[self.main_file] = self.main_code
else:
self.files[self.main_file], self.main_file = self._preprocess_file(self.files[self.main_file], self.main_file)
# TODO: Non-main files are not going to be pre-processed; is that a problem for anyone?
self._original_main_code = self.main_code
self.user = user
self.assignment = assignment
self.course = course
self.execution = execution
self.instructor_file = instructor_file
self.line_offsets = {}
self._lines_cache = {}
[docs]
def get_lines(self, filename=None):
"""
Retrieves the lines of code from this submission.
Returns:
list[str]: The lines of code for this submission.
"""
if filename is None:
code = self.main_code
else:
code = self.files[filename]
if code not in self._lines_cache:
self._lines_cache[code] = code.split("\n")
return self._lines_cache[code]
[docs]
def get_files_lines(self):
""" Retrieves a dictionary of lists of strings representing the files'
lines of code. """
return {filename: self.get_lines(filename) for filename in self.files}
[docs]
def replace_main(self, code: str, file: str = None):
"""
Substitutes the current main code and filename with the given arguments.
Args:
code (str): The new code to substitute in.
file (str): An optional filename to use.
"""
self.main_code = code
if file is not None:
self.main_file = file
code, self.main_file = self._preprocess_file(code, self.main_file)
self.files[self.main_file] = code
def _preprocess_file(self, code, filename):
"""
Give the system a chance to potentially preprocess the given file, based on its extension.
For example, to turn an IPYNB file into more conventional python code.
Args:
code:
filename:
Returns:
"""
extension = get_extension(filename, DEFAULT_EXTENSION)
parser = self.PARSERS.get(extension, python_submission_parser)
return parser(code, filename)
@property
def main_code(self):
return self.files.get(self.main_file, "")
@main_code.setter
def main_code(self, code):
self.files[self.main_file] = code
[docs]
def set_line_offset(self, lineno, filename=None):
""" Sets the line offset for the given filename. Defaults to main
file."""
if filename is None:
filename = self.main_file
self.line_offsets[filename] = lineno
def clear_line_offsets(self):
self.line_offsets.clear()
def to_json(self):
return dict(
user=self.user,
assignment=self.assignment,
course=self.course,
execution=self.execution,
files=self.files.copy()
)