%PDF- %PDF-
Direktori : /lib/python3/dist-packages/twisted/python/test/ |
Current File : //lib/python3/dist-packages/twisted/python/test/test_deprecate.py |
# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. """ Tests for Twisted's deprecation framework, L{twisted.python.deprecate}. """ import inspect import sys import types import warnings from os.path import normcase from warnings import catch_warnings, simplefilter try: from importlib import invalidate_caches except ImportError: invalidate_caches = None # type: ignore[assignment] from incremental import Version from twisted.python import deprecate from twisted.python.deprecate import ( DEPRECATION_WARNING_FORMAT, _appendToDocstring, _fullyQualifiedName as fullyQualifiedName, _getDeprecationDocstring, _getDeprecationWarningString, _mutuallyExclusiveArguments, _passedArgSpec, _passedSignature, deprecated, deprecatedKeywordParameter, deprecatedProperty, getDeprecationWarningString, ) from twisted.python.filepath import FilePath from twisted.python.runtime import platform from twisted.python.test import deprecatedattributes from twisted.python.test.modules_helpers import TwistedModulesMixin from twisted.trial.unittest import SynchronousTestCase # Note that various tests in this module require manual encoding of paths to # utf-8. This can be fixed once FilePath supports Unicode; see #2366, #4736, # #5203. class _MockDeprecatedAttribute: """ Mock of L{twisted.python.deprecate._DeprecatedAttribute}. @ivar value: The value of the attribute. """ def __init__(self, value): self.value = value def get(self): """ Get a known value. """ return self.value class ModuleProxyTests(SynchronousTestCase): """ Tests for L{twisted.python.deprecate._ModuleProxy}, which proxies access to module-level attributes, intercepting access to deprecated attributes and passing through access to normal attributes. """ def _makeProxy(self, **attrs): """ Create a temporary module proxy object. @param **kw: Attributes to initialise on the temporary module object @rtype: L{twistd.python.deprecate._ModuleProxy} """ mod = types.ModuleType("foo") for key, value in attrs.items(): setattr(mod, key, value) return deprecate._ModuleProxy(mod) def test_getattrPassthrough(self): """ Getting a normal attribute on a L{twisted.python.deprecate._ModuleProxy} retrieves the underlying attribute's value, and raises C{AttributeError} if a non-existent attribute is accessed. """ proxy = self._makeProxy(SOME_ATTRIBUTE="hello") self.assertIs(proxy.SOME_ATTRIBUTE, "hello") self.assertRaises(AttributeError, getattr, proxy, "DOES_NOT_EXIST") def test_getattrIntercept(self): """ Getting an attribute marked as being deprecated on L{twisted.python.deprecate._ModuleProxy} results in calling the deprecated wrapper's C{get} method. """ proxy = self._makeProxy() _deprecatedAttributes = object.__getattribute__(proxy, "_deprecatedAttributes") _deprecatedAttributes["foo"] = _MockDeprecatedAttribute(42) self.assertEqual(proxy.foo, 42) def test_privateAttributes(self): """ Private attributes of L{twisted.python.deprecate._ModuleProxy} are inaccessible when regular attribute access is used. """ proxy = self._makeProxy() self.assertRaises(AttributeError, getattr, proxy, "_module") self.assertRaises(AttributeError, getattr, proxy, "_deprecatedAttributes") def test_setattr(self): """ Setting attributes on L{twisted.python.deprecate._ModuleProxy} proxies them through to the wrapped module. """ proxy = self._makeProxy() proxy._module = 1 self.assertNotEqual(object.__getattribute__(proxy, "_module"), 1) self.assertEqual(proxy._module, 1) def test_repr(self): """ L{twisted.python.deprecated._ModuleProxy.__repr__} produces a string containing the proxy type and a representation of the wrapped module object. """ proxy = self._makeProxy() realModule = object.__getattribute__(proxy, "_module") self.assertEqual(repr(proxy), f"<{type(proxy).__name__} module={realModule!r}>") class DeprecatedAttributeTests(SynchronousTestCase): """ Tests for L{twisted.python.deprecate._DeprecatedAttribute} and L{twisted.python.deprecate.deprecatedModuleAttribute}, which issue warnings for deprecated module-level attributes. """ def setUp(self): self.version = deprecatedattributes.version self.message = deprecatedattributes.message self._testModuleName = __name__ + ".foo" def _getWarningString(self, attr): """ Create the warning string used by deprecated attributes. """ return _getDeprecationWarningString( deprecatedattributes.__name__ + "." + attr, deprecatedattributes.version, DEPRECATION_WARNING_FORMAT + ": " + deprecatedattributes.message, ) def test_deprecatedAttributeHelper(self): """ L{twisted.python.deprecate._DeprecatedAttribute} correctly sets its __name__ to match that of the deprecated attribute and emits a warning when the original attribute value is accessed. """ name = "ANOTHER_DEPRECATED_ATTRIBUTE" setattr(deprecatedattributes, name, 42) attr = deprecate._DeprecatedAttribute( deprecatedattributes, name, self.version, self.message ) self.assertEqual(attr.__name__, name) # Since we're accessing the value getter directly, as opposed to via # the module proxy, we need to match the warning's stack level. def addStackLevel(): attr.get() # Access the deprecated attribute. addStackLevel() warningsShown = self.flushWarnings([self.test_deprecatedAttributeHelper]) self.assertIs(warningsShown[0]["category"], DeprecationWarning) self.assertEqual(warningsShown[0]["message"], self._getWarningString(name)) self.assertEqual(len(warningsShown), 1) def test_deprecatedAttribute(self): """ L{twisted.python.deprecate.deprecatedModuleAttribute} wraps a module-level attribute in an object that emits a deprecation warning when it is accessed the first time only, while leaving other unrelated attributes alone. """ # Accessing non-deprecated attributes does not issue a warning. deprecatedattributes.ANOTHER_ATTRIBUTE warningsShown = self.flushWarnings([self.test_deprecatedAttribute]) self.assertEqual(len(warningsShown), 0) name = "DEPRECATED_ATTRIBUTE" # Access the deprecated attribute. This uses getattr to avoid repeating # the attribute name. getattr(deprecatedattributes, name) warningsShown = self.flushWarnings([self.test_deprecatedAttribute]) self.assertEqual(len(warningsShown), 1) self.assertIs(warningsShown[0]["category"], DeprecationWarning) self.assertEqual(warningsShown[0]["message"], self._getWarningString(name)) def test_wrappedModule(self): """ Deprecating an attribute in a module replaces and wraps that module instance, in C{sys.modules}, with a L{twisted.python.deprecate._ModuleProxy} instance but only if it hasn't already been wrapped. """ sys.modules[self._testModuleName] = mod = types.ModuleType("foo") self.addCleanup(sys.modules.pop, self._testModuleName) setattr(mod, "first", 1) setattr(mod, "second", 2) deprecate.deprecatedModuleAttribute( Version("Twisted", 8, 0, 0), "message", self._testModuleName, "first" ) proxy = sys.modules[self._testModuleName] self.assertNotEqual(proxy, mod) deprecate.deprecatedModuleAttribute( Version("Twisted", 8, 0, 0), "message", self._testModuleName, "second" ) self.assertIs(proxy, sys.modules[self._testModuleName]) class ImportedModuleAttributeTests(TwistedModulesMixin, SynchronousTestCase): """ Tests for L{deprecatedModuleAttribute} which involve loading a module via 'import'. """ _packageInit = """\ from twisted.python.deprecate import deprecatedModuleAttribute from incremental import Version deprecatedModuleAttribute( Version('Package', 1, 2, 3), 'message', __name__, 'module') """ def pathEntryTree(self, tree): """ Create some files in a hierarchy, based on a dictionary describing those files. The resulting hierarchy will be placed onto sys.path for the duration of the test. @param tree: A dictionary representing a directory structure. Keys are strings, representing filenames, dictionary values represent directories, string values represent file contents. @return: another dictionary similar to the input, with file content strings replaced with L{FilePath} objects pointing at where those contents are now stored. """ def makeSomeFiles(pathobj, dirdict): pathdict = {} for key, value in dirdict.items(): child = pathobj.child(key) if isinstance(value, bytes): pathdict[key] = child child.setContent(value) elif isinstance(value, dict): child.createDirectory() pathdict[key] = makeSomeFiles(child, value) else: raise ValueError("only strings and dicts allowed as values") return pathdict base = FilePath(self.mktemp().encode("utf-8")) base.makedirs() result = makeSomeFiles(base, tree) # On Python 3, sys.path cannot include byte paths: self.replaceSysPath([base.path.decode("utf-8")] + sys.path) self.replaceSysModules(sys.modules.copy()) return result def simpleModuleEntry(self): """ Add a sample module and package to the path, returning a L{FilePath} pointing at the module which will be loadable as C{package.module}. """ paths = self.pathEntryTree( { b"package": { b"__init__.py": self._packageInit.encode("utf-8"), b"module.py": b"", } } ) return paths[b"package"][b"module.py"] def checkOneWarning(self, modulePath): """ Verification logic for L{test_deprecatedModule}. """ from package import module # type: ignore[import-not-found] self.assertEqual(FilePath(module.__file__.encode("utf-8")), modulePath) emitted = self.flushWarnings([self.checkOneWarning]) self.assertEqual(len(emitted), 1) self.assertEqual( emitted[0]["message"], "package.module was deprecated in Package 1.2.3: " "message", ) self.assertEqual(emitted[0]["category"], DeprecationWarning) def test_deprecatedModule(self): """ If L{deprecatedModuleAttribute} is used to deprecate a module attribute of a package, only one deprecation warning is emitted when the deprecated module is imported. """ self.checkOneWarning(self.simpleModuleEntry()) def test_deprecatedModuleMultipleTimes(self): """ If L{deprecatedModuleAttribute} is used to deprecate a module attribute of a package, only one deprecation warning is emitted when the deprecated module is subsequently imported. """ mp = self.simpleModuleEntry() # The first time, the code needs to be loaded. self.checkOneWarning(mp) # The second time, things are slightly different; the object's already # in the namespace. self.checkOneWarning(mp) # The third and fourth times, things things should all be exactly the # same, but this is a sanity check to make sure the implementation isn't # special casing the second time. Also, putting these cases into a loop # means that the stack will be identical, to make sure that the # implementation doesn't rely too much on stack-crawling. for x in range(2): self.checkOneWarning(mp) class WarnAboutFunctionTests(SynchronousTestCase): """ Tests for L{twisted.python.deprecate.warnAboutFunction} which allows the callers of a function to issue a C{DeprecationWarning} about that function. """ def setUp(self): """ Create a file that will have known line numbers when emitting warnings. """ self.package = FilePath(self.mktemp()).child("twisted_private_helper") self.package.makedirs() self.package.child("__init__.py").setContent(b"") self.package.child("module.py").setContent( b""" "A module string" from twisted.python import deprecate def testFunction(): "A doc string" a = 1 + 2 return a def callTestFunction(): b = testFunction() if b == 3: deprecate.warnAboutFunction(testFunction, "A Warning String") """ ) self.package.child("pep626.py").setContent( b""" "A module string" from twisted.python import deprecate def noop(): pass def testFunction(a=1, b=1): "A doc string" if a: if b: noop() else: pass def callTestFunction(): b = testFunction() if b is None: deprecate.warnAboutFunction(testFunction, "A Warning String") """ ) # Python 3 doesn't accept bytes in sys.path: packagePath = self.package.parent().path sys.path.insert(0, packagePath) self.addCleanup(sys.path.remove, packagePath) modules = sys.modules.copy() self.addCleanup(lambda: (sys.modules.clear(), sys.modules.update(modules))) # On Windows, most FilePath interactions produce # DeprecationWarnings, so flush them here so that they don't interfere # with the tests. if platform.isWindows(): self.flushWarnings() def test_warning(self): """ L{deprecate.warnAboutFunction} emits a warning the file and line number of which point to the beginning of the implementation of the function passed to it. """ def aFunc(): pass deprecate.warnAboutFunction(aFunc, "A Warning Message") warningsShown = self.flushWarnings() filename = __file__ if filename.lower().endswith(".pyc"): filename = filename[:-1] self.assertSamePath(FilePath(warningsShown[0]["filename"]), FilePath(filename)) self.assertEqual(warningsShown[0]["message"], "A Warning Message") def test_warningLineNumber(self): """ L{deprecate.warnAboutFunction} emits a C{DeprecationWarning} with the number of a line within the implementation of the function passed to it. """ from twisted_private_helper import module # type: ignore[import-not-found] module.callTestFunction() warningsShown = self.flushWarnings() self.assertSamePath( FilePath(warningsShown[0]["filename"].encode("utf-8")), self.package.sibling(b"twisted_private_helper").child(b"module.py"), ) # Line number 9 is the last line in the testFunction in the helper # module. self.assertEqual(warningsShown[0]["lineno"], 9) self.assertEqual(warningsShown[0]["message"], "A Warning String") self.assertEqual(len(warningsShown), 1) def test_warningLineNumberDisFindlinestarts(self): """ L{deprecate.warnAboutFunction} emits a C{DeprecationWarning} with the number of a line within the implementation handling the case in which dis.findlinestarts returns the lines in random order. """ from twisted_private_helper import pep626 pep626.callTestFunction() warningsShown = self.flushWarnings() self.assertSamePath( FilePath(warningsShown[0]["filename"].encode("utf-8")), self.package.sibling(b"twisted_private_helper").child(b"pep626.py"), ) # Line number 15 is the last line in the testFunction in the helper # module. self.assertEqual(warningsShown[0]["lineno"], 15) self.assertEqual(warningsShown[0]["message"], "A Warning String") self.assertEqual(len(warningsShown), 1) def assertSamePath(self, first, second): """ Assert that the two paths are the same, considering case normalization appropriate for the current platform. @type first: L{FilePath} @type second: L{FilePath} @raise C{self.failureType}: If the paths are not the same. """ self.assertTrue( normcase(first.path) == normcase(second.path), f"{first!r} != {second!r}", ) def test_renamedFile(self): """ Even if the implementation of a deprecated function is moved around on the filesystem, the line number in the warning emitted by L{deprecate.warnAboutFunction} points to a line in the implementation of the deprecated function. """ from twisted_private_helper import module # Clean up the state resulting from that import; we're not going to use # this module, so it should go away. del sys.modules["twisted_private_helper"] del sys.modules[module.__name__] # Rename the source directory self.package.moveTo(self.package.sibling(b"twisted_renamed_helper")) # Make sure importlib notices we've changed importable packages: if invalidate_caches: # type: ignore[truthy-function] invalidate_caches() # Import the newly renamed version from twisted_renamed_helper import module # type: ignore[import-not-found] self.addCleanup(sys.modules.pop, "twisted_renamed_helper") self.addCleanup(sys.modules.pop, module.__name__) module.callTestFunction() warningsShown = self.flushWarnings([module.testFunction]) warnedPath = FilePath(warningsShown[0]["filename"].encode("utf-8")) expectedPath = self.package.sibling(b"twisted_renamed_helper").child( b"module.py" ) self.assertSamePath(warnedPath, expectedPath) self.assertEqual(warningsShown[0]["lineno"], 9) self.assertEqual(warningsShown[0]["message"], "A Warning String") self.assertEqual(len(warningsShown), 1) def test_filteredWarning(self): """ L{deprecate.warnAboutFunction} emits a warning that will be filtered if L{warnings.filterwarning} is called with the module name of the deprecated function. """ # Clean up anything *else* that might spuriously filter out the warning, # such as the "always" simplefilter set up by unittest._collectWarnings. # We'll also rely on trial to restore the original filters afterwards. del warnings.filters[:] warnings.filterwarnings(action="ignore", module="twisted_private_helper") from twisted_private_helper import module module.callTestFunction() warningsShown = self.flushWarnings() self.assertEqual(len(warningsShown), 0) def test_filteredOnceWarning(self): """ L{deprecate.warnAboutFunction} emits a warning that will be filtered once if L{warnings.filterwarning} is called with the module name of the deprecated function and an action of once. """ # Clean up anything *else* that might spuriously filter out the warning, # such as the "always" simplefilter set up by unittest._collectWarnings. # We'll also rely on trial to restore the original filters afterwards. del warnings.filters[:] warnings.filterwarnings(action="module", module="twisted_private_helper") from twisted_private_helper import module module.callTestFunction() module.callTestFunction() warningsShown = self.flushWarnings() self.assertEqual(len(warningsShown), 1) message = warningsShown[0]["message"] category = warningsShown[0]["category"] filename = warningsShown[0]["filename"] lineno = warningsShown[0]["lineno"] msg = warnings.formatwarning(message, category, filename, lineno) self.assertTrue( msg.endswith( "module.py:9: DeprecationWarning: A Warning String\n" " return a\n" ), f"Unexpected warning string: {msg!r}", ) def dummyCallable(): """ Do nothing. This is used to test the deprecation decorators. """ def dummyReplacementMethod(): """ Do nothing. This is used to test the replacement parameter to L{deprecated}. """ class DeprecationWarningsTests(SynchronousTestCase): def test_getDeprecationWarningString(self): """ L{getDeprecationWarningString} returns a string that tells us that a callable was deprecated at a certain released version of Twisted. """ version = Version("Twisted", 8, 0, 0) self.assertEqual( getDeprecationWarningString(self.test_getDeprecationWarningString, version), "%s.DeprecationWarningsTests.test_getDeprecationWarningString " "was deprecated in Twisted 8.0.0" % (__name__,), ) def test_getDeprecationWarningStringWithFormat(self): """ L{getDeprecationWarningString} returns a string that tells us that a callable was deprecated at a certain released version of Twisted, with a message containing additional information about the deprecation. """ version = Version("Twisted", 8, 0, 0) format = DEPRECATION_WARNING_FORMAT + ": This is a message" self.assertEqual( getDeprecationWarningString( self.test_getDeprecationWarningString, version, format ), "%s.DeprecationWarningsTests.test_getDeprecationWarningString was " "deprecated in Twisted 8.0.0: This is a message" % (__name__,), ) def test_deprecateEmitsWarning(self): """ Decorating a callable with L{deprecated} emits a warning. """ version = Version("Twisted", 8, 0, 0) dummy = deprecated(version)(dummyCallable) def addStackLevel(): dummy() with catch_warnings(record=True) as caught: simplefilter("always") addStackLevel() self.assertEqual(caught[0].category, DeprecationWarning) self.assertEqual( str(caught[0].message), getDeprecationWarningString(dummyCallable, version), ) # rstrip in case .pyc/.pyo self.assertEqual(caught[0].filename.rstrip("co"), __file__.rstrip("co")) def test_deprecatedPreservesName(self): """ The decorated function has the same name as the original. """ version = Version("Twisted", 8, 0, 0) dummy = deprecated(version)(dummyCallable) self.assertEqual(dummyCallable.__name__, dummy.__name__) self.assertEqual(fullyQualifiedName(dummyCallable), fullyQualifiedName(dummy)) def test_getDeprecationDocstring(self): """ L{_getDeprecationDocstring} returns a note about the deprecation to go into a docstring. """ version = Version("Twisted", 8, 0, 0) self.assertEqual( "Deprecated in Twisted 8.0.0.", _getDeprecationDocstring(version, "") ) def test_deprecatedUpdatesDocstring(self): """ The docstring of the deprecated function is appended with information about the deprecation. """ def localDummyCallable(): """ Do nothing. This is used to test the deprecation decorators. """ version = Version("Twisted", 8, 0, 0) dummy = deprecated(version)(localDummyCallable) _appendToDocstring(localDummyCallable, _getDeprecationDocstring(version, "")) self.assertEqual(localDummyCallable.__doc__, dummy.__doc__) def test_versionMetadata(self): """ Deprecating a function adds version information to the decorated version of that function. """ version = Version("Twisted", 8, 0, 0) dummy = deprecated(version)(dummyCallable) self.assertEqual(version, dummy.deprecatedVersion) def test_getDeprecationWarningStringReplacement(self): """ L{getDeprecationWarningString} takes an additional replacement parameter that can be used to add information to the deprecation. If the replacement parameter is a string, it will be interpolated directly into the result. """ version = Version("Twisted", 8, 0, 0) warningString = getDeprecationWarningString( self.test_getDeprecationWarningString, version, replacement="something.foobar", ) self.assertEqual( warningString, "%s was deprecated in Twisted 8.0.0; please use something.foobar " "instead" % (fullyQualifiedName(self.test_getDeprecationWarningString),), ) def test_getDeprecationWarningStringReplacementWithCallable(self): """ L{getDeprecationWarningString} takes an additional replacement parameter that can be used to add information to the deprecation. If the replacement parameter is a callable, its fully qualified name will be interpolated into the result. """ version = Version("Twisted", 8, 0, 0) warningString = getDeprecationWarningString( self.test_getDeprecationWarningString, version, replacement=dummyReplacementMethod, ) self.assertEqual( warningString, "%s was deprecated in Twisted 8.0.0; please use " "%s.dummyReplacementMethod instead" % (fullyQualifiedName(self.test_getDeprecationWarningString), __name__), ) @deprecated(Version("Twisted", 1, 2, 3)) class DeprecatedClass: """ Class which is entirely deprecated without having a replacement. """ class ClassWithDeprecatedProperty: """ 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 # type: ignore[no-redef] def someProperty(self, value): """ Setter docstring. """ self._someProtectedValue = value @deprecatedKeywordParameter(Version("Twisted", 19, 2, 0), "foo") def functionWithDeprecatedParameter(a, b, c=1, foo=2, bar=3): """ Function with a deprecated keyword parameter. """ 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 to indicate the new, non-deprecated method developers should use. If the replacement parameter is a string, it will be interpolated directly into the warning message. """ version = Version("Twisted", 8, 0, 0) dummy = deprecated(version, "something.foobar")(dummyCallable) self.assertEqual( dummy.__doc__, "\n" " Do nothing.\n\n" " This is used to test the deprecation decorators.\n\n" " Deprecated in Twisted 8.0.0; please use " "something.foobar" " instead.\n" " ", ) def test_deprecatedReplacementWithCallable(self): """ L{deprecated} takes an additional replacement parameter that can be used to indicate the new, non-deprecated method developers should use. If the replacement parameter is a callable, its fully qualified name will be interpolated into the warning message. """ version = Version("Twisted", 8, 0, 0) decorator = deprecated(version, replacement=dummyReplacementMethod) dummy = decorator(dummyCallable) self.assertEqual( dummy.__doc__, "\n" " Do nothing.\n\n" " This is used to test the deprecation decorators.\n\n" " Deprecated in Twisted 8.0.0; please use " "%s.dummyReplacementMethod instead.\n" " " % (__name__,), ) def test_deprecatedKeywordParameter(self): message = ( "The 'foo' parameter to " "twisted.python.test.test_deprecate." "functionWithDeprecatedParameter " "was deprecated in Twisted 19.2.0" ) with catch_warnings(record=True) as ws: simplefilter("always") functionWithDeprecatedParameter(10, 20) self.assertEqual(ws, []) functionWithDeprecatedParameter(10, 20, 30) self.assertEqual(ws, []) functionWithDeprecatedParameter(10, 20, foo=40) self.assertEqual(len(ws), 1) self.assertEqual(ws[0].category, DeprecationWarning) self.assertEqual(str(ws[0].message), message) ws.clear() functionWithDeprecatedParameter(10, 20, bar=50) self.assertEqual(ws, []) functionWithDeprecatedParameter(10, 20, 30, 40) self.assertEqual(len(ws), 1) self.assertEqual(ws[0].category, DeprecationWarning) self.assertEqual(str(ws[0].message), message) class AppendToDocstringTests(SynchronousTestCase): """ Test the _appendToDocstring function. _appendToDocstring is used to add text to a docstring. """ def test_appendToEmptyDocstring(self): """ Appending to an empty docstring simply replaces the docstring. """ def noDocstring(): pass _appendToDocstring(noDocstring, "Appended text.") self.assertEqual("Appended text.", noDocstring.__doc__) def test_appendToSingleLineDocstring(self): """ Appending to a single line docstring places the message on a new line, with a blank line separating it from the rest of the docstring. The docstring ends with a newline, conforming to Twisted and PEP 8 standards. Unfortunately, the indentation is incorrect, since the existing docstring doesn't have enough info to help us indent properly. """ def singleLineDocstring(): """This doesn't comply with standards, but is here for a test.""" _appendToDocstring(singleLineDocstring, "Appended text.") self.assertEqual( [ "This doesn't comply with standards, but is here for a test.", "", "Appended text.", ], singleLineDocstring.__doc__.splitlines(), ) self.assertTrue(singleLineDocstring.__doc__.endswith("\n")) def test_appendToMultilineDocstring(self): """ Appending to a multi-line docstring places the messade on a new line, with a blank line separating it from the rest of the docstring. Because we have multiple lines, we have enough information to do indentation. """ def multiLineDocstring(): """ This is a multi-line docstring. """ def expectedDocstring(): """ This is a multi-line docstring. Appended text. """ _appendToDocstring(multiLineDocstring, "Appended text.") self.assertEqual(expectedDocstring.__doc__, multiLineDocstring.__doc__) class MutualArgumentExclusionTests(SynchronousTestCase): """ Tests for L{mutuallyExclusiveArguments}. """ def checkPassed(self, func, *args, **kw): """ Test an invocation of L{passed} with the given function, arguments, and keyword arguments. @param func: A function whose argspec will be inspected. @type func: A callable. @param args: The arguments which could be passed to C{func}. @param kw: The keyword arguments which could be passed to C{func}. @return: L{_passedSignature} or L{_passedArgSpec}'s return value @rtype: L{dict} """ if getattr(inspect, "signature", None): # Python 3 return _passedSignature(inspect.signature(func), args, kw) else: # Python 2 return _passedArgSpec(inspect.getargspec(func), args, kw) def test_passed_simplePositional(self): """ L{passed} identifies the arguments passed by a simple positional test. """ def func(a, b): pass self.assertEqual(self.checkPassed(func, 1, 2), dict(a=1, b=2)) def test_passed_tooManyArgs(self): """ L{passed} raises a L{TypeError} if too many arguments are passed. """ def func(a, b): pass self.assertRaises(TypeError, self.checkPassed, func, 1, 2, 3) def test_passed_doublePassKeyword(self): """ L{passed} raises a L{TypeError} if a argument is passed both positionally and by keyword. """ def func(a): pass self.assertRaises(TypeError, self.checkPassed, func, 1, a=2) def test_passed_unspecifiedKeyword(self): """ L{passed} raises a L{TypeError} if a keyword argument not present in the function's declaration is passed. """ def func(a): pass self.assertRaises(TypeError, self.checkPassed, func, 1, z=2) def test_passed_star(self): """ L{passed} places additional positional arguments into a tuple under the name of the star argument. """ def func(a, *b): pass self.assertEqual(self.checkPassed(func, 1, 2, 3), dict(a=1, b=(2, 3))) def test_passed_starStar(self): """ Additional keyword arguments are passed as a dict to the star star keyword argument. """ def func(a, **b): pass self.assertEqual( self.checkPassed(func, 1, x=2, y=3, z=4), dict(a=1, b=dict(x=2, y=3, z=4)) ) def test_passed_noDefaultValues(self): """ The results of L{passed} only include arguments explicitly passed, not default values. """ def func(a, b, c=1, d=2, e=3): pass self.assertEqual(self.checkPassed(func, 1, 2, e=7), dict(a=1, b=2, e=7)) def test_mutualExclusionPrimeDirective(self): """ L{mutuallyExclusiveArguments} does not interfere in its decoratee's operation, either its receipt of arguments or its return value. """ @_mutuallyExclusiveArguments([("a", "b")]) def func(x, y, a=3, b=4): return x + y + a + b self.assertEqual(func(1, 2), 10) self.assertEqual(func(1, 2, 7), 14) self.assertEqual(func(1, 2, b=7), 13) def test_mutualExclusionExcludesByKeyword(self): """ L{mutuallyExclusiveArguments} raises a L{TypeError}n if its decoratee is passed a pair of mutually exclusive arguments. """ @_mutuallyExclusiveArguments([["a", "b"]]) def func(a=3, b=4): return a + b self.assertRaises(TypeError, func, a=3, b=4) def test_invalidParameterType(self): """ Create a fake signature with an invalid parameter type to test error handling. The valid parameter types are specified in L{inspect.Parameter}. """ class FakeSignature: def __init__(self, parameters): self.parameters = parameters class FakeParameter: def __init__(self, name, kind): self.name = name self.kind = kind def func(a, b): pass func(1, 2) parameters = inspect.signature(func).parameters dummyParameters = parameters.copy() dummyParameters["c"] = FakeParameter("fake", "fake") fakeSig = FakeSignature(dummyParameters) self.assertRaises(TypeError, _passedSignature, fakeSig, (1, 2), {}) class KeywordOnlyTests(SynchronousTestCase): """ Keyword only arguments (PEP 3102). """ def checkPassed(self, func, *args, **kw): """ Test an invocation of L{passed} with the given function, arguments, and keyword arguments. @param func: A function whose argspec to pass to L{_passed}. @type func: A callable. @param args: The arguments which could be passed to L{func}. @param kw: The keyword arguments which could be passed to L{func}. @return: L{_passed}'s return value @rtype: L{dict} """ return _passedSignature(inspect.signature(func), args, kw) def test_passedKeywordOnly(self): """ Keyword only arguments follow varargs. They are specified in PEP 3102. """ def func1(*a, b=True): """ b is a keyword-only argument, with a default value. """ def func2(*a, b=True, c, d, e): """ b, c, d, e are keyword-only arguments. b has a default value. """ self.assertEqual(self.checkPassed(func1, 1, 2, 3), dict(a=(1, 2, 3), b=True)) self.assertEqual( self.checkPassed(func1, 1, 2, 3, b=False), dict(a=(1, 2, 3), b=False) ) self.assertEqual( self.checkPassed(func2, 1, 2, b=False, c=1, d=2, e=3), dict(a=(1, 2), b=False, c=1, d=2, e=3), ) self.assertRaises(TypeError, self.checkPassed, func2, 1, 2, b=False, c=1, d=2)