Remove filters factory, introduce docs translation (#978)

* Rewrite filters

* Update README.rst

* Fixed tests

* Small optimization of the Text filter (TY to @bomzheg)

* Remove dataclass slots argument in due to the only Python 3.10 has an slots argument

* Fixed mypy

* Update tests

* Disable Python 3.11

* Fixed #1013: Empty mention should be None instead of empty string.

* Added #990 to the changelog

* Added #942 to the changelog

* Fixed coverage

* Update poetry and dependencies

* Fixed mypy

* Remove deprecated code

* Added more tests, update pyproject.toml

* Partial update docs

* Added initial Docs translation files

* Added more changes

* Added log message when connection is established in polling process

* Fixed action

* Disable lint for PyPy

* Added changelog for docs translation
This commit is contained in:
Alex Root Junior 2022-10-02 00:04:31 +03:00 committed by GitHub
parent 94030903ec
commit f4251382e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
610 changed files with 61738 additions and 1687 deletions

View file

@ -2,7 +2,8 @@ from typing import Awaitable
import pytest
from aiogram.filters import BaseFilter
from aiogram.filters import Filter
from aiogram.filters.base import _InvertFilter
try:
from asynctest import CoroutineMock, patch
@ -13,16 +14,14 @@ except ImportError:
pytestmark = pytest.mark.asyncio
class MyFilter(BaseFilter):
foo: str
class MyFilter(Filter):
async def __call__(self, event: str):
return
class TestBaseFilter:
async def test_awaitable(self):
my_filter = MyFilter(foo="bar")
my_filter = MyFilter()
assert isinstance(my_filter, Awaitable)
@ -33,3 +32,20 @@ class TestBaseFilter:
call = my_filter(event="test")
await call
mocked_call.assert_awaited_with(event="test")
async def test_invert(self):
my_filter = MyFilter()
my_inverted_filter = ~my_filter
assert str(my_inverted_filter) == f"~{str(my_filter)}"
assert isinstance(my_inverted_filter, _InvertFilter)
with patch(
"tests.test_filters.test_base.MyFilter.__call__",
new_callable=CoroutineMock,
) as mocked_call:
call = my_inverted_filter(event="test")
result = await call
mocked_call.assert_awaited_with(event="test")
assert not result

View file

@ -179,3 +179,7 @@ class TestCallbackDataFilter:
async def test_invalid_call(self):
filter_object = MyCallback.filter(F.test)
assert not await filter_object(User(id=42, is_bot=False, first_name="test"))
def test_str(self):
filter_object = MyCallback.filter(F.test)
assert str(filter_object).startswith("CallbackQueryFilter(callback_data=")

View file

@ -343,3 +343,7 @@ class TestChatMemberUpdatedStatusFilter:
)
assert await updated_filter(event) is result
def test_str(self):
updated_filter = ChatMemberUpdatedFilter(member_status_changed=JOIN_TRANSITION)
assert str(updated_filter).startswith("ChatMemberUpdatedFilter(member_status_changed=")

View file

@ -6,44 +6,67 @@ import pytest
from aiogram import F
from aiogram.filters import Command, CommandObject
from aiogram.filters.command import CommandStart
from aiogram.types import Chat, Message, User
from aiogram.types import BotCommand, Chat, Message, User
from tests.mocked_bot import MockedBot
pytestmark = pytest.mark.asyncio
class TestCommandFilter:
def test_commands_not_iterable(self):
with pytest.raises(ValueError):
Command(commands=1)
def test_bad_type(self):
with pytest.raises(ValueError):
Command(1)
def test_without_args(self):
with pytest.raises(ValueError):
Command()
def test_resolve_bot_command(self):
command = Command(BotCommand(command="test", description="Test"))
assert isinstance(command.commands[0], str)
assert command.commands[0] == "test"
def test_convert_to_list(self):
cmd = Command(commands="start")
assert cmd.commands
assert isinstance(cmd.commands, list)
assert isinstance(cmd.commands, tuple)
assert cmd.commands[0] == "start"
assert cmd == Command(commands=["start"])
# assert cmd == Command(commands=["start"])
@pytest.mark.parametrize(
"text,command,result",
[
["/test@tbot", Command(commands=["test"], commands_prefix="/"), True],
["!test", Command(commands=["test"], commands_prefix="/"), False],
["/test@mention", Command(commands=["test"], commands_prefix="/"), False],
["/tests", Command(commands=["test"], commands_prefix="/"), False],
["/", Command(commands=["test"], commands_prefix="/"), False],
["/ test", Command(commands=["test"], commands_prefix="/"), False],
["", Command(commands=["test"], commands_prefix="/"), False],
[" ", Command(commands=["test"], commands_prefix="/"), False],
["test", Command(commands=["test"], commands_prefix="/"), False],
[" test", Command(commands=["test"], commands_prefix="/"), False],
["a", Command(commands=["test"], commands_prefix="/"), False],
["/test@tbot", Command(commands=["test"], prefix="/"), True],
["/test@tbot", Command("test", prefix="/"), True],
[
"/test@tbot",
Command(BotCommand(command="test", description="description"), prefix="/"),
True,
],
["!test", Command(commands=["test"], prefix="/"), False],
["/test@mention", Command(commands=["test"], prefix="/"), False],
["/tests", Command(commands=["test"], prefix="/"), False],
["/", Command(commands=["test"], prefix="/"), False],
["/ test", Command(commands=["test"], prefix="/"), False],
["", Command(commands=["test"], prefix="/"), False],
[" ", Command(commands=["test"], prefix="/"), False],
["test", Command(commands=["test"], prefix="/"), False],
[" test", Command(commands=["test"], prefix="/"), False],
["a", Command(commands=["test"], prefix="/"), False],
["/test@tbot some args", Command(commands=["test"]), True],
["/test42@tbot some args", Command(commands=[re.compile(r"test(\d+)")]), True],
[
"/test42@tbot some args",
Command(commands=[re.compile(r"test(\d+)")], command_magic=F.args == "some args"),
Command(commands=[re.compile(r"test(\d+)")], magic=F.args == "some args"),
True,
],
[
"/test42@tbot some args",
Command(commands=[re.compile(r"test(\d+)")], command_magic=F.args == "test"),
Command(commands=[re.compile(r"test(\d+)")], magic=F.args == "test"),
False,
],
["/start test", CommandStart(), True],
@ -86,6 +109,7 @@ class TestCommandFilter:
),
True,
],
[None, False],
],
)
async def test_call(self, message: Message, result: bool, bot: MockedBot):
@ -99,11 +123,41 @@ class TestCommandFilter:
chat=Chat(id=42, type="private"),
date=datetime.datetime.now(),
)
command = Command(commands=["test"], command_magic=(F.args.as_("args")))
command = Command(commands=["test"], magic=(F.args.as_("args")))
result = await command(message=message, bot=bot)
assert "args" in result
assert result["args"] == "42"
async def test_empty_mention_is_none(self, bot: MockedBot):
# Fixed https://github.com/aiogram/aiogram/issues/1013:
# Empty mention should be None instead of empty string.
message = Message(
message_id=0,
text="/test",
chat=Chat(id=42, type="private"),
date=datetime.datetime.now(),
)
command = Command("test")
result = await command(message=message, bot=bot)
assert "command" in result
command_obj: CommandObject = result["command"]
assert command_obj.mention is None
def test_str(self):
cmd = Command(commands=["start"])
assert str(cmd) == "Command('start', prefix='/', ignore_case=False, ignore_mention=False)"
class TestCommandStart:
def test_str(self):
cmd = CommandStart()
assert (
str(cmd)
== "CommandStart(ignore_case=False, ignore_mention=False, deep_link=False, deep_link_encoded=False)"
)
class TestCommandObject:
@pytest.mark.parametrize(

View file

@ -1,52 +0,0 @@
from dataclasses import dataclass
from typing import cast
import pytest
from pydantic import ValidationError
from aiogram.filters import ContentTypesFilter
from aiogram.types import ContentType, Message
pytestmark = pytest.mark.asyncio
@dataclass
class MinimalMessage:
content_type: str
class TestContentTypesFilter:
def test_validator_empty_list(self):
filter_ = ContentTypesFilter(content_types=[])
assert filter_.content_types == []
def test_convert_to_list(self):
filter_ = ContentTypesFilter(content_types="text")
assert filter_.content_types
assert isinstance(filter_.content_types, list)
assert filter_.content_types[0] == "text"
assert filter_ == ContentTypesFilter(content_types=["text"])
@pytest.mark.parametrize("values", [["text", "photo"], ["sticker"]])
def test_validator_with_values(self, values):
filter_ = ContentTypesFilter(content_types=values)
assert filter_.content_types == values
@pytest.mark.parametrize("values", [["test"], ["text", "test"], ["TEXT"]])
def test_validator_with_bad_values(self, values):
with pytest.raises(ValidationError):
ContentTypesFilter(content_types=values)
@pytest.mark.parametrize(
"values,content_type,result",
[
[[ContentType.TEXT], ContentType.TEXT, True],
[[ContentType.PHOTO], ContentType.TEXT, False],
[[ContentType.ANY], ContentType.TEXT, True],
[[ContentType.TEXT, ContentType.PHOTO, ContentType.DOCUMENT], ContentType.TEXT, True],
[[ContentType.ANY, ContentType.PHOTO, ContentType.DOCUMENT], ContentType.TEXT, True],
],
)
async def test_call(self, values, content_type, result):
filter_ = ContentTypesFilter(content_types=values)
assert await filter_(cast(Message, MinimalMessage(content_type=content_type))) == result

View file

@ -5,6 +5,7 @@ import pytest
from aiogram import Dispatcher
from aiogram.filters import ExceptionMessageFilter, ExceptionTypeFilter
from aiogram.types import Update
from aiogram.types.error_event import ErrorEvent
pytestmark = pytest.mark.asyncio
@ -18,13 +19,17 @@ class TestExceptionMessageFilter:
async def test_match(self):
obj = ExceptionMessageFilter(pattern="KABOOM")
result = await obj(Update(update_id=0), exception=Exception())
result = await obj(ErrorEvent(update=Update(update_id=0), exception=Exception()))
assert not result
result = await obj(Update(update_id=0), exception=Exception("KABOOM"))
result = await obj(ErrorEvent(update=Update(update_id=0), exception=Exception("KABOOM")))
assert isinstance(result, dict)
assert "match_exception" in result
async def test_str(self):
obj = ExceptionMessageFilter(pattern="KABOOM")
assert str(obj) == "ExceptionMessageFilter(pattern=re.compile('KABOOM'))"
class MyException(Exception):
pass
@ -46,12 +51,16 @@ class TestExceptionTypeFilter:
],
)
async def test_check(self, exception: Exception, value: bool):
obj = ExceptionTypeFilter(exception=MyException)
obj = ExceptionTypeFilter(MyException)
result = await obj(Update(update_id=0), exception=exception)
result = await obj(ErrorEvent(update=Update(update_id=0), exception=exception))
assert result == value
def test_without_arguments(self):
with pytest.raises(ValueError):
ExceptionTypeFilter()
class TestDispatchException:
async def test_handle_exception(self, bot):
@ -62,7 +71,7 @@ class TestDispatchException:
raise ValueError("KABOOM")
@dp.errors(ExceptionMessageFilter(pattern="KABOOM"))
async def handler0(update, exception):
async def handler0(event):
return "Handled"
assert await dp.feed_update(bot, Update(update_id=0)) == "Handled"

View file

@ -1,37 +0,0 @@
import pytest
from aiogram.filters import Text, and_f, invert_f, or_f
from aiogram.filters.logic import _AndFilter, _InvertFilter, _OrFilter
class TestLogic:
@pytest.mark.parametrize(
"obj,case,result",
[
[True, and_f(lambda t: t is True, lambda t: t is True), True],
[True, and_f(lambda t: t is True, lambda t: t is False), False],
[True, and_f(lambda t: t is False, lambda t: t is False), False],
[True, and_f(lambda t: {"t": t}, lambda t: t is False), False],
[True, and_f(lambda t: {"t": t}, lambda t: t is True), {"t": True}],
[True, or_f(lambda t: t is True, lambda t: t is True), True],
[True, or_f(lambda t: t is True, lambda t: t is False), True],
[True, or_f(lambda t: t is False, lambda t: t is False), False],
[True, or_f(lambda t: t is False, lambda t: t is True), True],
[True, or_f(lambda t: t is False, lambda t: {"t": t}), {"t": True}],
[True, or_f(lambda t: {"t": t}, lambda t: {"a": 42}), {"t": True}],
[True, invert_f(lambda t: t is False), True],
],
)
async def test_logic(self, obj, case, result):
assert await case(obj) == result
@pytest.mark.parametrize(
"case,type_",
[
[Text(text="test") | Text(text="test"), _OrFilter],
[Text(text="test") & Text(text="test"), _AndFilter],
[~Text(text="test"), _InvertFilter],
],
)
def test_dunder_methods(self, case, type_):
assert isinstance(case, type_)

View file

@ -28,3 +28,7 @@ class TestMagicDataFilter:
assert called
assert isinstance(result, dict)
assert result["test"]
def test_str(self):
f = MagicData(magic_data=F.event.text == "test")
assert str(f).startswith("MagicData(magic_data=")

View file

@ -16,13 +16,11 @@ class MyGroup(StatesGroup):
class TestStateFilter:
@pytest.mark.parametrize(
"state", [None, State("test"), MyGroup, MyGroup(), "state", ["state"]]
)
@pytest.mark.parametrize("state", [None, State("test"), MyGroup, MyGroup(), "state"])
def test_validator(self, state):
f = StateFilter(state=state)
assert isinstance(f.state, list)
value = f.state[0]
f = StateFilter(state)
assert isinstance(f.states, tuple)
value = f.states[0]
assert (
isinstance(value, (State, str, MyGroup))
or (isclass(value) and issubclass(value, StatesGroup))
@ -32,17 +30,11 @@ class TestStateFilter:
@pytest.mark.parametrize(
"state,current_state,result",
[
[State("state"), "@:state", True],
[[State("state")], "@:state", True],
[MyGroup, "MyGroup:state", True],
[[MyGroup], "MyGroup:state", True],
[MyGroup(), "MyGroup:state", True],
[[MyGroup()], "MyGroup:state", True],
["*", "state", True],
[None, None, True],
[["*"], "state", True],
[[None], None, True],
[None, "state", False],
[[], "state", False],
[[State("state"), "state"], "state", True],
[[MyGroup(), State("state")], "@:state", True],
[[MyGroup, State("state")], "state", False],
@ -50,9 +42,13 @@ class TestStateFilter:
)
@pytestmark
async def test_filter(self, state, current_state, result):
f = StateFilter(state=state)
f = StateFilter(*state)
assert bool(await f(obj=Update(update_id=42), raw_state=current_state)) is result
def test_empty_filter(self):
with pytest.raises(ValueError):
StateFilter()
@pytestmark
async def test_create_filter_from_state(self):
FilterObject(callback=State(state="state"))
@ -72,3 +68,7 @@ class TestStateFilter:
states = {SG.state: "OK"}
assert states.get(copy(SG.state)) == "OK"
def test_str(self):
f = StateFilter("test")
assert str(f) == "StateFilter('test')"

View file

@ -3,50 +3,38 @@ from itertools import permutations
from typing import Sequence, Type
import pytest
from pydantic import ValidationError
from aiogram.filters import BUILTIN_FILTERS, Text
from aiogram.filters import Text
from aiogram.types import CallbackQuery, Chat, InlineQuery, Message, Poll, PollOption, User
pytestmark = pytest.mark.asyncio
class TestText:
def test_default_for_observer(self):
registered_for = {
update_type for update_type, filters in BUILTIN_FILTERS.items() if Text in filters
}
assert registered_for == {
"message",
"edited_message",
"channel_post",
"edited_channel_post",
"inline_query",
"callback_query",
}
def test_validator_not_enough_arguments(self):
with pytest.raises(ValidationError):
Text()
with pytest.raises(ValidationError):
Text(text_ignore_case=True)
@pytest.mark.parametrize(
"first,last",
permutations(["text", "text_contains", "text_startswith", "text_endswith"], 2),
"kwargs",
[
{},
{"ignore_case": True},
{"ignore_case": False},
],
)
@pytest.mark.parametrize("ignore_case", [True, False])
def test_validator_too_few_arguments(self, first, last, ignore_case):
kwargs = {first: "test", last: "test"}
if ignore_case:
kwargs["text_ignore_case"] = True
with pytest.raises(ValidationError):
def test_not_enough_arguments(self, kwargs):
with pytest.raises(ValueError):
Text(**kwargs)
@pytest.mark.parametrize(
"argument", ["text", "text_contains", "text_startswith", "text_endswith"]
"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")})
@ -121,7 +109,7 @@ class TestText:
False,
],
[
"text_startswith",
"startswith",
False,
"test",
Message(
@ -134,7 +122,7 @@ class TestText:
True,
],
[
"text_endswith",
"endswith",
False,
"case",
Message(
@ -147,7 +135,7 @@ class TestText:
True,
],
[
"text_contains",
"contains",
False,
" ",
Message(
@ -160,7 +148,7 @@ class TestText:
True,
],
[
"text_startswith",
"startswith",
True,
"question",
Message(
@ -182,7 +170,7 @@ class TestText:
True,
],
[
"text_startswith",
"startswith",
True,
"callback:",
CallbackQuery(
@ -194,7 +182,7 @@ class TestText:
True,
],
[
"text_startswith",
"startswith",
True,
"query",
InlineQuery(
@ -242,5 +230,10 @@ class TestText:
],
)
async def test_check_text(self, argument, ignore_case, input_value, result, update_type):
text = Text(**{argument: input_value}, text_ignore_case=ignore_case)
assert await text(obj=update_type) is result
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)"