import operator
import os
import shutil
import sys
import textwrap
import tempfile
from unittest import skipIf, TestCase
import six
def isTwistedInstalled():
try:
__import__('twisted')
except ImportError:
return False
else:
return True
class _WritesPythonModules(TestCase):
"""
A helper that enables generating Python module test fixtures.
"""
def setUp(self):
super(_WritesPythonModules, self).setUp()
from twisted.python.modules import getModule, PythonPath
from twisted.python.filepath import FilePath
self.getModule = getModule
self.PythonPath = PythonPath
self.FilePath = FilePath
self.originalSysModules = set(sys.modules.keys())
self.savedSysPath = sys.path[:]
self.pathDir = tempfile.mkdtemp()
self.makeImportable(self.pathDir)
def tearDown(self):
super(_WritesPythonModules, self).tearDown()
sys.path[:] = self.savedSysPath
modulesToDelete = six.viewkeys(sys.modules) - self.originalSysModules
for module in modulesToDelete:
del sys.modules[module]
shutil.rmtree(self.pathDir)
def makeImportable(self, path):
sys.path.append(path)
def writeSourceInto(self, source, path, moduleName):
directory = self.FilePath(path)
module = directory.child(moduleName)
# FilePath always opens a file in binary mode - but that will
# break on Python 3
with open(module.path, 'w') as f:
f.write(textwrap.dedent(source))
return self.PythonPath([directory.path])
def makeModule(self, source, path, moduleName):
pythonModuleName, _ = os.path.splitext(moduleName)
return self.writeSourceInto(source, path, moduleName)[pythonModuleName]
def attributesAsDict(self, hasIterAttributes):
return {attr.name: attr for attr in hasIterAttributes.iterAttributes()}
def loadModuleAsDict(self, module):
module.load()
return self.attributesAsDict(module)
def makeModuleAsDict(self, source, path, name):
return self.loadModuleAsDict(self.makeModule(source, path, name))
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class OriginalLocationTests(_WritesPythonModules):
"""
Tests that L{isOriginalLocation} detects when a
L{PythonAttribute}'s FQPN refers to an object inside the module
where it was defined.
For example: A L{twisted.python.modules.PythonAttribute} with a
name of 'foo.bar' that refers to a 'bar' object defined in module
'baz' does *not* refer to bar's original location, while a
L{PythonAttribute} with a name of 'baz.bar' does.
"""
def setUp(self):
super(OriginalLocationTests, self).setUp()
from .._discover import isOriginalLocation
self.isOriginalLocation = isOriginalLocation
def test_failsWithNoModule(self):
"""
L{isOriginalLocation} returns False when the attribute refers to an
object whose source module cannot be determined.
"""
source = """\
class Fake(object):
pass
hasEmptyModule = Fake()
hasEmptyModule.__module__ = None
"""
moduleDict = self.makeModuleAsDict(source,
self.pathDir,
'empty_module_attr.py')
self.assertFalse(self.isOriginalLocation(
moduleDict['empty_module_attr.hasEmptyModule']))
def test_failsWithDifferentModule(self):
"""
L{isOriginalLocation} returns False when the attribute refers to
an object outside of the module where that object was defined.
"""
originalSource = """\
class ImportThisClass(object):
pass
importThisObject = ImportThisClass()
importThisNestingObject = ImportThisClass()
importThisNestingObject.nestedObject = ImportThisClass()
"""
importingSource = """\
from original import (ImportThisClass,
importThisObject,
importThisNestingObject)
"""
self.makeModule(originalSource, self.pathDir, 'original.py')
importingDict = self.makeModuleAsDict(importingSource,
self.pathDir,
'importing.py')
self.assertFalse(
self.isOriginalLocation(
importingDict['importing.ImportThisClass']))
self.assertFalse(
self.isOriginalLocation(
importingDict['importing.importThisObject']))
nestingObject = importingDict['importing.importThisNestingObject']
nestingObjectDict = self.attributesAsDict(nestingObject)
nestedObject = nestingObjectDict[
'importing.importThisNestingObject.nestedObject']
self.assertFalse(self.isOriginalLocation(nestedObject))
def test_succeedsWithSameModule(self):
"""
L{isOriginalLocation} returns True when the attribute refers to an
object inside the module where that object was defined.
"""
mSource = textwrap.dedent("""
class ThisClassWasDefinedHere(object):
pass
anObject = ThisClassWasDefinedHere()
aNestingObject = ThisClassWasDefinedHere()
aNestingObject.nestedObject = ThisClassWasDefinedHere()
""")
mDict = self.makeModuleAsDict(mSource, self.pathDir, 'm.py')
self.assertTrue(self.isOriginalLocation(
mDict['m.ThisClassWasDefinedHere']))
self.assertTrue(self.isOriginalLocation(mDict['m.aNestingObject']))
nestingObject = mDict['m.aNestingObject']
nestingObjectDict = self.attributesAsDict(nestingObject)
nestedObject = nestingObjectDict['m.aNestingObject.nestedObject']
self.assertTrue(self.isOriginalLocation(nestedObject))
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class FindMachinesViaWrapperTests(_WritesPythonModules):
"""
L{findMachinesViaWrapper} recursively yields FQPN,
L{MethodicalMachine} pairs in and under a given
L{twisted.python.modules.PythonModule} or
L{twisted.python.modules.PythonAttribute}.
"""
TEST_MODULE_SOURCE = """
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
class NestedClass(object):
_nestedClassMachine = MethodicalMachine()
ignoredAttribute = "I am ignored."
def ignoredMethod(self):
"I am also ignored."
rootLevelMachine = MethodicalMachine()
ignoredPythonObject = PythonClass()
anotherIgnoredPythonObject = "I am ignored."
"""
def setUp(self):
super(FindMachinesViaWrapperTests, self).setUp()
from .._discover import findMachinesViaWrapper
self.findMachinesViaWrapper = findMachinesViaWrapper
def test_yieldsMachine(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
directly to a L{MethodicalMachine}, L{findMachinesViaWrapper}
yields that machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
rootMachine = MethodicalMachine()
"""
moduleDict = self.makeModuleAsDict(source, self.pathDir, 'root.py')
rootMachine = moduleDict['root.rootMachine']
self.assertIn(('root.rootMachine', rootMachine.load()),
list(self.findMachinesViaWrapper(rootMachine)))
def test_yieldsMachineInClass(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
to a class that contains a L{MethodicalMachine} as a class
variable, L{findMachinesViaWrapper} yields that machine and
its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
"""
moduleDict = self.makeModuleAsDict(source, self.pathDir, 'clsmod.py')
PythonClass = moduleDict['clsmod.PythonClass']
self.assertIn(('clsmod.PythonClass._classMachine',
PythonClass.load()._classMachine),
list(self.findMachinesViaWrapper(PythonClass)))
def test_yieldsMachineInNestedClass(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
to a nested class that contains a L{MethodicalMachine} as a
class variable, L{findMachinesViaWrapper} yields that machine
and its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
class NestedClass(object):
_classMachine = MethodicalMachine()
"""
moduleDict = self.makeModuleAsDict(source,
self.pathDir,
'nestedcls.py')
PythonClass = moduleDict['nestedcls.PythonClass']
self.assertIn(('nestedcls.PythonClass.NestedClass._classMachine',
PythonClass.load().NestedClass._classMachine),
list(self.findMachinesViaWrapper(PythonClass)))
def test_yieldsMachineInModule(self):
"""
When given a L{twisted.python.modules.PythonModule} that refers to
a module that contains a L{MethodicalMachine},
L{findMachinesViaWrapper} yields that machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
rootMachine = MethodicalMachine()
"""
module = self.makeModule(source, self.pathDir, 'root.py')
rootMachine = self.loadModuleAsDict(module)['root.rootMachine'].load()
self.assertIn(('root.rootMachine', rootMachine),
list(self.findMachinesViaWrapper(module)))
def test_yieldsMachineInClassInModule(self):
"""
When given a L{twisted.python.modules.PythonModule} that refers to
the original module of a class containing a
L{MethodicalMachine}, L{findMachinesViaWrapper} yields that
machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
"""
module = self.makeModule(source, self.pathDir, 'clsmod.py')
PythonClass = self.loadModuleAsDict(
module)['clsmod.PythonClass'].load()
self.assertIn(('clsmod.PythonClass._classMachine',
PythonClass._classMachine),
list(self.findMachinesViaWrapper(module)))
def test_yieldsMachineInNestedClassInModule(self):
"""
When given a L{twisted.python.modules.PythonModule} that refers to
the original module of a nested class containing a
L{MethodicalMachine}, L{findMachinesViaWrapper} yields that
machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
class NestedClass(object):
_classMachine = MethodicalMachine()
"""
module = self.makeModule(source, self.pathDir, 'nestedcls.py')
PythonClass = self.loadModuleAsDict(
module)['nestedcls.PythonClass'].load()
self.assertIn(('nestedcls.PythonClass.NestedClass._classMachine',
PythonClass.NestedClass._classMachine),
list(self.findMachinesViaWrapper(module)))
def test_ignoresImportedClass(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
to a class imported from another module, any
L{MethodicalMachine}s on that class are ignored.
This behavior ensures that a machine is only discovered on a
class when visiting the module where that class was defined.
"""
originalSource = """
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
"""
importingSource = """
from original import PythonClass
"""
self.makeModule(originalSource, self.pathDir, 'original.py')
importingModule = self.makeModule(importingSource,
self.pathDir,
'importing.py')
self.assertFalse(list(self.findMachinesViaWrapper(importingModule)))
def test_descendsIntoPackages(self):
"""
L{findMachinesViaWrapper} descends into packages to discover
machines.
"""
pythonPath = self.PythonPath([self.pathDir])
package = self.FilePath(self.pathDir).child("test_package")
package.makedirs()
package.child('__init__.py').touch()
source = """
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
rootMachine = MethodicalMachine()
"""
self.makeModule(source, package.path, 'module.py')
test_package = pythonPath['test_package']
machines = sorted(self.findMachinesViaWrapper(test_package),
key=operator.itemgetter(0))
moduleDict = self.loadModuleAsDict(test_package['module'])
rootMachine = moduleDict['test_package.module.rootMachine'].load()
PythonClass = moduleDict['test_package.module.PythonClass'].load()
expectedMachines = sorted(
[('test_package.module.rootMachine',
rootMachine),
('test_package.module.PythonClass._classMachine',
PythonClass._classMachine)], key=operator.itemgetter(0))
self.assertEqual(expectedMachines, machines)
def test_infiniteLoop(self):
"""
L{findMachinesViaWrapper} ignores infinite loops.
Note this test can't fail - it can only run forever!
"""
source = """
class InfiniteLoop(object):
pass
InfiniteLoop.loop = InfiniteLoop
"""
module = self.makeModule(source, self.pathDir, 'loop.py')
self.assertFalse(list(self.findMachinesViaWrapper(module)))
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class WrapFQPNTests(TestCase):
"""
Tests that ensure L{wrapFQPN} loads the
L{twisted.python.modules.PythonModule} or
L{twisted.python.modules.PythonAttribute} for a given FQPN.
"""
def setUp(self):
from twisted.python.modules import PythonModule, PythonAttribute
from .._discover import wrapFQPN, InvalidFQPN, NoModule, NoObject
self.PythonModule = PythonModule
self.PythonAttribute = PythonAttribute
self.wrapFQPN = wrapFQPN
self.InvalidFQPN = InvalidFQPN
self.NoModule = NoModule
self.NoObject = NoObject
def assertModuleWrapperRefersTo(self, moduleWrapper, module):
"""
Assert that a L{twisted.python.modules.PythonModule} refers to a
particular Python module.
"""
self.assertIsInstance(moduleWrapper, self.PythonModule)
self.assertEqual(moduleWrapper.name, module.__name__)
self.assertIs(moduleWrapper.load(), module)
def assertAttributeWrapperRefersTo(self, attributeWrapper, fqpn, obj):
"""
Assert that a L{twisted.python.modules.PythonAttribute} refers to a
particular Python object.
"""
self.assertIsInstance(attributeWrapper, self.PythonAttribute)
self.assertEqual(attributeWrapper.name, fqpn)
self.assertIs(attributeWrapper.load(), obj)
def test_failsWithEmptyFQPN(self):
"""
L{wrapFQPN} raises L{InvalidFQPN} when given an empty string.
"""
with self.assertRaises(self.InvalidFQPN):
self.wrapFQPN('')
def test_failsWithBadDotting(self):
""""
L{wrapFQPN} raises L{InvalidFQPN} when given a badly-dotted
FQPN. (e.g., x..y).
"""
for bad in ('.fails', 'fails.', 'this..fails'):
with self.assertRaises(self.InvalidFQPN):
self.wrapFQPN(bad)
def test_singleModule(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the single module a dotless FQPN describes.
"""
import os
moduleWrapper = self.wrapFQPN('os')
self.assertIsInstance(moduleWrapper, self.PythonModule)
self.assertIs(moduleWrapper.load(), os)
def test_failsWithMissingSingleModuleOrPackage(self):
"""
L{wrapFQPN} raises L{NoModule} when given a dotless FQPN that does
not refer to a module or package.
"""
with self.assertRaises(self.NoModule):
self.wrapFQPN("this is not an acceptable name!")
def test_singlePackage(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the single package a dotless FQPN describes.
"""
import xml
self.assertModuleWrapperRefersTo(self.wrapFQPN('xml'), xml)
def test_multiplePackages(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the deepest package described by dotted FQPN.
"""
import xml.etree
self.assertModuleWrapperRefersTo(self.wrapFQPN('xml.etree'), xml.etree)
def test_multiplePackagesFinalModule(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the deepest module described by dotted FQPN.
"""
import xml.etree.ElementTree
self.assertModuleWrapperRefersTo(
self.wrapFQPN('xml.etree.ElementTree'), xml.etree.ElementTree)
def test_singleModuleObject(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute}
referring to the deepest object an FQPN names, traversing one module.
"""
import os
self.assertAttributeWrapperRefersTo(
self.wrapFQPN('os.path'), 'os.path', os.path)
def test_multiplePackagesObject(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute}
referring to the deepest object described by an FQPN,
descending through several packages.
"""
import xml.etree.ElementTree
import automat
for fqpn, obj in [('xml.etree.ElementTree.fromstring',
xml.etree.ElementTree.fromstring),
('automat.MethodicalMachine.__doc__',
automat.MethodicalMachine.__doc__)]:
self.assertAttributeWrapperRefersTo(
self.wrapFQPN(fqpn), fqpn, obj)
def test_failsWithMultiplePackagesMissingModuleOrPackage(self):
"""
L{wrapFQPN} raises L{NoObject} when given an FQPN that contains a
missing attribute, module, or package.
"""
for bad in ('xml.etree.nope!',
'xml.etree.nope!.but.the.rest.is.believable'):
with self.assertRaises(self.NoObject):
self.wrapFQPN(bad)
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class FindMachinesIntegrationTests(_WritesPythonModules):
"""
Integration tests to check that L{findMachines} yields all
machines discoverable at or below an FQPN.
"""
SOURCE = """
from automat import MethodicalMachine
class PythonClass(object):
_machine = MethodicalMachine()
ignored = "i am ignored"
rootLevel = MethodicalMachine()
ignored = "i am ignored"
"""
def setUp(self):
super(FindMachinesIntegrationTests, self).setUp()
from .._discover import findMachines
self.findMachines = findMachines
packageDir = self.FilePath(self.pathDir).child("test_package")
packageDir.makedirs()
self.pythonPath = self.PythonPath([self.pathDir])
self.writeSourceInto(self.SOURCE, packageDir.path, '__init__.py')
subPackageDir = packageDir.child('subpackage')
subPackageDir.makedirs()
subPackageDir.child('__init__.py').touch()
self.makeModule(self.SOURCE, subPackageDir.path, 'module.py')
self.packageDict = self.loadModuleAsDict(
self.pythonPath['test_package'])
self.moduleDict = self.loadModuleAsDict(
self.pythonPath['test_package']['subpackage']['module'])
def test_discoverAll(self):
"""
Given a top-level package FQPN, L{findMachines} discovers all
L{MethodicalMachine} instances in and below it.
"""
machines = sorted(self.findMachines('test_package'),
key=operator.itemgetter(0))
tpRootLevel = self.packageDict['test_package.rootLevel'].load()
tpPythonClass = self.packageDict['test_package.PythonClass'].load()
mRLAttr = self.moduleDict['test_package.subpackage.module.rootLevel']
mRootLevel = mRLAttr.load()
mPCAttr = self.moduleDict['test_package.subpackage.module.PythonClass']
mPythonClass = mPCAttr.load()
expectedMachines = sorted(
[('test_package.rootLevel', tpRootLevel),
('test_package.PythonClass._machine', tpPythonClass._machine),
('test_package.subpackage.module.rootLevel', mRootLevel),
('test_package.subpackage.module.PythonClass._machine',
mPythonClass._machine)],
key=operator.itemgetter(0))
self.assertEqual(expectedMachines, machines)