diff --git a/ixmp/backend/base.py b/ixmp/backend/base.py index ebe0fcbb3..dffc16214 100644 --- a/ixmp/backend/base.py +++ b/ixmp/backend/base.py @@ -921,6 +921,22 @@ def set_meta(self, meta: dict, model: str, scenario: str, version): run version that meta should be attached to """ + @abstractmethod + def remove_meta(self, categories: list, model: str, scenario: str, + version: int): + """Remove meta categories. + + Parameters + ---------- + categories : list of str, meta-categories to remove + model : str, optional + model name that meta should be attached to + scenario : str, optional + scenario name that meta should be attached to + version : int or str, optional + run version that meta should be attached to + """ + @abstractmethod def set_scenario_meta(self, s: Scenario, name_or_dict, value=None): """Set single or multiple scenario meta entries. @@ -996,7 +1012,7 @@ def set_scenario_meta(self, s: Scenario, name_or_dict, value=None): """ @abstractmethod - def delete_scenario_meta(self, s, name): + def remove_scenario_meta(self, s, name): """Remove single or multiple scenario meta entries. Parameters diff --git a/ixmp/backend/jdbc.py b/ixmp/backend/jdbc.py index 2e56edfd5..871e9cfaf 100644 --- a/ixmp/backend/jdbc.py +++ b/ixmp/backend/jdbc.py @@ -902,6 +902,17 @@ def set_meta(self, meta: dict, model: str = None, scenario: str = None, jmeta.put(str(k), v) self.jobj.setMeta(model, scenario, version, jmeta) + def remove_meta(self, categories, model: str = None, scenario: str = None, + version: int = None): + if not (model or scenario or version): + msg = ('At least one parameter has to be provided out of: ' + 'model, scenario, version') + raise ValueError(msg) + if version is not None: + version = java.Long(version) + return self.jobj.removeMeta(model, scenario, version, + to_jlist(categories)) + def get_scenario_meta(self, s): return {entry.getKey(): _unwrap(entry.getValue()) for entry in self.jindex[s].getMeta().entrySet()} @@ -923,7 +934,11 @@ def set_scenario_meta(self, s, name_or_dict, value=None): getattr(self.jindex[s], method_name)(name_or_dict, value) - def delete_scenario_meta(self, s, name): + def delete_scenario_meta(self, *args, **kwargs): + # Add DeprecationWarning + return self.remove_scenario_meta(self, *args, **kwargs) + + def remove_scenario_meta(self, s, name): if type(name) == str: name = [name] jdata = java.LinkedList() diff --git a/ixmp/core.py b/ixmp/core.py index a7f2faaf2..41a56c4a2 100644 --- a/ixmp/core.py +++ b/ixmp/core.py @@ -63,6 +63,7 @@ class Platform: 'set_doc', 'get_meta', 'set_meta', + 'remove_meta', ] def __init__(self, name=None, backend=None, **backend_args): @@ -1600,15 +1601,27 @@ def set_meta(self, name_or_dict, value=None): name_or_dict = list(name_or_dict.items()) self._backend('set_scenario_meta', name_or_dict, value) - def delete_meta(self, name): - """Delete scenario meta. + def delete_meta(self, *args, **kwargs): + """DEPRECATED: Remove scenario meta. Parameters ---------- name : str or list of str Either single meta key or list of keys. """ - self._backend('delete_scenario_meta', name) + warn('Scenario.delete_meta is deprecated; use Scenario.remove_meta ' + 'instead', DeprecationWarning) + self.remove_meta(*args, **kwargs) + + def remove_meta(self, name): + """Remove scenario meta. + + Parameters + ---------- + name : str or list of str + Either single meta key or list of keys. + """ + self._backend('remove_scenario_meta', name) # Input and output def to_excel(self, path, items=ItemType.SET | ItemType.PAR, max_row=None): diff --git a/ixmp/tests/core/test_meta.py b/ixmp/tests/core/test_meta.py index 93f7b9be7..3961cf556 100644 --- a/ixmp/tests/core/test_meta.py +++ b/ixmp/tests/core/test_meta.py @@ -1,5 +1,6 @@ """Test meta functionality of ixmp.Platform.""" +import copy import pytest import ixmp @@ -10,17 +11,16 @@ def test_set_meta_missing_argument(mp): - meta = {'sample_string': 3} with pytest.raises(ValueError): - mp.set_meta(meta) + mp.set_meta(sample_meta) -def test_set_meta(mp): - meta = {'sample_string': 3} +def test_set_get_meta(mp): + """ASsert that storing+retrieving meta yields expected values""" model = models['dantzig']['model'] - mp.set_meta(meta, model=model) + mp.set_meta(sample_meta, model=model) obs = mp.get_meta(model=model) - assert obs == meta + assert obs == sample_meta def test_unique_meta(mp): @@ -54,14 +54,98 @@ def test_unique_meta_model_scenario(mp): def test_unique_meta_scenario(mp): """ When setting a meta key on a specific Scenario run, setting the same key - on an higher level should fail too. + on an higher level (Model or Model+Scenario) should fail. """ model = models['dantzig'] scen = ixmp.Scenario(mp, **model) scen.set_meta(sample_meta) + # add a second scenario and verify that setting Meta for it works + scen2 = ixmp.Scenario(mp, **model, version="new") + scen2.commit('save dummy scenario') + scen2.set_meta(sample_meta) + assert scen2.get_meta() == scen.get_meta() expected = ("Metadata already contains category") with pytest.raises(Exception, match=expected): mp.set_meta(sample_meta, **model) with pytest.raises(Exception, match=expected): mp.set_meta(sample_meta, model=model['model']) + + +def test_meta_partial_overwrite(mp): + meta1 = {'sample_string': 3.0, 'another_string': 'string_value'} + meta2 = {'sample_string': 5.0, 'yet_another_string': 'hello'} + model = models['dantzig'] + scen = ixmp.Scenario(mp, **model) + scen.set_meta(meta1) + scen.set_meta(meta2) + expected = copy.copy(meta1) + expected.update(meta2) + obs = scen.get_meta() + assert obs == expected + + +def test_remove_meta(mp): + meta = {'sample_string': 3.0, 'another_string': 'string_value'} + remove_key = 'another_string' + model = models['dantzig'] + mp.set_meta(sample_meta, **model) + mp.remove_meta(remove_key, **model) + expected = copy.copy(meta) + del expected[remove_key] + obs = mp.get_meta(**model) + assert expected == obs + + +def test_remove_invalid_meta(mp): + """ + Removing nonexisting meta entries or None shouldn't result in any meta + being removed. Providing None should give a ValueError. + """ + model = models['dantzig'] + mp.set_meta(sample_meta, **model) + with pytest.raises(ValueError): + mp.remove_meta(None, **model) + mp.remove_meta('nonexisting_category', **model) + mp.remove_meta([], **model) + obs = mp.get_meta(**model) + assert obs == sample_meta + + +def test_set_and_remove_meta_scenario(mp): + """ + Test partial overwriting and meta deletion on scenario level + """ + meta1 = {'sample_string': 3.0, 'another_string': 'string_value'} + meta2 = {'sample_string': 5.0, 'yet_another_string': 'hello'} + remove_key = 'another_string' + model = models['dantzig'] + + scen = ixmp.Scenario(mp, **model) + scen.set_meta(meta1) + scen.set_meta(meta2) + expected = copy.copy(meta1) + expected.update(meta2) + obs = scen.get_meta() + assert expected == obs + + scen.remove_meta(remove_key) + del expected[remove_key] + obs = scen.get_meta() + assert obs == expected + + +def test_scenario_delete_meta_warning(mp): + """Scenario.delete_meta works but raises a deprecation warning""" + model = models['dantzig'] + scen = ixmp.Scenario(mp, **model) + meta = {'sample_string': 3, 'another_string': 'string_value'} + remove_key = 'another_string' + + scen.set_meta(sample_meta) + with pytest.warns(DeprecationWarning): + scen.delete_meta(remove_key) + expected = copy.copy(meta) + del expected[remove_key] + obs = scen.get_meta() + assert obs == expected