From 08d6c83ec8d8897648add7f6b74f84f7dfd1ac16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:36:50 +0000 Subject: [PATCH 1/2] Initial plan From 084d5fc4946b13ee553943473cc9a42f56e099d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:50:37 +0000 Subject: [PATCH 2/2] Add source code context to assertion failures Co-authored-by: acbart <897227+acbart@users.noreply.github.com> --- pedal/assertions/feedbacks.py | 36 +++++++++++ pedal/utilities/source_context.py | 104 ++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 pedal/utilities/source_context.py diff --git a/pedal/assertions/feedbacks.py b/pedal/assertions/feedbacks.py index 92e1fcd6..237f4213 100644 --- a/pedal/assertions/feedbacks.py +++ b/pedal/assertions/feedbacks.py @@ -121,6 +121,7 @@ class RuntimeAssertionFeedback(AssertionFeedback): message_template = ("Student code failed instructor test.\n" "{context_message}" "{assertion_message}" + "{source_context}" "{explanation}") # TODO: The explanation field is broken, because it's not a keyword parameter _expected_verb: str @@ -149,6 +150,8 @@ def __init__(self, left, right, *args, **kwargs): assertion_message = self.format_assertion(left, right, contexts) # Calculate explanation explanation = kwargs.get("explanation", "") + # Calculate source context + source_context = self._get_source_context(contexts) # Add in new fields fields = kwargs.setdefault('fields', {}) fields['left'] = left.value @@ -161,6 +164,7 @@ def __init__(self, left, right, *args, **kwargs): fields['inverse_operator'] = self._inverse_operator fields['context_message'] = context_message fields['assertion_message'] = assertion_message + fields['source_context'] = source_context fields['explanation'] = explanation try: @@ -274,6 +278,38 @@ def suppress_runtime_error(self, exception): if hasattr(exception, "feedback") and exception.feedback is not None: exception.feedback.parent = self + def _get_source_context(self, contexts): + """ + Generate source code context for function calls in assertion failures. + + Args: + contexts (list): List of sandbox contexts from assertion + + Returns: + str: Formatted source context or empty string + """ + from pedal.utilities.source_context import format_source_context_for_function + + if not contexts: + return "" + + # Look for function calls in the contexts + for context_list in contexts: + if isinstance(context_list, list): + for context in context_list: + if hasattr(context, 'called') and context.called: + function_context = format_source_context_for_function(context.called, self.report) + if function_context: + return "\n" + function_context + "\n" + else: + # Handle case where context is not a list + if hasattr(context_list, 'called') and context_list.called: + function_context = format_source_context_for_function(context_list.called, self.report) + if function_context: + return "\n" + function_context + "\n" + + return "" + class RuntimePrintingAssertionFeedback(RuntimeAssertionFeedback): """ Variant for handling printing instead of return value""" diff --git a/pedal/utilities/source_context.py b/pedal/utilities/source_context.py new file mode 100644 index 00000000..5fe6ccf8 --- /dev/null +++ b/pedal/utilities/source_context.py @@ -0,0 +1,104 @@ +""" +Utilities for extracting source code context from student submissions. +""" + +import ast +from pedal.core.report import MAIN_REPORT +from pedal.core.location import Location + + +def find_function_definition(function_name, report=MAIN_REPORT): + """ + Find the definition location of a function in the student's source code. + + Args: + function_name (str): The name of the function to find. + report (Report): The report containing the source AST. + + Returns: + Location or None: The location of the function definition, or None if not found. + """ + if 'source' not in report or not report['source']['ast']: + return None + + student_ast = report['source']['ast'] + + for node in ast.walk(student_ast): + if isinstance(node, ast.FunctionDef) and node.name == function_name: + return Location(line=node.lineno, col=node.col_offset, + end_line=getattr(node, 'end_lineno', None), + end_col=getattr(node, 'end_col_offset', None)) + + return None + + +def get_source_line_context(location, report=MAIN_REPORT, context_lines=0): + """ + Get source code lines around a given location. + + Args: + location (Location): The location to get context for. + report (Report): The report containing the submission. + context_lines (int): Number of lines before/after to include (0 = just the target line). + + Returns: + str or None: The source code context, or None if not available. + """ + if not location: + return None + + if 'source' not in report: + return None + + # Get the main submission + submission = report.submission + if not submission or not submission.main_file or not submission.files: + return None + + # Get the source code + main_file_key = submission.main_file + if main_file_key not in submission.files: + return None + + source_code = submission.files[main_file_key] + if not source_code: + return None + + lines = source_code.splitlines() + + # Convert to 0-based indexing + target_line = location.line - 1 + start_line = max(0, target_line - context_lines) + end_line = min(len(lines), target_line + context_lines + 1) + + if target_line >= len(lines): + return None + + context_lines_list = lines[start_line:end_line] + return "\n".join(context_lines_list) + + +def format_source_context_for_function(function_name, report=MAIN_REPORT): + """ + Get formatted source context for a function definition. + + Args: + function_name (str): The name of the function. + report (Report): The report to use. + + Returns: + str or None: Formatted context string, or None if not available. + """ + location = find_function_definition(function_name, report) + if not location: + return None + + context = get_source_line_context(location, report, context_lines=0) + if not context: + return None + + formatter = report.format if hasattr(report, 'format') else None + if formatter and hasattr(formatter, 'line') and hasattr(formatter, 'python_code'): + return f"In your function {formatter.name(function_name)} on line {formatter.line(location.line)}:\n{formatter.python_code(context)}" + else: + return f"In your function '{function_name}' on line {location.line}:\n{context}" \ No newline at end of file