From e3a6a08e3528d8a104d815e99b28e02fd53177d2 Mon Sep 17 00:00:00 2001 From: Imogen Date: Sat, 30 Sep 2023 17:23:47 +0200 Subject: [PATCH 1/3] move reference validation code to problem module --- algobattle/battle.py | 2 +- algobattle/problem.py | 175 ++++++++++++++++++++++++++++++++++++++++-- algobattle/types.py | 6 +- algobattle/util.py | 170 +--------------------------------------- tests/test_types.py | 4 +- 5 files changed, 174 insertions(+), 183 deletions(-) diff --git a/algobattle/battle.py b/algobattle/battle.py index 2cd86a3e..eb380b14 100644 --- a/algobattle/battle.py +++ b/algobattle/battle.py @@ -237,7 +237,7 @@ class Config(BaseModel): :meth:`Battle.run` method with its fields set accordingly. """ - type: str + type: Any """Type of battle that will be used.""" @classmethod diff --git a/algobattle/problem.py b/algobattle/problem.py index 94543cfc..8074cd8c 100644 --- a/algobattle/problem.py +++ b/algobattle/problem.py @@ -1,7 +1,9 @@ """Module defining the Problem and Solution base classes and related objects.""" from abc import ABC, abstractmethod +from dataclasses import dataclass from functools import wraps from importlib.metadata import entry_points +from inspect import Parameter, Signature, signature from itertools import chain from pathlib import Path from typing import ( @@ -17,12 +19,21 @@ Generic, TypeVar, overload, + cast, + get_args, ) from math import inf, isnan +from annotated_types import GroupedMetadata + +from pydantic import ( + GetCoreSchemaHandler, + ValidationInfo, +) +from pydantic_core import CoreSchema +from pydantic_core.core_schema import with_info_after_validator_function from algobattle.util import ( EncodableModel, - InstanceSolutionModel, Role, Encodable, import_file_as_module, @@ -363,19 +374,171 @@ def available(cls) -> set[str]: AnyProblem = Problem[Any, Any] +ModelType = Literal["instance", "solution"] +ModelReference = ModelType | Literal["self"] + + +@dataclass(frozen=True, slots=True) +class AttributeReference: + """Creates a reference to the attribute of a model to be used in validaton schemas.""" + + model: ModelReference + attribute: str + + def get_value(self, info: ValidationInfo) -> Any | None: + """Returns the referenced value from the correct object in the info context. + + If the correct object is not in the context or doesn't have the referenced attribute it returns None. + """ + if info.context is None or self.model not in info.context: + return None + model = info.context[self.model] + if hasattr(model, self.attribute): + return getattr(model, self.attribute) + else: + return None + + def __str__(self) -> str: + return f"{self.model}.{self.attribute}" + + def needs_self(self, model_type: Literal["instance", "solution"]) -> bool: + """Checks if an attribute reference needs a reference to the current model in order to be resolved.""" + if self.model == "self": + return True + else: + return self.model == model_type + + +NoInfoAttrValidatorFunction = Callable[[Any, Any], Any] +GeneralAttrValidatorFunction = Callable[[Any, Any, ValidationInfo], Any] +AttrValidatorFunction = NoInfoAttrValidatorFunction | GeneralAttrValidatorFunction + + +def count_positional_params(sig: Signature) -> int: + """Counts the number of positional parameters in a signature.""" + return sum(1 for param in sig.parameters.values() if can_be_positional(param)) + + +def can_be_positional(param: Parameter) -> bool: + """Checks whether a parameter is positional.""" + return param.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD) + + +def is_info_validator(validator: AttrValidatorFunction) -> bool: + """Helper method to discriminate the union.""" + match count_positional_params(signature(validator)): + case 2: + return False + case 3: + return True + case _: + raise TypeError + + +@dataclass(frozen=True, slots=True) +class AttributeReferenceValidator: + """An AfterValidator that can resolve a reference to a model attribute and pass it to the validator function. + + Using this with a reference to an attribute in the model it is defined may significantly impact performance. + """ + func: AttrValidatorFunction + attribute: AttributeReference -class InstanceModel(Instance, EncodableModel, InstanceSolutionModel, ABC): + def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema: + schema = handler(source_type) + info_arg = is_info_validator(self.func) + if info_arg: + func = cast(GeneralAttrValidatorFunction, self.func) + + def wrapper(value: Any, info: ValidationInfo) -> Any: + attribute_val = self.attribute.get_value(info) + if attribute_val is None: + return value + return func(value, attribute_val, info) + + else: + func = cast(NoInfoAttrValidatorFunction, self.func) + + def wrapper(value: Any, info: ValidationInfo) -> Any: + attribute_val = self.attribute.get_value(info) + if attribute_val is None: + return value + return func(value, attribute_val) + + return with_info_after_validator_function(wrapper, schema=schema) + + def needs_self(self, model_type: ModelType) -> bool: + """Checks if the validator needs a reference to the current model in order to work fully.""" + if self.attribute.model == "self": + return True + else: + return self.attribute.model == model_type + + +@dataclass +class AttributeReferenceMaker: + """Helper class to easily create attribute references.""" + + _attr_ref_maker_model: ModelReference + + def __getattr__(self, __name: str) -> AttributeReference: + return AttributeReference(self._attr_ref_maker_model, __name) + + +SelfRef = AttributeReferenceMaker("self") +InstanceRef = AttributeReferenceMaker("instance") +SolutionRef = AttributeReferenceMaker("solution") + + +class InstanceSolutionModel(EncodableModel): + """Base class for Instance and solution models.""" + + @classmethod + def model_validate( # noqa: D102 + cls, + obj: Any, + *, + strict: bool | None = None, + from_attributes: bool | None = None, + context: dict[str, Any] | None = None, + ) -> Self: + model = super().model_validate(obj, strict=strict, from_attributes=from_attributes, context=context) + model_type = "instance" if issubclass(cls, InstanceModel) else "solution" + if cls._validate_with_self(model_type): + context = (context or {}) | {"self": model, model_type: model} + model = super().model_validate(obj, context=context) + return model + + @classmethod + def _annotation_needs_self(cls, annotation: object, model_type: ModelType) -> bool: + if isinstance(annotation, AttributeReferenceValidator): + return annotation.needs_self(model_type) + if isinstance(annotation, GroupedMetadata): + return any(cls._annotation_needs_self(e, model_type) for e in annotation) + return any(cls._annotation_needs_self(e, model_type) for e in get_args(annotation)) + + @classmethod + def _validate_with_self(cls, model_type: ModelType) -> bool: + # info.annotation contains the type and any nested metadata, info.metadata the top level metadata + # we can use _annotation_needs_self for all of them, so we iterate over all fields and see if any of them + # either have an annotation or metadata we need to parse with a self reference + for info in cls.model_fields.values(): + values = chain((info.annotation,), info.metadata) + if any(cls._annotation_needs_self(value, model_type) for value in values): + return True + return False + + +class InstanceModel(Instance, InstanceSolutionModel, ABC): """An instance that can easily be parsed to/from a json file.""" - _algobattle_model_type: ClassVar[Literal["instance"]] = "instance" + pass -class SolutionModel(Solution[InstanceT], EncodableModel, InstanceSolutionModel, ABC): +class SolutionModel(Solution[InstanceT], InstanceSolutionModel, ABC): """A solution that can easily be parsed to/from a json file.""" - _algobattle_model_type: ClassVar[Literal["solution"]] = "solution" - @classmethod def decode(cls, source: Path, max_size: int, role: Role, instance: InstanceT | None = None) -> Self: """Uses pydantic to create a python object from a `.json` file.""" diff --git a/algobattle/types.py b/algobattle/types.py index e402ca55..02605a92 100644 --- a/algobattle/types.py +++ b/algobattle/types.py @@ -34,15 +34,11 @@ from algobattle.problem import ( InstanceModel, SolutionModel, -) -from algobattle.util import ( - BaseModel, - Role, AttributeReference, AttributeReferenceValidator, InstanceRef, - ValidationError, ) +from algobattle.util import BaseModel, Role, ValidationError __all__ = ( "u64", diff --git a/algobattle/util.py b/algobattle/util.py index 0e0a625f..6e7c2c39 100644 --- a/algobattle/util.py +++ b/algobattle/util.py @@ -7,26 +7,19 @@ from datetime import datetime from enum import StrEnum from importlib.util import module_from_spec, spec_from_file_location -from inspect import Parameter, Signature, signature -from itertools import chain import json from pathlib import Path import sys from tempfile import TemporaryDirectory from traceback import format_exception from types import ModuleType -from typing import Any, Callable, ClassVar, Iterable, Literal, LiteralString, TypeVar, Self, cast, get_args -from annotated_types import GroupedMetadata +from typing import Any, Iterable, LiteralString, TypeVar, Self from pydantic import ( ConfigDict, BaseModel as PydandticBaseModel, - GetCoreSchemaHandler, ValidationError as PydanticValidationError, - ValidationInfo, ) -from pydantic_core import CoreSchema -from pydantic_core.core_schema import with_info_after_validator_function class Role(StrEnum): @@ -39,163 +32,12 @@ class Role(StrEnum): T = TypeVar("T") -ModelType = Literal["instance", "solution", "other"] -ModelReference = ModelType | Literal["self"] - - class BaseModel(PydandticBaseModel): """Base class for all pydantic models.""" model_config = ConfigDict(extra="forbid", from_attributes=True) -class InstanceSolutionModel(BaseModel): - """Base class for Instance and solution models.""" - - _algobattle_model_type: ClassVar[ModelType] = "other" - - @classmethod - def model_validate( # noqa: D102 - cls, - obj: Any, - *, - strict: bool | None = None, - from_attributes: bool | None = None, - context: dict[str, Any] | None = None, - ) -> Self: - model = super().model_validate(obj, strict=strict, from_attributes=from_attributes, context=context) - if cls._validate_with_self(cls._algobattle_model_type): - context = (context or {}) | {"self": model, cls._algobattle_model_type: model} - model = super().model_validate(obj, context=context) - return model - - @classmethod - def _annotation_needs_self(cls, annotation: object, model_type: ModelType) -> bool: - if isinstance(annotation, AttributeReferenceValidator): - return annotation.needs_self(model_type) - if isinstance(annotation, GroupedMetadata): - return any(cls._annotation_needs_self(e, model_type) for e in annotation) - return any(cls._annotation_needs_self(e, model_type) for e in get_args(annotation)) - - @classmethod - def _validate_with_self(cls, model_type: ModelType) -> bool: - # info.annotation contains the type and any nested metadata, info.metadata the top level metadata - # we can use _annotation_needs_self for all of them, so we iterate over all fields and see if any of them - # either have an annotation or metadata we need to parse with a self reference - for info in cls.model_fields.values(): - values = chain((info.annotation,), info.metadata) - if any(cls._annotation_needs_self(value, model_type) for value in values): - return True - return False - - -@dataclass(frozen=True, slots=True) -class AttributeReference: - """Creates a reference to the attribute of a model to be used in validaton schemas.""" - - model: ModelReference - attribute: str - - def get_value(self, info: ValidationInfo) -> Any | None: - """Returns the referenced value from the correct object in the info context. - - If the correct object is not in the context or doesn't have the referenced attribute it returns None. - """ - if info.context is None or self.model not in info.context: - return None - model = info.context[self.model] - if hasattr(model, self.attribute): - return getattr(model, self.attribute) - else: - return None - - def __str__(self) -> str: - return f"{self.model}.{self.attribute}" - - def needs_self(self, model_type: Literal["instance", "solution"]) -> bool: - """Checks if an attribute reference needs a reference to the current model in order to be resolved.""" - if self.model == "self": - return True - else: - return self.model == model_type - - -NoInfoAttrValidatorFunction = Callable[[Any, Any], Any] -GeneralAttrValidatorFunction = Callable[[Any, Any, ValidationInfo], Any] -AttrValidatorFunction = NoInfoAttrValidatorFunction | GeneralAttrValidatorFunction - - -def is_info_validator(validator: AttrValidatorFunction) -> bool: - """Helper method to discriminate the union.""" - match count_positional_params(signature(validator)): - case 2: - return False - case 3: - return True - case _: - raise TypeError - - -@dataclass(frozen=True, slots=True) -class AttributeReferenceValidator: - """An AfterValidator that can resolve a reference to a model attribute and pass it to the validator function. - - Using this with a reference to an attribute in the model it is defined may significantly impact performance. - """ - - func: AttrValidatorFunction - attribute: AttributeReference - - def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema: - schema = handler(source_type) - info_arg = is_info_validator(self.func) - if info_arg: - func = cast(GeneralAttrValidatorFunction, self.func) - - def wrapper(value: Any, info: ValidationInfo) -> Any: - attribute_val = self.attribute.get_value(info) - if attribute_val is None: - return value - return func(value, attribute_val, info) - - else: - func = cast(NoInfoAttrValidatorFunction, self.func) - - def wrapper(value: Any, info: ValidationInfo) -> Any: - attribute_val = self.attribute.get_value(info) - if attribute_val is None: - return value - return func(value, attribute_val) - - return with_info_after_validator_function(wrapper, schema=schema) - - def needs_self(self, model_type: ModelType) -> bool: - """Checks if the validator needs a reference to the current model in order to work fully.""" - if self.attribute.model == "self": - return True - else: - return self.attribute.model == model_type - - -@dataclass -class AttributeReferenceMaker: - """Helper class to easily create attribute references.""" - - _attr_ref_maker_model: ModelReference - - def __getattr__(self, __name: str) -> AttributeReference: - return AttributeReference(self._attr_ref_maker_model, __name) - - -SelfRef = AttributeReferenceMaker("self") - - -InstanceRef = AttributeReferenceMaker("instance") - - -SolutionRef = AttributeReferenceMaker("solution") - - class Encodable(ABC): """Represents data that docker containers can interact with.""" @@ -373,16 +215,6 @@ def from_exception(cls, error: Exception) -> Self: ) -def count_positional_params(sig: Signature) -> int: - """Counts the number of positional parameters in a signature.""" - return sum(1 for param in sig.parameters.values() if can_be_positional(param)) - - -def can_be_positional(param: Parameter) -> bool: - """Checks whether a parameter is positional.""" - return param.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD) - - class TempDir(TemporaryDirectory): """Python's `TemporaryDirectory` but with a contextmanager returning a Path.""" diff --git a/tests/test_types.py b/tests/test_types.py index 0716d598..26507683 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -5,8 +5,8 @@ from pydantic import ValidationError -from algobattle.problem import InstanceModel -from algobattle.util import AttributeReference, Role, SelfRef +from algobattle.problem import InstanceModel, AttributeReference, SelfRef +from algobattle.util import Role from algobattle.types import Ge, Interval, LaxComp, SizeIndex, UniqueItems From 6678af349cc412fc914128edfc10c4a40fe1eb77 Mon Sep 17 00:00:00 2001 From: Imogen Date: Sun, 1 Oct 2023 01:05:33 +0200 Subject: [PATCH 2/3] remove type vars from Problem --- algobattle/battle.py | 4 ++-- algobattle/match.py | 6 +++--- algobattle/problem.py | 31 +++++++++++++++++++++---------- algobattle/program.py | 21 ++++++++++----------- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/algobattle/battle.py b/algobattle/battle.py index eb380b14..a167d06a 100644 --- a/algobattle/battle.py +++ b/algobattle/battle.py @@ -42,7 +42,7 @@ ProgramUi, Solver, ) -from algobattle.problem import AnyProblem +from algobattle.problem import Problem from algobattle.util import Encodable, ExceptionInfo, BaseModel @@ -106,7 +106,7 @@ async def inner(self: "FightHandler", *args: P.args, **kwargs: P.kwargs) -> Figh class FightHandler: """Helper class to run fights of a given battle.""" - problem: AnyProblem + problem: Problem generator: Generator solver: Solver battle: "Battle" diff --git a/algobattle/match.py b/algobattle/match.py index 3f9a50b9..ef3b5aba 100644 --- a/algobattle/match.py +++ b/algobattle/match.py @@ -29,7 +29,7 @@ from algobattle.battle import Battle, FightHandler, FightUi, BattleUi, Iterated from algobattle.program import ProgramConfigView, ProgramUi, Matchup, TeamHandler, BuildUi -from algobattle.problem import InstanceT, Problem, SolutionT +from algobattle.problem import Problem from algobattle.util import ( ExceptionInfo, Role, @@ -74,7 +74,7 @@ async def _run_battle( battle: Battle, matchup: Matchup, config: "AlgobattleConfig", - problem: Problem[InstanceT, SolutionT], + problem: Problem, cpus: list[str | None], ui: "Ui", limiter: CapacityLimiter, @@ -634,7 +634,7 @@ def check_problem_defined(self) -> Self: return self @cached_property - def problem(self) -> Problem[Any, Any]: + def problem(self) -> Problem: """The problem this config uses.""" return Problem.load(self.match.problem, self.problems) diff --git a/algobattle/problem.py b/algobattle/problem.py index 8074cd8c..e0f60386 100644 --- a/algobattle/problem.py +++ b/algobattle/problem.py @@ -221,7 +221,7 @@ class DynamicProblemInfo(Protocol): location: Path -class Problem(Generic[InstanceT, SolutionT]): +class Problem: """The definition of a problem.""" @overload @@ -291,19 +291,21 @@ def __init__( self._problems[name] = self __slots__ = ("name", "instance_cls", "solution_cls", "min_size", "with_solution", "score_function", "test_instance") - _problems: "ClassVar[dict[str, AnyProblem]]" = {} + _problems: ClassVar[dict[str, Self]] = {} @overload - def score(self, instance: InstanceT, *, solution: SolutionT) -> float: + def score(self, instance: InstanceT, *, solution: Solution[InstanceT]) -> float: ... @overload - def score(self, instance: InstanceT, *, generator_solution: SolutionT, solver_solution: SolutionT) -> float: + def score( + self, instance: InstanceT, *, generator_solution: Solution[InstanceT], solver_solution: Solution[InstanceT] + ) -> float: ... def score( self, - instance: InstanceT, + instance: Instance, *, solution: SolutionT | None = None, generator_solution: SolutionT | None = None, @@ -311,20 +313,30 @@ def score( ) -> float: """Helper function to call self.score_function with easier to use overloads.""" if self.with_solution: - if solution is not None or generator_solution is None or solver_solution is None: + if not ( + isinstance(instance, self.instance_cls) + and isinstance(generator_solution, self.solution_cls) + and isinstance(solver_solution, self.solution_cls) + and solution is None + ): raise TypeError if TYPE_CHECKING: assert isinstance(self.score_function, ScoreFunctionWithSol) return self.score_function(instance, generator_solution=generator_solution, solver_solution=solver_solution) else: - if solution is None or generator_solution is not None or solver_solution is not None: + if not ( + isinstance(instance, self.instance_cls) + and isinstance(solution, self.solution_cls) + and generator_solution is None + and solver_solution is None + ): raise TypeError if TYPE_CHECKING: assert isinstance(self.score_function, ScoreFunctionNoSol) return self.score_function(instance, solution=solution) @classmethod - def load_file(cls, name: str, file: Path) -> "AnyProblem": + def load_file(cls, name: str, file: Path) -> Self: """Loads the problem from the specified file.""" existing_problems = cls._problems.copy() import_file_as_module(file, "__algobattle_problem__") @@ -335,7 +347,7 @@ def load_file(cls, name: str, file: Path) -> "AnyProblem": return cls._problems[name] @classmethod - def load(cls, name: str, dynamic: Mapping[str, DynamicProblemInfo]) -> "AnyProblem": + def load(cls, name: str, dynamic: Mapping[str, DynamicProblemInfo]) -> Self: """Loads the problem with the given name. Args: @@ -373,7 +385,6 @@ def available(cls) -> set[str]: return set(chain(cls._problems.keys(), (e.name for e in entry_points(group="algobattle.problem")))) -AnyProblem = Problem[Any, Any] ModelType = Literal["instance", "solution"] ModelReference = ModelType | Literal["self"] diff --git a/algobattle/program.py b/algobattle/program.py index 964d0d1d..395e36a9 100644 --- a/algobattle/program.py +++ b/algobattle/program.py @@ -39,16 +39,15 @@ Role, BaseModel, ) -from algobattle.problem import AnyProblem, Instance, Solution - - -AnySolution = Solution[Instance] +from algobattle.problem import Problem, Instance, Solution _client_var: DockerClient | None = None T = TypeVar("T") +_I = TypeVar("_I") +_S = TypeVar("_S") def client() -> DockerClient: @@ -186,14 +185,14 @@ class GeneratorResult(ProgramResult): """Result of a single generator execution.""" instance: Instance | None = None - solution: AnySolution | None = None + solution: Solution[Instance] | None = None @dataclass class SolverResult(ProgramResult): """Result of a single solver execution.""" - solution: AnySolution | None = None + solution: Solution[Instance] | None = None @dataclass @@ -210,7 +209,7 @@ class Program(ABC): id: str """The id of the Docker image.""" - problem: AnyProblem + problem: Problem """The problem this program generates/solves.""" config: ProgramConfigView """Config settings used for this program.""" @@ -248,7 +247,7 @@ async def build( cls, path: Path, *, - problem: AnyProblem, + problem: Problem, config: ProgramConfigView, team_name: str | None = None, ) -> Self: @@ -601,7 +600,7 @@ def _encode_input(self, input: Path, max_size: int, instance: Instance | None) - assert instance is not None instance.encode(input / "instance", self.role) - def _parse_output(self, output: Path, max_size: int, instance: Instance | None) -> AnySolution: + def _parse_output(self, output: Path, max_size: int, instance: Instance | None) -> Solution[Instance]: assert instance is not None try: solution = self.problem.solution_cls.decode(output / "solution", max_size, self.role, instance) @@ -729,7 +728,7 @@ async def build( cls, name: str, info: _TeamInfo, - problem: AnyProblem, + problem: Problem, config: ProgramConfigView, ui: BuildUi, ) -> "Team": @@ -825,7 +824,7 @@ class TeamHandler: async def build( cls, infos: Mapping[str, _TeamInfo], - problem: AnyProblem, + problem: Problem, config: ProgramConfigView, ui: BuildUi, ) -> Self: From 4c644373661cfeff923df96b20ebae8a29449b99 Mon Sep 17 00:00:00 2001 From: Imogen Date: Sun, 1 Oct 2023 01:07:19 +0200 Subject: [PATCH 3/3] remove dead code --- algobattle/program.py | 22 ---------------------- algobattle/util.py | 11 +---------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/algobattle/program.py b/algobattle/program.py index 395e36a9..f62f5e90 100644 --- a/algobattle/program.py +++ b/algobattle/program.py @@ -46,8 +46,6 @@ T = TypeVar("T") -_I = TypeVar("_I") -_S = TypeVar("_S") def client() -> DockerClient: @@ -596,26 +594,6 @@ class Solver(Program): role: ClassVar[Role] = Role.solver - def _encode_input(self, input: Path, max_size: int, instance: Instance | None) -> None: - assert instance is not None - instance.encode(input / "instance", self.role) - - def _parse_output(self, output: Path, max_size: int, instance: Instance | None) -> Solution[Instance]: - assert instance is not None - try: - solution = self.problem.solution_cls.decode(output / "solution", max_size, self.role, instance) - except EncodingError: - raise - except Exception as e: - raise EncodingError("Error thrown while decoding the solution.", detail=str(e)) from e - try: - solution.validate_solution(instance, Role.solver) - except ValidationError: - raise - except Exception as e: - raise ValidationError("Unknown error during solution validation.", detail=str(e)) from e - return solution - async def run( self, instance: Instance, diff --git a/algobattle/util.py b/algobattle/util.py index 6e7c2c39..1c87cb07 100644 --- a/algobattle/util.py +++ b/algobattle/util.py @@ -13,7 +13,7 @@ from tempfile import TemporaryDirectory from traceback import format_exception from types import ModuleType -from typing import Any, Iterable, LiteralString, TypeVar, Self +from typing import Any, LiteralString, TypeVar, Self from pydantic import ( ConfigDict, @@ -132,15 +132,6 @@ class RunningTimer: timeout: float | None -def flat_intersperse(iterable: Iterable[Iterable[T]], element: T) -> Iterable[T]: - """Inserts `element` between each iterator in `iterable`.""" - iterator = iter(iterable) - yield from next(iterator) - for item in iterator: - yield element - yield from item - - class AlgobattleBaseException(Exception): """Base exception class for errors used by the algobattle package."""