Skip to content

Commit

Permalink
Merge pull request #183 from botstory/hotfix/random-order-of-switch
Browse files Browse the repository at this point in the history
Hotfix/random order of switch
  • Loading branch information
hyzhak committed Apr 10, 2017
2 parents 5041ce8 + a397ffb commit b6d175e
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 25 deletions.
11 changes: 6 additions & 5 deletions botstory/ast/forking.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from botstory import ast, matchers
from collections import OrderedDict
import logging
import json

Expand Down Expand Up @@ -75,7 +76,9 @@ def __repr__(self):
@matchers.matcher()
class Switch:
def __init__(self, cases):
self.cases = cases
assert isinstance(cases, list)
assert all(isinstance(i, tuple) for i in cases)
self.cases = OrderedDict(cases)

def validate(self, message):
for case_id, validator in self.cases.items():
Expand All @@ -91,10 +94,8 @@ def serialize(self):

@staticmethod
def deserialize(data):
return Switch({
case['id']: matchers.deserialize(case['data'])
for case in data
})
return Switch([(case['id'], matchers.deserialize(case['data']))
for case in data])

def to_json(self):
return {
Expand Down
64 changes: 52 additions & 12 deletions botstory/ast/forking_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from . import forking
from .. import matchers
from ..middlewares import location, text
from ..middlewares import any, location, text
from ..utils import answer, SimpleTrigger

logger = logging.getLogger(__name__)
Expand All @@ -28,10 +28,10 @@ def one_story():
@story.part()
async def start(ctx):
await story.say('Where do you go?', user=ctx['user'])
return forking.Switch({
'location': location.Any(),
'text': text.Any(),
})
return forking.Switch([
('location', location.Any()),
('text', text.Any()),
])

@story.case(match='location')
def location_case():
Expand Down Expand Up @@ -271,9 +271,9 @@ def next_room_1(ctx):
def room_1_1():
@story.part()
def next_room_1_1(ctx):
return forking.Switch({
'no-exit': text.Match('no-exit'),
})
return forking.Switch([
('no-exit', text.Match('no-exit')),
])

@story.case(match='no-exit')
def room_1_1_1():
Expand Down Expand Up @@ -368,6 +368,46 @@ def right_room_passed(ctx):
assert right_trigger.is_triggered


@pytest.mark.asyncio
async def test_validators_are_overlapped_so_order_rules_here():
wrong_trigger = SimpleTrigger()
right_trigger = SimpleTrigger()

with answer.Talk() as talk:
story = talk.story

@story.on('title')
def set_title_story():
@story.part()
async def ask_title(ctx):
return await story.ask('What is the title?',
user=ctx['user'])

@story.case('cancel')
def handle_cancel():
@story.part()
def process_cancel(ctx):
return right_trigger.passed()

@story.case(text.Any())
def handel_text():
@story.part()
def process_text(ctx):
return wrong_trigger.passed()

@story.case(any.Any())
def handle_any():
@story.part()
def process_any(ctx):
return wrong_trigger.passed()

await talk.pure_text('title')
await talk.pure_text('cancel')

assert not wrong_trigger.is_triggered
assert right_trigger.is_triggered


@pytest.mark.asyncio
@pytest.mark.parametrize('return_value, first_condition, second_condition',
[
Expand Down Expand Up @@ -642,10 +682,10 @@ async def see_you(message):


def test_serialize():
m_old = forking.Switch({
'location': location.Any(),
'text': text.Any(),
})
m_old = forking.Switch([
('location', location.Any()),
('text', text.Any()),
])
ready_to_store = json.dumps(matchers.serialize(m_old))
print('ready_to_store', ready_to_store)
m_new = matchers.deserialize(json.loads(ready_to_store))
Expand Down
6 changes: 3 additions & 3 deletions botstory/ast/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ def add(self, story):
self.stories.append(story)

def all_filters(self):
return {s.topic: s.extensions['validator']
for s in self.stories if 'validator' in s.extensions}
return [(s.topic, s.extensions['validator'])
for s in self.stories if 'validator' in s.extensions]

def match(self, message):
matched_stories = [
Expand Down Expand Up @@ -111,7 +111,7 @@ def get_story_by_topic(self, topic, stack=None):
# for forking.StoryPartFork
inner_stories = [
story.children for story in parent.story_line if hasattr(story, 'children')
]
]

inner_stories = [item for sublist in inner_stories for item in sublist]

Expand Down
4 changes: 4 additions & 0 deletions botstory/ast/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,13 @@ async def match_message(self, message_ctx):

# looking for first valid matcher
while True:
logger.debug('# looking for first valid matcher')
if ctx.is_empty_stack():
# we have reach the bottom of stack
logger.debug('# we have reach the bottom of stack')
if ctx.does_it_match_any_story():
# but sometimes we could jump on other global matcher
logger.debug('# but sometimes we could jump on other global matcher')
ctx = story_context.reducers.scope_in(ctx)
ctx = await self.process_story(ctx)
ctx = story_context.reducers.scope_out(ctx)
Expand All @@ -88,6 +91,7 @@ async def match_message(self, message_ctx):
ctx = story_context.reducers.scope_out(ctx)

if ctx.has_child_story():
logger.debug('# has child story')
ctx = story_context.reducers.scope_in(ctx)
ctx = await self.process_story(ctx)
logger.debug('# match_message scope_out')
Expand Down
22 changes: 19 additions & 3 deletions botstory/ast/story_context/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def does_it_match_any_story(self):
return self.compiled_story() is not None and not self.matched

def get_child_story(self):
logger.debug('get_child_story')
logger.debug('# get_child_story')
"""
try child story that match message and get scope of it
:return:
Expand All @@ -67,23 +67,33 @@ def get_child_story(self):
story_part = self.get_current_story_part()

if not hasattr(story_part, 'get_child_by_validation_result'):
logger.debug('# does not have get_child_by_validation_result')
return None

if isinstance(self.waiting_for, forking.SwitchOnValue):
logger.debug('# switch on value')
return story_part.get_child_by_validation_result(self.waiting_for.value)

# for some base classes we could try validate result direct
child_story = story_part.get_child_by_validation_result(self.waiting_for)
logger.debug('child_story')
logger.debug(child_story)
if child_story:
logger.debug('# child_story')
logger.debug(child_story)
return child_story

stack_tail = self.stack_tail()
if stack_tail['data'] is not None and not self.matched:
validator = matchers.deserialize(stack_tail['data'])
logger.debug('# validator')
logger.debug(validator)
logger.debug('# self.message')
logger.debug(self.message)
validation_result = validator.validate(self.message)
logger.debug('# validation_result')
logger.debug(validation_result)
res = story_part.get_child_by_validation_result(validation_result)
logger.debug('# res')
logger.debug(res)
# or we validate message
# but can't find right child story
# maybe we should use independent validators for each story here
Expand All @@ -95,12 +105,18 @@ def get_child_story(self):
return None

def get_story_scope_child(self, story_part):
logger.debug('# get_story_scope_child')
validator = story_part.children_matcher()
logger.debug('# validator')
logger.debug(validator)
logger.debug('# self.message')
logger.debug(self.message)
topic = validator.validate(self.message)
# if topic == None:
# we inside story loop scope
# but got message that doesn't match
# any local stories
logger.debug('# topic {}'.format(topic))
return story_part.by_topic(topic)

def get_current_story_part(self):
Expand Down
4 changes: 3 additions & 1 deletion botstory/ast/story_context/reducers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ async def execute(ctx):
story_part = ctx.get_current_story_part()
logger.debug('# going to call: {}'.format(story_part.__name__))
waiting_for = story_part(ctx.message)
logger.debug('# got result {}'.format(waiting_for))
if inspect.iscoroutinefunction(story_part):
waiting_for = await waiting_for
logger.debug('# got result {}'.format(waiting_for))

# story part could run callable story and return its context
if isinstance(waiting_for, story_context.StoryContext):
Expand Down Expand Up @@ -129,6 +129,8 @@ def scope_in(ctx):
compiled_story = None
if not ctx.is_empty_stack():
compiled_story = ctx.get_child_story()
logger.debug('# child')
logger.debug(compiled_story)
# we match child story loop once by message
# what should prevent multiple matching by the same message
ctx.matched = True
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.62
0.0.63

0 comments on commit b6d175e

Please sign in to comment.