Skip to content

Commit 0bf23f8

Browse files
committed
Add REPL-style bare expression evaluation support with tests
Implements `_add_repl_print` helper to wrap bare expressions for REPL-style output in `demo` and `io_test` modes. Updates evaluation pipeline, adds unit tests, and expands Postman collection for REPL-specific cases.
1 parent f17d9ff commit 0bf23f8

3 files changed

Lines changed: 122 additions & 0 deletions

File tree

evaluation_function/evaluation.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ast
12
import json
23
import os
34
import shutil
@@ -76,6 +77,21 @@ def addSuccess(self, test):
7677
"""
7778

7879

80+
def _add_repl_print(code: str) -> str:
81+
"""Wrap last bare expression in print(repr(...)) for REPL-style output."""
82+
try:
83+
tree = ast.parse(code)
84+
except SyntaxError:
85+
return code
86+
if not tree.body or not isinstance(tree.body[-1], ast.Expr):
87+
return code
88+
node = tree.body[-1].value
89+
if (isinstance(node, ast.Call) and
90+
isinstance(node.func, ast.Name) and node.func.id == 'print'):
91+
return code
92+
return code + f"\nprint(repr({ast.unparse(node)}))"
93+
94+
7995
def _run_code(code: str, stdin: str) -> tuple[str, str, bool, list[Image.Image]]:
8096
plot_dir = tempfile.mkdtemp()
8197
preamble = _PREAMBLE_TEMPLATE.format(plot_dir=plot_dir)
@@ -122,6 +138,7 @@ def _upload_plots(images: list[Image.Image]) -> list[str]:
122138

123139

124140
def _evaluate_demo(response: str, result: Result) -> Result:
141+
response = _add_repl_print(response)
125142
stdout, stderr, timed_out, images = _run_code(response, "")
126143
if timed_out:
127144
result.add_feedback("error", f"Code timed out after {_TIMEOUT}s.")
@@ -136,6 +153,7 @@ def _evaluate_demo(response: str, result: Result) -> Result:
136153

137154
def _evaluate_io(response: str, tests: list, result: Result) -> Result:
138155
passed = 0
156+
response = _add_repl_print(response)
139157

140158
for i, test in enumerate(tests, 1):
141159
inject = test.get("inject")

evaluation_function/evaluation_test.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,28 @@ def test_inject_string_value(self):
109109
self.assertTrue(result["is_correct"])
110110

111111

112+
class TestReplExpression(unittest.TestCase):
113+
114+
def test_bare_expression_demo(self):
115+
result = evaluation_function("3.14159*2*5", None, {"mode": "demo"}).to_dict()
116+
self.assertIn("31.4159", result["feedback"])
117+
118+
def test_bare_expression_io_test(self):
119+
params = _params(_test("", "31.4159\n"))
120+
result = evaluation_function("3.14159*2*5", None, params).to_dict()
121+
self.assertTrue(result["is_correct"])
122+
123+
def test_existing_print_not_double_wrapped(self):
124+
params = _params(_test("", "31.4159\n"))
125+
result = evaluation_function("print(31.4159)", None, params).to_dict()
126+
self.assertTrue(result["is_correct"])
127+
128+
def test_assignment_no_auto_print(self):
129+
params = _params(_test("", "5\n"))
130+
result = evaluation_function("x = 5", None, params).to_dict()
131+
self.assertFalse(result["is_correct"])
132+
133+
112134
_PLOT_CODE = "import matplotlib.pyplot as plt\nplt.plot([1, 2, 3])\n"
113135
_MULTI_PLOT_CODE = (
114136
"import matplotlib.pyplot as plt\n"

postman/evaluatePython.postman_collection.json

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,88 @@
785785
}
786786
]
787787
},
788+
{
789+
"name": "26 – REPL expression in demo mode",
790+
"request": {
791+
"method": "POST",
792+
"header": [
793+
{ "key": "Content-Type", "value": "application/json" }
794+
],
795+
"body": {
796+
"mode": "raw",
797+
"raw": "{\n \"response\": \"3.14159*2*5\",\n \"answer\": \"\",\n \"params\": {\n \"mode\": \"demo\"\n }\n}",
798+
"options": { "raw": { "language": "json" } }
799+
},
800+
"url": { "raw": "{{baseUrl}}", "host": ["{{baseUrl}}"] }
801+
},
802+
"event": [
803+
{
804+
"listen": "prerequest",
805+
"script": {
806+
"type": "text/javascript",
807+
"exec": ["pm.request.timeout = 40000;"]
808+
}
809+
},
810+
{
811+
"listen": "test",
812+
"script": {
813+
"type": "text/javascript",
814+
"exec": [
815+
"pm.test('Status 200', () => {",
816+
" pm.expect(pm.response.code, 'Body: ' + pm.response.text()).to.equal(200);",
817+
"});",
818+
"",
819+
"pm.test('Bare expression output is auto-displayed (REPL style)', () => {",
820+
" pm.expect(pm.response.json().result.feedback).to.include('31.4159');",
821+
"});"
822+
]
823+
}
824+
}
825+
]
826+
},
827+
{
828+
"name": "27 – REPL expression in io_test mode",
829+
"request": {
830+
"method": "POST",
831+
"header": [
832+
{ "key": "Content-Type", "value": "application/json" }
833+
],
834+
"body": {
835+
"mode": "raw",
836+
"raw": "{\n \"response\": \"3.14159*2*5\",\n \"answer\": \"\",\n \"params\": {\n \"mode\": \"io_test\",\n \"tests\": [\n { \"input\": \"\", \"expected_output\": \"31.4159\\n\", \"hidden\": false }\n ]\n }\n}",
837+
"options": { "raw": { "language": "json" } }
838+
},
839+
"url": { "raw": "{{baseUrl}}", "host": ["{{baseUrl}}"] }
840+
},
841+
"event": [
842+
{
843+
"listen": "prerequest",
844+
"script": {
845+
"type": "text/javascript",
846+
"exec": ["pm.request.timeout = 40000;"]
847+
}
848+
},
849+
{
850+
"listen": "test",
851+
"script": {
852+
"type": "text/javascript",
853+
"exec": [
854+
"pm.test('Status 200', () => {",
855+
" pm.expect(pm.response.code, 'Body: ' + pm.response.text()).to.equal(200);",
856+
"});",
857+
"",
858+
"pm.test('is_correct is true', () => {",
859+
" pm.expect(pm.response.json().result.is_correct).to.be.true;",
860+
"});",
861+
"",
862+
"pm.test('Feedback reports 1/1 tests passed', () => {",
863+
" pm.expect(pm.response.json().result.feedback).to.include('1/1 tests passed');",
864+
"});"
865+
]
866+
}
867+
}
868+
]
869+
},
788870
{
789871
"name": "20 – Image generation (demo mode)",
790872
"request": {

0 commit comments

Comments
 (0)