Repository: pytorch/ignite

Issue:
Improve deprecation messages
## 🚀 Feature

House keeping request to improve the codebase. 
The idea is to 1) improve deprecation messages inspired from scikit-image: https://github.com/scikit-image/scikit-image/blob/87a8806cca7fb5366b6e5ddbe5e46364b44f90fe/skimage/_shared/utils.py#L15-L214
```
            f"{self.arg_name} argument is deprecated and will be removed "
            f"in version {self.changed_version}. To avoid this warning, "
            f"please do not use the {self.arg_name} argument. Please "
            f"see {func.__name__} documentation for more details.")
```
and 2) introduce similar decorators in our codebase in a minimalistic way.



Discussion:
Hey, I'm pretty new to contributing and I think this issue is something I can help you with.
The CONTRIBUTING.md is super detailed(!) so I'm feeling confident I can follow those instructions.
I'd like a to ask for little more specific detail about what is needed in this issue and where to look for the messages that need to be modified. As I understand it, the formatting of the current messages needs to be more simialr to the the one quoted? (short concise f-strings.)
Hi @pillemer sorry for delayed reply ! The idea is to make sure that current deprecation messages like 
https://github.com/pytorch/ignite/blob/96689c5f010c39115b76eaa3281af10fbc631ab0/ignite/contrib/engines/common.py#L277-L280
express a similar message:
- Aomething (e.g. function) is deprecated (we can keep the info since when it is deprecated)
- And will be removed in version vX.Y.Z
- To avoid this warning, please use something else (e.g. another function).

Currently, I found only one deprecation warning. Let's improve its message and for the others that I forgot.
~~We can not use f-strings as we still provide a support for python 3.5.~~
EDIT: we are not supporting python 3.5 since v0.5.0 => OK for the usage of f-strings (see https://github.com/pytorch/ignite/issues/1496)

Feel free to ask other questions if needed...
> We can not use f-strings as we still provide a support for python 3.5.

Hi, do you still provide support for python 3.5? Thanks.


> Hi, do you still provide support for python 3.5? Thanks.

@Afzal-Ind we just dropped the support of python 3.5 today.
Hi @vfdev-5 !
I would like to give it a shot, if it is okay with you and if no one else is already working on it :)

PS: I did not know if I was supposed to tag you, sorry if I wasn't supposed to do that
@Devanshu24 sure, sounds good ! Currently, no one is working on this issue. Let me assign it to you such that other would know. Feel free to ask questions to detail what we would like to implement if needed and send a draft PR that we could improve. Let me know if it sounds good for you too. Thanks !
Sure sounds great, and thank you for the opportunity!
I'll get right to it. I will first find the current deprecation messages and understand the codebase a little, and will send a draft PR soon!
One small question, where would we want to place all these decorators?
As in, inside `utils.py` or maybe a separate file in itself?
I think `ignite.utils.py` can be a good place for that. If needed we can come back to this point in the PR.

Resolving patch:
diff --git a/ignite/contrib/engines/common.py b/ignite/contrib/engines/common.py
--- a/ignite/contrib/engines/common.py
+++ b/ignite/contrib/engines/common.py
@@ -30,6 +30,7 @@
 from ignite.handlers import Checkpoint, DiskSaver, EarlyStopping, TerminateOnNan
 from ignite.handlers.checkpoint import BaseSaveHandler
 from ignite.metrics import RunningAverage
+from ignite.utils import deprecated
 
 
 def setup_common_training_handlers(
@@ -274,11 +275,21 @@ def empty_cuda_cache(_: Engine) -> None:
     gc.collect()
 
 
-def setup_any_logging(logger, logger_module, trainer, optimizers, evaluators, log_every_iters) -> None:  # type: ignore
-    raise DeprecationWarning(
-        "ignite.contrib.engines.common.setup_any_logging is deprecated since 0.4.0. and will be remove in 0.6.0. "
-        "Please use instead: setup_tb_logging, setup_visdom_logging or setup_mlflow_logging etc."
-    )
+@deprecated(
+    "0.4.0",
+    "0.6.0",
+    ("Please use instead: setup_tb_logging, setup_visdom_logging or setup_mlflow_logging etc.",),
+    raise_exception=True,
+)
+def setup_any_logging(
+    logger: BaseLogger,
+    logger_module: Any,
+    trainer: Engine,
+    optimizers: Optional[Union[Optimizer, Dict[str, Optimizer], Dict[None, Optimizer]]],
+    evaluators: Optional[Union[Engine, Dict[str, Engine]]],
+    log_every_iters: int,
+) -> None:
+    pass
 
 
 def _setup_logging(
diff --git a/ignite/utils.py b/ignite/utils.py
--- a/ignite/utils.py
+++ b/ignite/utils.py
@@ -1,8 +1,10 @@
 import collections.abc as collections
+import functools
 import logging
 import random
 import sys
-from typing import Any, Callable, Optional, TextIO, Tuple, Type, Union, cast
+import warnings
+from typing import Any, Callable, Dict, Optional, TextIO, Tuple, Type, TypeVar, Union, cast
 
 import torch
 
@@ -171,3 +173,34 @@ def manual_seed(seed: int) -> None:
         np.random.seed(seed)
     except ImportError:
         pass
+
+
+def deprecated(
+    deprecated_in: str, removed_in: str = "", reasons: Tuple[str, ...] = (), raise_exception: bool = False
+) -> Callable:
+
+    F = TypeVar("F", bound=Callable[..., Any])
+
+    def decorator(func: F) -> F:
+        func_doc = func.__doc__ if func.__doc__ else ""
+        deprecation_warning = (
+            f"This function has been deprecated since version {deprecated_in}"
+            + (f" and will be removed in version {removed_in}" if removed_in else "")
+            + ".\n Please refer to the documentation for more details."
+        )
+
+        @functools.wraps(func)
+        def wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Callable:
+            if raise_exception:
+                raise DeprecationWarning(deprecation_warning)
+            warnings.warn(deprecation_warning, DeprecationWarning, stacklevel=2)
+            return func(*args, **kwargs)
+
+        appended_doc = f".. deprecated:: {deprecated_in}" + ("\n\n\t" if len(reasons) else "")
+
+        for reason in reasons:
+            appended_doc += "\n\t- " + reason
+        wrapper.__doc__ = f"**Deprecated function**.\n\n    {func_doc}{appended_doc}"
+        return cast(F, wrapper)
+
+    return decorator


Test patch:
diff --git a/tests/ignite/contrib/engines/test_common.py b/tests/ignite/contrib/engines/test_common.py
--- a/tests/ignite/contrib/engines/test_common.py
+++ b/tests/ignite/contrib/engines/test_common.py
@@ -274,7 +274,7 @@ def set_eval_metric(engine):
 
 def test_deprecated_setup_any_logging():
 
-    with pytest.raises(DeprecationWarning, match=r"is deprecated since 0\.4\.0\."):
+    with pytest.raises(DeprecationWarning, match=r"deprecated since version 0.4.0"):
         setup_any_logging(None, None, None, None, None, None)
 
 
diff --git a/tests/ignite/test_utils.py b/tests/ignite/test_utils.py
--- a/tests/ignite/test_utils.py
+++ b/tests/ignite/test_utils.py
@@ -1,13 +1,14 @@
 import logging
 import os
 import sys
+import warnings
 from collections import namedtuple
 
 import pytest
 import torch
 
 from ignite.engine import Engine, Events
-from ignite.utils import convert_tensor, setup_logger, to_onehot
+from ignite.utils import convert_tensor, deprecated, setup_logger, to_onehot
 
 
 def test_convert_tensor():
@@ -150,3 +151,72 @@ def _(_):
 
     # Needed by windows to release FileHandler in the loggers
     logging.shutdown()
+
+
+def test_deprecated():
+
+    # Test on function without docs, @deprecated without reasons
+    @deprecated("0.4.2", "0.6.0")
+    def func_no_docs():
+        return 24
+
+    assert func_no_docs.__doc__ == "**Deprecated function**.\n\n    .. deprecated:: 0.4.2"
+
+    # Test on function with docs, @deprecated without reasons
+    @deprecated("0.4.2", "0.6.0")
+    def func_no_reasons():
+        """Docs are cool
+        """
+        return 24
+
+    assert func_no_reasons.__doc__ == "**Deprecated function**.\n\n    Docs are cool\n        .. deprecated:: 0.4.2"
+
+    # Test on function with docs, @deprecated with reasons
+    @deprecated("0.4.2", "0.6.0", reasons=("r1", "r2"))
+    def func_no_warnings():
+        """Docs are very cool
+        """
+        return 24
+
+    assert (
+        func_no_warnings.__doc__
+        == "**Deprecated function**.\n\n    Docs are very cool\n        .. deprecated:: 0.4.2\n\n\t\n\t- r1\n\t- r2"
+    )
+
+    # Tests that the function emits DeprecationWarning
+    @deprecated("0.4.2", "0.6.0", reasons=("r1", "r2"))
+    def func_check_warning():
+        """Docs are very ...
+        """
+        return 24
+
+    with pytest.deprecated_call():
+        func_check_warning()
+    assert func_check_warning() == 24
+    with warnings.catch_warnings(record=True) as w:
+        # Cause all warnings to always be triggered.
+        warnings.simplefilter("always")
+        # Trigger a warning.
+        func_check_warning()
+        # Verify some things
+        assert len(w) == 1
+        assert issubclass(w[-1].category, DeprecationWarning)
+        assert (
+            "This function has been deprecated since version 0.4.2 and will be removed in version 0.6.0."
+            + "\n Please refer to the documentation for more details."
+            in str(w[-1].message)
+        )
+
+    # Test that the function raises Exception
+    @deprecated("0.4.2", "0.6.0", reasons=("reason1", "reason2"), raise_exception=True)
+    def func_with_everything():
+        return 1
+
+    with pytest.raises(Exception) as exec_info:
+        func_with_everything()
+
+    assert (
+        str(exec_info.value)
+        == "This function has been deprecated since version 0.4.2 and will be removed in version 0.6.0."
+        + "\n Please refer to the documentation for more details."
+    )