diff --git a/CHANGES/1170.removal.rst b/CHANGES/1170.removal.rst new file mode 100644 index 00000000..c2a06444 --- /dev/null +++ b/CHANGES/1170.removal.rst @@ -0,0 +1,3 @@ +Removed text filter in due to is planned to remove this filter few versions ago. + +Use :code:`F.text` instead diff --git a/aiogram/filters/__init__.py b/aiogram/filters/__init__.py index b3bee0d8..bcadc178 100644 --- a/aiogram/filters/__init__.py +++ b/aiogram/filters/__init__.py @@ -19,14 +19,12 @@ from .exception import ExceptionMessageFilter, ExceptionTypeFilter from .logic import and_f, invert_f, or_f from .magic_data import MagicData from .state import StateFilter -from .text import Text BaseFilter = Filter __all__ = ( "Filter", "BaseFilter", - "Text", "Command", "CommandObject", "CommandStart", diff --git a/aiogram/filters/text.py b/aiogram/filters/text.py deleted file mode 100644 index bdef26f0..00000000 --- a/aiogram/filters/text.py +++ /dev/null @@ -1,136 +0,0 @@ -from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Union - -from aiogram.filters.base import Filter -from aiogram.types import CallbackQuery, InlineQuery, Message, Poll - -if TYPE_CHECKING: - from aiogram.utils.i18n.lazy_proxy import LazyProxy # NOQA - -TextType = Union[str, "LazyProxy"] - - -class Text(Filter): - """ - Is useful for filtering text :class:`aiogram.types.message.Message`, - any :class:`aiogram.types.callback_query.CallbackQuery` with `data`, - :class:`aiogram.types.inline_query.InlineQuery` or :class:`aiogram.types.poll.Poll` question. - - .. warning:: - - Only one of `text`, `contains`, `startswith` or `endswith` argument can be used at once. - Any of that arguments can be string, list, set or tuple of strings. - - .. deprecated:: 3.0 - - use :ref:`magic-filter `. For example do :pycode:`F.text == "text"` instead - """ - - __slots__ = ( - "text", - "contains", - "startswith", - "endswith", - "ignore_case", - ) - - def __init__( - self, - text: Optional[Union[Sequence[TextType], TextType]] = None, - *, - contains: Optional[Union[Sequence[TextType], TextType]] = None, - startswith: Optional[Union[Sequence[TextType], TextType]] = None, - endswith: Optional[Union[Sequence[TextType], TextType]] = None, - ignore_case: bool = False, - ): - """ - - :param text: Text equals value or one of values - :param contains: Text contains value or one of values - :param startswith: Text starts with value or one of values - :param endswith: Text ends with value or one of values - :param ignore_case: Ignore case when checks - """ - self._validate_constraints( - text=text, - contains=contains, - startswith=startswith, - endswith=endswith, - ) - self.text = self._prepare_argument(text) - self.contains = self._prepare_argument(contains) - self.startswith = self._prepare_argument(startswith) - self.endswith = self._prepare_argument(endswith) - self.ignore_case = ignore_case - - def __str__(self) -> str: - return self._signature_to_string( - text=self.text, - contains=self.contains, - startswith=self.startswith, - endswith=self.endswith, - ignore_case=self.ignore_case, - ) - - @classmethod - def _prepare_argument( - cls, value: Optional[Union[Sequence[TextType], TextType]] - ) -> Optional[Sequence[TextType]]: - from aiogram.utils.i18n.lazy_proxy import LazyProxy - - if isinstance(value, (str, LazyProxy)): - return [value] - return value - - @classmethod - def _validate_constraints(cls, **values: Any) -> None: - # Validate that only one text filter type is presented - used_args = {key for key, value in values.items() if value is not None} - if len(used_args) < 1: - raise ValueError(f"Filter should contain one of arguments: {set(values.keys())}") - if len(used_args) > 1: - raise ValueError(f"Arguments {used_args} cannot be used together") - - async def __call__( - self, obj: Union[Message, CallbackQuery, InlineQuery, Poll] - ) -> Union[bool, Dict[str, Any]]: - if isinstance(obj, Message): - text = obj.text or obj.caption or "" - if not text and obj.poll: - text = obj.poll.question - elif isinstance(obj, CallbackQuery) and obj.data: - text = obj.data - elif isinstance(obj, InlineQuery): - text = obj.query - elif isinstance(obj, Poll): - text = obj.question - else: - return False - - if not text: - return False - if self.ignore_case: - text = text.lower() - - if self.text is not None: - equals = map(self.prepare_text, self.text) - return text in equals - - if self.contains is not None: - contains = map(self.prepare_text, self.contains) - return all(map(text.__contains__, contains)) - - if self.startswith is not None: - startswith = map(self.prepare_text, self.startswith) - return any(map(text.startswith, startswith)) - - if self.endswith is not None: - endswith = map(self.prepare_text, self.endswith) - return any(map(text.endswith, endswith)) - - # Impossible because the validator prevents this situation - return False # pragma: no cover - - def prepare_text(self, text: str) -> str: - if self.ignore_case: - return str(text).lower() - return str(text) diff --git a/docs/dispatcher/filters/index.rst b/docs/dispatcher/filters/index.rst index 856b8677..9ac14213 100644 --- a/docs/dispatcher/filters/index.rst +++ b/docs/dispatcher/filters/index.rst @@ -16,7 +16,6 @@ Here is list of builtin filters: :maxdepth: 1 command - text chat_member_updated magic_filters magic_data @@ -69,7 +68,7 @@ If you specify multiple filters in a row, it will be checked with an "and" condi .. code-block:: python - @.message(Text(startswith="show"), Text(endswith="example")) + @.message(F.text.startswith("show"), F.text.endswith("example")) Also, if you want to use two alternative ways to run the same handler ("or" condition) @@ -77,7 +76,7 @@ you can register the handler twice or more times as you like .. code-block:: python - @.message(Text(text="hi")) + @.message(F.text == "hi") @.message(CommandStart()) @@ -96,7 +95,7 @@ An alternative way is to combine using special functions (:func:`and_f`, :func:` .. code-block:: python - and_f(Text(startswith="show"), Text(endswith="example")) - or_f(Text(text="hi"), CommandStart()) + and_f(F.text.startswith("show"), F.text.endswith("example")) + or_f(F.text(text="hi"), CommandStart()) invert_f(IsAdmin()) and_f(, or_f(, )) diff --git a/docs/dispatcher/filters/text.rst b/docs/dispatcher/filters/text.rst deleted file mode 100644 index 622f41d8..00000000 --- a/docs/dispatcher/filters/text.rst +++ /dev/null @@ -1,35 +0,0 @@ -==== -Text -==== - -.. autoclass:: aiogram.filters.text.Text - :members: - :member-order: bysource - :undoc-members: False - -Can be imported: - -- :code:`from aiogram.filters.text import Text` -- :code:`from aiogram.filters import Text` - -Usage -===== - -#. Text equals with the specified value: :code:`Text(text="text") # value == 'text'` -#. Text starts with the specified value: :code:`Text(startswith="text") # value.startswith('text')` -#. Text ends with the specified value: :code:`Text(endswith="text") # value.endswith('text')` -#. Text contains the specified value: :code:`Text(contains="text") # value in 'text'` -#. Any of previous listed filters can be list, set or tuple of strings that's mean any of listed value should be equals/startswith/endswith/contains: :code:`Text(text=["text", "spam"])` -#. Ignore case can be combined with any previous listed filter: :code:`Text(text="Text", ignore_case=True) # value.lower() == 'text'.lower()` - -Allowed handlers -================ - -Allowed update types for this filter: - -- :code:`message` -- :code:`edited_message` -- :code:`channel_post` -- :code:`edited_channel_post` -- :code:`inline_query` -- :code:`callback_query` diff --git a/docs/locale/uk_UA/LC_MESSAGES/dispatcher/filters/text.po b/docs/locale/uk_UA/LC_MESSAGES/dispatcher/filters/text.po deleted file mode 100644 index a19ae4ad..00000000 --- a/docs/locale/uk_UA/LC_MESSAGES/dispatcher/filters/text.po +++ /dev/null @@ -1,153 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) 2022, aiogram Team -# This file is distributed under the same license as the aiogram package. -# FIRST AUTHOR , 2022. -# -msgid "" -msgstr "" -"Project-Id-Version: aiogram\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-10-25 22:10+0300\n" -"PO-Revision-Date: 2022-10-25 17:49+0300\n" -"Last-Translator: \n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.3\n" - -#: ../../dispatcher/filters/text.rst:3 -msgid "Text" -msgstr "Текст" - -#: aiogram.filters.text.Text:1 of -msgid "" -"Is useful for filtering text :class:`aiogram.types.message.Message`, any " -":class:`aiogram.types.callback_query.CallbackQuery` with `data`, " -":class:`aiogram.types.inline_query.InlineQuery` or " -":class:`aiogram.types.poll.Poll` question." -msgstr "" -"Корисно для фільтрації тексту :class:`aiogram.types.message.Message`, " -"будь-якого :class:`aiogram.types.callback_query.CallbackQuery` з `data`, " -":class:`aiogram.types.inline_query.InlineQuery` або : " -"class:`aiogram.types.poll.Poll` опитування." - -#: aiogram.filters.text.Text:7 of -msgid "" -"Only one of `text`, `contains`, `startswith` or `endswith` argument can " -"be used at once. Any of that arguments can be string, list, set or tuple " -"of strings." -msgstr "" -"Одночасно можна використати лише один із аргументів `text`, `contains`, " -"`startswith` або `endswith` . Будь-який із цих аргументів може бути " -"рядком, списком, набором (set) або кортежем рядків." - -#: aiogram.filters.text.Text:12 of -msgid "" -"use :ref:`magic-filter `. For example do :pycode:`F.text " -"== \"text\"` instead" -msgstr "" -"використати :ref:`magic-filter `. Наприклад " -":pycode:`F.text == \"text\"` instead" - -#: ../../dispatcher/filters/text.rst:10 -msgid "Can be imported:" -msgstr "Можна імпортувати:" - -#: ../../dispatcher/filters/text.rst:12 -msgid ":code:`from aiogram.filters.text import Text`" -msgstr ":code:`from aiogram.filters.text import Text`" - -#: ../../dispatcher/filters/text.rst:13 -msgid ":code:`from aiogram.filters import Text`" -msgstr ":code:`from aiogram.filters import Text`" - -#: ../../dispatcher/filters/text.rst:16 -msgid "Usage" -msgstr "Використання" - -#: ../../dispatcher/filters/text.rst:18 -msgid "" -"Text equals with the specified value: :code:`Text(text=\"text\") # value" -" == 'text'`" -msgstr "" -"Текст дорівнює вказаному значенню: :code:`Text(text=\"text\") # value ==" -" 'text'`" - -#: ../../dispatcher/filters/text.rst:19 -msgid "" -"Text starts with the specified value: :code:`Text(startswith=\"text\") #" -" value.startswith('text')`" -msgstr "" -"Текст починається з указаного значення: :code:`Text(startswith=\"text\")" -" # value.startswith('text')`" - -#: ../../dispatcher/filters/text.rst:20 -msgid "" -"Text ends with the specified value: :code:`Text(endswith=\"text\") # " -"value.endswith('text')`" -msgstr "" -"Текст закінчується вказаним значенням: :code:`Text(endswith=\"text\") # " -"value.endswith('text')`" - -#: ../../dispatcher/filters/text.rst:21 -msgid "" -"Text contains the specified value: :code:`Text(contains=\"text\") # " -"value in 'text'`" -msgstr "" -"Текст містить вказане значення: :code:`Text(contains=\"text\") # value " -"in 'text'`" - -#: ../../dispatcher/filters/text.rst:22 -msgid "" -"Any of previous listed filters can be list, set or tuple of strings " -"that's mean any of listed value should be " -"equals/startswith/endswith/contains: :code:`Text(text=[\"text\", " -"\"spam\"])`" -msgstr "" -"Будь-який із попередніх перерахованих фільтрів може бути списком, набором" -" або кортежем рядків, що означає, що будь-яке значення у списку має " -"дорівнювати/починатися/закінчуватися/містити: :code:`Text(text=[\"text\"," -" \"spam\"])`" - -#: ../../dispatcher/filters/text.rst:23 -msgid "" -"Ignore case can be combined with any previous listed filter: " -":code:`Text(text=\"Text\", ignore_case=True) # value.lower() == " -"'text'.lower()`" -msgstr "" -"Ігнорування регістру можна поєднати з будь-яким фільтром із попереднього " -"списку: :code:`Text(text=\"Text\", ignore_case=True) # value.lower() == " -"'text'.lower()`" - -#: ../../dispatcher/filters/text.rst:26 -msgid "Allowed handlers" -msgstr "Дозволені обробники (handlers)" - -#: ../../dispatcher/filters/text.rst:28 -msgid "Allowed update types for this filter:" -msgstr "Дозволені типи оновлень для цього фільтра:" - -#: ../../dispatcher/filters/text.rst:30 -msgid ":code:`message`" -msgstr ":code:`message`" - -#: ../../dispatcher/filters/text.rst:31 -msgid ":code:`edited_message`" -msgstr ":code:`edited_message`" - -#: ../../dispatcher/filters/text.rst:32 -msgid ":code:`channel_post`" -msgstr ":code:`channel_post`" - -#: ../../dispatcher/filters/text.rst:33 -msgid ":code:`edited_channel_post`" -msgstr ":code:`edited_channel_post`" - -#: ../../dispatcher/filters/text.rst:34 -msgid ":code:`inline_query`" -msgstr ":code:`inline_query`" - -#: ../../dispatcher/filters/text.rst:35 -msgid ":code:`callback_query`" -msgstr ":code:`callback_query`" diff --git a/pyproject.toml b/pyproject.toml index f5032850..faf6d02c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,7 @@ test = [ "pytest-cov~=4.0.0", "pytest-aiohttp~=1.0.4", "aresponses~=2.1.6", + "pytz~=2022.7.1" ] docs = [ "Sphinx~=5.2.3", diff --git a/tests/test_filters/test_logic.py b/tests/test_filters/test_logic.py index 9c4d4f48..b1382766 100644 --- a/tests/test_filters/test_logic.py +++ b/tests/test_filters/test_logic.py @@ -1,6 +1,6 @@ import pytest -from aiogram.filters import Text, and_f, invert_f, or_f +from aiogram.filters import Command, and_f, invert_f, or_f from aiogram.filters.logic import _AndFilter, _InvertFilter, _OrFilter @@ -28,10 +28,10 @@ class TestLogic: @pytest.mark.parametrize( "case,type_", [ - [or_f(Text(text="test"), Text(text="test")), _OrFilter], - [and_f(Text(text="test"), Text(text="test")), _AndFilter], - [invert_f(Text(text="test")), _InvertFilter], - [~Text(text="test"), _InvertFilter], + [or_f(Command("test"), Command("test")), _OrFilter], + [and_f(Command("test"), Command("test")), _AndFilter], + [invert_f(Command("test")), _InvertFilter], + [~Command("test"), _InvertFilter], ], ) def test_dunder_methods(self, case, type_): diff --git a/tests/test_filters/test_text.py b/tests/test_filters/test_text.py deleted file mode 100644 index de823097..00000000 --- a/tests/test_filters/test_text.py +++ /dev/null @@ -1,245 +0,0 @@ -import datetime -from itertools import permutations -from typing import Sequence, Type - -import pytest - -from aiogram.filters import Text -from aiogram.types import ( - CallbackQuery, - Chat, - InlineQuery, - Message, - Poll, - PollOption, - User, -) - - -class TestText: - @pytest.mark.parametrize( - "kwargs", - [ - {}, - {"ignore_case": True}, - {"ignore_case": False}, - ], - ) - def test_not_enough_arguments(self, kwargs): - with pytest.raises(ValueError): - Text(**kwargs) - - @pytest.mark.parametrize( - "first,last", - permutations(["text", "contains", "startswith", "endswith"], 2), - ) - @pytest.mark.parametrize("ignore_case", [True, False]) - def test_validator_too_few_arguments(self, first, last, ignore_case): - kwargs = {first: "test", last: "test", "ignore_case": ignore_case} - - with pytest.raises(ValueError): - Text(**kwargs) - - @pytest.mark.parametrize("argument", ["text", "contains", "startswith", "endswith"]) - @pytest.mark.parametrize("input_type", [str, list, tuple]) - def test_validator_convert_to_list(self, argument: str, input_type: Type): - text = Text(**{argument: input_type("test")}) - assert hasattr(text, argument) - assert isinstance(getattr(text, argument), Sequence) - - @pytest.mark.parametrize( - "argument,ignore_case,input_value,update_type,result", - [ - [ - "text", - False, - "test", - Message( - message_id=42, - date=datetime.datetime.now(), - chat=Chat(id=42, type="private"), - from_user=User(id=42, is_bot=False, first_name="Test"), - ), - False, - ], - [ - "text", - False, - "test", - Message( - message_id=42, - date=datetime.datetime.now(), - caption="test", - chat=Chat(id=42, type="private"), - from_user=User(id=42, is_bot=False, first_name="Test"), - ), - True, - ], - [ - "text", - False, - "test", - Message( - message_id=42, - date=datetime.datetime.now(), - text="test", - chat=Chat(id=42, type="private"), - from_user=User(id=42, is_bot=False, first_name="Test"), - ), - True, - ], - [ - "text", - True, - "TEst", - Message( - message_id=42, - date=datetime.datetime.now(), - text="tesT", - chat=Chat(id=42, type="private"), - from_user=User(id=42, is_bot=False, first_name="Test"), - ), - True, - ], - [ - "text", - False, - "TEst", - Message( - message_id=42, - date=datetime.datetime.now(), - text="tesT", - chat=Chat(id=42, type="private"), - from_user=User(id=42, is_bot=False, first_name="Test"), - ), - False, - ], - [ - "startswith", - False, - "test", - Message( - message_id=42, - date=datetime.datetime.now(), - text="test case", - chat=Chat(id=42, type="private"), - from_user=User(id=42, is_bot=False, first_name="Test"), - ), - True, - ], - [ - "endswith", - False, - "case", - Message( - message_id=42, - date=datetime.datetime.now(), - text="test case", - chat=Chat(id=42, type="private"), - from_user=User(id=42, is_bot=False, first_name="Test"), - ), - True, - ], - [ - "contains", - False, - " ", - Message( - message_id=42, - date=datetime.datetime.now(), - text="test case", - chat=Chat(id=42, type="private"), - from_user=User(id=42, is_bot=False, first_name="Test"), - ), - True, - ], - [ - "startswith", - True, - "question", - Message( - message_id=42, - date=datetime.datetime.now(), - poll=Poll( - id="poll id", - question="Question?", - options=[PollOption(text="A", voter_count=0)], - is_closed=False, - is_anonymous=False, - type="regular", - allows_multiple_answers=False, - total_voter_count=0, - ), - chat=Chat(id=42, type="private"), - from_user=User(id=42, is_bot=False, first_name="Test"), - ), - True, - ], - [ - "startswith", - True, - "callback:", - CallbackQuery( - id="query id", - from_user=User(id=42, is_bot=False, first_name="Test"), - chat_instance="instance", - data="callback:data", - ), - True, - ], - [ - "startswith", - True, - "query", - InlineQuery( - id="query id", - from_user=User(id=42, is_bot=False, first_name="Test"), - query="query line", - offset="offset", - ), - True, - ], - [ - "text", - True, - "question", - Poll( - id="poll id", - question="Question", - options=[PollOption(text="A", voter_count=0)], - is_closed=False, - is_anonymous=False, - type="regular", - allows_multiple_answers=False, - total_voter_count=0, - ), - True, - ], - [ - "text", - True, - ["question", "another question"], - Poll( - id="poll id", - question="Another question", - options=[PollOption(text="A", voter_count=0)], - is_closed=False, - is_anonymous=False, - type="quiz", - allows_multiple_answers=False, - total_voter_count=0, - correct_option_id=0, - ), - True, - ], - ["text", True, ["question", "another question"], object(), False], - ], - ) - async def test_check_text(self, argument, ignore_case, input_value, result, update_type): - text = Text(**{argument: input_value}, ignore_case=ignore_case) - test = await text(update_type) - assert test is result - - def test_str(self): - text = Text("test") - assert str(text) == "Text(text=['test'], ignore_case=False)"