From 52f598c8b391daf5fd39468d370d4c10b12f9841 Mon Sep 17 00:00:00 2001 From: Gergely Polonkai Date: Wed, 21 May 2025 18:46:33 +0200 Subject: [PATCH] feat: Add an Oracle base class --- gm_assistant/oracle/__init__.py | 4 +++ gm_assistant/oracle/base.py | 45 ++++++++++++++++++++++++++ tests/test_oracle_base.py | 57 +++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 gm_assistant/oracle/__init__.py create mode 100644 gm_assistant/oracle/base.py create mode 100644 tests/test_oracle_base.py diff --git a/gm_assistant/oracle/__init__.py b/gm_assistant/oracle/__init__.py new file mode 100644 index 0000000..b1ae0c6 --- /dev/null +++ b/gm_assistant/oracle/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2025 2025 +# SPDX-FileContributor: Gergely Polonkai +# +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/gm_assistant/oracle/base.py b/gm_assistant/oracle/base.py new file mode 100644 index 0000000..2de0c71 --- /dev/null +++ b/gm_assistant/oracle/base.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2025 2025 +# SPDX-FileContributor: Gergely Polonkai +# +# SPDX-License-Identifier: GPL-3.0-or-later +"""Oracle base class and related utilities""" + +from abc import ABCMeta, abstractmethod +from typing import Any + + +class Oracle(metaclass=ABCMeta): # pylint: disable=too-few-public-methods + """Base class for Oracles""" + + TYPE_MARKER: str + + def __init__(self, oracle_data: dict[str, Any]) -> None: + self.name: str | None = None + self.source: str | None = None + self.source_url: str | None = None + self.parse_and_validate(oracle_data) + + def parse_and_validate(self, oracle_data: dict[str, Any]) -> None: + """Parse and validate ``oracle_data``, essentially setting up the Oracle""" + + if "type" not in oracle_data: + raise KeyError("type") + + if "name" not in oracle_data: + raise KeyError("name") + + if "source" not in oracle_data: + raise KeyError("source") + + if (data_type := oracle_data["type"]) != self.TYPE_MARKER: + raise TypeError(f"This class can only handle {self.TYPE_MARKER} data, not {data_type}") + + self.name = oracle_data["name"] + self.source = oracle_data["source"] + self.source_url = oracle_data.get("source-url") + + @abstractmethod + def generate(self) -> str: # pragma: no cover + """Make the Oracle do its work""" + + raise NotImplementedError() diff --git a/tests/test_oracle_base.py b/tests/test_oracle_base.py new file mode 100644 index 0000000..201cb56 --- /dev/null +++ b/tests/test_oracle_base.py @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: 2025 2025 +# SPDX-FileContributor: Gergely Polonkai +# +# SPDX-License-Identifier: GPL-3.0-or-later +"""Tests for the base Oracle class""" + +from typing import Literal + +import pytest + +from gm_assistant.oracle.base import Oracle + +TEST_ORACLE_DATA = { + "type": "test-oracle", + "name": "Test Oracle", + "source": "Test Source", +} + + +class OracleTest(Oracle): + """Test Oracle class""" + + TYPE_MARKER = "test-oracle" + + def generate(self) -> str: # pragma: no cover + return "Something" + + +@pytest.mark.parametrize("missing", ["type", "name", "source"]) +def test_missing_data(missing: Literal["type", "name", "source"]) -> None: + """Test if oracle_data doesn’t have a type""" + + oracle_data = TEST_ORACLE_DATA.copy() + del oracle_data[missing] + + with pytest.raises(KeyError): + OracleTest(oracle_data) + + +def test_incorrect_type() -> None: + """Test if the type in the oracle data doesn’t match the class’ TYPE_MARKER""" + + oracle_data = TEST_ORACLE_DATA.copy() + oracle_data["type"] = "something-else" + + with pytest.raises(TypeError): + OracleTest(oracle_data) + + +def test_init() -> None: + """Test if initialisation succeeds with valid data""" + + oracle = OracleTest(TEST_ORACLE_DATA) + + assert oracle.name == "Test Oracle" + assert oracle.source == "Test Source" + assert oracle.source_url is None