You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_discover.py 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. import operator
  2. import os
  3. import shutil
  4. import sys
  5. import textwrap
  6. import tempfile
  7. from unittest import skipIf, TestCase
  8. import six
  9. def isTwistedInstalled():
  10. try:
  11. __import__('twisted')
  12. except ImportError:
  13. return False
  14. else:
  15. return True
  16. class _WritesPythonModules(TestCase):
  17. """
  18. A helper that enables generating Python module test fixtures.
  19. """
  20. def setUp(self):
  21. super(_WritesPythonModules, self).setUp()
  22. from twisted.python.modules import getModule, PythonPath
  23. from twisted.python.filepath import FilePath
  24. self.getModule = getModule
  25. self.PythonPath = PythonPath
  26. self.FilePath = FilePath
  27. self.originalSysModules = set(sys.modules.keys())
  28. self.savedSysPath = sys.path[:]
  29. self.pathDir = tempfile.mkdtemp()
  30. self.makeImportable(self.pathDir)
  31. def tearDown(self):
  32. super(_WritesPythonModules, self).tearDown()
  33. sys.path[:] = self.savedSysPath
  34. modulesToDelete = six.viewkeys(sys.modules) - self.originalSysModules
  35. for module in modulesToDelete:
  36. del sys.modules[module]
  37. shutil.rmtree(self.pathDir)
  38. def makeImportable(self, path):
  39. sys.path.append(path)
  40. def writeSourceInto(self, source, path, moduleName):
  41. directory = self.FilePath(path)
  42. module = directory.child(moduleName)
  43. # FilePath always opens a file in binary mode - but that will
  44. # break on Python 3
  45. with open(module.path, 'w') as f:
  46. f.write(textwrap.dedent(source))
  47. return self.PythonPath([directory.path])
  48. def makeModule(self, source, path, moduleName):
  49. pythonModuleName, _ = os.path.splitext(moduleName)
  50. return self.writeSourceInto(source, path, moduleName)[pythonModuleName]
  51. def attributesAsDict(self, hasIterAttributes):
  52. return {attr.name: attr for attr in hasIterAttributes.iterAttributes()}
  53. def loadModuleAsDict(self, module):
  54. module.load()
  55. return self.attributesAsDict(module)
  56. def makeModuleAsDict(self, source, path, name):
  57. return self.loadModuleAsDict(self.makeModule(source, path, name))
  58. @skipIf(not isTwistedInstalled(), "Twisted is not installed.")
  59. class OriginalLocationTests(_WritesPythonModules):
  60. """
  61. Tests that L{isOriginalLocation} detects when a
  62. L{PythonAttribute}'s FQPN refers to an object inside the module
  63. where it was defined.
  64. For example: A L{twisted.python.modules.PythonAttribute} with a
  65. name of 'foo.bar' that refers to a 'bar' object defined in module
  66. 'baz' does *not* refer to bar's original location, while a
  67. L{PythonAttribute} with a name of 'baz.bar' does.
  68. """
  69. def setUp(self):
  70. super(OriginalLocationTests, self).setUp()
  71. from .._discover import isOriginalLocation
  72. self.isOriginalLocation = isOriginalLocation
  73. def test_failsWithNoModule(self):
  74. """
  75. L{isOriginalLocation} returns False when the attribute refers to an
  76. object whose source module cannot be determined.
  77. """
  78. source = """\
  79. class Fake(object):
  80. pass
  81. hasEmptyModule = Fake()
  82. hasEmptyModule.__module__ = None
  83. """
  84. moduleDict = self.makeModuleAsDict(source,
  85. self.pathDir,
  86. 'empty_module_attr.py')
  87. self.assertFalse(self.isOriginalLocation(
  88. moduleDict['empty_module_attr.hasEmptyModule']))
  89. def test_failsWithDifferentModule(self):
  90. """
  91. L{isOriginalLocation} returns False when the attribute refers to
  92. an object outside of the module where that object was defined.
  93. """
  94. originalSource = """\
  95. class ImportThisClass(object):
  96. pass
  97. importThisObject = ImportThisClass()
  98. importThisNestingObject = ImportThisClass()
  99. importThisNestingObject.nestedObject = ImportThisClass()
  100. """
  101. importingSource = """\
  102. from original import (ImportThisClass,
  103. importThisObject,
  104. importThisNestingObject)
  105. """
  106. self.makeModule(originalSource, self.pathDir, 'original.py')
  107. importingDict = self.makeModuleAsDict(importingSource,
  108. self.pathDir,
  109. 'importing.py')
  110. self.assertFalse(
  111. self.isOriginalLocation(
  112. importingDict['importing.ImportThisClass']))
  113. self.assertFalse(
  114. self.isOriginalLocation(
  115. importingDict['importing.importThisObject']))
  116. nestingObject = importingDict['importing.importThisNestingObject']
  117. nestingObjectDict = self.attributesAsDict(nestingObject)
  118. nestedObject = nestingObjectDict[
  119. 'importing.importThisNestingObject.nestedObject']
  120. self.assertFalse(self.isOriginalLocation(nestedObject))
  121. def test_succeedsWithSameModule(self):
  122. """
  123. L{isOriginalLocation} returns True when the attribute refers to an
  124. object inside the module where that object was defined.
  125. """
  126. mSource = textwrap.dedent("""
  127. class ThisClassWasDefinedHere(object):
  128. pass
  129. anObject = ThisClassWasDefinedHere()
  130. aNestingObject = ThisClassWasDefinedHere()
  131. aNestingObject.nestedObject = ThisClassWasDefinedHere()
  132. """)
  133. mDict = self.makeModuleAsDict(mSource, self.pathDir, 'm.py')
  134. self.assertTrue(self.isOriginalLocation(
  135. mDict['m.ThisClassWasDefinedHere']))
  136. self.assertTrue(self.isOriginalLocation(mDict['m.aNestingObject']))
  137. nestingObject = mDict['m.aNestingObject']
  138. nestingObjectDict = self.attributesAsDict(nestingObject)
  139. nestedObject = nestingObjectDict['m.aNestingObject.nestedObject']
  140. self.assertTrue(self.isOriginalLocation(nestedObject))
  141. @skipIf(not isTwistedInstalled(), "Twisted is not installed.")
  142. class FindMachinesViaWrapperTests(_WritesPythonModules):
  143. """
  144. L{findMachinesViaWrapper} recursively yields FQPN,
  145. L{MethodicalMachine} pairs in and under a given
  146. L{twisted.python.modules.PythonModule} or
  147. L{twisted.python.modules.PythonAttribute}.
  148. """
  149. TEST_MODULE_SOURCE = """
  150. from automat import MethodicalMachine
  151. class PythonClass(object):
  152. _classMachine = MethodicalMachine()
  153. class NestedClass(object):
  154. _nestedClassMachine = MethodicalMachine()
  155. ignoredAttribute = "I am ignored."
  156. def ignoredMethod(self):
  157. "I am also ignored."
  158. rootLevelMachine = MethodicalMachine()
  159. ignoredPythonObject = PythonClass()
  160. anotherIgnoredPythonObject = "I am ignored."
  161. """
  162. def setUp(self):
  163. super(FindMachinesViaWrapperTests, self).setUp()
  164. from .._discover import findMachinesViaWrapper
  165. self.findMachinesViaWrapper = findMachinesViaWrapper
  166. def test_yieldsMachine(self):
  167. """
  168. When given a L{twisted.python.modules.PythonAttribute} that refers
  169. directly to a L{MethodicalMachine}, L{findMachinesViaWrapper}
  170. yields that machine and its FQPN.
  171. """
  172. source = """\
  173. from automat import MethodicalMachine
  174. rootMachine = MethodicalMachine()
  175. """
  176. moduleDict = self.makeModuleAsDict(source, self.pathDir, 'root.py')
  177. rootMachine = moduleDict['root.rootMachine']
  178. self.assertIn(('root.rootMachine', rootMachine.load()),
  179. list(self.findMachinesViaWrapper(rootMachine)))
  180. def test_yieldsMachineInClass(self):
  181. """
  182. When given a L{twisted.python.modules.PythonAttribute} that refers
  183. to a class that contains a L{MethodicalMachine} as a class
  184. variable, L{findMachinesViaWrapper} yields that machine and
  185. its FQPN.
  186. """
  187. source = """\
  188. from automat import MethodicalMachine
  189. class PythonClass(object):
  190. _classMachine = MethodicalMachine()
  191. """
  192. moduleDict = self.makeModuleAsDict(source, self.pathDir, 'clsmod.py')
  193. PythonClass = moduleDict['clsmod.PythonClass']
  194. self.assertIn(('clsmod.PythonClass._classMachine',
  195. PythonClass.load()._classMachine),
  196. list(self.findMachinesViaWrapper(PythonClass)))
  197. def test_yieldsMachineInNestedClass(self):
  198. """
  199. When given a L{twisted.python.modules.PythonAttribute} that refers
  200. to a nested class that contains a L{MethodicalMachine} as a
  201. class variable, L{findMachinesViaWrapper} yields that machine
  202. and its FQPN.
  203. """
  204. source = """\
  205. from automat import MethodicalMachine
  206. class PythonClass(object):
  207. class NestedClass(object):
  208. _classMachine = MethodicalMachine()
  209. """
  210. moduleDict = self.makeModuleAsDict(source,
  211. self.pathDir,
  212. 'nestedcls.py')
  213. PythonClass = moduleDict['nestedcls.PythonClass']
  214. self.assertIn(('nestedcls.PythonClass.NestedClass._classMachine',
  215. PythonClass.load().NestedClass._classMachine),
  216. list(self.findMachinesViaWrapper(PythonClass)))
  217. def test_yieldsMachineInModule(self):
  218. """
  219. When given a L{twisted.python.modules.PythonModule} that refers to
  220. a module that contains a L{MethodicalMachine},
  221. L{findMachinesViaWrapper} yields that machine and its FQPN.
  222. """
  223. source = """\
  224. from automat import MethodicalMachine
  225. rootMachine = MethodicalMachine()
  226. """
  227. module = self.makeModule(source, self.pathDir, 'root.py')
  228. rootMachine = self.loadModuleAsDict(module)['root.rootMachine'].load()
  229. self.assertIn(('root.rootMachine', rootMachine),
  230. list(self.findMachinesViaWrapper(module)))
  231. def test_yieldsMachineInClassInModule(self):
  232. """
  233. When given a L{twisted.python.modules.PythonModule} that refers to
  234. the original module of a class containing a
  235. L{MethodicalMachine}, L{findMachinesViaWrapper} yields that
  236. machine and its FQPN.
  237. """
  238. source = """\
  239. from automat import MethodicalMachine
  240. class PythonClass(object):
  241. _classMachine = MethodicalMachine()
  242. """
  243. module = self.makeModule(source, self.pathDir, 'clsmod.py')
  244. PythonClass = self.loadModuleAsDict(
  245. module)['clsmod.PythonClass'].load()
  246. self.assertIn(('clsmod.PythonClass._classMachine',
  247. PythonClass._classMachine),
  248. list(self.findMachinesViaWrapper(module)))
  249. def test_yieldsMachineInNestedClassInModule(self):
  250. """
  251. When given a L{twisted.python.modules.PythonModule} that refers to
  252. the original module of a nested class containing a
  253. L{MethodicalMachine}, L{findMachinesViaWrapper} yields that
  254. machine and its FQPN.
  255. """
  256. source = """\
  257. from automat import MethodicalMachine
  258. class PythonClass(object):
  259. class NestedClass(object):
  260. _classMachine = MethodicalMachine()
  261. """
  262. module = self.makeModule(source, self.pathDir, 'nestedcls.py')
  263. PythonClass = self.loadModuleAsDict(
  264. module)['nestedcls.PythonClass'].load()
  265. self.assertIn(('nestedcls.PythonClass.NestedClass._classMachine',
  266. PythonClass.NestedClass._classMachine),
  267. list(self.findMachinesViaWrapper(module)))
  268. def test_ignoresImportedClass(self):
  269. """
  270. When given a L{twisted.python.modules.PythonAttribute} that refers
  271. to a class imported from another module, any
  272. L{MethodicalMachine}s on that class are ignored.
  273. This behavior ensures that a machine is only discovered on a
  274. class when visiting the module where that class was defined.
  275. """
  276. originalSource = """
  277. from automat import MethodicalMachine
  278. class PythonClass(object):
  279. _classMachine = MethodicalMachine()
  280. """
  281. importingSource = """
  282. from original import PythonClass
  283. """
  284. self.makeModule(originalSource, self.pathDir, 'original.py')
  285. importingModule = self.makeModule(importingSource,
  286. self.pathDir,
  287. 'importing.py')
  288. self.assertFalse(list(self.findMachinesViaWrapper(importingModule)))
  289. def test_descendsIntoPackages(self):
  290. """
  291. L{findMachinesViaWrapper} descends into packages to discover
  292. machines.
  293. """
  294. pythonPath = self.PythonPath([self.pathDir])
  295. package = self.FilePath(self.pathDir).child("test_package")
  296. package.makedirs()
  297. package.child('__init__.py').touch()
  298. source = """
  299. from automat import MethodicalMachine
  300. class PythonClass(object):
  301. _classMachine = MethodicalMachine()
  302. rootMachine = MethodicalMachine()
  303. """
  304. self.makeModule(source, package.path, 'module.py')
  305. test_package = pythonPath['test_package']
  306. machines = sorted(self.findMachinesViaWrapper(test_package),
  307. key=operator.itemgetter(0))
  308. moduleDict = self.loadModuleAsDict(test_package['module'])
  309. rootMachine = moduleDict['test_package.module.rootMachine'].load()
  310. PythonClass = moduleDict['test_package.module.PythonClass'].load()
  311. expectedMachines = sorted(
  312. [('test_package.module.rootMachine',
  313. rootMachine),
  314. ('test_package.module.PythonClass._classMachine',
  315. PythonClass._classMachine)], key=operator.itemgetter(0))
  316. self.assertEqual(expectedMachines, machines)
  317. def test_infiniteLoop(self):
  318. """
  319. L{findMachinesViaWrapper} ignores infinite loops.
  320. Note this test can't fail - it can only run forever!
  321. """
  322. source = """
  323. class InfiniteLoop(object):
  324. pass
  325. InfiniteLoop.loop = InfiniteLoop
  326. """
  327. module = self.makeModule(source, self.pathDir, 'loop.py')
  328. self.assertFalse(list(self.findMachinesViaWrapper(module)))
  329. @skipIf(not isTwistedInstalled(), "Twisted is not installed.")
  330. class WrapFQPNTests(TestCase):
  331. """
  332. Tests that ensure L{wrapFQPN} loads the
  333. L{twisted.python.modules.PythonModule} or
  334. L{twisted.python.modules.PythonAttribute} for a given FQPN.
  335. """
  336. def setUp(self):
  337. from twisted.python.modules import PythonModule, PythonAttribute
  338. from .._discover import wrapFQPN, InvalidFQPN, NoModule, NoObject
  339. self.PythonModule = PythonModule
  340. self.PythonAttribute = PythonAttribute
  341. self.wrapFQPN = wrapFQPN
  342. self.InvalidFQPN = InvalidFQPN
  343. self.NoModule = NoModule
  344. self.NoObject = NoObject
  345. def assertModuleWrapperRefersTo(self, moduleWrapper, module):
  346. """
  347. Assert that a L{twisted.python.modules.PythonModule} refers to a
  348. particular Python module.
  349. """
  350. self.assertIsInstance(moduleWrapper, self.PythonModule)
  351. self.assertEqual(moduleWrapper.name, module.__name__)
  352. self.assertIs(moduleWrapper.load(), module)
  353. def assertAttributeWrapperRefersTo(self, attributeWrapper, fqpn, obj):
  354. """
  355. Assert that a L{twisted.python.modules.PythonAttribute} refers to a
  356. particular Python object.
  357. """
  358. self.assertIsInstance(attributeWrapper, self.PythonAttribute)
  359. self.assertEqual(attributeWrapper.name, fqpn)
  360. self.assertIs(attributeWrapper.load(), obj)
  361. def test_failsWithEmptyFQPN(self):
  362. """
  363. L{wrapFQPN} raises L{InvalidFQPN} when given an empty string.
  364. """
  365. with self.assertRaises(self.InvalidFQPN):
  366. self.wrapFQPN('')
  367. def test_failsWithBadDotting(self):
  368. """"
  369. L{wrapFQPN} raises L{InvalidFQPN} when given a badly-dotted
  370. FQPN. (e.g., x..y).
  371. """
  372. for bad in ('.fails', 'fails.', 'this..fails'):
  373. with self.assertRaises(self.InvalidFQPN):
  374. self.wrapFQPN(bad)
  375. def test_singleModule(self):
  376. """
  377. L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
  378. referring to the single module a dotless FQPN describes.
  379. """
  380. import os
  381. moduleWrapper = self.wrapFQPN('os')
  382. self.assertIsInstance(moduleWrapper, self.PythonModule)
  383. self.assertIs(moduleWrapper.load(), os)
  384. def test_failsWithMissingSingleModuleOrPackage(self):
  385. """
  386. L{wrapFQPN} raises L{NoModule} when given a dotless FQPN that does
  387. not refer to a module or package.
  388. """
  389. with self.assertRaises(self.NoModule):
  390. self.wrapFQPN("this is not an acceptable name!")
  391. def test_singlePackage(self):
  392. """
  393. L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
  394. referring to the single package a dotless FQPN describes.
  395. """
  396. import xml
  397. self.assertModuleWrapperRefersTo(self.wrapFQPN('xml'), xml)
  398. def test_multiplePackages(self):
  399. """
  400. L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
  401. referring to the deepest package described by dotted FQPN.
  402. """
  403. import xml.etree
  404. self.assertModuleWrapperRefersTo(self.wrapFQPN('xml.etree'), xml.etree)
  405. def test_multiplePackagesFinalModule(self):
  406. """
  407. L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
  408. referring to the deepest module described by dotted FQPN.
  409. """
  410. import xml.etree.ElementTree
  411. self.assertModuleWrapperRefersTo(
  412. self.wrapFQPN('xml.etree.ElementTree'), xml.etree.ElementTree)
  413. def test_singleModuleObject(self):
  414. """
  415. L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute}
  416. referring to the deepest object an FQPN names, traversing one module.
  417. """
  418. import os
  419. self.assertAttributeWrapperRefersTo(
  420. self.wrapFQPN('os.path'), 'os.path', os.path)
  421. def test_multiplePackagesObject(self):
  422. """
  423. L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute}
  424. referring to the deepest object described by an FQPN,
  425. descending through several packages.
  426. """
  427. import xml.etree.ElementTree
  428. import automat
  429. for fqpn, obj in [('xml.etree.ElementTree.fromstring',
  430. xml.etree.ElementTree.fromstring),
  431. ('automat.MethodicalMachine.__doc__',
  432. automat.MethodicalMachine.__doc__)]:
  433. self.assertAttributeWrapperRefersTo(
  434. self.wrapFQPN(fqpn), fqpn, obj)
  435. def test_failsWithMultiplePackagesMissingModuleOrPackage(self):
  436. """
  437. L{wrapFQPN} raises L{NoObject} when given an FQPN that contains a
  438. missing attribute, module, or package.
  439. """
  440. for bad in ('xml.etree.nope!',
  441. 'xml.etree.nope!.but.the.rest.is.believable'):
  442. with self.assertRaises(self.NoObject):
  443. self.wrapFQPN(bad)
  444. @skipIf(not isTwistedInstalled(), "Twisted is not installed.")
  445. class FindMachinesIntegrationTests(_WritesPythonModules):
  446. """
  447. Integration tests to check that L{findMachines} yields all
  448. machines discoverable at or below an FQPN.
  449. """
  450. SOURCE = """
  451. from automat import MethodicalMachine
  452. class PythonClass(object):
  453. _machine = MethodicalMachine()
  454. ignored = "i am ignored"
  455. rootLevel = MethodicalMachine()
  456. ignored = "i am ignored"
  457. """
  458. def setUp(self):
  459. super(FindMachinesIntegrationTests, self).setUp()
  460. from .._discover import findMachines
  461. self.findMachines = findMachines
  462. packageDir = self.FilePath(self.pathDir).child("test_package")
  463. packageDir.makedirs()
  464. self.pythonPath = self.PythonPath([self.pathDir])
  465. self.writeSourceInto(self.SOURCE, packageDir.path, '__init__.py')
  466. subPackageDir = packageDir.child('subpackage')
  467. subPackageDir.makedirs()
  468. subPackageDir.child('__init__.py').touch()
  469. self.makeModule(self.SOURCE, subPackageDir.path, 'module.py')
  470. self.packageDict = self.loadModuleAsDict(
  471. self.pythonPath['test_package'])
  472. self.moduleDict = self.loadModuleAsDict(
  473. self.pythonPath['test_package']['subpackage']['module'])
  474. def test_discoverAll(self):
  475. """
  476. Given a top-level package FQPN, L{findMachines} discovers all
  477. L{MethodicalMachine} instances in and below it.
  478. """
  479. machines = sorted(self.findMachines('test_package'),
  480. key=operator.itemgetter(0))
  481. tpRootLevel = self.packageDict['test_package.rootLevel'].load()
  482. tpPythonClass = self.packageDict['test_package.PythonClass'].load()
  483. mRLAttr = self.moduleDict['test_package.subpackage.module.rootLevel']
  484. mRootLevel = mRLAttr.load()
  485. mPCAttr = self.moduleDict['test_package.subpackage.module.PythonClass']
  486. mPythonClass = mPCAttr.load()
  487. expectedMachines = sorted(
  488. [('test_package.rootLevel', tpRootLevel),
  489. ('test_package.PythonClass._machine', tpPythonClass._machine),
  490. ('test_package.subpackage.module.rootLevel', mRootLevel),
  491. ('test_package.subpackage.module.PythonClass._machine',
  492. mPythonClass._machine)],
  493. key=operator.itemgetter(0))
  494. self.assertEqual(expectedMachines, machines)