Ausgabe der neuen DB Einträge
This commit is contained in:
parent
bad48e1627
commit
cfbbb9ee3d
2399 changed files with 843193 additions and 43 deletions
86
venv/lib/python3.9/site-packages/automat/_test/test_core.py
Normal file
86
venv/lib/python3.9/site-packages/automat/_test/test_core.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
|
||||
from .._core import Automaton, NoTransition
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
class CoreTests(TestCase):
|
||||
"""
|
||||
Tests for Automat's (currently private, implementation detail) core.
|
||||
"""
|
||||
|
||||
def test_NoTransition(self):
|
||||
"""
|
||||
A L{NoTransition} exception describes the state and input symbol
|
||||
that caused it.
|
||||
"""
|
||||
# NoTransition requires two arguments
|
||||
with self.assertRaises(TypeError):
|
||||
NoTransition()
|
||||
|
||||
state = "current-state"
|
||||
symbol = "transitionless-symbol"
|
||||
noTransitionException = NoTransition(state=state, symbol=symbol)
|
||||
|
||||
self.assertIs(noTransitionException.symbol, symbol)
|
||||
|
||||
self.assertIn(state, str(noTransitionException))
|
||||
self.assertIn(symbol, str(noTransitionException))
|
||||
|
||||
|
||||
def test_noOutputForInput(self):
|
||||
"""
|
||||
L{Automaton.outputForInput} raises L{NoTransition} if no
|
||||
transition for that input is defined.
|
||||
"""
|
||||
a = Automaton()
|
||||
self.assertRaises(NoTransition, a.outputForInput,
|
||||
"no-state", "no-symbol")
|
||||
|
||||
|
||||
def test_oneTransition(self):
|
||||
"""
|
||||
L{Automaton.addTransition} adds its input symbol to
|
||||
L{Automaton.inputAlphabet}, all its outputs to
|
||||
L{Automaton.outputAlphabet}, and causes L{Automaton.outputForInput} to
|
||||
start returning the new state and output symbols.
|
||||
"""
|
||||
a = Automaton()
|
||||
a.addTransition("beginning", "begin", "ending", ["end"])
|
||||
self.assertEqual(a.inputAlphabet(), {"begin"})
|
||||
self.assertEqual(a.outputAlphabet(), {"end"})
|
||||
self.assertEqual(a.outputForInput("beginning", "begin"),
|
||||
("ending", ["end"]))
|
||||
self.assertEqual(a.states(), {"beginning", "ending"})
|
||||
|
||||
|
||||
def test_oneTransition_nonIterableOutputs(self):
|
||||
"""
|
||||
L{Automaton.addTransition} raises a TypeError when given outputs
|
||||
that aren't iterable and doesn't add any transitions.
|
||||
"""
|
||||
a = Automaton()
|
||||
nonIterableOutputs = 1
|
||||
self.assertRaises(
|
||||
TypeError,
|
||||
a.addTransition,
|
||||
"fromState", "viaSymbol", "toState", nonIterableOutputs)
|
||||
self.assertFalse(a.inputAlphabet())
|
||||
self.assertFalse(a.outputAlphabet())
|
||||
self.assertFalse(a.states())
|
||||
self.assertFalse(a.allTransitions())
|
||||
|
||||
|
||||
def test_initialState(self):
|
||||
"""
|
||||
L{Automaton.initialState} is a descriptor that sets the initial
|
||||
state if it's not yet set, and raises L{ValueError} if it is.
|
||||
|
||||
"""
|
||||
a = Automaton()
|
||||
a.initialState = "a state"
|
||||
self.assertEqual(a.initialState, "a state")
|
||||
with self.assertRaises(ValueError):
|
||||
a.initialState = "another state"
|
||||
|
||||
|
||||
# FIXME: addTransition for transition that's been added before
|
||||
609
venv/lib/python3.9/site-packages/automat/_test/test_discover.py
Normal file
609
venv/lib/python3.9/site-packages/automat/_test/test_discover.py
Normal file
|
|
@ -0,0 +1,609 @@
|
|||
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)
|
||||
|
|
@ -0,0 +1,587 @@
|
|||
|
||||
"""
|
||||
Tests for the public interface of Automat.
|
||||
"""
|
||||
|
||||
from functools import reduce
|
||||
from unittest import TestCase
|
||||
|
||||
from automat._methodical import ArgSpec, _getArgNames, _getArgSpec, _filterArgs
|
||||
from .. import MethodicalMachine, NoTransition
|
||||
from .. import _methodical
|
||||
|
||||
|
||||
class MethodicalTests(TestCase):
|
||||
"""
|
||||
Tests for L{MethodicalMachine}.
|
||||
"""
|
||||
|
||||
def test_oneTransition(self):
|
||||
"""
|
||||
L{MethodicalMachine} provides a way for you to declare a state machine
|
||||
with inputs, outputs, and states as methods. When you have declared an
|
||||
input, an output, and a state, calling the input method in that state
|
||||
will produce the specified output.
|
||||
"""
|
||||
|
||||
class Machination(object):
|
||||
machine = MethodicalMachine()
|
||||
@machine.input()
|
||||
def anInput(self):
|
||||
"an input"
|
||||
|
||||
@machine.output()
|
||||
def anOutput(self):
|
||||
"an output"
|
||||
return "an-output-value"
|
||||
|
||||
@machine.output()
|
||||
def anotherOutput(self):
|
||||
"another output"
|
||||
return "another-output-value"
|
||||
|
||||
@machine.state(initial=True)
|
||||
def anState(self):
|
||||
"a state"
|
||||
|
||||
@machine.state()
|
||||
def anotherState(self):
|
||||
"another state"
|
||||
|
||||
anState.upon(anInput, enter=anotherState, outputs=[anOutput])
|
||||
anotherState.upon(anInput, enter=anotherState,
|
||||
outputs=[anotherOutput])
|
||||
|
||||
m = Machination()
|
||||
self.assertEqual(m.anInput(), ["an-output-value"])
|
||||
self.assertEqual(m.anInput(), ["another-output-value"])
|
||||
|
||||
|
||||
def test_machineItselfIsPrivate(self):
|
||||
"""
|
||||
L{MethodicalMachine} is an implementation detail. If you attempt to
|
||||
access it on an instance of your class, you will get an exception.
|
||||
However, since tools may need to access it for the purposes of, for
|
||||
example, visualization, you may access it on the class itself.
|
||||
"""
|
||||
expectedMachine = MethodicalMachine()
|
||||
class Machination(object):
|
||||
machine = expectedMachine
|
||||
machination = Machination()
|
||||
with self.assertRaises(AttributeError) as cm:
|
||||
machination.machine
|
||||
self.assertIn("MethodicalMachine is an implementation detail",
|
||||
str(cm.exception))
|
||||
self.assertIs(Machination.machine, expectedMachine)
|
||||
|
||||
|
||||
def test_outputsArePrivate(self):
|
||||
"""
|
||||
One of the benefits of using a state machine is that your output method
|
||||
implementations don't need to take invalid state transitions into
|
||||
account - the methods simply won't be called. This property would be
|
||||
broken if client code called output methods directly, so output methods
|
||||
are not directly visible under their names.
|
||||
"""
|
||||
class Machination(object):
|
||||
machine = MethodicalMachine()
|
||||
counter = 0
|
||||
@machine.input()
|
||||
def anInput(self):
|
||||
"an input"
|
||||
@machine.output()
|
||||
def anOutput(self):
|
||||
self.counter += 1
|
||||
@machine.state(initial=True)
|
||||
def state(self):
|
||||
"a machine state"
|
||||
state.upon(anInput, enter=state, outputs=[anOutput])
|
||||
mach1 = Machination()
|
||||
mach1.anInput()
|
||||
self.assertEqual(mach1.counter, 1)
|
||||
mach2 = Machination()
|
||||
with self.assertRaises(AttributeError) as cm:
|
||||
mach2.anOutput
|
||||
self.assertEqual(mach2.counter, 0)
|
||||
|
||||
self.assertIn(
|
||||
"Machination.anOutput is a state-machine output method; to "
|
||||
"produce this output, call an input method instead.",
|
||||
str(cm.exception)
|
||||
)
|
||||
|
||||
|
||||
def test_multipleMachines(self):
|
||||
"""
|
||||
Two machines may co-exist happily on the same instance; they don't
|
||||
interfere with each other.
|
||||
"""
|
||||
class MultiMach(object):
|
||||
a = MethodicalMachine()
|
||||
b = MethodicalMachine()
|
||||
|
||||
@a.input()
|
||||
def inputA(self):
|
||||
"input A"
|
||||
@b.input()
|
||||
def inputB(self):
|
||||
"input B"
|
||||
@a.state(initial=True)
|
||||
def initialA(self):
|
||||
"initial A"
|
||||
@b.state(initial=True)
|
||||
def initialB(self):
|
||||
"initial B"
|
||||
@a.output()
|
||||
def outputA(self):
|
||||
return "A"
|
||||
@b.output()
|
||||
def outputB(self):
|
||||
return "B"
|
||||
initialA.upon(inputA, initialA, [outputA])
|
||||
initialB.upon(inputB, initialB, [outputB])
|
||||
|
||||
mm = MultiMach()
|
||||
self.assertEqual(mm.inputA(), ["A"])
|
||||
self.assertEqual(mm.inputB(), ["B"])
|
||||
|
||||
|
||||
def test_collectOutputs(self):
|
||||
"""
|
||||
Outputs can be combined with the "collector" argument to "upon".
|
||||
"""
|
||||
import operator
|
||||
class Machine(object):
|
||||
m = MethodicalMachine()
|
||||
@m.input()
|
||||
def input(self):
|
||||
"an input"
|
||||
@m.output()
|
||||
def outputA(self):
|
||||
return "A"
|
||||
@m.output()
|
||||
def outputB(self):
|
||||
return "B"
|
||||
@m.state(initial=True)
|
||||
def state(self):
|
||||
"a state"
|
||||
state.upon(input, state, [outputA, outputB],
|
||||
collector=lambda x: reduce(operator.add, x))
|
||||
m = Machine()
|
||||
self.assertEqual(m.input(), "AB")
|
||||
|
||||
|
||||
def test_methodName(self):
|
||||
"""
|
||||
Input methods preserve their declared names.
|
||||
"""
|
||||
class Mech(object):
|
||||
m = MethodicalMachine()
|
||||
@m.input()
|
||||
def declaredInputName(self):
|
||||
"an input"
|
||||
@m.state(initial=True)
|
||||
def aState(self):
|
||||
"state"
|
||||
m = Mech()
|
||||
with self.assertRaises(TypeError) as cm:
|
||||
m.declaredInputName("too", "many", "arguments")
|
||||
self.assertIn("declaredInputName", str(cm.exception))
|
||||
|
||||
|
||||
def test_inputWithArguments(self):
|
||||
"""
|
||||
If an input takes an argument, it will pass that along to its output.
|
||||
"""
|
||||
class Mechanism(object):
|
||||
m = MethodicalMachine()
|
||||
@m.input()
|
||||
def input(self, x, y=1):
|
||||
"an input"
|
||||
@m.state(initial=True)
|
||||
def state(self):
|
||||
"a state"
|
||||
@m.output()
|
||||
def output(self, x, y=1):
|
||||
self._x = x
|
||||
return x + y
|
||||
state.upon(input, state, [output])
|
||||
|
||||
m = Mechanism()
|
||||
self.assertEqual(m.input(3), [4])
|
||||
self.assertEqual(m._x, 3)
|
||||
|
||||
|
||||
def test_outputWithSubsetOfArguments(self):
|
||||
"""
|
||||
Inputs pass arguments that output will accept.
|
||||
"""
|
||||
class Mechanism(object):
|
||||
m = MethodicalMachine()
|
||||
@m.input()
|
||||
def input(self, x, y=1):
|
||||
"an input"
|
||||
@m.state(initial=True)
|
||||
def state(self):
|
||||
"a state"
|
||||
@m.output()
|
||||
def outputX(self, x):
|
||||
self._x = x
|
||||
return x
|
||||
@m.output()
|
||||
def outputY(self, y):
|
||||
self._y = y
|
||||
return y
|
||||
@m.output()
|
||||
def outputNoArgs(self):
|
||||
return None
|
||||
state.upon(input, state, [outputX, outputY, outputNoArgs])
|
||||
|
||||
m = Mechanism()
|
||||
|
||||
# Pass x as positional argument.
|
||||
self.assertEqual(m.input(3), [3, 1, None])
|
||||
self.assertEqual(m._x, 3)
|
||||
self.assertEqual(m._y, 1)
|
||||
|
||||
# Pass x as key word argument.
|
||||
self.assertEqual(m.input(x=4), [4, 1, None])
|
||||
self.assertEqual(m._x, 4)
|
||||
self.assertEqual(m._y, 1)
|
||||
|
||||
# Pass y as positional argument.
|
||||
self.assertEqual(m.input(6, 3), [6, 3, None])
|
||||
self.assertEqual(m._x, 6)
|
||||
self.assertEqual(m._y, 3)
|
||||
|
||||
# Pass y as key word argument.
|
||||
self.assertEqual(m.input(5, y=2), [5, 2, None])
|
||||
self.assertEqual(m._x, 5)
|
||||
self.assertEqual(m._y, 2)
|
||||
|
||||
|
||||
def test_inputFunctionsMustBeEmpty(self):
|
||||
"""
|
||||
The wrapped input function must have an empty body.
|
||||
"""
|
||||
# input functions are executed to assert that the signature matches,
|
||||
# but their body must be empty
|
||||
|
||||
_methodical._empty() # chase coverage
|
||||
_methodical._docstring()
|
||||
|
||||
class Mechanism(object):
|
||||
m = MethodicalMachine()
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
@m.input()
|
||||
def input(self):
|
||||
"an input"
|
||||
list() # pragma: no cover
|
||||
self.assertEqual(str(cm.exception), "function body must be empty")
|
||||
|
||||
# all three of these cases should be valid. Functions/methods with
|
||||
# docstrings produce slightly different bytecode than ones without.
|
||||
|
||||
class MechanismWithDocstring(object):
|
||||
m = MethodicalMachine()
|
||||
@m.input()
|
||||
def input(self):
|
||||
"an input"
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"starting state"
|
||||
start.upon(input, enter=start, outputs=[])
|
||||
MechanismWithDocstring().input()
|
||||
|
||||
class MechanismWithPass(object):
|
||||
m = MethodicalMachine()
|
||||
@m.input()
|
||||
def input(self):
|
||||
pass
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"starting state"
|
||||
start.upon(input, enter=start, outputs=[])
|
||||
MechanismWithPass().input()
|
||||
|
||||
class MechanismWithDocstringAndPass(object):
|
||||
m = MethodicalMachine()
|
||||
@m.input()
|
||||
def input(self):
|
||||
"an input"
|
||||
pass
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"starting state"
|
||||
start.upon(input, enter=start, outputs=[])
|
||||
MechanismWithDocstringAndPass().input()
|
||||
|
||||
class MechanismReturnsNone(object):
|
||||
m = MethodicalMachine()
|
||||
@m.input()
|
||||
def input(self):
|
||||
return None
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"starting state"
|
||||
start.upon(input, enter=start, outputs=[])
|
||||
MechanismReturnsNone().input()
|
||||
|
||||
class MechanismWithDocstringAndReturnsNone(object):
|
||||
m = MethodicalMachine()
|
||||
@m.input()
|
||||
def input(self):
|
||||
"an input"
|
||||
return None
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"starting state"
|
||||
start.upon(input, enter=start, outputs=[])
|
||||
MechanismWithDocstringAndReturnsNone().input()
|
||||
|
||||
|
||||
def test_inputOutputMismatch(self):
|
||||
"""
|
||||
All the argument lists of the outputs for a given input must match; if
|
||||
one does not the call to C{upon} will raise a C{TypeError}.
|
||||
"""
|
||||
class Mechanism(object):
|
||||
m = MethodicalMachine()
|
||||
@m.input()
|
||||
def nameOfInput(self, a):
|
||||
"an input"
|
||||
@m.output()
|
||||
def outputThatMatches(self, a):
|
||||
"an output that matches"
|
||||
@m.output()
|
||||
def outputThatDoesntMatch(self, b):
|
||||
"an output that doesn't match"
|
||||
@m.state()
|
||||
def state(self):
|
||||
"a state"
|
||||
with self.assertRaises(TypeError) as cm:
|
||||
state.upon(nameOfInput, state, [outputThatMatches,
|
||||
outputThatDoesntMatch])
|
||||
self.assertIn("nameOfInput", str(cm.exception))
|
||||
self.assertIn("outputThatDoesntMatch", str(cm.exception))
|
||||
|
||||
|
||||
def test_getArgNames(self):
|
||||
"""
|
||||
Type annotations should be included in the set of
|
||||
"""
|
||||
spec = ArgSpec(
|
||||
args=('a', 'b'),
|
||||
varargs=None,
|
||||
varkw=None,
|
||||
defaults=None,
|
||||
kwonlyargs=(),
|
||||
kwonlydefaults=None,
|
||||
annotations=(('a', int), ('b', str)),
|
||||
)
|
||||
self.assertEqual(
|
||||
_getArgNames(spec),
|
||||
{'a', 'b', ('a', int), ('b', str)},
|
||||
)
|
||||
|
||||
|
||||
def test_filterArgs(self):
|
||||
"""
|
||||
filterArgs() should not filter the `args` parameter
|
||||
if outputSpec accepts `*args`.
|
||||
"""
|
||||
inputSpec = _getArgSpec(lambda *args, **kwargs: None)
|
||||
outputSpec = _getArgSpec(lambda *args, **kwargs: None)
|
||||
argsIn = ()
|
||||
argsOut, _ = _filterArgs(argsIn, {}, inputSpec, outputSpec)
|
||||
self.assertIs(argsIn, argsOut)
|
||||
|
||||
|
||||
def test_multipleInitialStatesFailure(self):
|
||||
"""
|
||||
A L{MethodicalMachine} can only have one initial state.
|
||||
"""
|
||||
|
||||
class WillFail(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.state(initial=True)
|
||||
def firstInitialState(self):
|
||||
"The first initial state -- this is OK."
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
@m.state(initial=True)
|
||||
def secondInitialState(self):
|
||||
"The second initial state -- results in a ValueError."
|
||||
|
||||
|
||||
def test_multipleTransitionsFailure(self):
|
||||
"""
|
||||
A L{MethodicalMachine} can only have one transition per start/event
|
||||
pair.
|
||||
"""
|
||||
|
||||
class WillFail(object):
|
||||
m = MethodicalMachine()
|
||||
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"We start here."
|
||||
@m.state()
|
||||
def end(self):
|
||||
"Rainbows end."
|
||||
|
||||
@m.input()
|
||||
def event(self):
|
||||
"An event."
|
||||
start.upon(event, enter=end, outputs=[])
|
||||
with self.assertRaises(ValueError):
|
||||
start.upon(event, enter=end, outputs=[])
|
||||
|
||||
|
||||
def test_badTransitionForCurrentState(self):
|
||||
"""
|
||||
Calling any input method that lacks a transition for the machine's
|
||||
current state raises an informative L{NoTransition}.
|
||||
"""
|
||||
|
||||
class OnlyOnePath(object):
|
||||
m = MethodicalMachine()
|
||||
@m.state(initial=True)
|
||||
def start(self):
|
||||
"Start state."
|
||||
@m.state()
|
||||
def end(self):
|
||||
"End state."
|
||||
@m.input()
|
||||
def advance(self):
|
||||
"Move from start to end."
|
||||
@m.input()
|
||||
def deadEnd(self):
|
||||
"A transition from nowhere to nowhere."
|
||||
start.upon(advance, end, [])
|
||||
|
||||
machine = OnlyOnePath()
|
||||
with self.assertRaises(NoTransition) as cm:
|
||||
machine.deadEnd()
|
||||
self.assertIn("deadEnd", str(cm.exception))
|
||||
self.assertIn("start", str(cm.exception))
|
||||
machine.advance()
|
||||
with self.assertRaises(NoTransition) as cm:
|
||||
machine.deadEnd()
|
||||
self.assertIn("deadEnd", str(cm.exception))
|
||||
self.assertIn("end", str(cm.exception))
|
||||
|
||||
|
||||
def test_saveState(self):
|
||||
"""
|
||||
L{MethodicalMachine.serializer} is a decorator that modifies its
|
||||
decoratee's signature to take a "state" object as its first argument,
|
||||
which is the "serialized" argument to the L{MethodicalMachine.state}
|
||||
decorator.
|
||||
"""
|
||||
|
||||
class Mechanism(object):
|
||||
m = MethodicalMachine()
|
||||
def __init__(self):
|
||||
self.value = 1
|
||||
@m.state(serialized="first-state", initial=True)
|
||||
def first(self):
|
||||
"First state."
|
||||
@m.state(serialized="second-state")
|
||||
def second(self):
|
||||
"Second state."
|
||||
@m.serializer()
|
||||
def save(self, state):
|
||||
return {
|
||||
'machine-state': state,
|
||||
'some-value': self.value,
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
Mechanism().save(),
|
||||
{
|
||||
"machine-state": "first-state",
|
||||
"some-value": 1,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_restoreState(self):
|
||||
"""
|
||||
L{MethodicalMachine.unserializer} decorates a function that becomes a
|
||||
machine-state unserializer; its return value is mapped to the
|
||||
C{serialized} parameter to C{state}, and the L{MethodicalMachine}
|
||||
associated with that instance's state is updated to that state.
|
||||
"""
|
||||
|
||||
class Mechanism(object):
|
||||
m = MethodicalMachine()
|
||||
def __init__(self):
|
||||
self.value = 1
|
||||
self.ranOutput = False
|
||||
@m.state(serialized="first-state", initial=True)
|
||||
def first(self):
|
||||
"First state."
|
||||
@m.state(serialized="second-state")
|
||||
def second(self):
|
||||
"Second state."
|
||||
@m.input()
|
||||
def input(self):
|
||||
"an input"
|
||||
@m.output()
|
||||
def output(self):
|
||||
self.value = 2
|
||||
self.ranOutput = True
|
||||
return 1
|
||||
@m.output()
|
||||
def output2(self):
|
||||
return 2
|
||||
first.upon(input, second, [output],
|
||||
collector=lambda x: list(x)[0])
|
||||
second.upon(input, second, [output2],
|
||||
collector=lambda x: list(x)[0])
|
||||
@m.serializer()
|
||||
def save(self, state):
|
||||
return {
|
||||
'machine-state': state,
|
||||
'some-value': self.value,
|
||||
}
|
||||
|
||||
@m.unserializer()
|
||||
def _restore(self, blob):
|
||||
self.value = blob['some-value']
|
||||
return blob['machine-state']
|
||||
|
||||
@classmethod
|
||||
def fromBlob(cls, blob):
|
||||
self = cls()
|
||||
self._restore(blob)
|
||||
return self
|
||||
|
||||
m1 = Mechanism()
|
||||
m1.input()
|
||||
blob = m1.save()
|
||||
m2 = Mechanism.fromBlob(blob)
|
||||
self.assertEqual(m2.ranOutput, False)
|
||||
self.assertEqual(m2.input(), 2)
|
||||
self.assertEqual(
|
||||
m2.save(),
|
||||
{
|
||||
'machine-state': 'second-state',
|
||||
'some-value': 2,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
# FIXME: error for wrong types on any call to _oneTransition
|
||||
# FIXME: better public API for .upon; maybe a context manager?
|
||||
# FIXME: when transitions are defined, validate that we can always get to
|
||||
# terminal? do we care about this?
|
||||
# FIXME: implementation (and use-case/example) for passing args from in to out
|
||||
|
||||
# FIXME: possibly these need some kind of support from core
|
||||
# FIXME: wildcard state (in all states, when input X, emit Y and go to Z)
|
||||
# FIXME: wildcard input (in state X, when any input, emit Y and go to Z)
|
||||
# FIXME: combined wildcards (in any state for any input, emit Y go to Z)
|
||||
98
venv/lib/python3.9/site-packages/automat/_test/test_trace.py
Normal file
98
venv/lib/python3.9/site-packages/automat/_test/test_trace.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
from unittest import TestCase
|
||||
from .._methodical import MethodicalMachine
|
||||
|
||||
class SampleObject(object):
|
||||
mm = MethodicalMachine()
|
||||
|
||||
@mm.state(initial=True)
|
||||
def begin(self):
|
||||
"initial state"
|
||||
@mm.state()
|
||||
def middle(self):
|
||||
"middle state"
|
||||
@mm.state()
|
||||
def end(self):
|
||||
"end state"
|
||||
|
||||
@mm.input()
|
||||
def go1(self):
|
||||
"sample input"
|
||||
@mm.input()
|
||||
def go2(self):
|
||||
"sample input"
|
||||
@mm.input()
|
||||
def back(self):
|
||||
"sample input"
|
||||
|
||||
@mm.output()
|
||||
def out(self):
|
||||
"sample output"
|
||||
|
||||
setTrace = mm._setTrace
|
||||
|
||||
begin.upon(go1, middle, [out])
|
||||
middle.upon(go2, end, [out])
|
||||
end.upon(back, middle, [])
|
||||
middle.upon(back, begin, [])
|
||||
|
||||
class TraceTests(TestCase):
|
||||
def test_only_inputs(self):
|
||||
traces = []
|
||||
def tracer(old_state, input, new_state):
|
||||
traces.append((old_state, input, new_state))
|
||||
return None # "I only care about inputs, not outputs"
|
||||
s = SampleObject()
|
||||
s.setTrace(tracer)
|
||||
|
||||
s.go1()
|
||||
self.assertEqual(traces, [("begin", "go1", "middle"),
|
||||
])
|
||||
|
||||
s.go2()
|
||||
self.assertEqual(traces, [("begin", "go1", "middle"),
|
||||
("middle", "go2", "end"),
|
||||
])
|
||||
s.setTrace(None)
|
||||
s.back()
|
||||
self.assertEqual(traces, [("begin", "go1", "middle"),
|
||||
("middle", "go2", "end"),
|
||||
])
|
||||
s.go2()
|
||||
self.assertEqual(traces, [("begin", "go1", "middle"),
|
||||
("middle", "go2", "end"),
|
||||
])
|
||||
|
||||
def test_inputs_and_outputs(self):
|
||||
traces = []
|
||||
def tracer(old_state, input, new_state):
|
||||
traces.append((old_state, input, new_state, None))
|
||||
def trace_outputs(output):
|
||||
traces.append((old_state, input, new_state, output))
|
||||
return trace_outputs # "I care about outputs too"
|
||||
s = SampleObject()
|
||||
s.setTrace(tracer)
|
||||
|
||||
s.go1()
|
||||
self.assertEqual(traces, [("begin", "go1", "middle", None),
|
||||
("begin", "go1", "middle", "out"),
|
||||
])
|
||||
|
||||
s.go2()
|
||||
self.assertEqual(traces, [("begin", "go1", "middle", None),
|
||||
("begin", "go1", "middle", "out"),
|
||||
("middle", "go2", "end", None),
|
||||
("middle", "go2", "end", "out"),
|
||||
])
|
||||
s.setTrace(None)
|
||||
s.back()
|
||||
self.assertEqual(traces, [("begin", "go1", "middle", None),
|
||||
("begin", "go1", "middle", "out"),
|
||||
("middle", "go2", "end", None),
|
||||
("middle", "go2", "end", "out"),
|
||||
])
|
||||
s.go2()
|
||||
self.assertEqual(traces, [("begin", "go1", "middle", None),
|
||||
("begin", "go1", "middle", "out"),
|
||||
("middle", "go2", "end", None),
|
||||
("middle", "go2", "end", "out"),
|
||||
])
|
||||
430
venv/lib/python3.9/site-packages/automat/_test/test_visualize.py
Normal file
430
venv/lib/python3.9/site-packages/automat/_test/test_visualize.py
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
from __future__ import print_function
|
||||
import functools
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from unittest import TestCase, skipIf
|
||||
|
||||
import attr
|
||||
|
||||
from .._methodical import MethodicalMachine
|
||||
|
||||
from .test_discover import isTwistedInstalled
|
||||
|
||||
|
||||
def isGraphvizModuleInstalled():
|
||||
"""
|
||||
Is the graphviz Python module installed?
|
||||
"""
|
||||
try:
|
||||
__import__("graphviz")
|
||||
except ImportError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def isGraphvizInstalled():
|
||||
"""
|
||||
Are the graphviz tools installed?
|
||||
"""
|
||||
r, w = os.pipe()
|
||||
os.close(w)
|
||||
try:
|
||||
return not subprocess.call("dot", stdin=r, shell=True)
|
||||
finally:
|
||||
os.close(r)
|
||||
|
||||
|
||||
|
||||
def sampleMachine():
|
||||
"""
|
||||
Create a sample L{MethodicalMachine} with some sample states.
|
||||
"""
|
||||
mm = MethodicalMachine()
|
||||
class SampleObject(object):
|
||||
@mm.state(initial=True)
|
||||
def begin(self):
|
||||
"initial state"
|
||||
@mm.state()
|
||||
def end(self):
|
||||
"end state"
|
||||
@mm.input()
|
||||
def go(self):
|
||||
"sample input"
|
||||
@mm.output()
|
||||
def out(self):
|
||||
"sample output"
|
||||
begin.upon(go, end, [out])
|
||||
so = SampleObject()
|
||||
so.go()
|
||||
return mm
|
||||
|
||||
|
||||
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
|
||||
class ElementMakerTests(TestCase):
|
||||
"""
|
||||
L{elementMaker} generates HTML representing the specified element.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
from .._visualize import elementMaker
|
||||
self.elementMaker = elementMaker
|
||||
|
||||
def test_sortsAttrs(self):
|
||||
"""
|
||||
L{elementMaker} orders HTML attributes lexicographically.
|
||||
"""
|
||||
expected = r'<div a="1" b="2" c="3"></div>'
|
||||
self.assertEqual(expected,
|
||||
self.elementMaker("div",
|
||||
b='2',
|
||||
a='1',
|
||||
c='3'))
|
||||
|
||||
def test_quotesAttrs(self):
|
||||
"""
|
||||
L{elementMaker} quotes HTML attributes according to DOT's quoting rule.
|
||||
|
||||
See U{http://www.graphviz.org/doc/info/lang.html}, footnote 1.
|
||||
"""
|
||||
expected = r'<div a="1" b="a \" quote" c="a string"></div>'
|
||||
self.assertEqual(expected,
|
||||
self.elementMaker("div",
|
||||
b='a " quote',
|
||||
a=1,
|
||||
c="a string"))
|
||||
|
||||
def test_noAttrs(self):
|
||||
"""
|
||||
L{elementMaker} should render an element with no attributes.
|
||||
"""
|
||||
expected = r'<div ></div>'
|
||||
self.assertEqual(expected, self.elementMaker("div"))
|
||||
|
||||
|
||||
@attr.s
|
||||
class HTMLElement(object):
|
||||
"""Holds an HTML element, as created by elementMaker."""
|
||||
name = attr.ib()
|
||||
children = attr.ib()
|
||||
attributes = attr.ib()
|
||||
|
||||
|
||||
def findElements(element, predicate):
|
||||
"""
|
||||
Recursively collect all elements in an L{HTMLElement} tree that
|
||||
match the optional predicate.
|
||||
"""
|
||||
if predicate(element):
|
||||
return [element]
|
||||
elif isLeaf(element):
|
||||
return []
|
||||
|
||||
return [result
|
||||
for child in element.children
|
||||
for result in findElements(child, predicate)]
|
||||
|
||||
|
||||
def isLeaf(element):
|
||||
"""
|
||||
This HTML element is actually leaf node.
|
||||
"""
|
||||
return not isinstance(element, HTMLElement)
|
||||
|
||||
|
||||
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
|
||||
class TableMakerTests(TestCase):
|
||||
"""
|
||||
Tests that ensure L{tableMaker} generates HTML tables usable as
|
||||
labels in DOT graphs.
|
||||
|
||||
For more information, read the "HTML-Like Labels" section of
|
||||
U{http://www.graphviz.org/doc/info/shapes.html}.
|
||||
"""
|
||||
|
||||
def fakeElementMaker(self, name, *children, **attributes):
|
||||
return HTMLElement(name=name, children=children, attributes=attributes)
|
||||
|
||||
def setUp(self):
|
||||
from .._visualize import tableMaker
|
||||
|
||||
self.inputLabel = "input label"
|
||||
self.port = "the port"
|
||||
self.tableMaker = functools.partial(tableMaker,
|
||||
_E=self.fakeElementMaker)
|
||||
|
||||
def test_inputLabelRow(self):
|
||||
"""
|
||||
The table returned by L{tableMaker} always contains the input
|
||||
symbol label in its first row, and that row contains one cell
|
||||
with a port attribute set to the provided port.
|
||||
"""
|
||||
|
||||
def hasPort(element):
|
||||
return (not isLeaf(element)
|
||||
and element.attributes.get("port") == self.port)
|
||||
|
||||
for outputLabels in ([], ["an output label"]):
|
||||
table = self.tableMaker(self.inputLabel, outputLabels,
|
||||
port=self.port)
|
||||
self.assertGreater(len(table.children), 0)
|
||||
inputLabelRow = table.children[0]
|
||||
|
||||
portCandidates = findElements(table, hasPort)
|
||||
|
||||
self.assertEqual(len(portCandidates), 1)
|
||||
self.assertEqual(portCandidates[0].name, "td")
|
||||
self.assertEqual(findElements(inputLabelRow, isLeaf),
|
||||
[self.inputLabel])
|
||||
|
||||
def test_noOutputLabels(self):
|
||||
"""
|
||||
L{tableMaker} does not add a colspan attribute to the input
|
||||
label's cell or a second row if there no output labels.
|
||||
"""
|
||||
table = self.tableMaker("input label", (), port=self.port)
|
||||
self.assertEqual(len(table.children), 1)
|
||||
(inputLabelRow,) = table.children
|
||||
self.assertNotIn("colspan", inputLabelRow.attributes)
|
||||
|
||||
def test_withOutputLabels(self):
|
||||
"""
|
||||
L{tableMaker} adds a colspan attribute to the input label's cell
|
||||
equal to the number of output labels and a second row that
|
||||
contains the output labels.
|
||||
"""
|
||||
table = self.tableMaker(self.inputLabel, ("output label 1",
|
||||
"output label 2"),
|
||||
port=self.port)
|
||||
|
||||
self.assertEqual(len(table.children), 2)
|
||||
inputRow, outputRow = table.children
|
||||
|
||||
def hasCorrectColspan(element):
|
||||
return (not isLeaf(element)
|
||||
and element.name == "td"
|
||||
and element.attributes.get('colspan') == "2")
|
||||
|
||||
self.assertEqual(len(findElements(inputRow, hasCorrectColspan)),
|
||||
1)
|
||||
self.assertEqual(findElements(outputRow, isLeaf), ["output label 1",
|
||||
"output label 2"])
|
||||
|
||||
|
||||
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
|
||||
@skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.")
|
||||
class IntegrationTests(TestCase):
|
||||
"""
|
||||
Tests which make sure Graphviz can understand the output produced by
|
||||
Automat.
|
||||
"""
|
||||
|
||||
def test_validGraphviz(self):
|
||||
"""
|
||||
L{graphviz} emits valid graphviz data.
|
||||
"""
|
||||
p = subprocess.Popen("dot", stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE)
|
||||
out, err = p.communicate("".join(sampleMachine().asDigraph())
|
||||
.encode("utf-8"))
|
||||
self.assertEqual(p.returncode, 0)
|
||||
|
||||
|
||||
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
|
||||
class SpotChecks(TestCase):
|
||||
"""
|
||||
Tests to make sure that the output contains salient features of the machine
|
||||
being generated.
|
||||
"""
|
||||
|
||||
def test_containsMachineFeatures(self):
|
||||
"""
|
||||
The output of L{graphviz} should contain the names of the states,
|
||||
inputs, outputs in the state machine.
|
||||
"""
|
||||
gvout = "".join(sampleMachine().asDigraph())
|
||||
self.assertIn("begin", gvout)
|
||||
self.assertIn("end", gvout)
|
||||
self.assertIn("go", gvout)
|
||||
self.assertIn("out", gvout)
|
||||
|
||||
|
||||
class RecordsDigraphActions(object):
|
||||
"""
|
||||
Records calls made to L{FakeDigraph}.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.renderCalls = []
|
||||
self.saveCalls = []
|
||||
|
||||
|
||||
class FakeDigraph(object):
|
||||
"""
|
||||
A fake L{graphviz.Digraph}. Instantiate it with a
|
||||
L{RecordsDigraphActions}.
|
||||
"""
|
||||
|
||||
def __init__(self, recorder):
|
||||
self._recorder = recorder
|
||||
|
||||
def render(self, **kwargs):
|
||||
self._recorder.renderCalls.append(kwargs)
|
||||
|
||||
def save(self, **kwargs):
|
||||
self._recorder.saveCalls.append(kwargs)
|
||||
|
||||
|
||||
class FakeMethodicalMachine(object):
|
||||
"""
|
||||
A fake L{MethodicalMachine}. Instantiate it with a L{FakeDigraph}
|
||||
"""
|
||||
|
||||
def __init__(self, digraph):
|
||||
self._digraph = digraph
|
||||
|
||||
def asDigraph(self):
|
||||
return self._digraph
|
||||
|
||||
|
||||
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
|
||||
@skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.")
|
||||
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
|
||||
class VisualizeToolTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.digraphRecorder = RecordsDigraphActions()
|
||||
self.fakeDigraph = FakeDigraph(self.digraphRecorder)
|
||||
|
||||
self.fakeProgname = 'tool-test'
|
||||
self.fakeSysPath = ['ignored']
|
||||
self.collectedOutput = []
|
||||
self.fakeFQPN = 'fake.fqpn'
|
||||
|
||||
def collectPrints(self, *args):
|
||||
self.collectedOutput.append(' '.join(args))
|
||||
|
||||
def fakeFindMachines(self, fqpn):
|
||||
yield fqpn, FakeMethodicalMachine(self.fakeDigraph)
|
||||
|
||||
def tool(self,
|
||||
progname=None,
|
||||
argv=None,
|
||||
syspath=None,
|
||||
findMachines=None,
|
||||
print=None):
|
||||
from .._visualize import tool
|
||||
return tool(
|
||||
_progname=progname or self.fakeProgname,
|
||||
_argv=argv or [self.fakeFQPN],
|
||||
_syspath=syspath or self.fakeSysPath,
|
||||
_findMachines=findMachines or self.fakeFindMachines,
|
||||
_print=print or self.collectPrints)
|
||||
|
||||
def test_checksCurrentDirectory(self):
|
||||
"""
|
||||
L{tool} adds '' to sys.path to ensure
|
||||
L{automat._discover.findMachines} searches the current
|
||||
directory.
|
||||
"""
|
||||
self.tool(argv=[self.fakeFQPN])
|
||||
self.assertEqual(self.fakeSysPath[0], '')
|
||||
|
||||
def test_quietHidesOutput(self):
|
||||
"""
|
||||
Passing -q/--quiet hides all output.
|
||||
"""
|
||||
self.tool(argv=[self.fakeFQPN, '--quiet'])
|
||||
self.assertFalse(self.collectedOutput)
|
||||
self.tool(argv=[self.fakeFQPN, '-q'])
|
||||
self.assertFalse(self.collectedOutput)
|
||||
|
||||
def test_onlySaveDot(self):
|
||||
"""
|
||||
Passing an empty string for --image-directory/-i disables
|
||||
rendering images.
|
||||
"""
|
||||
for arg in ('--image-directory', '-i'):
|
||||
self.digraphRecorder.reset()
|
||||
self.collectedOutput = []
|
||||
|
||||
self.tool(argv=[self.fakeFQPN, arg, ''])
|
||||
self.assertFalse(any("image" in line
|
||||
for line in self.collectedOutput))
|
||||
|
||||
self.assertEqual(len(self.digraphRecorder.saveCalls), 1)
|
||||
(call,) = self.digraphRecorder.saveCalls
|
||||
self.assertEqual("{}.dot".format(self.fakeFQPN),
|
||||
call['filename'])
|
||||
|
||||
self.assertFalse(self.digraphRecorder.renderCalls)
|
||||
|
||||
def test_saveOnlyImage(self):
|
||||
"""
|
||||
Passing an empty string for --dot-directory/-d disables saving dot
|
||||
files.
|
||||
"""
|
||||
for arg in ('--dot-directory', '-d'):
|
||||
self.digraphRecorder.reset()
|
||||
self.collectedOutput = []
|
||||
self.tool(argv=[self.fakeFQPN, arg, ''])
|
||||
|
||||
self.assertFalse(any("dot" in line
|
||||
for line in self.collectedOutput))
|
||||
|
||||
self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
|
||||
(call,) = self.digraphRecorder.renderCalls
|
||||
self.assertEqual("{}.dot".format(self.fakeFQPN),
|
||||
call['filename'])
|
||||
self.assertTrue(call['cleanup'])
|
||||
|
||||
self.assertFalse(self.digraphRecorder.saveCalls)
|
||||
|
||||
def test_saveDotAndImagesInDifferentDirectories(self):
|
||||
"""
|
||||
Passing different directories to --image-directory and --dot-directory
|
||||
writes images and dot files to those directories.
|
||||
"""
|
||||
imageDirectory = 'image'
|
||||
dotDirectory = 'dot'
|
||||
self.tool(argv=[self.fakeFQPN,
|
||||
'--image-directory', imageDirectory,
|
||||
'--dot-directory', dotDirectory])
|
||||
|
||||
self.assertTrue(any("image" in line
|
||||
for line in self.collectedOutput))
|
||||
self.assertTrue(any("dot" in line
|
||||
for line in self.collectedOutput))
|
||||
|
||||
self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
|
||||
(renderCall,) = self.digraphRecorder.renderCalls
|
||||
self.assertEqual(renderCall["directory"], imageDirectory)
|
||||
self.assertTrue(renderCall['cleanup'])
|
||||
|
||||
self.assertEqual(len(self.digraphRecorder.saveCalls), 1)
|
||||
(saveCall,) = self.digraphRecorder.saveCalls
|
||||
self.assertEqual(saveCall["directory"], dotDirectory)
|
||||
|
||||
def test_saveDotAndImagesInSameDirectory(self):
|
||||
"""
|
||||
Passing the same directory to --image-directory and --dot-directory
|
||||
writes images and dot files to that one directory.
|
||||
"""
|
||||
directory = 'imagesAndDot'
|
||||
self.tool(argv=[self.fakeFQPN,
|
||||
'--image-directory', directory,
|
||||
'--dot-directory', directory])
|
||||
|
||||
self.assertTrue(any("image and dot" in line
|
||||
for line in self.collectedOutput))
|
||||
|
||||
self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
|
||||
(renderCall,) = self.digraphRecorder.renderCalls
|
||||
self.assertEqual(renderCall["directory"], directory)
|
||||
self.assertFalse(renderCall['cleanup'])
|
||||
|
||||
self.assertFalse(len(self.digraphRecorder.saveCalls))
|
||||
Loading…
Add table
Add a link
Reference in a new issue