From 22094eb477711e644924eaeb2424c8cef20e637a Mon Sep 17 00:00:00 2001 From: Forden Date: Wed, 5 Aug 2020 15:06:02 +0300 Subject: [PATCH 01/26] Patch 1 (#398) * Update callback_data_factory.py * Update callback_data_factory_simple.py --- examples/callback_data_factory.py | 2 +- examples/callback_data_factory_simple.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/callback_data_factory.py b/examples/callback_data_factory.py index 9a8affe9..2335ea95 100644 --- a/examples/callback_data_factory.py +++ b/examples/callback_data_factory.py @@ -112,7 +112,7 @@ async def query_post_vote(query: types.CallbackQuery, callback_data: dict): @dp.errors_handler(exception=MessageNotModified) async def message_not_modified_handler(update, error): - return True + return True # errors_handler must return True if error was handled correctly if __name__ == '__main__': diff --git a/examples/callback_data_factory_simple.py b/examples/callback_data_factory_simple.py index 5fc9c548..a6d246d5 100644 --- a/examples/callback_data_factory_simple.py +++ b/examples/callback_data_factory_simple.py @@ -61,7 +61,7 @@ async def callback_vote_action(query: types.CallbackQuery, callback_data: dict): @dp.errors_handler(exception=MessageNotModified) # handle the cases when this exception raises async def message_not_modified_handler(update, error): - return True + return True # errors_handler must return True if error was handled correctly if __name__ == '__main__': From 4863675d28e841fb0ec8b85e77940599d7b5f013 Mon Sep 17 00:00:00 2001 From: Forden Date: Fri, 14 Aug 2020 17:09:45 +0300 Subject: [PATCH 02/26] Add exception MessageToPinNotFound (#404) --- aiogram/utils/exceptions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index cee2820a..a289be25 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -7,6 +7,7 @@ - MessageNotModified - MessageToForwardNotFound - MessageToDeleteNotFound + - MessageToPinNotFound - MessageIdentifierNotSpecified - MessageTextIsEmpty - MessageCantBeEdited @@ -182,6 +183,13 @@ class MessageToDeleteNotFound(MessageError): match = 'message to delete not found' +class MessageToPinNotFound(MessageError): + """ + Will be raised when you try to pin deleted or unknown message. + """ + match = 'message to pin not found' + + class MessageToReplyNotFound(MessageError): """ Will be raised when you try to reply to very old or deleted or unknown message. From 00cff4acf5af422cb2f1c5c34c0833d8d021301a Mon Sep 17 00:00:00 2001 From: Martin Winks <50446230+uwinx@users.noreply.github.com> Date: Sat, 22 Aug 2020 01:07:03 +0400 Subject: [PATCH 03/26] fix(handlerObj): fix parameter-spec solving (#408) --- aiogram/dispatcher/handler.py | 2 +- tests/test_dispatcher/test_handler.py | 66 +++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tests/test_dispatcher/test_handler.py diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index cd5e9b50..38219012 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -33,7 +33,7 @@ def _check_spec(spec: inspect.FullArgSpec, kwargs: dict): if spec.varkw: return kwargs - return {k: v for k, v in kwargs.items() if k in spec.args} + return {k: v for k, v in kwargs.items() if k in set(spec.args + spec.kwonlyargs)} class Handler: diff --git a/tests/test_dispatcher/test_handler.py b/tests/test_dispatcher/test_handler.py new file mode 100644 index 00000000..b823c8f8 --- /dev/null +++ b/tests/test_dispatcher/test_handler.py @@ -0,0 +1,66 @@ +import functools + +import pytest + +from aiogram.dispatcher.handler import Handler, _check_spec, _get_spec + + +def callback1(foo: int, bar: int, baz: int): + return locals() + + +async def callback2(foo: int, bar: int, baz: int): + return locals() + + +async def callback3(foo: int, **kwargs): + return locals() + + +class TestHandlerObj: + def test_init_decorated(self): + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + @decorator + def callback1(foo, bar, baz): + pass + + @decorator + @decorator + def callback2(foo, bar, baz): + pass + + obj1 = Handler.HandlerObj(callback1, _get_spec(callback1)) + obj2 = Handler.HandlerObj(callback2, _get_spec(callback2)) + + assert set(obj1.spec.args) == {"foo", "bar", "baz"} + assert obj1.handler == callback1 + assert set(obj2.spec.args) == {"foo", "bar", "baz"} + assert obj2.handler == callback2 + + @pytest.mark.parametrize( + "callback,kwargs,result", + [ + pytest.param( + callback1, {"foo": 42, "spam": True, "baz": "fuz"}, {"foo": 42, "baz": "fuz"} + ), + pytest.param( + callback2, + {"foo": 42, "spam": True, "baz": "fuz", "bar": "test"}, + {"foo": 42, "baz": "fuz", "bar": "test"}, + ), + pytest.param( + callback3, + {"foo": 42, "spam": True, "baz": "fuz", "bar": "test"}, + {"foo": 42, "spam": True, "baz": "fuz", "bar": "test"}, + ), + ], + ) + def test__check_spec(self, callback, kwargs, result): + spec = _get_spec(callback) + assert _check_spec(spec, kwargs) == result From d8c6214170a87cb46048f18623e6bbb96dc1b37c Mon Sep 17 00:00:00 2001 From: Daniil Kovalenko <40635760+WhiteMemory99@users.noreply.github.com> Date: Sun, 30 Aug 2020 05:06:48 +0700 Subject: [PATCH 04/26] Fix HTML characters escaping (#409) html.escape replaces " and ' characters by default, but it's not required by Telegram and causes unexpected behavior, quote=False argument fixes that. --- aiogram/utils/text_decorations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index 3d22f637..4b3109af 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -159,7 +159,7 @@ class HtmlDecoration(TextDecoration): return f"{value}" def quote(self, value: str) -> str: - return html.escape(value) + return html.escape(value, quote=False) class MarkdownDecoration(TextDecoration): From 17eb8a56d06954a1540ebef0bdca80c8dd9cff4c Mon Sep 17 00:00:00 2001 From: unintended Date: Fri, 4 Sep 2020 18:08:15 +0300 Subject: [PATCH 05/26] Fix #413 parse entities positioning (#414) * fix entity positioning in parse_entities() #413 * add tests and small fixes --- aiogram/utils/text_decorations.py | 26 +++++++++++++++-------- tests/test_utils/test_text_decorations.py | 25 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 tests/test_utils/test_text_decorations.py diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index 4b3109af..81592465 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -57,14 +57,14 @@ class TextDecoration(ABC): """ result = "".join( self._unparse_entities( - text, sorted(entities, key=lambda item: item.offset) if entities else [] + self._add_surrogates(text), sorted(entities, key=lambda item: item.offset) if entities else [] ) ) return result def _unparse_entities( self, - text: str, + text: bytes, entities: List[MessageEntity], offset: Optional[int] = None, length: Optional[int] = None, @@ -74,15 +74,15 @@ class TextDecoration(ABC): length = length or len(text) for index, entity in enumerate(entities): - if entity.offset < offset: + if entity.offset * 2 < offset: continue - if entity.offset > offset: - yield self.quote(text[offset : entity.offset]) - start = entity.offset - offset = entity.offset + entity.length + if entity.offset * 2 > offset: + yield self.quote(self._remove_surrogates(text[offset : entity.offset * 2])) + start = entity.offset * 2 + offset = entity.offset * 2 + entity.length * 2 sub_entities = list( - filter(lambda e: e.offset < (offset or 0), entities[index + 1 :]) + filter(lambda e: e.offset * 2 < (offset or 0), entities[index + 1 :]) ) yield self.apply_entity( entity, @@ -94,7 +94,15 @@ class TextDecoration(ABC): ) if offset < length: - yield self.quote(text[offset:length]) + yield self.quote(self._remove_surrogates(text[offset:length])) + + @staticmethod + def _add_surrogates(text: str): + return text.encode('utf-16-le') + + @staticmethod + def _remove_surrogates(text: bytes): + return text.decode('utf-16-le') @abstractmethod def link(self, value: str, link: str) -> str: # pragma: no cover diff --git a/tests/test_utils/test_text_decorations.py b/tests/test_utils/test_text_decorations.py new file mode 100644 index 00000000..dd0e595d --- /dev/null +++ b/tests/test_utils/test_text_decorations.py @@ -0,0 +1,25 @@ +from aiogram.types import MessageEntity, MessageEntityType +from aiogram.utils import text_decorations + + +class TestTextDecorations: + def test_unparse_entities_normal_text(self): + assert text_decorations.markdown_decoration.unparse( + "hi i'm bold and italic and still bold", + entities=[ + MessageEntity(offset=3, length=34, type=MessageEntityType.BOLD), + MessageEntity(offset=12, length=10, type=MessageEntityType.ITALIC), + ] + ) == "hi *i'm bold _and italic_\r and still bold*" + + def test_unparse_entities_emoji_text(self): + """ + emoji is encoded as two chars in json + """ + assert text_decorations.markdown_decoration.unparse( + "πŸš€ i'm bold and italic and still bold", + entities=[ + MessageEntity(offset=3, length=34, type=MessageEntityType.BOLD), + MessageEntity(offset=12, length=10, type=MessageEntityType.ITALIC), + ] + ) == "πŸš€ *i'm bold _and italic_\r and still bold*" From 9ab2945267aab4a9567fca6086d12fc21774e1a0 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Thu, 10 Sep 2020 22:31:54 +0300 Subject: [PATCH 06/26] fixed CallbackQuery.answer() returns nothing (#420) --- aiogram/types/callback_query.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/aiogram/types/callback_query.py b/aiogram/types/callback_query.py index 51ba1f17..e847bff8 100644 --- a/aiogram/types/callback_query.py +++ b/aiogram/types/callback_query.py @@ -54,8 +54,11 @@ class CallbackQuery(base.TelegramObject): :type cache_time: :obj:`typing.Union[base.Integer, None]` :return: On success, True is returned. :rtype: :obj:`base.Boolean`""" - await self.bot.answer_callback_query(callback_query_id=self.id, text=text, - show_alert=show_alert, url=url, cache_time=cache_time) + return await self.bot.answer_callback_query(callback_query_id=self.id, + text=text, + show_alert=show_alert, + url=url, + cache_time=cache_time) def __hash__(self): return hash(self.id) From 56ffa00c8ca8e80040fe9c58cf271d080fc46957 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Thu, 10 Sep 2020 22:32:56 +0300 Subject: [PATCH 07/26] I18n example patch (added docs and fixed typos) (#419) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update i18n example 1. Replaced one file translation to all project folder translation. It's more usable case. 2. `For e.g.` --> `E.g.`. E.g. is short for `exempli gratia` which means simply β€œfor example.” So if you write for e.g., you are in effect writing `for for example`. 3. `xargs` replased with serveral lines, `xargs` is not appliable for Microsoft Windows users. 4. Added info about translation tools. 5. Minor edits. * i18n middlaware typo fix * i18n example header typo fix --- aiogram/contrib/middlewares/i18n.py | 4 ++-- examples/i18n_example.py | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py index 63f54510..bb6d8003 100644 --- a/aiogram/contrib/middlewares/i18n.py +++ b/aiogram/contrib/middlewares/i18n.py @@ -59,13 +59,13 @@ class I18nMiddleware(BaseMiddleware): with open(mo_path, 'rb') as fp: translations[name] = gettext.GNUTranslations(fp) elif os.path.exists(mo_path[:-2] + 'po'): - raise RuntimeError(f"Found locale '{name} but this language is not compiled!") + raise RuntimeError(f"Found locale '{name}' but this language is not compiled!") return translations def reload(self): """ - Hot reload locles + Hot reload locales """ self.locales = self.find_locales() diff --git a/examples/i18n_example.py b/examples/i18n_example.py index b626d048..29b43210 100644 --- a/examples/i18n_example.py +++ b/examples/i18n_example.py @@ -1,8 +1,8 @@ """ -Internalize your bot +Internationalize your bot Step 1: extract texts - # pybabel extract i18n_example.py -o locales/mybot.pot + # pybabel extract --input-dirs=. -o locales/mybot.pot Some useful options: - Extract texts with pluralization support @@ -16,9 +16,14 @@ Step 1: extract texts - Set version # --version=2.2 -Step 2: create *.po files. For e.g. create en, ru, uk locales. - # echo {en,ru,uk} | xargs -n1 pybabel init -i locales/mybot.pot -d locales -D mybot -l -Step 3: translate texts +Step 2: create *.po files. E.g. create en, ru, uk locales. + # pybabel init -i locales/mybot.pot -d locales -D mybot -l en + # pybabel init -i locales/mybot.pot -d locales -D mybot -l ru + # pybabel init -i locales/mybot.pot -d locales -D mybot -l uk + +Step 3: translate texts located in locales/{language}/LC_MESSAGES/mybot.po + To open .po file you can use basic text editor or any PO editor, e.g. https://poedit.net/ + Step 4: compile translations # pybabel compile -d locales -D mybot @@ -27,7 +32,8 @@ Step 5: When you change the code of your bot you need to update po & mo files. command from step 1 Step 5.2: update po files # pybabel update -d locales -D mybot -i locales/mybot.pot - Step 5.3: update your translations + Step 5.3: update your translations + location and tools you know from step 3 Step 5.4: compile mo files command from step 4 """ @@ -92,5 +98,6 @@ async def cmd_like(message: types.Message, locale): # NOTE: This is comment for a translator await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', likes).format(number=likes)) + if __name__ == '__main__': executor.start_polling(dp, skip_updates=True) From fc5ccc9d5a04981d5debfb2af00d20e00339e293 Mon Sep 17 00:00:00 2001 From: Lamroy95 <50185460+Lamroy95@users.noreply.github.com> Date: Thu, 10 Sep 2020 22:33:25 +0300 Subject: [PATCH 08/26] Fixed docs Quick start page code blocks (#417) --- docs/source/quick_start.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/quick_start.rst b/docs/source/quick_start.rst index b0724a78..319886ce 100644 --- a/docs/source/quick_start.rst +++ b/docs/source/quick_start.rst @@ -28,13 +28,13 @@ If you want to handle all messages in the chat simply add handler without filter .. literalinclude:: ../../examples/echo_bot.py :language: python - :lines: 35-37 + :lines: 44-49 Last step: run long polling. .. literalinclude:: ../../examples/echo_bot.py :language: python - :lines: 40-41 + :lines: 52-53 Summary ------- @@ -42,4 +42,4 @@ Summary .. literalinclude:: ../../examples/echo_bot.py :language: python :linenos: - :lines: -19,27- + :lines: -27,43- From a529619d7999af98d157fdeaa46dc8eee77948e1 Mon Sep 17 00:00:00 2001 From: Martin Winks <50446230+uwinx@users.noreply.github.com> Date: Thu, 10 Sep 2020 23:34:20 +0400 Subject: [PATCH 09/26] hotfix(updates): CHOSEN_INLINE_RESULT is a correct API-term (#415) * hotfix(updates): CHOSEN_INLINE_RESULT is a correct API-term * feat(utils): deprecated descriptor deprecate CHOSEN_INLINE_QUERY and always return CHOSEN_INLINE_RESULT instead of incorrect value * fix(tests): remove example from test * fix(utils): use stacklevel=3 level on which descriptor is being called --- aiogram/types/update.py | 10 +++++++-- aiogram/utils/deprecated.py | 35 ++++++++++++++++++++++++++++- tests/test_utils/test_deprecated.py | 14 ++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 tests/test_utils/test_deprecated.py diff --git a/aiogram/types/update.py b/aiogram/types/update.py index 2146cb9d..9d1afacc 100644 --- a/aiogram/types/update.py +++ b/aiogram/types/update.py @@ -9,7 +9,7 @@ from .message import Message from .poll import Poll, PollAnswer from .pre_checkout_query import PreCheckoutQuery from .shipping_query import ShippingQuery -from ..utils import helper +from ..utils import helper, deprecated class Update(base.TelegramObject): @@ -55,9 +55,15 @@ class AllowedUpdates(helper.Helper): CHANNEL_POST = helper.ListItem() # channel_post EDITED_CHANNEL_POST = helper.ListItem() # edited_channel_post INLINE_QUERY = helper.ListItem() # inline_query - CHOSEN_INLINE_QUERY = helper.ListItem() # chosen_inline_result + CHOSEN_INLINE_RESULT = helper.ListItem() # chosen_inline_result CALLBACK_QUERY = helper.ListItem() # callback_query SHIPPING_QUERY = helper.ListItem() # shipping_query PRE_CHECKOUT_QUERY = helper.ListItem() # pre_checkout_query POLL = helper.ListItem() # poll POLL_ANSWER = helper.ListItem() # poll_answer + + CHOSEN_INLINE_QUERY = deprecated.DeprecatedReadOnlyClassVar( + "`CHOSEN_INLINE_QUERY` is a deprecated value for allowed update. " + "Use `CHOSEN_INLINE_RESULT`", + new_value_getter=lambda cls: cls.CHOSEN_INLINE_RESULT, + ) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 83a9034c..6d0d7ee3 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -2,7 +2,7 @@ import asyncio import inspect import warnings import functools -from typing import Callable +from typing import Callable, Generic, TypeVar, Type, Optional def deprecated(reason, stacklevel=2) -> Callable: @@ -129,3 +129,36 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve return wrapped return decorator + + +_VT = TypeVar("_VT") +_OwnerCls = TypeVar("_OwnerCls") + + +class DeprecatedReadOnlyClassVar(Generic[_OwnerCls, _VT]): + """ + DeprecatedReadOnlyClassVar[Owner, ValueType] + + :param warning_message: Warning message when getter gets called + :param new_value_getter: Any callable with (owner_class: Type[Owner]) -> ValueType + signature that will be executed + + Usage example: + + >>> class MyClass: + ... some_attribute: DeprecatedReadOnlyClassVar[MyClass, int] = \ + ... DeprecatedReadOnlyClassVar( + ... "Warning message.", lambda owner: 15) + ... + >>> MyClass.some_attribute # does warning.warn with `Warning message` and returns 15 in the current case + """ + + __slots__ = "_new_value_getter", "_warning_message" + + def __init__(self, warning_message: str, new_value_getter: Callable[[_OwnerCls], _VT]): + self._warning_message = warning_message + self._new_value_getter = new_value_getter + + def __get__(self, instance: Optional[_OwnerCls], owner: Type[_OwnerCls]): + warn_deprecated(self._warning_message, stacklevel=3) + return self._new_value_getter(owner) diff --git a/tests/test_utils/test_deprecated.py b/tests/test_utils/test_deprecated.py new file mode 100644 index 00000000..114d6810 --- /dev/null +++ b/tests/test_utils/test_deprecated.py @@ -0,0 +1,14 @@ +import pytest + +from aiogram.utils.deprecated import DeprecatedReadOnlyClassVar + + +def test_DeprecatedReadOnlyClassVarCD(): + assert DeprecatedReadOnlyClassVar.__slots__ == ("_new_value_getter", "_warning_message") + + new_value_of_deprecated_cls_cd = "mpa" + pseudo_owner_cls = type("OpekaCla$$", (), {}) + deprecated_cd = DeprecatedReadOnlyClassVar("mopekaa", lambda owner: new_value_of_deprecated_cls_cd) + + with pytest.warns(DeprecationWarning): + assert deprecated_cd.__get__(None, pseudo_owner_cls) == new_value_of_deprecated_cls_cd From fce48e3127c645fbfe7b771d34c9175f3c41d2cf Mon Sep 17 00:00:00 2001 From: Daneel L <53001370+lovkiymusic@users.noreply.github.com> Date: Sun, 13 Sep 2020 22:07:14 +0300 Subject: [PATCH 10/26] comment of RedisStorage2 fix (#423) --- aiogram/contrib/fsm_storage/redis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index bf88eff7..74dd736c 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -208,7 +208,7 @@ class RedisStorage2(BaseStorage): .. code-block:: python3 - storage = RedisStorage('localhost', 6379, db=5, pool_size=10, prefix='my_fsm_key') + storage = RedisStorage2('localhost', 6379, db=5, pool_size=10, prefix='my_fsm_key') dp = Dispatcher(bot, storage=storage) And need to close Redis connection when shutdown From a936465f427b25c149e4e508c666fb5ed3de91d5 Mon Sep 17 00:00:00 2001 From: Gabben <43146729+gabbhack@users.noreply.github.com> Date: Mon, 14 Sep 2020 00:07:29 +0500 Subject: [PATCH 11/26] add missing attributes (#422) https://core.telegram.org/bots/api#animation --- aiogram/types/animation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aiogram/types/animation.py b/aiogram/types/animation.py index 78f5235a..b08089c1 100644 --- a/aiogram/types/animation.py +++ b/aiogram/types/animation.py @@ -15,6 +15,9 @@ class Animation(base.TelegramObject, mixins.Downloadable): file_id: base.String = fields.Field() file_unique_id: base.String = fields.Field() + width: base.Integer = fields.Field() + height: base.Integer = fields.Field() + duration: base.Integer = fields.Field() thumb: PhotoSize = fields.Field(base=PhotoSize) file_name: base.String = fields.Field() mime_type: base.String = fields.Field() From 51547f974511dc909bc2697c36c7b3b2681335c8 Mon Sep 17 00:00:00 2001 From: lyteloli Date: Sun, 13 Sep 2020 22:08:55 +0300 Subject: [PATCH 12/26] Fixed usage of deprecated is_private function (#421) Co-authored-by: Alex Root Junior --- aiogram/dispatcher/filters/builtin.py | 2 +- aiogram/types/message.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index fcaaf786..20317f57 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -673,7 +673,7 @@ class AdminFilter(Filter): message = obj.message else: return False - if ChatType.is_private(message): # there is no admin in private chats + if message.chat.type == ChatType.PRIVATE: # there is no admin in private chats return False chat_ids = [message.chat.id] else: diff --git a/aiogram/types/message.py b/aiogram/types/message.py index fedd656e..52c2b3a8 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -236,9 +236,8 @@ class Message(base.TelegramObject): :return: str """ - if ChatType.is_private(self.chat): - raise TypeError("Invalid chat type!") - + if self.chat.type == ChatType.PRIVATE: + raise TypeError('Invalid chat type url = "https://t.me/" if self.chat.username: # Generates public link From 00202565e4941166a468f9b9b16d5b8b7502ecd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9?= Date: Sun, 13 Sep 2020 22:09:43 +0300 Subject: [PATCH 13/26] fixed type hints of callback_data (#400) its impotant to remeber all data saved in callback_data is text even if you pass to it integer insofar as newbies often copy examples and modyfy this typing may help them make no mistake --- examples/callback_data_factory.py | 7 ++++--- examples/callback_data_factory_simple.py | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/callback_data_factory.py b/examples/callback_data_factory.py index 2335ea95..c95860f1 100644 --- a/examples/callback_data_factory.py +++ b/examples/callback_data_factory.py @@ -1,6 +1,7 @@ import logging import random import uuid +import typing from aiogram import Bot, Dispatcher, executor, md, types from aiogram.contrib.fsm_storage.memory import MemoryStorage @@ -52,7 +53,7 @@ def format_post(post_id: str, post: dict) -> (str, types.InlineKeyboardMarkup): md.quote_html(post['body']), '', # just new empty line f"Votes: {post['votes']}", - sep = '\n', + sep='\n', ) markup = types.InlineKeyboardMarkup() @@ -75,7 +76,7 @@ async def query_show_list(query: types.CallbackQuery): @dp.callback_query_handler(posts_cb.filter(action='view')) -async def query_view(query: types.CallbackQuery, callback_data: dict): +async def query_view(query: types.CallbackQuery, callback_data: typing.Dict[str, str]): post_id = callback_data['id'] post = POSTS.get(post_id, None) @@ -87,7 +88,7 @@ async def query_view(query: types.CallbackQuery, callback_data: dict): @dp.callback_query_handler(posts_cb.filter(action=['like', 'dislike'])) -async def query_post_vote(query: types.CallbackQuery, callback_data: dict): +async def query_post_vote(query: types.CallbackQuery, callback_data: typing.Dict[str, str]): try: await dp.throttle('vote', rate=1) except Throttled: diff --git a/examples/callback_data_factory_simple.py b/examples/callback_data_factory_simple.py index a6d246d5..d8c1a327 100644 --- a/examples/callback_data_factory_simple.py +++ b/examples/callback_data_factory_simple.py @@ -4,6 +4,7 @@ For more comprehensive example see callback_data_factory.py """ import logging +import typing from aiogram import Bot, Dispatcher, executor, types from aiogram.contrib.middlewares.logging import LoggingMiddleware @@ -38,7 +39,7 @@ async def cmd_start(message: types.Message): @dp.callback_query_handler(vote_cb.filter(action=['up', 'down'])) -async def callback_vote_action(query: types.CallbackQuery, callback_data: dict): +async def callback_vote_action(query: types.CallbackQuery, callback_data: typing.Dict[str, str]): logging.info('Got this callback data: %r', callback_data) # callback_data contains all info from callback data await query.answer() # don't forget to answer callback query as soon as possible callback_data_action = callback_data['action'] From 60fe0931a72b4b02588d5d153d1903a05be81496 Mon Sep 17 00:00:00 2001 From: Martin Winks <50446230+uwinx@users.noreply.github.com> Date: Sun, 13 Sep 2020 23:11:27 +0400 Subject: [PATCH 14/26] docs(readme): prettify readme, update downloads stats badge (#406) * docs(readme): prettify readme, update downloads stats badge * chore(readme): describe steps for minimal code * chore(rme): remove comma etc Co-authored-by: evgfilim1 * chore(rme): add heading for details/summary blocks * Update README.md Co-authored-by: evgfilim1 Co-authored-by: Alex Root Junior --- README.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dfef918f..5cc22e92 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,72 @@ **aiogram** is a pretty simple and fully asynchronous framework for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.7 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler. -You can [read the docs here](http://docs.aiogram.dev/en/latest/). + +## Examples +
+ πŸ“š Click to see some basic examples + + +**Few steps before getting started...** +- First, you should obtain token for your bot from [BotFather](https://t.me/BotFather). +- Install latest stable version of aiogram, simply running `pip install aiogram` + +### Simple [`getMe`](https://core.telegram.org/bots/api#getme) request + +```python +import asyncio +from aiogram import Bot + + +async def main(): + bot = Bot(token=BOT-TOKEN) + + try: + me = await bot.get_me() + print(f"πŸ€– Hello, I'm {me.first_name}.\nHave a nice Day!") + finally: + await bot.close() + +asyncio.run(main()) +``` + +### Poll BotAPI for updates and process updates + +```python +import asyncio +from aiogram import Bot, Dispatcher, types + +async def start_handler(event: types.Message): + await event.answer( + f"Hello, {event.from_user.get_mention(as_html=True)} πŸ‘‹!", + parse_mode=types.ParseMode.HTML, + ) + +async def main(): + bot = Bot(token=BOT-TOKEN) + try: + disp = Dispatcher(bot=bot) + disp.register_message_handler(start_handler, commands={"start", "restart"}) + await disp.start_polling() + finally: + await bot.close() + +asyncio.run(main()) +``` + +### Moar! + +You can find more examples in [`examples/`](https://github.com/aiogram/aiogram/tree/dev-2.x/examples) directory + +
## Official aiogram resources: - News: [@aiogram_live](https://t.me/aiogram_live) - Community: [@aiogram](https://t.me/aiogram) - Russian community: [@aiogram_ru](https://t.me/aiogram_ru) - - Pip: [aiogram](https://pypi.python.org/pypi/aiogram) - - Docs: [aiogram site](https://docs.aiogram.dev/) + - PyPI: [aiogram](https://pypi.python.org/pypi/aiogram) + - Documentation: [aiogram site](https://docs.aiogram.dev/en/latest/) - Source: [Github repo](https://github.com/aiogram/aiogram) - Issues/Bug tracker: [Github issues tracker](https://github.com/aiogram/aiogram/issues) - Test bot: [@aiogram_bot](https://t.me/aiogram_bot) From f926d80ba2ffe3a62cdd906a2094693a025ed2f0 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sun, 13 Sep 2020 22:13:15 +0300 Subject: [PATCH 15/26] Add missed emoji argument to reply_dice (#395) * fix: add missed emoji argument to reply_dice * ref: order arguments for send_dice --- aiogram/types/message.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 52c2b3a8..3540a736 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1048,8 +1048,8 @@ class Message(base.TelegramObject): """ return await self.bot.send_dice( chat_id=self.chat.id, - disable_notification=disable_notification, emoji=emoji, + disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup, ) @@ -1834,6 +1834,7 @@ class Message(base.TelegramObject): """ return await self.bot.send_dice( chat_id=self.chat.id, + emoji=emoji, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup, From 7f053412bf5c3f4803b13ea6a028961bb55caef1 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sun, 13 Sep 2020 22:14:01 +0300 Subject: [PATCH 16/26] Add is_chat_creator method to ChatMemberStatus (#394) * new: add is_chat_creator method to ChatMemberStatus * enh: use tuples instead of lists for some checks --- aiogram/bot/api.py | 2 +- aiogram/bot/bot.py | 4 ++-- aiogram/types/chat_member.py | 11 +++++++++-- aiogram/types/input_media.py | 2 +- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 1d0c4f7b..cb258cdf 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -80,7 +80,7 @@ def check_result(method_name: str, content_type: str, status_code: int, body: st exceptions.NotFound.detect(description) elif status_code == HTTPStatus.CONFLICT: exceptions.ConflictError.detect(description) - elif status_code in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN]: + elif status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): exceptions.Unauthorized.detect(description) elif status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE: raise exceptions.NetworkError('File too large for uploading. ' diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index f427fa8c..c5678a37 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1135,10 +1135,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): permissions = prepare_arg(permissions) payload = generate_payload(**locals()) - for permission in ['can_send_messages', + for permission in ('can_send_messages', 'can_send_media_messages', 'can_send_other_messages', - 'can_add_web_page_previews']: + 'can_add_web_page_previews'): if permission in payload: warnings.warn(f"The method `restrict_chat_member` now takes the new user permissions " f"in a single argument of the type ChatPermissions instead of " diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 347b2750..274c8a26 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -34,6 +34,9 @@ class ChatMember(base.TelegramObject): can_send_other_messages: base.Boolean = fields.Field() can_add_web_page_previews: base.Boolean = fields.Field() + def is_chat_creator(self) -> bool: + return ChatMemberStatus.is_chat_creator(self.status) + def is_chat_admin(self) -> bool: return ChatMemberStatus.is_chat_admin(self.status) @@ -57,10 +60,14 @@ class ChatMemberStatus(helper.Helper): LEFT = helper.Item() # left KICKED = helper.Item() # kicked + @classmethod + def is_chat_creator(cls, role: str) -> bool: + return role == cls.CREATOR + @classmethod def is_chat_admin(cls, role: str) -> bool: - return role in [cls.ADMINISTRATOR, cls.CREATOR] + return role in (cls.ADMINISTRATOR, cls.CREATOR) @classmethod def is_chat_member(cls, role: str) -> bool: - return role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED] + return role in (cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED) diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index 9a77658f..25422df1 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -239,7 +239,7 @@ class MediaGroup(base.TelegramObject): elif not isinstance(media, InputMedia): raise TypeError(f"Media must be an instance of InputMedia or dict, not {type(media).__name__}") - elif media.type in ['document', 'audio', 'animation']: + elif media.type in ('document', 'audio', 'animation'): raise ValueError(f"This type of media is not supported by media groups!") self.media.append(media) From 5b40a2b8cfee24bfef997bfcede70b2c784b03ca Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sun, 13 Sep 2020 22:14:33 +0300 Subject: [PATCH 17/26] Add missed ChatPermissions to __all__ (#393) * new: add missed ChatPermissions to __all__ * ref: standardize all __all__ --- aiogram/__init__.py | 6 ++--- aiogram/bot/__init__.py | 6 ++--- aiogram/contrib/fsm_storage/rethinkdb.py | 2 +- aiogram/dispatcher/__init__.py | 4 ++-- aiogram/dispatcher/filters/__init__.py | 30 ++++++++++++------------ aiogram/types/__init__.py | 1 + aiogram/utils/text_decorations.py | 10 ++++---- 7 files changed, 30 insertions(+), 29 deletions(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index bebafcec..74dc49ab 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -25,7 +25,7 @@ else: if 'DISABLE_UVLOOP' not in os.environ: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -__all__ = [ +__all__ = ( 'Bot', 'Dispatcher', '__api_version__', @@ -40,8 +40,8 @@ __all__ = [ 'md', 'middlewares', 'types', - 'utils' -] + 'utils', +) __version__ = '2.9.2' __api_version__ = '4.9' diff --git a/aiogram/bot/__init__.py b/aiogram/bot/__init__.py index 252c465b..19c051bf 100644 --- a/aiogram/bot/__init__.py +++ b/aiogram/bot/__init__.py @@ -2,8 +2,8 @@ from . import api from .base import BaseBot from .bot import Bot -__all__ = [ +__all__ = ( 'BaseBot', 'Bot', - 'api' -] + 'api', +) diff --git a/aiogram/contrib/fsm_storage/rethinkdb.py b/aiogram/contrib/fsm_storage/rethinkdb.py index 38d24efa..a9d822d5 100644 --- a/aiogram/contrib/fsm_storage/rethinkdb.py +++ b/aiogram/contrib/fsm_storage/rethinkdb.py @@ -7,7 +7,7 @@ from rethinkdb.asyncio_net.net_asyncio import Connection from ...dispatcher.storage import BaseStorage -__all__ = ['RethinkDBStorage'] +__all__ = ('RethinkDBStorage',) r = rethinkdb.RethinkDB() r.set_loop_type('asyncio') diff --git a/aiogram/dispatcher/__init__.py b/aiogram/dispatcher/__init__.py index 6ad43bbe..e412dd36 100644 --- a/aiogram/dispatcher/__init__.py +++ b/aiogram/dispatcher/__init__.py @@ -5,7 +5,7 @@ from . import storage from . import webhook from .dispatcher import Dispatcher, FSMContext, DEFAULT_RATE_LIMIT -__all__ = [ +__all__ = ( 'DEFAULT_RATE_LIMIT', 'Dispatcher', 'FSMContext', @@ -14,4 +14,4 @@ __all__ = [ 'middlewares', 'storage', 'webhook' -] +) diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index 5f839662..d64a2667 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -6,33 +6,33 @@ from .factory import FiltersFactory from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \ check_filters, get_filter_spec, get_filters_spec -__all__ = [ - 'AbstractFilter', - 'BoundFilter', +__all__ = ( 'Command', - 'CommandStart', 'CommandHelp', 'CommandPrivacy', 'CommandSettings', + 'CommandStart', 'ContentTypeFilter', 'ExceptionsFilter', 'HashTag', - 'Filter', - 'FilterNotPassed', - 'FilterRecord', - 'FiltersFactory', - 'RegexpCommandsFilter', 'Regexp', + 'RegexpCommandsFilter', 'StateFilter', 'Text', 'IDFilter', + 'AdminFilter', 'IsReplyFilter', 'IsSenderContact', - 'AdminFilter', - 'get_filter_spec', - 'get_filters_spec', - 'execute_filter', - 'check_filters', 'ForwardedMessageFilter', 'ChatTypeFilter', -] + 'FiltersFactory', + 'AbstractFilter', + 'BoundFilter', + 'Filter', + 'FilterNotPassed', + 'FilterRecord', + 'execute_filter', + 'check_filters', + 'get_filter_spec', + 'get_filters_spec', +) diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 1221ec72..d46f24da 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -78,6 +78,7 @@ __all__ = ( 'ChatActions', 'ChatMember', 'ChatMemberStatus', + 'ChatPermissions', 'ChatPhoto', 'ChatType', 'ChosenInlineResult', diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index 81592465..0fed91e8 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -9,11 +9,11 @@ if TYPE_CHECKING: # pragma: no cover from aiogram.types import MessageEntity __all__ = ( - "TextDecoration", - "HtmlDecoration", - "MarkdownDecoration", - "html_decoration", - "markdown_decoration", + 'HtmlDecoration', + 'MarkdownDecoration', + 'TextDecoration', + 'html_decoration', + 'markdown_decoration', ) From 0cc6a2c8f6e77b1a653d4fd6151164ee43892bd8 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sun, 13 Sep 2020 22:15:07 +0300 Subject: [PATCH 18/26] Add is_forward method to Message (#390) * new: add is_forward method to Message * enh: add return types to Message methods * fix: docs typo * ref: make command methods closer --- aiogram/types/message.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 3540a736..f867a44a 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -153,7 +153,16 @@ class Message(base.TelegramObject): return ContentType.UNKNOWN - def is_command(self): + def is_forward(self) -> bool: + """ + Check that the message is forwarded. + Only `forward_date` is required to be in forwarded message. + + :return: bool + """ + return bool(self.forward_date) + + def is_command(self) -> bool: """ Check message text is command @@ -161,7 +170,7 @@ class Message(base.TelegramObject): """ return self.text and self.text.startswith("/") - def get_full_command(self): + def get_full_command(self) -> typing.Optional[typing.Tuple[str, str]]: """ Split command and args @@ -172,7 +181,7 @@ class Message(base.TelegramObject): args = args[-1] if args else "" return command, args - def get_command(self, pure=False): + def get_command(self, pure=False) -> typing.Optional[str]: """ Get command from message @@ -185,7 +194,7 @@ class Message(base.TelegramObject): command, _, _ = command[1:].partition("@") return command - def get_args(self): + def get_args(self) -> typing.Optional[str]: """ Get arguments @@ -195,7 +204,7 @@ class Message(base.TelegramObject): if command: return command[1] - def parse_entities(self, as_html=True): + def parse_entities(self, as_html=True) -> str: """ Text or caption formatted as HTML or Markdown. From 7d1c8c42d3cdd333e3a2933eb09fcd9523810852 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 13 Sep 2020 22:27:50 +0300 Subject: [PATCH 19/26] Fix syntax error --- aiogram/types/message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index f867a44a..9d2ad505 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -245,8 +245,8 @@ class Message(base.TelegramObject): :return: str """ - if self.chat.type == ChatType.PRIVATE: - raise TypeError('Invalid chat type + if ChatType.is_private(self.chat): + raise TypeError("Invalid chat type!") url = "https://t.me/" if self.chat.username: # Generates public link From c99b165668d305c534c56f6345960c1461219adf Mon Sep 17 00:00:00 2001 From: Martin Winks <50446230+uwinx@users.noreply.github.com> Date: Sun, 13 Sep 2020 23:42:21 +0400 Subject: [PATCH 20/26] fix(bot,dispatcher): do not use _MainThread event loop (#397) * fix(bot,dispatcher): do not use _MainThread event loop on ::Bot, ::Dispatcher initializations * fix: use more generic get approach * docs: comments * chore(task): asyncio.create_task comes with py3.7 * fix(dispatcher): todo --- aiogram/bot/base.py | 19 ++++++------ aiogram/dispatcher/dispatcher.py | 49 +++++++++++++++++++++++++------ aiogram/dispatcher/middlewares.py | 5 +++- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 86347e88..f45546c3 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -56,6 +56,8 @@ class BaseBot: :type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]` :raise: when token is invalid throw an :obj:`aiogram.utils.exceptions.ValidationError` """ + self._main_loop = loop + # Authentication if validate_token: api.check_token(token) @@ -66,19 +68,12 @@ class BaseBot: self.proxy = proxy self.proxy_auth = proxy_auth - # Asyncio loop instance - if loop is None: - loop = asyncio.get_event_loop() - self.loop = loop - # aiohttp main session ssl_context = ssl.create_default_context(cafile=certifi.where()) self._session: Optional[aiohttp.ClientSession] = None self._connector_class: Type[aiohttp.TCPConnector] = aiohttp.TCPConnector - self._connector_init = dict( - limit=connections_limit, ssl=ssl_context, loop=self.loop - ) + self._connector_init = dict(limit=connections_limit, ssl=ssl_context) if isinstance(proxy, str) and (proxy.startswith('socks5://') or proxy.startswith('socks4://')): from aiohttp_socks import SocksConnector @@ -106,11 +101,15 @@ class BaseBot: def get_new_session(self) -> aiohttp.ClientSession: return aiohttp.ClientSession( - connector=self._connector_class(**self._connector_init), - loop=self.loop, + connector=self._connector_class(**self._connector_init, loop=self._main_loop), + loop=self._main_loop, json_serialize=json.dumps ) + @property + def loop(self) -> Optional[asyncio.AbstractEventLoop]: + return self._main_loop + @property def session(self) -> Optional[aiohttp.ClientSession]: if self._session is None or self._session.closed: diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 7a3aa5b3..b38d3af1 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -27,6 +27,13 @@ log = logging.getLogger(__name__) DEFAULT_RATE_LIMIT = .1 +def _ensure_loop(x: "asyncio.AbstractEventLoop"): + assert isinstance( + x, asyncio.AbstractEventLoop + ), f"Loop must be the implementation of {asyncio.AbstractEventLoop!r}, " \ + f"not {type(x)!r}" + + class Dispatcher(DataMixin, ContextInstanceMixin): """ Simple Updates dispatcher @@ -43,15 +50,15 @@ class Dispatcher(DataMixin, ContextInstanceMixin): if not isinstance(bot, Bot): raise TypeError(f"Argument 'bot' must be an instance of Bot, not '{type(bot).__name__}'") - if loop is None: - loop = bot.loop if storage is None: storage = DisabledStorage() if filters_factory is None: filters_factory = FiltersFactory(self) self.bot: Bot = bot - self.loop = loop + if loop is not None: + _ensure_loop(loop) + self._main_loop = loop self.storage = storage self.run_tasks_by_default = run_tasks_by_default @@ -79,10 +86,27 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self._polling = False self._closed = True - self._close_waiter = loop.create_future() + self._dispatcher_close_waiter = None self._setup_filters() + @property + def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]: + # for the sake of backward compatibility + # lib internally must delegate tasks with respect to _main_loop attribute + # however should never be used by the library itself + # use more generic approaches from asyncio's namespace + return self._main_loop + + @property + def _close_waiter(self) -> "asyncio.Future": + if self._dispatcher_close_waiter is None: + if self._main_loop is not None: + self._dispatcher_close_waiter = self._main_loop.create_future() + else: + self._dispatcher_close_waiter = asyncio.get_event_loop().create_future() + return self._dispatcher_close_waiter + def _setup_filters(self): filters_factory = self.filters_factory @@ -282,6 +306,13 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return await self.bot.delete_webhook() + def _loop_create_task(self, coro): + if self._main_loop is None: + return asyncio.create_task(coro) + else: + _ensure_loop(self._main_loop) + return self._main_loop.create_task(coro) + async def start_polling(self, timeout=20, relax=0.1, @@ -337,7 +368,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): log.debug(f"Received {len(updates)} updates.") offset = updates[-1].update_id + 1 - self.loop.create_task(self._process_polling_updates(updates, fast)) + self._loop_create_task(self._process_polling_updates(updates, fast)) if relax: await asyncio.sleep(relax) @@ -381,7 +412,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): :return: """ - await asyncio.shield(self._close_waiter, loop=self.loop) + await asyncio.shield(self._close_waiter) def is_polling(self): """ @@ -1158,15 +1189,15 @@ class Dispatcher(DataMixin, ContextInstanceMixin): try: response = task.result() except Exception as e: - self.loop.create_task( + self._loop_create_task( self.errors_handlers.notify(types.Update.get_current(), e)) else: if isinstance(response, BaseResponse): - self.loop.create_task(response.execute_response(self.bot)) + self._loop_create_task(response.execute_response(self.bot)) @functools.wraps(func) async def wrapper(*args, **kwargs): - task = self.loop.create_task(func(*args, **kwargs)) + task = self._loop_create_task(func(*args, **kwargs)) task.add_done_callback(process_response) return wrapper diff --git a/aiogram/dispatcher/middlewares.py b/aiogram/dispatcher/middlewares.py index dba3db4c..5fa09830 100644 --- a/aiogram/dispatcher/middlewares.py +++ b/aiogram/dispatcher/middlewares.py @@ -16,11 +16,14 @@ class MiddlewareManager: :param dispatcher: instance of Dispatcher """ self.dispatcher = dispatcher - self.loop = dispatcher.loop self.bot = dispatcher.bot self.storage = dispatcher.storage self.applications = [] + @property + def loop(self): + return self.dispatcher.loop + def setup(self, middleware): """ Setup middleware From 84785e48807fbcbb224889524a4e3d3544d5b12c Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 13 Sep 2020 22:56:32 +0300 Subject: [PATCH 21/26] Fix italic and underline decorations --- Makefile | 2 +- aiogram/utils/text_decorations.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index f21ec8ae..da6493d9 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ test: tox summary: - cloc aiogram/ tests/ examples/ setup.py + cloc aiogram/ tests/ examples/ docs/ setup.py docs: docs/source/* cd docs && $(MAKE) html diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index 0fed91e8..4de8d69a 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -180,7 +180,7 @@ class MarkdownDecoration(TextDecoration): return f"*{value}*" def italic(self, value: str) -> str: - return f"_{value}_\r" + return f"_\r{value}_\r" def code(self, value: str) -> str: return f"`{value}`" @@ -192,7 +192,7 @@ class MarkdownDecoration(TextDecoration): return f"```{language}\n{value}\n```" def underline(self, value: str) -> str: - return f"__{value}__" + return f"__\r{value}__\r" def strikethrough(self, value: str) -> str: return f"~{value}~" From 369bff422a326d4f47c63f8719e7f1b7d4aeb11a Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 13 Sep 2020 23:13:27 +0300 Subject: [PATCH 22/26] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 74dc49ab..9a11b7a2 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.9.2' +__version__ = '2.10' __api_version__ = '4.9' From 7c65344dcdd88bb1a852dacd946c82b47ed30509 Mon Sep 17 00:00:00 2001 From: Martin Winks <50446230+uwinx@users.noreply.github.com> Date: Mon, 14 Sep 2020 15:44:18 +0400 Subject: [PATCH 23/26] hotfix(dispatcher): prop DP.loop may be None, executor and webhook view should respect that (#424) --- aiogram/contrib/middlewares/environment.py | 4 +++- aiogram/dispatcher/webhook.py | 4 ++-- aiogram/utils/executor.py | 10 +++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/aiogram/contrib/middlewares/environment.py b/aiogram/contrib/middlewares/environment.py index 0427a739..f6ad56dd 100644 --- a/aiogram/contrib/middlewares/environment.py +++ b/aiogram/contrib/middlewares/environment.py @@ -1,3 +1,5 @@ +import asyncio + from aiogram.dispatcher.middlewares import BaseMiddleware @@ -14,7 +16,7 @@ class EnvironmentMiddleware(BaseMiddleware): data.update( bot=dp.bot, dispatcher=dp, - loop=dp.loop + loop=dp.loop or asyncio.get_event_loop() ) if self.context: data.update(self.context) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index ed2ebf99..5199d591 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -169,7 +169,7 @@ class WebhookRequestHandler(web.View): :return: """ dispatcher = self.get_dispatcher() - loop = dispatcher.loop + loop = dispatcher.loop or asyncio.get_event_loop() # Analog of `asyncio.wait_for` but without cancelling task waiter = loop.create_future() @@ -209,7 +209,7 @@ class WebhookRequestHandler(web.View): TimeoutWarning) dispatcher = self.get_dispatcher() - loop = dispatcher.loop + loop = dispatcher.loop or asyncio.get_event_loop() try: results = task.result() diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index fe3483f6..fe4837e3 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -123,13 +123,13 @@ class Executor: """ def __init__(self, dispatcher, skip_updates=None, check_ip=False, retry_after=None, loop=None): - if loop is None: - loop = dispatcher.loop + if loop is not None: + self._loop = loop + self.dispatcher = dispatcher self.skip_updates = skip_updates self.check_ip = check_ip self.retry_after = retry_after - self.loop = loop self._identity = secrets.token_urlsafe(16) self._web_app = None @@ -145,6 +145,10 @@ class Executor: Bot.set_current(dispatcher.bot) Dispatcher.set_current(dispatcher) + @property + def loop(self) -> asyncio.AbstractEventLoop: + return getattr(self, "_loop", asyncio.get_event_loop()) + @property def frozen(self): return self._freeze From 7b33e5c68a5cdf2de54e6e27c8bf65a20716ceb2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 14 Sep 2020 22:56:11 +0300 Subject: [PATCH 24/26] Update docs --- aiogram/contrib/fsm_storage/mongo.py | 4 +- aiogram/contrib/fsm_storage/rethinkdb.py | 5 ++- dev_requirements.txt | 1 + docs/source/dispatcher/filters.rst | 50 +++++++++++----------- docs/source/dispatcher/fsm.rst | 20 +++++++-- docs/source/examples/throtling_example.rst | 4 +- docs/source/examples/webhook_example_2.rst | 8 ++-- docs/source/utils/index.rst | 1 + 8 files changed, 55 insertions(+), 38 deletions(-) diff --git a/aiogram/contrib/fsm_storage/mongo.py b/aiogram/contrib/fsm_storage/mongo.py index a7601cc4..f810a3eb 100644 --- a/aiogram/contrib/fsm_storage/mongo.py +++ b/aiogram/contrib/fsm_storage/mongo.py @@ -5,9 +5,9 @@ This module has mongo storage for finite-state machine from typing import Union, Dict, Optional, List, Tuple, AnyStr -import pymongo try: + import pymongo import motor from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase except ModuleNotFoundError as e: @@ -26,6 +26,7 @@ COLLECTIONS = (STATE, DATA, BUCKET) class MongoStorage(BaseStorage): """ Mongo-based storage for FSM. + Usage: .. code-block:: python3 @@ -39,7 +40,6 @@ class MongoStorage(BaseStorage): await dp.storage.close() await dp.storage.wait_closed() - """ def __init__(self, host='localhost', port=27017, db_name='aiogram_fsm', uri=None, diff --git a/aiogram/contrib/fsm_storage/rethinkdb.py b/aiogram/contrib/fsm_storage/rethinkdb.py index a9d822d5..b19327ca 100644 --- a/aiogram/contrib/fsm_storage/rethinkdb.py +++ b/aiogram/contrib/fsm_storage/rethinkdb.py @@ -19,16 +19,17 @@ class RethinkDBStorage(BaseStorage): Usage: - ..code-block:: python3 + .. code-block:: python3 storage = RethinkDBStorage(db='aiogram', table='aiogram', user='aiogram', password='aiogram_secret') dispatcher = Dispatcher(bot, storage=storage) And need to close connection when shutdown - ..code-clock:: python3 + .. code-block:: python3 await storage.close() + await storage.wait_closed() """ diff --git a/dev_requirements.txt b/dev_requirements.txt index c0c2a39d..0252e7e1 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -16,3 +16,4 @@ sphinxcontrib-programoutput>=0.14 aiohttp-socks>=0.3.4 rethinkdb>=2.4.1 coverage==4.5.3 +motor>=2.2.0 diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index f53a4c95..b8f4962e 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -10,7 +10,7 @@ Filter factory greatly simplifies the reuse of filters when registering handlers Filters factory =============== -.. autoclass:: aiogram.dispatcher.filters.factory.FiltersFactory +.. autoclass:: aiogram.dispatcher.filters.FiltersFactory :members: :show-inheritance: @@ -21,28 +21,28 @@ Builtin filters Command ------- -.. autoclass:: aiogram.dispatcher.filters.builtin.Command +.. autoclass:: aiogram.dispatcher.filters.Command :members: :show-inheritance: CommandStart ------------ -.. autoclass:: aiogram.dispatcher.filters.builtin.CommandStart +.. autoclass:: aiogram.dispatcher.filters.CommandStart :members: :show-inheritance: CommandHelp ----------- -.. autoclass:: aiogram.dispatcher.filters.builtin.CommandHelp +.. autoclass:: aiogram.dispatcher.filters.CommandHelp :members: :show-inheritance: CommandSettings --------------- -.. autoclass:: aiogram.dispatcher.filters.builtin.CommandSettings +.. autoclass:: aiogram.dispatcher.filters.CommandSettings :members: :show-inheritance: @@ -50,7 +50,7 @@ CommandSettings CommandPrivacy -------------- -.. autoclass:: aiogram.dispatcher.filters.builtin.CommandPrivacy +.. autoclass:: aiogram.dispatcher.filters.CommandPrivacy :members: :show-inheritance: @@ -58,7 +58,7 @@ CommandPrivacy Text ---- -.. autoclass:: aiogram.dispatcher.filters.builtin.Text +.. autoclass:: aiogram.dispatcher.filters.Text :members: :show-inheritance: @@ -66,7 +66,7 @@ Text HashTag ------- -.. autoclass:: aiogram.dispatcher.filters.builtin.HashTag +.. autoclass:: aiogram.dispatcher.filters.HashTag :members: :show-inheritance: @@ -74,7 +74,7 @@ HashTag Regexp ------ -.. autoclass:: aiogram.dispatcher.filters.builtin.Regexp +.. autoclass:: aiogram.dispatcher.filters.Regexp :members: :show-inheritance: @@ -82,7 +82,7 @@ Regexp RegexpCommandsFilter -------------------- -.. autoclass:: aiogram.dispatcher.filters.builtin.RegexpCommandsFilter +.. autoclass:: aiogram.dispatcher.filters.RegexpCommandsFilter :members: :show-inheritance: @@ -90,21 +90,21 @@ RegexpCommandsFilter ContentTypeFilter ----------------- -.. autoclass:: aiogram.dispatcher.filters.builtin.ContentTypeFilter +.. autoclass:: aiogram.dispatcher.filters.ContentTypeFilter :members: :show-inheritance: IsSenderContact --------------- -.. autoclass:: aiogram.dispatcher.filters.builtin.IsSenderContact +.. autoclass:: aiogram.dispatcher.filters.IsSenderContact :members: :show-inheritance: StateFilter ----------- -.. autoclass:: aiogram.dispatcher.filters.builtin.StateFilter +.. autoclass:: aiogram.dispatcher.filters.StateFilter :members: :show-inheritance: @@ -112,13 +112,13 @@ StateFilter ExceptionsFilter ---------------- -.. autoclass:: aiogram.dispatcher.filters.builtin.ExceptionsFilter +.. autoclass:: aiogram.dispatcher.filters.ExceptionsFilter :members: :show-inheritance: IDFilter ----------------- +-------- .. autoclass:: aiogram.dispatcher.filters.builtin.IDFilter :members: @@ -126,9 +126,9 @@ IDFilter AdminFilter ----------------- +----------- -.. autoclass:: aiogram.dispatcher.filters.builtin.AdminFilter +.. autoclass:: aiogram.dispatcher.filters.AdminFilter :members: :show-inheritance: @@ -136,23 +136,23 @@ AdminFilter IsReplyFilter ------------- -.. autoclass:: aiogram.dispatcher.filters.filters.IsReplyFilter +.. autoclass:: aiogram.dispatcher.filters.IsReplyFilter :members: :show-inheritance: ForwardedMessageFilter -------------- +---------------------- -.. autoclass:: aiogram.dispatcher.filters.filters.ForwardedMessageFilter +.. autoclass:: aiogram.dispatcher.filters.ForwardedMessageFilter :members: :show-inheritance: ChatTypeFilter -------------- +-------------- -.. autoclass:: aiogram.dispatcher.filters.filters.ChatTypeFilter +.. autoclass:: aiogram.dispatcher.filters.ChatTypeFilter :members: :show-inheritance: @@ -170,19 +170,19 @@ Own filter can be: AbstractFilter -------------- -.. autoclass:: aiogram.dispatcher.filters.filters.AbstractFilter +.. autoclass:: aiogram.dispatcher.filters.AbstractFilter :members: :show-inheritance: Filter ------ -.. autoclass:: aiogram.dispatcher.filters.filters.Filter +.. autoclass:: aiogram.dispatcher.filters.Filter :members: :show-inheritance: BoundFilter ----------- -.. autoclass:: aiogram.dispatcher.filters.filters.BoundFilter +.. autoclass:: aiogram.dispatcher.filters.BoundFilter :members: :show-inheritance: diff --git a/docs/source/dispatcher/fsm.rst b/docs/source/dispatcher/fsm.rst index 93c94aa6..1b00e81e 100644 --- a/docs/source/dispatcher/fsm.rst +++ b/docs/source/dispatcher/fsm.rst @@ -12,15 +12,29 @@ Coming soon... Memory storage ~~~~~~~~~~~~~~ -Coming soon... + +.. autoclass:: aiogram.contrib.fsm_storage.memory.MemoryStorage + :show-inheritance: Redis storage ~~~~~~~~~~~~~ -Coming soon... + +.. autoclass:: aiogram.contrib.fsm_storage.redis.RedisStorage + :show-inheritance: + +Mongo storage +~~~~~~~~~~~~~ + +.. autoclass:: aiogram.contrib.fsm_storage.mongo.MongoStorage + :show-inheritance: + Rethink DB storage ~~~~~~~~~~~~~~~~~~ -Coming soon... + +.. autoclass:: aiogram.contrib.fsm_storage.rethinkdb.RethinkDBStorage + :show-inheritance: + Making own storage's ~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/examples/throtling_example.rst b/docs/source/examples/throtling_example.rst index eaeb337e..f64eaccc 100644 --- a/docs/source/examples/throtling_example.rst +++ b/docs/source/examples/throtling_example.rst @@ -7,8 +7,8 @@ Throtling example Example for throttling manager. You can use that for flood controlling. -.. literalinclude:: ../../../examples/throtling_example.py - :caption: throtling_example.py +.. literalinclude:: ../../../examples/throttling_example.py + :caption: throttling_example.py :language: python :linenos: :lines: 7- diff --git a/docs/source/examples/webhook_example_2.rst b/docs/source/examples/webhook_example_2.rst index 2ffdfed9..025264e4 100644 --- a/docs/source/examples/webhook_example_2.rst +++ b/docs/source/examples/webhook_example_2.rst @@ -1,10 +1,10 @@ .. Autogenerated file at 2018-09-08 02:07:37.576034 -================= -Webhook example 2 -================= +=================== +Webhook example old +=================== -.. literalinclude:: ../../../examples/webhook_example_2.py +.. literalinclude:: ../../../examples/webhook_example_old.py :caption: webhook_example_2.py :language: python :linenos: diff --git a/docs/source/utils/index.rst b/docs/source/utils/index.rst index 1ac3777c..4865518e 100644 --- a/docs/source/utils/index.rst +++ b/docs/source/utils/index.rst @@ -13,3 +13,4 @@ Utils parts json emoji + deep_linking From 41c104bb5705cf93fce91a82e773a80feef88a28 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sat, 26 Sep 2020 15:22:23 +0300 Subject: [PATCH 25/26] new: add exceptions (#427) * new: add FileIsTooBig exception * new: add MessageCantBeForwarded exception --- aiogram/utils/exceptions.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index a289be25..58e757e9 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -12,6 +12,7 @@ - MessageTextIsEmpty - MessageCantBeEdited - MessageCantBeDeleted + - MessageCantBeForwarded - MessageToEditNotFound - MessageToReplyNotFound - ToMuchMessages @@ -38,6 +39,7 @@ - URLHostIsEmpty - StartParamInvalid - ButtonDataInvalid + - FileIsTooBig - WrongFileIdentifier - GroupDeactivated - BadWebhook @@ -213,6 +215,10 @@ class MessageCantBeDeleted(MessageError): match = 'message can\'t be deleted' +class MessageCantBeForwarded(MessageError): + match = 'message can\'t be forwarded' + + class MessageToEditNotFound(MessageError): match = 'message to edit not found' @@ -347,6 +353,10 @@ class ButtonDataInvalid(BadRequest): text = 'Button data invalid' +class FileIsTooBig(BadRequest): + match = 'File is too big' + + class WrongFileIdentifier(BadRequest): match = 'wrong file identifier/HTTP URL specified' From 1e2fe72aca12a3fc6f2d1f66c71539af5a84ea00 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 26 Sep 2020 15:22:52 +0300 Subject: [PATCH 26/26] #429 added disable_web_page_preview for Message.send_copy(...) (#430) --- aiogram/types/message.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 9d2ad505..fc6bc77b 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -2084,6 +2084,7 @@ class Message(base.TelegramObject): self: Message, chat_id: typing.Union[str, int], disable_notification: typing.Optional[bool] = None, + disable_web_page_preview: typing.Optional[bool] = None, reply_to_message_id: typing.Optional[int] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, None @@ -2094,6 +2095,7 @@ class Message(base.TelegramObject): :param chat_id: :param disable_notification: + :param disable_web_page_preview: for text messages only :param reply_to_message_id: :param reply_markup: :return: @@ -2108,6 +2110,7 @@ class Message(base.TelegramObject): text = self.html_text if (self.text or self.caption) else None if self.text: + kwargs["disable_web_page_preview"] = disable_web_page_preview return await self.bot.send_message(text=text, **kwargs) elif self.audio: return await self.bot.send_audio(