From 7ec1b2aaeb81aae33e55003918b98760f81b9354 Mon Sep 17 00:00:00 2001 From: Dane Finlay Date: Wed, 6 Aug 2025 16:37:11 +1000 Subject: [PATCH 1/3] Change the behaviour of Repetition and Integer max constructor arguments Re: #394. The *max* constructor arguments of the Repetition and Integer class- es are now treated as inclusive, rather than exclusive. This seems more sensible and matches the treatment of the *min* argument. For the Integer class, I have elected not to change the complex int- ernal classes that do the most of the work. They receive the value *max + 1*. This shouldn't matter, but I'm noting it here. The Digits element class is affected by this change, as it is a sub- class of Repetition. --- dragonfly/grammar/elements_basic.py | 22 ++++++++++----------- dragonfly/language/base/integer.py | 11 +++++++++++ dragonfly/language/base/integer_internal.py | 5 +++++ 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/dragonfly/grammar/elements_basic.py b/dragonfly/grammar/elements_basic.py index bd85fee7..813c8dcc 100644 --- a/dragonfly/grammar/elements_basic.py +++ b/dragonfly/grammar/elements_basic.py @@ -570,8 +570,8 @@ class Repetition(Sequence): be recognized (inclusive); may be 0 - *max* (*int*, default: *None*) -- the maximum number of times that the child element must - be recognized (exclusive!); if *None*, the child element must be - recognized exactly *min* times (i.e. *max = min + 1*) + be recognized (inclusive); if *None*, the child element may be + recognized up to *min + 1* times (i.e. *max = min + 1*) - *name* (*str*, default: *None*) -- the name of this element - *default* (*object*, default: *None*) -- @@ -585,11 +585,11 @@ class Repetition(Sequence): least *min* times and strictly less than *max* times. Examples: - - *Repetition(child, min=2, max=5)* -- child 2, 3, or 4 times - - *Repetition(child, min=0, max=3)* -- child 0, 1, or 2 times - - *Repetition(child, max=3)* -- child 1 or 2 times - - *Repetition(child, min=1, max=2)* -- child exactly once - - *Repetition(child, min=1)* -- child exactly once + - *Repetition(child, min=2, max=4)* -- child 2, 3, or 4 times + - *Repetition(child, min=0, max=2)* -- child 0, 1, or 2 times + - *Repetition(child, max=2)* -- child 1 or 2 times + - *Repetition(child, min=1, max=1)* -- child exactly once + - *Repetition(child, min=1)* -- child 1 or 2 times - *Repetition(child)* -- child exactly once If the *optimize* argument is set to *True*, the engine's compiler may @@ -610,7 +610,7 @@ def __init__(self, child, min=1, max=None, name=None, default=None, " ElementBase instance." % self) assert isinstance(min, six.integer_types) assert max is None or isinstance(max, six.integer_types) - assert max is None or min < max, "min must be less than max" + assert max is None or min <= max, "min must be less than or equal to max" self._child = child self._min = min @@ -618,7 +618,7 @@ def __init__(self, child, min=1, max=None, name=None, default=None, else: self._max = max self._optimize = optimize - optional_length = self._max - self._min - 1 + optional_length = self._max - self._min if optional_length > 0: element = Optional(child) for index in range(optional_length-1): @@ -644,8 +644,8 @@ def __init__(self, child, min=1, max=None, name=None, default=None, max = property( lambda self: self._max, doc="The maximum number of times that the child element must be " - "recognized; if *None*, the child element must be " - "recognized exactly *min* times (i.e. *max = min + 1*). " + "recognized; if *None*, the child element may be " + "recognized up to *min + 1* times (i.e. *max = min + 1*). " "(Read-only)" ) diff --git a/dragonfly/language/base/integer.py b/dragonfly/language/base/integer.py index a0e9f49b..e6826a76 100644 --- a/dragonfly/language/base/integer.py +++ b/dragonfly/language/base/integer.py @@ -24,6 +24,8 @@ """ +from six import integer_types + from dragonfly.language.loader import language from dragonfly.grammar.elements import (Alternative, Sequence, Optional, Compound, ListRef, RuleWrap) @@ -60,6 +62,15 @@ def __init__(self, name=None, min=None, max=None, default=None, self._set_content(language.IntegerContent) self._builders = self._content.builders + assert isinstance(min, integer_types), "min must be a number" + assert isinstance(max, integer_types), "max must be a number" + assert min <= max, "min must be less than or equal to max" + + # Make the *max* argument behave inclusively. + # Note: This is an easier change than modifying the internal integer + # classes. + max = max + 1 + self._min = min; self._max = max children = self._build_children(min, max) Alternative.__init__(self, children, name=name, default=default) diff --git a/dragonfly/language/base/integer_internal.py b/dragonfly/language/base/integer_internal.py index 96f2fe66..6427dcee 100644 --- a/dragonfly/language/base/integer_internal.py +++ b/dragonfly/language/base/integer_internal.py @@ -36,6 +36,11 @@ #--------------------------------------------------------------------------- # Numeric element builder classes. +# +# Note: The classes in this file treat the *max* argument as exclusive, +# meaning that build_element() methods will return element trees for +# recognizing integers up to max - 1. + class IntBuilderBase(object): From 386f9be7f625849aa28828b0d5213bda1c10e13b Mon Sep 17 00:00:00 2001 From: Dane Finlay Date: Wed, 6 Aug 2025 16:49:34 +1000 Subject: [PATCH 2/3] Update Repetition and number element test cases Re changes to Repetition and Integer *max* constructor arguments. --- .../test_grammar_elements_basic_doctest.txt | 8 ++--- documentation/test_recobs_doctest.txt | 2 +- dragonfly/test/test_language_en_number.py | 34 +++++++++---------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/documentation/test_grammar_elements_basic_doctest.txt b/documentation/test_grammar_elements_basic_doctest.txt index d947f747..cceaebbf 100644 --- a/documentation/test_grammar_elements_basic_doctest.txt +++ b/documentation/test_grammar_elements_basic_doctest.txt @@ -178,7 +178,7 @@ Basic usage:: Exact number of repetitions:: >>> seq = Sequence([Literal("hello"), Literal("world")]) - >>> rep = Repetition(seq, min=3, max=None, optimize=False) + >>> rep = Repetition(seq, min=3, max=3, optimize=False) >>> test_rep = ElementTester(rep) >>> test_rep.recognize("hello world") RecognitionFailure @@ -189,12 +189,12 @@ Exact number of repetitions:: >>> test_rep.recognize("hello world hello world hello world hello world") RecognitionFailure -min must be less than max:: +min must be less than or equal to max:: - >>> rep = Repetition(Literal("hello"), min=3, max=3, optimize=False) + >>> rep = Repetition(Literal("hello"), min=4, max=3, optimize=False) Traceback (most recent call last): ... - AssertionError: min must be less than max + AssertionError: min must be less than or equal to max Modifier element class diff --git a/documentation/test_recobs_doctest.txt b/documentation/test_recobs_doctest.txt index bc5a327a..135eacf2 100644 --- a/documentation/test_recobs_doctest.txt +++ b/documentation/test_recobs_doctest.txt @@ -88,7 +88,7 @@ Simple literal element recognitions:: Integer element recognitions:: - >>> test_int = ElementTester(Integer(min=1, max=100)) + >>> test_int = ElementTester(Integer(min=1, max=99)) >>> test_int.recognize("seven") 7 >>> test_recobs.waiting, test_recobs.words diff --git a/dragonfly/test/test_language_en_number.py b/dragonfly/test/test_language_en_number.py index ede17de3..f43294de 100644 --- a/dragonfly/test/test_language_en_number.py +++ b/dragonfly/test/test_language_en_number.py @@ -67,10 +67,10 @@ def _build_element(self): ] -class Limit3to14TestCase(ElementTestCase): - """ Verify integer limits of range 3 -- 14. """ +class Limit3to13TestCase(ElementTestCase): + """ Verify integer limits of range 3 -- 13. """ def _build_element(self): - return Integer(content=IntegerContent, min=3, max=14) + return Integer(content=IntegerContent, min=3, max=13) input_output = [ ("oh", RecognitionFailure), ("zero", RecognitionFailure), @@ -96,10 +96,10 @@ def _build_element(self): ] -class Limit23to47TestCase(ElementTestCase): - """ Verify integer limits of range 23 -- 47. """ +class Limit23to46TestCase(ElementTestCase): + """ Verify integer limits of range 23 -- 46. """ def _build_element(self): - return Integer(content=IntegerContent, min=23, max=47) + return Integer(content=IntegerContent, min=23, max=46) input_output = [ ("twenty two", RecognitionFailure), ("twenty three", 23), @@ -108,10 +108,10 @@ def _build_element(self): ] -class Limit230to350TestCase(ElementTestCase): - """ Verify integer limits of range 230 -- 350. """ +class Limit230to349TestCase(ElementTestCase): + """ Verify integer limits of range 230 -- 349. """ def _build_element(self): - return Integer(content=IntegerContent, min=230, max=350) + return Integer(content=IntegerContent, min=230, max=349) input_output = [ ("two hundred twenty nine", RecognitionFailure), ("two hundred thirty", 230), @@ -125,10 +125,10 @@ def _build_element(self): ] -class Limit351TestCase(ElementTestCase): - """ Verify integer limits of range up to 351. """ +class Limit350TestCase(ElementTestCase): + """ Verify integer limits of range up to 350. """ def _build_element(self): - return Integer(content=IntegerContent, min=230, max=351) + return Integer(content=IntegerContent, min=230, max=350) input_output = [ ("three hundred forty nine", 349), ("three hundred fifty", 350), @@ -137,10 +137,10 @@ def _build_element(self): ] -class Limit352TestCase(ElementTestCase): - """ Verify integer limits of range up to 352. """ +class Limit351TestCase(ElementTestCase): + """ Verify integer limits of range up to 351. """ def _build_element(self): - return Integer(content=IntegerContent, min=230, max=352) + return Integer(content=IntegerContent, min=230, max=351) input_output = [ ("three hundred forty nine", 349), ("three hundred fifty", 350), @@ -153,7 +153,7 @@ def _build_element(self): class DigitsTestCase(ElementTestCase): """ Verify that digits between 0 and 10 can be recognized. """ def _build_element(self): - return Digits(content=DigitsContent, min=1, max=6) + return Digits(content=DigitsContent, min=1, max=5) input_output = [ ("zero", [0]), ("oh", [0]), @@ -182,7 +182,7 @@ def _build_element(self): class DigitsAsIntTestCase(ElementTestCase): """ Verify that Digits used with as_int=True gives correct values. """ def _build_element(self): - return Digits(content=DigitsContent, min=1, max=6, as_int=True) + return Digits(content=DigitsContent, min=1, max=5, as_int=True) input_output = [ ("zero", 0), ("oh", 0), From 9861562966e23563852fa39ddf36542dab1e3b72 Mon Sep 17 00:00:00 2001 From: Dane Finlay Date: Fri, 8 Aug 2025 16:43:35 +1000 Subject: [PATCH 3/3] Add a new OneOrMore element class and update documentation + tests Re: #394. The OneOrMore element class is a sub-class of Repetition which matc- hes a child element one or more times -- i.e. Kleene plus behaviour. The default maximum number of repetitions is sixteen and can be adj- usted via the class member *default_max*. OneOrMore is a simpler way of using Dragonfly's Repetition element for what is commonly called Continuous Command Recognition (CCR). --- documentation/ccr.txt | 3 +- documentation/elements.txt | 6 ++ .../test_grammar_elements_basic_doctest.txt | 33 +++++++++++ dragonfly/__init__.py | 6 +- dragonfly/grammar/elements.py | 1 + dragonfly/grammar/elements_basic.py | 56 ++++++++++++++++--- 6 files changed, 92 insertions(+), 13 deletions(-) diff --git a/documentation/ccr.txt b/documentation/ccr.txt index 26140646..4a9a0041 100644 --- a/documentation/ccr.txt +++ b/documentation/ccr.txt @@ -7,7 +7,8 @@ Continous Command Recognition (CCR) One of dragonfly's most powerful features is continuous command recognition (CCR), that is commands that can be spoken together without pausing. This is done through use of a :class:`dragonfly.grammar.element_basic.Repetition` -rule element. There is a mini-demo of continuous command recognition +or :class:`dragonfly.grammar.element_basic.OneOrMore` rule element. There is +a mini-demo of continuous command recognition `on YouTube `__. There are also a few projects using dragonfly which make writing CCR rules easier: diff --git a/documentation/elements.txt b/documentation/elements.txt index 59a924b7..0ee5adbf 100644 --- a/documentation/elements.txt +++ b/documentation/elements.txt @@ -33,6 +33,12 @@ Repetition class .. autoclass:: dragonfly.grammar.elements_basic.Repetition :members: dependencies, gstring, decode, value, children, get_repetitions +OneOrMore class +---------------------------------------------------------------------------- +.. autoclass:: dragonfly.grammar.elements_basic.OneOrMore + :members: dependencies, gstring, decode, value, children, + get_repetitions, default_max + Literal class ---------------------------------------------------------------------------- .. autoclass:: dragonfly.grammar.elements_basic.Literal diff --git a/documentation/test_grammar_elements_basic_doctest.txt b/documentation/test_grammar_elements_basic_doctest.txt index cceaebbf..f52a0b78 100644 --- a/documentation/test_grammar_elements_basic_doctest.txt +++ b/documentation/test_grammar_elements_basic_doctest.txt @@ -197,6 +197,39 @@ min must be less than or equal to max:: AssertionError: min must be less than or equal to max +OneOrMore element classes +============================================================================ + +Basic usage:: + + >>> # OneOrMore is given a dragonfly element, in this case a Sequence. + >>> seq = Sequence([Literal("hello"), Literal("world")]) + >>> # OneOrMore is a type of Repetition element. + >>> rep = OneOrMore(seq) + >>> test_rep = ElementTester(rep) + >>> test_rep.recognize("hello world") + [[u'hello', u'world']] + >>> test_rep.recognize("hello world hello world") + [[u'hello', u'world'], [u'hello', u'world']] + >>> # Incomplete recognitions result in recognition failure. + >>> test_rep.recognize("hello universe") + RecognitionFailure + >>> test_rep.recognize("hello world hello universe") + RecognitionFailure + +Large number of repetitions:: + + >>> # Too many recognitions also result in recognition failure. + >>> test_rep.recognize(" ".join(["hello world"] * 17)) + RecognitionFailure + >>> # Further repetitions may be matched by adjusting the class member. + >>> OneOrMore.default_max = 20 + >>> rep = OneOrMore(seq) + >>> test_rep = ElementTester(rep) + >>> test_rep.recognize(" ".join(["hello world"] * 17)) + [[u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world'], [u'hello', u'world']] + + Modifier element class ============================================================================ diff --git a/dragonfly/__init__.py b/dragonfly/__init__.py index a4f393e4..74eaa094 100644 --- a/dragonfly/__init__.py +++ b/dragonfly/__init__.py @@ -40,10 +40,10 @@ from .grammar.rule_compound import CompoundRule from .grammar.rule_mapping import MappingRule from .grammar.elements import (ElementBase, Sequence, Alternative, - Optional, Repetition, Literal, + Optional, Repetition, OneOrMore, Literal, ListRef, DictListRef, Dictation, Modifier, - RuleRef, RuleWrap, Compound, Choice, - Empty, Impossible) + RuleRef, RuleWrap, Compound, Choice, Empty, + Impossible) from .grammar.context import Context, AppContext, FuncContext from .grammar.list import ListBase, List, DictList diff --git a/dragonfly/grammar/elements.py b/dragonfly/grammar/elements.py index fda79ecb..d428acd5 100644 --- a/dragonfly/grammar/elements.py +++ b/dragonfly/grammar/elements.py @@ -45,6 +45,7 @@ Alternative = basic_.Alternative Optional = basic_.Optional Repetition = basic_.Repetition +OneOrMore = basic_.OneOrMore Literal = basic_.Literal RuleRef = basic_.RuleRef Rule = basic_.RuleRef # For backwards compatibility. diff --git a/dragonfly/grammar/elements_basic.py b/dragonfly/grammar/elements_basic.py index 813c8dcc..9ba7d990 100644 --- a/dragonfly/grammar/elements_basic.py +++ b/dragonfly/grammar/elements_basic.py @@ -36,6 +36,8 @@ wrapper around a child element which makes the child element optional - :class:`Repetition` -- repetition of a child element + - :class:`OneOrMore` -- + one or more repetitions of a child element - :class:`Literal` -- literal word which must be said exactly by the speaker as given - :class:`RuleRef` -- @@ -578,11 +580,11 @@ class Repetition(Sequence): the default value used if this element is optional and wasn't spoken - *optimize* (*bool*, default: *True*) -- - whether the engine's compiler should attempt to compile the element - optimally + whether the engine's compiler should attempt to compile the + element optimally For a recognition to match, the child element must be recognized at - least *min* times and strictly less than *max* times. + least *min* times and strictly less than or equal to *max* times. Examples: - *Repetition(child, min=2, max=4)* -- child 2, 3, or 4 times @@ -590,14 +592,14 @@ class Repetition(Sequence): - *Repetition(child, max=2)* -- child 1 or 2 times - *Repetition(child, min=1, max=1)* -- child exactly once - *Repetition(child, min=1)* -- child 1 or 2 times - - *Repetition(child)* -- child exactly once + - *Repetition(child)* -- child 1 or 2 times - If the *optimize* argument is set to *True*, the engine's compiler may - attempt to ignore the *min* and *max* limits to reduce grammar + If the *optimize* argument is set to *True*, the engine's compiler + may attempt to ignore the *min* and *max* limits to reduce grammar complexity. Not all engines support this, and some engines may only - support some rule structures. Regardless, if the number of repetitions - recognized is less than the *min* value -- or equal to or more than the - *max* value -- the rule will still fail to match. + support some rule structures. Regardless, if the number of + repetitions recognized is less than the *min* value -- or equal to + or more than the *max* value -- the rule will still fail to match. """ @@ -717,6 +719,42 @@ def value(self, node): return [r.value() for r in repetitions] +#--------------------------------------------------------------------------- + +class OneOrMore(Repetition): + """ + Element class representing one or more repetitions of one child + element. + + Constructor arguments: + - *child* (*ElementBase*) -- + the child element of this element + - *name* (*str*, default: *None*) -- + the name of this element + - *default* (*object*, default: *None*) -- + the default value used if this element is optional and wasn't + spoken + + This class is a sub-class of :class:`Repetition`. For a recognition + to match, the child element must be recognized one or more times -- + i.e. Kleene plus behavior. + + **Note**: If more than sixteen repetitions of the child element are + given, this element class will fail to match the recognition and + cause an error. In the event that this occurs, the *default_max* + class member should be increased to match a larger number of + repetitions. + + """ + + #: Default *max* parameter to pass to the Repetition superclass. + default_max = 16 + + def __init__(self, child, name=None, default=None): + Repetition.__init__(self, child, min=1, max=self.default_max, + name=name, default=default, optimize=True) + + #--------------------------------------------------------------------------- class Literal(ElementBase):