test_deprecate.py 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Tests for Twisted's deprecation framework, L{twisted.python.deprecate}.
  5. """
  6. from __future__ import division, absolute_import
  7. import sys, types, warnings, inspect
  8. from os.path import normcase
  9. from warnings import simplefilter, catch_warnings
  10. try:
  11. from importlib import invalidate_caches
  12. except ImportError:
  13. invalidate_caches = None
  14. from twisted.python import deprecate
  15. from twisted.python.deprecate import _getDeprecationWarningString
  16. from twisted.python.deprecate import DEPRECATION_WARNING_FORMAT
  17. from twisted.python.deprecate import (
  18. getDeprecationWarningString,
  19. deprecated, _appendToDocstring, _getDeprecationDocstring,
  20. _fullyQualifiedName as fullyQualifiedName,
  21. _mutuallyExclusiveArguments,
  22. deprecatedProperty,
  23. _passedArgSpec, _passedSignature
  24. )
  25. from twisted.python.compat import _PY3, execfile
  26. from incremental import Version
  27. from twisted.python.runtime import platform
  28. from twisted.python.filepath import FilePath
  29. from twisted.python.test import deprecatedattributes
  30. from twisted.python.test.modules_helpers import TwistedModulesMixin
  31. from twisted.trial.unittest import SynchronousTestCase
  32. # Note that various tests in this module require manual encoding of paths to
  33. # utf-8. This can be fixed once FilePath supports Unicode; see #2366, #4736,
  34. # #5203.
  35. class _MockDeprecatedAttribute(object):
  36. """
  37. Mock of L{twisted.python.deprecate._DeprecatedAttribute}.
  38. @ivar value: The value of the attribute.
  39. """
  40. def __init__(self, value):
  41. self.value = value
  42. def get(self):
  43. """
  44. Get a known value.
  45. """
  46. return self.value
  47. class ModuleProxyTests(SynchronousTestCase):
  48. """
  49. Tests for L{twisted.python.deprecate._ModuleProxy}, which proxies
  50. access to module-level attributes, intercepting access to deprecated
  51. attributes and passing through access to normal attributes.
  52. """
  53. def _makeProxy(self, **attrs):
  54. """
  55. Create a temporary module proxy object.
  56. @param **kw: Attributes to initialise on the temporary module object
  57. @rtype: L{twistd.python.deprecate._ModuleProxy}
  58. """
  59. mod = types.ModuleType('foo')
  60. for key, value in attrs.items():
  61. setattr(mod, key, value)
  62. return deprecate._ModuleProxy(mod)
  63. def test_getattrPassthrough(self):
  64. """
  65. Getting a normal attribute on a L{twisted.python.deprecate._ModuleProxy}
  66. retrieves the underlying attribute's value, and raises C{AttributeError}
  67. if a non-existent attribute is accessed.
  68. """
  69. proxy = self._makeProxy(SOME_ATTRIBUTE='hello')
  70. self.assertIs(proxy.SOME_ATTRIBUTE, 'hello')
  71. self.assertRaises(AttributeError, getattr, proxy, 'DOES_NOT_EXIST')
  72. def test_getattrIntercept(self):
  73. """
  74. Getting an attribute marked as being deprecated on
  75. L{twisted.python.deprecate._ModuleProxy} results in calling the
  76. deprecated wrapper's C{get} method.
  77. """
  78. proxy = self._makeProxy()
  79. _deprecatedAttributes = object.__getattribute__(
  80. proxy, '_deprecatedAttributes')
  81. _deprecatedAttributes['foo'] = _MockDeprecatedAttribute(42)
  82. self.assertEqual(proxy.foo, 42)
  83. def test_privateAttributes(self):
  84. """
  85. Private attributes of L{twisted.python.deprecate._ModuleProxy} are
  86. inaccessible when regular attribute access is used.
  87. """
  88. proxy = self._makeProxy()
  89. self.assertRaises(AttributeError, getattr, proxy, '_module')
  90. self.assertRaises(
  91. AttributeError, getattr, proxy, '_deprecatedAttributes')
  92. def test_setattr(self):
  93. """
  94. Setting attributes on L{twisted.python.deprecate._ModuleProxy} proxies
  95. them through to the wrapped module.
  96. """
  97. proxy = self._makeProxy()
  98. proxy._module = 1
  99. self.assertNotEqual(object.__getattribute__(proxy, '_module'), 1)
  100. self.assertEqual(proxy._module, 1)
  101. def test_repr(self):
  102. """
  103. L{twisted.python.deprecated._ModuleProxy.__repr__} produces a string
  104. containing the proxy type and a representation of the wrapped module
  105. object.
  106. """
  107. proxy = self._makeProxy()
  108. realModule = object.__getattribute__(proxy, '_module')
  109. self.assertEqual(
  110. repr(proxy), '<%s module=%r>' % (type(proxy).__name__, realModule))
  111. class DeprecatedAttributeTests(SynchronousTestCase):
  112. """
  113. Tests for L{twisted.python.deprecate._DeprecatedAttribute} and
  114. L{twisted.python.deprecate.deprecatedModuleAttribute}, which issue
  115. warnings for deprecated module-level attributes.
  116. """
  117. def setUp(self):
  118. self.version = deprecatedattributes.version
  119. self.message = deprecatedattributes.message
  120. self._testModuleName = __name__ + '.foo'
  121. def _getWarningString(self, attr):
  122. """
  123. Create the warning string used by deprecated attributes.
  124. """
  125. return _getDeprecationWarningString(
  126. deprecatedattributes.__name__ + '.' + attr,
  127. deprecatedattributes.version,
  128. DEPRECATION_WARNING_FORMAT + ': ' + deprecatedattributes.message)
  129. def test_deprecatedAttributeHelper(self):
  130. """
  131. L{twisted.python.deprecate._DeprecatedAttribute} correctly sets its
  132. __name__ to match that of the deprecated attribute and emits a warning
  133. when the original attribute value is accessed.
  134. """
  135. name = 'ANOTHER_DEPRECATED_ATTRIBUTE'
  136. setattr(deprecatedattributes, name, 42)
  137. attr = deprecate._DeprecatedAttribute(
  138. deprecatedattributes, name, self.version, self.message)
  139. self.assertEqual(attr.__name__, name)
  140. # Since we're accessing the value getter directly, as opposed to via
  141. # the module proxy, we need to match the warning's stack level.
  142. def addStackLevel():
  143. attr.get()
  144. # Access the deprecated attribute.
  145. addStackLevel()
  146. warningsShown = self.flushWarnings([
  147. self.test_deprecatedAttributeHelper])
  148. self.assertIs(warningsShown[0]['category'], DeprecationWarning)
  149. self.assertEqual(
  150. warningsShown[0]['message'],
  151. self._getWarningString(name))
  152. self.assertEqual(len(warningsShown), 1)
  153. def test_deprecatedAttribute(self):
  154. """
  155. L{twisted.python.deprecate.deprecatedModuleAttribute} wraps a
  156. module-level attribute in an object that emits a deprecation warning
  157. when it is accessed the first time only, while leaving other unrelated
  158. attributes alone.
  159. """
  160. # Accessing non-deprecated attributes does not issue a warning.
  161. deprecatedattributes.ANOTHER_ATTRIBUTE
  162. warningsShown = self.flushWarnings([self.test_deprecatedAttribute])
  163. self.assertEqual(len(warningsShown), 0)
  164. name = 'DEPRECATED_ATTRIBUTE'
  165. # Access the deprecated attribute. This uses getattr to avoid repeating
  166. # the attribute name.
  167. getattr(deprecatedattributes, name)
  168. warningsShown = self.flushWarnings([self.test_deprecatedAttribute])
  169. self.assertEqual(len(warningsShown), 1)
  170. self.assertIs(warningsShown[0]['category'], DeprecationWarning)
  171. self.assertEqual(
  172. warningsShown[0]['message'],
  173. self._getWarningString(name))
  174. def test_wrappedModule(self):
  175. """
  176. Deprecating an attribute in a module replaces and wraps that module
  177. instance, in C{sys.modules}, with a
  178. L{twisted.python.deprecate._ModuleProxy} instance but only if it hasn't
  179. already been wrapped.
  180. """
  181. sys.modules[self._testModuleName] = mod = types.ModuleType('foo')
  182. self.addCleanup(sys.modules.pop, self._testModuleName)
  183. setattr(mod, 'first', 1)
  184. setattr(mod, 'second', 2)
  185. deprecate.deprecatedModuleAttribute(
  186. Version('Twisted', 8, 0, 0),
  187. 'message',
  188. self._testModuleName,
  189. 'first')
  190. proxy = sys.modules[self._testModuleName]
  191. self.assertNotEqual(proxy, mod)
  192. deprecate.deprecatedModuleAttribute(
  193. Version('Twisted', 8, 0, 0),
  194. 'message',
  195. self._testModuleName,
  196. 'second')
  197. self.assertIs(proxy, sys.modules[self._testModuleName])
  198. class ImportedModuleAttributeTests(TwistedModulesMixin, SynchronousTestCase):
  199. """
  200. Tests for L{deprecatedModuleAttribute} which involve loading a module via
  201. 'import'.
  202. """
  203. _packageInit = """\
  204. from twisted.python.deprecate import deprecatedModuleAttribute
  205. from incremental import Version
  206. deprecatedModuleAttribute(
  207. Version('Package', 1, 2, 3), 'message', __name__, 'module')
  208. """
  209. def pathEntryTree(self, tree):
  210. """
  211. Create some files in a hierarchy, based on a dictionary describing those
  212. files. The resulting hierarchy will be placed onto sys.path for the
  213. duration of the test.
  214. @param tree: A dictionary representing a directory structure. Keys are
  215. strings, representing filenames, dictionary values represent
  216. directories, string values represent file contents.
  217. @return: another dictionary similar to the input, with file content
  218. strings replaced with L{FilePath} objects pointing at where those
  219. contents are now stored.
  220. """
  221. def makeSomeFiles(pathobj, dirdict):
  222. pathdict = {}
  223. for (key, value) in dirdict.items():
  224. child = pathobj.child(key)
  225. if isinstance(value, bytes):
  226. pathdict[key] = child
  227. child.setContent(value)
  228. elif isinstance(value, dict):
  229. child.createDirectory()
  230. pathdict[key] = makeSomeFiles(child, value)
  231. else:
  232. raise ValueError("only strings and dicts allowed as values")
  233. return pathdict
  234. base = FilePath(self.mktemp().encode("utf-8"))
  235. base.makedirs()
  236. result = makeSomeFiles(base, tree)
  237. # On Python 3, sys.path cannot include byte paths:
  238. self.replaceSysPath([base.path.decode("utf-8")] + sys.path)
  239. self.replaceSysModules(sys.modules.copy())
  240. return result
  241. def simpleModuleEntry(self):
  242. """
  243. Add a sample module and package to the path, returning a L{FilePath}
  244. pointing at the module which will be loadable as C{package.module}.
  245. """
  246. paths = self.pathEntryTree(
  247. {b"package": {b"__init__.py": self._packageInit.encode("utf-8"),
  248. b"module.py": b""}})
  249. return paths[b'package'][b'module.py']
  250. def checkOneWarning(self, modulePath):
  251. """
  252. Verification logic for L{test_deprecatedModule}.
  253. """
  254. from package import module
  255. self.assertEqual(FilePath(module.__file__.encode("utf-8")),
  256. modulePath)
  257. emitted = self.flushWarnings([self.checkOneWarning])
  258. self.assertEqual(len(emitted), 1)
  259. self.assertEqual(emitted[0]['message'],
  260. 'package.module was deprecated in Package 1.2.3: '
  261. 'message')
  262. self.assertEqual(emitted[0]['category'], DeprecationWarning)
  263. def test_deprecatedModule(self):
  264. """
  265. If L{deprecatedModuleAttribute} is used to deprecate a module attribute
  266. of a package, only one deprecation warning is emitted when the
  267. deprecated module is imported.
  268. """
  269. self.checkOneWarning(self.simpleModuleEntry())
  270. def test_deprecatedModuleMultipleTimes(self):
  271. """
  272. If L{deprecatedModuleAttribute} is used to deprecate a module attribute
  273. of a package, only one deprecation warning is emitted when the
  274. deprecated module is subsequently imported.
  275. """
  276. mp = self.simpleModuleEntry()
  277. # The first time, the code needs to be loaded.
  278. self.checkOneWarning(mp)
  279. # The second time, things are slightly different; the object's already
  280. # in the namespace.
  281. self.checkOneWarning(mp)
  282. # The third and fourth times, things things should all be exactly the
  283. # same, but this is a sanity check to make sure the implementation isn't
  284. # special casing the second time. Also, putting these cases into a loop
  285. # means that the stack will be identical, to make sure that the
  286. # implementation doesn't rely too much on stack-crawling.
  287. for x in range(2):
  288. self.checkOneWarning(mp)
  289. class WarnAboutFunctionTests(SynchronousTestCase):
  290. """
  291. Tests for L{twisted.python.deprecate.warnAboutFunction} which allows the
  292. callers of a function to issue a C{DeprecationWarning} about that function.
  293. """
  294. def setUp(self):
  295. """
  296. Create a file that will have known line numbers when emitting warnings.
  297. """
  298. self.package = FilePath(self.mktemp().encode("utf-8")
  299. ).child(b'twisted_private_helper')
  300. self.package.makedirs()
  301. self.package.child(b'__init__.py').setContent(b'')
  302. self.package.child(b'module.py').setContent(b'''
  303. "A module string"
  304. from twisted.python import deprecate
  305. def testFunction():
  306. "A doc string"
  307. a = 1 + 2
  308. return a
  309. def callTestFunction():
  310. b = testFunction()
  311. if b == 3:
  312. deprecate.warnAboutFunction(testFunction, "A Warning String")
  313. ''')
  314. # Python 3 doesn't accept bytes in sys.path:
  315. packagePath = self.package.parent().path.decode("utf-8")
  316. sys.path.insert(0, packagePath)
  317. self.addCleanup(sys.path.remove, packagePath)
  318. modules = sys.modules.copy()
  319. self.addCleanup(
  320. lambda: (sys.modules.clear(), sys.modules.update(modules)))
  321. # On Windows on Python 3, most FilePath interactions produce
  322. # DeprecationWarnings, so flush them here so that they don't interfere
  323. # with the tests.
  324. if platform.isWindows() and _PY3:
  325. self.flushWarnings()
  326. def test_warning(self):
  327. """
  328. L{deprecate.warnAboutFunction} emits a warning the file and line number
  329. of which point to the beginning of the implementation of the function
  330. passed to it.
  331. """
  332. def aFunc():
  333. pass
  334. deprecate.warnAboutFunction(aFunc, 'A Warning Message')
  335. warningsShown = self.flushWarnings()
  336. filename = __file__
  337. if filename.lower().endswith('.pyc'):
  338. filename = filename[:-1]
  339. self.assertSamePath(
  340. FilePath(warningsShown[0]["filename"]), FilePath(filename))
  341. self.assertEqual(warningsShown[0]["message"], "A Warning Message")
  342. def test_warningLineNumber(self):
  343. """
  344. L{deprecate.warnAboutFunction} emits a C{DeprecationWarning} with the
  345. number of a line within the implementation of the function passed to it.
  346. """
  347. from twisted_private_helper import module
  348. module.callTestFunction()
  349. warningsShown = self.flushWarnings()
  350. self.assertSamePath(
  351. FilePath(warningsShown[0]["filename"].encode("utf-8")),
  352. self.package.sibling(b'twisted_private_helper').child(b'module.py'))
  353. # Line number 9 is the last line in the testFunction in the helper
  354. # module.
  355. self.assertEqual(warningsShown[0]["lineno"], 9)
  356. self.assertEqual(warningsShown[0]["message"], "A Warning String")
  357. self.assertEqual(len(warningsShown), 1)
  358. def assertSamePath(self, first, second):
  359. """
  360. Assert that the two paths are the same, considering case normalization
  361. appropriate for the current platform.
  362. @type first: L{FilePath}
  363. @type second: L{FilePath}
  364. @raise C{self.failureType}: If the paths are not the same.
  365. """
  366. self.assertTrue(
  367. normcase(first.path) == normcase(second.path),
  368. "%r != %r" % (first, second))
  369. def test_renamedFile(self):
  370. """
  371. Even if the implementation of a deprecated function is moved around on
  372. the filesystem, the line number in the warning emitted by
  373. L{deprecate.warnAboutFunction} points to a line in the implementation of
  374. the deprecated function.
  375. """
  376. from twisted_private_helper import module
  377. # Clean up the state resulting from that import; we're not going to use
  378. # this module, so it should go away.
  379. del sys.modules['twisted_private_helper']
  380. del sys.modules[module.__name__]
  381. # Rename the source directory
  382. self.package.moveTo(self.package.sibling(b'twisted_renamed_helper'))
  383. # Make sure importlib notices we've changed importable packages:
  384. if invalidate_caches:
  385. invalidate_caches()
  386. # Import the newly renamed version
  387. from twisted_renamed_helper import module
  388. self.addCleanup(sys.modules.pop, 'twisted_renamed_helper')
  389. self.addCleanup(sys.modules.pop, module.__name__)
  390. module.callTestFunction()
  391. warningsShown = self.flushWarnings([module.testFunction])
  392. warnedPath = FilePath(warningsShown[0]["filename"].encode("utf-8"))
  393. expectedPath = self.package.sibling(
  394. b'twisted_renamed_helper').child(b'module.py')
  395. self.assertSamePath(warnedPath, expectedPath)
  396. self.assertEqual(warningsShown[0]["lineno"], 9)
  397. self.assertEqual(warningsShown[0]["message"], "A Warning String")
  398. self.assertEqual(len(warningsShown), 1)
  399. def test_filteredWarning(self):
  400. """
  401. L{deprecate.warnAboutFunction} emits a warning that will be filtered if
  402. L{warnings.filterwarning} is called with the module name of the
  403. deprecated function.
  404. """
  405. # Clean up anything *else* that might spuriously filter out the warning,
  406. # such as the "always" simplefilter set up by unittest._collectWarnings.
  407. # We'll also rely on trial to restore the original filters afterwards.
  408. del warnings.filters[:]
  409. warnings.filterwarnings(
  410. action="ignore", module="twisted_private_helper")
  411. from twisted_private_helper import module
  412. module.callTestFunction()
  413. warningsShown = self.flushWarnings()
  414. self.assertEqual(len(warningsShown), 0)
  415. def test_filteredOnceWarning(self):
  416. """
  417. L{deprecate.warnAboutFunction} emits a warning that will be filtered
  418. once if L{warnings.filterwarning} is called with the module name of the
  419. deprecated function and an action of once.
  420. """
  421. # Clean up anything *else* that might spuriously filter out the warning,
  422. # such as the "always" simplefilter set up by unittest._collectWarnings.
  423. # We'll also rely on trial to restore the original filters afterwards.
  424. del warnings.filters[:]
  425. warnings.filterwarnings(
  426. action="module", module="twisted_private_helper")
  427. from twisted_private_helper import module
  428. module.callTestFunction()
  429. module.callTestFunction()
  430. warningsShown = self.flushWarnings()
  431. self.assertEqual(len(warningsShown), 1)
  432. message = warningsShown[0]['message']
  433. category = warningsShown[0]['category']
  434. filename = warningsShown[0]['filename']
  435. lineno = warningsShown[0]['lineno']
  436. msg = warnings.formatwarning(message, category, filename, lineno)
  437. self.assertTrue(
  438. msg.endswith("module.py:9: DeprecationWarning: A Warning String\n"
  439. " return a\n"),
  440. "Unexpected warning string: %r" % (msg,))
  441. def dummyCallable():
  442. """
  443. Do nothing.
  444. This is used to test the deprecation decorators.
  445. """
  446. def dummyReplacementMethod():
  447. """
  448. Do nothing.
  449. This is used to test the replacement parameter to L{deprecated}.
  450. """
  451. class DeprecationWarningsTests(SynchronousTestCase):
  452. def test_getDeprecationWarningString(self):
  453. """
  454. L{getDeprecationWarningString} returns a string that tells us that a
  455. callable was deprecated at a certain released version of Twisted.
  456. """
  457. version = Version('Twisted', 8, 0, 0)
  458. self.assertEqual(
  459. getDeprecationWarningString(self.test_getDeprecationWarningString,
  460. version),
  461. "%s.DeprecationWarningsTests.test_getDeprecationWarningString "
  462. "was deprecated in Twisted 8.0.0" % (__name__,))
  463. def test_getDeprecationWarningStringWithFormat(self):
  464. """
  465. L{getDeprecationWarningString} returns a string that tells us that a
  466. callable was deprecated at a certain released version of Twisted, with
  467. a message containing additional information about the deprecation.
  468. """
  469. version = Version('Twisted', 8, 0, 0)
  470. format = DEPRECATION_WARNING_FORMAT + ': This is a message'
  471. self.assertEqual(
  472. getDeprecationWarningString(self.test_getDeprecationWarningString,
  473. version, format),
  474. '%s.DeprecationWarningsTests.test_getDeprecationWarningString was '
  475. 'deprecated in Twisted 8.0.0: This is a message' % (__name__,))
  476. def test_deprecateEmitsWarning(self):
  477. """
  478. Decorating a callable with L{deprecated} emits a warning.
  479. """
  480. version = Version('Twisted', 8, 0, 0)
  481. dummy = deprecated(version)(dummyCallable)
  482. def addStackLevel():
  483. dummy()
  484. with catch_warnings(record=True) as caught:
  485. simplefilter("always")
  486. addStackLevel()
  487. self.assertEqual(caught[0].category, DeprecationWarning)
  488. self.assertEqual(str(caught[0].message), getDeprecationWarningString(dummyCallable, version))
  489. # rstrip in case .pyc/.pyo
  490. self.assertEqual(caught[0].filename.rstrip('co'), __file__.rstrip('co'))
  491. def test_deprecatedPreservesName(self):
  492. """
  493. The decorated function has the same name as the original.
  494. """
  495. version = Version('Twisted', 8, 0, 0)
  496. dummy = deprecated(version)(dummyCallable)
  497. self.assertEqual(dummyCallable.__name__, dummy.__name__)
  498. self.assertEqual(fullyQualifiedName(dummyCallable),
  499. fullyQualifiedName(dummy))
  500. def test_getDeprecationDocstring(self):
  501. """
  502. L{_getDeprecationDocstring} returns a note about the deprecation to go
  503. into a docstring.
  504. """
  505. version = Version('Twisted', 8, 0, 0)
  506. self.assertEqual(
  507. "Deprecated in Twisted 8.0.0.",
  508. _getDeprecationDocstring(version, ''))
  509. def test_deprecatedUpdatesDocstring(self):
  510. """
  511. The docstring of the deprecated function is appended with information
  512. about the deprecation.
  513. """
  514. def localDummyCallable():
  515. """
  516. Do nothing.
  517. This is used to test the deprecation decorators.
  518. """
  519. version = Version('Twisted', 8, 0, 0)
  520. dummy = deprecated(version)(localDummyCallable)
  521. _appendToDocstring(
  522. localDummyCallable,
  523. _getDeprecationDocstring(version, ''))
  524. self.assertEqual(localDummyCallable.__doc__, dummy.__doc__)
  525. def test_versionMetadata(self):
  526. """
  527. Deprecating a function adds version information to the decorated
  528. version of that function.
  529. """
  530. version = Version('Twisted', 8, 0, 0)
  531. dummy = deprecated(version)(dummyCallable)
  532. self.assertEqual(version, dummy.deprecatedVersion)
  533. def test_getDeprecationWarningStringReplacement(self):
  534. """
  535. L{getDeprecationWarningString} takes an additional replacement parameter
  536. that can be used to add information to the deprecation. If the
  537. replacement parameter is a string, it will be interpolated directly into
  538. the result.
  539. """
  540. version = Version('Twisted', 8, 0, 0)
  541. warningString = getDeprecationWarningString(
  542. self.test_getDeprecationWarningString, version,
  543. replacement="something.foobar")
  544. self.assertEqual(
  545. warningString,
  546. "%s was deprecated in Twisted 8.0.0; please use something.foobar "
  547. "instead" % (
  548. fullyQualifiedName(self.test_getDeprecationWarningString),))
  549. def test_getDeprecationWarningStringReplacementWithCallable(self):
  550. """
  551. L{getDeprecationWarningString} takes an additional replacement parameter
  552. that can be used to add information to the deprecation. If the
  553. replacement parameter is a callable, its fully qualified name will be
  554. interpolated into the result.
  555. """
  556. version = Version('Twisted', 8, 0, 0)
  557. warningString = getDeprecationWarningString(
  558. self.test_getDeprecationWarningString, version,
  559. replacement=dummyReplacementMethod)
  560. self.assertEqual(
  561. warningString,
  562. "%s was deprecated in Twisted 8.0.0; please use "
  563. "%s.dummyReplacementMethod instead" % (
  564. fullyQualifiedName(self.test_getDeprecationWarningString),
  565. __name__))
  566. @deprecated(Version('Twisted', 1, 2, 3))
  567. class DeprecatedClass(object):
  568. """
  569. Class which is entirely deprecated without having a replacement.
  570. """
  571. class ClassWithDeprecatedProperty(object):
  572. """
  573. Class with a single deprecated property.
  574. """
  575. _someProtectedValue = None
  576. @deprecatedProperty(Version('Twisted', 1, 2, 3))
  577. def someProperty(self):
  578. """
  579. Getter docstring.
  580. @return: The property.
  581. """
  582. return self._someProtectedValue
  583. @someProperty.setter
  584. def someProperty(self, value):
  585. """
  586. Setter docstring.
  587. """
  588. self._someProtectedValue = value
  589. class DeprecatedDecoratorTests(SynchronousTestCase):
  590. """
  591. Tests for deprecated decorators.
  592. """
  593. def assertDocstring(self, target, expected):
  594. """
  595. Check that C{target} object has the C{expected} docstring lines.
  596. @param target: Object which is checked.
  597. @type target: C{anything}
  598. @param expected: List of lines, ignoring empty lines or leading or
  599. trailing spaces.
  600. @type expected: L{list} or L{str}
  601. """
  602. self.assertEqual(
  603. expected,
  604. [x.strip() for x in target.__doc__.splitlines() if x.strip()]
  605. )
  606. def test_propertyGetter(self):
  607. """
  608. When L{deprecatedProperty} is used on a C{property}, accesses raise a
  609. L{DeprecationWarning} and getter docstring is updated to inform the
  610. version in which it was deprecated. C{deprecatedVersion} attribute is
  611. also set to inform the deprecation version.
  612. """
  613. obj = ClassWithDeprecatedProperty()
  614. obj.someProperty
  615. self.assertDocstring(
  616. ClassWithDeprecatedProperty.someProperty,
  617. [
  618. 'Getter docstring.',
  619. '@return: The property.',
  620. 'Deprecated in Twisted 1.2.3.',
  621. ],
  622. )
  623. ClassWithDeprecatedProperty.someProperty.deprecatedVersion = Version(
  624. 'Twisted', 1, 2, 3)
  625. message = (
  626. 'twisted.python.test.test_deprecate.ClassWithDeprecatedProperty.'
  627. 'someProperty was deprecated in Twisted 1.2.3'
  628. )
  629. warnings = self.flushWarnings([self.test_propertyGetter])
  630. self.assertEqual(1, len(warnings))
  631. self.assertEqual(DeprecationWarning, warnings[0]['category'])
  632. self.assertEqual(message, warnings[0]['message'])
  633. def test_propertySetter(self):
  634. """
  635. When L{deprecatedProperty} is used on a C{property}, setter accesses
  636. raise a L{DeprecationWarning}.
  637. """
  638. newValue = object()
  639. obj = ClassWithDeprecatedProperty()
  640. obj.someProperty = newValue
  641. self.assertIs(newValue, obj._someProtectedValue)
  642. message = (
  643. 'twisted.python.test.test_deprecate.ClassWithDeprecatedProperty.'
  644. 'someProperty was deprecated in Twisted 1.2.3'
  645. )
  646. warnings = self.flushWarnings([self.test_propertySetter])
  647. self.assertEqual(1, len(warnings))
  648. self.assertEqual(DeprecationWarning, warnings[0]['category'])
  649. self.assertEqual(message, warnings[0]['message'])
  650. def test_class(self):
  651. """
  652. When L{deprecated} is used on a class, instantiations raise a
  653. L{DeprecationWarning} and class's docstring is updated to inform the
  654. version in which it was deprecated. C{deprecatedVersion} attribute is
  655. also set to inform the deprecation version.
  656. """
  657. DeprecatedClass()
  658. self.assertDocstring(
  659. DeprecatedClass,
  660. [('Class which is entirely deprecated without having a '
  661. 'replacement.'),
  662. 'Deprecated in Twisted 1.2.3.'],
  663. )
  664. DeprecatedClass.deprecatedVersion = Version('Twisted', 1, 2, 3)
  665. message = (
  666. 'twisted.python.test.test_deprecate.DeprecatedClass '
  667. 'was deprecated in Twisted 1.2.3'
  668. )
  669. warnings = self.flushWarnings([self.test_class])
  670. self.assertEqual(1, len(warnings))
  671. self.assertEqual(DeprecationWarning, warnings[0]['category'])
  672. self.assertEqual(message, warnings[0]['message'])
  673. def test_deprecatedReplacement(self):
  674. """
  675. L{deprecated} takes an additional replacement parameter that can be used
  676. to indicate the new, non-deprecated method developers should use. If
  677. the replacement parameter is a string, it will be interpolated directly
  678. into the warning message.
  679. """
  680. version = Version('Twisted', 8, 0, 0)
  681. dummy = deprecated(version, "something.foobar")(dummyCallable)
  682. self.assertEqual(dummy.__doc__,
  683. "\n"
  684. " Do nothing.\n\n"
  685. " This is used to test the deprecation decorators.\n\n"
  686. " Deprecated in Twisted 8.0.0; please use "
  687. "something.foobar"
  688. " instead.\n"
  689. " ")
  690. def test_deprecatedReplacementWithCallable(self):
  691. """
  692. L{deprecated} takes an additional replacement parameter that can be used
  693. to indicate the new, non-deprecated method developers should use. If
  694. the replacement parameter is a callable, its fully qualified name will
  695. be interpolated into the warning message.
  696. """
  697. version = Version('Twisted', 8, 0, 0)
  698. decorator = deprecated(version, replacement=dummyReplacementMethod)
  699. dummy = decorator(dummyCallable)
  700. self.assertEqual(dummy.__doc__,
  701. "\n"
  702. " Do nothing.\n\n"
  703. " This is used to test the deprecation decorators.\n\n"
  704. " Deprecated in Twisted 8.0.0; please use "
  705. "%s.dummyReplacementMethod instead.\n"
  706. " " % (__name__,))
  707. class AppendToDocstringTests(SynchronousTestCase):
  708. """
  709. Test the _appendToDocstring function.
  710. _appendToDocstring is used to add text to a docstring.
  711. """
  712. def test_appendToEmptyDocstring(self):
  713. """
  714. Appending to an empty docstring simply replaces the docstring.
  715. """
  716. def noDocstring():
  717. pass
  718. _appendToDocstring(noDocstring, "Appended text.")
  719. self.assertEqual("Appended text.", noDocstring.__doc__)
  720. def test_appendToSingleLineDocstring(self):
  721. """
  722. Appending to a single line docstring places the message on a new line,
  723. with a blank line separating it from the rest of the docstring.
  724. The docstring ends with a newline, conforming to Twisted and PEP 8
  725. standards. Unfortunately, the indentation is incorrect, since the
  726. existing docstring doesn't have enough info to help us indent
  727. properly.
  728. """
  729. def singleLineDocstring():
  730. """This doesn't comply with standards, but is here for a test."""
  731. _appendToDocstring(singleLineDocstring, "Appended text.")
  732. self.assertEqual(
  733. ["This doesn't comply with standards, but is here for a test.",
  734. "",
  735. "Appended text."],
  736. singleLineDocstring.__doc__.splitlines())
  737. self.assertTrue(singleLineDocstring.__doc__.endswith('\n'))
  738. def test_appendToMultilineDocstring(self):
  739. """
  740. Appending to a multi-line docstring places the messade on a new line,
  741. with a blank line separating it from the rest of the docstring.
  742. Because we have multiple lines, we have enough information to do
  743. indentation.
  744. """
  745. def multiLineDocstring():
  746. """
  747. This is a multi-line docstring.
  748. """
  749. def expectedDocstring():
  750. """
  751. This is a multi-line docstring.
  752. Appended text.
  753. """
  754. _appendToDocstring(multiLineDocstring, "Appended text.")
  755. self.assertEqual(
  756. expectedDocstring.__doc__, multiLineDocstring.__doc__)
  757. class MutualArgumentExclusionTests(SynchronousTestCase):
  758. """
  759. Tests for L{mutuallyExclusiveArguments}.
  760. """
  761. def checkPassed(self, func, *args, **kw):
  762. """
  763. Test an invocation of L{passed} with the given function, arguments, and
  764. keyword arguments.
  765. @param func: A function whose argspec will be inspected.
  766. @type func: A callable.
  767. @param args: The arguments which could be passed to C{func}.
  768. @param kw: The keyword arguments which could be passed to C{func}.
  769. @return: L{_passedSignature} or L{_passedArgSpec}'s return value
  770. @rtype: L{dict}
  771. """
  772. if getattr(inspect, "signature", None):
  773. # Python 3
  774. return _passedSignature(inspect.signature(func), args, kw)
  775. else:
  776. # Python 2
  777. return _passedArgSpec(inspect.getargspec(func), args, kw)
  778. def test_passed_simplePositional(self):
  779. """
  780. L{passed} identifies the arguments passed by a simple
  781. positional test.
  782. """
  783. def func(a, b):
  784. pass
  785. self.assertEqual(self.checkPassed(func, 1, 2), dict(a=1, b=2))
  786. def test_passed_tooManyArgs(self):
  787. """
  788. L{passed} raises a L{TypeError} if too many arguments are
  789. passed.
  790. """
  791. def func(a, b):
  792. pass
  793. self.assertRaises(TypeError, self.checkPassed, func, 1, 2, 3)
  794. def test_passed_doublePassKeyword(self):
  795. """
  796. L{passed} raises a L{TypeError} if a argument is passed both
  797. positionally and by keyword.
  798. """
  799. def func(a):
  800. pass
  801. self.assertRaises(TypeError, self.checkPassed, func, 1, a=2)
  802. def test_passed_unspecifiedKeyword(self):
  803. """
  804. L{passed} raises a L{TypeError} if a keyword argument not
  805. present in the function's declaration is passed.
  806. """
  807. def func(a):
  808. pass
  809. self.assertRaises(TypeError, self.checkPassed, func, 1, z=2)
  810. def test_passed_star(self):
  811. """
  812. L{passed} places additional positional arguments into a tuple
  813. under the name of the star argument.
  814. """
  815. def func(a, *b):
  816. pass
  817. self.assertEqual(self.checkPassed(func, 1, 2, 3),
  818. dict(a=1, b=(2, 3)))
  819. def test_passed_starStar(self):
  820. """
  821. Additional keyword arguments are passed as a dict to the star star
  822. keyword argument.
  823. """
  824. def func(a, **b):
  825. pass
  826. self.assertEqual(self.checkPassed(func, 1, x=2, y=3, z=4),
  827. dict(a=1, b=dict(x=2, y=3, z=4)))
  828. def test_passed_noDefaultValues(self):
  829. """
  830. The results of L{passed} only include arguments explicitly
  831. passed, not default values.
  832. """
  833. def func(a, b, c=1, d=2, e=3):
  834. pass
  835. self.assertEqual(self.checkPassed(func, 1, 2, e=7),
  836. dict(a=1, b=2, e=7))
  837. def test_mutualExclusionPrimeDirective(self):
  838. """
  839. L{mutuallyExclusiveArguments} does not interfere in its
  840. decoratee's operation, either its receipt of arguments or its return
  841. value.
  842. """
  843. @_mutuallyExclusiveArguments([('a', 'b')])
  844. def func(x, y, a=3, b=4):
  845. return x + y + a + b
  846. self.assertEqual(func(1, 2), 10)
  847. self.assertEqual(func(1, 2, 7), 14)
  848. self.assertEqual(func(1, 2, b=7), 13)
  849. def test_mutualExclusionExcludesByKeyword(self):
  850. """
  851. L{mutuallyExclusiveArguments} raises a L{TypeError}n if its
  852. decoratee is passed a pair of mutually exclusive arguments.
  853. """
  854. @_mutuallyExclusiveArguments([['a', 'b']])
  855. def func(a=3, b=4):
  856. return a + b
  857. self.assertRaises(TypeError, func, a=3, b=4)
  858. def test_invalidParameterType(self):
  859. """
  860. Create a fake signature with an invalid parameter
  861. type to test error handling. The valid parameter
  862. types are specified in L{inspect.Parameter}.
  863. """
  864. class FakeSignature:
  865. def __init__(self, parameters):
  866. self.parameters = parameters
  867. class FakeParameter:
  868. def __init__(self, name, kind):
  869. self.name = name
  870. self.kind = kind
  871. def func(a, b):
  872. pass
  873. func(1, 2)
  874. parameters = inspect.signature(func).parameters
  875. dummyParameters = parameters.copy()
  876. dummyParameters['c'] = FakeParameter("fake", "fake")
  877. fakeSig = FakeSignature(dummyParameters)
  878. self.assertRaises(TypeError, _passedSignature, fakeSig, (1, 2), {})
  879. if not getattr(inspect, "signature", None):
  880. test_invalidParameterType.skip = "inspect.signature() not available"
  881. if sys.version_info >= (3,):
  882. _path = FilePath(__file__).parent().child("_deprecatetests.py.3only")
  883. _g = {}
  884. execfile(_path.path, _g)
  885. KeywordOnlyTests = _g["KeywordOnlyTests"]
  886. else:
  887. from twisted.trial.unittest import TestCase
  888. class KeywordOnlyTests(TestCase):
  889. """
  890. A dummy class to show that this test file was discovered but the tests
  891. are unable to be ran in this version of Python.
  892. """
  893. skip = (
  894. "keyword only arguments (PEP 3102) are "
  895. "only in Python 3 and higher")
  896. def test_notAvailable(self):
  897. """
  898. A skipped test to show that this was not ran because the Python is
  899. too old.
  900. """