From 4ed42140130bff48bc83ab485bca5a352238f2f0 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Wed, 11 Nov 2026 00:54:51 -0500 Subject: [PATCH] Allow common builtins such as sum and super by default Correct solutions that use sum, any, all, super and similar builtins failed with a NameError unless the question whitelisted them. These builtins don't give student code anything that plain Python syntax doesn't. Co-Authored-By: Claude Opus 5.5 --- src/pytest_prairielearn_grader/utils.py | 23 ++++++++++ .../test_default_builtins/__init__.py | 0 .../expected_outcome.json | 34 ++++++++++++++ .../test_default_builtins/scenario.py | 34 ++++++++++++++ .../test_default_builtins/student_code.py | 46 +++++++++++++++++++ .../helper_function.py | 2 +- 6 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 tests/scenario_root/test_default_builtins/__init__.py create mode 100644 tests/scenario_root/test_default_builtins/expected_outcome.json create mode 100644 tests/scenario_root/test_default_builtins/scenario.py create mode 100644 tests/scenario_root/test_default_builtins/student_code.py diff --git a/src/pytest_prairielearn_grader/utils.py b/src/pytest_prairielearn_grader/utils.py index cda4c3e..a0b0827 100644 --- a/src/pytest_prairielearn_grader/utils.py +++ b/src/pytest_prairielearn_grader/utils.py @@ -257,6 +257,29 @@ def get_builtins(builtin_whitelist: list[str] | None) -> dict[str, Any]: "min", "max", "map", + # Not in RestrictedPython's list, but they don't give student code anything that plain Python + # syntax doesn't. Courses commonly use them, and without them, correct solutions fail with a + # NameError. + "all", + "any", + "ascii", + "bin", + "bytearray", + "classmethod", + "Ellipsis", + "filter", + "format", + "frozenset", + "iter", + "next", + "NotImplemented", + "object", + "property", + "reversed", + "staticmethod", + "sum", + "super", + "type", ) _safe_exceptions = ( diff --git a/tests/scenario_root/test_default_builtins/__init__.py b/tests/scenario_root/test_default_builtins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/scenario_root/test_default_builtins/expected_outcome.json b/tests/scenario_root/test_default_builtins/expected_outcome.json new file mode 100644 index 0000000..3047299 --- /dev/null +++ b/tests/scenario_root/test_default_builtins/expected_outcome.json @@ -0,0 +1,34 @@ +{ + "expected_data_object": { + "score": 1.0, + "tests": [ + { + "max_points": 1, + "message": "", + "name": "common builtins", + "outcome": "passed", + "points": 1.0, + "points_frac": 1.0, + "test_id": "test_default_builtins.py::test_common_builtins[student_code]" + }, + { + "max_points": 1, + "message": "", + "name": "classes", + "outcome": "passed", + "points": 1.0, + "points_frac": 1.0, + "test_id": "test_default_builtins.py::test_classes[student_code]" + }, + { + "max_points": 1, + "message": "", + "name": "dangerous builtins stay blocked", + "outcome": "passed", + "points": 1.0, + "points_frac": 1.0, + "test_id": "test_default_builtins.py::test_open_blocked[student_code]" + } + ] + } +} diff --git a/tests/scenario_root/test_default_builtins/scenario.py b/tests/scenario_root/test_default_builtins/scenario.py new file mode 100644 index 0000000..9c1e1a0 --- /dev/null +++ b/tests/scenario_root/test_default_builtins/scenario.py @@ -0,0 +1,34 @@ +"""Commonly used builtins are available to student code without a builtin_whitelist.""" + +import pytest + +from pytest_prairielearn_grader.fixture import StudentFixture + + +@pytest.mark.grading_data(name="common builtins", points=1) +def test_common_builtins(sandbox: StudentFixture) -> None: + assert sandbox.query_function("common_builtins", [3, 1, 2]) == [ + 6, + True, + True, + [2, 1, 3], + [3, 1], + 3, + [1, 2, 3], + "3.14", + "0b101", + "'\\xe9'", + "list", + True, + ] + + +@pytest.mark.grading_data(name="classes", points=1) +def test_classes(sandbox: StudentFixture) -> None: + assert sandbox.query_function("classes") == ["square!", 0, "circle"] + + +@pytest.mark.grading_data(name="dangerous builtins stay blocked", points=1) +def test_open_blocked(sandbox: StudentFixture) -> None: + response = sandbox.query_function_raw("call_open") + assert response["exception_name"] == "NameError" diff --git a/tests/scenario_root/test_default_builtins/student_code.py b/tests/scenario_root/test_default_builtins/student_code.py new file mode 100644 index 0000000..150ad4f --- /dev/null +++ b/tests/scenario_root/test_default_builtins/student_code.py @@ -0,0 +1,46 @@ +class Shape: + def __init__(self, name): + self.name = name + + @property + def label(self): + return f"{self.name}!" + + @staticmethod + def sides(): + return 0 + + @classmethod + def make(cls, name): + return cls(name) + + +class Square(Shape): + def __init__(self): + super().__init__("square") + + +def common_builtins(values): + return [ + sum(values), + any(v > 2 for v in values), + all(v > 0 for v in values), + list(reversed(values)), + list(filter(lambda v: v % 2, values)), + next(iter(values)), + sorted(frozenset(values)), + format(3.14159, ".2f"), + bin(5), + ascii("é"), + type(values).__name__, + isinstance(object(), object), + ] + + +def classes(): + square = Square() + return [square.label, Square.sides(), Shape.make("circle").name] + + +def call_open(): + return open("data.txt").read() diff --git a/tests/scenario_root/test_helper_modules_workspace/helper_function.py b/tests/scenario_root/test_helper_modules_workspace/helper_function.py index db7b74a..17d9675 100644 --- a/tests/scenario_root/test_helper_modules_workspace/helper_function.py +++ b/tests/scenario_root/test_helper_modules_workspace/helper_function.py @@ -6,4 +6,4 @@ def triple(x): def total(values): - return sum(values) + return vars(values)