Repository: Qiskit/qiskit-optimization
Pull request #172 (2021-06-20 14:29:48)

Improvements to deprecate methods
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add it in the CHANGELOG file under Unreleased section.
⚠️ If your pull request fixes an open issue, please link to the issue.

✅ I have added the tests to cover my changes.
✅ I have updated the documentation accordingly.
✅ I have read the CONTRIBUTING document.
-->

### Summary
New Deprecate Enum and deprecate_property
Improvements to deprecation unit tests


### Details and comments





Patch:
diff --git a/.pylintdict b/.pylintdict
index 95b983f..c28093c 100644
--- a/.pylintdict
+++ b/.pylintdict
@@ -46,6 +46,7 @@ eigen
 eigensolver
 eigenstate
 entangler
+enum
 eq
 et
 exponentiated
@@ -101,6 +102,7 @@ mdl
 minimizer
 minimumeigenoptimizer
 multiset
+mypy
 ndarray
 ndarrays
 noop
diff --git a/qiskit_optimization/deprecation.py b/qiskit_optimization/deprecation.py
index 040e5d6..3d068b4 100644
--- a/qiskit_optimization/deprecation.py
+++ b/qiskit_optimization/deprecation.py
@@ -12,20 +12,68 @@
 
 """Contains the Deprecation message methods."""
 
+from abc import abstractmethod
 import warnings
 import functools
 from typing import NamedTuple, Optional, Callable, Dict, Set
-from enum import Enum
+from enum import Enum, EnumMeta
+
+
+class DeprecatedEnum(Enum):
+    """
+    Shows deprecate message whenever member is accessed
+    """
+
+    def __new__(cls, value, *args):
+        member = object.__new__(cls)
+        member._value_ = value
+        member._args = args
+        member._show_deprecate = member.deprecate
+        return member
+
+    @abstractmethod
+    def deprecate(self):
+        """show deprecate message"""
+        pass
+
+
+class DeprecatedEnumMeta(EnumMeta):
+    """
+    Shows deprecate message whenever member is accessed
+    """
+
+    def __getattribute__(cls, name):
+        obj = super().__getattribute__(name)
+        if isinstance(obj, DeprecatedEnum) and obj._show_deprecate:
+            obj._show_deprecate()
+        return obj
+
+    def __getitem__(cls, name):
+        member = super().__getitem__(name)
+        if member._show_deprecate:
+            member._show_deprecate()
+        return member
+
+    # pylint: disable=redefined-builtin
+    def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, start=1):
+        obj = super().__call__(
+            value, names, module=module, qualname=qualname, type=type, start=start
+        )
+        if isinstance(obj, DeprecatedEnum) and obj._show_deprecate:
+            obj._show_deprecate()
+        return obj
 
 
 class DeprecatedType(Enum):
     """ " Deprecation Types"""
 
     PACKAGE = "package"
+    ENUM = "enum"
     CLASS = "class"
     METHOD = "method"
     FUNCTION = "function"
     ARGUMENT = "argument"
+    PROPERTY = "property"
 
 
 class _DeprecatedObject(NamedTuple):
@@ -52,10 +100,10 @@ def warn_deprecated(
     """Emits deprecation warning the first time only
     Args:
         version: Version to be used
-        old_type: Old type to be used.
-        old_name: Old name of function to be used
-        new_type: New type to be used.
-        new_name: New name of function to be used
+        old_type: Old type to be used
+        old_name: Old name to be used
+        new_type: New type to be used, if None, old_type is used instead.
+        new_name: New name to be used
         additional_msg: any additional message
         stack_level: stack level
     """
@@ -70,13 +118,35 @@ def warn_deprecated(
         f"The {old_name} {old_type.value} is deprecated as of version {version} "
         "and will be removed no sooner than 3 months after the release"
     )
-    if new_type is not None and new_name:
-        msg += f". Instead use the {new_name} {new_type.value}"
+    if new_name:
+        type_str = new_type.value if new_type is not None else old_type.value
+        msg += f". Instead use the {new_name} {type_str}"
     if additional_msg:
         msg += f" {additional_msg}"
     msg += "."
 
-    warnings.warn(msg, DeprecationWarning, stacklevel=stack_level)
+    warnings.warn(msg, DeprecationWarning, stacklevel=stack_level + 1)
+
+
+def warn_deprecated_same_type_name(
+    version: str,
+    new_type: DeprecatedType,
+    new_name: str,
+    additional_msg: Optional[str] = None,
+    stack_level: int = 2,
+) -> None:
+    """Emits deprecation warning the first time only
+       Used when the type and name remained the same.
+    Args:
+        version: Version to be used
+        new_type: new type to be used
+        new_name: new name to be used
+        additional_msg: any additional message
+        stack_level: stack level
+    """
+    warn_deprecated(
+        version, new_type, new_name, new_type, new_name, additional_msg, stack_level + 1
+    )
 
 
 def _rename_kwargs(version, func_name, kwargs, kwarg_map, additional_msg, stack_level):
@@ -109,12 +179,12 @@ def deprecate_arguments(
     """Decorator to alias deprecated argument names and warn upon use.
     Args:
         version: Version to be used
-        kwarg_map: Args dictionary with old, new arguments.
+        kwarg_map: Args dictionary with old, new arguments
         additional_msg: any additional message
         stack_level: stack level
 
     Returns:
-        The decorated function.
+        The decorated function
     """
 
     def decorator(func):
@@ -131,43 +201,46 @@ def deprecate_arguments(
     return decorator
 
 
-def deprecate_method(
+def _deprecate_object(
     version: str,
+    old_type: DeprecatedType,
     new_type: DeprecatedType,
     new_name: str,
-    additional_msg: Optional[str] = None,
-    stack_level: int = 2,
+    additional_msg: str,
+    stack_level: int,
 ) -> Callable:
-    """Decorator that prints deprecated message for an instance method
+    """Decorator that prints deprecated message
     Args:
         version: Version to be used
-        new_type: New type to be used.
-        new_name: New name of function to be used
+        old_type: New type to be used
+        new_type: New type to be used, if None, old_type is used instead.
+        new_name: New name to be used
         additional_msg: any additional message
         stack_level: stack level
 
     Returns:
-        The decorated method.
+        The decorated method
     """
 
-    def decorator(method):
+    def decorator(func):
         msg = (
-            f"The {method.__name__} {DeprecatedType.METHOD.value} is deprecated "
-            f"as of version {version} and will be removed no sooner "
-            "than 3 months after the release. Instead use the "
-            f"{new_name} {new_type.value}"
+            f"The {func.__name__} {old_type.value} is deprecated as of version {version} "
+            "and will be removed no sooner than 3 months after the release"
         )
+        if new_name:
+            type_str = new_type.value if new_type is not None else old_type.value
+            msg += f". Instead use the {new_name} {type_str}"
         if additional_msg:
             msg += f" {additional_msg}"
         msg += "."
 
-        @functools.wraps(method)
-        def wrapper(self, *method_args, **method_kwargs):
+        @functools.wraps(func)
+        def wrapper(*args, **kwargs):
             # warn only once
             if not wrapper._warned:
                 warnings.warn(msg, DeprecationWarning, stacklevel=stack_level)
                 wrapper._warned = True
-            return method(self, *method_args, **method_kwargs)
+            return func(*args, **kwargs)
 
         wrapper._warned = False
         return wrapper
@@ -175,45 +248,73 @@ def deprecate_method(
     return decorator
 
 
-def deprecate_function(
+def deprecate_method(
     version: str,
-    new_type: DeprecatedType,
-    new_name: str,
+    new_type: Optional[DeprecatedType] = None,
+    new_name: Optional[str] = None,
     additional_msg: Optional[str] = None,
     stack_level: int = 2,
 ) -> Callable:
-    """Decorator that prints deprecated message for a function
+    """Decorator that prints deprecated message for an instance method
     Args:
         version: Version to be used
-        new_type: New type to be used.
-        new_name: New name of function to be used
+        new_type: New type to be used
+        new_name: New name to be used
         additional_msg: any additional message
         stack_level: stack level
 
     Returns:
-        The decorated function.
+        The decorated method
     """
+    return _deprecate_object(
+        version, DeprecatedType.METHOD, new_type, new_name, additional_msg, stack_level
+    )
 
-    def decorator(func):
-        msg = (
-            f"The {func.__name__} {DeprecatedType.FUNCTION.value} is deprecated "
-            f"as of version {version} and will be removed no sooner "
-            "than 3 months after the release. Instead use the "
-            f"{new_name} {new_type.value}"
-        )
-        if additional_msg:
-            msg += f" {additional_msg}"
-        msg += "."
 
-        @functools.wraps(func)
-        def wrapper(*method_args, **method_kwargs):
-            # warn only once
-            if not wrapper._warned:
-                warnings.warn(msg, DeprecationWarning, stacklevel=stack_level)
-                wrapper._warned = True
-            return func(*method_args, **method_kwargs)
+def deprecate_property(
+    version: str,
+    new_type: Optional[DeprecatedType] = None,
+    new_name: Optional[str] = None,
+    additional_msg: Optional[str] = None,
+    stack_level: int = 2,
+) -> Callable:
+    """Decorator that prints deprecated message for a property
 
-        wrapper._warned = False
-        return wrapper
+    *** This decorator must be placed below the property decorator ***
 
-    return decorator
+    Args:
+        version: Version to be used
+        new_type: New type to be used
+        new_name: New name to be used
+        additional_msg: any additional message
+        stack_level: stack level
+
+    Returns:
+        The decorated property
+    """
+    return _deprecate_object(
+        version, DeprecatedType.PROPERTY, new_type, new_name, additional_msg, stack_level
+    )
+
+
+def deprecate_function(
+    version: str,
+    new_type: Optional[DeprecatedType] = None,
+    new_name: Optional[str] = None,
+    additional_msg: Optional[str] = None,
+    stack_level: int = 2,
+) -> Callable:
+    """Decorator that prints deprecated message for a function
+    Args:
+        version: Version to be used
+        new_type: New type to be used
+        new_name: New name to be used
+        additional_msg: any additional message
+        stack_level: stack level
+
+    Returns:
+        The decorated function
+    """
+    return _deprecate_object(
+        version, DeprecatedType.FUNCTION, new_type, new_name, additional_msg, stack_level
+    )


Test patch:
diff --git a/test/test_deprecation.py b/test/test_deprecation.py
index b5173e2..9b6dc22 100644
--- a/test/test_deprecation.py
+++ b/test/test_deprecation.py
@@ -15,13 +15,20 @@ Test The deprecation methods
 """
 
 import unittest
+import sys
+import inspect
 import warnings
+from typing import Tuple, Optional
 from test import QiskitOptimizationTestCase
 from ddt import data, ddt
 from qiskit_optimization.deprecation import (
+    DeprecatedEnum,
+    DeprecatedEnumMeta,
     DeprecatedType,
     warn_deprecated,
+    warn_deprecated_same_type_name,
     deprecate_function,
+    deprecate_property,
     deprecate_method,
     deprecate_arguments,
 )
@@ -29,68 +36,140 @@ from qiskit_optimization.deprecation import (
 # pylint: disable=bad-docstring-quotes
 
 
-@deprecate_function("0.1.1", DeprecatedType.FUNCTION, "some_function1", "and more information", 2)
-def func1(arg1):
-    """func 1"""
-    del arg1
-    pass
+class EnumTest(DeprecatedEnum, metaclass=DeprecatedEnumMeta):
+    """Enum Test"""
 
+    ONE = "one"
+    TWO = "two"
 
-@deprecate_function("0.2.0", DeprecatedType.FUNCTION, "some_function2")
-def func2(arg2):
-    """func 2"""
-    del arg2
-    pass
+    def deprecate(self):
+        """show deprecate message"""
+        warn_deprecated(
+            "0.2.0", DeprecatedType.ENUM, self.__class__.__name__, new_name="NewEnum", stack_level=3
+        )
+
+
+@deprecate_function("0.1.1", DeprecatedType.CLASS, "some_class", "and more information")
+def func1(arg1: int) -> int:
+    """function 1"""
+    return arg1
+
+
+@deprecate_function("0.2.0", new_name="some_function2")
+def func2(arg2: int) -> int:
+    """function 2"""
+    return arg2
 
 
 @deprecate_arguments("0.1.2", {"old_arg": "new_arg"})
-def func3(new_arg=None, old_arg=None):
-    """func 3"""
-    del new_arg, old_arg
-    pass
+def func3(new_arg: Optional[int] = None, old_arg: Optional[int] = None) -> Tuple[int, int]:
+    """function 3"""
+    return new_arg, old_arg
 
 
-class DeprecatedClass:
-    """Deprecated Test class"""
+class DeprecatedClass1:
+    """Deprecated Test class 1"""
 
     def __init__(self):
         warn_deprecated(
-            "0.3.0", DeprecatedType.CLASS, "DeprecatedClass", DeprecatedType.CLASS, "NewClass"
+            "0.3.0", DeprecatedType.CLASS, "DeprecatedClass1", DeprecatedType.CLASS, "NewClass"
+        )
+
+
+class DeprecatedClass2:
+    """Deprecated Test class 2"""
+
+    def __init__(self):
+        warn_deprecated_same_type_name(
+            "0.3.0", DeprecatedType.CLASS, "DeprecatedClass2", "from package test2"
         )
 
 
 class TestClass:
     """Test class with deprecation"""
 
-    @deprecate_method(
-        "0.1.0", DeprecatedType.METHOD, "some_method1", "and additional information", 1
+    def __init__(self):
+        self._value = 20
+
+    # Bug in mypy, if property decorator is used with another one
+    # https://github.com/python/mypy/issues/1362
+
+    @property  # type: ignore
+    @deprecate_property("0.1.0")
+    def property1(self) -> int:
+        """property1 get"""
+        return self._value
+
+    @property1.setter  # type: ignore
+    @deprecate_property(
+        "0.1.0", new_name="new_property", additional_msg="and some additional information"
     )
-    def method1(self):
+    def property1(self, value: int):
+        """property 1 set"""
+        self._value = value
+
+    @deprecate_method("0.1.0", new_name="some_method1", additional_msg="and additional information")
+    def method1(self, arg: int) -> int:
         """method 1"""
-        pass
+        return arg
 
-    @deprecate_method("0.2.0", DeprecatedType.METHOD, "some_method2")
-    def method2(self):
+    @deprecate_method("0.2.0", new_name="some_method2")
+    def method2(self, arg: int) -> int:
         """method 2"""
-        pass
+        return arg
 
     @deprecate_arguments("0.1.2", {"old_arg": "new_arg"})
-    def method3(self, new_arg=None, old_arg=None):
+    def method3(
+        self, new_arg: Optional[int] = None, old_arg: Optional[int] = None
+    ) -> Tuple[int, int]:
         """method3"""
-        del new_arg, old_arg
-        pass
+        return new_arg, old_arg
 
 
 @ddt
 class TestDeprecation(QiskitOptimizationTestCase):
     """Test deprecation methods"""
 
+    def setUp(self) -> None:
+        super().setUp()
+        self._source = inspect.getsource(sys.modules[self.__module__]).splitlines()
+
+    def _get_line_from_str(self, text: str) -> int:
+        for idx, line in enumerate(self._source):
+            if text in line:
+                return idx + 1
+        return -1
+
+    def test_enum_deprecation(self):
+        """test enum deprecation"""
+
+        msg_ref = (
+            "The EnumTest enum is deprecated as of version 0.2.0 "
+            "and will be removed no sooner than 3 months after the release. "
+            "Instead use the NewEnum enum."
+        )
+
+        # emit deprecation the first time it is used
+        with warnings.catch_warnings(record=True) as c_m:
+            warnings.simplefilter("always")
+            _ = EnumTest.ONE
+            msg = str(c_m[0].message)
+            self.assertEqual(msg, msg_ref)
+            self.assertTrue("test_deprecation.py" in c_m[0].filename, c_m[0].filename)
+            self.assertEqual(self._get_line_from_str("EnumTest.ONE"), c_m[0].lineno)
+
+        # trying again should not emit deprecation
+        with warnings.catch_warnings(record=True) as c_m:
+            warnings.simplefilter("always")
+            _ = EnumTest.TWO
+            self.assertListEqual(c_m, [])
+
     @data(
         (
             "func1",
             "The func1 function is deprecated as of version 0.1.1 "
             "and will be removed no sooner than 3 months after the release. "
-            "Instead use the some_function1 function and more information.",
+            "Instead use the some_class class and more information.",
         ),
         (
             "func2",
@@ -107,9 +186,11 @@ class TestDeprecation(QiskitOptimizationTestCase):
         # emit deprecation the first time it is used
         with warnings.catch_warnings(record=True) as c_m:
             warnings.simplefilter("always")
-            globals()[function_name](None)
+            self.assertEqual(2, globals()[function_name](2))
             msg = str(c_m[0].message)
             self.assertEqual(msg, msg_ref)
+            self.assertTrue("test_deprecation.py" in c_m[0].filename, c_m[0].filename)
+            self.assertEqual(self._get_line_from_str("globals()[function_name](2)"), c_m[0].lineno)
 
         # trying again should not emit deprecation
         with warnings.catch_warnings(record=True) as c_m:
@@ -117,11 +198,11 @@ class TestDeprecation(QiskitOptimizationTestCase):
             globals()[function_name](None)
             self.assertListEqual(c_m, [])
 
-    def test_class_deprecation(self):
-        """test class deprecation"""
+    def test_class_deprecation1(self):
+        """test class deprecation 1"""
 
         msg_ref = (
-            "The DeprecatedClass class is deprecated as of version 0.3.0 "
+            "The DeprecatedClass1 class is deprecated as of version 0.3.0 "
             "and will be removed no sooner than 3 months after the release. "
             "Instead use the NewClass class."
         )
@@ -129,14 +210,40 @@ class TestDeprecation(QiskitOptimizationTestCase):
         # emit deprecation the first time it is used
         with warnings.catch_warnings(record=True) as c_m:
             warnings.simplefilter("always")
-            DeprecatedClass()
+            DeprecatedClass1()
             msg = str(c_m[0].message)
             self.assertEqual(msg, msg_ref)
+            self.assertTrue("test_deprecation.py" in c_m[0].filename, c_m[0].filename)
+            self.assertEqual(self._get_line_from_str("DeprecatedClass1()"), c_m[0].lineno)
 
         # trying again should not emit deprecation
         with warnings.catch_warnings(record=True) as c_m:
             warnings.simplefilter("always")
-            DeprecatedClass()
+            DeprecatedClass1()
+            self.assertListEqual(c_m, [])
+
+    def test_class_deprecation2(self):
+        """test class deprecation 2"""
+
+        msg_ref = (
+            "The DeprecatedClass2 class is deprecated as of version 0.3.0 "
+            "and will be removed no sooner than 3 months after the release. "
+            "Instead use the DeprecatedClass2 class from package test2."
+        )
+
+        # emit deprecation the first time it is used
+        with warnings.catch_warnings(record=True) as c_m:
+            warnings.simplefilter("always")
+            DeprecatedClass2()
+            msg = str(c_m[0].message)
+            self.assertEqual(msg, msg_ref)
+            self.assertTrue("test_deprecation.py" in c_m[0].filename, c_m[0].filename)
+            self.assertEqual(self._get_line_from_str("DeprecatedClass2()"), c_m[0].lineno)
+
+        # trying again should not emit deprecation
+        with warnings.catch_warnings(record=True) as c_m:
+            warnings.simplefilter("always")
+            DeprecatedClass2()
             self.assertListEqual(c_m, [])
 
     @data(
@@ -162,14 +269,16 @@ class TestDeprecation(QiskitOptimizationTestCase):
         # emit deprecation the first time it is used
         with warnings.catch_warnings(record=True) as c_m:
             warnings.simplefilter("always")
-            method()
+            self.assertEqual(3, method(3))
             msg = str(c_m[0].message)
             self.assertEqual(msg, msg_ref)
+            self.assertTrue("test_deprecation.py" in c_m[0].filename, c_m[0].filename)
+            self.assertEqual(self._get_line_from_str("method(3)"), c_m[0].lineno)
 
         # trying again should not emit deprecation
         with warnings.catch_warnings(record=True) as c_m:
             warnings.simplefilter("always")
-            method()
+            self.assertEqual(4, method(4))
             self.assertListEqual(c_m, [])
 
     def test_function_arguments_deprecation(self):
@@ -186,9 +295,11 @@ class TestDeprecation(QiskitOptimizationTestCase):
 
         with warnings.catch_warnings(record=True) as c_m:
             warnings.simplefilter("always")
-            func3(old_arg="hello")
+            self.assertEqual(("hello", None), func3(old_arg="hello"))
             msg = str(c_m[0].message)
             self.assertEqual(msg, msg_ref)
+            self.assertTrue("test_deprecation.py" in c_m[0].filename, c_m[0].filename)
+            self.assertEqual(self._get_line_from_str('func3(old_arg="hello")'), c_m[0].lineno)
 
     def test_method_arguments_deprecation(self):
         """test method arguments deprecation"""
@@ -206,9 +317,59 @@ class TestDeprecation(QiskitOptimizationTestCase):
 
         with warnings.catch_warnings(record=True) as c_m:
             warnings.simplefilter("always")
-            obj.method3(old_arg="hello")
+            self.assertEqual(("hello", None), obj.method3(old_arg="hello"))
+            msg = str(c_m[0].message)
+            self.assertEqual(msg, msg_ref)
+            self.assertTrue("test_deprecation.py" in c_m[0].filename, c_m[0].filename)
+            self.assertEqual(self._get_line_from_str('obj.method3(old_arg="hello")'), c_m[0].lineno)
+
+    def test_property_deprecation(self):
+        """test property deprecation"""
+
+        obj = TestClass()
+
+        msg_ref = (
+            "The property1 property is deprecated as of version 0.1.0 "
+            "and will be removed no sooner than 3 months after the release."
+        )
+        # property get
+        # emit deprecation the first time it is used
+        with warnings.catch_warnings(record=True) as c_m:
+            warnings.simplefilter("always")
+            self.assertEqual(20, obj.property1)
+            msg = str(c_m[0].message)
+            self.assertEqual(msg, msg_ref)
+            self.assertTrue("test_deprecation.py" in c_m[0].filename, c_m[0].filename)
+            self.assertEqual(self._get_line_from_str("obj.property1"), c_m[0].lineno)
+
+        # trying again should not emit deprecation
+        with warnings.catch_warnings(record=True) as c_m:
+            warnings.simplefilter("always")
+            self.assertEqual(20, obj.property1)
+            self.assertListEqual(c_m, [])
+
+        msg_ref = (
+            "The property1 property is deprecated as of version 0.1.0 "
+            "and will be removed no sooner than 3 months after the release. "
+            "Instead use the new_property property and some additional information."
+        )
+        # property set
+        # emit deprecation the first time it is used
+        with warnings.catch_warnings(record=True) as c_m:
+            warnings.simplefilter("always")
+            obj.property1 = 0
+            self.assertEqual(0, obj.property1)
             msg = str(c_m[0].message)
             self.assertEqual(msg, msg_ref)
+            self.assertTrue("test_deprecation.py" in c_m[0].filename, c_m[0].filename)
+            self.assertEqual(self._get_line_from_str("obj.property1 = 0"), c_m[0].lineno)
+
+        # trying again should not emit deprecation
+        with warnings.catch_warnings(record=True) as c_m:
+            warnings.simplefilter("always")
+            obj.property1 = 0
+            self.assertEqual(0, obj.property1)
+            self.assertListEqual(c_m, [])
 
 
 if __name__ == "__main__":