Skip to content

Commit 9854f91

Browse files
authored
Merge pull request #191 from userlocalhost/bugfix/issue-176/evaluate-yaql-context
Fixed a problem not to be able to parse input value which is type of dict in array in some YAQL operators.
2 parents dc85df7 + 4bc5575 commit 9854f91

File tree

6 files changed

+161
-61
lines changed

6 files changed

+161
-61
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ Fixed
3939
* Check syntax on with items task to ensure action is indented correctly. Fixes #184 (bug fix)
4040
* Fix variable inspection where ctx().get() method calls are identified as errors.
4141
Fixes StackStorm/st2#4866 (bug fix)
42+
* Fix a problem of TypeError orccuring when a list (or dict) value that contains unhashable typed
43+
value (list or dict) is passed in some YAQL operators (e.g. distinct()). Fixes #176 (bug fix)
44+
Contributed by Hiroyasu Ohyama (@userlocalhost)
4245

4346
1.0.0
4447
-----

orquesta/expressions/functions/workflow.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import collections
16+
1517
from orquesta import constants
1618
from orquesta import exceptions as exc
1719
from orquesta import statuses
@@ -115,8 +117,8 @@ def item_(context, key=None):
115117
if not key:
116118
return current_item
117119

118-
if not isinstance(current_item, dict):
119-
raise exc.ExpressionEvaluationException('Item is not type of dict.')
120+
if not isinstance(current_item, collections.Mapping):
121+
raise exc.ExpressionEvaluationException('Item is not type of collections.Mapping.')
120122

121123
if key not in current_item:
122124
raise exc.ExpressionEvaluationException('Item does not have key "%s".' % key)

orquesta/expressions/yql.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import yaql
2222
import yaql.language.exceptions as yaql_exc
23+
import yaql.language.utils as yaql_utils
2324

2425
from orquesta import exceptions as exc
2526
from orquesta.expressions import base as expr_base
@@ -79,7 +80,15 @@ class YAQLEvaluator(expr_base.Evaluator):
7980
@classmethod
8081
def contextualize(cls, data):
8182
ctx = cls._root_ctx.create_child_context()
82-
ctx['__vars'] = data or {}
83+
84+
# Some yaql expressions (e.g. distinct()) refer to hash value of variable.
85+
# But some built-in Python type values (e.g. list and dict) don't have __hash__() method.
86+
# The convert_input_data method parses specified variable and convert it to hashable one.
87+
if isinstance(data, yaql_utils.SequenceType) or isinstance(data, yaql_utils.MappingType):
88+
ctx['__vars'] = yaql_utils.convert_input_data(data)
89+
else:
90+
ctx['__vars'] = data or {}
91+
8392
ctx['__state'] = ctx['__vars'].get('__state')
8493
ctx['__current_task'] = ctx['__vars'].get('__current_task')
8594
ctx['__current_item'] = ctx['__vars'].get('__current_item')

orquesta/tests/unit/conducting/native/test_task_rendering_for_with_items.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def test_bad_item_type(self):
8282
'type': 'error',
8383
'message': (
8484
'YaqlEvaluationException: Unable to evaluate expression \'<% item(x) %>\'. '
85-
'ExpressionEvaluationException: Item is not type of dict.'
85+
'ExpressionEvaluationException: Item is not type of collections.Mapping.'
8686
),
8787
'task_id': 'task1',
8888
'route': 0

orquesta/tests/unit/conducting/test_workflow_conductor_data_flow.py

Lines changed: 122 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -20,47 +20,84 @@
2020
from orquesta.specs import native as native_specs
2121
from orquesta import statuses
2222
from orquesta.tests.unit import base as test_base
23+
import yaql.language.utils as yaql_utils
2324

2425

2526
class WorkflowConductorDataFlowTest(test_base.WorkflowConductorTest):
2627

27-
def _prep_conductor(self, context=None, inputs=None, status=None):
28-
wf_def = """
29-
version: 1.0
30-
31-
description: A basic sequential workflow.
32-
33-
input:
34-
- a1
35-
- b1: <% ctx().a1 %>
36-
37-
vars:
38-
- a2: <% ctx().b1 %>
39-
- b2: <% ctx().a2 %>
40-
41-
output:
42-
- a5: <% ctx().b4 %>
43-
- b5: <% ctx().a5 %>
44-
45-
tasks:
46-
task1:
47-
action: core.noop
48-
next:
49-
- when: <% succeeded() %>
50-
publish:
51-
- a3: <% ctx().b2 %>
52-
- b3: <% ctx().a3 %>
53-
do: task2
54-
task2:
55-
action: core.noop
56-
next:
57-
- when: <% succeeded() %>
58-
publish: a4=<% ctx().b3 %> b4=<% ctx().a4 %>
59-
do: task3
60-
task3:
61-
action: core.noop
62-
"""
63-
28+
wf_def_yaql = """
29+
version: 1.0
30+
31+
description: A basic sequential workflow.
32+
33+
input:
34+
- a1
35+
- b1: <% ctx().a1 %>
36+
37+
vars:
38+
- a2: <% ctx().b1 %>
39+
- b2: <% ctx().a2 %>
40+
41+
output:
42+
- a5: <% ctx().b4 %>
43+
- b5: <% ctx().a5 %>
44+
45+
tasks:
46+
task1:
47+
action: core.noop
48+
next:
49+
- when: <% succeeded() %>
50+
publish:
51+
- a3: <% ctx().b2 %>
52+
- b3: <% ctx().a3 %>
53+
do: task2
54+
task2:
55+
action: core.noop
56+
next:
57+
- when: <% succeeded() %>
58+
publish: a4=<% ctx().b3 %> b4=<% ctx().a4 %>
59+
do: task3
60+
task3:
61+
action: core.noop
62+
"""
63+
64+
wf_def_jinja = """
65+
version: 1.0
66+
67+
description: A basic sequential workflow.
68+
69+
input:
70+
- a1
71+
- b1: '{{ ctx("a1") }}'
72+
73+
vars:
74+
- a2: '{{ ctx("b1") }}'
75+
- b2: '{{ ctx("a2") }}'
76+
77+
output:
78+
- a5: '{{ ctx("b4") }}'
79+
- b5: '{{ ctx("a5") }}'
80+
81+
tasks:
82+
task1:
83+
action: core.noop
84+
next:
85+
- when: '{{ succeeded() }}'
86+
publish:
87+
- a3: '{{ ctx("b2") }}'
88+
- b3: '{{ ctx("a3") }}'
89+
do: task2
90+
task2:
91+
action: core.noop
92+
next:
93+
- when: '{{ succeeded() }}'
94+
publish: a4='{{ ctx("b3") }}' b4='{{ ctx("a4") }}'
95+
do: task3
96+
task3:
97+
action: core.noop
98+
"""
99+
100+
def _prep_conductor(self, wf_def, context=None, inputs=None, status=None):
64101
spec = native_specs.WorkflowSpec(wf_def)
65102
self.assertDictEqual(spec.inspect(), {})
66103

@@ -76,33 +113,52 @@ def _prep_conductor(self, context=None, inputs=None, status=None):
76113

77114
return conductor
78115

116+
def _get_combined_value(self, callstack_depth=0):
117+
# This returns dict typed value all Python built-in type values
118+
# which orquesta spec could accept.
119+
if callstack_depth < 2:
120+
return {
121+
'null': None,
122+
'integer_positive': 123,
123+
'integer_negative': -123,
124+
'number_positive': 99.99,
125+
'number_negative': -99.99,
126+
'string': 'xyz',
127+
'boolean_true': True,
128+
'boolean_false': False,
129+
'array': list(self._get_combined_value(callstack_depth + 1).values()),
130+
'object': self._get_combined_value(callstack_depth + 1),
131+
}
132+
else:
133+
return {}
134+
135+
def _assert_data_flow(self, inputs, expected_output):
136+
# This assert method checks input value would be handled and published
137+
# as an expected type and value with both YAQL and Jinja expressions.
138+
for wf_def in [self.wf_def_jinja, self.wf_def_yaql]:
139+
conductor = self._prep_conductor(wf_def, inputs=inputs, status=statuses.RUNNING)
140+
141+
for i in range(1, len(conductor.spec.tasks) + 1):
142+
task_name = 'task' + str(i)
143+
forward_statuses = [statuses.RUNNING, statuses.SUCCEEDED]
144+
self.forward_task_statuses(conductor, task_name, forward_statuses)
145+
146+
# Render workflow output and checkout workflow status and output.
147+
conductor.render_workflow_output()
148+
self.assertEqual(conductor.get_workflow_status(), statuses.SUCCEEDED)
149+
self.assertDictEqual(conductor.get_workflow_output(), expected_output)
150+
79151
def assert_data_flow(self, input_value):
80152
inputs = {'a1': input_value}
81153
expected_output = {'a5': inputs['a1'], 'b5': inputs['a1']}
82-
conductor = self._prep_conductor(inputs=inputs, status=statuses.RUNNING)
83154

84-
for i in range(1, len(conductor.spec.tasks) + 1):
85-
task_name = 'task' + str(i)
86-
self.forward_task_statuses(conductor, task_name, [statuses.RUNNING, statuses.SUCCEEDED])
87-
88-
# Render workflow output and checkout workflow status and output.
89-
conductor.render_workflow_output()
90-
self.assertEqual(conductor.get_workflow_status(), statuses.SUCCEEDED)
91-
self.assertDictEqual(conductor.get_workflow_output(), expected_output)
155+
self._assert_data_flow(inputs, expected_output)
92156

93157
def assert_unicode_data_flow(self, input_value):
94158
inputs = {u'a1': unicode(input_value, 'utf8') if six.PY2 else input_value}
95159
expected_output = {u'a5': inputs['a1'], u'b5': inputs['a1']}
96-
conductor = self._prep_conductor(inputs=inputs, status=statuses.RUNNING)
97-
98-
for i in range(1, len(conductor.spec.tasks) + 1):
99-
task_name = 'task' + str(i)
100-
self.forward_task_statuses(conductor, task_name, [statuses.RUNNING, statuses.SUCCEEDED])
101160

102-
# Render workflow output and checkout workflow status and output.
103-
conductor.render_workflow_output()
104-
self.assertEqual(conductor.get_workflow_status(), statuses.SUCCEEDED)
105-
self.assertDictEqual(conductor.get_workflow_output(), expected_output)
161+
self._assert_data_flow(inputs, expected_output)
106162

107163
def test_data_flow_string(self):
108164
self.assert_data_flow('xyz')
@@ -119,11 +175,20 @@ def test_data_flow_boolean(self):
119175
self.assert_data_flow(True)
120176
self.assert_data_flow(False)
121177

178+
def test_data_flow_none(self):
179+
self.assert_data_flow(None)
180+
122181
def test_data_flow_dict(self):
123-
self.assert_data_flow({'x': 123, 'y': 'abc'})
182+
mapping_typed_data = self._get_combined_value()
183+
184+
self.assertIsInstance(mapping_typed_data, yaql_utils.MappingType)
185+
self.assert_data_flow(mapping_typed_data)
124186

125187
def test_data_flow_list(self):
126-
self.assert_data_flow([123, 'abc', True])
188+
sequence_typed_data = list(self._get_combined_value().values())
189+
190+
self.assertIsInstance(sequence_typed_data, yaql_utils.SequenceType)
191+
self.assert_data_flow(sequence_typed_data)
127192

128193
def test_data_flow_unicode(self):
129194
self.assert_unicode_data_flow('光合作用')

orquesta/tests/unit/expressions/test_facade_yaql_evaluate.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,24 @@ def test_custom_function_failure(self):
269269
expr_base.evaluate,
270270
expr
271271
)
272+
273+
def test_distinct_operator(self):
274+
test_cases = [
275+
{
276+
'expr': '<% ctx(val).distinct() %>',
277+
'input': {'val': [1, 2, 3, 1]},
278+
'expect': [1, 2, 3]
279+
},
280+
{
281+
'expr': '<% ctx(val).distinct() %>',
282+
'input': {'val': [{'a': 1}, {'b': 2}, {'a': 1}]},
283+
'expect': [{'a': 1}, {'b': 2}]
284+
},
285+
{
286+
'expr': '<% ctx(val).distinct($[1]) %>',
287+
'input': {'val': [['a', 1], ['b', 2], ['c', 1], ['a', 3]]},
288+
'expect': [['a', 1], ['b', 2], ['a', 3]]
289+
}
290+
]
291+
for case in test_cases:
292+
self.assertEqual(case['expect'], expr_base.evaluate(case['expr'], case['input']))

0 commit comments

Comments
 (0)