diff --git a/ixmp/__init__.py b/ixmp/__init__.py index acfc57117..a3c881884 100644 --- a/ixmp/__init__.py +++ b/ixmp/__init__.py @@ -69,8 +69,9 @@ def __getattr__(name): if name == "utils": - import ixmp.util + # Import via the old name to trigger DeprecatedPathFinder + import ixmp.utils as util # type: ignore [import-not-found] - return ixmp.util + return util else: raise AttributeError(name) diff --git a/ixmp/backend/jdbc.py b/ixmp/backend/jdbc.py index 7ba6b8b5d..637783bd1 100644 --- a/ixmp/backend/jdbc.py +++ b/ixmp/backend/jdbc.py @@ -4,6 +4,7 @@ import re from collections import ChainMap from collections.abc import Iterable, Sequence +from contextlib import contextmanager from copy import copy from functools import lru_cache from pathlib import Path, PurePosixPath @@ -141,6 +142,15 @@ def _raise_jexception(exc, msg="unhandled Java exception: "): raise RuntimeError(msg) from None +@contextmanager +def _handle_jexception(): + """Context manager form of :func:`_raise_jexception`.""" + try: + yield + except java.Exception as e: + _raise_jexception(e) + + @lru_cache def _fixed_index_sets(scheme: str) -> Mapping[str, List[str]]: """Return index sets for items that are fixed in the Java code. @@ -475,10 +485,8 @@ def get_scenario_names(self) -> Generator[str, None, None]: def get_scenarios(self, default, model, scenario): # List> - try: + with _handle_jexception(): scenarios = self.jobj.getScenarioList(default, model, scenario) - except java.IxException as e: - _raise_jexception(e) for s in scenarios: data = [] @@ -537,7 +545,7 @@ def read_file(self, path, item_type: ItemType, **kwargs): if path.suffix == ".gdx" and item_type is ItemType.MODEL: kw = {"check_solution", "comment", "equ_list", "var_list"} - if not isinstance(ts, Scenario): + if not isinstance(ts, Scenario): # pragma: no cover raise ValueError("read from GDX requires a Scenario object") elif set(kwargs.keys()) != kw: raise ValueError( @@ -556,10 +564,8 @@ def read_file(self, path, item_type: ItemType, **kwargs): if len(kwargs): raise ValueError(f"extra keyword arguments {kwargs}") - try: + with _handle_jexception(): self.jindex[ts].readSolutionFromGDX(*args) - except java.Exception as e: - _raise_jexception(e) self.cache_invalidate(ts) else: @@ -601,7 +607,7 @@ def write_file(self, path, item_type: ItemType, **kwargs): if path.suffix == ".gdx" and item_type is ItemType.SET | ItemType.PAR: if len(filters): raise NotImplementedError("write to GDX with filters") - elif not isinstance(ts, Scenario): + elif not isinstance(ts, Scenario): # pragma: no cover raise ValueError("write to GDX requires a Scenario object") # include_var_equ=False -> do not include variables/equations in GDX @@ -688,10 +694,8 @@ def init(self, ts, annotation): # Call either newTimeSeries or newScenario method = getattr(self.jobj, "new" + klass) - try: + with _handle_jexception(): jobj = method(ts.model, ts.scenario, *args) - except java.IxException as e: # pragma: no cover - _raise_jexception(e) self._index_and_set_attrs(jobj, ts) @@ -703,12 +707,11 @@ def get(self, ts): # either getTimeSeries or getScenario method = getattr(self.jobj, "get" + ts.__class__.__name__) - try: + + # Re-raise as a ValueError for bad model or scenario name, or other + with _handle_jexception(): # Either the 2- or 3- argument form, depending on args jobj = method(*args) - except java.IxException as e: - # Re-raise as a ValueError for bad model or scenario name, or other - _raise_jexception(e) self._index_and_set_attrs(jobj, ts) @@ -719,10 +722,8 @@ def del_ts(self, ts): self.gc() def check_out(self, ts, timeseries_only): - try: + with _handle_jexception(): self.jindex[ts].checkOut(timeseries_only) - except java.IxException as e: - _raise_jexception(e) def commit(self, ts, comment): try: @@ -1120,10 +1121,8 @@ def get_meta( if version is not None: version = java.Long(version) - try: + with _handle_jexception(): meta = self.jobj.getMeta(model, scenario, version, strict) - except java.IxException as e: - _raise_jexception(e) return {entry.getKey(): _unwrap(entry.getValue()) for entry in meta.entrySet()} @@ -1142,10 +1141,8 @@ def set_meta( for k, v in meta.items(): jmeta.put(str(k), _wrap(v)) - try: + with _handle_jexception(): self.jobj.setMeta(model, scenario, version, jmeta) - except java.IxException as e: - _raise_jexception(e) def remove_meta( self, diff --git a/ixmp/core/scenario.py b/ixmp/core/scenario.py index dc539dea8..0ac2bcc78 100644 --- a/ixmp/core/scenario.py +++ b/ixmp/core/scenario.py @@ -478,7 +478,7 @@ def init_item( if idx_names and len(idx_names) != len(idx_sets): raise ValueError( - f"index names {repr(idx_names)} must have same length as index sets" + f"index names {repr(idx_names)} must have the same length as index sets" f" {repr(idx_sets)}" ) diff --git a/ixmp/testing/resource.py b/ixmp/testing/resource.py index c7e9da121..ee9a5f6fb 100644 --- a/ixmp/testing/resource.py +++ b/ixmp/testing/resource.py @@ -7,7 +7,7 @@ import resource has_resource_module = True -except ImportError: +except ImportError: # pragma: no cover # Windows has_resource_module = False diff --git a/ixmp/tests/backend/test_jdbc.py b/ixmp/tests/backend/test_jdbc.py index afd254e94..b17147ef6 100644 --- a/ixmp/tests/backend/test_jdbc.py +++ b/ixmp/tests/backend/test_jdbc.py @@ -201,6 +201,11 @@ def test_write_file(self, tmp_path, be): with pytest.raises(NotImplementedError): be.write_file(tmp_path / "test.csv", ixmp.ItemType.ALL, filters={}) + # Specific to JDBCBackend + def test_gc(self, monkeypatch, be): + monkeypatch.setattr(ixmp.backend.jdbc, "_GC_AGGRESSIVE", True) + be.gc() + def test_exceptions(test_mp): """Ensure that Python exceptions are raised for some actions.""" diff --git a/ixmp/tests/core/test_scenario.py b/ixmp/tests/core/test_scenario.py index 8c6ec1d97..3eaa2dd50 100644 --- a/ixmp/tests/core/test_scenario.py +++ b/ixmp/tests/core/test_scenario.py @@ -147,7 +147,7 @@ def test_init_set(self, scen): with pytest.raises(ValueError, match="'foo' already exists"): scen.init_set("foo") - def test_init_par(self, scen): + def test_init_par(self, scen) -> None: scen = scen.clone(keep_solution=False) scen.check_out() @@ -157,6 +157,10 @@ def test_init_par(self, scen): # Return type of idx_sets is still list assert scen.idx_sets("foo") == ["i", "j"] + # Mismatched sets and names + with pytest.raises(ValueError, match="must have the same length"): + scen.init_par("bar", idx_sets=("i", "j"), idx_names=("a", "b", "c")) + def test_init_scalar(self, scen): scen2 = scen.clone(keep_solution=False) scen2.check_out() @@ -243,14 +247,15 @@ def test_par(self, scen): def test_items0(self, scen): # Without filters - iterator = scen.items() + with pytest.warns(FutureWarning, match="par_data=False will be the default"): + iterator = scen.items() # next() can be called → an iterator was returned next(iterator) # Iterator returns the expected parameter names exp = ["a", "b", "d", "f"] - for i, (name, data) in enumerate(scen.items()): + for i, (name, data) in enumerate(scen.items(par_data=True)): # Name is correct in the expected order assert exp[i] == name # Data is one of the return types of .par() @@ -260,7 +265,7 @@ def test_items0(self, scen): assert i == 3 # With filters - iterator = scen.items(filters=dict(i=["seattle"])) + iterator = scen.items(filters=dict(i=["seattle"]), par_data=True) exp = [("a", 1), ("d", 3)] for i, (name, data) in enumerate(iterator): # Name is correct in the expected order diff --git a/ixmp/tests/report/test_util.py b/ixmp/tests/report/test_util.py new file mode 100644 index 000000000..47ff277ae --- /dev/null +++ b/ixmp/tests/report/test_util.py @@ -0,0 +1,5 @@ +def test_import_rename_dims(): + """RENAME_DIMS can be imported from .report.util, though defined in .common.""" + from ixmp.report.util import RENAME_DIMS + + assert isinstance(RENAME_DIMS, dict) diff --git a/ixmp/tests/test_util.py b/ixmp/tests/test_util.py index 4c9c8f1de..12c71a694 100644 --- a/ixmp/tests/test_util.py +++ b/ixmp/tests/test_util.py @@ -20,6 +20,13 @@ def test_import(self): ): import ixmp.reporting.computations # type: ignore # noqa: F401 + @pytest.mark.filterwarnings("ignore") + def test_import1(self): + """utils can be imported from ixmp, but raises DeprecationWarning.""" + from ixmp import utils + + assert "diff" in dir(utils) + def test_importerror(self): with pytest.warns(DeprecationWarning), pytest.raises(ImportError): import ixmp.reporting.foo # type: ignore # noqa: F401 diff --git a/ixmp/util/__init__.py b/ixmp/util/__init__.py index 65d8ee29e..f6c1798a9 100644 --- a/ixmp/util/__init__.py +++ b/ixmp/util/__init__.py @@ -115,8 +115,8 @@ def diff(a, b, filters=None) -> Iterator[Tuple[str, pd.DataFrame]]: """ # Iterators; index 0 corresponds to `a`, 1 to `b` items = [ - a.items(filters=filters, type=ItemType.PAR), - b.items(filters=filters, type=ItemType.PAR), + a.items(filters=filters, type=ItemType.PAR, par_data=True), + b.items(filters=filters, type=ItemType.PAR, par_data=True), ] # State variables for loop name = ["", ""]