1 # Copyright (c) Twisted Matrix Laboratories.
2 # See LICENSE for details.
5 Tests for Twisted's deprecation framework, L{twisted.python.deprecate}.
10 from os.path import normcase
12 from twisted.trial.unittest import TestCase
14 from twisted.python import deprecate
15 from twisted.python.deprecate import _appendToDocstring
16 from twisted.python.deprecate import _getDeprecationDocstring
17 from twisted.python.deprecate import deprecated, getDeprecationWarningString
18 from twisted.python.deprecate import _getDeprecationWarningString
19 from twisted.python.deprecate import DEPRECATION_WARNING_FORMAT
20 from twisted.python.reflect import fullyQualifiedName
21 from twisted.python.versions import Version
22 from twisted.python.filepath import FilePath
24 from twisted.python.test import deprecatedattributes
25 from twisted.python.test.modules_helpers import TwistedModulesTestCase
33 This is used to test the deprecation decorators.
37 def dummyReplacementMethod():
41 This is used to test the replacement parameter to L{deprecated}.
46 class TestDeprecationWarnings(TestCase):
47 def test_getDeprecationWarningString(self):
49 L{getDeprecationWarningString} returns a string that tells us that a
50 callable was deprecated at a certain released version of Twisted.
52 version = Version('Twisted', 8, 0, 0)
54 getDeprecationWarningString(self.test_getDeprecationWarningString,
56 "twisted.python.test.test_deprecate.TestDeprecationWarnings."
57 "test_getDeprecationWarningString was deprecated in "
61 def test_getDeprecationWarningStringWithFormat(self):
63 L{getDeprecationWarningString} returns a string that tells us that a
64 callable was deprecated at a certain released version of Twisted, with
65 a message containing additional information about the deprecation.
67 version = Version('Twisted', 8, 0, 0)
68 format = deprecate.DEPRECATION_WARNING_FORMAT + ': This is a message'
70 getDeprecationWarningString(self.test_getDeprecationWarningString,
72 'twisted.python.test.test_deprecate.TestDeprecationWarnings.'
73 'test_getDeprecationWarningString was deprecated in '
74 'Twisted 8.0.0: This is a message')
77 def test_deprecateEmitsWarning(self):
79 Decorating a callable with L{deprecated} emits a warning.
81 version = Version('Twisted', 8, 0, 0)
82 dummy = deprecated(version)(dummyCallable)
87 getDeprecationWarningString(dummyCallable, version),
92 def test_deprecatedPreservesName(self):
94 The decorated function has the same name as the original.
96 version = Version('Twisted', 8, 0, 0)
97 dummy = deprecated(version)(dummyCallable)
98 self.assertEqual(dummyCallable.__name__, dummy.__name__)
99 self.assertEqual(fullyQualifiedName(dummyCallable),
100 fullyQualifiedName(dummy))
103 def test_getDeprecationDocstring(self):
105 L{_getDeprecationDocstring} returns a note about the deprecation to go
108 version = Version('Twisted', 8, 0, 0)
110 "Deprecated in Twisted 8.0.0.",
111 _getDeprecationDocstring(version, ''))
114 def test_deprecatedUpdatesDocstring(self):
116 The docstring of the deprecated function is appended with information
117 about the deprecation.
120 version = Version('Twisted', 8, 0, 0)
121 dummy = deprecated(version)(dummyCallable)
125 _getDeprecationDocstring(version, ''))
127 self.assertEqual(dummyCallable.__doc__, dummy.__doc__)
130 def test_versionMetadata(self):
132 Deprecating a function adds version information to the decorated
133 version of that function.
135 version = Version('Twisted', 8, 0, 0)
136 dummy = deprecated(version)(dummyCallable)
137 self.assertEqual(version, dummy.deprecatedVersion)
140 def test_getDeprecationWarningStringReplacement(self):
142 L{getDeprecationWarningString} takes an additional replacement parameter
143 that can be used to add information to the deprecation. If the
144 replacement parameter is a string, it will be interpolated directly into
147 version = Version('Twisted', 8, 0, 0)
148 warningString = getDeprecationWarningString(
149 self.test_getDeprecationWarningString, version,
150 replacement="something.foobar")
153 "%s was deprecated in Twisted 8.0.0; please use something.foobar "
155 fullyQualifiedName(self.test_getDeprecationWarningString),))
158 def test_getDeprecationWarningStringReplacementWithCallable(self):
160 L{getDeprecationWarningString} takes an additional replacement parameter
161 that can be used to add information to the deprecation. If the
162 replacement parameter is a callable, its fully qualified name will be
163 interpolated into the result.
165 version = Version('Twisted', 8, 0, 0)
166 warningString = getDeprecationWarningString(
167 self.test_getDeprecationWarningString, version,
168 replacement=dummyReplacementMethod)
171 "%s was deprecated in Twisted 8.0.0; please use "
172 "twisted.python.test.test_deprecate.dummyReplacementMethod "
174 fullyQualifiedName(self.test_getDeprecationWarningString),))
177 def test_deprecatedReplacement(self):
179 L{deprecated} takes an additional replacement parameter that can be used
180 to indicate the new, non-deprecated method developers should use. If
181 the replacement parameter is a string, it will be interpolated directly
182 into the warning message.
184 version = Version('Twisted', 8, 0, 0)
185 dummy = deprecated(version, "something.foobar")(dummyCallable)
186 self.assertEqual(dummy.__doc__,
189 " This is used to test the deprecation decorators.\n\n"
190 " Deprecated in Twisted 8.0.0; please use "
196 def test_deprecatedReplacementWithCallable(self):
198 L{deprecated} takes an additional replacement parameter that can be used
199 to indicate the new, non-deprecated method developers should use. If
200 the replacement parameter is a callable, its fully qualified name will
201 be interpolated into the warning message.
203 version = Version('Twisted', 8, 0, 0)
204 decorator = deprecated(version, replacement=dummyReplacementMethod)
205 dummy = decorator(dummyCallable)
206 self.assertEqual(dummy.__doc__,
209 " This is used to test the deprecation decorators.\n\n"
210 " Deprecated in Twisted 8.0.0; please use "
211 "twisted.python.test.test_deprecate.dummyReplacementMethod"
217 class TestAppendToDocstring(TestCase):
219 Test the _appendToDocstring function.
221 _appendToDocstring is used to add text to a docstring.
224 def test_appendToEmptyDocstring(self):
226 Appending to an empty docstring simply replaces the docstring.
232 _appendToDocstring(noDocstring, "Appended text.")
233 self.assertEqual("Appended text.", noDocstring.__doc__)
236 def test_appendToSingleLineDocstring(self):
238 Appending to a single line docstring places the message on a new line,
239 with a blank line separating it from the rest of the docstring.
241 The docstring ends with a newline, conforming to Twisted and PEP 8
242 standards. Unfortunately, the indentation is incorrect, since the
243 existing docstring doesn't have enough info to help us indent
247 def singleLineDocstring():
248 """This doesn't comply with standards, but is here for a test."""
250 _appendToDocstring(singleLineDocstring, "Appended text.")
252 ["This doesn't comply with standards, but is here for a test.",
255 singleLineDocstring.__doc__.splitlines())
256 self.assertTrue(singleLineDocstring.__doc__.endswith('\n'))
259 def test_appendToMultilineDocstring(self):
261 Appending to a multi-line docstring places the messade on a new line,
262 with a blank line separating it from the rest of the docstring.
264 Because we have multiple lines, we have enough information to do
268 def multiLineDocstring():
270 This is a multi-line docstring.
273 def expectedDocstring():
275 This is a multi-line docstring.
280 _appendToDocstring(multiLineDocstring, "Appended text.")
282 expectedDocstring.__doc__, multiLineDocstring.__doc__)
286 class _MockDeprecatedAttribute(object):
288 Mock of L{twisted.python.deprecate._DeprecatedAttribute}.
290 @ivar value: The value of the attribute.
292 def __init__(self, value):
304 class ModuleProxyTests(TestCase):
306 Tests for L{twisted.python.deprecate._ModuleProxy}, which proxies
307 access to module-level attributes, intercepting access to deprecated
308 attributes and passing through access to normal attributes.
310 def _makeProxy(self, **attrs):
312 Create a temporary module proxy object.
314 @param **kw: Attributes to initialise on the temporary module object
316 @rtype: L{twistd.python.deprecate._ModuleProxy}
318 mod = types.ModuleType('foo')
319 for key, value in attrs.iteritems():
320 setattr(mod, key, value)
321 return deprecate._ModuleProxy(mod)
324 def test_getattrPassthrough(self):
326 Getting a normal attribute on a L{twisted.python.deprecate._ModuleProxy}
327 retrieves the underlying attribute's value, and raises C{AttributeError}
328 if a non-existant attribute is accessed.
330 proxy = self._makeProxy(SOME_ATTRIBUTE='hello')
331 self.assertIdentical(proxy.SOME_ATTRIBUTE, 'hello')
332 self.assertRaises(AttributeError, getattr, proxy, 'DOES_NOT_EXIST')
335 def test_getattrIntercept(self):
337 Getting an attribute marked as being deprecated on
338 L{twisted.python.deprecate._ModuleProxy} results in calling the
339 deprecated wrapper's C{get} method.
341 proxy = self._makeProxy()
342 _deprecatedAttributes = object.__getattribute__(
343 proxy, '_deprecatedAttributes')
344 _deprecatedAttributes['foo'] = _MockDeprecatedAttribute(42)
345 self.assertEqual(proxy.foo, 42)
348 def test_privateAttributes(self):
350 Private attributes of L{twisted.python.deprecate._ModuleProxy} are
351 inaccessible when regular attribute access is used.
353 proxy = self._makeProxy()
354 self.assertRaises(AttributeError, getattr, proxy, '_module')
356 AttributeError, getattr, proxy, '_deprecatedAttributes')
359 def test_setattr(self):
361 Setting attributes on L{twisted.python.deprecate._ModuleProxy} proxies
362 them through to the wrapped module.
364 proxy = self._makeProxy()
366 self.assertNotEquals(object.__getattribute__(proxy, '_module'), 1)
367 self.assertEqual(proxy._module, 1)
372 L{twisted.python.deprecated._ModuleProxy.__repr__} produces a string
373 containing the proxy type and a representation of the wrapped module
376 proxy = self._makeProxy()
377 realModule = object.__getattribute__(proxy, '_module')
379 repr(proxy), '<%s module=%r>' % (type(proxy).__name__, realModule))
383 class DeprecatedAttributeTests(TestCase):
385 Tests for L{twisted.python.deprecate._DeprecatedAttribute} and
386 L{twisted.python.deprecate.deprecatedModuleAttribute}, which issue
387 warnings for deprecated module-level attributes.
390 self.version = deprecatedattributes.version
391 self.message = deprecatedattributes.message
392 self._testModuleName = __name__ + '.foo'
395 def _getWarningString(self, attr):
397 Create the warning string used by deprecated attributes.
399 return _getDeprecationWarningString(
400 deprecatedattributes.__name__ + '.' + attr,
401 deprecatedattributes.version,
402 DEPRECATION_WARNING_FORMAT + ': ' + deprecatedattributes.message)
405 def test_deprecatedAttributeHelper(self):
407 L{twisted.python.deprecate._DeprecatedAttribute} correctly sets its
408 __name__ to match that of the deprecated attribute and emits a warning
409 when the original attribute value is accessed.
411 name = 'ANOTHER_DEPRECATED_ATTRIBUTE'
412 setattr(deprecatedattributes, name, 42)
413 attr = deprecate._DeprecatedAttribute(
414 deprecatedattributes, name, self.version, self.message)
416 self.assertEqual(attr.__name__, name)
418 # Since we're accessing the value getter directly, as opposed to via
419 # the module proxy, we need to match the warning's stack level.
423 # Access the deprecated attribute.
425 warningsShown = self.flushWarnings([
426 self.test_deprecatedAttributeHelper])
427 self.assertIdentical(warningsShown[0]['category'], DeprecationWarning)
429 warningsShown[0]['message'],
430 self._getWarningString(name))
431 self.assertEqual(len(warningsShown), 1)
434 def test_deprecatedAttribute(self):
436 L{twisted.python.deprecate.deprecatedModuleAttribute} wraps a
437 module-level attribute in an object that emits a deprecation warning
438 when it is accessed the first time only, while leaving other unrelated
441 # Accessing non-deprecated attributes does not issue a warning.
442 deprecatedattributes.ANOTHER_ATTRIBUTE
443 warningsShown = self.flushWarnings([self.test_deprecatedAttribute])
444 self.assertEqual(len(warningsShown), 0)
446 name = 'DEPRECATED_ATTRIBUTE'
448 # Access the deprecated attribute. This uses getattr to avoid repeating
449 # the attribute name.
450 getattr(deprecatedattributes, name)
452 warningsShown = self.flushWarnings([self.test_deprecatedAttribute])
453 self.assertEqual(len(warningsShown), 1)
454 self.assertIdentical(warningsShown[0]['category'], DeprecationWarning)
456 warningsShown[0]['message'],
457 self._getWarningString(name))
460 def test_wrappedModule(self):
462 Deprecating an attribute in a module replaces and wraps that module
463 instance, in C{sys.modules}, with a
464 L{twisted.python.deprecate._ModuleProxy} instance but only if it hasn't
465 already been wrapped.
467 sys.modules[self._testModuleName] = mod = types.ModuleType('foo')
468 self.addCleanup(sys.modules.pop, self._testModuleName)
470 setattr(mod, 'first', 1)
471 setattr(mod, 'second', 2)
473 deprecate.deprecatedModuleAttribute(
474 Version('Twisted', 8, 0, 0),
476 self._testModuleName,
479 proxy = sys.modules[self._testModuleName]
480 self.assertNotEqual(proxy, mod)
482 deprecate.deprecatedModuleAttribute(
483 Version('Twisted', 8, 0, 0),
485 self._testModuleName,
488 self.assertIdentical(proxy, sys.modules[self._testModuleName])
492 class ImportedModuleAttributeTests(TwistedModulesTestCase):
494 Tests for L{deprecatedModuleAttribute} which involve loading a module via
499 from twisted.python.deprecate import deprecatedModuleAttribute
500 from twisted.python.versions import Version
502 deprecatedModuleAttribute(
503 Version('Package', 1, 2, 3), 'message', __name__, 'module')
507 def pathEntryTree(self, tree):
509 Create some files in a hierarchy, based on a dictionary describing those
510 files. The resulting hierarchy will be placed onto sys.path for the
511 duration of the test.
513 @param tree: A dictionary representing a directory structure. Keys are
514 strings, representing filenames, dictionary values represent
515 directories, string values represent file contents.
517 @return: another dictionary similar to the input, with file content
518 strings replaced with L{FilePath} objects pointing at where those
519 contents are now stored.
521 def makeSomeFiles(pathobj, dirdict):
523 for (key, value) in dirdict.items():
524 child = pathobj.child(key)
525 if isinstance(value, str):
526 pathdict[key] = child
527 child.setContent(value)
528 elif isinstance(value, dict):
529 child.createDirectory()
530 pathdict[key] = makeSomeFiles(child, value)
532 raise ValueError("only strings and dicts allowed as values")
534 base = FilePath(self.mktemp())
537 result = makeSomeFiles(base, tree)
538 self.replaceSysPath([base.path] + sys.path)
539 self.replaceSysModules(sys.modules.copy())
543 def simpleModuleEntry(self):
545 Add a sample module and package to the path, returning a L{FilePath}
546 pointing at the module which will be loadable as C{package.module}.
548 paths = self.pathEntryTree(
549 {"package": {"__init__.py": self._packageInit,
551 return paths['package']['module.py']
554 def checkOneWarning(self, modulePath):
556 Verification logic for L{test_deprecatedModule}.
558 # import package.module
559 from package import module
560 self.assertEqual(module.__file__, modulePath.path)
561 emitted = self.flushWarnings([self.checkOneWarning])
562 self.assertEqual(len(emitted), 1)
563 self.assertEqual(emitted[0]['message'],
564 'package.module was deprecated in Package 1.2.3: '
566 self.assertEqual(emitted[0]['category'], DeprecationWarning)
569 def test_deprecatedModule(self):
571 If L{deprecatedModuleAttribute} is used to deprecate a module attribute
572 of a package, only one deprecation warning is emitted when the
573 deprecated module is imported.
575 self.checkOneWarning(self.simpleModuleEntry())
578 def test_deprecatedModuleMultipleTimes(self):
580 If L{deprecatedModuleAttribute} is used to deprecate a module attribute
581 of a package, only one deprecation warning is emitted when the
582 deprecated module is subsequently imported.
584 mp = self.simpleModuleEntry()
585 # The first time, the code needs to be loaded.
586 self.checkOneWarning(mp)
587 # The second time, things are slightly different; the object's already
589 self.checkOneWarning(mp)
590 # The third and fourth times, things things should all be exactly the
591 # same, but this is a sanity check to make sure the implementation isn't
592 # special casing the second time. Also, putting these cases into a loop
593 # means that the stack will be identical, to make sure that the
594 # implementation doesn't rely too much on stack-crawling.
596 self.checkOneWarning(mp)
600 class WarnAboutFunctionTests(TestCase):
602 Tests for L{twisted.python.deprecate.warnAboutFunction} which allows the
603 callers of a function to issue a C{DeprecationWarning} about that function.
607 Create a file that will have known line numbers when emitting warnings.
609 self.package = FilePath(self.mktemp()).child('twisted_private_helper')
610 self.package.makedirs()
611 self.package.child('__init__.py').setContent('')
612 self.package.child('module.py').setContent('''
615 from twisted.python import deprecate
622 def callTestFunction():
625 deprecate.warnAboutFunction(testFunction, "A Warning String")
627 sys.path.insert(0, self.package.parent().path)
628 self.addCleanup(sys.path.remove, self.package.parent().path)
630 modules = sys.modules.copy()
632 lambda: (sys.modules.clear(), sys.modules.update(modules)))
635 def test_warning(self):
637 L{deprecate.warnAboutFunction} emits a warning the file and line number
638 of which point to the beginning of the implementation of the function
643 deprecate.warnAboutFunction(aFunc, 'A Warning Message')
644 warningsShown = self.flushWarnings()
646 if filename.lower().endswith('.pyc'):
647 filename = filename[:-1]
649 FilePath(warningsShown[0]["filename"]), FilePath(filename))
650 self.assertEqual(warningsShown[0]["message"], "A Warning Message")
653 def test_warningLineNumber(self):
655 L{deprecate.warnAboutFunction} emits a C{DeprecationWarning} with the
656 number of a line within the implementation of the function passed to it.
658 from twisted_private_helper import module
659 module.callTestFunction()
660 warningsShown = self.flushWarnings()
662 FilePath(warningsShown[0]["filename"]),
663 self.package.sibling('twisted_private_helper').child('module.py'))
664 # Line number 9 is the last line in the testFunction in the helper
666 self.assertEqual(warningsShown[0]["lineno"], 9)
667 self.assertEqual(warningsShown[0]["message"], "A Warning String")
668 self.assertEqual(len(warningsShown), 1)
671 def assertSamePath(self, first, second):
673 Assert that the two paths are the same, considering case normalization
674 appropriate for the current platform.
676 @type first: L{FilePath}
677 @type second: L{FilePath}
679 @raise C{self.failureType}: If the paths are not the same.
682 normcase(first.path) == normcase(second.path),
683 "%r != %r" % (first, second))
686 def test_renamedFile(self):
688 Even if the implementation of a deprecated function is moved around on
689 the filesystem, the line number in the warning emitted by
690 L{deprecate.warnAboutFunction} points to a line in the implementation of
691 the deprecated function.
693 from twisted_private_helper import module
694 # Clean up the state resulting from that import; we're not going to use
695 # this module, so it should go away.
696 del sys.modules['twisted_private_helper']
697 del sys.modules[module.__name__]
699 # Rename the source directory
700 self.package.moveTo(self.package.sibling('twisted_renamed_helper'))
702 # Import the newly renamed version
703 from twisted_renamed_helper import module
704 self.addCleanup(sys.modules.pop, 'twisted_renamed_helper')
705 self.addCleanup(sys.modules.pop, module.__name__)
707 module.callTestFunction()
708 warningsShown = self.flushWarnings()
709 warnedPath = FilePath(warningsShown[0]["filename"])
710 expectedPath = self.package.sibling(
711 'twisted_renamed_helper').child('module.py')
712 self.assertSamePath(warnedPath, expectedPath)
713 self.assertEqual(warningsShown[0]["lineno"], 9)
714 self.assertEqual(warningsShown[0]["message"], "A Warning String")
715 self.assertEqual(len(warningsShown), 1)
718 def test_filteredWarning(self):
720 L{deprecate.warnAboutFunction} emits a warning that will be filtered if
721 L{warnings.filterwarning} is called with the module name of the
724 # Clean up anything *else* that might spuriously filter out the warning,
725 # such as the "always" simplefilter set up by unittest._collectWarnings.
726 # We'll also rely on trial to restore the original filters afterwards.
727 del warnings.filters[:]
729 warnings.filterwarnings(
730 action="ignore", module="twisted_private_helper")
732 from twisted_private_helper import module
733 module.callTestFunction()
735 warningsShown = self.flushWarnings()
736 self.assertEqual(len(warningsShown), 0)
739 def test_filteredOnceWarning(self):
741 L{deprecate.warnAboutFunction} emits a warning that will be filtered
742 once if L{warnings.filterwarning} is called with the module name of the
743 deprecated function and an action of once.
745 # Clean up anything *else* that might spuriously filter out the warning,
746 # such as the "always" simplefilter set up by unittest._collectWarnings.
747 # We'll also rely on trial to restore the original filters afterwards.
748 del warnings.filters[:]
750 warnings.filterwarnings(
751 action="module", module="twisted_private_helper")
753 from twisted_private_helper import module
754 module.callTestFunction()
755 module.callTestFunction()
757 warningsShown = self.flushWarnings()
758 self.assertEqual(len(warningsShown), 1)
759 message = warningsShown[0]['message']
760 category = warningsShown[0]['category']
761 filename = warningsShown[0]['filename']
762 lineno = warningsShown[0]['lineno']
763 msg = warnings.formatwarning(message, category, filename, lineno)
765 msg.endswith("module.py:9: DeprecationWarning: A Warning String\n"
767 "Unexpected warning string: %r" % (msg,))