r47001 - Merge deprecated-property-8124: Make @deprecatedProperty, to make deprecated properties

hawkowl-TA+aISz0psMTMxyoc4vAAJOcrHinNvQL0E9HWUfgJXw@public.gmane.org Tue, 15 Mar 2016 21:54:35 -0600 (MDT)
Newsgroups gmane.comp.python.twisted.commits
Message-ID <[email protected]>
Author: hawkowl
Date: Tue Mar 15 21:54:30 2016
New Revision: 47001

Added:
   trunk/twisted/topfiles/8124.misc
Modified:
   trunk/twisted/python/deprecate.py
   trunk/twisted/python/test/test_deprecate.py

Log:
Merge deprecated-property-8124: Make @deprecatedProperty, to make deprecated properties

Author: adiroiban, hawkowl
Reviewer: glyph
Fixes: #8124

Modified: trunk/twisted/python/deprecate.py
==============================================================================
--- trunk/twisted/python/deprecate.py	(original)
+++ trunk/twisted/python/deprecate.py	Tue Mar 15 21:54:30 2016
@@ -5,7 +5,7 @@
 """
 Deprecation framework for Twisted.
 
-To mark a method or function as being deprecated do this::
+To mark a method, function, or class as being deprecated do this::
 
     from twisted.python.versions import Version
     from twisted.python.deprecate import deprecated
@@ -17,8 +17,35 @@
         '''
         ...
 
-The newly-decorated badAPI will issue a warning when called. It will also have
-a deprecation notice appended to its docstring.
+    @deprecated(Version("Twisted", 16, 0, 0))
+    class BadClass(object):
+        '''
+        Docstring for BadClass.
+        '''
+
+The newly-decorated badAPI will issue a warning when called, and BadClass will
+issue a warning when instantiated. Both will also have  a deprecation notice
+appended to their docstring.
+
+To deprecate properties you can use::
+
+    from twisted.python.versions import Version
+    from twisted.python.deprecate import deprecatedProperty
+
+    class OtherwiseUndeprecatedClass(object):
+
+        @deprecatedProperty(Version('Twisted', 16, 0, 0))
+        def badProperty(self):
+            '''
+            Docstring for badProperty.
+            '''
+
+        @badProperty.setter
+        def badProperty(self, value):
+            '''
+            Setter sill also raise the deprecation warning.
+            '''
+
 
 To mark module-level attributes as being deprecated you can use::
 
@@ -48,6 +75,7 @@
 
 __all__ = [
     'deprecated',
+    'deprecatedProperty',
     'getDeprecationWarningString',
     'getWarningMethod',
     'setWarningMethod',
@@ -61,6 +89,7 @@
 from functools import wraps
 
 from twisted.python.versions import getVersionString
+from twisted.python.compat import _PY3
 
 DEPRECATION_WARNING_FORMAT = '%(fqpn)s was deprecated in %(version)s'
 
@@ -239,7 +268,8 @@
 
 def deprecated(version, replacement=None):
     """
-    Return a decorator that marks callables as deprecated.
+    Return a decorator that marks callables as deprecated. To deprecate a
+    property, see L{deprecatedProperty}.
 
     @type version: L{twisted.python.versions.Version}
     @param version: The version in which the callable will be marked as
@@ -279,6 +309,89 @@
 
 
 
+def deprecatedProperty(version, replacement=None):
+    """
+    Return a decorator that marks a property as deprecated. To deprecate a
+    regular callable or class, see L{deprecated}.
+
+    @type version: L{twisted.python.versions.Version}
+    @param version: The version in which the callable will be marked as
+        having been deprecated.  The decorated function will be annotated
+        with this version, having it set as its C{deprecatedVersion}
+        attribute.
+
+    @param version: the version that the callable was deprecated in.
+    @type version: L{twisted.python.versions.Version}
+
+    @param replacement: what should be used in place of the callable.
+        Either pass in a string, which will be inserted into the warning
+        message, or a callable, which will be expanded to its full import
+        path.
+    @type replacement: C{str} or callable
+
+    @return: A new property with deprecated setter and getter.
+    @rtype: C{property}
+
+    @since: 16.1.0
+    """
+
+    class _DeprecatedProperty(property):
+        """
+        Extension of the build-in property to allow deprecated setters.
+        """
+
+        def _deprecatedWrapper(self, function):
+            @wraps(function)
+            def deprecatedFunction(*args, **kwargs):
+                warn(
+                    self.warningString,
+                    DeprecationWarning,
+                    stacklevel=2)
+                return function(*args, **kwargs)
+            return deprecatedFunction
+
+
+        def setter(self, function):
+            return property.setter(self, self._deprecatedWrapper(function))
+
+
+    def deprecationDecorator(function):
+        if _PY3:
+            warningString = getDeprecationWarningString(
+                function, version, None, replacement)
+        else:
+            # Because Python 2 sucks, we need to implement our own here -- lack
+            # of __qualname__ means that we kinda have to stack walk. It maybe
+            # probably works. Probably. -Amber
+            functionName = function.__name__
+            className = inspect.stack()[1][3]  # wow hax
+            moduleName = function.__module__
+
+            fqdn = "%s.%s.%s" % (moduleName, className, functionName)
+
+            warningString = _getDeprecationWarningString(
+                fqdn, version, None, replacement)
+
+        @wraps(function)
+        def deprecatedFunction(*args, **kwargs):
+            warn(
+                warningString,
+                DeprecationWarning,
+                stacklevel=2)
+            return function(*args, **kwargs)
+
+        _appendToDocstring(deprecatedFunction,
+                           _getDeprecationDocstring(version, replacement))
+        deprecatedFunction.deprecatedVersion = version
+
+        result = _DeprecatedProperty(deprecatedFunction)
+        result.warningString = warningString
+        return result
+
+    return deprecationDecorator
+
+
+
 def getWarningMethod():
     """
     Return the warning method currently used to record deprecation warnings.

Modified: trunk/twisted/python/test/test_deprecate.py
==============================================================================
--- trunk/twisted/python/test/test_deprecate.py	(original)
+++ trunk/twisted/python/test/test_deprecate.py	Tue Mar 15 21:54:30 2016
@@ -22,7 +22,8 @@
     getDeprecationWarningString,
     deprecated, _appendToDocstring, _getDeprecationDocstring,
     _fullyQualifiedName as fullyQualifiedName,
-    _passed, _mutuallyExclusiveArguments
+    _passed, _mutuallyExclusiveArguments,
+    deprecatedProperty,
 )
 
 from twisted.python.versions import Version
@@ -530,6 +531,7 @@
             "Unexpected warning string: %r" % (msg,))
 
 
+
 def dummyCallable():
     """
     Do nothing.
@@ -686,6 +688,143 @@
                 __name__))
 
 
+
+@deprecated(Version('Twisted', 1, 2, 3))
+class DeprecatedClass(object):
+    """
+    Class which is entirely deprecated without having a replacement.
+    """
+
+
+
+class ClassWithDeprecatedProperty(object):
+    """
+    Class with a single deprecated property.
+    """
+
+    _someProtectedValue = None
+
+    @deprecatedProperty(Version('Twisted', 1, 2, 3))
+    def someProperty(self):
+        """
+        Getter docstring.
+
+        @return: The property.
+        """
+        return self._someProtectedValue
+
+
+    @someProperty.setter
+    def someProperty(self, value):
+        """
+        Setter docstring.
+        """
+        self._someProtectedValue = value
+
+
+
+class DeprecatedDecoratorTests(SynchronousTestCase):
+    """
+    Tests for deprecated decorators.
+    """
+
+    def assertDocstring(self, target, expected):
+        """
+        Check that C{target} object has the C{expected} docstring lines.
+
+        @param target: Object which is checked.
+        @type target: C{anything}
+
+        @param expected: List of lines, ignoring empty lines or leading or
+            trailing spaces.
+        @type expected: L{list} or L{str}
+        """
+        self.assertEqual(
+            expected,
+            [x.strip() for x in target.__doc__.splitlines() if x.strip()]
+            )
+
+
+    def test_propertyGetter(self):
+        """
+        When L{deprecatedProperty} is used on a C{property}, accesses raise a
+        L{DeprecationWarning} and getter docstring is updated to inform the
+        version in which it was deprecated. C{deprecatedVersion} attribute is
+        also set to inform the deprecation version.
+        """
+        obj = ClassWithDeprecatedProperty()
+
+        obj.someProperty
+
+        self.assertDocstring(
+            ClassWithDeprecatedProperty.someProperty,
+            [
+                'Getter docstring.',
+                '@return: The property.',
+                'Deprecated in Twisted 1.2.3.',
+                ],
+            )
+        ClassWithDeprecatedProperty.someProperty.deprecatedVersion = Version(
+            'Twisted', 1, 2, 3)
+
+        message = (
+            'twisted.python.test.test_deprecate.ClassWithDeprecatedProperty.'
+            'someProperty was deprecated in Twisted 1.2.3'
+            )
+        warnings = self.flushWarnings([self.test_propertyGetter])
+        self.assertEqual(1, len(warnings))
+        self.assertEqual(DeprecationWarning, warnings[0]['category'])
+        self.assertEqual(message, warnings[0]['message'])
+
+
+    def test_propertySetter(self):
+        """
+        When L{deprecatedProperty} is used on a C{property}, setter accesses
+        raise a L{DeprecationWarning}.
+        """
+        newValue = object()
+        obj = ClassWithDeprecatedProperty()
+
+        obj.someProperty = newValue
+
+        self.assertIs(newValue, obj._someProtectedValue)
+        message = (
+            'twisted.python.test.test_deprecate.ClassWithDeprecatedProperty.'
+            'someProperty was deprecated in Twisted 1.2.3'
+        )
+        warnings = self.flushWarnings([self.test_propertySetter])
+        self.assertEqual(1, len(warnings))
+        self.assertEqual(DeprecationWarning, warnings[0]['category'])
+        self.assertEqual(message, warnings[0]['message'])
+
+
+    def test_class(self):
+        """
+        When L{deprecated} is used on a class, instantiations raise a
+        L{DeprecationWarning} and class's docstring is updated to inform the
+        version in which it was deprecated. C{deprecatedVersion} attribute is
+        also set to inform the deprecation version.
+        """
+        DeprecatedClass()
+
+        self.assertDocstring(
+            DeprecatedClass,
+            [('Class which is entirely deprecated without having a '
+              'replacement.'),
+            'Deprecated in Twisted 1.2.3.'],
+            )
+        DeprecatedClass.deprecatedVersion = Version('Twisted', 1, 2, 3)
+
+        message = (
+            'twisted.python.test.test_deprecate.DeprecatedClass '
+            'was deprecated in Twisted 1.2.3'
+            )
+        warnings = self.flushWarnings([self.test_class])
+        self.assertEqual(1, len(warnings))
+        self.assertEqual(DeprecationWarning, warnings[0]['category'])
+        self.assertEqual(message, warnings[0]['message'])
+
+
     def test_deprecatedReplacement(self):
         """
         L{deprecated} takes an additional replacement parameter that can be used