Squashed 'SpiffWorkflow/' changes from 2ca6ebf80..7b39b2235

7b39b2235 Merge pull request #300 from sartography/bugfix/remove-minidom-dependency
0642d48b1 remove minidom

git-subtree-dir: SpiffWorkflow
git-subtree-split: 7b39b223562eb510dd68c8d451922721ebb721a7
This commit is contained in:
Dan 2023-02-27 14:06:23 -05:00
parent 8e3b905b07
commit 798984a23c
6 changed files with 133 additions and 123 deletions

View File

@ -16,26 +16,29 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
import re
import xml.dom.minidom as minidom
from .. import operators from .. import operators
from ..specs.Simple import Simple from ..specs.Simple import Simple
from ..specs.WorkflowSpec import WorkflowSpec from ..specs.WorkflowSpec import WorkflowSpec
from ..exceptions import StorageException from ..exceptions import SpiffWorkflowException
from .base import Serializer, spec_map, op_map from .base import Serializer, spec_map, op_map
# Create a list of tag names out of the spec names. # Create a list of tag names out of the spec names.
_spec_map = spec_map() _spec_map = spec_map()
_op_map = op_map() _op_map = op_map()
_exc = StorageException
class XMLParserExcetion(SpiffWorkflowException):
pass
class XmlSerializer(Serializer): class XmlSerializer(Serializer):
"""Parses XML into a WorkflowSpec object."""
""" # Note: This is not a serializer. It is a parser for Spiff's XML format
Parses XML into a WorkflowSpec object. # However, it is too disruptive to rename everything that uses it.
"""
def raise_parser_exception(self, message):
raise XMLParserExcetion(message)
def deserialize_assign(self, workflow, start_node): def deserialize_assign(self, workflow, start_node):
""" """
@ -43,17 +46,18 @@ class XmlSerializer(Serializer):
start_node -- the xml node (xml.dom.minidom.Node) start_node -- the xml node (xml.dom.minidom.Node)
""" """
name = start_node.getAttribute('name') name = start_node.attrib.get('name')
attrib = start_node.getAttribute('field') attrib = start_node.attrib.get('field')
value = start_node.getAttribute('value') value = start_node.attrib.get('value')
kwargs = {} kwargs = {}
if name == '': if name == '':
_exc('name attribute required') self.raise_parser_exception('name attribute required')
if attrib != '' and value != '': if attrib is not None and value is not None:
_exc('Both, field and right-value attributes found') self.raise_parser_exception('Both, field and right-value attributes found')
elif attrib == '' and value == '': elif attrib is None and value is None:
_exc('field or value attribute required') self.raise_parser_exception('field or value attribute required')
elif value != '': elif value is not None:
kwargs['right'] = value kwargs['right'] = value
else: else:
kwargs['right_attribute'] = attrib kwargs['right_attribute'] = attrib
@ -65,8 +69,8 @@ class XmlSerializer(Serializer):
start_node -- the xml node (xml.dom.minidom.Node) start_node -- the xml node (xml.dom.minidom.Node)
""" """
name = start_node.getAttribute('name') name = start_node.attrib.get('name')
value = start_node.getAttribute('value') value = start_node.attrib.get('value')
return name, value return name, value
def deserialize_assign_list(self, workflow, start_node): def deserialize_assign_list(self, workflow, start_node):
@ -78,13 +82,13 @@ class XmlSerializer(Serializer):
""" """
# Collect all information. # Collect all information.
assignments = [] assignments = []
for node in start_node.childNodes: for node in start_node.getchildren():
if node.nodeType != minidom.Node.ELEMENT_NODE: if not isinstance(start_node.tag, str):
continue pass
if node.nodeName.lower() == 'assign': elif node.tag.lower() == 'assign':
assignments.append(self.deserialize_assign(workflow, node)) assignments.append(self.deserialize_assign(workflow, node))
else: else:
_exc('Unknown node: %s' % node.nodeName) self.raise_parser_exception('Unknown node: %s' % node.tag)
return assignments return assignments
def deserialize_logical(self, node): def deserialize_logical(self, node):
@ -93,26 +97,26 @@ class XmlSerializer(Serializer):
node -- the xml node (xml.dom.minidom.Node) node -- the xml node (xml.dom.minidom.Node)
""" """
term1_attrib = node.getAttribute('left-field') term1_attrib = node.attrib.get('left-field')
term1_value = node.getAttribute('left-value') term1_value = node.attrib.get('left-value')
op = node.nodeName.lower() op = node.tag.lower()
term2_attrib = node.getAttribute('right-field') term2_attrib = node.attrib.get('right-field')
term2_value = node.getAttribute('right-value') term2_value = node.attrib.get('right-value')
if op not in _op_map: if op not in _op_map:
_exc('Invalid operator') self.raise_parser_exception('Invalid operator')
if term1_attrib != '' and term1_value != '': if term1_attrib is not None and term1_value is not None:
_exc('Both, left-field and left-value attributes found') self.raise_parser_exception('Both, left-field and left-value attributes found')
elif term1_attrib == '' and term1_value == '': elif term1_attrib is None and term1_value is None:
_exc('left-field or left-value attribute required') self.raise_parser_exception('left-field or left-value attribute required')
elif term1_value != '': elif term1_value is not None:
left = term1_value left = term1_value
else: else:
left = operators.Attrib(term1_attrib) left = operators.Attrib(term1_attrib)
if term2_attrib != '' and term2_value != '': if term2_attrib is not None and term2_value is not None:
_exc('Both, right-field and right-value attributes found') self.raise_parser_exception('Both, right-field and right-value attributes found')
elif term2_attrib == '' and term2_value == '': elif term2_attrib is None and term2_value is None:
_exc('right-field or right-value attribute required') self.raise_parser_exception('right-field or right-value attribute required')
elif term2_value != '': elif term2_value is not None:
right = term2_value right = term2_value
else: else:
right = operators.Attrib(term2_attrib) right = operators.Attrib(term2_attrib)
@ -128,26 +132,26 @@ class XmlSerializer(Serializer):
# Collect all information. # Collect all information.
condition = None condition = None
spec_name = None spec_name = None
for node in start_node.childNodes: for node in start_node.getchildren():
if node.nodeType != minidom.Node.ELEMENT_NODE: if not isinstance(node.tag, str):
continue pass
if node.nodeName.lower() == 'successor': elif node.tag.lower() == 'successor':
if spec_name is not None: if spec_name is not None:
_exc('Duplicate task name %s' % spec_name) self.raise_parser_exception('Duplicate task name %s' % spec_name)
if node.firstChild is None: if node.text is None:
_exc('Successor tag without a task name') self.raise_parser_exception('Successor tag without a task name')
spec_name = node.firstChild.nodeValue spec_name = node.text
elif node.nodeName.lower() in _op_map: elif node.tag.lower() in _op_map:
if condition is not None: if condition is not None:
_exc('Multiple conditions are not yet supported') self.raise_parser_exception('Multiple conditions are not yet supported')
condition = self.deserialize_logical(node) condition = self.deserialize_logical(node)
else: else:
_exc('Unknown node: %s' % node.nodeName) self.raise_parser_exception('Unknown node: %s' % node.tag)
if condition is None: if condition is None:
_exc('Missing condition in conditional statement') self.raise_parser_exception('Missing condition in conditional statement')
if spec_name is None: if spec_name is None:
_exc('A %s has no task specified' % start_node.nodeName) self.raise_parser_exception('A %s has no task specified' % start_node.tag)
return condition, spec_name return condition, spec_name
def deserialize_task_spec(self, workflow, start_node, read_specs): def deserialize_task_spec(self, workflow, start_node, read_specs):
@ -160,31 +164,31 @@ class XmlSerializer(Serializer):
start_node -- the xml structure (xml.dom.minidom.Node) start_node -- the xml structure (xml.dom.minidom.Node)
""" """
# Extract attributes from the node. # Extract attributes from the node.
nodetype = start_node.nodeName.lower() nodetype = start_node.tag.lower()
name = start_node.getAttribute('name').lower() name = start_node.attrib.get('name', '').lower()
context = start_node.getAttribute('context').lower() context = start_node.attrib.get('context', '').lower()
mutex = start_node.getAttribute('mutex').lower() mutex = start_node.attrib.get('mutex', '').lower()
cancel = start_node.getAttribute('cancel').lower() cancel = start_node.attrib.get('cancel', '').lower()
success = start_node.getAttribute('success').lower() success = start_node.attrib.get('success', '').lower()
times = start_node.getAttribute('times').lower() times = start_node.attrib.get('times', '').lower()
times_field = start_node.getAttribute('times-field').lower() times_field = start_node.attrib.get('times-field', '').lower()
threshold = start_node.getAttribute('threshold').lower() threshold = start_node.attrib.get('threshold', '').lower()
threshold_field = start_node.getAttribute('threshold-field').lower() threshold_field = start_node.attrib.get('threshold-field', '').lower()
file_name = start_node.getAttribute('file').lower() file_name = start_node.attrib.get('file', '').lower()
file_field = start_node.getAttribute('file-field').lower() file_field = start_node.attrib.get('file-field', '').lower()
kwargs = {'lock': [], kwargs = {'lock': [],
'data': {}, 'data': {},
'defines': {}, 'defines': {},
'pre_assign': [], 'pre_assign': [],
'post_assign': []} 'post_assign': []}
if nodetype not in _spec_map: if nodetype not in _spec_map:
_exc('Invalid task type "%s"' % nodetype) self.raise_parser_exception('Invalid task type "%s"' % nodetype)
if nodetype == 'start-task': if nodetype == 'start-task':
name = 'start' name = 'start'
if name == '': if name == '':
_exc('Invalid task name "%s"' % name) self.raise_parser_exception('Invalid task name "%s"' % name)
if name in read_specs: if name in read_specs:
_exc('Duplicate task name "%s"' % name) self.raise_parser_exception('Duplicate task name "%s"' % name)
if cancel != '' and cancel != '0': if cancel != '' and cancel != '0':
kwargs['cancel'] = True kwargs['cancel'] = True
if success != '' and success != '0': if success != '' and success != '0':
@ -210,55 +214,55 @@ class XmlSerializer(Serializer):
# Walk through the children of the node. # Walk through the children of the node.
successors = [] successors = []
for node in start_node.childNodes: for node in start_node.getchildren():
if node.nodeType != minidom.Node.ELEMENT_NODE: if not isinstance(node.tag, str):
continue pass
if node.nodeName == 'description': elif node.tag == 'description':
kwargs['description'] = node.firstChild.nodeValue kwargs['description'] = node.text
elif node.nodeName == 'successor' \ elif node.tag == 'successor' \
or node.nodeName == 'default-successor': or node.tag == 'default-successor':
if node.firstChild is None: if not node.text:
_exc('Empty %s tag' % node.nodeName) self.raise_parser_exception('Empty %s tag' % node.tag)
successors.append((None, node.firstChild.nodeValue)) successors.append((None, node.text))
elif node.nodeName == 'conditional-successor': elif node.tag == 'conditional-successor':
successors.append(self.deserialize_condition(workflow, node)) successors.append(self.deserialize_condition(workflow, node))
elif node.nodeName == 'define': elif node.tag == 'define':
key, value = self.deserialize_data(workflow, node) key, value = self.deserialize_data(workflow, node)
kwargs['defines'][key] = value kwargs['defines'][key] = value
# "property" tag exists for backward compatibility. # "property" tag exists for backward compatibility.
elif node.nodeName == 'data' or node.nodeName == 'property': elif node.tag == 'data' or node.tag == 'property':
key, value = self.deserialize_data(workflow, node) key, value = self.deserialize_data(workflow, node)
kwargs['data'][key] = value kwargs['data'][key] = value
elif node.nodeName == 'pre-assign': elif node.tag == 'pre-assign':
kwargs['pre_assign'].append( kwargs['pre_assign'].append(
self.deserialize_assign(workflow, node)) self.deserialize_assign(workflow, node))
elif node.nodeName == 'post-assign': elif node.tag == 'post-assign':
kwargs['post_assign'].append( kwargs['post_assign'].append(
self.deserialize_assign(workflow, node)) self.deserialize_assign(workflow, node))
elif node.nodeName == 'in': elif node.tag == 'in':
kwargs['in_assign'] = self.deserialize_assign_list( kwargs['in_assign'] = self.deserialize_assign_list(
workflow, node) workflow, node)
elif node.nodeName == 'out': elif node.tag == 'out':
kwargs['out_assign'] = self.deserialize_assign_list( kwargs['out_assign'] = self.deserialize_assign_list(
workflow, node) workflow, node)
elif node.nodeName == 'cancel': elif node.tag == 'cancel':
if node.firstChild is None: if not node.text:
_exc('Empty %s tag' % node.nodeName) self.raise_parser_exception('Empty %s tag' % node.tag)
if context == '': if context == '':
context = [] context = []
elif not isinstance(context, list): elif not isinstance(context, list):
context = [context] context = [context]
context.append(node.firstChild.nodeValue) context.append(node.text)
elif node.nodeName == 'lock': elif node.tag == 'lock':
if node.firstChild is None: if not node.text:
_exc('Empty %s tag' % node.nodeName) self.raise_parser_exception('Empty %s tag' % node.tag)
kwargs['lock'].append(node.firstChild.nodeValue) kwargs['lock'].append(node.text)
elif node.nodeName == 'pick': elif node.tag == 'pick':
if node.firstChild is None: if not node.text:
_exc('Empty %s tag' % node.nodeName) self.raise_parser_exception('Empty %s tag' % node.tag)
kwargs['choice'].append(node.firstChild.nodeValue) kwargs['choice'].append(node.text)
else: else:
_exc('Unknown node: %s' % node.nodeName) self.raise_parser_exception('Unknown node: %s' % node.tag)
# Create a new instance of the task spec. # Create a new instance of the task spec.
module = _spec_map[nodetype] module = _spec_map[nodetype]
@ -266,9 +270,9 @@ class XmlSerializer(Serializer):
spec = module(workflow, **kwargs) spec = module(workflow, **kwargs)
elif nodetype == 'multi-instance' or nodetype == 'thread-split': elif nodetype == 'multi-instance' or nodetype == 'thread-split':
if times == '' and times_field == '': if times == '' and times_field == '':
_exc('Missing "times" or "times-field" in "%s"' % name) self.raise_parser_exception('Missing "times" or "times-field" in "%s"' % name)
elif times != '' and times_field != '': elif times != '' and times_field != '':
_exc('Both, "times" and "times-field" in "%s"' % name) self.raise_parser_exception('Both, "times" and "times-field" in "%s"' % name)
spec = module(workflow, name, **kwargs) spec = module(workflow, name, **kwargs)
elif context == '': elif context == '':
spec = module(workflow, name, **kwargs) spec = module(workflow, name, **kwargs)
@ -277,34 +281,31 @@ class XmlSerializer(Serializer):
read_specs[name] = spec, successors read_specs[name] = spec, successors
def deserialize_workflow_spec(self, s_state, filename=None): def deserialize_workflow_spec(self, root_node, filename=None):
""" """
Reads the workflow from the given XML structure and returns a Reads the workflow from the given XML structure and returns a
WorkflowSpec instance. WorkflowSpec instance.
""" """
dom = minidom.parseString(s_state) name = root_node.attrib.get('name')
node = dom.getElementsByTagName('process-definition')[0]
name = node.getAttribute('name')
if name == '': if name == '':
_exc('%s without a name attribute' % node.nodeName) self.raise_parser_exception('%s without a name attribute' % root_node.tag)
# Read all task specs and create a list of successors. # Read all task specs and create a list of successors.
workflow_spec = WorkflowSpec(name, filename) workflow_spec = WorkflowSpec(name, filename)
del workflow_spec.task_specs['Start'] del workflow_spec.task_specs['Start']
end = Simple(workflow_spec, 'End'), [] end = Simple(workflow_spec, 'End'), []
read_specs = dict(end=end) read_specs = dict(end=end)
for child_node in node.childNodes: for child_node in root_node.getchildren():
if child_node.nodeType != minidom.Node.ELEMENT_NODE: if not isinstance(child_node.tag, str):
continue pass
if child_node.nodeName == 'name': elif child_node.tag == 'name':
workflow_spec.name = child_node.firstChild.nodeValue workflow_spec.name = child_node.text
elif child_node.nodeName == 'description': elif child_node.tag == 'description':
workflow_spec.description = child_node.firstChild.nodeValue workflow_spec.description = child_node.text
elif child_node.nodeName.lower() in _spec_map: elif child_node.tag.lower() in _spec_map:
self.deserialize_task_spec( self.deserialize_task_spec(workflow_spec, child_node, read_specs)
workflow_spec, child_node, read_specs)
else: else:
_exc('Unknown node: %s' % child_node.nodeName) self.raise_parser_exception('Unknown node: %s' % child_node.tag)
# Remove the default start-task from the workflow. # Remove the default start-task from the workflow.
workflow_spec.start = read_specs['start'][0] workflow_spec.start = read_specs['start'][0]
@ -314,7 +315,7 @@ class XmlSerializer(Serializer):
spec, successors = read_specs[name] spec, successors = read_specs[name]
for condition, successor_name in successors: for condition, successor_name in successors:
if successor_name not in read_specs: if successor_name not in read_specs:
_exc('Unknown successor: "%s"' % successor_name) self.raise_parser_exception('Unknown successor: "%s"' % successor_name)
successor, foo = read_specs[successor_name] successor, foo = read_specs[successor_name]
if condition is None: if condition is None:
spec.connect(successor) spec.connect(successor)

View File

@ -18,6 +18,8 @@
# 02110-1301 USA # 02110-1301 USA
import os import os
from lxml import etree
from .StartTask import StartTask from .StartTask import StartTask
from .base import TaskSpec from .base import TaskSpec
from ..task import TaskState from ..task import TaskState
@ -93,9 +95,8 @@ class SubWorkflow(TaskSpec):
file_name = valueof(my_task, self.file) file_name = valueof(my_task, self.file)
serializer = XmlSerializer() serializer = XmlSerializer()
with open(file_name) as fp: with open(file_name) as fp:
xml = fp.read() xml = etree.parse(fp).getroot()
wf_spec = WorkflowSpec.deserialize( wf_spec = WorkflowSpec.deserialize(serializer, xml, filename=file_name)
serializer, xml, filename=file_name)
outer_workflow = my_task.workflow.outer_workflow outer_workflow = my_task.workflow.outer_workflow
return Workflow(wf_spec, parent=outer_workflow) return Workflow(wf_spec, parent=outer_workflow)

View File

@ -6,6 +6,8 @@ import unittest
import os import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from lxml import etree
from SpiffWorkflow.specs.WorkflowSpec import WorkflowSpec from SpiffWorkflow.specs.WorkflowSpec import WorkflowSpec
from SpiffWorkflow.task import Task from SpiffWorkflow.task import Task
from SpiffWorkflow.serializer.prettyxml import XmlSerializer from SpiffWorkflow.serializer.prettyxml import XmlSerializer
@ -64,7 +66,7 @@ class PatternTest(unittest.TestCase):
# Test patterns that are defined in XML format. # Test patterns that are defined in XML format.
if filename.endswith('.xml'): if filename.endswith('.xml'):
with open(filename) as fp: with open(filename) as fp:
xml = fp.read() xml = etree.parse(fp).getroot()
serializer = XmlSerializer() serializer = XmlSerializer()
wf_spec = WorkflowSpec.deserialize( wf_spec = WorkflowSpec.deserialize(
serializer, xml, filename=filename) serializer, xml, filename=filename)

View File

@ -6,6 +6,8 @@ import os
data_dir = os.path.join(os.path.dirname(__file__), 'data') data_dir = os.path.join(os.path.dirname(__file__), 'data')
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from lxml import etree
from SpiffWorkflow.workflow import Workflow from SpiffWorkflow.workflow import Workflow
from SpiffWorkflow.specs.Cancel import Cancel from SpiffWorkflow.specs.Cancel import Cancel
from SpiffWorkflow.specs.Simple import Simple from SpiffWorkflow.specs.Simple import Simple
@ -27,7 +29,7 @@ class WorkflowTest(unittest.TestCase):
""" """
xml_file = os.path.join(data_dir, 'spiff', 'workflow1.xml') xml_file = os.path.join(data_dir, 'spiff', 'workflow1.xml')
with open(xml_file) as fp: with open(xml_file) as fp:
xml = fp.read() xml = etree.parse(fp).getroot()
wf_spec = WorkflowSpec.deserialize(XmlSerializer(), xml) wf_spec = WorkflowSpec.deserialize(XmlSerializer(), xml)
workflow = Workflow(wf_spec) workflow = Workflow(wf_spec)

View File

@ -4,6 +4,8 @@ import sys
import unittest import unittest
import os import os
from lxml import etree
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))
from SpiffWorkflow.specs.WorkflowSpec import WorkflowSpec from SpiffWorkflow.specs.WorkflowSpec import WorkflowSpec
@ -30,7 +32,7 @@ class TaskSpecTest(unittest.TestCase):
os.path.dirname(__file__), '..', 'data', 'spiff', folder, f) os.path.dirname(__file__), '..', 'data', 'spiff', folder, f)
serializer = XmlSerializer() serializer = XmlSerializer()
with open(file) as fp: with open(file) as fp:
xml = fp.read() xml = etree.parse(fp).getroot()
self.wf_spec = WorkflowSpec.deserialize( self.wf_spec = WorkflowSpec.deserialize(
serializer, xml, filename=file) serializer, xml, filename=file)
self.workflow = Workflow(self.wf_spec) self.workflow = Workflow(self.wf_spec)

View File

@ -8,6 +8,8 @@ import unittest
data_dir = os.path.join(os.path.dirname(__file__), '..', 'data') data_dir = os.path.join(os.path.dirname(__file__), '..', 'data')
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))
from lxml import etree
import pickle import pickle
from random import randint from random import randint
try: try:
@ -82,7 +84,7 @@ class WorkflowSpecTest(unittest.TestCase):
# Read a complete workflow spec. # Read a complete workflow spec.
xml_file = os.path.join(data_dir, 'spiff', 'workflow1.xml') xml_file = os.path.join(data_dir, 'spiff', 'workflow1.xml')
with open(xml_file) as fp: with open(xml_file) as fp:
xml = fp.read() xml = etree.parse(fp).getroot()
path_file = os.path.splitext(xml_file)[0] + '.path' path_file = os.path.splitext(xml_file)[0] + '.path'
with open(path_file) as fp: with open(path_file) as fp:
expected_path = fp.read().strip().split('\n') expected_path = fp.read().strip().split('\n')