From 07ff9d77d642eb3fd0cb8dc7ac0f4a7b853f243a Mon Sep 17 00:00:00 2001 From: birdi Date: Wed, 24 Jul 2019 11:57:17 +0300 Subject: [PATCH 1/7] Add tests for multiple Text filter --- tests/test_filters.py | 57 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_filters.py b/tests/test_filters.py index da530910..e0002381 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -195,3 +195,60 @@ class TestTextFilter: assert await check(CallbackQuery(data=test_text)) assert await check(InlineQuery(query=test_text)) assert await check(Poll(question=test_text)) + + @pytest.mark.asyncio + @pytest.mark.parametrize("test_filter_list, test_text, ignore_case", + [(['', 'new_string'], '', True), + (['new_string', ''], 'exAmple_string', True), + (['new_string', ''], '', False), + (['', 'new_string'], 'exAmple_string', False), + + (['example_string'], 'example_string', True), + (['example_string'], 'exAmple_string', True), + (['exAmple_string'], 'example_string', True), + + (['example_string'], 'example_string', False), + (['example_string'], 'exAmple_string', False), + (['exAmple_string'], 'example_string', False), + + (['example_string'], 'not_example_string', True), + (['example_string'], 'not_eXample_string', True), + (['EXample_string'], 'not_eXample_string', True), + + (['example_string'], 'not_example_string', False), + (['example_string'], 'not_eXample_string', False), + (['EXample_string'], 'not_example_string', False), + + (['example_string', 'new_string'], 'example_string', True), + (['new_string', 'example_string'], 'exAmple_string', True), + (['exAmple_string', 'new_string'], 'example_string', True), + + (['example_string', 'new_string'], 'example_string', False), + (['new_string', 'example_string'], 'exAmple_string', False), + (['exAmple_string', 'new_string'], 'example_string', False), + + (['example_string', 'new_string'], 'not_example_string', True), + (['new_string', 'example_string'], 'not_eXample_string', True), + (['EXample_string', 'new_string'], 'not_eXample_string', True), + + (['example_string', 'new_string'], 'not_example_string', False), + (['new_string', 'example_string'], 'not_eXample_string', False), + (['EXample_string', 'new_string'], 'not_example_string', False), + ]) + async def test_equals_list(self, test_filter_list, test_text, ignore_case): + test_filter = Text(equals=test_filter_list, ignore_case=ignore_case) + + async def check(obj): + result = await test_filter.check(obj) + if ignore_case: + _test_filter_list = list(map(str.lower, test_filter_list)) + _test_text = test_text.lower() + else: + _test_filter_list = test_filter_list + _test_text = test_text + assert result is (_test_text in _test_filter_list) + + await check(Message(text=test_text)) + await check(CallbackQuery(data=test_text)) + await check(InlineQuery(query=test_text)) + await check(Poll(question=test_text)) From 1a9a11f3fd8e6c6d1a02b757ca313478e3e4d7d3 Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 27 Jul 2019 12:20:08 +0300 Subject: [PATCH 2/7] add multiple text filter --- aiogram/dispatcher/filters/builtin.py | 35 ++++++++++++--------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 15cd73dd..eb3845e1 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -206,10 +206,10 @@ class Text(Filter): """ def __init__(self, - equals: Optional[Union[str, LazyProxy]] = None, - contains: Optional[Union[str, LazyProxy]] = None, - startswith: Optional[Union[str, LazyProxy]] = None, - endswith: Optional[Union[str, LazyProxy]] = None, + equals: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None, + contains: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None, + startswith: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None, + endswith: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None, ignore_case=False): """ Check text for one of pattern. Only one mode can be used in one filter. @@ -232,6 +232,9 @@ class Text(Filter): elif check == 0: raise ValueError(f"No one mode is specified!") + equals, contains, endswith, startswith = map(lambda e: [e] if isinstance(e, str) or isinstance(e, LazyProxy) + else e, + (equals, contains, endswith, startswith)) self.equals = equals self.contains = contains self.endswith = endswith @@ -267,25 +270,17 @@ class Text(Filter): text = text.lower() if self.equals is not None: - self.equals = str(self.equals) - if self.ignore_case: - self.equals = self.equals.lower() - return text == self.equals + self.equals = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.equals)) + return text in self.equals elif self.contains is not None: - self.contains = str(self.contains) - if self.ignore_case: - self.contains = self.contains.lower() - return self.contains in text + self.contains = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.contains)) + return any(map(text.__contains__, self.contains)) elif self.startswith is not None: - self.startswith = str(self.startswith) - if self.ignore_case: - self.startswith = self.startswith.lower() - return text.startswith(self.startswith) + self.startswith = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.startswith)) + return any(map(text.startswith, self.startswith)) elif self.endswith is not None: - self.endswith = str(self.endswith) - if self.ignore_case: - self.endswith = self.endswith.lower() - return text.endswith(self.endswith) + self.endswith = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.endswith)) + return any(map(text.endswith, self.endswith)) return False From a057558ecd24acc23f92f9b5063372f344d02cf0 Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 27 Jul 2019 12:55:08 +0300 Subject: [PATCH 3/7] Fix contains to check if text contains everything from list instead of just something --- aiogram/dispatcher/filters/builtin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index eb3845e1..386ed7ef 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -274,7 +274,7 @@ class Text(Filter): return text in self.equals elif self.contains is not None: self.contains = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.contains)) - return any(map(text.__contains__, self.contains)) + return all(map(text.__contains__, self.contains)) elif self.startswith is not None: self.startswith = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.startswith)) return any(map(text.startswith, self.startswith)) From 55c7c2792542016efc69d8c8ef87a14ab7e1878c Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 27 Jul 2019 13:26:57 +0300 Subject: [PATCH 4/7] add tests for multiple text filter --- tests/test_filters.py | 130 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/tests/test_filters.py b/tests/test_filters.py index e0002381..288aa0a4 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -55,6 +55,56 @@ class TestTextFilter: assert await check(InlineQuery(query=test_text)) assert await check(Poll(question=test_text)) + @pytest.mark.asyncio + @pytest.mark.parametrize("test_prefix_list, test_text, ignore_case", + [(['not_example', ''], '', True), + (['', 'not_example'], 'exAmple_string', True), + (['not_example', ''], '', False), + (['', 'not_example'], 'exAmple_string', False), + + (['example_string', 'not_example'], 'example_string', True), + (['not_example', 'example_string'], 'exAmple_string', True), + (['exAmple_string', 'not_example'], 'example_string', True), + + (['not_example', 'example_string'], 'example_string', False), + (['example_string', 'not_example'], 'exAmple_string', False), + (['not_example', 'exAmple_string'], 'example_string', False), + + (['example_string', 'not_example'], 'example_string_dsf', True), + (['not_example', 'example_string'], 'example_striNG_dsf', True), + (['example_striNG', 'not_example'], 'example_string_dsf', True), + + (['not_example', 'example_string'], 'example_string_dsf', False), + (['example_string', 'not_example'], 'example_striNG_dsf', False), + (['not_example', 'example_striNG'], 'example_string_dsf', False), + + (['example_string', 'not_example'], 'not_example_string', True), + (['not_example', 'example_string'], 'not_eXample_string', True), + (['EXample_string', 'not_example'], 'not_example_string', True), + + (['not_example', 'example_string'], 'not_example_string', False), + (['example_string', 'not_example'], 'not_eXample_string', False), + (['not_example', 'EXample_string'], 'not_example_string', False), + ]) + async def test_startswith_list(self, test_prefix_list, test_text, ignore_case): + test_filter = Text(startswith=test_prefix_list, ignore_case=ignore_case) + + async def check(obj): + result = await test_filter.check(obj) + if ignore_case: + _test_prefix_list = map(str.lower, test_prefix_list) + _test_text = test_text.lower() + else: + _test_prefix_list = test_prefix_list + _test_text = test_text + + return result is any(map(_test_text.startswith, _test_prefix_list)) + + assert await check(Message(text=test_text)) + assert await check(CallbackQuery(data=test_text)) + assert await check(InlineQuery(query=test_text)) + assert await check(Poll(question=test_text)) + @pytest.mark.asyncio @pytest.mark.parametrize("test_postfix, test_text, ignore_case", [('', '', True), @@ -105,6 +155,55 @@ class TestTextFilter: assert await check(InlineQuery(query=test_text)) assert await check(Poll(question=test_text)) + @pytest.mark.asyncio + @pytest.mark.parametrize("test_postfix_list, test_text, ignore_case", + [(['', 'not_example'], '', True), + (['not_example', ''], 'exAmple_string', True), + (['', 'not_example'], '', False), + (['not_example', ''], 'exAmple_string', False), + + (['example_string', 'not_example'], 'example_string', True), + (['not_example', 'example_string'], 'exAmple_string', True), + (['exAmple_string', 'not_example'], 'example_string', True), + + (['example_string', 'not_example'], 'example_string', False), + (['not_example', 'example_string'], 'exAmple_string', False), + (['exAmple_string', 'not_example'], 'example_string', False), + + (['example_string', 'not_example'], 'example_string_dsf', True), + (['not_example', 'example_string'], 'example_striNG_dsf', True), + (['example_striNG', 'not_example'], 'example_string_dsf', True), + + (['not_example', 'example_string'], 'example_string_dsf', False), + (['example_string', 'not_example'], 'example_striNG_dsf', False), + (['not_example', 'example_striNG'], 'example_string_dsf', False), + + (['not_example', 'example_string'], 'not_example_string', True), + (['example_string', 'not_example'], 'not_eXample_string', True), + (['not_example', 'EXample_string'], 'not_eXample_string', True), + + (['not_example', 'example_string'], 'not_example_string', False), + (['example_string', 'not_example'], 'not_eXample_string', False), + (['not_example', 'EXample_string'], 'not_example_string', False), + ]) + async def test_endswith_list(self, test_postfix_list, test_text, ignore_case): + test_filter = Text(endswith=test_postfix_list, ignore_case=ignore_case) + + async def check(obj): + result = await test_filter.check(obj) + if ignore_case: + _test_postfix_list = map(str.lower, test_postfix_list) + _test_text = test_text.lower() + else: + _test_postfix_list = test_postfix_list + _test_text = test_text + + return result is any(map(_test_text.endswith, _test_postfix_list)) + assert await check(Message(text=test_text)) + assert await check(CallbackQuery(data=test_text)) + assert await check(InlineQuery(query=test_text)) + assert await check(Poll(question=test_text)) + @pytest.mark.asyncio @pytest.mark.parametrize("test_string, test_text, ignore_case", [('', '', True), @@ -155,6 +254,37 @@ class TestTextFilter: assert await check(InlineQuery(query=test_text)) assert await check(Poll(question=test_text)) + @pytest.mark.asyncio + @pytest.mark.parametrize("test_filter_list, test_text, ignore_case", + [(['a', 'ab', 'abc'], 'A', True), + (['a', 'ab', 'abc'], 'ab', True), + (['a', 'ab', 'abc'], 'aBc', True), + (['a', 'ab', 'abc'], 'd', True), + + (['a', 'ab', 'abc'], 'A', False), + (['a', 'ab', 'abc'], 'ab', False), + (['a', 'ab', 'abc'], 'aBc', False), + (['a', 'ab', 'abc'], 'd', False), + ]) + async def test_contains_list(self, test_filter_list, test_text, ignore_case): + test_filter = Text(contains=test_filter_list, ignore_case=ignore_case) + + async def check(obj): + result = await test_filter.check(obj) + if ignore_case: + _test_filter_list = list(map(str.lower, test_filter_list)) + _test_text = test_text.lower() + else: + _test_filter_list = test_filter_list + _test_text = test_text + + return result is all(map(_test_text.__contains__, _test_filter_list)) + + assert await check(Message(text=test_text)) + assert await check(CallbackQuery(data=test_text)) + assert await check(InlineQuery(query=test_text)) + assert await check(Poll(question=test_text)) + @pytest.mark.asyncio @pytest.mark.parametrize("test_filter_text, test_text, ignore_case", [('', '', True), From e4b0c2a3dfa8b30f72e6e4dcced1356498455319 Mon Sep 17 00:00:00 2001 From: birdi Date: Sun, 28 Jul 2019 11:59:20 +0300 Subject: [PATCH 5/7] Add example of usage of multiple text filter --- examples/text_filter_example.py | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 examples/text_filter_example.py diff --git a/examples/text_filter_example.py b/examples/text_filter_example.py new file mode 100644 index 00000000..a47a5c92 --- /dev/null +++ b/examples/text_filter_example.py @@ -0,0 +1,50 @@ +""" +This is a bot to show the usage of the builtin Text filter +Instead of a list, a single element can be passed to any filter, it will be treated as list with an element +""" + +import logging + +from aiogram import Bot, Dispatcher, executor, types + +API_TOKEN = 'API_TOKEN_HERE' + +# Configure logging +logging.basicConfig(level=logging.INFO) + +# Initialize bot and dispatcher +bot = Bot(token=API_TOKEN) +dp = Dispatcher(bot) + +# if the text from user in the list +@dp.message_handler(text=['text1', 'text2']) +async def text_in_handler(message: types.Message): + await message.answer("The message text is in the list!") + +# if the text contains any string +@dp.message_handler(text_contains='example1') +@dp.message_handler(text_contains='example2') +async def text_contains_any_handler(message: types.Message): + await message.answer("The message text contains any of strings") + + +# if the text contains all the strings from the list +@dp.message_handler(text_contains=['str1', 'str2']) +async def text_contains_all_handler(message: types.Message): + await message.answer("The message text contains all strings from the list") + + +# if the text starts with any string from the list +@dp.message_handler(text_startswith=['prefix1', 'prefix2']) +async def text_startswith_handler(message: types.Message): + await message.answer("The message text starts with any of prefixes") + + +# if the text ends with any string from the list +@dp.message_handler(text_endswith=['postfix1', 'postfix2']) +async def text_endswith_handler(message: types.Message): + await message.answer("The message text ends with any of postfixes") + + +if __name__ == '__main__': + executor.start_polling(dp, skip_updates=True) From 321aa404c3f8350d8718f0eb46dc4aa70438ea69 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 5 Aug 2019 10:02:06 +0300 Subject: [PATCH 6/7] Add docstrings to the Text filter --- aiogram/dispatcher/filters/builtin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 386ed7ef..af328a13 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -213,11 +213,12 @@ class Text(Filter): ignore_case=False): """ Check text for one of pattern. Only one mode can be used in one filter. + In every pattern, a single string is treated as a list with 1 element. - :param equals: - :param contains: - :param startswith: - :param endswith: + :param equals: True if object text in the list + :param contains: True if object text contains all strings from the list + :param startswith: True if object text startswith any of strings from the list + :param endswith: True if object text endswith any of strings from the list :param ignore_case: case insensitive """ # Only one mode can be used. check it. From 6246432a0c6196974244b15bddea27733e1562fe Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 5 Aug 2019 10:12:48 +0300 Subject: [PATCH 7/7] Improve Text filter docs --- aiogram/dispatcher/filters/builtin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index af328a13..df72b9d0 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -215,10 +215,10 @@ class Text(Filter): Check text for one of pattern. Only one mode can be used in one filter. In every pattern, a single string is treated as a list with 1 element. - :param equals: True if object text in the list - :param contains: True if object text contains all strings from the list - :param startswith: True if object text startswith any of strings from the list - :param endswith: True if object text endswith any of strings from the list + :param equals: True if object's text in the list + :param contains: True if object's text contains all strings from the list + :param startswith: True if object's text starts with any of strings from the list + :param endswith: True if object's text ends with any of strings from the list :param ignore_case: case insensitive """ # Only one mode can be used. check it.