diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 391d52ded..991396fe9 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -175,12 +175,15 @@ To make deprecations as robust as possible and give users all needed information to adjust their code, we provide helper functions inside the module :mod:`glotaran.deprecation`. +.. currentmodule:: glotaran.deprecation.deprecation_utils + The functions you most likely want to use are * :func:`deprecate` for functions, methods and classes * :func:`warn_deprecated` for call arguments * :func:`deprecate_module_attribute` for module attributes * :func:`deprecate_submodule` for modules +* :func:`deprecate_dict_entry` for dict entries Those functions not only make it easier to deprecate something, but they also check that @@ -193,7 +196,7 @@ provides the test helper functions ``deprecation_warning_on_call_test_helper`` a Since the tests for deprecation are mainly for maintainability and not to test the functionality (those tests should be in the appropriate place) ``deprecation_warning_on_call_test_helper`` will by default just test that a -``DeprecationWarning`` was raised and ignore all raise ``Exception`` s. +``GlotaranApiDeprecationWarning`` was raised and ignore all raise ``Exception`` s. An exception to this rule is when adding back removed functionality (which shouldn't happen in the first place but might), which should be implemented in a file under ``glotaran/deprecation/modules`` and filenames should be like the @@ -300,6 +303,17 @@ as an attribute to the parent package. to_be_removed_in_version="0.6.0", ) +Deprecating dict entries +~~~~~~~~~~~~~~~~~~~~~~~~ +The possible dict deprecation actions are: + +- Swapping of keys ``{"foo": 1} -> {"bar": 1}`` (done via ``swap_keys=("foo", "bar")``) +- Replacing of matching values ``{"foo": 1} -> {"foo": 2}`` (done via ``replace_rules=({"foo": 1}, {"foo": 2})``) +- Replacing of matching values and swapping of keys ``{"foo": 1} -> {"bar": 2}`` (done via ``replace_rules=({"foo": 1}, {"bar": 2})``) + +For full examples have a look at the examples from the docstring (:func:`deprecate_dict_entry`). + + Deploying --------- diff --git a/glotaran/builtin/io/yml/yml.py b/glotaran/builtin/io/yml/yml.py index f0b0a42df..e45ad6cca 100644 --- a/glotaran/builtin/io/yml/yml.py +++ b/glotaran/builtin/io/yml/yml.py @@ -7,6 +7,7 @@ import yaml +from glotaran.deprecation.modules.builtin_io_yml import model_spec_deprecations from glotaran.io import ProjectIoInterface from glotaran.io import load_dataset from glotaran.io import load_model @@ -19,7 +20,6 @@ from glotaran.parameter import ParameterGroup from glotaran.project import SavingOptions from glotaran.project import Scheme -from glotaran.utils.sanitize import check_deprecations from glotaran.utils.sanitize import sanitize_yaml if TYPE_CHECKING: @@ -49,7 +49,7 @@ def load_model(self, file_name: str) -> Model: with open(file_name) as f: spec = yaml.safe_load(f) - check_deprecations(spec) + model_spec_deprecations(spec) spec = sanitize_yaml(spec) diff --git a/glotaran/deprecation/__init__.py b/glotaran/deprecation/__init__.py index fd4578f55..14edceab3 100644 --- a/glotaran/deprecation/__init__.py +++ b/glotaran/deprecation/__init__.py @@ -1,5 +1,6 @@ """Deprecation helpers and place to put deprecated implementations till removing.""" from glotaran.deprecation.deprecation_utils import deprecate +from glotaran.deprecation.deprecation_utils import deprecate_dict_entry from glotaran.deprecation.deprecation_utils import deprecate_module_attribute from glotaran.deprecation.deprecation_utils import deprecate_submodule from glotaran.deprecation.deprecation_utils import warn_deprecated diff --git a/glotaran/deprecation/deprecation_utils.py b/glotaran/deprecation/deprecation_utils.py index 8f9c18db1..d6547116b 100644 --- a/glotaran/deprecation/deprecation_utils.py +++ b/glotaran/deprecation/deprecation_utils.py @@ -11,10 +11,15 @@ from typing import TYPE_CHECKING from typing import Any from typing import Callable +from typing import Hashable +from typing import Mapping +from typing import MutableMapping from typing import TypeVar from typing import cast from warnings import warn +import numpy as np + DecoratedCallable = TypeVar( "DecoratedCallable", bound=Callable[..., Any] ) # decorated function or class @@ -32,6 +37,20 @@ class OverDueDeprecation(Exception): warn_deprecated deprecate_module_attribute deprecate_submodule + deprecate_dict_entry + """ + + +class GlotaranApiDeprecationWarning(UserWarning): + """Warning to give users about API changes. + + See Also + -------- + deprecate + warn_deprecated + deprecate_module_attribute + deprecate_submodule + deprecate_dict_entry """ @@ -166,8 +185,8 @@ def warn_deprecated( will import ``package.module.class`` and check if ``class`` has an attribute ``mapping``. - Warns - ----- + Raises + ------ OverDueDeprecation If the current version is greater or equal to ``end_of_life_version``. @@ -212,7 +231,7 @@ def read_parameters_from_yaml_file(model_path: str): selected_indices = importable_indices[: len(selected_qual_names)] check_qualnames_in_tests(qual_names=selected_qual_names, importable_indices=selected_indices) warn( - DeprecationWarning( + GlotaranApiDeprecationWarning( f"Usage of {deprecated_qual_name_usage!r} was deprecated, " f"use {new_qual_name_usage!r} instead.\n" f"This usage will be an error in version: {to_be_removed_in_version!r}." @@ -265,8 +284,8 @@ def deprecate( DecoratedCallable Original function or class throwing a Deprecation warning when used. - Warns - ----- + Raises + ------ OverDueDeprecation If the current version is greater or equal to ``end_of_life_version``. @@ -334,6 +353,134 @@ def outer_wrapper(deprecated_object: DecoratedCallable) -> DecoratedCallable: return cast(Callable[[DecoratedCallable], DecoratedCallable], outer_wrapper) +def deprecate_dict_entry( + *, + dict_to_check: MutableMapping[Hashable, Any], + deprecated_usage: str, + new_usage: str, + to_be_removed_in_version: str, + swap_keys: tuple[Hashable, Hashable] | None = None, + replace_rules: tuple[Mapping[Hashable, Any], Mapping[Hashable, Any]] | None = None, + stacklevel: int = 3, +) -> None: + """Replace dict entry inplace and warn about usage change, if present in the dict. + + Parameters + ---------- + dict_to_check : MutableMapping[Hashable, Any] + Dict which should be checked. + deprecated_usage : str + Old usage to inform user (only used in warning). + new_usage : str + New usage to inform user (only used in warning). + to_be_removed_in_version : str + Version the support for this usage will be removed. + swap_keys : tuple[Hashable, Hashable] + (old_key, new_key), + ``dict_to_check[new_key]`` will be assigned the value ``dict_to_check[old_key]`` + and ``old_key`` will be removed from the dict. + by default None + replace_rules : Mapping[Hashable, tuple[Any, Any]] + ({old_key: old_value}, {new_key: new_value}), + If ``dict_to_check[old_key]`` has the value ``old_value``, + ``dict_to_check[new_key]`` it will be set to ``new_value``. + ``old_key`` will be removed from the dict if ``old_key`` and ``new_key`` aren't equal. + by default None + stacklevel : int + Stack at which the warning should be shown as raise. , by default 3 + + + Raises + ------ + ValueError + If both ``swap_keys`` and ``replace_rules`` are None (default) or not None. + OverDueDeprecation + If the current version is greater or equal to ``end_of_life_version``. + + See Also + -------- + warn_deprecated + + Notes + ----- + To prevent confusion exactly one of ``replace_rules`` and ``swap_keys`` + needs to be passed. + + Examples + -------- + For readability sake the warnings won't be shown in the examples. + + Swapping key names: + + >>> dict_to_check = {"foo": 123} + >>> deprecate_dict_entry( + dict_to_check=dict_to_check, + deprecated_usage="foo", + new_usage="bar", + to_be_removed_in_version="0.6.0", + swap_keys=("foo", "bar") + ) + >>> dict_to_check + {"bar": 123} + + Changing values: + + >>> dict_to_check = {"foo": 123} + >>> deprecate_dict_entry( + dict_to_check=dict_to_check, + deprecated_usage="foo: 123", + new_usage="foo: 123.0", + to_be_removed_in_version="0.6.0", + replace_rules=({"foo": 123}, {"foo": 123.0}) + ) + >>> dict_to_check + {"foo": 123.0} + + Swapping key names AND changing values: + + >>> dict_to_check = {"type": "kinetic-spectrum"} + >>> deprecate_dict_entry( + dict_to_check=dict_to_check, + deprecated_usage="type: kinectic-spectrum", + new_usage="default-megacomplex: decay", + to_be_removed_in_version="0.6.0", + replace_rules=({"type": "kinetic-spectrum"}, {"default-megacomplex": "decay"}) + ) + >>> dict_to_check + {"default-megacomplex": "decay"} + + + .. # noqa: DAR402 + """ + dict_changed = False + + if not np.logical_xor(swap_keys is None, replace_rules is None): + raise ValueError( + "Exactly one of the parameters `swap_keys` or `replace_rules` needs to be provided." + ) + if swap_keys is not None and swap_keys[0] in dict_to_check: + dict_changed = True + dict_to_check[swap_keys[1]] = dict_to_check[swap_keys[0]] + del dict_to_check[swap_keys[0]] + if replace_rules is not None: + old_key, old_value = next(iter(replace_rules[0].items())) + new_key, new_value = next(iter(replace_rules[1].items())) + if old_key in dict_to_check and dict_to_check[old_key] == old_value: + dict_changed = True + dict_to_check[new_key] = new_value + if new_key != old_key: + del dict_to_check[old_key] + + if dict_changed: + warn_deprecated( + deprecated_qual_name_usage=deprecated_usage, + new_qual_name_usage=new_usage, + to_be_removed_in_version=to_be_removed_in_version, + stacklevel=stacklevel, + check_qual_names=(False, False), + ) + + def module_attribute(module_qual_name: str, attribute_name: str) -> Any: """Import and return the attribute (e.g. function or class) of a module. @@ -384,6 +531,11 @@ def deprecate_module_attribute( Any Module attribute from its new location. + Raises + ------ + OverDueDeprecation + If the current version is greater or equal to ``end_of_life_version``. + See Also -------- deprecate @@ -412,6 +564,8 @@ def __getattr__(attribute_name: str): raise AttributeError(f"module {__name__} has no attribute {attribute_name}") + + .. # noqa: DAR402 """ module_name = ".".join(new_qual_name.split(".")[:-1]) attribute_name = new_qual_name.split(".")[-1] @@ -457,6 +611,11 @@ def deprecate_submodule( ModuleType Module containing + Raises + ------ + OverDueDeprecation + If the current version is greater or equal to ``end_of_life_version``. + See Also -------- deprecate @@ -478,6 +637,9 @@ def deprecate_submodule( new_module_name="glotaran.project.result", to_be_removed_in_version="0.6.0", ) + + + .. # noqa: DAR402 """ new_module = import_module(new_module_name) deprecated_module = ModuleType( diff --git a/glotaran/deprecation/modules/builtin_io_yml.py b/glotaran/deprecation/modules/builtin_io_yml.py new file mode 100644 index 000000000..635a5a9a9 --- /dev/null +++ b/glotaran/deprecation/modules/builtin_io_yml.py @@ -0,0 +1,87 @@ +"""Deprecation functions for the yaml parser.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from glotaran.deprecation import deprecate_dict_entry + +if TYPE_CHECKING: + from typing import Any + from typing import MutableMapping + + +def model_spec_deprecations(spec: MutableMapping[Any, Any]) -> None: + """Check deprecations in the model specification ``spec`` dict. + + Parameters + ---------- + spec : MutableMapping[Any, Any] + Model specification dictionary + """ + load_model_stack_level = 7 + deprecate_dict_entry( + dict_to_check=spec, + deprecated_usage="type: kinetic-spectrum", + new_usage="default-megacomplex: decay", + to_be_removed_in_version="0.7.0", + replace_rules=({"type": "kinetic-spectrum"}, {"default-megacomplex": "decay"}), + stacklevel=load_model_stack_level, + ) + + deprecate_dict_entry( + dict_to_check=spec, + deprecated_usage="type: spectrum", + new_usage="default-megacomplex: spectral", + to_be_removed_in_version="0.7.0", + replace_rules=({"type": "spectrum"}, {"default-megacomplex": "spectral"}), + stacklevel=load_model_stack_level, + ) + + deprecate_dict_entry( + dict_to_check=spec, + deprecated_usage="spectral_relations", + new_usage="relations", + to_be_removed_in_version="0.7.0", + swap_keys=("spectral_relations", "relations"), + stacklevel=load_model_stack_level, + ) + + if "relations" in spec: + for relation in spec["relations"]: + deprecate_dict_entry( + dict_to_check=relation, + deprecated_usage="compartment", + new_usage="source", + to_be_removed_in_version="0.7.0", + swap_keys=("compartment", "source"), + stacklevel=load_model_stack_level, + ) + + deprecate_dict_entry( + dict_to_check=spec, + deprecated_usage="spectral_constraints", + new_usage="constraints", + to_be_removed_in_version="0.7.0", + swap_keys=("spectral_constraints", "constraints"), + stacklevel=load_model_stack_level, + ) + + if "constraints" in spec: + for constraint in spec["constraints"]: + deprecate_dict_entry( + dict_to_check=constraint, + deprecated_usage="constraint.compartment", + new_usage="constraint.target", + to_be_removed_in_version="0.7.0", + swap_keys=("compartment", "target"), + stacklevel=load_model_stack_level, + ) + + deprecate_dict_entry( + dict_to_check=spec, + deprecated_usage="equal_area_penalties", + new_usage="clp_area_penalties", + to_be_removed_in_version="0.7.0", + swap_keys=("equal_area_penalties", "clp_area_penalties"), + stacklevel=load_model_stack_level, + ) diff --git a/glotaran/deprecation/modules/test/__init__.py b/glotaran/deprecation/modules/test/__init__.py index dfff9c230..9e67df7e7 100644 --- a/glotaran/deprecation/modules/test/__init__.py +++ b/glotaran/deprecation/modules/test/__init__.py @@ -1,16 +1,21 @@ """Package with deprecation tests and helper functions.""" from __future__ import annotations +from pathlib import Path from typing import TYPE_CHECKING import pytest +from glotaran.deprecation.deprecation_utils import GlotaranApiDeprecationWarning + if TYPE_CHECKING: from typing import Any from typing import Callable from typing import Mapping from typing import Sequence + from _pytest.recwarn import WarningsRecorder + def deprecation_warning_on_call_test_helper( deprecated_callable: Callable[..., Any], @@ -18,7 +23,7 @@ def deprecation_warning_on_call_test_helper( raise_exception=False, args: Sequence[Any] = [], kwargs: Mapping[str, Any] = {}, -) -> Any: +) -> tuple[WarningsRecorder, Any]: """Helperfunction to quickly test that a deprecated class or function warns. By default this ignores error when calling the function/class, @@ -41,17 +46,24 @@ def deprecation_warning_on_call_test_helper( Returns ------- - Any - Return value of deprecated_callable + tuple[WarningsRecorder, Any] + Tuple of the WarningsRecorder and return value of deprecated_callable Raises ------ Exception Exception caused by deprecated_callable if raise_exception is True. """ - with pytest.warns(DeprecationWarning): + with pytest.warns(GlotaranApiDeprecationWarning) as record: try: - return deprecated_callable(*args, **kwargs) - except Exception: + result = deprecated_callable(*args, **kwargs) + + assert len(record) >= 1 + assert Path(record[0].filename) == Path(__file__) + + return record, result + + except Exception as e: if raise_exception: - raise + raise e + return record, None diff --git a/glotaran/deprecation/modules/test/test_builtin_io_yml.py b/glotaran/deprecation/modules/test/test_builtin_io_yml.py new file mode 100644 index 000000000..26202b947 --- /dev/null +++ b/glotaran/deprecation/modules/test/test_builtin_io_yml.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from textwrap import dedent +from typing import TYPE_CHECKING + +import pytest + +import glotaran.builtin.io.yml.yml as yml_module +from glotaran.deprecation.modules.test import deprecation_warning_on_call_test_helper +from glotaran.io import load_model + +if TYPE_CHECKING: + from typing import Any + + from _pytest.monkeypatch import MonkeyPatch + + +@pytest.mark.parametrize( + "model_yml_str, expected_nr_of_warnings, expected_key, expected_value", + ( + ("type: kinetic-spectrum", 1, "default-megacomplex", "decay"), + ("type: spectrum", 1, "default-megacomplex", "spectral"), + ( + dedent( + """ + spectral_relations: + - compartment: s1 + - compartment: s2 + """ + ), + 3, + "relations", + [{"source": "s1"}, {"source": "s2"}], + ), + ( + dedent( + """ + spectral_constraints: + - compartment: s1 + - compartment: s2 + """ + ), + 3, + "constraints", + [{"target": "s1"}, {"target": "s2"}], + ), + ( + dedent( + """ + equal_area_penalties: + - type: equal_area + """ + ), + 1, + "clp_area_penalties", + [{"type": "equal_area"}], + ), + ), + ids=( + "type: kinetic-spectrum", + "type: spectrum", + "spectral_relations", + "spectral_constraints", + "equal_area_penalties", + ), +) +def test_model_spec_deprecations( + monkeypatch: MonkeyPatch, + model_yml_str: str, + expected_nr_of_warnings: int, + expected_key: str, + expected_value: Any, +): + """Warning gets emitted by load_model""" + return_dicts = [] + with monkeypatch.context() as m: + m.setattr(yml_module, "sanitize_yaml", lambda spec: return_dicts.append(spec)) + record, _ = deprecation_warning_on_call_test_helper( + load_model, args=(model_yml_str,), kwargs={"format_name": "yml_str"} + ) + + return_dict = return_dicts[0] + + assert expected_key in return_dict + assert return_dict[expected_key] == expected_value + + assert len(record) == expected_nr_of_warnings diff --git a/glotaran/deprecation/modules/test/test_changed_imports.py b/glotaran/deprecation/modules/test/test_changed_imports.py index 1911cc80d..2c7673bd7 100644 --- a/glotaran/deprecation/modules/test/test_changed_imports.py +++ b/glotaran/deprecation/modules/test/test_changed_imports.py @@ -6,6 +6,7 @@ import pytest +from glotaran.deprecation.deprecation_utils import GlotaranApiDeprecationWarning from glotaran.deprecation.deprecation_utils import module_attribute from glotaran.io import load_dataset from glotaran.parameter import ParameterGroup @@ -24,7 +25,7 @@ def check_recwarn(records: WarningsRecorder, warn_nr=1): print(record) assert len(records) == warn_nr - assert records[0].category == DeprecationWarning + assert records[0].category == GlotaranApiDeprecationWarning records.clear() diff --git a/glotaran/deprecation/modules/test/test_glotaran_root.py b/glotaran/deprecation/modules/test/test_glotaran_root.py index bb02eab34..f1a8f9cc6 100644 --- a/glotaran/deprecation/modules/test/test_glotaran_root.py +++ b/glotaran/deprecation/modules/test/test_glotaran_root.py @@ -11,6 +11,7 @@ from glotaran import read_parameters_from_csv_file from glotaran import read_parameters_from_yaml from glotaran import read_parameters_from_yaml_file +from glotaran.deprecation.deprecation_utils import GlotaranApiDeprecationWarning from glotaran.deprecation.modules.test import deprecation_warning_on_call_test_helper from glotaran.model import Model from glotaran.parameter import ParameterGroup @@ -20,7 +21,7 @@ def dummy_warn(foo, bar=False): - warn(DeprecationWarning("foo")) + warn(GlotaranApiDeprecationWarning("foo"), stacklevel=2) if not isinstance(bar, bool): raise ValueError("not a bool") return foo, bar @@ -32,10 +33,10 @@ def dummy_no_warn(foo, bar=False): def test_deprecation_warning_on_call_test_helper(): """Correct result passed on""" - result = deprecation_warning_on_call_test_helper( + record, result = deprecation_warning_on_call_test_helper( dummy_warn, args=["foo"], kwargs={"bar": True} ) - + assert len(record) == 1 assert result == ("foo", True) @@ -60,7 +61,7 @@ def test_read_model_from_yaml(): type: kinetic-spectrum megacomplex: {} """ - result = deprecation_warning_on_call_test_helper( + _, result = deprecation_warning_on_call_test_helper( read_model_from_yaml, args=[yaml], raise_exception=True ) @@ -75,7 +76,7 @@ def test_read_model_from_yaml_file(tmp_path: Path): """ model_file = tmp_path / "model.yaml" model_file.write_text(yaml) - result = deprecation_warning_on_call_test_helper( + _, result = deprecation_warning_on_call_test_helper( read_model_from_yaml_file, args=[str(model_file)], raise_exception=True ) @@ -86,7 +87,7 @@ def test_read_parameters_from_csv_file(tmp_path: Path): """read_parameters_from_csv_file raises warning""" parameters_file = tmp_path / "parameters.csv" parameters_file.write_text("label,value\nfoo,123") - result = deprecation_warning_on_call_test_helper( + _, result = deprecation_warning_on_call_test_helper( read_parameters_from_csv_file, args=[str(parameters_file)], raise_exception=True, @@ -98,7 +99,7 @@ def test_read_parameters_from_csv_file(tmp_path: Path): def test_read_parameters_from_yaml(): """read_parameters_from_yaml raises warning""" - result = deprecation_warning_on_call_test_helper( + _, result = deprecation_warning_on_call_test_helper( read_parameters_from_yaml, args=["foo:\n - 123"], raise_exception=True ) @@ -111,7 +112,7 @@ def test_read_parameters_from_yaml_file(tmp_path: Path): """read_parameters_from_yaml_file raises warning""" parameters_file = tmp_path / "parameters.yaml" parameters_file.write_text("foo:\n - 123") - result = deprecation_warning_on_call_test_helper( + _, result = deprecation_warning_on_call_test_helper( read_parameters_from_yaml_file, args=[str(parameters_file)], raise_exception=True ) diff --git a/glotaran/deprecation/modules/test/test_project_result.py b/glotaran/deprecation/modules/test/test_project_result.py index 7dc9ec6a4..83d2c5248 100644 --- a/glotaran/deprecation/modules/test/test_project_result.py +++ b/glotaran/deprecation/modules/test/test_project_result.py @@ -33,7 +33,7 @@ def test_Result_save_method(tmpdir: LocalPath, dummy_result: Result): # noqa: F def test_Result_get_dataset_method(dummy_result: Result): # noqa: F811 """Result.get_dataset(dataset_label) gives correct dataset.""" - result = deprecation_warning_on_call_test_helper( + _, result = deprecation_warning_on_call_test_helper( dummy_result.get_dataset, args=["dataset1"], raise_exception=True ) diff --git a/glotaran/deprecation/modules/test/test_project_sheme.py b/glotaran/deprecation/modules/test/test_project_sheme.py index 93ba18793..42ce6daa1 100644 --- a/glotaran/deprecation/modules/test/test_project_sheme.py +++ b/glotaran/deprecation/modules/test/test_project_sheme.py @@ -46,7 +46,7 @@ def test_Scheme_from_yaml_file_method(tmp_path: Path): dataset1: {dataset_path}""" ) - result = deprecation_warning_on_call_test_helper( + _, result = deprecation_warning_on_call_test_helper( Scheme.from_yaml_file, args=[str(scheme_path)], raise_exception=True ) diff --git a/glotaran/deprecation/test/test_deprecation_utils.py b/glotaran/deprecation/test/test_deprecation_utils.py index 08db4e82c..786864d25 100644 --- a/glotaran/deprecation/test/test_deprecation_utils.py +++ b/glotaran/deprecation/test/test_deprecation_utils.py @@ -7,14 +7,20 @@ import pytest import glotaran +from glotaran.deprecation.deprecation_utils import GlotaranApiDeprecationWarning from glotaran.deprecation.deprecation_utils import OverDueDeprecation from glotaran.deprecation.deprecation_utils import deprecate +from glotaran.deprecation.deprecation_utils import deprecate_dict_entry from glotaran.deprecation.deprecation_utils import glotaran_version from glotaran.deprecation.deprecation_utils import module_attribute from glotaran.deprecation.deprecation_utils import parse_version from glotaran.deprecation.deprecation_utils import warn_deprecated if TYPE_CHECKING: + from typing import Any + from typing import Hashable + from typing import Mapping + from _pytest.monkeypatch import MonkeyPatch from _pytest.recwarn import WarningsRecorder @@ -79,7 +85,7 @@ def test_parse_version_errors(version_str: str): @pytest.mark.usefixtures("glotaran_0_3_0") def test_warn_deprecated(): """Warning gets shown when all is in order.""" - with pytest.warns(DeprecationWarning) as record: + with pytest.warns(GlotaranApiDeprecationWarning) as record: warn_deprecated( deprecated_qual_name_usage=DEPRECATION_QUAL_NAME, new_qual_name_usage=NEW_QUAL_NAME, @@ -169,7 +175,7 @@ def test_warn_deprecated_broken_qualname_no_check( deprecated_qual_name_usage: str, new_qual_name_usage: str, check_qualnames: tuple[bool, bool] ): """Not checking broken imports.""" - with pytest.warns(DeprecationWarning): + with pytest.warns(GlotaranApiDeprecationWarning): warn_deprecated( deprecated_qual_name_usage=deprecated_qual_name_usage, new_qual_name_usage=new_qual_name_usage, @@ -181,7 +187,7 @@ def test_warn_deprecated_broken_qualname_no_check( @pytest.mark.usefixtures("glotaran_0_3_0") def test_warn_deprecated_sliced_method(): """Slice away method for importing and check class for attribute""" - with pytest.warns(DeprecationWarning): + with pytest.warns(GlotaranApiDeprecationWarning): warn_deprecated( deprecated_qual_name_usage=( "glotaran.deprecation.test.test_deprecation_utils.DummyClass.foo()" @@ -195,7 +201,7 @@ def test_warn_deprecated_sliced_method(): @pytest.mark.usefixtures("glotaran_0_3_0") def test_warn_deprecated_sliced_mapping(): """Slice away mapping for importing and check class for attribute""" - with pytest.warns(DeprecationWarning): + with pytest.warns(GlotaranApiDeprecationWarning): warn_deprecated( deprecated_qual_name_usage=( "glotaran.deprecation.test.test_deprecation_utils.DummyClass.foo['bar']" @@ -238,7 +244,7 @@ def dummy(): assert dummy.__doc__ == "Dummy docstring for testing." assert len(recwarn) == 1 - assert recwarn[0].category == DeprecationWarning + assert recwarn[0].category == GlotaranApiDeprecationWarning assert recwarn[0].message.args[0] == DEPRECATION_WARN_MESSAGE # type: ignore [union-attr] assert Path(recwarn[0].filename) == Path(__file__) @@ -269,7 +275,7 @@ def from_string(cls, string: str): assert Foo.__doc__ == "Foo class docstring for testing." assert len(recwarn) == 1 - assert recwarn[0].category == DeprecationWarning + assert recwarn[0].category == GlotaranApiDeprecationWarning assert recwarn[0].message.args[0] == DEPRECATION_WARN_MESSAGE # type: ignore [union-attr] assert Path(recwarn[0].filename) == Path(__file__) @@ -278,6 +284,145 @@ def from_string(cls, string: str): assert len(recwarn) == 2 +@pytest.mark.usefixtures("glotaran_0_3_0") +def test_deprecate_dict_key_swap_keys(): + """Replace old with new key while keeping the value.""" + test_dict = {"foo": 123} + with pytest.warns( + GlotaranApiDeprecationWarning, match="'foo'.+was deprecated, use 'bar'" + ) as record: + deprecate_dict_entry( + dict_to_check=test_dict, + deprecated_usage="foo", + new_usage="bar", + to_be_removed_in_version="0.6.0", + swap_keys=("foo", "bar"), + ) + + assert "bar" in test_dict + assert test_dict["bar"] == 123 + assert "foo" not in test_dict + + assert len(record) == 1 + assert Path(record[0].filename) == Path(__file__) + + +@pytest.mark.usefixtures("glotaran_0_3_0") +def test_deprecate_dict_key_replace_rules_only_values(): + """Replace old value for key with new value.""" + test_dict = {"foo": 123} + with pytest.warns( + GlotaranApiDeprecationWarning, match="'foo: 123'.+was deprecated, use 'foo: 321'" + ) as record: + deprecate_dict_entry( + dict_to_check=test_dict, + deprecated_usage="foo: 123", + new_usage="foo: 321", + to_be_removed_in_version="0.6.0", + replace_rules=({"foo": 123}, {"foo": 321}), + ) + + assert "foo" in test_dict + assert test_dict["foo"] == 321 + + assert len(record) == 1 + assert Path(record[0].filename) == Path(__file__) + + +@pytest.mark.usefixtures("glotaran_0_3_0") +def test_deprecate_dict_key_replace_rules_keys_and_values(): + """Replace old with new key AND replace old value for key with new value.""" + test_dict = {"foo": 123} + with pytest.warns( + GlotaranApiDeprecationWarning, match="'foo: 123'.+was deprecated, use 'bar: 321'" + ) as record: + deprecate_dict_entry( + dict_to_check=test_dict, + deprecated_usage="foo: 123", + new_usage="bar: 321", + to_be_removed_in_version="0.6.0", + replace_rules=({"foo": 123}, {"bar": 321}), + ) + + assert "bar" in test_dict + assert test_dict["bar"] == 321 + assert "foo" not in test_dict + + assert len(record) == 1 + assert Path(record[0].filename) == Path(__file__) + + +@pytest.mark.xfail(strict=True) +@pytest.mark.usefixtures("glotaran_0_3_0") +def test_deprecate_dict_key_does_not_apply_swap_keys(): + """Don't warn if the dict doesn't change because old_key didn't match""" + + with pytest.warns( + GlotaranApiDeprecationWarning, match="'foo: 123'.+was deprecated, use 'foo: 321'" + ): + deprecate_dict_entry( + dict_to_check={"foo": 123}, + deprecated_usage="foo: 123", + new_usage="foo: 321", + to_be_removed_in_version="0.6.0", + swap_keys=("bar", "baz"), + ) + + +@pytest.mark.xfail(strict=True) +@pytest.mark.parametrize( + "replace_rules", + ( + ({"bar": 123}, {"bar": 321}), + ({"foo": 111}, {"bar": 321}), + ), +) +@pytest.mark.usefixtures("glotaran_0_3_0") +def test_deprecate_dict_key_does_not_apply( + replace_rules: tuple[Mapping[Hashable, Any], Mapping[Hashable, Any]] +): + """Don't warn if the dict doesn't change because old_key or old_value didn't match""" + with pytest.warns( + GlotaranApiDeprecationWarning, match="'foo: 123'.+was deprecated, use 'foo: 321'" + ): + deprecate_dict_entry( + dict_to_check={"foo": 123}, + deprecated_usage="foo: 123", + new_usage="foo: 321", + to_be_removed_in_version="0.6.0", + replace_rules=replace_rules, + ) + + +@pytest.mark.parametrize( + "swap_keys, replace_rules", + ( + (None, None), + (("bar", "baz"), ({"bar": 1}, {"baz": 2})), + ), +) +@pytest.mark.usefixtures("glotaran_0_3_0") +def test_deprecate_dict_key_error_no_action( + swap_keys: tuple[Hashable, Hashable] | None, + replace_rules: tuple[Mapping[Hashable, Any], Mapping[Hashable, Any]] | None, +): + """Raise error if none or both `swap_keys` and `replace_rules` were provided.""" + with pytest.raises( + ValueError, + match=( + r"Exactly one of the parameters `swap_keys` or `replace_rules` needs to be provided\." + ), + ): + deprecate_dict_entry( + dict_to_check={}, + deprecated_usage="", + new_usage="", + to_be_removed_in_version="", + swap_keys=swap_keys, + replace_rules=replace_rules, + ) + + def test_module_attribute(): """Same code as the original import""" @@ -290,7 +435,7 @@ def test_module_attribute(): def test_deprecate_module_attribute(): """Same code as the original import and warning""" - with pytest.warns(DeprecationWarning) as record: + with pytest.warns(GlotaranApiDeprecationWarning) as record: from glotaran.deprecation.test.dummy_package.deprecated_module_attribute import ( deprecated_attribute, @@ -312,7 +457,7 @@ def test_deprecate_submodule(recwarn: WarningsRecorder): ) assert len(recwarn) == 1 - assert recwarn[0].category == DeprecationWarning + assert recwarn[0].category == GlotaranApiDeprecationWarning @pytest.mark.usefixtures("glotaran_0_3_0") @@ -324,7 +469,7 @@ def test_deprecate_submodule_from_import(recwarn: WarningsRecorder): ) assert len(recwarn) == 1 - assert recwarn[0].category == DeprecationWarning + assert recwarn[0].category == GlotaranApiDeprecationWarning assert Path(recwarn[0].filename) == Path(__file__) diff --git a/glotaran/utils/sanitize.py b/glotaran/utils/sanitize.py index d26a97d3e..8a3f40ebe 100644 --- a/glotaran/utils/sanitize.py +++ b/glotaran/utils/sanitize.py @@ -3,7 +3,6 @@ from typing import Any -from glotaran.deprecation import warn_deprecated from glotaran.utils.regex import RegexPattern as rp @@ -216,83 +215,3 @@ def sanitize_parameter_list(parameter_list: list[str | float]) -> list[str | flo parameter_list[i] = convert_scientific_to_float(value) return parameter_list - - -def check_deprecations(spec: dict): - """Check deprecations in a `spec` dict. - - Parameters - ---------- - spec : dict - A specification dictionary - """ - if "type" in spec: - if spec["type"] == "kinetic-spectrum": - warn_deprecated( - deprecated_qual_name_usage="type: kinectic-spectrum", - new_qual_name_usage="default-megacomplex: decay", - to_be_removed_in_version="0.6.0", - check_qual_names=(False, False), - ) - spec["default-megacomplex"] = "decay" - elif spec["type"] == "spectral": - warn_deprecated( - deprecated_qual_name_usage="type: spectral", - new_qual_name_usage="default-megacomplex: spectral", - to_be_removed_in_version="0.6.0", - check_qual_names=(False, False), - ) - spec["default-megacomplex"] = "spectral" - del spec["type"] - - if "spectral_relations" in spec: - warn_deprecated( - deprecated_qual_name_usage="spectral_relations", - new_qual_name_usage="relations", - to_be_removed_in_version="0.6.0", - check_qual_names=(False, False), - ) - spec["relations"] = spec["spectral_relations"] - del spec["spectral_relations"] - - for i, relation in enumerate(spec["relations"]): - if "compartment" in relation: - warn_deprecated( - deprecated_qual_name_usage="relation.compartment", - new_qual_name_usage="relation.source", - to_be_removed_in_version="0.6.0", - check_qual_names=(False, False), - ) - relation["source"] = relation["compartment"] - del relation["compartment"] - - if "spectral_constraints" in spec: - warn_deprecated( - deprecated_qual_name_usage="spectral_constraints", - new_qual_name_usage="constraints", - to_be_removed_in_version="0.6.0", - check_qual_names=(False, False), - ) - spec["constraints"] = spec["spectral_constraints"] - del spec["spectral_constraints"] - - for i, constraint in enumerate(spec["constraints"]): - if "compartment" in constraint: - warn_deprecated( - deprecated_qual_name_usage="constraint.compartment", - new_qual_name_usage="constraint.target", - to_be_removed_in_version="0.6.0", - check_qual_names=(False, False), - ) - constraint["target"] = constraint["compartment"] - del constraint["compartment"] - - if "equal_area_penalties" in spec: - warn_deprecated( - deprecated_qual_name_usage="equal_area_penalties", - new_qual_name_usage="clp_area_penalties", - to_be_removed_in_version="0.6.0", - check_qual_names=(False, False), - ) - spec["clp_area_penalties"] = spec["equal_area_penalties"] - del spec["equal_area_penalties"] diff --git a/tox.ini b/tox.ini index 6fe9c651c..4a4dfeee2 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ envlist = py{38}, pre-commit, docs, docs-notebooks, docs-links ; Uncomment to ignore deprecation warnings coming from pyglotaran ; (this helps to see the warnings from dependencies) ; filterwarnings = -; ignore:.+glotaran:DeprecationWarning +; ignore:.+glotaran:GlotaranApiDeprecationWarning [flake8] extend-ignore = E231, E203