Repository: cogent3/cogent3
Pull request #1369 (2023-04-17 23:46:50)

added is_discontinued to deprecated_args decorater
fixes issue #1364   


Patch:
diff --git a/changelog.md b/changelog.md
index 4c94a87b0..7db357e57 100755
--- a/changelog.md
+++ b/changelog.md
@@ -19,16 +19,16 @@ Thanks to dgslos who raised the issue regarding IUPAC consensus. Thanks to users
 ### Composable apps
 
 - app_help() and get_app() available as top-level imports.
-   - app_help() takes the name of the app as a string and displays its summary, parameters and how to create one using get_app().
-   - get_app() creates an app instance given its name as a string and constructor arguments.
+     - app_help() takes the name of the app as a string and displays its summary, parameters and how to create one using get_app().
+     - get_app() creates an app instance given its name as a string and constructor arguments.
 - added skip_not_completed keyword parameter to define_app decorator.
-   - Some apps need to process NotCompleted instances. The current `app.__call__` method returns these instances immediately without passing through to the apps `.main()` method. The change introduces a semi-private attribute `_skip_not_completed` to the class. If it's False, the instance will be passed to `main()`.
+    - Some apps need to process NotCompleted instances. The current `app.__call__` method returns these instances immediately without passing through to the apps `.main()` method. The change introduces a semi-private attribute `_skip_not_completed` to the class. If it's False, the instance will be passed to `main()`.
 - composable data validation now allows NotCompleted
-   - if <app>.input returned a NotCompleted, it was being treated as an invalid data type rather than preserving the original cause for failure. The data validation method now immediately returns a provided NotCompleted instance
+    - if <app>.input returned a NotCompleted, it was being treated as an invalid data type rather than preserving the original cause for failure. The data validation method now immediately returns a provided NotCompleted instance
 - add argument id_from_source to all writer apps for a naming callback
-  - It should be a callable that generates a unique ID from input data
-  - Defaults to new get_unique_id() function, which extracts base name by calling get_data_source() and processing the result, removing file suffixes identified by get_format_suffixes().
-  - this means filename suffixes are dropped more cleanly
+    - It should be a callable that generates a unique ID from input data
+    - Defaults to new get_unique_id() function, which extracts base name by calling get_data_source() and processing the result, removing file suffixes identified by get_format_suffixes().
+    - this means filename suffixes are dropped more cleanly
 - new app to_primitive(). This uses a to_rich_dict() method if available otherwise it just returns the original object.
 - new app from_primitive(). This takes a dict and deserialises the object using the standard cogent3 functions.
 - new app pickle_it(). Does as the name implies.
@@ -41,8 +41,8 @@ Thanks to dgslos who raised the issue regarding IUPAC consensus. Thanks to users
   - these are to_primitive() + pickle_it() (and the reverse)
 - app.typing.get_constraint_names() now supports all standard python Sequence built-ins (list, tuple, set).
 - add type resolver for nested types
-  - function resolves the type tree of nested types and also returns the depth of that type tree
-  - ensure custom apps don't have excessive nested types. The motivation for this check is it is difficult to efficiently resolve, so we advise the developer (via a TypeError message) to define a custom class for such complex types. They can then choose to validate construction of those class attributes themselves.
+    - function resolves the type tree of nested types and also returns the depth of that type tree
+    - ensure custom apps don't have excessive nested types. The motivation for this check is it is difficult to efficiently resolve, so we advise the developer (via a TypeError message) to define a custom class for such complex types. They can then choose to validate construction of those class attributes themselves.
 
 ### DataStores
 
@@ -160,17 +160,17 @@ We will drop support for python 3.7 by release 2022.10.
 ## ENH
 
 - new `cogent3.util.parallel.as_completed()` generator function
-  - `as_completed()` wraps MPI or `concurrent.futures` executors and delivers results as they are completed. In contrast, `parallel.imap()` / `parallel.map()` deliver results in the same order as the input series. The advantage of `as_completed()` is the interval of result arrival at the parent process is better distributed.
+    - `as_completed()` wraps MPI or `concurrent.futures` executors and delivers results as they are completed. In contrast, `parallel.imap()` / `parallel.map()` deliver results in the same order as the input series. The advantage of `as_completed()` is the interval of result arrival at the parent process is better distributed.
 - new function `cogent3.load_seq()` loads a single sequence from a file
 - convert substitution model `__str__` to `__repr__`; more useful since `__repr__` is called also by str().
 
 ## BUG
 
 - fixes to `annotation_from_gff()` method on annotatable sequence / alignment objects
-  - method would break if GFF records had no ID. This situation is quite common in some Ensembl gff3 files. We generate a "no-id-#" identifier in those cases.
-  - we now add features are added to their parent feature.
+    - method would break if GFF records had no ID. This situation is quite common in some Ensembl gff3 files. We generate a "no-id-#" identifier in those cases.
+    - we now add features are added to their parent feature.
 - improve consistency in setting motif_probs on likelihood function
-  - only apply a pseudocount if optimising motif probs and at least one state has zero frequency, default pseudocount is 0.5. Thanks to StephenRogers1 for finding this issue!
+    - only apply a pseudocount if optimising motif probs and at least one state has zero frequency, default pseudocount is 0.5. Thanks to StephenRogers1 for finding this issue!
 
 ## DOC
 
@@ -289,4 +289,4 @@ We will drop support for python 3.7 by release 2022.10.
 
 - More robust alignment to reference algorithm. Builds a multiple sequence alignment from a series of pairwise alignments to a reference sequence. cogent3.app.align.align_to_ref() now retains gaps in the reference. This will be modestly slower than previously, but avoids losing information if the choice of reference sequence is a bad one.
 - cogent3.app.composable.appify decorator class, simplifies converting a user defined function into a cogent3 composable app
-- JSD calculation now uses more accurate math.fsum()
+- JSD calculation now uses more accurate math.fsum()
\ No newline at end of file
diff --git a/src/cogent3/__init__.py b/src/cogent3/__init__.py
index 6f9dbf337..e7d00edbe 100644
--- a/src/cogent3/__init__.py
+++ b/src/cogent3/__init__.py
@@ -25,6 +25,8 @@ from cogent3.core.moltype import (
     DNA,
     PROTEIN,
     RNA,
+    STANDARD_CODON,
+    CodonAlphabet,
     available_moltypes,
     get_moltype,
 )
diff --git a/src/cogent3/core/genetic_code.py b/src/cogent3/core/genetic_code.py
index a63ae0ca6..dab09dd2e 100644
--- a/src/cogent3/core/genetic_code.py
+++ b/src/cogent3/core/genetic_code.py
@@ -346,13 +346,6 @@ class GeneticCode:
 
         return "".join(mappings)
 
-    def get_alphabet(self, include_stop=False):
-        from cogent3.core.alphabet import Alphabet
-        from cogent3.core.moltype import DNA
-
-        motifs = list(self.codons) if include_stop else list(self.sense_codons)
-        return Alphabet(motifs, moltype=DNA)
-
 
 NcbiGeneticCodeData = [
     GeneticCode(*data)
diff --git a/src/cogent3/core/moltype.py b/src/cogent3/core/moltype.py
index dd0bef8b7..99328fc46 100644
--- a/src/cogent3/core/moltype.py
+++ b/src/cogent3/core/moltype.py
@@ -1380,10 +1380,6 @@ AB = MolType(
     label="ab",
 )
 
-# todo the _CodonAlphabet class should not exist,
-# the genetic code should have an alphabet, not
-# the alphabet has a genetic code
-
 
 class _CodonAlphabet(Alphabet):
     """Codon alphabets are DNA TupleAlphabets with a genetic code attribute and some codon-specific methods"""
@@ -1417,9 +1413,15 @@ def CodonAlphabet(gc=1, include_stop_codons=False):
     return a
 
 
+def _method_codon_alphabet(ignore, *args, **kwargs):
+    """If CodonAlphabet is set as a property, it gets self as extra 1st arg."""
+    return CodonAlphabet(*args, **kwargs)
+
+
 STANDARD_CODON = CodonAlphabet()
 
 # Modify NucleicAcidSequence to avoid circular import
+NucleicAcidSequence.codon_alphabet = _method_codon_alphabet
 NucleicAcidSequence.protein = PROTEIN
 ArrayRnaSequence.moltype = RNA
 ArrayRnaSequence.alphabet = RNA.alphabets.degen_gapped
diff --git a/src/cogent3/core/sequence.py b/src/cogent3/core/sequence.py
index d3baf5d01..0c26d4490 100644
--- a/src/cogent3/core/sequence.py
+++ b/src/cogent3/core/sequence.py
@@ -1247,6 +1247,7 @@ class ProteinWithStopSequence(Sequence):
 class NucleicAcidSequence(Sequence):
     """Abstract base class for DNA and RNA sequences."""
 
+    PROTEIN = None  # will set in moltype
     codon_alphabet = None  # will set in moltype
 
     def reverse_complement(self):
@@ -1309,7 +1310,7 @@ class NucleicAcidSequence(Sequence):
 
         return self.__class__(codons, name=self.name, info=self.info)
 
-    def get_translation(self, gc=None, incomplete_ok=False, include_stop=False):
+    def get_translation(self, gc=None, incomplete_ok=False):
         """translate to amino acid sequence
 
         Parameters
@@ -1325,7 +1326,7 @@ class NucleicAcidSequence(Sequence):
         sequence of PROTEIN moltype
         """
         gc = get_code(gc)
-        codon_alphabet = gc.get_alphabet(include_stop=include_stop).with_gap_motif()
+        codon_alphabet = self.codon_alphabet(gc).with_gap_motif()
         # translate the codons
         translation = []
         for posn in range(0, len(self._seq) - 2, 3):
@@ -1498,9 +1499,9 @@ class SeqView:
 
             return self.__class__(self.seq, start=val, stop=val - 1, step=-1)
 
-    def _get_slice(self, segment, step):
-        slice_start = segment.start if segment.start is not None else 0
-        slice_stop = segment.stop if segment.stop is not None else len(self)
+    def _get_slice(self, slice, step):
+        slice_start = slice.start if slice.start is not None else 0
+        slice_stop = slice.stop if slice.stop is not None else len(self)
 
         if self.step > 0:
             return self._get_forward_slice_from_forward_seqview_(
@@ -1571,9 +1572,9 @@ class SeqView:
             step=self.step * step,
         )
 
-    def _get_reverse_slice(self, segment, step):
-        slice_start = segment.start if segment.start is not None else -1
-        slice_stop = segment.stop if segment.stop is not None else -len(self) - 1
+    def _get_reverse_slice(self, slice, step):
+        slice_start = slice.start if slice.start is not None else -1
+        slice_stop = slice.stop if slice.stop is not None else -len(self) - 1
 
         if self.step < 0:
             return self._get_reverse_slice_from_reverse_seqview_(
@@ -1660,18 +1661,18 @@ class SeqView:
             step=self.step * step,
         )
 
-    def __getitem__(self, segment):
-        if isinstance(segment, int):
-            return self._get_index(segment)
+    def __getitem__(self, slice):
+        if isinstance(slice, int):
+            return self._get_index(slice)
         if len(self) == 0:
             return self
 
-        slice_step = 1 if segment.step is None else segment.step
+        slice_step = 1 if slice.step is None else slice.step
 
         if slice_step > 0:
-            return self._get_slice(segment, slice_step)
+            return self._get_slice(slice, slice_step)
         elif slice_step < 0:
-            return self._get_reverse_slice(segment, slice_step)
+            return self._get_reverse_slice(slice, slice_step)
         else:
             raise ValueError(
                 f"{self.__class__.__name__} cannot be sliced with a step of 0"
diff --git a/src/cogent3/evolve/ns_substitution_model.py b/src/cogent3/evolve/ns_substitution_model.py
index c8f6c3712..d2089c323 100644
--- a/src/cogent3/evolve/ns_substitution_model.py
+++ b/src/cogent3/evolve/ns_substitution_model.py
@@ -1,6 +1,6 @@
 import numpy
 
-from cogent3.core import genetic_code, moltype
+from cogent3.core import moltype
 from cogent3.evolve.discrete_markov import PsubMatrixDefn
 from cogent3.evolve.predicate import MotifChange
 from cogent3.maths.optimisers import ParameterOutOfBoundsError
@@ -188,11 +188,11 @@ class NonReversibleTrinucleotide(NonReversibleNucleotide):
 class NonReversibleCodon(_Codon, NonReversibleNucleotide):
     """Base non-reversible codon substitution model."""
 
-    # todo deprecate alphabet argument
     @extend_docstring_from(Parametric.__init__)
     def __init__(self, alphabet=None, gc=None, **kw):
-        self.gc = genetic_code.get_code(gc)
-        kw["alphabet"] = self.gc.get_alphabet()
+        if gc is not None:
+            alphabet = moltype.CodonAlphabet(gc=gc)
+        kw["alphabet"] = alphabet or moltype.STANDARD_CODON
         NonReversibleNucleotide.__init__(self, **kw)
 
 
diff --git a/src/cogent3/evolve/substitution_model.py b/src/cogent3/evolve/substitution_model.py
index fbc02ca24..dc5af7d97 100644
--- a/src/cogent3/evolve/substitution_model.py
+++ b/src/cogent3/evolve/substitution_model.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
 """
 substitution_model.py
 
@@ -39,7 +40,7 @@ import numpy
 
 from numpy.linalg import svd
 
-from cogent3.core import genetic_code, moltype
+from cogent3.core import moltype
 from cogent3.evolve import motif_prob_model, parameter_controller, predicate
 from cogent3.evolve.likelihood_tree import make_likelihood_tree_leaf
 from cogent3.evolve.substitution_calculation import (
@@ -1010,7 +1011,7 @@ class _Codon:
             return ndiffs == 1
 
     def get_predefined_predicates(self):
-        codon_preds = _CodonPredicates(self.gc)
+        codon_preds = _CodonPredicates(self.get_alphabet().get_genetic_code())
 
         preds = _TimeReversibleNucleotide.get_predefined_predicates(self)
         preds.update(
@@ -1027,9 +1028,9 @@ class _Codon:
 class TimeReversibleCodon(_Codon, _TimeReversibleNucleotide):
     """Core substitution model for codons"""
 
-    # todo deprecate alphabet argument
     @extend_docstring_from(_TimeReversibleNucleotide.__init__)
     def __init__(self, alphabet=None, gc=None, **kw):
-        self.gc = genetic_code.get_code(gc)
-        kw["alphabet"] = self.gc.get_alphabet()
+        if gc is not None:
+            alphabet = moltype.CodonAlphabet(gc=gc)
+        kw["alphabet"] = alphabet or moltype.STANDARD_CODON
         _TimeReversibleNucleotide.__init__(self, **kw)
diff --git a/src/cogent3/parse/sequence.py b/src/cogent3/parse/sequence.py
index 3b8d682f0..17779afe5 100644
--- a/src/cogent3/parse/sequence.py
+++ b/src/cogent3/parse/sequence.py
@@ -79,7 +79,7 @@ def FromFileParser(f, format, dialign_recode=False, **kw):
     f.close()
 
 
-def format_from_filename(filename, format=None):  # pragma: no cover
+def format_from_filename(filename, format=None):  # pragma: no coverage
     from cogent3.util.warning import deprecated
 
     deprecated(
diff --git a/src/cogent3/util/warning.py b/src/cogent3/util/warning.py
index cb3a1460e..da0b95434 100644
--- a/src/cogent3/util/warning.py
+++ b/src/cogent3/util/warning.py
@@ -69,10 +69,14 @@ def discontinued(_type, old, version, reason=None, stack_level=3):
         _warn(msg, DeprecationWarning, stacklevel=stack_level)
 
 
+_discontinued = discontinued  # renamed to avoid name clash with discontinued argument in deprecated args decorator
+
+
 def deprecated_args(
-    mapping: List[Tuple[str, str]],
     version: str,
     reason: str,
+    old_new: List[Tuple[str, str]] = None,
+    discontinued: str = None,
 ) -> Callable[..., Any]:
     """
     A decorator that marks specific arguments of a function as deprecated.
@@ -83,12 +87,14 @@ def deprecated_args(
 
     Parameters
     ----------
-    mapping : List[Tuple[str, str]]
-        A list of 2-tuples specifying the mapping of old argument names to new argument names.
     version : str
-        A string indicating the version when the deprecated arguments will be removed.
+        The version when the old arguments will be removed in calver format, e.g. 'YYYY.MM'
     reason : str
-        A string providing a reason for deprecation.
+        Reason for deprecation or guidance on what to do
+    old-new : List[Tuple[str, str]]
+        A list of deprecated old and replacement new argument names.
+    discontinued : str
+        A discontinued argument name
 
     Returns
     -------
@@ -98,32 +104,33 @@ def deprecated_args(
     Warnings
     --------
     DeprecationWarning
-        A warning will be raised when the decorated function is called with any of the deprecated arguments.
+        A warning will be raised when the decorated function is called for each deprecated argument
+        used in the calling function.
 
     Examples
     --------
     Here's an example of how to use the `deprecated_args` decorator to mark the argument `old_name` as deprecated
     and replace it with the new name `new_name`.
 
-    >>> @deprecated_args(mapping=[('old_name', 'new_name')], version='2.0', reason='Use new_name instead')
+    >>> @deprecated_args('2.0', 'Use new_name instead',old_new=[('old_arg', 'new_arg')],discontinued='discontinued_arg',)
     >>> def my_function(new_name):
     >>>     # do something here
 
-    When `my_function` is called with the argument `old_name`, a warning will be raised indicating that the argument
-    is deprecated and should be replaced with `new_name`.
+    When `my_function` is called with the argument `old_arg`, a warning will be raised indicating that the argument
+    is deprecated and should be replaced with `new_arg`, and `discontinued_arg` is to be discontinued.
     """
 
     def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
         @functools.wraps(func)
         def wrapper(*args: Any, **kwargs: Any) -> Callable[..., Any]:
-            message = []
-            for old, new in mapping:
-                if old in kwargs:
-                    kwargs[new] = kwargs.pop(old)
-                    message.append(f"{old}->{new}")
-            if message:
-                msg = f'The following parameters of `{func.__name__}` are deprecated. [{", ".join(message)}] will be removed in {version or "a future release"}.{f"  {reason}" if reason else ""}'
-                _warn(msg, DeprecationWarning, stacklevel=2)
+            if old_new:
+                for old, new in old_new:
+                    if old in kwargs:
+                        kwargs[new] = kwargs.pop(old)
+                        deprecated("argument", old, new, version, reason)
+            if discontinued and discontinued in kwargs:
+                _discontinued("argument", discontinued, version, reason)
+
             return func(*args, **kwargs)
 
         return wrapper


Test patch:
diff --git a/tests/test_core/test_genetic_code.py b/tests/test_core/test_genetic_code.py
index fe578e094..dfc5371f6 100644
--- a/tests/test_core/test_genetic_code.py
+++ b/tests/test_core/test_genetic_code.py
@@ -3,8 +3,6 @@
 """
 from unittest import TestCase, main
 
-import pytest
-
 from cogent3 import DNA, RNA
 from cogent3.core.genetic_code import (
     DEFAULT,
@@ -416,15 +414,6 @@ class GeneticCodeTests(TestCase):
         self.assertIn("Standard", got)
 
 
-# starting pytest versions
-@pytest.mark.parametrize("code", (1, 2))
-def test_get_alphabet(code):
-    from cogent3.core.alphabet import Alphabet
-
-    gc = get_code()
-    alpha_w_stop = gc.get_alphabet(include_stop=True)
-    assert tuple(alpha_w_stop) == tuple(gc.codons)
-    assert isinstance(alpha_w_stop, Alphabet)
-    alpha_wo_stop = gc.get_alphabet(include_stop=False)
-    assert tuple(alpha_wo_stop) == tuple(gc.sense_codons)
-    assert isinstance(alpha_wo_stop, Alphabet)
+# Run tests if called from command line
+if __name__ == "__main__":
+    main()
diff --git a/tests/test_core/test_moltype.py b/tests/test_core/test_moltype.py
index ae3d70cda..52f734ab7 100644
--- a/tests/test_core/test_moltype.py
+++ b/tests/test_core/test_moltype.py
@@ -8,6 +8,7 @@ from cogent3.core.moltype import (
     DNA,
     PROTEIN,
     RNA,
+    STANDARD_CODON,
     AlphabetError,
     AlphabetGroup,
     CodonAlphabet,
@@ -974,8 +975,6 @@ class DinucAlphabet(_AlphabetTestCase):
 
 class TestCodonAlphabet(_AlphabetTestCase):
     def setUp(self):
-        from cogent3.core.moltype import STANDARD_CODON
-
         self.alpha = STANDARD_CODON
 
     def test_ambiguous_gaps(self):
diff --git a/tests/test_util/test_warning.py b/tests/test_util/test_warning.py
index bf193fb0c..8baf72aab 100644
--- a/tests/test_util/test_warning.py
+++ b/tests/test_util/test_warning.py
@@ -19,9 +19,9 @@ __status__ = "Production"
 @pytest.mark.filterwarnings("ignore::DeprecationWarning")
 def test_function_deprecated_args():
     @deprecated_args(
-        [("x", "a"), ("y", "b")],
         version="a future release",
         reason="x and y are not descriptive",
+        old_new=[("x", "a"), ("y", "b")],
     )
     def changed(a: int, b: int) -> int:
         return a + b
@@ -34,9 +34,9 @@ def test_function_deprecated_args():
 @pytest.mark.filterwarnings("ignore::DeprecationWarning")
 def test_function_deprecated_args_docstring():
     @deprecated_args(
-        [("x", "a"), ("y", "b")],
         version="a future release",
         reason="x and y are not descriptive",
+        old_new=[("x", "a"), ("y", "b")],
     )
     def changed(a: int, b: int) -> int:
         """This is a test function"""
@@ -49,9 +49,9 @@ def test_function_deprecated_args_docstring():
 def test_function_deprecated_args_warn(kwargs):
     # Example target function to be decorated
     @deprecated_args(
-        [("x", "a"), ("y", "b")],
         version="a future release",
         reason="x and y are not descriptive",
+        old_new=[("x", "a"), ("y", "b")],
     )
     def changed(a: int, b: int) -> int:
         return a + b
@@ -62,9 +62,9 @@ def test_function_deprecated_args_warn(kwargs):
 
 def test_function_correct_args_do_not_warn():
     @deprecated_args(
-        [("x", "a"), ("y", "b")],
         version="a future release",
         reason="x and y are not descriptive",
+        old_new=[("x", "a"), ("y", "b")],
     )
     def changed(a: int, b: int) -> int:
         return a + b
@@ -78,9 +78,9 @@ def test_function_correct_args_do_not_warn():
 @pytest.mark.filterwarnings("ignore::DeprecationWarning")
 def test_function_deprecated_args_pickled():
     @deprecated_args(
-        [("x", "a"), ("y", "b")],
         version="a future release",
         reason="x and y are not descriptive",
+        old_new=[("x", "a"), ("y", "b")],
     )
     def changed(a: int, b: int) -> int:
         return a + b
@@ -98,9 +98,9 @@ class foo:
         self.b = 0
 
     @deprecated_args(
-        [("x", "a"), ("y", "b")],
         version="a future release",
         reason="x and y are not descriptive",
+        old_new=[("x", "a"), ("y", "b")],
     )
     def changed(self, a: int, b: int):
         """This is a test function"""
@@ -127,7 +127,7 @@ def test_method_deprecated_args_docstring():
 def test_method_deprecated_args_warn(kwargs):
 
     with pytest.deprecated_call():
-        foo().changed(**kwargs)
+        foo().changed(**kwargs) # pylint: disable=no-value-for-parameter
 
 
 def test_method_correct_args_do_not_warn():
@@ -143,7 +143,7 @@ def test_method_deprecated_args_pickled():
     foo_instance = foo()
     assert foo_instance.a == 0
     assert foo_instance.b == 0
-    foo_instance.changed(x=1, y=2)
+    foo_instance.changed(x=1, y=2) # pylint: disable=no-value-for-parameter
     assert foo_instance.a == 1
     assert foo_instance.b == 2
 
@@ -224,3 +224,21 @@ def test_method_deprecated_method_pickling(recwarn):
 def test_deprecated_callable_resolves_type(recwarn, func, _type):
     func(2)
     assert _type in recwarn.list[0].message.args[0]
+
+
+@pytest.mark.filterwarnings("ignore::DeprecationWarning")
+def test_function_deprecated_args_deprecated_callable_chained_decorators(recwarn):
+    @deprecated_args("2023.6", "x is not descriptive", old_new=[("x", "a")])
+    @deprecated_args("2023.6", "b is no longer required", discontinued="b")
+    @deprecated_callable(
+        "2023.6", "Improved change function", new="changed2", is_discontinued=True
+    )
+    def changed(a: int, b: int) -> int:
+        return a + b
+
+    got = changed(x=5, b=3)  # pylint: disable=no-value-for-parameter
+    assert got == 8
+    warnings = [warning.message.args[0] for warning in recwarn.list]
+    assert any("argument x which will be removed" in warning for warning in warnings)
+    assert any("argument b is discontinued" in warning for warning in warnings)
+    assert any("function changed is discontinued" in warning for warning in warnings)