From dd2a8231d614fbdd0d26022870fc8025ec89bae2 Mon Sep 17 00:00:00 2001 From: Heyuan Zeng Date: Tue, 3 Oct 2023 20:15:16 +0100 Subject: [PATCH 1/9] fix: environment value loading error --- src/aga/loader.py | 11 ++++++----- src/aga/runner.py | 8 ++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/aga/loader.py b/src/aga/loader.py index b32dd11..96b383d 100644 --- a/src/aga/loader.py +++ b/src/aga/loader.py @@ -44,6 +44,10 @@ class MultipleScripts(InvalidSubmissionError): """Too many scripts were found.""" +class EnvironmentContextError(InvalidSubmissionError): + """The submission file does not contain the values required by the environment.""" + + def _get_spec_from_path(path: str, name: str) -> ModuleSpec: """Get the spec of the module at path.""" spec = importlib.util.spec_from_file_location(name, path) @@ -132,11 +136,8 @@ def _load_from_module_by( def _load_problems_from_module(module: ModuleType) -> Iterable[Problem[Any, Any]]: """Return all problems in the module.""" - yield from ( - prob.update_env_from(module) - for prob in _load_from_module_by( - lambda i: isinstance(i, Problem), module # type: ignore - ) + yield from _load_from_module_by( + lambda i: isinstance(i, Problem), module # type: ignore ) diff --git a/src/aga/runner.py b/src/aga/runner.py index 4d1c86c..bfaef68 100644 --- a/src/aga/runner.py +++ b/src/aga/runner.py @@ -23,6 +23,8 @@ TooManyMatchingSymbols, load_script_from_path, load_symbol_from_path, + _load_source_from_file, + EnvironmentContextError, ) from .score import ScoredPrize from .util import limited_traceback @@ -312,6 +314,12 @@ def load_and_run( score=0.0, ) + try: + submission_mod = _load_source_from_file(path) + problem.update_env_from(submission_mod) + except Exception as e: + raise EnvironmentContextError from e + suite, prizes = problem.generate_test_suite(under_test, metadata) return _run(suite, prizes, metadata) From 92fc9feb88a2376b8ad0c6444bd41c97a3a39572 Mon Sep 17 00:00:00 2001 From: Heyuan Zeng Date: Tue, 3 Oct 2023 21:17:32 +0100 Subject: [PATCH 2/9] fix: change to iterative loading --- src/aga/core/{environment.py => context.py} | 15 +++++------ src/aga/core/problem.py | 29 +++++++++------------ src/aga/core/suite.py | 6 ++--- src/aga/loader.py | 2 +- src/aga/runner.py | 15 +++++------ 5 files changed, 29 insertions(+), 38 deletions(-) rename src/aga/core/{environment.py => context.py} (59%) diff --git a/src/aga/core/environment.py b/src/aga/core/context.py similarity index 59% rename from src/aga/core/environment.py rename to src/aga/core/context.py index 7abb622..fe6bfbf 100644 --- a/src/aga/core/environment.py +++ b/src/aga/core/context.py @@ -1,11 +1,10 @@ """Environment value wrapper.""" from __future__ import annotations -import types from typing import Iterable, Any -class Environment(dict[str, Any]): +class SubmissionContext(dict[str, Any]): """Environment value wrapper.""" def __init__(self, env_targets: Iterable[str]) -> None: @@ -13,14 +12,12 @@ def __init__(self, env_targets: Iterable[str]) -> None: for target in env_targets: self[target] = None - def update_from_module(self, mod: types.ModuleType) -> None: + def update_from_path(self, path: str) -> None: """Update environment values from a given module.""" - for value_name in self.keys(): - if value_name not in vars(mod): - raise ValueError( - f"The variable `{value_name} does not exist in the module {mod}" - ) - self[value_name] = getattr(mod, value_name) + from ..loader import load_symbol_from_path + + for symbol in self.keys(): + self[symbol] = load_symbol_from_path(path, symbol) def __getattr__(self, item: str) -> Any: """Get the value of an environment variable.""" diff --git a/src/aga/core/problem.py b/src/aga/core/problem.py index 7858c5c..57bd948 100644 --- a/src/aga/core/problem.py +++ b/src/aga/core/problem.py @@ -14,7 +14,7 @@ from ..config import AgaConfig from ..score import Prize, ScoredPrize, compute_scores -from .environment import Environment +from .context import SubmissionContext from .suite import AgaTestSuite, SubmissionMetadata, _TestInputGroup, _TestInputs # pylint: disable=invalid-name @@ -39,7 +39,7 @@ def __init__( name: str, config: AgaConfig, is_script: bool, - env_targets: Iterable[str] = (), + ctx_targets: Iterable[str] = (), ) -> None: self._golden: Callable[ProblemParamSpec, ProblemOutputType] = golden self._name = name @@ -47,7 +47,7 @@ def __init__( self._ungrouped_prizes: list[Prize] = [] self._ungrouped_tests: list[_TestInputs[ProblemOutputType]] = [] self._groups: list[_TestInputGroup[ProblemOutputType]] = [] - self._environment: Environment = Environment(env_targets) + self._submission_context: SubmissionContext = SubmissionContext(ctx_targets) self.is_script = is_script def add_test_case(self, param: _TestParam) -> None: @@ -57,7 +57,9 @@ def add_test_case(self, param: _TestParam) -> None: does _not_ produce a test of the golden solution. """ case: _TestInputs[ProblemOutputType] = _TestInputs( - param, mock_input=self._config.problem.mock_input, env=self.environment + param, + mock_input=self._config.problem.mock_input, + ctx=self.submission_context, ) self._ungrouped_tests.append(case) @@ -143,16 +145,9 @@ def name(self) -> str: return self._name @property - def environment(self) -> Environment: + def submission_context(self) -> SubmissionContext: """The environment values captured from the problem module.""" - return self._environment - - def update_env_from( - self, mod: ModuleType - ) -> Problem[ProblemParamSpec, ProblemOutputType]: - """Update the environment values from a given module.""" - self.environment.update_from_module(mod) - return self + return self._submission_context def expected_symbol(self) -> str: """Get the name of the symbol that should be tested against.""" @@ -194,7 +189,7 @@ def problem( script: bool = False, check_stdout: Optional[bool] = None, mock_input: Optional[bool] = None, - envs: Iterable[str] = (), + ctx: Iterable[str] = (), ) -> Callable[ [Callable[ProblemParamSpec, ProblemOutputType]], Problem[ProblemParamSpec, ProblemOutputType], @@ -221,8 +216,8 @@ def problem( Overrides the `problem.mock_input` configuration option. If True, test cases for this problem will be interpreted as mocked outputs of `builtins.input`, rather than inputs to the function. - envs : Iterable[str] - The environment values to be captured + ctx: Iterable[str] + The context values required in the submission and will be captured Returns ------- @@ -253,7 +248,7 @@ def outer( config.problem.mock_input = True config.problem.mock_input_overridden = True - return Problem(func, problem_name, config, script, env_targets=envs) + return Problem(func, problem_name, config, script, ctx) return outer diff --git a/src/aga/core/suite.py b/src/aga/core/suite.py index 57a3936..d35375c 100644 --- a/src/aga/core/suite.py +++ b/src/aga/core/suite.py @@ -9,7 +9,7 @@ from unittest import TestCase, TestSuite from unittest.mock import patch -from aga.core.environment import Environment +from aga.core.context import SubmissionContext from ..config import AgaConfig, AgaTestConfig from ..score import Prize, ScoredPrize, ScoreInfo, compute_scores @@ -154,11 +154,11 @@ def __init__( self, aga_param: _TestParam, mock_input: bool, - env: Environment | None = None, + ctx: SubmissionContext | None = None, ) -> None: super().__init__() self._mock_input = mock_input - self.env = env + self.ctx = ctx self._param: _TestParam = aga_param self.score_info = ScoreInfo( self.aga_kwargs.weight, self.aga_kwargs.value, self.aga_kwargs.extra_credit diff --git a/src/aga/loader.py b/src/aga/loader.py index 96b383d..8e175bb 100644 --- a/src/aga/loader.py +++ b/src/aga/loader.py @@ -44,7 +44,7 @@ class MultipleScripts(InvalidSubmissionError): """Too many scripts were found.""" -class EnvironmentContextError(InvalidSubmissionError): +class ContextMissing(InvalidSubmissionError): """The submission file does not contain the values required by the environment.""" diff --git a/src/aga/runner.py b/src/aga/runner.py index bfaef68..c52b6b5 100644 --- a/src/aga/runner.py +++ b/src/aga/runner.py @@ -7,11 +7,14 @@ For convenience, it also provides the `load_and_run` method, which loads a student submission and then runs it. """ - +import os.path from dataclasses import dataclass +from os.path import isdir from typing import Any, Literal, Optional, TypeVar from unittest import TestResult +import typer + from .config import AgaConfig from .core import AgaTestCase, AgaTestSuite, Problem, SubmissionMetadata from .core.problem import ProblemOutputType, ProblemParamSpec @@ -24,7 +27,7 @@ load_script_from_path, load_symbol_from_path, _load_source_from_file, - EnvironmentContextError, + ContextMissing, ) from .score import ScoredPrize from .util import limited_traceback @@ -279,6 +282,8 @@ def load_and_run( try: if not problem.is_script: under_test = load_symbol_from_path(path, problem.expected_symbol()) + problem.submission_context.update_from_path(path) + else: under_test = load_script_from_path(path) except SubmissionSyntaxError as err: @@ -314,12 +319,6 @@ def load_and_run( score=0.0, ) - try: - submission_mod = _load_source_from_file(path) - problem.update_env_from(submission_mod) - except Exception as e: - raise EnvironmentContextError from e - suite, prizes = problem.generate_test_suite(under_test, metadata) return _run(suite, prizes, metadata) From 763d2f00b181e1a331d06a08a4c3827e2cc948ac Mon Sep 17 00:00:00 2001 From: Heyuan Zeng Date: Tue, 3 Oct 2023 22:12:23 +0100 Subject: [PATCH 3/9] fix: duplicated symbols due to pycache and testing --- src/aga/loader.py | 8 ++++++-- src/aga/runner.py | 7 +++++-- tests/conftest.py | 33 +++++++++++++++++++++++++++++- tests/test_gradescope/test_main.py | 16 +++++++++++++++ 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/aga/loader.py b/src/aga/loader.py index 8e175bb..a4ceabb 100644 --- a/src/aga/loader.py +++ b/src/aga/loader.py @@ -157,6 +157,10 @@ def _load_symbol_from_dir(path: str, symbol: str) -> Any: """Load a specific symbol from any of the source files in a directory.""" matching_symbols = [] for file in os.listdir(path): + # ignore the pycache folder to avoid duplicated symbols + if file == "__pycache__": + continue + try: file_path = pathjoin(path, file) matching_symbols.append(load_symbol_from_path(file_path, symbol)) @@ -164,9 +168,9 @@ def _load_symbol_from_dir(path: str, symbol: str) -> Any: pass if len(matching_symbols) > 1: - raise TooManyMatchingSymbols + raise TooManyMatchingSymbols(f"Multiple matching symbols {symbol} found.") if len(matching_symbols) == 0: - raise NoMatchingSymbol + raise NoMatchingSymbol(f"No matching symbol {symbol} found.") return matching_symbols[0] diff --git a/src/aga/runner.py b/src/aga/runner.py index c52b6b5..9c38713 100644 --- a/src/aga/runner.py +++ b/src/aga/runner.py @@ -282,8 +282,6 @@ def load_and_run( try: if not problem.is_script: under_test = load_symbol_from_path(path, problem.expected_symbol()) - problem.submission_context.update_from_path(path) - else: under_test = load_script_from_path(path) except SubmissionSyntaxError as err: @@ -319,6 +317,11 @@ def load_and_run( score=0.0, ) + if not problem.is_script: + # If the submission is a module, we need to update the required context + # with the values from the submission. + problem.submission_context.update_from_path(path) + suite, prizes = problem.generate_test_suite(under_test, metadata) return _run(suite, prizes, metadata) diff --git a/tests/conftest.py b/tests/conftest.py index 50ab348..aa406c7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ from os.path import join as pathjoin from pathlib import Path from shutil import copyfileobj -from typing import Callable, Generator, Iterable, Iterator, List +from typing import Callable, Generator, Iterable, Iterator, List, Any from unittest import TestCase import pytest @@ -156,6 +156,14 @@ def __init__(self): def adder(self, x: int) -> int: return self.x + self.y + x +""", + "test_context_loading": """ +class GasTank: + pass + +class Car: + def __init__(self, tank: GasTank): + self.tank = tank """, } @@ -1187,3 +1195,26 @@ class TestObj(_TestObj): """A test object for testing.""" return TestObj # type: ignore + + +@pytest.fixture(name="test_context_loading") +def fixture_test_context_loading() -> Problem[[], Any]: + """Generate a test requiring context values.""" + + def override_test(test_case, golden, student): + """Check if test_case's context has GasTank.""" + # GasTank should be run without problem + test_case.ctx.GasTank() + + @test_case(aga_override_test=override_test) + @problem(ctx=["GasTank"]) + class Car: + """A car with a gas tank.""" + + def __int__(self, tank): + """Initialize the car.""" + self.gas_tank = tank + + assert Car.submission_context.GasTank is None + + return Car diff --git a/tests/test_gradescope/test_main.py b/tests/test_gradescope/test_main.py index b5c548f..f6f628b 100644 --- a/tests/test_gradescope/test_main.py +++ b/tests/test_gradescope/test_main.py @@ -628,3 +628,19 @@ def test_json_override_name(gs_json_bad_override_description: Any, target: str) gs_json_bad_override_description["tests"], ) ) + + +def test_loading_context_from_submission( + test_context_loading: AnyProblem, + source_test_context_loading: Any, + mocker: MockerFixture, + tmp_path: Path, + example_metadata_file: str, +) -> None: + get_gs_json( + test_context_loading, + source_test_context_loading, + mocker, + tmp_path, + example_metadata_file, + ) From ef8572aa8dc07e4725dd1924f458b2b37f5fcddf Mon Sep 17 00:00:00 2001 From: Heyuan Zeng Date: Tue, 3 Oct 2023 22:19:08 +0100 Subject: [PATCH 4/9] fix: better error display --- src/aga/core/problem.py | 1 - src/aga/loader.py | 4 ++++ src/aga/runner.py | 17 +++++++++++------ tests/conftest.py | 5 +++++ tests/test_gradescope/test_main.py | 16 ++++++++++++++++ 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/aga/core/problem.py b/src/aga/core/problem.py index 57bd948..1e68faa 100644 --- a/src/aga/core/problem.py +++ b/src/aga/core/problem.py @@ -1,7 +1,6 @@ """Problem and utilities.""" from __future__ import annotations -from types import ModuleType from typing import ( TYPE_CHECKING, Callable, diff --git a/src/aga/loader.py b/src/aga/loader.py index a4ceabb..b8cc745 100644 --- a/src/aga/loader.py +++ b/src/aga/loader.py @@ -48,6 +48,10 @@ class ContextMissing(InvalidSubmissionError): """The submission file does not contain the values required by the environment.""" +class AmbiguousContext(InvalidSubmissionError): + """The submission file contains multiple values for the same environment variable.""" + + def _get_spec_from_path(path: str, name: str) -> ModuleSpec: """Get the spec of the module at path.""" spec = importlib.util.spec_from_file_location(name, path) diff --git a/src/aga/runner.py b/src/aga/runner.py index 9c38713..b29d363 100644 --- a/src/aga/runner.py +++ b/src/aga/runner.py @@ -7,14 +7,10 @@ For convenience, it also provides the `load_and_run` method, which loads a student submission and then runs it. """ -import os.path from dataclasses import dataclass -from os.path import isdir from typing import Any, Literal, Optional, TypeVar from unittest import TestResult -import typer - from .config import AgaConfig from .core import AgaTestCase, AgaTestSuite, Problem, SubmissionMetadata from .core.problem import ProblemOutputType, ProblemParamSpec @@ -26,8 +22,8 @@ TooManyMatchingSymbols, load_script_from_path, load_symbol_from_path, - _load_source_from_file, ContextMissing, + AmbiguousContext, ) from .score import ScoredPrize from .util import limited_traceback @@ -320,7 +316,16 @@ def load_and_run( if not problem.is_script: # If the submission is a module, we need to update the required context # with the values from the submission. - problem.submission_context.update_from_path(path) + try: + problem.submission_context.update_from_path(path) + except NoMatchingSymbol as e: + raise ContextMissing( + "The submission does not include some required context" + ) from e + except TooManyMatchingSymbols as e: + raise AmbiguousContext( + "The submission includes multiple values for the same context" + ) from e suite, prizes = problem.generate_test_suite(under_test, metadata) return _run(suite, prizes, metadata) diff --git a/tests/conftest.py b/tests/conftest.py index aa406c7..e0b22fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -161,6 +161,11 @@ def adder(self, x: int) -> int: class GasTank: pass +class Car: + def __init__(self, tank: GasTank): + self.tank = tank +""", + "test_no_context_values": """ class Car: def __init__(self, tank: GasTank): self.tank = tank diff --git a/tests/test_gradescope/test_main.py b/tests/test_gradescope/test_main.py index f6f628b..6b1a274 100644 --- a/tests/test_gradescope/test_main.py +++ b/tests/test_gradescope/test_main.py @@ -644,3 +644,19 @@ def test_loading_context_from_submission( tmp_path, example_metadata_file, ) + + +def test_loading_missing_context_from_submission( + test_context_loading: AnyProblem, + source_test_no_context_values: Any, + mocker: MockerFixture, + tmp_path: Path, + example_metadata_file: str, +) -> None: + get_gs_json( + test_context_loading, + source_test_no_context_values, + mocker, + tmp_path, + example_metadata_file, + ) From 1edebac0ea98249601b11d46657eed3406638697 Mon Sep 17 00:00:00 2001 From: Heyuan Zeng Date: Tue, 3 Oct 2023 22:32:50 +0100 Subject: [PATCH 5/9] fix: make linter happy --- src/aga/config.py | 2 ++ src/aga/core/context.py | 2 ++ src/aga/core/parameter.py | 3 ++- src/aga/loader.py | 2 +- tests/conftest.py | 26 ++++++++++++++++++-------- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/aga/config.py b/src/aga/config.py index 7af3747..0177570 100644 --- a/src/aga/config.py +++ b/src/aga/config.py @@ -23,6 +23,8 @@ def _default_value(path: list[str]) -> Any: def _from_default(path: list[str]) -> Any: """Given a path of config names, construct a field which inherits the default.""" + # pylint: disable=invalid-field-call + # this is necessary for the default_factory return field(default_factory=lambda: _default_value(path)) # type: ignore diff --git a/src/aga/core/context.py b/src/aga/core/context.py index fe6bfbf..29cd931 100644 --- a/src/aga/core/context.py +++ b/src/aga/core/context.py @@ -14,6 +14,8 @@ def __init__(self, env_targets: Iterable[str]) -> None: def update_from_path(self, path: str) -> None: """Update environment values from a given module.""" + # pylint: disable=import-outside-toplevel + # to avoid circular imports from ..loader import load_symbol_from_path for symbol in self.keys(): diff --git a/src/aga/core/parameter.py b/src/aga/core/parameter.py index eeaa87d..0ab943c 100644 --- a/src/aga/core/parameter.py +++ b/src/aga/core/parameter.py @@ -208,6 +208,7 @@ class _TestParam(AgaKeywordContainer): pipeline: ClassVar[partial[_TestParam]] + # pylint: disable=too-many-arguments @overload def __init__( self, @@ -383,7 +384,7 @@ class _TestParams: product: ClassVar[partial[_TestParams]] singular_params: ClassVar[partial[_TestParams]] - # pylint: disable=too-many-locals + # pylint: disable=too-many-locals, too-many-arguments @overload def __init__( self, diff --git a/src/aga/loader.py b/src/aga/loader.py index b8cc745..2165c5e 100644 --- a/src/aga/loader.py +++ b/src/aga/loader.py @@ -49,7 +49,7 @@ class ContextMissing(InvalidSubmissionError): class AmbiguousContext(InvalidSubmissionError): - """The submission file contains multiple values for the same environment variable.""" + """The submission files have multiple values for the same environment variable.""" def _get_spec_from_path(path: str, name: str) -> ModuleSpec: diff --git a/tests/conftest.py b/tests/conftest.py index e0b22fc..c1d585a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ from os.path import join as pathjoin from pathlib import Path from shutil import copyfileobj -from typing import Callable, Generator, Iterable, Iterator, List, Any +from typing import Callable, Generator, Iterable, Iterator, List, Any, Type from unittest import TestCase import pytest @@ -160,7 +160,6 @@ def adder(self, x: int) -> int: "test_context_loading": """ class GasTank: pass - class Car: def __init__(self, tank: GasTank): self.tank = tank @@ -1206,20 +1205,31 @@ class TestObj(_TestObj): def fixture_test_context_loading() -> Problem[[], Any]: """Generate a test requiring context values.""" - def override_test(test_case, golden, student): + def override_test( + tc: _TestInputs[Car], golden: Type[Car], student: Type[Car] + ) -> None: """Check if test_case's context has GasTank.""" # GasTank should be run without problem - test_case.ctx.GasTank() + assert tc.ctx is not None + tank = tc.ctx["GasTank"] + golden_car = golden(tank) + student_car = student(tank) + assert golden_car.tank == student_car.tank + + class GasTank: + """A gas tank needed by the Car.""" @test_case(aga_override_test=override_test) @problem(ctx=["GasTank"]) class Car: """A car with a gas tank.""" - def __int__(self, tank): + def __init__(self, tank: GasTank) -> None: """Initialize the car.""" - self.gas_tank = tank + self.tank = tank - assert Car.submission_context.GasTank is None + # pylint: disable=no-member + # since Car is transformed into a Problem by the decorator + assert Car.submission_context.GasTank is None # type: ignore - return Car + return Car # type: ignore From bbea3a2ad626324f0cf8acb180f2ce3c9a99f0b2 Mon Sep 17 00:00:00 2001 From: Heyuan Zeng Date: Tue, 3 Oct 2023 22:34:23 +0100 Subject: [PATCH 6/9] chore: bump up version --- pyproject.toml | 2 +- tests/test_metadata.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5746806..b1bb52e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aga" -version = "0.13.9" +version = "0.13.10" description = "aga grades assignments" authors = ["Riley Shahar "] license = "MIT" diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 91dfa8c..00d15dd 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -5,4 +5,4 @@ def test_version() -> None: """Test that the version is correct.""" - assert agaversion == "0.13.9" + assert agaversion == "0.13.10" From c239c707e8bc1027f592d67bbde4a2c53e2dc554 Mon Sep 17 00:00:00 2001 From: Heyuan Zeng Date: Tue, 3 Oct 2023 22:35:42 +0100 Subject: [PATCH 7/9] doc: update ctx capture doc --- docs/advanced.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/advanced.md b/docs/advanced.md index e5012ab..95b5b72 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -123,20 +123,20 @@ The `case` exposes `args` arguments and `kwargs` variables which are passed from The `case` also exposes `name` and `description` variables which are the name of the test case and the description of the test case. Changing those variables is equivalent to changing `aga_name` and `aga_description` but this means you can set it dynamically during the testing. -## Capture Environment Values +## Capture Context Values -Sometimes a piece of assignment file includes multiple classes, and even though only one class is eventually tested, the other parts of students' answers can be crucial. For example, consider the following file. You can specify in the `envs` argument of `problem` decorator to capture the `GasStation` class, and in the override check function, you can reference the `GasStation` class in the student's answer. +Sometimes a piece of assignment file includes multiple classes, and even though only one class is eventually tested, the other parts of students' answers can be crucial. For example, consider the following file. You can specify in the `ctx` argument of `problem` decorator to capture the `GasStation` class, and in the override check function, you can reference the `GasStation` class in the student's answer. ```python from aga import problem, test_case def override_check(case, golden, student): - # use case.env.GasStation to reference student's GasStation class implementation + # use case.ctx.GasStation to reference student's GasStation class implementation ... @test_case(aga_override_check=override_check) -@problem(envs=['GasStation']) +@problem(ctx=['GasStation']) class Car: # uses gas station somewhere in the code ... @@ -145,4 +145,4 @@ class GasStation: ... ``` -Essentially, `envs` argument takes in an iterable of strings, and aga will search the corresponding fields in the students' submitted module (file). +Essentially, `ctx` argument takes in an iterable of strings, and aga will search the corresponding fields in the students' submitted module (file). From 992f5111dc927d7584e845fd7291388f5e5fde69 Mon Sep 17 00:00:00 2001 From: Heyuan Zeng Date: Tue, 3 Oct 2023 22:42:24 +0100 Subject: [PATCH 8/9] fix: test and note to the doc --- docs/advanced.md | 2 ++ tests/conftest.py | 2 +- tests/test_gradescope/test_main.py | 16 +++++++++------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/advanced.md b/docs/advanced.md index 95b5b72..fbf0165 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -146,3 +146,5 @@ class GasStation: ``` Essentially, `ctx` argument takes in an iterable of strings, and aga will search the corresponding fields in the students' submitted module (file). + +Note that `ctx` is should not be modified during overriden check functions, since the changes will persist to all the following test cases, which might not be the intended behavior. diff --git a/tests/conftest.py b/tests/conftest.py index c1d585a..8b4b177 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -166,7 +166,7 @@ def __init__(self, tank: GasTank): """, "test_no_context_values": """ class Car: - def __init__(self, tank: GasTank): + def __init__(self, tank): self.tank = tank """, } diff --git a/tests/test_gradescope/test_main.py b/tests/test_gradescope/test_main.py index 6b1a274..2758b38 100644 --- a/tests/test_gradescope/test_main.py +++ b/tests/test_gradescope/test_main.py @@ -12,6 +12,7 @@ from aga import problem as agaproblem from aga.core import Problem from aga.gradescope.main import gradescope_main +from aga.loader import ContextMissing from aga.runner import TcOutput AnyProblem = Problem[Any, Any] @@ -653,10 +654,11 @@ def test_loading_missing_context_from_submission( tmp_path: Path, example_metadata_file: str, ) -> None: - get_gs_json( - test_context_loading, - source_test_no_context_values, - mocker, - tmp_path, - example_metadata_file, - ) + with pytest.raises(ContextMissing): + get_gs_json( + test_context_loading, + source_test_no_context_values, + mocker, + tmp_path, + example_metadata_file, + ) From 973f3c1cf1c93d350bb097c6a17e37ef8c6d94e7 Mon Sep 17 00:00:00 2001 From: Heyuan Zeng Date: Tue, 3 Oct 2023 22:51:13 +0100 Subject: [PATCH 9/9] fix: linter errors --- src/aga/core/context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aga/core/context.py b/src/aga/core/context.py index 29cd931..f73d024 100644 --- a/src/aga/core/context.py +++ b/src/aga/core/context.py @@ -14,8 +14,8 @@ def __init__(self, env_targets: Iterable[str]) -> None: def update_from_path(self, path: str) -> None: """Update environment values from a given module.""" - # pylint: disable=import-outside-toplevel - # to avoid circular imports + # pylint: disable=import-outside-toplevel, cyclic-import + # to avoid circular imports, I place the import here from ..loader import load_symbol_from_path for symbol in self.keys():