diff --git a/ixmp/backend/ixmp4.py b/ixmp/backend/ixmp4.py index b9294a205..e2e60f1e6 100644 --- a/ixmp/backend/ixmp4.py +++ b/ixmp/backend/ixmp4.py @@ -11,34 +11,34 @@ class IXMP4Backend(CachingBackend): _platform: "ixmp4.Platform" - def __init__(self): - import ixmp4 + def __init__(self, **kwargs) -> None: + from ixmp4 import Platform + from ixmp4.core.exceptions import PlatformNotFound - # TODO Obtain `name` from the ixmp.Platform creating this Backend - name = "test" + # TODO Handle errors or make sure name is always present for this backend + name = kwargs.pop("name") # Add an ixmp4.Platform using ixmp4's own configuration code # TODO Move this to a test fixture # NB ixmp.tests.conftest.test_sqlite_mp exists, but is not importable (missing # __init__.py) + # NB test_platform is parametrized for both backends, but + # TestPlatform::test_init1 calls this function without defining an + # ixmp4.Platform first import ixmp4.conf - dsn = "sqlite:///:memory:" try: + ixmp4.conf.settings.toml.get_platform(name) + except PlatformNotFound: + # TODO Handle errors or make sure dsn is always present when the platform is + # not known + dsn = kwargs.pop("dsn") ixmp4.conf.settings.toml.add_platform(name, dsn) - except ixmp4.core.exceptions.PlatformNotUnique: - pass # Instantiate and store - self._platform = ixmp4.Platform(name) + self._platform = Platform(name) def get_scenarios(self, default, model, scenario): - # Current fails with: - # sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: run - # [SQL: SELECT DISTINCT run.model__id, run.scenario__id, run.version, - # run.is_default, run.id - # FROM run - # WHERE run.is_default = 1 ORDER BY run.id ASC] return self._platform.runs.list() # The below methods of base.Backend are not yet implemented diff --git a/ixmp/core/platform.py b/ixmp/core/platform.py index 7107773e0..bc5c7686a 100644 --- a/ixmp/core/platform.py +++ b/ixmp/core/platform.py @@ -81,6 +81,9 @@ def __init__( # Overwrite any platform config with explicit keyword arguments kwargs.update(backend_args) + if backend == "ixmp4": + kwargs["name"] = self.name + # Retrieve the Backend class try: backend_class_name = kwargs.pop("class") diff --git a/ixmp/testing/__init__.py b/ixmp/testing/__init__.py index 1eee1f923..63080c66b 100644 --- a/ixmp/testing/__init__.py +++ b/ixmp/testing/__init__.py @@ -35,16 +35,20 @@ import logging import os import shutil +from collections.abc import Generator from contextlib import contextmanager, nullcontext from copy import deepcopy from itertools import chain from pathlib import Path +from typing import Any import pint import pytest from click.testing import CliRunner +from ixmp4.conf.base import PlatformInfo +from ixmp4.data.backend import SqliteTestBackend -from ixmp import Platform, cli +from ixmp import BACKENDS, Platform, cli from ixmp import config as ixmp_config from .data import ( @@ -89,7 +93,7 @@ # Pytest hooks -def pytest_addoption(parser): +def pytest_addoption(parser: pytest.Parser) -> None: """Add the ``--user-config`` command-line option to pytest.""" parser.addoption( "--ixmp-jvm-mem", @@ -103,7 +107,7 @@ def pytest_addoption(parser): ) -def pytest_sessionstart(session): +def pytest_sessionstart(session: pytest.Session) -> None: """Unset any configuration read from the user's directory.""" from ixmp.backend import jdbc @@ -117,7 +121,7 @@ def pytest_sessionstart(session): jdbc._GC_AGGRESSIVE = False -def pytest_report_header(config, start_path): +def pytest_report_header(config, start_path) -> str: """Add the ixmp configuration to the pytest report header.""" return f"ixmp config: {repr(ixmp_config.values)}" @@ -137,7 +141,7 @@ def invoke(self, *args, **kwargs): @pytest.fixture(scope="module") -def mp(test_mp): +def mp(test_mp: Platform) -> Generator[Platform, Any, None]: """A :class:`.Platform` containing test data. This fixture is **module** -scoped, and is used in :mod:`.test_platform`, @@ -149,23 +153,29 @@ def mp(test_mp): @pytest.fixture(scope="session") -def test_data_path(): +def test_data_path() -> Path: """Path to the directory containing test data.""" return Path(__file__).parents[1].joinpath("tests", "data") -@pytest.fixture(scope="module") -def test_mp(request, tmp_env, test_data_path): +@pytest.fixture(scope="module", params=list(BACKENDS.keys())) +def test_mp( + request: pytest.FixtureRequest, tmp_env, test_data_path +) -> Generator[Platform, Any, None]: """An empty :class:`.Platform` connected to a temporary, in-memory database. This fixture has **module** scope: the same Platform is reused for all tests in a module. """ - yield from _platform_fixture(request, tmp_env, test_data_path) + yield from _platform_fixture( + request, tmp_env, test_data_path, backend=request.param + ) @pytest.fixture(scope="session") -def tmp_env(pytestconfig, tmp_path_factory): +def tmp_env( + pytestconfig: pytest.Config, tmp_path_factory: pytest.TempPathFactory +) -> Generator[os._Environ[str], Any, None]: """Return the os.environ dict with the IXMP_DATA variable set. IXMP_DATA will point to a temporary directory that is unique to the test session. @@ -187,13 +197,13 @@ def tmp_env(pytestconfig, tmp_path_factory): @pytest.fixture(scope="session") -def tutorial_path(): +def tutorial_path() -> Path: """Path to the directory containing the tutorials.""" return Path(__file__).parents[2].joinpath("tutorial") @pytest.fixture(scope="session") -def ureg(): +def ureg() -> Generator[pint.UnitRegistry, Any, None]: """Application-wide units registry.""" registry = pint.get_application_registry() @@ -242,8 +252,8 @@ def protect_rename_dims(): RENAME_DIMS.update(saved) -@pytest.fixture(scope="function") -def test_mp_f(request, tmp_env, test_data_path): +@pytest.fixture(scope="function", params=list(BACKENDS.keys())) +def test_mp_f(request: pytest.FixtureRequest, tmp_env, test_data_path): """An empty :class:`Platform` connected to a temporary, in-memory database. This fixture has **function** scope: the same Platform is reused for one test @@ -253,7 +263,9 @@ def test_mp_f(request, tmp_env, test_data_path): -------- test_mp """ - yield from _platform_fixture(request, tmp_env, test_data_path) + yield from _platform_fixture( + request, tmp_env, test_data_path, backend=request.param + ) # Assertions @@ -384,19 +396,38 @@ def create_test_platform(tmp_path, data_path, name, **properties): # Private utilities -def _platform_fixture(request, tmp_env, test_data_path): +def _platform_fixture( + request: pytest.FixtureRequest, tmp_env, test_data_path, backend: str +) -> Generator[Platform, Any, None]: """Helper for :func:`test_mp` and other fixtures.""" # Long, unique name for the platform. # Remove '/' so that the name can be used in URL tests. platform_name = request.node.nodeid.replace("/", " ") # Add a platform - ixmp_config.add_platform( - platform_name, "jdbc", "hsqldb", url=f"jdbc:hsqldb:mem:{platform_name}" - ) + if backend == "jdbc": + ixmp_config.add_platform( + platform_name, backend, "hsqldb", url=f"jdbc:hsqldb:mem:{platform_name}" + ) + elif backend == "ixmp4": + import ixmp4.conf + + # Setup ixmp4 backend and run DB migrations + sqlite = SqliteTestBackend( + PlatformInfo(name=platform_name, dsn="sqlite:///:memory:") + ) + sqlite.setup() + + # Add DB to ixmp4 config + ixmp4.conf.settings.toml.add_platform( + name=platform_name, dsn="sqlite:///:memory:" + ) + + # Add ixmp4 backend to ixmp platforms + ixmp_config.add_platform(platform_name, backend) # Launch Platform - mp = Platform(name=platform_name) + mp = Platform(name=platform_name, backend=backend) yield mp # Teardown: don't show log messages when destroying the platform, even if @@ -406,3 +437,11 @@ def _platform_fixture(request, tmp_env, test_data_path): # Remove from config ixmp_config.remove_platform(platform_name) + + if backend == "ixmp4": + assert sqlite # to satisfy type checkers + + # Close DB connection and remove platform + sqlite.close() + sqlite.teardown() + ixmp4.conf.settings.toml.remove_platform(platform_name) diff --git a/ixmp/testing/data.py b/ixmp/testing/data.py index 8f551bf67..f00043550 100644 --- a/ixmp/testing/data.py +++ b/ixmp/testing/data.py @@ -250,7 +250,7 @@ def make_dantzig( return scen -def populate_test_platform(platform): +def populate_test_platform(platform: Platform) -> None: """Populate `platform` with data for testing. Many of the tests in :mod:`ixmp.tests.core` depend on this set of data. diff --git a/ixmp/tests/core/test_platform.py b/ixmp/tests/core/test_platform.py index 4727106ef..6a069a1c1 100644 --- a/ixmp/tests/core/test_platform.py +++ b/ixmp/tests/core/test_platform.py @@ -3,7 +3,7 @@ import logging import re from sys import getrefcount -from typing import TYPE_CHECKING, Generator +from typing import TYPE_CHECKING from weakref import getweakrefcount import pandas as pd @@ -20,17 +20,6 @@ class TestPlatform: - @pytest.fixture(params=list(ixmp.BACKENDS)) - def mp(self, request, test_mp) -> Generator[ixmp.Platform, None, None]: - """Fixture that yields 2 different platforms: one JDBC-backed, one ixmp4.""" - backend = request.param - - if backend == "jdbc": - yield test_mp - elif backend == "ixmp4": - # TODO Use a fixture similar to test_mp (with same contents) backed by ixmp4 - yield ixmp.Platform(backend="ixmp4") - def test_init0(self): with pytest.raises( ValueError, @@ -46,7 +35,8 @@ def test_init0(self): "backend, backend_args", ( ("jdbc", dict(driver="hsqldb", url="jdbc:hsqldb:mem:TestPlatform")), - ("ixmp4", dict()), + # TODO use this/default name for ixmp4 platforms without passing it manually + ("ixmp4", dict(name="ixmp4-local")), ), ) def test_init1(self, backend, backend_args): @@ -58,7 +48,7 @@ def test_getattr(self, test_mp): with pytest.raises(AttributeError): test_mp.not_a_direct_backend_method - def test_scenario_list(self, mp): + def test_scenario_list(self, mp: ixmp.Platform) -> None: scenario = mp.scenario_list() assert isinstance(scenario, pd.DataFrame)