Repository: microsoft/botbuilder-python
Pull request #233 (2019-06-28 19:53:00)

tests for find methods


Patch:
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/__init__.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/__init__.py
index 1b02474a..60349bfe 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/__init__.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/__init__.py
@@ -9,6 +9,7 @@ from .channel import Channel
 from .choice import Choice
 from .choice_factory_options import ChoiceFactoryOptions
 from .choice_factory import ChoiceFactory
+from .find import Find
 from .find_choices_options import FindChoicesOptions, FindValuesOptions
 from .found_choice import FoundChoice
 from .found_value import FoundValue
@@ -23,6 +24,7 @@ __all__ = [
     "Choice",
     "ChoiceFactory",
     "ChoiceFactoryOptions",
+    "Find",
     "FindChoicesOptions",
     "FindValuesOptions",
     "FoundChoice",
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find.py
index 6bf02531..0471f180 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find.py
@@ -5,30 +5,32 @@ from typing import Callable, List, Union
 
 from .choice import Choice
 from .find_choices_options import FindChoicesOptions, FindValuesOptions
+from .found_choice import FoundChoice
 from .found_value import FoundValue
 from .model_result import ModelResult
 from .sorted_value import SortedValue
 from .token import Token
 from .tokenizer import Tokenizer
 
+
 class Find:
     """ Contains methods for matching user input against a list of choices """
-    
+
     @staticmethod
     def find_choices(
-        utterance: str,
-        choices: [ Union[str, Choice] ],
-        options: FindChoicesOptions = None
+            utterance: str,
+            choices: [Union[str, Choice]],
+            options: FindChoicesOptions = None
     ):
         """ Matches user input against a list of choices """
 
         if not choices:
             raise TypeError('Find: choices cannot be None. Must be a [str] or [Choice].')
-        
+
         opt = options if options else FindChoicesOptions()
 
         # Normalize list of choices
-        choices_list = [ Choice(value=choice) if isinstance(choice, str) else choice for choice in choices ]
+        choices_list = [Choice(value=choice) if isinstance(choice, str) else choice for choice in choices]
 
         # Build up full list of synonyms to search over.
         # - Each entry in the list contains the index of the choice it belongs to which will later be
@@ -39,33 +41,49 @@ class Find:
             choice = choices_list[index]
 
             if not opt.no_value:
-                synonyms.append( SortedValue(value=choice.value, index=index) )
-            
+                synonyms.append(SortedValue(value=choice.value, index=index))
+
             if (
-                getattr(choice, 'action', False) and 
-                getattr(choice.action, 'title', False) and 
-                not opt.no_value
+                    getattr(choice, 'action', False) and
+                    getattr(choice.action, 'title', False) and
+                    not opt.no_value
             ):
-                synonyms.append( SortedValue(value=choice.action.title, index=index) )
-            
-            if choice.synonyms != None:
+                synonyms.append(SortedValue(value=choice.action.title, index=index))
+
+            if choice.synonyms is not None:
                 for synonym in synonyms:
-                    synonyms.append( SortedValue(value=synonym, index=index) )
-        
+                    synonyms.append(SortedValue(value=synonym, index=index))
+
+        def found_choice_constructor(value_model: ModelResult) -> ModelResult:
+            choice = choices_list[value_model.resolution.index]
+
+            return ModelResult(
+                start=value_model.start,
+                end=value_model.end,
+                type_name='choice',
+                text=value_model.text,
+                resolution=FoundChoice(
+                    value=choice.value,
+                    index=value_model.resolution.index,
+                    score=value_model.resolution.score,
+                    synonym=value_model.resolution.value,
+                )
+            )
+
         # Find synonyms in utterance and map back to their choices_list
-        return Find._find_values(utterance, synonyms, options)
-    
+        return list(map(found_choice_constructor, Find.find_values(utterance, synonyms, options)))
+
     @staticmethod
-    def _find_values(
-        utterance: str,
-        values: List[SortedValue],
-        options: FindValuesOptions = None
+    def find_values(
+            utterance: str,
+            values: List[SortedValue],
+            options: FindValuesOptions = None
     ):
         # Sort values in descending order by length, so that the longest value is searchd over first.
         sorted_values = sorted(
             values,
-            key = lambda sorted_val: len(sorted_val.value),
-            reverse = True
+            key=lambda sorted_val: len(sorted_val.value),
+            reverse=True
         )
 
         # Search for each value within the utterance.
@@ -73,7 +91,7 @@ class Find:
         opt = options if options else FindValuesOptions()
         tokenizer: Callable[[str, str], List[Token]] = opt.tokenizer if opt.tokenizer else Tokenizer.default_tokenizer
         tokens = tokenizer(utterance, opt.locale)
-        max_distance = opt.max_token_distance if opt.max_token_distance != None else 2
+        max_distance = opt.max_token_distance if opt.max_token_distance is not None else 2
 
         for i in range(len(sorted_values)):
             entry = sorted_values[i]
@@ -95,18 +113,18 @@ class Find:
                     searched_tokens,
                     start_pos
                 )
-                
-                if match != None:
+
+                if match is not None:
                     start_pos = match.end + 1
                     matches.append(match)
                 else:
                     break
-            
+
         # Sort matches by score descending
         sorted_matches = sorted(
             matches,
-            key = lambda model_result: model_result.resolution.score,
-            reverse = True
+            key=lambda model_result: model_result.resolution.score,
+            reverse=True
         )
 
         # Filter out duplicate matching indexes and overlapping characters
@@ -125,7 +143,7 @@ class Find:
                 if i in used_tokens:
                     add = False
                     break
-            
+
             # Add to results
             if add:
                 # Update filter info
@@ -137,21 +155,21 @@ class Find:
                 # Translate start & end and populate text field
                 match.start = tokens[match.start].start
                 match.end = tokens[match.end].end
-                match.text = utterance[match.start : match.end + 1]
+                match.text = utterance[match.start: match.end + 1]
                 results.append(match)
-        
+
         # Return the results sorted by position in the utterance
-        return sorted(results, key = lambda model_result: model_result.start)
+        return sorted(results, key=lambda model_result: model_result.start)
 
     @staticmethod
     def _match_value(
-        source_tokens: List[Token],
-        max_distance: int,
-        options: FindValuesOptions,
-        index: int,
-        value: str,
-        searched_tokens: List[Token],
-        start_pos: int
+            source_tokens: List[Token],
+            max_distance: int,
+            options: FindValuesOptions,
+            index: int,
+            value: str,
+            searched_tokens: List[Token],
+            start_pos: int
     ) -> Union[ModelResult, None]:
         # Match value to utterance and calculate total deviation.
         # - The tokens are matched in order so "second last" will match in
@@ -180,16 +198,16 @@ class Find:
                     # Update start & end position that will track the span of the utterance that's matched.
                     if (start < 0):
                         start = pos
-                    
+
                     end = pos
-        
+
         # Calculate score and format result
         # - The start & end positions and the results text field will be corrected by the caller.
         result: ModelResult = None
 
         if (
-            matched > 0 and
-            (matched == len(searched_tokens) or options.allow_partial_matches)
+                matched > 0 and
+                (matched == len(searched_tokens) or options.allow_partial_matches)
         ):
             # Percentage of tokens matched. If matching "second last" in
             # "the second form last one" the completeness would be 1.0 since
@@ -207,28 +225,27 @@ class Find:
 
             # Format result
             result = ModelResult(
-                text = '',
-                start = start,
-                end = end,
-                type_name = "value",
-                resolution = FoundValue(
-                    value = value,
-                    index = index,
-                    score = score
+                text='',
+                start=start,
+                end=end,
+                type_name="value",
+                resolution=FoundValue(
+                    value=value,
+                    index=index,
+                    score=score
                 )
             )
-        
+
         return result
-    
+
     @staticmethod
     def _index_of_token(
-        tokens: List[Token],
-        token: Token,
-        start_pos: int
+            tokens: List[Token],
+            token: Token,
+            start_pos: int
     ) -> int:
         for i in range(start_pos, len(tokens)):
             if tokens[i].normalized == token.normalized:
                 return i
-        
-        return -1
 
+        return -1
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/found_choice.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/found_choice.py
index 6d6a82fc..fcb50e6f 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/found_choice.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/found_choice.py
@@ -1,6 +1,7 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
+
 class FoundChoice:
     """ Represents a result from matching user input against a list of choices """
 
@@ -27,4 +28,4 @@ class FoundChoice:
         self.value = value
         self.index = index
         self.score = score
-        self.synonym = synonym
\ No newline at end of file
+        self.synonym = synonym
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/found_value.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/found_value.py
index 03409854..31c88bf5 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/found_value.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/found_value.py
@@ -24,4 +24,4 @@ class FoundValue:
         """
         self.value = value
         self.index = index
-        self.score = score
\ No newline at end of file
+        self.score = score
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/model_result.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/model_result.py
index 6f4b7026..31ecbe90 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/model_result.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/model_result.py
@@ -30,4 +30,4 @@ class ModelResult:
         self.start = start
         self.end = end
         self.type_name = type_name
-        self.resolution = resolution
\ No newline at end of file
+        self.resolution = resolution
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/sorted_value.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/sorted_value.py
index 563ad8a8..48ed7a5e 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/sorted_value.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/sorted_value.py
@@ -1,6 +1,7 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
+
 class SortedValue:
     """ A value that can be sorted and still refer to its original position with a source array. """
 
@@ -14,5 +15,5 @@ class SortedValue:
         index: The values original position within its unsorted array.
         """
         
-        self.value = value,
-        self.index = index
\ No newline at end of file
+        self.value = value
+        self.index = index


Test patch:
diff --git a/libraries/botbuilder-dialogs/tests/choices/test_choices_recognizers.py b/libraries/botbuilder-dialogs/tests/choices/test_choices_recognizers.py
index 7957b6e6..d860e2f9 100644
--- a/libraries/botbuilder-dialogs/tests/choices/test_choices_recognizers.py
+++ b/libraries/botbuilder-dialogs/tests/choices/test_choices_recognizers.py
@@ -1,62 +1,121 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
+from typing import List
 
 import aiounittest
-import unittest
-from typing import List
 
-from botbuilder.dialogs.choices import Choice, SortedValue
+from botbuilder.dialogs.choices import SortedValue, Find, FindValuesOptions
+
+
+def assert_result(result, start, end, text):
+    assert result.start == start, f"Invalid ModelResult.start of '{result.start}' for '{text}' result."
+    assert result.end == end, f"Invalid ModelResult.end of '{result.end}' for '{text}' result."
+    assert result.text == text, f"Invalid ModelResult.text of '{result.text}' for '{text}' result."
+
+
+def assert_value(result, value, index, score):
+    assert result.type_name == 'value', f"Invalid ModelResult.type_name of '{result.type_name}' for '{value}' value."
+    assert result.resolution, f"Missing ModelResult.resolution for '{value}' value."
+    resolution = result.resolution
+    assert resolution.value == value, f"Invalid resolution.value of '{resolution.value}' for '{value}' value."
+    assert resolution.index == index, f"Invalid resolution.index of '{resolution.index}' for '{value}' value."
+    assert resolution.score == score, f"Invalid resolution.score of '{resolution.score}' for '{value}' value."
+
+
+def assert_choice(result, value, index, score, synonym=None):
+    assert result.type_name == 'choice', f"Invalid ModelResult.type_name of '{result.type_name}' for '{value}' choice."
+    assert result.resolution, f"Missing ModelResult.resolution for '{value}' choice."
+    resolution = result.resolution
+    assert resolution.value == value, f"Invalid resolution.value of '{resolution.value}' for '{value}' choice."
+    assert resolution.index == index, f"Invalid resolution.index of '{resolution.index}' for '{value}' choice."
+    assert resolution.score == score, f"Invalid resolution.score of '{resolution.score}' for '{value}' choice."
+    if synonym:
+        assert (resolution.synonym == synonym,
+                f"Invalid resolution.synonym of '{resolution.synonym}' for '{value}' choice.")
+
+
+_color_choices: List[str] = ['red', 'green', 'blue']
+_overlapping_choices: List[str] = ['bread', 'bread pudding', 'pudding']
+
+_color_values: List[SortedValue] = [
+    SortedValue(value='red', index=0),
+    SortedValue(value='green', index=1),
+    SortedValue(value='blue', index=2)
+]
+
+_overlapping_values: List[SortedValue] = [
+    SortedValue(value='bread', index=0),
+    SortedValue(value='bread pudding', index=1),
+    SortedValue(value='pudding', index=2)
+]
+
+_similar_values: List[SortedValue] = [
+    SortedValue(value='option A', index=0),
+    SortedValue(value='option B', index=1),
+    SortedValue(value='option C', index=2)
+]
+
 
 class ChoiceRecognizersTest(aiounittest.AsyncTestCase):
-    # FindChoices
-
-    _color_choices: List[str] = ['red', 'green', 'blue']
-    _overlapping_choices: List[str] = ['bread', 'bread pudding', 'pudding']
-    
-    _color_values: List[SortedValue] = [
-        SortedValue(value = 'red', index = 0),
-        SortedValue(value = 'green', index = 1),
-        SortedValue(value = 'blue', index = 2)
-    ]
-
-    _overlapping_values: List[SortedValue] = [
-        SortedValue(value = 'bread', index = 0),
-        SortedValue(value = 'bread pudding', index = 1),
-        SortedValue(value = 'pudding', index = 2)
-    ]
-
-    _similar_values: List[SortedValue] = [
-        SortedValue(value = 'option A', index = 0),
-        SortedValue(value = 'option B', index = 1),
-        SortedValue(value = 'option C', index = 2)
-    ]
+    # Find.find_choices
 
     def test_should_find_a_simple_value_in_a_single_word_utterance(self):
-        pass
+        found = Find.find_values('red', _color_values)
+        assert len(found) == 1, f"Invalid token count of '{len(found)}' returned."
+        assert_result(found[0], 0, 2, 'red')
+        assert_value(found[0], 'red', 0, 1.0)
 
     def test_should_find_a_simple_value_in_an_utterance(self):
-        pass
+        found = Find.find_values('the red one please.', _color_values)
+        assert len(found) == 1, f"Invalid token count of '{len(found)}' returned."
+        assert_result(found[0], 4, 6, 'red')
+        assert_value(found[0], 'red', 0, 1.0)
 
     def test_should_find_multiple_values_within_an_utterance(self):
-        pass
+        found = Find.find_values('the red and blue ones please.', _color_values)
+        assert len(found) == 2, f"Invalid token count of '{len(found)}' returned."
+        assert_result(found[0], 4, 6, 'red')
+        assert_value(found[0], 'red', 0, 1.0)
+        assert_value(found[1], 'blue', 2, 1.0)
 
     def test_should_find_multiple_values_that_overlap(self):
-        pass
-    
+        found = Find.find_values('the bread pudding and bread please.', _overlapping_values)
+        assert len(found) == 2, f"Invalid token count of '{len(found)}' returned."
+        assert_result(found[0], 4, 16, 'bread pudding')
+        assert_value(found[0], 'bread pudding', 1, 1.0)
+        assert_value(found[1], 'bread', 0, 1.0)
+
     def test_should_correctly_disambiguate_between_similar_values(self):
-        pass
-    
-    def test_should_find_a_single_choice_within_an_utterance(self):
-        pass
-    
+        found = Find.find_values('option B', _similar_values, FindValuesOptions(allow_partial_matches=True))
+        assert len(found) == 1, f"Invalid token count of '{len(found)}' returned."
+        assert_value(found[0], 'option B', 1, 1.0)
+
+    def test_should_find_a_single_choice_in_an_utterance(self):
+        found = Find.find_choices('the red one please.', _color_choices)
+        assert len(found) == 1, f"Invalid token count of '{len(found)}' returned."
+        assert_result(found[0], 4, 6, 'red')
+        assert_choice(found[0], 'red', 0, 1.0, 'red')
+
+    def test_should_find_multiple_choices_within_an_utterance(self):
+        found = Find.find_choices('the red and blue ones please.', _color_choices)
+        assert len(found) == 2, f"Invalid token count of '{len(found)}' returned."
+        assert_result(found[0], 4, 6, 'red')
+        assert_choice(found[0], 'red', 0, 1.0)
+        assert_choice(found[1], 'blue', 2, 1.0)
+
     def test_should_find_multiple_choices_that_overlap(self):
-        pass
-    
+        found = Find.find_choices('the bread pudding and bread please.', _overlapping_choices)
+        assert len(found) == 2, f"Invalid token count of '{len(found)}' returned."
+        assert_result(found[0], 4, 16, 'bread pudding')
+        assert_choice(found[0], 'bread pudding', 1, 1.0)
+        assert_choice(found[1], 'bread', 0, 1.0)
+
     def test_should_accept_null_utterance_in_find_choices(self):
-        pass
-    
+        found = Find.find_choices(None, _color_choices)
+        assert len(found) == 0
+
     def test_should_find_a_choice_in_an_utterance_by_name(self):
         pass
-    
+
     def test_should_find_a_choice_in_an_utterance_by_ordinal_position(self):
-        pass
\ No newline at end of file
+        pass