From 61e1015c1e71ae5b86e830465076168206f81d01 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 20 Jul 2019 22:47:23 +0300 Subject: [PATCH 01/35] Minor refactor callback_data --- aiogram/utils/callback_data.py | 47 +++++++++++++++++----------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/aiogram/utils/callback_data.py b/aiogram/utils/callback_data.py index 916d08c4..b0162a7e 100644 --- a/aiogram/utils/callback_data.py +++ b/aiogram/utils/callback_data.py @@ -28,13 +28,13 @@ class CallbackData: def __init__(self, prefix, *parts, sep=':'): if not isinstance(prefix, str): - raise TypeError(f"Prefix must be instance of str not {type(prefix).__name__}") - elif not prefix: - raise ValueError('Prefix can\'t be empty') - elif sep in prefix: - raise ValueError(f"Separator '{sep}' can't be used in prefix") - elif not parts: - raise TypeError('Parts is not passed!') + raise TypeError(f'Prefix must be instance of str not {type(prefix).__name__}') + if not prefix: + raise ValueError("Prefix can't be empty") + if sep in prefix: + raise ValueError(f"Separator {sep!r} can't be used in prefix") + if not parts: + raise TypeError('Parts were not passed!') self.prefix = prefix self.sep = sep @@ -59,20 +59,20 @@ class CallbackData: if args: value = args.pop(0) else: - raise ValueError(f"Value for '{part}' is not passed!") + raise ValueError(f'Value for {part!r} was not passed!') if value is not None and not isinstance(value, str): value = str(value) if not value: - raise ValueError(f"Value for part {part} can't be empty!'") - elif self.sep in value: - raise ValueError(f"Symbol defined as separator can't be used in values of parts") + raise ValueError(f"Value for part {part!r} can't be empty!'") + if self.sep in value: + raise ValueError(f"Symbol {self.sep!r} is defined as the separator and can't be used in parts' values") data.append(value) if args or kwargs: - raise TypeError('Too many arguments is passed!') + raise TypeError('Too many arguments were passed!') callback_data = self.sep.join(data) if len(callback_data) > 64: @@ -106,30 +106,31 @@ class CallbackData: """ for key in config.keys(): if key not in self._part_names: - raise ValueError(f"Invalid field name '{key}'") + raise ValueError(f'Invalid field name {key!r}') return CallbackDataFilter(self, config) class CallbackDataFilter(Filter): + def __init__(self, factory: CallbackData, config: typing.Dict[str, str]): self.config = config self.factory = factory @classmethod def validate(cls, full_config: typing.Dict[str, typing.Any]): - raise ValueError('That filter can\'t be used in filters factory!') + raise ValueError("That filter can't be used in filters factory!") async def check(self, query: types.CallbackQuery): try: data = self.factory.parse(query.data) except ValueError: return False - else: - for key, value in self.config.items(): - if isinstance(value, (list, tuple, set)): - if data.get(key) not in value: - return False - else: - if value != data.get(key): - return False - return {'callback_data': data} + + for key, value in self.config.items(): + if isinstance(value, (list, tuple, set, frozenset)): + if data.get(key) not in value: + return False + else: + if data.get(key) != value: + return False + return {'callback_data': data} From 846f83f11713a3ba5185e13b238ae9c87123b82f Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 20 Jul 2019 23:08:38 +0300 Subject: [PATCH 02/35] Some callback_data_factory.py refactor --- aiogram/utils/executor.py | 2 +- examples/callback_data_factory.py | 28 +++++++++++++++------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index 65594371..cdb3fe91 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -339,7 +339,7 @@ class Executor: async def _skip_updates(self): await self.dispatcher.reset_webhook(True) await self.dispatcher.skip_updates() - log.warning(f"Updates are skipped successfully.") + log.warning(f'Updates were skipped successfully.') async def _welcome(self): user = await self.dispatcher.bot.me diff --git a/examples/callback_data_factory.py b/examples/callback_data_factory.py index 8fd197df..9a8affe9 100644 --- a/examples/callback_data_factory.py +++ b/examples/callback_data_factory.py @@ -1,4 +1,3 @@ -import asyncio import logging import random import uuid @@ -21,12 +20,12 @@ dp.middleware.setup(LoggingMiddleware()) POSTS = { str(uuid.uuid4()): { - 'title': f"Post {index}", + 'title': f'Post {index}', 'body': 'Lorem ipsum dolor sit amet, ' 'consectetur adipiscing elit, ' 'sed do eiusmod tempor incididunt ut ' 'labore et dolore magna aliqua', - 'votes': random.randint(-2, 5) + 'votes': random.randint(-2, 5), } for index in range(1, 6) } @@ -42,21 +41,24 @@ def get_keyboard() -> types.InlineKeyboardMarkup: markup.add( types.InlineKeyboardButton( post['title'], - callback_data=posts_cb.new(id=post_id, action='view')) + callback_data=posts_cb.new(id=post_id, action='view')), ) return markup def format_post(post_id: str, post: dict) -> (str, types.InlineKeyboardMarkup): - text = f"{md.hbold(post['title'])}\n" \ - f"{md.quote_html(post['body'])}\n" \ - f"\n" \ - f"Votes: {post['votes']}" + text = md.text( + md.hbold(post['title']), + md.quote_html(post['body']), + '', # just new empty line + f"Votes: {post['votes']}", + sep = '\n', + ) markup = types.InlineKeyboardMarkup() markup.row( types.InlineKeyboardButton('👍', callback_data=posts_cb.new(id=post_id, action='like')), - types.InlineKeyboardButton('👎', callback_data=posts_cb.new(id=post_id, action='unlike')), + types.InlineKeyboardButton('👎', callback_data=posts_cb.new(id=post_id, action='dislike')), ) markup.add(types.InlineKeyboardButton('<< Back', callback_data=posts_cb.new(id='-', action='list'))) return text, markup @@ -84,7 +86,7 @@ async def query_view(query: types.CallbackQuery, callback_data: dict): await query.message.edit_text(text, reply_markup=markup) -@dp.callback_query_handler(posts_cb.filter(action=['like', 'unlike'])) +@dp.callback_query_handler(posts_cb.filter(action=['like', 'dislike'])) async def query_post_vote(query: types.CallbackQuery, callback_data: dict): try: await dp.throttle('vote', rate=1) @@ -100,10 +102,10 @@ async def query_post_vote(query: types.CallbackQuery, callback_data: dict): if action == 'like': post['votes'] += 1 - elif action == 'unlike': + elif action == 'dislike': post['votes'] -= 1 - await query.answer('Voted.') + await query.answer('Vote accepted') text, markup = format_post(post_id, post) await query.message.edit_text(text, reply_markup=markup) @@ -114,4 +116,4 @@ async def message_not_modified_handler(update, error): if __name__ == '__main__': - executor.start_polling(dp, loop=loop, skip_updates=True) + executor.start_polling(dp, skip_updates=True) From cb7ec3edb83518fafd2f0ddc9aa1f229f51bfb87 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 20 Jul 2019 23:37:52 +0300 Subject: [PATCH 03/35] Refactor examples/callback_data_factory_simple.py --- examples/callback_data_factory_simple.py | 42 +++++++++++------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/examples/callback_data_factory_simple.py b/examples/callback_data_factory_simple.py index 2c6a8358..5fc9c548 100644 --- a/examples/callback_data_factory_simple.py +++ b/examples/callback_data_factory_simple.py @@ -27,42 +27,40 @@ likes = {} # user_id: amount_of_likes def get_keyboard(): return types.InlineKeyboardMarkup().row( types.InlineKeyboardButton('👍', callback_data=vote_cb.new(action='up')), - types.InlineKeyboardButton('👎', callback_data=vote_cb.new(action='down'))) + types.InlineKeyboardButton('👎', callback_data=vote_cb.new(action='down')), + ) @dp.message_handler(commands=['start']) async def cmd_start(message: types.Message): amount_of_likes = likes.get(message.from_user.id, 0) # get value if key exists else set to 0 - await message.reply(f'Vote! Now you have {amount_of_likes} votes.', reply_markup=get_keyboard()) + await message.reply(f'Vote! You have {amount_of_likes} votes now.', reply_markup=get_keyboard()) -@dp.callback_query_handler(vote_cb.filter(action='up')) -async def vote_up_cb_handler(query: types.CallbackQuery, callback_data: dict): - logging.info(callback_data) # callback_data contains all info from callback data - likes[query.from_user.id] = likes.get(query.from_user.id, 0) + 1 # update amount of likes in storage - amount_of_likes = likes[query.from_user.id] +@dp.callback_query_handler(vote_cb.filter(action=['up', 'down'])) +async def callback_vote_action(query: types.CallbackQuery, callback_data: dict): + 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'] + likes_count = likes.get(query.from_user.id, 0) - await bot.edit_message_text(f'You voted up! Now you have {amount_of_likes} votes.', - query.from_user.id, - query.message.message_id, - reply_markup=get_keyboard()) + if callback_data_action == 'up': + likes_count += 1 + else: + likes_count -= 1 + likes[query.from_user.id] = likes_count # update amount of likes in storage -@dp.callback_query_handler(vote_cb.filter(action='down')) -async def vote_down_cb_handler(query: types.CallbackQuery, callback_data: dict): - logging.info(callback_data) # callback_data contains all info from callback data - likes[query.from_user.id] = likes.get(query.from_user.id, 0) - 1 # update amount of likes in storage - amount_of_likes = likes[query.from_user.id] - - await bot.edit_message_text(f'You voted down! Now you have {amount_of_likes} votes.', - query.from_user.id, - query.message.message_id, - reply_markup=get_keyboard()) + await bot.edit_message_text( + f'You voted {callback_data_action}! Now you have {likes_count} vote[s].', + query.from_user.id, + query.message.message_id, + reply_markup=get_keyboard(), + ) @dp.errors_handler(exception=MessageNotModified) # handle the cases when this exception raises async def message_not_modified_handler(update, error): - # pass return True From f4aafb043e7c3e0d82e4070b1351cf22ecb6647c Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 21 Jul 2019 11:19:08 +0300 Subject: [PATCH 04/35] Refactor examples/check_user_language.py --- examples/check_user_language.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/check_user_language.py b/examples/check_user_language.py index f59246cf..98bed8a6 100644 --- a/examples/check_user_language.py +++ b/examples/check_user_language.py @@ -2,7 +2,6 @@ Babel is required. """ -import asyncio import logging from aiogram import Bot, Dispatcher, executor, md, types @@ -22,12 +21,13 @@ async def check_language(message: types.Message): await message.reply(md.text( md.bold('Info about your language:'), - md.text(' 🔸', md.bold('Code:'), md.italic(locale.locale)), - md.text(' 🔸', md.bold('Territory:'), md.italic(locale.territory or 'Unknown')), - md.text(' 🔸', md.bold('Language name:'), md.italic(locale.language_name)), - md.text(' 🔸', md.bold('English language name:'), md.italic(locale.english_name)), - sep='\n')) + md.text('🔸', md.bold('Code:'), md.code(locale.language)), + md.text('🔸', md.bold('Territory:'), md.code(locale.territory or 'Unknown')), + md.text('🔸', md.bold('Language name:'), md.code(locale.language_name)), + md.text('🔸', md.bold('English language name:'), md.code(locale.english_name)), + sep='\n', + )) if __name__ == '__main__': - executor.start_polling(dp, loop=loop, skip_updates=True) + executor.start_polling(dp, skip_updates=True) From 3867b72df6e95ae6bddc3979f62deaf3870cc22f Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 21 Jul 2019 11:30:33 +0300 Subject: [PATCH 05/35] Refactor examples/echo_bot.py --- examples/echo_bot.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/examples/echo_bot.py b/examples/echo_bot.py index 27dc70d9..00046f3a 100644 --- a/examples/echo_bot.py +++ b/examples/echo_bot.py @@ -20,7 +20,7 @@ dp = Dispatcher(bot) @dp.message_handler(commands=['start', 'help']) async def send_welcome(message: types.Message): """ - This handler will be called when client send `/start` or `/help` commands. + This handler will be called when user sends `/start` or `/help` command """ await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.") @@ -28,13 +28,25 @@ async def send_welcome(message: types.Message): @dp.message_handler(regexp='(^cat[s]?$|puss)') async def cats(message: types.Message): with open('data/cats.jpg', 'rb') as photo: - await bot.send_photo(message.chat.id, photo, caption='Cats is here 😺', - reply_to_message_id=message.message_id) + ''' + # Old fashioned way: + await bot.send_photo( + message.chat.id, + photo, + caption='Cats are here 😺', + reply_to_message_id=message.message_id, + ) + ''' + + await message.reply_photo(photo, caption='Cats are here 😺') @dp.message_handler() async def echo(message: types.Message): - await bot.send_message(message.chat.id, message.text) + # old style: + # await bot.send_message(message.chat.id, message.text) + + await message.reply(message.text, reply=False) if __name__ == '__main__': From 6cf06bd081678948718970b2c73a5da2ce14d27d Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 21 Jul 2019 23:44:24 +0300 Subject: [PATCH 06/35] Fix test_states_group name --- tests/{states_group.py => test_states_group.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{states_group.py => test_states_group.py} (100%) diff --git a/tests/states_group.py b/tests/test_states_group.py similarity index 100% rename from tests/states_group.py rename to tests/test_states_group.py From cabb10bc06d045f89adc9111ccc60c30fa7643d1 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 21 Jul 2019 23:45:25 +0300 Subject: [PATCH 07/35] Create some tests --- .../test_filters/test_builtin.py | 18 ++++++++++++++++++ .../test_dispatcher/test_filters/test_state.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 tests/test_dispatcher/test_filters/test_builtin.py create mode 100644 tests/test_dispatcher/test_filters/test_state.py diff --git a/tests/test_dispatcher/test_filters/test_builtin.py b/tests/test_dispatcher/test_filters/test_builtin.py new file mode 100644 index 00000000..86344cec --- /dev/null +++ b/tests/test_dispatcher/test_filters/test_builtin.py @@ -0,0 +1,18 @@ +import pytest + +from aiogram.dispatcher.filters.builtin import Text + + +class TestText: + + @pytest.mark.parametrize('param, key', [ + ('text', 'equals'), + ('text_contains', 'contains'), + ('text_startswith', 'startswith'), + ('text_endswith', 'endswith'), + ]) + def test_validate(self, param, key): + value = 'spam and eggs' + config = {param: value} + res = Text.validate(config) + assert res == {key: value} diff --git a/tests/test_dispatcher/test_filters/test_state.py b/tests/test_dispatcher/test_filters/test_state.py new file mode 100644 index 00000000..b7f5a5fd --- /dev/null +++ b/tests/test_dispatcher/test_filters/test_state.py @@ -0,0 +1,18 @@ +from aiogram.dispatcher.filters.state import StatesGroup + +class TestStatesGroup: + + def test_all_childs(self): + + class InnerState1(StatesGroup): + pass + + class InnerState2(InnerState1): + pass + + class Form(StatesGroup): + inner1 = InnerState1 + inner2 = InnerState2 + + form_childs = Form.all_childs + assert form_childs == (InnerState1, InnerState2) From ad7238fda8d51f9a96fb5d32be4f1f2b68b9396a Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Mon, 22 Jul 2019 00:11:06 +0300 Subject: [PATCH 08/35] Some refactor --- aiogram/dispatcher/dispatcher.py | 10 +-- aiogram/dispatcher/filters/builtin.py | 18 ++-- aiogram/dispatcher/filters/state.py | 23 +++--- aiogram/utils/mixins.py | 4 +- dev_requirements.txt | 1 + examples/finite_state_machine_example.py | 43 ++++++---- tests/types/dataset.py | 100 +++++++++++------------ 7 files changed, 106 insertions(+), 93 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 0da5f621..8236118e 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -97,22 +97,22 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_factory.bind(Text, event_handlers=[ self.message_handlers, self.edited_message_handlers, self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers + self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers, ]) filters_factory.bind(HashTag, event_handlers=[ self.message_handlers, self.edited_message_handlers, - self.channel_post_handlers, self.edited_channel_post_handlers + self.channel_post_handlers, self.edited_channel_post_handlers, ]) filters_factory.bind(Regexp, event_handlers=[ self.message_handlers, self.edited_message_handlers, self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers + self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers, ]) filters_factory.bind(RegexpCommandsFilter, event_handlers=[ - self.message_handlers, self.edited_message_handlers + self.message_handlers, self.edited_message_handlers, ]) filters_factory.bind(ExceptionsFilter, event_handlers=[ - self.errors_handlers + self.errors_handlers, ]) filters_factory.bind(IdFilter, event_handlers=[ self.message_handlers, self.edited_message_handlers, diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index df72b9d0..9a44de23 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -205,6 +205,13 @@ class Text(Filter): Simple text filter """ + _default_params = ( + ('text', 'equals'), + ('text_contains', 'contains'), + ('text_startswith', 'startswith'), + ('text_endswith', 'endswith'), + ) + def __init__(self, equals: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None, contains: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None, @@ -244,14 +251,9 @@ class Text(Filter): @classmethod def validate(cls, full_config: Dict[str, Any]): - if 'text' in full_config: - return {'equals': full_config.pop('text')} - elif 'text_contains' in full_config: - return {'contains': full_config.pop('text_contains')} - elif 'text_startswith' in full_config: - return {'startswith': full_config.pop('text_startswith')} - elif 'text_endswith' in full_config: - return {'endswith': full_config.pop('text_endswith')} + for param, key in cls._default_params: + if param in full_config: + return {key: full_config.pop(param)} async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]): if isinstance(obj, Message): diff --git a/aiogram/dispatcher/filters/state.py b/aiogram/dispatcher/filters/state.py index afe08e64..16937e1c 100644 --- a/aiogram/dispatcher/filters/state.py +++ b/aiogram/dispatcher/filters/state.py @@ -25,17 +25,17 @@ class State: @property def state(self): - if self._state is None: - return None - elif self._state == '*': + if self._state is None or self._state == '*': return self._state - elif self._group_name is None and self._group: + + if self._group_name is None and self._group: group = self._group.__full_group_name__ elif self._group_name: group = self._group_name else: group = '@' - return f"{group}:{self._state}" + + return f'{group}:{self._state}' def set_parent(self, group): if not issubclass(group, StatesGroup): @@ -73,7 +73,6 @@ class StatesGroupMeta(type): elif inspect.isclass(prop) and issubclass(prop, StatesGroup): childs.append(prop) prop._parent = cls - # continue cls._parent = None cls._childs = tuple(childs) @@ -83,13 +82,13 @@ class StatesGroupMeta(type): return cls @property - def __group_name__(cls): + def __group_name__(cls) -> str: return cls._group_name @property - def __full_group_name__(cls): + def __full_group_name__(cls) -> str: if cls._parent: - return cls._parent.__full_group_name__ + '.' + cls._group_name + return '.'.join((cls._parent.__full_group_name__, cls._group_name)) return cls._group_name @property @@ -97,7 +96,7 @@ class StatesGroupMeta(type): return cls._states @property - def childs(cls): + def childs(cls) -> tuple: return cls._childs @property @@ -130,9 +129,9 @@ class StatesGroupMeta(type): def __contains__(cls, item): if isinstance(item, str): return item in cls.all_states_names - elif isinstance(item, State): + if isinstance(item, State): return item in cls.all_states - elif isinstance(item, StatesGroup): + if isinstance(item, StatesGroup): return item in cls.all_childs return False diff --git a/aiogram/utils/mixins.py b/aiogram/utils/mixins.py index 776479bd..e6857263 100644 --- a/aiogram/utils/mixins.py +++ b/aiogram/utils/mixins.py @@ -31,7 +31,7 @@ T = TypeVar('T') class ContextInstanceMixin: def __init_subclass__(cls, **kwargs): - cls.__context_instance = contextvars.ContextVar('instance_' + cls.__name__) + cls.__context_instance = contextvars.ContextVar(f'instance_{cls.__name__}') return cls @classmethod @@ -43,5 +43,5 @@ class ContextInstanceMixin: @classmethod def set_current(cls: Type[T], value: T): if not isinstance(value, cls): - raise TypeError(f"Value should be instance of '{cls.__name__}' not '{type(value).__name__}'") + raise TypeError(f'Value should be instance of {cls.__name__!r} not {type(value).__name__!r}') cls.__context_instance.set(value) diff --git a/dev_requirements.txt b/dev_requirements.txt index 79adc949..06bc3e9c 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -15,3 +15,4 @@ sphinx-rtd-theme>=0.4.3 sphinxcontrib-programoutput>=0.14 aiohttp-socks>=0.2.2 rethinkdb>=2.4.1 +coverage==4.5.3 diff --git a/examples/finite_state_machine_example.py b/examples/finite_state_machine_example.py index 66f89fb2..7c0536a7 100644 --- a/examples/finite_state_machine_example.py +++ b/examples/finite_state_machine_example.py @@ -1,16 +1,19 @@ -import asyncio -from typing import Optional +import logging import aiogram.utils.markdown as md from aiogram import Bot, Dispatcher, types from aiogram.contrib.fsm_storage.memory import MemoryStorage from aiogram.dispatcher import FSMContext +from aiogram.dispatcher.filters import Text from aiogram.dispatcher.filters.state import State, StatesGroup from aiogram.types import ParseMode from aiogram.utils import executor +logging.basicConfig(level=logging.INFO) + API_TOKEN = 'BOT TOKEN HERE' + bot = Bot(token=API_TOKEN) # For example use simple MemoryStorage for Dispatcher. @@ -25,7 +28,7 @@ class Form(StatesGroup): gender = State() # Will be represented in storage as 'Form:gender' -@dp.message_handler(commands=['start']) +@dp.message_handler(commands='start') async def cmd_start(message: types.Message): """ Conversation's entry point @@ -37,19 +40,21 @@ async def cmd_start(message: types.Message): # You can use state '*' if you need to handle all states -@dp.message_handler(state='*', commands=['cancel']) -@dp.message_handler(lambda message: message.text.lower() == 'cancel', state='*') -async def cancel_handler(message: types.Message, state: FSMContext, raw_state: Optional[str] = None): +@dp.message_handler(state='*', commands='cancel') +@dp.message_handler(Text(equals='cancel', ignore_case=True), state='*') +async def cancel_handler(message: types.Message, state: FSMContext): """ Allow user to cancel any action """ - if raw_state is None: + current_state = await state.get_state() + if current_state is None: return + logging.info('Cancelling state %r', current_state) # Cancel state and inform user about it await state.finish() # And remove keyboard (just in case) - await message.reply('Canceled.', reply_markup=types.ReplyKeyboardRemove()) + await message.reply('Cancelled.', reply_markup=types.ReplyKeyboardRemove()) @dp.message_handler(state=Form.name) @@ -66,7 +71,7 @@ async def process_name(message: types.Message, state: FSMContext): # Check age. Age gotta be digit @dp.message_handler(lambda message: not message.text.isdigit(), state=Form.age) -async def failed_process_age(message: types.Message): +async def process_age_invalid(message: types.Message): """ If age is invalid """ @@ -88,11 +93,11 @@ async def process_age(message: types.Message, state: FSMContext): @dp.message_handler(lambda message: message.text not in ["Male", "Female", "Other"], state=Form.gender) -async def failed_process_gender(message: types.Message): +async def process_gender_invalid(message: types.Message): """ In this example gender has to be one of: Male, Female, Other. """ - return await message.reply("Bad gender name. Choose you gender from keyboard.") + return await message.reply("Bad gender name. Choose your gender from the keyboard.") @dp.message_handler(state=Form.gender) @@ -104,11 +109,17 @@ async def process_gender(message: types.Message, state: FSMContext): markup = types.ReplyKeyboardRemove() # And send message - await bot.send_message(message.chat.id, md.text( - md.text('Hi! Nice to meet you,', md.bold(data['name'])), - md.text('Age:', data['age']), - md.text('Gender:', data['gender']), - sep='\n'), reply_markup=markup, parse_mode=ParseMode.MARKDOWN) + await bot.send_message( + message.chat.id, + md.text( + md.text('Hi! Nice to meet you,', md.bold(data['name'])), + md.text('Age:', md.code(data['age'])), + md.text('Gender:', data['gender']), + sep='\n', + ), + reply_markup=markup, + parse_mode=ParseMode.MARKDOWN, + ) # Finish conversation await state.finish() diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 4167eae1..18bcbdad 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -8,7 +8,7 @@ USER = { "first_name": "FirstName", "last_name": "LastName", "username": "username", - "language_code": "ru" + "language_code": "ru", } CHAT = { @@ -16,14 +16,14 @@ CHAT = { "first_name": "FirstName", "last_name": "LastName", "username": "username", - "type": "private" + "type": "private", } PHOTO = { "file_id": "AgADBAADFak0G88YZAf8OAug7bHyS9x2ZxkABHVfpJywcloRAAGAAQABAg", "file_size": 1101, "width": 90, - "height": 51 + "height": 51, } AUDIO = { @@ -32,7 +32,7 @@ AUDIO = { "title": "The Best Song", "performer": "The Best Singer", "file_id": "CQADAgADbQEAAsnrIUpNoRRNsH7_hAI", - "file_size": 9507774 + "file_size": 9507774, } CHAT_MEMBER = { @@ -44,7 +44,7 @@ CHAT_MEMBER = { "can_invite_users": True, "can_restrict_members": True, "can_pin_messages": True, - "can_promote_members": False + "can_promote_members": False, } CONTACT = { @@ -57,7 +57,7 @@ DOCUMENT = { "file_name": "test.docx", "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "file_id": "BQADAgADpgADy_JxS66XQTBRHFleAg", - "file_size": 21331 + "file_size": 21331, } ANIMATION = { @@ -65,51 +65,51 @@ ANIMATION = { "mime_type": "video/mp4", "thumb": PHOTO, "file_id": "CgADBAAD4DUAAoceZAe2WiE9y0crrAI", - "file_size": 65837 + "file_size": 65837, } ENTITY_BOLD = { "offset": 5, "length": 2, - "type": "bold" + "type": "bold", } ENTITY_ITALIC = { "offset": 8, "length": 1, - "type": "italic" + "type": "italic", } ENTITY_LINK = { "offset": 10, "length": 6, "type": "text_link", - "url": "http://google.com/" + "url": "http://google.com/", } ENTITY_CODE = { "offset": 17, "length": 7, - "type": "code" + "type": "code", } ENTITY_PRE = { "offset": 30, "length": 4, - "type": "pre" + "type": "pre", } ENTITY_MENTION = { "offset": 47, "length": 9, - "type": "mention" + "type": "mention", } GAME = { "title": "Karate Kido", "description": "No trees were harmed in the making of this game :)", "photo": [PHOTO, PHOTO, PHOTO], - "animation": ANIMATION + "animation": ANIMATION, } INVOICE = { @@ -120,19 +120,19 @@ INVOICE = { "Order our Working Time Machine today!", "start_parameter": "time-machine-example", "currency": "USD", - "total_amount": 6250 + "total_amount": 6250, } LOCATION = { "latitude": 50.693416, - "longitude": 30.624605 + "longitude": 30.624605, } VENUE = { "location": LOCATION, "title": "Venue Name", "address": "Venue Address", - "foursquare_id": "4e6f2cec483bad563d150f98" + "foursquare_id": "4e6f2cec483bad563d150f98", } SHIPPING_ADDRESS = { @@ -141,7 +141,7 @@ SHIPPING_ADDRESS = { "city": "DefaultCity", "street_line1": "Central", "street_line2": "Middle", - "post_code": "424242" + "post_code": "424242", } STICKER = { @@ -156,7 +156,7 @@ STICKER = { "height": 128 }, "file_id": "AAbbCCddEEffGGhh1234567890", - "file_size": 12345 + "file_size": 12345, } SUCCESSFUL_PAYMENT = { @@ -164,7 +164,7 @@ SUCCESSFUL_PAYMENT = { "total_amount": 6250, "invoice_payload": "HAPPY FRIDAYS COUPON", "telegram_payment_charge_id": "_", - "provider_payment_charge_id": "12345678901234_test" + "provider_payment_charge_id": "12345678901234_test", } VIDEO = { @@ -174,7 +174,7 @@ VIDEO = { "mime_type": "video/quicktime", "thumb": PHOTO, "file_id": "BAADAgpAADdawy_JxS72kRvV3cortAg", - "file_size": 10099782 + "file_size": 10099782, } VIDEO_NOTE = { @@ -182,14 +182,14 @@ VIDEO_NOTE = { "length": 240, "thumb": PHOTO, "file_id": "AbCdEfGhIjKlMnOpQrStUvWxYz", - "file_size": 186562 + "file_size": 186562, } VOICE = { "duration": 1, "mime_type": "audio/ogg", "file_id": "AwADawAgADADy_JxS2gopIVIIxlhAg", - "file_size": 4321 + "file_size": 4321, } CALLBACK_QUERY = {} @@ -206,7 +206,7 @@ EDITED_MESSAGE = { "chat": CHAT, "date": 1508825372, "edit_date": 1508825379, - "text": "hi there (edited)" + "text": "hi there (edited)", } FORWARDED_MESSAGE = { @@ -219,7 +219,7 @@ FORWARDED_MESSAGE = { "forward_date": 1522749037, "text": "Forwarded text with entities from public channel ", "entities": [ENTITY_BOLD, ENTITY_CODE, ENTITY_ITALIC, ENTITY_LINK, - ENTITY_LINK, ENTITY_MENTION, ENTITY_PRE] + ENTITY_LINK, ENTITY_MENTION, ENTITY_PRE], } INLINE_QUERY = {} @@ -229,7 +229,7 @@ MESSAGE = { "from": USER, "chat": CHAT, "date": 1508709711, - "text": "Hi, world!" + "text": "Hi, world!", } MESSAGE_WITH_AUDIO = { @@ -238,7 +238,7 @@ MESSAGE_WITH_AUDIO = { "chat": CHAT, "date": 1508739776, "audio": AUDIO, - "caption": "This is my favourite song" + "caption": "This is my favourite song", } MESSAGE_WITH_AUTHOR_SIGNATURE = {} @@ -250,7 +250,7 @@ MESSAGE_WITH_CONTACT = { "from": USER, "chat": CHAT, "date": 1522850298, - "contact": CONTACT + "contact": CONTACT, } MESSAGE_WITH_DELETE_CHAT_PHOTO = {} @@ -261,7 +261,7 @@ MESSAGE_WITH_DOCUMENT = { "chat": CHAT, "date": 1508768012, "document": DOCUMENT, - "caption": "Read my document" + "caption": "Read my document", } MESSAGE_WITH_EDIT_DATE = {} @@ -273,7 +273,7 @@ MESSAGE_WITH_GAME = { "from": USER, "chat": CHAT, "date": 1508824810, - "game": GAME + "game": GAME, } MESSAGE_WITH_GROUP_CHAT_CREATED = {} @@ -283,7 +283,7 @@ MESSAGE_WITH_INVOICE = { "from": USER, "chat": CHAT, "date": 1508761719, - "invoice": INVOICE + "invoice": INVOICE, } MESSAGE_WITH_LEFT_CHAT_MEMBER = {} @@ -293,7 +293,7 @@ MESSAGE_WITH_LOCATION = { "from": USER, "chat": CHAT, "date": 1508755473, - "location": LOCATION + "location": LOCATION, } MESSAGE_WITH_MIGRATE_TO_CHAT_ID = { @@ -301,7 +301,7 @@ MESSAGE_WITH_MIGRATE_TO_CHAT_ID = { "from": USER, "chat": CHAT, "date": 1526943253, - "migrate_to_chat_id": -1234567890987 + "migrate_to_chat_id": -1234567890987, } MESSAGE_WITH_MIGRATE_FROM_CHAT_ID = { @@ -309,7 +309,7 @@ MESSAGE_WITH_MIGRATE_FROM_CHAT_ID = { "from": USER, "chat": CHAT, "date": 1526943253, - "migrate_from_chat_id": -123456789 + "migrate_from_chat_id": -123456789, } MESSAGE_WITH_NEW_CHAT_MEMBERS = {} @@ -324,7 +324,7 @@ MESSAGE_WITH_PHOTO = { "chat": CHAT, "date": 1508825154, "photo": [PHOTO, PHOTO, PHOTO, PHOTO], - "caption": "photo description" + "caption": "photo description", } MESSAGE_WITH_MEDIA_GROUP = { @@ -333,7 +333,7 @@ MESSAGE_WITH_MEDIA_GROUP = { "chat": CHAT, "date": 1522843665, "media_group_id": "12182749320567362", - "photo": [PHOTO, PHOTO, PHOTO, PHOTO] + "photo": [PHOTO, PHOTO, PHOTO, PHOTO], } MESSAGE_WITH_PINNED_MESSAGE = {} @@ -345,7 +345,7 @@ MESSAGE_WITH_STICKER = { "from": USER, "chat": CHAT, "date": 1508771450, - "sticker": STICKER + "sticker": STICKER, } MESSAGE_WITH_SUCCESSFUL_PAYMENT = { @@ -353,7 +353,7 @@ MESSAGE_WITH_SUCCESSFUL_PAYMENT = { "from": USER, "chat": CHAT, "date": 1508761169, - "successful_payment": SUCCESSFUL_PAYMENT + "successful_payment": SUCCESSFUL_PAYMENT, } MESSAGE_WITH_SUPERGROUP_CHAT_CREATED = {} @@ -364,7 +364,7 @@ MESSAGE_WITH_VENUE = { "chat": CHAT, "date": 1522849819, "location": LOCATION, - "venue": VENUE + "venue": VENUE, } MESSAGE_WITH_VIDEO = { @@ -373,7 +373,7 @@ MESSAGE_WITH_VIDEO = { "chat": CHAT, "date": 1508756494, "video": VIDEO, - "caption": "description" + "caption": "description", } MESSAGE_WITH_VIDEO_NOTE = { @@ -381,7 +381,7 @@ MESSAGE_WITH_VIDEO_NOTE = { "from": USER, "chat": CHAT, "date": 1522835890, - "video_note": VIDEO_NOTE + "video_note": VIDEO_NOTE, } MESSAGE_WITH_VOICE = { @@ -389,7 +389,7 @@ MESSAGE_WITH_VOICE = { "from": USER, "chat": CHAT, "date": 1508768403, - "voice": VOICE + "voice": VOICE, } PRE_CHECKOUT_QUERY = { @@ -397,7 +397,7 @@ PRE_CHECKOUT_QUERY = { "from": USER, "currency": "USD", "total_amount": 6250, - "invoice_payload": "HAPPY FRIDAYS COUPON" + "invoice_payload": "HAPPY FRIDAYS COUPON", } REPLY_MESSAGE = { @@ -406,37 +406,37 @@ REPLY_MESSAGE = { "chat": CHAT, "date": 1508751866, "reply_to_message": MESSAGE, - "text": "Reply to quoted message" + "text": "Reply to quoted message", } SHIPPING_QUERY = { "id": "262181558684397422", "from": USER, "invoice_payload": "HAPPY FRIDAYS COUPON", - "shipping_address": SHIPPING_ADDRESS + "shipping_address": SHIPPING_ADDRESS, } USER_PROFILE_PHOTOS = { "total_count": 1, "photos": [ - [PHOTO, PHOTO, PHOTO] - ] + [PHOTO, PHOTO, PHOTO], + ], } FILE = { "file_id": "XXXYYYZZZ", "file_size": 5254, - "file_path": "voice\/file_8" + "file_path": "voice/file_8", } INVITE_LINK = 'https://t.me/joinchat/AbCdEfjKILDADwdd123' UPDATE = { "update_id": 123456789, - "message": MESSAGE + "message": MESSAGE, } WEBHOOK_INFO = { "url": "", "has_custom_certificate": False, - "pending_update_count": 0 + "pending_update_count": 0, } From d360538bba17ae8032889ae804307025db90b5e8 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 10 Aug 2019 00:08:05 +0300 Subject: [PATCH 09/35] Some localization changes --- examples/locales/mybot.pot | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/examples/locales/mybot.pot b/examples/locales/mybot.pot index b0736569..e2cf1014 100644 --- a/examples/locales/mybot.pot +++ b/examples/locales/mybot.pot @@ -1,31 +1,27 @@ # Translations template for PROJECT. -# Copyright (C) 2018 ORGANIZATION +# Copyright (C) 2019 ORGANIZATION # This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2018. +# FIRST AUTHOR , 2019. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2018-06-30 03:50+0300\n" +"POT-Creation-Date: 2019-07-22 20:45+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.6.0\n" +"Generated-By: Babel 2.7.0\n" -#: i18n_example.py:48 +#: i18n_example.py:60 msgid "Hello, {user}!" msgstr "" -#: i18n_example.py:53 +#: i18n_example.py:65 msgid "Your current language: {language}" msgstr "" -msgid "Aiogram has {number} like!" -msgid_plural "Aiogram has {number} likes!" -msgstr[0] "" -msgstr[1] "" From a78b93c5cdeb9918893bb05247b484112ef35037 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 10 Aug 2019 17:55:39 +0300 Subject: [PATCH 10/35] Refactor examples/i18n_example.py --- aiogram/contrib/middlewares/i18n.py | 6 ++--- aiogram/types/user.py | 4 +++- examples/i18n_example.py | 28 ++++++++++++++++++------ examples/locales/mybot.pot | 5 ++--- examples/locales/ru/LC_MESSAGES/mybot.po | 17 +++++++------- 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py index 8373f3d6..0bb10680 100644 --- a/aiogram/contrib/middlewares/i18n.py +++ b/aiogram/contrib/middlewares/i18n.py @@ -97,15 +97,13 @@ class I18nMiddleware(BaseMiddleware): if locale not in self.locales: if n is 1: return singular - else: - return plural + return plural translator = self.locales[locale] if plural is None: return translator.gettext(singular) - else: - return translator.ngettext(singular, plural, n) + return translator.ngettext(singular, plural, n) def lazy_gettext(self, singular, plural=None, n=1, locale=None, enable_cache=True) -> LazyProxy: """ diff --git a/aiogram/types/user.py b/aiogram/types/user.py index 441c275f..27ee27e0 100644 --- a/aiogram/types/user.py +++ b/aiogram/types/user.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Optional + import babel from . import base @@ -45,7 +47,7 @@ class User(base.TelegramObject): return self.full_name @property - def locale(self) -> babel.core.Locale or None: + def locale(self) -> Optional[babel.core.Locale]: """ Get user's locale diff --git a/examples/i18n_example.py b/examples/i18n_example.py index bf23c8d1..f69482af 100644 --- a/examples/i18n_example.py +++ b/examples/i18n_example.py @@ -54,14 +54,16 @@ dp.middleware.setup(i18n) _ = i18n.gettext -@dp.message_handler(commands=['start']) +@dp.message_handler(commands='start') async def cmd_start(message: types.Message): # Simply use `_('message')` instead of `'message'` and never use f-strings for translatable texts. await message.reply(_('Hello, {user}!').format(user=message.from_user.full_name)) -@dp.message_handler(commands=['lang']) +@dp.message_handler(commands='lang') async def cmd_lang(message: types.Message, locale): + # For setting custom lang you have to modify i18n middleware, like this: + # https://github.com/aiogram/EventsTrackerBot/blob/master/modules/base/middlewares.py await message.reply(_('Your current language: {language}').format(language=locale)) # If you care about pluralization, here's small handler @@ -70,15 +72,27 @@ async def cmd_lang(message: types.Message, locale): # Alias for gettext method, parser will understand double underscore as plural (aka ngettext) __ = i18n.gettext -# Some pseudo numeric value -TOTAL_LIKES = 0 -@dp.message_handler(commands=['like']) +# some likes manager +LIKES_STORAGE = {'count': 0} + + +def get_likes() -> int: + return LIKES_STORAGE['count'] + + +def increase_likes() -> int: + LIKES_STORAGE['count'] += 1 + return get_likes() +# + + +@dp.message_handler(commands='like') async def cmd_like(message: types.Message, locale): - TOTAL_LIKES += 1 + likes = increase_likes() # NOTE: This is comment for a translator - await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', TOTAL_LIKES).format(number=TOTAL_LIKES)) + 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) diff --git a/examples/locales/mybot.pot b/examples/locales/mybot.pot index e2cf1014..62b2d425 100644 --- a/examples/locales/mybot.pot +++ b/examples/locales/mybot.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2019-07-22 20:45+0300\n" +"POT-Creation-Date: 2019-08-10 17:51+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -21,7 +21,6 @@ msgstr "" msgid "Hello, {user}!" msgstr "" -#: i18n_example.py:65 +#: i18n_example.py:67 msgid "Your current language: {language}" msgstr "" - diff --git a/examples/locales/ru/LC_MESSAGES/mybot.po b/examples/locales/ru/LC_MESSAGES/mybot.po index 8180af42..9064bc0e 100644 --- a/examples/locales/ru/LC_MESSAGES/mybot.po +++ b/examples/locales/ru/LC_MESSAGES/mybot.po @@ -1,14 +1,14 @@ # Russian translations for PROJECT. -# Copyright (C) 2018 ORGANIZATION +# Copyright (C) 2019 ORGANIZATION # This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2018. +# FIRST AUTHOR , 2019. # msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2018-06-30 03:50+0300\n" -"PO-Revision-Date: 2018-06-30 03:43+0300\n" +"POT-Creation-Date: 2019-08-10 17:51+0300\n" +"PO-Revision-Date: 2019-08-10 17:52+0300\n" "Last-Translator: FULL NAME \n" "Language: ru\n" "Language-Team: ru \n" @@ -17,18 +17,19 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.6.0\n" +"Generated-By: Babel 2.7.0\n" -#: i18n_example.py:48 +#: i18n_example.py:60 msgid "Hello, {user}!" msgstr "Привет, {user}!" -#: i18n_example.py:53 +#: i18n_example.py:67 msgid "Your current language: {language}" msgstr "Твой язык: {language}" +#: i18n_example.py:95 msgid "Aiogram has {number} like!" msgid_plural "Aiogram has {number} likes!" msgstr[0] "Aiogram имеет {number} лайк!" msgstr[1] "Aiogram имеет {number} лайка!" -msgstr[2] "Aiogram имеет {number} лайков!" \ No newline at end of file +msgstr[2] "Aiogram имеет {number} лайков!" From 5846d3a6c2aa8ae9b60b86247d4cf200d8c0a01a Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 10 Aug 2019 18:41:29 +0300 Subject: [PATCH 11/35] More minor refactoring --- aiogram/dispatcher/dispatcher.py | 49 +++++++++++++++++--------- aiogram/dispatcher/filters/__init__.py | 2 +- aiogram/dispatcher/filters/builtin.py | 4 +-- aiogram/dispatcher/filters/factory.py | 2 +- aiogram/dispatcher/filters/filters.py | 2 +- aiogram/dispatcher/handler.py | 7 ++-- 6 files changed, 42 insertions(+), 24 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 8236118e..24e13801 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -85,39 +85,56 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_factory.bind(StateFilter, exclude_event_handlers=[ self.errors_handlers, - self.poll_handlers + self.poll_handlers, ]) filters_factory.bind(ContentTypeFilter, event_handlers=[ - self.message_handlers, self.edited_message_handlers, - self.channel_post_handlers, self.edited_channel_post_handlers, + self.message_handlers, + self.edited_message_handlers, + self.channel_post_handlers, + self.edited_channel_post_handlers, ]), filters_factory.bind(Command, event_handlers=[ - self.message_handlers, self.edited_message_handlers + self.message_handlers, + self.edited_message_handlers ]) filters_factory.bind(Text, event_handlers=[ - self.message_handlers, self.edited_message_handlers, - self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers, + self.message_handlers, + self.edited_message_handlers, + self.channel_post_handlers, + self.edited_channel_post_handlers, + self.callback_query_handlers, + self.poll_handlers, + self.inline_query_handlers, ]) filters_factory.bind(HashTag, event_handlers=[ - self.message_handlers, self.edited_message_handlers, - self.channel_post_handlers, self.edited_channel_post_handlers, + self.message_handlers, + self.edited_message_handlers, + self.channel_post_handlers, + self.edited_channel_post_handlers, ]) filters_factory.bind(Regexp, event_handlers=[ - self.message_handlers, self.edited_message_handlers, - self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers, + self.message_handlers, + self.edited_message_handlers, + self.channel_post_handlers, + self.edited_channel_post_handlers, + self.callback_query_handlers, + self.poll_handlers, + self.inline_query_handlers, ]) filters_factory.bind(RegexpCommandsFilter, event_handlers=[ - self.message_handlers, self.edited_message_handlers, + self.message_handlers, + self.edited_message_handlers, ]) filters_factory.bind(ExceptionsFilter, event_handlers=[ self.errors_handlers, ]) filters_factory.bind(IdFilter, event_handlers=[ - self.message_handlers, self.edited_message_handlers, - self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers, self.inline_query_handlers + self.message_handlers, + self.edited_message_handlers, + self.channel_post_handlers, + self.edited_channel_post_handlers, + self.callback_query_handlers, + self.inline_query_handlers, ]) def __del__(self): diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index eb4a5a52..374bcf2a 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -27,5 +27,5 @@ __all__ = [ 'get_filter_spec', 'get_filters_spec', 'execute_filter', - 'check_filters' + 'check_filters', ] diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 9a44de23..b0324290 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -558,9 +558,9 @@ class IdFilter(Filter): if self.user_id and self.chat_id: return user_id in self.user_id and chat_id in self.chat_id - elif self.user_id: + if self.user_id: return user_id in self.user_id - elif self.chat_id: + if self.chat_id: return chat_id in self.chat_id return False diff --git a/aiogram/dispatcher/filters/factory.py b/aiogram/dispatcher/filters/factory.py index 89e3e792..13b188ff 100644 --- a/aiogram/dispatcher/filters/factory.py +++ b/aiogram/dispatcher/filters/factory.py @@ -70,4 +70,4 @@ class FiltersFactory: yield filter_ if full_config: - raise NameError('Invalid filter name(s): \'' + '\', '.join(full_config.keys()) + '\'') + raise NameError("Invalid filter name(s): '" + "', ".join(full_config.keys()) + "'") diff --git a/aiogram/dispatcher/filters/filters.py b/aiogram/dispatcher/filters/filters.py index 4806c55a..220ef96c 100644 --- a/aiogram/dispatcher/filters/filters.py +++ b/aiogram/dispatcher/filters/filters.py @@ -82,7 +82,7 @@ class FilterRecord: Filters record for factory """ - def __init__(self, callback: typing.Callable, + def __init__(self, callback: typing.Union[typing.Callable, 'AbstractFilter'], validator: typing.Optional[typing.Callable] = None, event_handlers: typing.Optional[typing.Iterable[Handler]] = None, exclude_event_handlers: typing.Optional[typing.Iterable[Handler]] = None): diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 889dc8d6..cd5e9b50 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -1,7 +1,7 @@ import inspect from contextvars import ContextVar from dataclasses import dataclass -from typing import Optional, Iterable +from typing import Optional, Iterable, List ctx_data = ContextVar('ctx_handler_data') current_handler = ContextVar('current_handler') @@ -41,11 +41,10 @@ class Handler: self.dispatcher = dispatcher self.once = once - self.handlers = [] + self.handlers: List[Handler.HandlerObj] = [] self.middleware_key = middleware_key def register(self, handler, filters=None, index=None): - from .filters import get_filters_spec """ Register callback @@ -55,6 +54,8 @@ class Handler: :param filters: list of filters :param index: you can reorder handlers """ + from .filters import get_filters_spec + spec = _get_spec(handler) if filters and not isinstance(filters, (list, tuple, set)): From a4f8dc907a9fc9e626627e142051551d0829dd1b Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 10 Aug 2019 18:41:50 +0300 Subject: [PATCH 12/35] Refactor examples/id_filter_example.py --- examples/id_filter_example.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/examples/id_filter_example.py b/examples/id_filter_example.py index 64dc3b3f..343253e3 100644 --- a/examples/id_filter_example.py +++ b/examples/id_filter_example.py @@ -1,37 +1,35 @@ from aiogram import Bot, Dispatcher, executor, types from aiogram.dispatcher.handler import SkipHandler -API_TOKEN = 'API_TOKE_HERE' + +API_TOKEN = 'BOT_TOKEN_HERE' bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) -user_id_to_test = None # todo: Set id here -chat_id_to_test = user_id_to_test +user_id_required = None # TODO: Set id here +chat_id_required = user_id_required # Change for use in groups (user_id == chat_id in pm) -@dp.message_handler(user_id=user_id_to_test) +@dp.message_handler(user_id=user_id_required) async def handler1(msg: types.Message): - await bot.send_message(msg.chat.id, - "Hello, checking with user_id=") - raise SkipHandler + await bot.send_message(msg.chat.id, "Hello, checking with user_id=") + raise SkipHandler # just for demo -@dp.message_handler(chat_id=chat_id_to_test) +@dp.message_handler(chat_id=chat_id_required) async def handler2(msg: types.Message): - await bot.send_message(msg.chat.id, - "Hello, checking with chat_id=") - raise SkipHandler + await bot.send_message(msg.chat.id, "Hello, checking with chat_id=") + raise SkipHandler # just for demo -@dp.message_handler(user_id=user_id_to_test, chat_id=chat_id_to_test) +@dp.message_handler(user_id=user_id_required, chat_id=chat_id_required) async def handler3(msg: types.Message): - await bot.send_message(msg.chat.id, - "Hello from user= & chat_id=") + await msg.reply("Hello from user= & chat_id=", reply=False) -@dp.message_handler(user_id=[user_id_to_test, 123]) # todo: add second id here +@dp.message_handler(user_id=[user_id_required, 42]) # TODO: You can add any number of ids here async def handler4(msg: types.Message): - print("Checked user_id with list!") + await msg.reply("Checked user_id with list!", reply=False) if __name__ == '__main__': From c6871f8071906dcc21f6a3bf55ad2f3b894e2b59 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 10 Aug 2019 20:07:15 +0300 Subject: [PATCH 13/35] Refactor examples/inline_bot.py --- examples/inline_bot.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/examples/inline_bot.py b/examples/inline_bot.py index f1a81bb4..28f83e43 100644 --- a/examples/inline_bot.py +++ b/examples/inline_bot.py @@ -1,9 +1,11 @@ -import asyncio +import hashlib import logging -from aiogram import Bot, types, Dispatcher, executor +from aiogram import Bot, Dispatcher, executor +from aiogram.types import InlineQuery, \ + InputTextMessageContent, InlineQueryResultArticle -API_TOKEN = 'BOT TOKEN HERE' +API_TOKEN = 'BOT_TOKEN_HERE' logging.basicConfig(level=logging.DEBUG) @@ -12,10 +14,22 @@ dp = Dispatcher(bot) @dp.inline_handler() -async def inline_echo(inline_query: types.InlineQuery): - input_content = types.InputTextMessageContent(inline_query.query or 'echo') - item = types.InlineQueryResultArticle(id='1', title='echo', - input_message_content=input_content) +async def inline_echo(inline_query: InlineQuery): + # id affects both preview and content, + # so it has to be unique for each result + # (Unique identifier for this result, 1-64 Bytes) + # you can set your unique id's + # but for example i'll generate it based on text because I know, that + # only text will be passed in this example + text = inline_query.query or 'echo' + input_content = InputTextMessageContent(text) + result_id: str = hashlib.md5(text.encode()).hexdigest() + item = InlineQueryResultArticle( + id=result_id, + title=f'Result {text!r}', + input_message_content=input_content, + ) + # don't forget to set cache_time=1 for testing (default is 300s or 5m) await bot.answer_inline_query(inline_query.id, results=[item], cache_time=1) From 7a155794e2322f4a24a8c6a2745fa3dbbd6b93e2 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 10 Aug 2019 21:55:52 +0300 Subject: [PATCH 14/35] Refactor examples/inline_keyboard_example.py --- examples/i18n_example.py | 2 +- examples/inline_keyboard_example.py | 44 ++++++++++++++++------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/examples/i18n_example.py b/examples/i18n_example.py index f69482af..3bb624bd 100644 --- a/examples/i18n_example.py +++ b/examples/i18n_example.py @@ -37,7 +37,7 @@ from pathlib import Path from aiogram import Bot, Dispatcher, executor, types from aiogram.contrib.middlewares.i18n import I18nMiddleware -TOKEN = 'BOT TOKEN HERE' +TOKEN = 'BOT_TOKEN_HERE' I18N_DOMAIN = 'mybot' BASE_DIR = Path(__file__).parent diff --git a/examples/inline_keyboard_example.py b/examples/inline_keyboard_example.py index 2478b9e0..8b950f98 100644 --- a/examples/inline_keyboard_example.py +++ b/examples/inline_keyboard_example.py @@ -6,50 +6,56 @@ import logging from aiogram import Bot, Dispatcher, executor, types + API_TOKEN = 'BOT_TOKEN_HERE' # Configure logging logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) # Initialize bot and dispatcher bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) -@dp.message_handler(commands=['start']) +@dp.message_handler(commands='start') async def start_cmd_handler(message: types.Message): keyboard_markup = types.InlineKeyboardMarkup(row_width=3) # default row_width is 3, so here we can omit it actually # kept for clearness - keyboard_markup.row(types.InlineKeyboardButton("Yes!", callback_data='yes'), - # in real life for the callback_data the callback data factory should be used - # here the raw string is used for the simplicity - types.InlineKeyboardButton("No!", callback_data='no')) + text_and_data = ( + ('Yes!', 'yes'), + ('No!', 'no'), + ) + # in real life for the callback_data the callback data factory should be used + # here the raw string is used for the simplicity + row_btns = (types.InlineKeyboardButton(text, callback_data=data) for text, data in text_and_data) - keyboard_markup.add(types.InlineKeyboardButton("aiogram link", - url='https://github.com/aiogram/aiogram')) - # url buttons has no callback data + keyboard_markup.row(*row_btns) + keyboard_markup.add( + # url buttons have no callback data + types.InlineKeyboardButton('aiogram source', url='https://github.com/aiogram/aiogram'), + ) await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup) -@dp.callback_query_handler(lambda cb: cb.data in ['yes', 'no']) # if cb.data is either 'yes' or 'no' -# @dp.callback_query_handler(text='yes') # if cb.data == 'yes' +# Use multiple registrators. Handler will execute when one of the filters is OK +@dp.callback_query_handler(text='no') # if cb.data == 'no' +@dp.callback_query_handler(text='yes') # if cb.data == 'yes' async def inline_kb_answer_callback_handler(query: types.CallbackQuery): - await query.answer() # send answer to close the rounding circle - answer_data = query.data - logger.debug(f"answer_data={answer_data}") - # here we can work with query.data + # always answer callback queries, even if you have nothing to say + await query.answer(f'You answered with {answer_data!r}') + if answer_data == 'yes': - await bot.send_message(query.from_user.id, "That's great!") + text = 'Great, me too!' elif answer_data == 'no': - await bot.send_message(query.from_user.id, "Oh no...Why so?") + text = 'Oh no...Why so?' else: - await bot.send_message(query.from_user.id, "Invalid callback data!") + text = f'Unexpected callback data {answer_data!r}!' + + await bot.send_message(query.from_user.id, text) if __name__ == '__main__': From 95fcaaeed798e34b974245667cf5fc14ef05efbe Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 10 Aug 2019 22:09:29 +0300 Subject: [PATCH 15/35] Micro refactor --- aiogram/dispatcher/filters/builtin.py | 10 +++++----- examples/media_group.py | 7 ++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index b0324290..2e561bcd 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -149,7 +149,7 @@ class CommandStart(Command): :param deep_link: string or compiled regular expression (by ``re.compile(...)``). """ - super(CommandStart, self).__init__(['start']) + super().__init__(['start']) self.deep_link = deep_link async def check(self, message: types.Message): @@ -159,7 +159,7 @@ class CommandStart(Command): :param message: :return: """ - check = await super(CommandStart, self).check(message) + check = await super().check(message) if check and self.deep_link is not None: if not isinstance(self.deep_link, re.Pattern): @@ -179,7 +179,7 @@ class CommandHelp(Command): """ def __init__(self): - super(CommandHelp, self).__init__(['help']) + super().__init__(['help']) class CommandSettings(Command): @@ -188,7 +188,7 @@ class CommandSettings(Command): """ def __init__(self): - super(CommandSettings, self).__init__(['settings']) + super().__init__(['settings']) class CommandPrivacy(Command): @@ -197,7 +197,7 @@ class CommandPrivacy(Command): """ def __init__(self): - super(CommandPrivacy, self).__init__(['privacy']) + super().__init__(['privacy']) class Text(Filter): diff --git a/examples/media_group.py b/examples/media_group.py index eafbac6a..3d488364 100644 --- a/examples/media_group.py +++ b/examples/media_group.py @@ -2,7 +2,8 @@ import asyncio from aiogram import Bot, Dispatcher, executor, filters, types -API_TOKEN = 'BOT TOKEN HERE' + +API_TOKEN = 'BOT_TOKEN_HERE' bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) @@ -13,10 +14,10 @@ async def send_welcome(message: types.Message): # So... At first I want to send something like this: await message.reply("Do you want to see many pussies? Are you ready?") - # And wait few seconds... + # Wait a little... await asyncio.sleep(1) - # Good bots should send chat actions. Or not. + # Good bots should send chat actions... await types.ChatActions.upload_photo() # Create media group From 189753cf6747be7303d547e41dc05e3518c43bd3 Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 10 Aug 2019 23:42:49 +0300 Subject: [PATCH 16/35] Add renamed_argument decorator for convenient warnings about deprecated arguments --- aiogram/utils/deprecated.py | 55 +++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 1ea2561d..792a4d17 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -5,6 +5,7 @@ Source: https://stackoverflow.com/questions/2536307/decorators-in-the-python-sta import functools import inspect import warnings +import asyncio def deprecated(reason): @@ -73,3 +74,57 @@ def warn_deprecated(message, warning=DeprecationWarning, stacklevel=2): warnings.simplefilter('always', warning) warnings.warn(message, category=warning, stacklevel=stacklevel) warnings.simplefilter('default', warning) + + +def renamed_argument(old_name: str, new_name: str, until_version: str): + """ + A meta-decorator to mark some arguments in function as deprecated. + + .. code-block:: python3 + + @renamed_argument("user", "user_id", "3.0") + def some_function(user_id): + print(f"user_id={user_id}") + + some_function(user=123) # prints 'user_id=123' with warning + some_function(123) # prints 'user_id=123' without warning + some_function(user_id=123) # prints 'user_id=123' without warning + + + :param old_name: + :param new_name: + :param until_version: the version in which the argument is scheduled to be removed + :return: decorator + """ + + def decorator(func): + if asyncio.iscoroutinefunction(func): + @functools.wraps(func) + async def wrapped(*args, **kwargs): + if old_name in kwargs: + warn_deprecated(f"In coroutine '{func.__name__}' argument '{old_name}' is renamed to '{new_name}' " + f"and will be removed in aiogram {until_version}", stacklevel=3) + kwargs.update( + { + new_name: kwargs[old_name], + } + ) + kwargs.pop(old_name) + await func(*args, **kwargs) + else: + @functools.wraps(func) + def wrapped(*args, **kwargs): + if old_name in kwargs: + warn_deprecated(f"In function '{func.__name__}' argument '{old_name}' is renamed to '{new_name}' " + f"and will be removed in aiogram {until_version}", stacklevel=3) + kwargs.update( + { + new_name: kwargs[old_name], + } + ) + kwargs.pop(old_name) + func(*args, **kwargs) + + return wrapped + + return decorator From 05bfb37e18d4afd3082c0356063e29bc5e505f30 Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 10 Aug 2019 23:44:45 +0300 Subject: [PATCH 17/35] Add docs for deprecated --- docs/source/utils/deprecated.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/source/utils/deprecated.rst b/docs/source/utils/deprecated.rst index 0a7b4089..ce80bb0e 100644 --- a/docs/source/utils/deprecated.rst +++ b/docs/source/utils/deprecated.rst @@ -1,4 +1,8 @@ ========== Deprecated ========== -Coming soon... +.. literalinclude:: ../../../aiogram/utils/deprecated.py + :caption: deprecated.py + :language: python3 + :linenos: + From cc601a7e0d1152ce0de543f45c663df785856937 Mon Sep 17 00:00:00 2001 From: birdi Date: Sun, 11 Aug 2019 00:07:30 +0300 Subject: [PATCH 18/35] add support of stacklevel parameter in renamed_argument decorator --- aiogram/utils/deprecated.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 792a4d17..4b954c56 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -76,24 +76,27 @@ def warn_deprecated(message, warning=DeprecationWarning, stacklevel=2): warnings.simplefilter('default', warning) -def renamed_argument(old_name: str, new_name: str, until_version: str): +def renamed_argument(old_name: str, new_name: str, until_version: str, stacklevel: int = 3): """ A meta-decorator to mark some arguments in function as deprecated. .. code-block:: python3 - @renamed_argument("user", "user_id", "3.0") - def some_function(user_id): - print(f"user_id={user_id}") + @renamed_argument("chat", "chat_id", "3.0") # stacklevel=3 by default + @renamed_argument("user", "user_id", "3.0", stacklevel=4) + def some_function(user_id, chat_id=None): + print(f"user_id={user_id}, chat_id={chat_id}") - some_function(user=123) # prints 'user_id=123' with warning - some_function(123) # prints 'user_id=123' without warning - some_function(user_id=123) # prints 'user_id=123' without warning + some_function(user=123) # prints 'user_id=123, chat_id=None' with warning + some_function(123) # prints 'user_id=123, chat_id=None' without warning + some_function(user_id=123) # prints 'user_id=123, chat_id=None' without warning :param old_name: :param new_name: :param until_version: the version in which the argument is scheduled to be removed + :param stacklevel: leave it to default if it's the first decorator used. + Increment with any new decorator used. :return: decorator """ @@ -102,8 +105,10 @@ def renamed_argument(old_name: str, new_name: str, until_version: str): @functools.wraps(func) async def wrapped(*args, **kwargs): if old_name in kwargs: - warn_deprecated(f"In coroutine '{func.__name__}' argument '{old_name}' is renamed to '{new_name}' " - f"and will be removed in aiogram {until_version}", stacklevel=3) + warn_deprecated(f"In coroutine '{func.__name__}' argument '{old_name}' " + f"is renamed to '{new_name}' " + f"and will be removed in aiogram {until_version}", + stacklevel=stacklevel) kwargs.update( { new_name: kwargs[old_name], @@ -115,8 +120,10 @@ def renamed_argument(old_name: str, new_name: str, until_version: str): @functools.wraps(func) def wrapped(*args, **kwargs): if old_name in kwargs: - warn_deprecated(f"In function '{func.__name__}' argument '{old_name}' is renamed to '{new_name}' " - f"and will be removed in aiogram {until_version}", stacklevel=3) + warn_deprecated(f"In function '{func.__name__}' argument '{old_name}' " + f"is renamed to '{new_name}' " + f"and will be removed in aiogram {until_version}", + stacklevel=stacklevel) kwargs.update( { new_name: kwargs[old_name], From f750ea13f5660889747a1f887e2849878739304b Mon Sep 17 00:00:00 2001 From: birdi Date: Sun, 11 Aug 2019 00:09:44 +0300 Subject: [PATCH 19/35] fix typo --- aiogram/utils/deprecated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 4b954c56..3917c72c 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -78,7 +78,7 @@ def warn_deprecated(message, warning=DeprecationWarning, stacklevel=2): def renamed_argument(old_name: str, new_name: str, until_version: str, stacklevel: int = 3): """ - A meta-decorator to mark some arguments in function as deprecated. + A meta-decorator to mark an argument as deprecated. .. code-block:: python3 From e1cd68d4d3a4ab286398df2a2450073f27593920 Mon Sep 17 00:00:00 2001 From: birdi Date: Sun, 11 Aug 2019 01:15:03 +0300 Subject: [PATCH 20/35] fix quotes --- aiogram/utils/deprecated.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 3917c72c..89081c8b 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -120,8 +120,8 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve @functools.wraps(func) def wrapped(*args, **kwargs): if old_name in kwargs: - warn_deprecated(f"In function '{func.__name__}' argument '{old_name}' " - f"is renamed to '{new_name}' " + warn_deprecated(f"In function `{func.__name__}` argument `{old_name}` " + f"is renamed to `{new_name}` " f"and will be removed in aiogram {until_version}", stacklevel=stacklevel) kwargs.update( From 68aa7dbf1a72dce7df87b22c227224efb558c7ad Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 15:30:01 +0300 Subject: [PATCH 21/35] Refactor examples/payments.py --- examples/middleware_and_antiflood.py | 2 +- examples/payments.py | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/examples/middleware_and_antiflood.py b/examples/middleware_and_antiflood.py index 4a0cc491..72fabf55 100644 --- a/examples/middleware_and_antiflood.py +++ b/examples/middleware_and_antiflood.py @@ -7,7 +7,7 @@ from aiogram.dispatcher.handler import CancelHandler, current_handler from aiogram.dispatcher.middlewares import BaseMiddleware from aiogram.utils.exceptions import Throttled -TOKEN = 'BOT TOKEN HERE' +TOKEN = 'BOT_TOKEN_HERE' # In this example Redis storage is used storage = RedisStorage2(db=5) diff --git a/examples/payments.py b/examples/payments.py index a01fbaf3..42162578 100644 --- a/examples/payments.py +++ b/examples/payments.py @@ -1,13 +1,12 @@ -import asyncio - from aiogram import Bot from aiogram import types from aiogram.dispatcher import Dispatcher from aiogram.types.message import ContentTypes from aiogram.utils import executor -BOT_TOKEN = 'BOT TOKEN HERE' -PAYMENTS_PROVIDER_TOKEN = '123456789:TEST:1234567890abcdef1234567890abcdef' + +BOT_TOKEN = 'BOT_TOKEN_HERE' +PAYMENTS_PROVIDER_TOKEN = '123456789:TEST:1422' bot = Bot(BOT_TOKEN) dp = Dispatcher(bot) @@ -15,13 +14,13 @@ dp = Dispatcher(bot) # Setup prices prices = [ types.LabeledPrice(label='Working Time Machine', amount=5750), - types.LabeledPrice(label='Gift wrapping', amount=500) + types.LabeledPrice(label='Gift wrapping', amount=500), ] # Setup shipping options shipping_options = [ types.ShippingOption(id='instant', title='WorldWide Teleporter').add(types.LabeledPrice('Teleporter', 1000)), - types.ShippingOption(id='pickup', title='Local pickup').add(types.LabeledPrice('Pickup', 300)) + types.ShippingOption(id='pickup', title='Local pickup').add(types.LabeledPrice('Pickup', 300)), ] @@ -59,7 +58,7 @@ async def cmd_buy(message: types.Message): ' Order our Working Time Machine today!', provider_token=PAYMENTS_PROVIDER_TOKEN, currency='usd', - photo_url='https://images.fineartamerica.com/images-medium-large/2-the-time-machine-dmitriy-khristenko.jpg', + photo_url='https://telegra.ph/file/d08ff863531f10bf2ea4b.jpg', photo_height=512, # !=0/None or picture won't be shown photo_width=512, photo_size=512, @@ -69,14 +68,14 @@ async def cmd_buy(message: types.Message): payload='HAPPY FRIDAYS COUPON') -@dp.shipping_query_handler(func=lambda query: True) +@dp.shipping_query_handler(lambda query: True) async def shipping(shipping_query: types.ShippingQuery): await bot.answer_shipping_query(shipping_query.id, ok=True, shipping_options=shipping_options, error_message='Oh, seems like our Dog couriers are having a lunch right now.' ' Try again later!') -@dp.pre_checkout_query_handler(func=lambda query: True) +@dp.pre_checkout_query_handler(lambda query: True) async def checkout(pre_checkout_query: types.PreCheckoutQuery): await bot.answer_pre_checkout_query(pre_checkout_query.id, ok=True, error_message="Aliens tried to steal your card's CVV," @@ -95,4 +94,4 @@ async def got_payment(message: types.Message): if __name__ == '__main__': - executor.start_polling(dp) + executor.start_polling(dp, skip_updates=True) From a93fb463820fc6924bc22ffeb1c1d2a579a0f3f0 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 15:49:27 +0300 Subject: [PATCH 22/35] Refactor examples/proxy_and_emojize.py --- examples/proxy_and_emojize.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/examples/proxy_and_emojize.py b/examples/proxy_and_emojize.py index 17e33872..5ef40608 100644 --- a/examples/proxy_and_emojize.py +++ b/examples/proxy_and_emojize.py @@ -1,4 +1,3 @@ -import asyncio import logging import aiohttp @@ -11,13 +10,13 @@ from aiogram.utils.executor import start_polling from aiogram.utils.markdown import bold, code, italic, text # Configure bot here -API_TOKEN = 'BOT TOKEN HERE' -PROXY_URL = 'http://PROXY_URL' # Or 'socks5://...' +API_TOKEN = 'BOT_TOKEN_HERE' +PROXY_URL = 'http://PROXY_URL' # Or 'socks5://host:port' -# If authentication is required in your proxy then uncomment next line and change login/password for it +# NOTE: If authentication is required in your proxy then uncomment next line and change login/password for it # PROXY_AUTH = aiohttp.BasicAuth(login='login', password='password') -# And add `proxy_auth=PROXY_AUTH` argument in line 25, like this: -# >>> bot = Bot(token=API_TOKEN, loop=loop, proxy=PROXY_URL, proxy_auth=PROXY_AUTH) +# And add `proxy_auth=PROXY_AUTH` argument in line 30, like this: +# >>> bot = Bot(token=API_TOKEN, proxy=PROXY_URL, proxy_auth=PROXY_AUTH) # Also you can use Socks5 proxy but you need manually install aiohttp_socks package. # Get my ip URL @@ -26,26 +25,32 @@ GET_IP_URL = 'http://bot.whatismyipaddress.com/' logging.basicConfig(level=logging.INFO) bot = Bot(token=API_TOKEN, proxy=PROXY_URL) + +# If auth is required: +# bot = Bot(token=API_TOKEN, proxy=PROXY_URL, proxy_auth=PROXY_AUTH) dp = Dispatcher(bot) -async def fetch(url, proxy=None, proxy_auth=None): - async with aiohttp.ClientSession() as session: - async with session.get(url, proxy=proxy, proxy_auth=proxy_auth) as response: - return await response.text() +async def fetch(url, session): + async with session.get(url) as response: + return await response.text() @dp.message_handler(commands=['start']) async def cmd_start(message: types.Message): + # fetching urls will take some time, so notify user that everything is OK + await types.ChatActions.typing() + content = [] # Make request (without proxy) - ip = await fetch(GET_IP_URL) + async with aiohttp.ClientSession() as session: + ip = await fetch(GET_IP_URL, session) content.append(text(':globe_showing_Americas:', bold('IP:'), code(ip))) # This line is formatted to '🌎 *IP:* `YOUR IP`' - # Make request through proxy - ip = await fetch(GET_IP_URL, bot.proxy, bot.proxy_auth) + # Make request through bot's proxy + ip = await fetch(GET_IP_URL, bot.session) content.append(text(':locked_with_key:', bold('IP:'), code(ip), italic('via proxy'))) # This line is formatted to '🔐 *IP:* `YOUR IP` _via proxy_' From 747c87631ed28dbf6da49ff48b86cdcd3bde0457 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 16:26:29 +0300 Subject: [PATCH 23/35] Refactor examples/regexp_commands_filter_example.py --- examples/regexp_commands_filter_example.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/examples/regexp_commands_filter_example.py b/examples/regexp_commands_filter_example.py index 86ccba55..05de9dd8 100644 --- a/examples/regexp_commands_filter_example.py +++ b/examples/regexp_commands_filter_example.py @@ -2,14 +2,28 @@ from aiogram import Bot, types from aiogram.dispatcher import Dispatcher, filters from aiogram.utils import executor -bot = Bot(token='TOKEN') + +bot = Bot(token='BOT_TOKEN_HERE', parse_mode=types.ParseMode.HTML) dp = Dispatcher(bot) @dp.message_handler(filters.RegexpCommandsFilter(regexp_commands=['item_([0-9]*)'])) async def send_welcome(message: types.Message, regexp_command): - await message.reply("You have requested an item with number: {}".format(regexp_command.group(1))) + await message.reply(f"You have requested an item with id {regexp_command.group(1)}") + + +@dp.message_handler(commands='start') +async def create_deeplink(message: types.Message): + bot_user = await bot.me + bot_username = bot_user.username + deeplink = f'https://t.me/{bot_username}?start=item_12345' + text = ( + f'Either send a command /item_1234 or follow this link {deeplink} and then click start\n' + 'It also can be hidden in a inline button\n\n' + 'Or just send /start item_123' + ) + await message.reply(text, disable_web_page_preview=True) if __name__ == '__main__': - executor.start_polling(dp) + executor.start_polling(dp, skip_updates=True) From 8538e6af573429dc374271d6e746c370bd206e7b Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 21:52:58 +0300 Subject: [PATCH 24/35] Refactor examples/regular_keyboard_example.py --- examples/regular_keyboard_example.py | 38 ++++++++++++++++------------ 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/examples/regular_keyboard_example.py b/examples/regular_keyboard_example.py index 350e007e..d111053c 100644 --- a/examples/regular_keyboard_example.py +++ b/examples/regular_keyboard_example.py @@ -6,6 +6,7 @@ import logging from aiogram import Bot, Dispatcher, executor, types + API_TOKEN = 'BOT_TOKEN_HERE' # Configure logging @@ -18,24 +19,27 @@ bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) -@dp.message_handler(commands=['start']) +@dp.message_handler(commands='start') async def start_cmd_handler(message: types.Message): keyboard_markup = types.ReplyKeyboardMarkup(row_width=3) # default row_width is 3, so here we can omit it actually # kept for clearness - keyboard_markup.row(types.KeyboardButton("Yes!"), - types.KeyboardButton("No!")) + btns_text = ('Yes!', 'No!') + keyboard_markup.row(*(types.KeyboardButton(text) for text in btns_text)) # adds buttons as a new row to the existing keyboard # the behaviour doesn't depend on row_width attribute - keyboard_markup.add(types.KeyboardButton("I don't know"), - types.KeyboardButton("Who am i?"), - types.KeyboardButton("Where am i?"), - types.KeyboardButton("Who is there?")) - # adds buttons. New rows is formed according to row_width parameter + more_btns_text = ( + "I don't know", + "Who am i?", + "Where am i?", + "Who is there?", + ) + keyboard_markup.add(*(types.KeyboardButton(text) for text in more_btns_text)) + # adds buttons. New rows are formed according to row_width parameter - await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup) + await message.reply("Hi!\nDo you like aiogram?", reply_markup=keyboard_markup) @dp.message_handler() @@ -45,15 +49,17 @@ async def all_msg_handler(message: types.Message): # in real bot, it's better to define message_handler(text="...") for each button # but here for the simplicity only one handler is defined - text_of_button = message.text - logger.debug(text_of_button) # print the text we got + button_text = message.text + logger.debug('The answer is %r', button_text) # print the text we've got - if text_of_button == 'Yes!': - await message.reply("That's great", reply_markup=types.ReplyKeyboardRemove()) - elif text_of_button == 'No!': - await message.reply("Oh no! Why?", reply_markup=types.ReplyKeyboardRemove()) + if button_text == 'Yes!': + reply_text = "That's great" + elif button_text == 'No!': + reply_text = "Oh no! Why?" else: - await message.reply("Keep calm...Everything is fine", reply_markup=types.ReplyKeyboardRemove()) + reply_text = "Keep calm...Everything is fine" + + await message.reply(reply_text, reply_markup=types.ReplyKeyboardRemove()) # with message, we send types.ReplyKeyboardRemove() to hide the keyboard From 94811c57e101e2589d1dd6e26def555ab16dabe0 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 21:59:56 +0300 Subject: [PATCH 25/35] Minor refactor examples/text_filter_example.py and examples/throtling_example.py --- examples/text_filter_example.py | 7 +++++-- examples/throtling_example.py | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/examples/text_filter_example.py b/examples/text_filter_example.py index a47a5c92..60d631e3 100644 --- a/examples/text_filter_example.py +++ b/examples/text_filter_example.py @@ -7,7 +7,8 @@ import logging from aiogram import Bot, Dispatcher, executor, types -API_TOKEN = 'API_TOKEN_HERE' + +API_TOKEN = 'BOT_TOKEN_HERE' # Configure logging logging.basicConfig(level=logging.INFO) @@ -16,10 +17,12 @@ logging.basicConfig(level=logging.INFO) bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) + # if the text from user in the list @dp.message_handler(text=['text1', 'text2']) async def text_in_handler(message: types.Message): - await message.answer("The message text is in the list!") + await message.answer("The message text equals to one of in the list!") + # if the text contains any string @dp.message_handler(text_contains='example1') diff --git a/examples/throtling_example.py b/examples/throtling_example.py index 2641b44b..18563472 100644 --- a/examples/throtling_example.py +++ b/examples/throtling_example.py @@ -4,7 +4,6 @@ Example for throttling manager. You can use that for flood controlling. """ -import asyncio import logging from aiogram import Bot, types @@ -13,14 +12,15 @@ from aiogram.dispatcher import Dispatcher from aiogram.utils.exceptions import Throttled from aiogram.utils.executor import start_polling -API_TOKEN = 'BOT TOKEN HERE' + +API_TOKEN = 'BOT_TOKEN_HERE' logging.basicConfig(level=logging.INFO) bot = Bot(token=API_TOKEN) # Throttling manager does not work without Leaky Bucket. -# Then need to use storages. For example use simple in-memory storage. +# You need to use a storage. For example use simple in-memory storage. storage = MemoryStorage() dp = Dispatcher(bot, storage=storage) From fdf3c448608950433d7cb1e9e73ebdeb5c4d4e48 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 22:50:58 +0300 Subject: [PATCH 26/35] Rename old webhook example --- examples/{webhook_example.py => webhook_example_old.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{webhook_example.py => webhook_example_old.py} (100%) diff --git a/examples/webhook_example.py b/examples/webhook_example_old.py similarity index 100% rename from examples/webhook_example.py rename to examples/webhook_example_old.py From 251760a0325647978207feec4065bdc7f6dfdab8 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 22:51:50 +0300 Subject: [PATCH 27/35] Rename webhook example --- examples/{webhook_example_2.py => webhook_example.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{webhook_example_2.py => webhook_example.py} (100%) diff --git a/examples/webhook_example_2.py b/examples/webhook_example.py similarity index 100% rename from examples/webhook_example_2.py rename to examples/webhook_example.py From 58a5da925ab5f89010ff5ac9f43cf8356c5db1b1 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 22:52:54 +0300 Subject: [PATCH 28/35] Refactor examples/webhook_example.py --- examples/webhook_example.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/examples/webhook_example.py b/examples/webhook_example.py index e2d9225c..6efa8767 100644 --- a/examples/webhook_example.py +++ b/examples/webhook_example.py @@ -1,11 +1,13 @@ -import asyncio import logging from aiogram import Bot, types +from aiogram.contrib.middlewares.logging import LoggingMiddleware from aiogram.dispatcher import Dispatcher +from aiogram.dispatcher.webhook import SendMessage from aiogram.utils.executor import start_webhook -API_TOKEN = 'BOT TOKEN HERE' + +API_TOKEN = 'BOT_TOKEN_HERE' # webhook settings WEBHOOK_HOST = 'https://your.domain' @@ -20,11 +22,16 @@ logging.basicConfig(level=logging.INFO) bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) +dp.middleware.setup(LoggingMiddleware()) @dp.message_handler() async def echo(message: types.Message): - await bot.send_message(message.chat.id, message.text) + # Regular request + # await bot.send_message(message.chat.id, message.text) + + # or reply INTO webhook + return SendMessage(message.chat.id, message.text) async def on_startup(dp): @@ -33,10 +40,27 @@ async def on_startup(dp): async def on_shutdown(dp): + logging.warning('Shutting down..') + # insert code here to run it before shutdown - pass + + # Remove webhook (not acceptable in some cases) + await bot.delete_webhook() + + # Close DB connection (if used) + await dp.storage.close() + await dp.storage.wait_closed() + + logging.warning('Bye!') if __name__ == '__main__': - start_webhook(dispatcher=dp, webhook_path=WEBHOOK_PATH, on_startup=on_startup, on_shutdown=on_shutdown, - skip_updates=True, host=WEBAPP_HOST, port=WEBAPP_PORT) + start_webhook( + dispatcher=dp, + webhook_path=WEBHOOK_PATH, + on_startup=on_startup, + on_shutdown=on_shutdown, + skip_updates=True, + host=WEBAPP_HOST, + port=WEBAPP_PORT, + ) From f4339d10b043a691aad662b80e9e7752af76213a Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 22:55:44 +0300 Subject: [PATCH 29/35] Super minor refactor webhook and executor --- aiogram/dispatcher/webhook.py | 32 ++++++++++++++++---------------- aiogram/utils/executor.py | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index bee635ae..135fe21e 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -523,7 +523,7 @@ class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificatio 'disable_web_page_preview': self.disable_web_page_preview, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } def write(self, *text, sep=' '): @@ -642,7 +642,7 @@ class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'caption': self.caption, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -704,7 +704,7 @@ class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'title': self.title, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -817,7 +817,7 @@ class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'caption': self.caption, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -871,7 +871,7 @@ class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'duration': self.duration, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -924,7 +924,7 @@ class SendVideoNote(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'length': self.length, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1050,7 +1050,7 @@ class SendLocation(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'longitude': self.longitude, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1109,7 +1109,7 @@ class SendVenue(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'foursquare_id': self.foursquare_id, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1160,7 +1160,7 @@ class SendContact(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'last_name': self.last_name, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1220,7 +1220,7 @@ class KickChatMember(BaseResponse): return { 'chat_id': self.chat_id, 'user_id': self.user_id, - 'until_date': prepare_arg(self.until_date) + 'until_date': prepare_arg(self.until_date), } @@ -1608,7 +1608,7 @@ class EditMessageText(BaseResponse, ParseModeMixin, DisableWebPagePreviewMixin): 'text': self.text, 'parse_mode': self.parse_mode, 'disable_web_page_preview': self.disable_web_page_preview, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1649,7 +1649,7 @@ class EditMessageCaption(BaseResponse): 'message_id': self.message_id, 'inline_message_id': self.inline_message_id, 'caption': self.caption, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1685,7 +1685,7 @@ class EditMessageReplyMarkup(BaseResponse): 'chat_id': self.chat_id, 'message_id': self.message_id, 'inline_message_id': self.inline_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1756,7 +1756,7 @@ class SendSticker(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'sticker': self.sticker, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1848,7 +1848,7 @@ class AddStickerToSet(BaseResponse): 'name': self.name, 'png_sticker': self.png_sticker, 'emojis': self.emojis, - 'mask_position': prepare_arg(self.mask_position) + 'mask_position': prepare_arg(self.mask_position), } @@ -2177,5 +2177,5 @@ class SendGame(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'game_short_name': self.game_short_name, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index cdb3fe91..854facae 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -15,7 +15,7 @@ from ..dispatcher.webhook import BOT_DISPATCHER_KEY, DEFAULT_ROUTE_NAME, Webhook APP_EXECUTOR_KEY = 'APP_EXECUTOR' -def _setup_callbacks(executor, on_startup=None, on_shutdown=None): +def _setup_callbacks(executor: 'Executor', on_startup=None, on_shutdown=None): if on_startup is not None: executor.on_startup(on_startup) if on_shutdown is not None: From 277eb8b701504917853d644a7f986f37ec885507 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 23:32:41 +0300 Subject: [PATCH 30/35] Refactor TextFilter and its tests --- aiogram/dispatcher/filters/builtin.py | 33 ++- tests/test_filters.py | 347 +++++++++----------------- 2 files changed, 133 insertions(+), 247 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 2e561bcd..5b50c23c 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -84,9 +84,9 @@ class Command(Filter): if not ignore_mention and mention and (await message.bot.me).username.lower() != mention.lower(): return False - elif prefix not in prefixes: + if prefix not in prefixes: return False - elif (command.lower() if ignore_case else command) not in commands: + if (command.lower() if ignore_case else command) not in commands: return False return {'command': Command.CommandObj(command=command, prefix=prefix, mention=mention)} @@ -271,19 +271,26 @@ class Text(Filter): if self.ignore_case: text = text.lower() + _pre_process_func = lambda s: str(s).lower() + else: + _pre_process_func = str + # now check if self.equals is not None: - self.equals = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.equals)) - return text in self.equals - elif self.contains is not None: - self.contains = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.contains)) - return all(map(text.__contains__, self.contains)) - elif self.startswith is not None: - self.startswith = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.startswith)) - return any(map(text.startswith, self.startswith)) - elif self.endswith is not None: - self.endswith = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.endswith)) - return any(map(text.endswith, self.endswith)) + equals = list(map(_pre_process_func, self.equals)) + return text in equals + + if self.contains is not None: + contains = list(map(_pre_process_func, self.contains)) + return all(map(text.__contains__, contains)) + + if self.startswith is not None: + startswith = list(map(_pre_process_func, self.startswith)) + return any(map(text.startswith, startswith)) + + if self.endswith is not None: + endswith = list(map(_pre_process_func, self.endswith)) + return any(map(text.endswith, endswith)) return False diff --git a/tests/test_filters.py b/tests/test_filters.py index 288aa0a4..609db736 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -4,38 +4,35 @@ from aiogram.dispatcher.filters import Text from aiogram.types import Message, CallbackQuery, InlineQuery, Poll +def data_sample_1(): + return [ + ('', ''), + ('', 'exAmple_string'), + + ('example_string', 'example_string'), + ('example_string', 'exAmple_string'), + ('exAmple_string', 'example_string'), + + ('example_string', 'example_string_dsf'), + ('example_string', 'example_striNG_dsf'), + ('example_striNG', 'example_string_dsf'), + + ('example_string', 'not_example_string'), + ('example_string', 'not_eXample_string'), + ('EXample_string', 'not_example_string'), + ] + class TestTextFilter: + + async def _run_check(self, check, test_text): + assert await check(Message(text=test_text)) + assert await check(CallbackQuery(data=test_text)) + assert await check(InlineQuery(query=test_text)) + assert await check(Poll(question=test_text)) + @pytest.mark.asyncio - @pytest.mark.parametrize("test_prefix, test_text, ignore_case", - [('', '', True), - ('', 'exAmple_string', True), - ('', '', False), - ('', 'exAmple_string', False), - - ('example_string', 'example_string', True), - ('example_string', 'exAmple_string', True), - ('exAmple_string', 'example_string', True), - - ('example_string', 'example_string', False), - ('example_string', 'exAmple_string', False), - ('exAmple_string', 'example_string', False), - - ('example_string', 'example_string_dsf', True), - ('example_string', 'example_striNG_dsf', True), - ('example_striNG', 'example_string_dsf', True), - - ('example_string', 'example_string_dsf', False), - ('example_string', 'example_striNG_dsf', False), - ('example_striNG', 'example_string_dsf', False), - - ('example_string', 'not_example_string', True), - ('example_string', 'not_eXample_string', True), - ('EXample_string', 'not_example_string', True), - - ('example_string', 'not_example_string', False), - ('example_string', 'not_eXample_string', False), - ('EXample_string', 'not_example_string', False), - ]) + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_prefix, test_text", data_sample_1()) async def test_startswith(self, test_prefix, test_text, ignore_case): test_filter = Text(startswith=test_prefix, ignore_case=ignore_case) @@ -50,42 +47,26 @@ class TestTextFilter: return result is _test_text.startswith(_test_prefix) - assert await check(Message(text=test_text)) - assert await check(CallbackQuery(data=test_text)) - assert await check(InlineQuery(query=test_text)) - assert await check(Poll(question=test_text)) + await self._run_check(check, test_text) @pytest.mark.asyncio - @pytest.mark.parametrize("test_prefix_list, test_text, ignore_case", - [(['not_example', ''], '', True), - (['', 'not_example'], 'exAmple_string', True), - (['not_example', ''], '', False), - (['', 'not_example'], 'exAmple_string', False), + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_prefix_list, test_text", [ + (['not_example', ''], ''), + (['', 'not_example'], 'exAmple_string'), - (['example_string', 'not_example'], 'example_string', True), - (['not_example', 'example_string'], 'exAmple_string', True), - (['exAmple_string', 'not_example'], 'example_string', True), + (['not_example', 'example_string'], 'example_string'), + (['example_string', 'not_example'], 'exAmple_string'), + (['not_example', 'exAmple_string'], 'example_string'), - (['not_example', 'example_string'], 'example_string', False), - (['example_string', 'not_example'], 'exAmple_string', False), - (['not_example', 'exAmple_string'], 'example_string', False), + (['not_example', 'example_string'], 'example_string_dsf'), + (['example_string', 'not_example'], 'example_striNG_dsf'), + (['not_example', 'example_striNG'], 'example_string_dsf'), - (['example_string', 'not_example'], 'example_string_dsf', True), - (['not_example', 'example_string'], 'example_striNG_dsf', True), - (['example_striNG', 'not_example'], 'example_string_dsf', True), - - (['not_example', 'example_string'], 'example_string_dsf', False), - (['example_string', 'not_example'], 'example_striNG_dsf', False), - (['not_example', 'example_striNG'], 'example_string_dsf', False), - - (['example_string', 'not_example'], 'not_example_string', True), - (['not_example', 'example_string'], 'not_eXample_string', True), - (['EXample_string', 'not_example'], 'not_example_string', True), - - (['not_example', 'example_string'], 'not_example_string', False), - (['example_string', 'not_example'], 'not_eXample_string', False), - (['not_example', 'EXample_string'], 'not_example_string', False), - ]) + (['not_example', 'example_string'], 'not_example_string'), + (['example_string', 'not_example'], 'not_eXample_string'), + (['not_example', 'EXample_string'], 'not_example_string'), + ]) async def test_startswith_list(self, test_prefix_list, test_text, ignore_case): test_filter = Text(startswith=test_prefix_list, ignore_case=ignore_case) @@ -100,42 +81,11 @@ class TestTextFilter: return result is any(map(_test_text.startswith, _test_prefix_list)) - assert await check(Message(text=test_text)) - assert await check(CallbackQuery(data=test_text)) - assert await check(InlineQuery(query=test_text)) - assert await check(Poll(question=test_text)) + await self._run_check(check, test_text) @pytest.mark.asyncio - @pytest.mark.parametrize("test_postfix, test_text, ignore_case", - [('', '', True), - ('', 'exAmple_string', True), - ('', '', False), - ('', 'exAmple_string', False), - - ('example_string', 'example_string', True), - ('example_string', 'exAmple_string', True), - ('exAmple_string', 'example_string', True), - - ('example_string', 'example_string', False), - ('example_string', 'exAmple_string', False), - ('exAmple_string', 'example_string', False), - - ('example_string', 'example_string_dsf', True), - ('example_string', 'example_striNG_dsf', True), - ('example_striNG', 'example_string_dsf', True), - - ('example_string', 'example_string_dsf', False), - ('example_string', 'example_striNG_dsf', False), - ('example_striNG', 'example_string_dsf', False), - - ('example_string', 'not_example_string', True), - ('example_string', 'not_eXample_string', True), - ('EXample_string', 'not_eXample_string', True), - - ('example_string', 'not_example_string', False), - ('example_string', 'not_eXample_string', False), - ('EXample_string', 'not_example_string', False), - ]) + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_postfix, test_text", data_sample_1()) async def test_endswith(self, test_postfix, test_text, ignore_case): test_filter = Text(endswith=test_postfix, ignore_case=ignore_case) @@ -150,42 +100,26 @@ class TestTextFilter: return result is _test_text.endswith(_test_postfix) - assert await check(Message(text=test_text)) - assert await check(CallbackQuery(data=test_text)) - assert await check(InlineQuery(query=test_text)) - assert await check(Poll(question=test_text)) + await self._run_check(check, test_text) @pytest.mark.asyncio - @pytest.mark.parametrize("test_postfix_list, test_text, ignore_case", - [(['', 'not_example'], '', True), - (['not_example', ''], 'exAmple_string', True), - (['', 'not_example'], '', False), - (['not_example', ''], 'exAmple_string', False), + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_postfix_list, test_text", [ + (['', 'not_example'], ''), + (['not_example', ''], 'exAmple_string'), - (['example_string', 'not_example'], 'example_string', True), - (['not_example', 'example_string'], 'exAmple_string', True), - (['exAmple_string', 'not_example'], 'example_string', True), + (['example_string', 'not_example'], 'example_string'), + (['not_example', 'example_string'], 'exAmple_string'), + (['exAmple_string', 'not_example'], 'example_string'), - (['example_string', 'not_example'], 'example_string', False), - (['not_example', 'example_string'], 'exAmple_string', False), - (['exAmple_string', 'not_example'], 'example_string', False), + (['not_example', 'example_string'], 'example_string_dsf'), + (['example_string', 'not_example'], 'example_striNG_dsf'), + (['not_example', 'example_striNG'], 'example_string_dsf'), - (['example_string', 'not_example'], 'example_string_dsf', True), - (['not_example', 'example_string'], 'example_striNG_dsf', True), - (['example_striNG', 'not_example'], 'example_string_dsf', True), - - (['not_example', 'example_string'], 'example_string_dsf', False), - (['example_string', 'not_example'], 'example_striNG_dsf', False), - (['not_example', 'example_striNG'], 'example_string_dsf', False), - - (['not_example', 'example_string'], 'not_example_string', True), - (['example_string', 'not_example'], 'not_eXample_string', True), - (['not_example', 'EXample_string'], 'not_eXample_string', True), - - (['not_example', 'example_string'], 'not_example_string', False), - (['example_string', 'not_example'], 'not_eXample_string', False), - (['not_example', 'EXample_string'], 'not_example_string', False), - ]) + (['not_example', 'example_string'], 'not_example_string'), + (['example_string', 'not_example'], 'not_eXample_string'), + (['not_example', 'EXample_string'], 'not_example_string'), + ]) async def test_endswith_list(self, test_postfix_list, test_text, ignore_case): test_filter = Text(endswith=test_postfix_list, ignore_case=ignore_case) @@ -199,42 +133,26 @@ class TestTextFilter: _test_text = test_text return result is any(map(_test_text.endswith, _test_postfix_list)) - assert await check(Message(text=test_text)) - assert await check(CallbackQuery(data=test_text)) - assert await check(InlineQuery(query=test_text)) - assert await check(Poll(question=test_text)) + await self._run_check(check, test_text) @pytest.mark.asyncio - @pytest.mark.parametrize("test_string, test_text, ignore_case", - [('', '', True), - ('', 'exAmple_string', True), - ('', '', False), - ('', 'exAmple_string', False), + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_string, test_text", [ + ('', ''), + ('', 'exAmple_string'), - ('example_string', 'example_string', True), - ('example_string', 'exAmple_string', True), - ('exAmple_string', 'example_string', True), + ('example_string', 'example_string'), + ('example_string', 'exAmple_string'), + ('exAmple_string', 'example_string'), - ('example_string', 'example_string', False), - ('example_string', 'exAmple_string', False), - ('exAmple_string', 'example_string', False), + ('example_string', 'example_string_dsf'), + ('example_string', 'example_striNG_dsf'), + ('example_striNG', 'example_string_dsf'), - ('example_string', 'example_string_dsf', True), - ('example_string', 'example_striNG_dsf', True), - ('example_striNG', 'example_string_dsf', True), - - ('example_string', 'example_string_dsf', False), - ('example_string', 'example_striNG_dsf', False), - ('example_striNG', 'example_string_dsf', False), - - ('example_string', 'not_example_strin', True), - ('example_string', 'not_eXample_strin', True), - ('EXample_string', 'not_eXample_strin', True), - - ('example_string', 'not_example_strin', False), - ('example_string', 'not_eXample_strin', False), - ('EXample_string', 'not_example_strin', False), - ]) + ('example_string', 'not_example_strin'), + ('example_string', 'not_eXample_strin'), + ('EXample_string', 'not_example_strin'), + ]) async def test_contains(self, test_string, test_text, ignore_case): test_filter = Text(contains=test_string, ignore_case=ignore_case) @@ -249,23 +167,16 @@ class TestTextFilter: return result is (_test_string in _test_text) - assert await check(Message(text=test_text)) - assert await check(CallbackQuery(data=test_text)) - assert await check(InlineQuery(query=test_text)) - assert await check(Poll(question=test_text)) + await self._run_check(check, test_text) @pytest.mark.asyncio - @pytest.mark.parametrize("test_filter_list, test_text, ignore_case", - [(['a', 'ab', 'abc'], 'A', True), - (['a', 'ab', 'abc'], 'ab', True), - (['a', 'ab', 'abc'], 'aBc', True), - (['a', 'ab', 'abc'], 'd', True), - - (['a', 'ab', 'abc'], 'A', False), - (['a', 'ab', 'abc'], 'ab', False), - (['a', 'ab', 'abc'], 'aBc', False), - (['a', 'ab', 'abc'], 'd', False), - ]) + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_filter_list, test_text", [ + (['a', 'ab', 'abc'], 'A'), + (['a', 'ab', 'abc'], 'ab'), + (['a', 'ab', 'abc'], 'aBc'), + (['a', 'ab', 'abc'], 'd'), + ]) async def test_contains_list(self, test_filter_list, test_text, ignore_case): test_filter = Text(contains=test_filter_list, ignore_case=ignore_case) @@ -280,34 +191,22 @@ class TestTextFilter: return result is all(map(_test_text.__contains__, _test_filter_list)) - assert await check(Message(text=test_text)) - assert await check(CallbackQuery(data=test_text)) - assert await check(InlineQuery(query=test_text)) - assert await check(Poll(question=test_text)) + await self._run_check(check, test_text) @pytest.mark.asyncio - @pytest.mark.parametrize("test_filter_text, test_text, ignore_case", - [('', '', True), - ('', 'exAmple_string', True), - ('', '', False), - ('', 'exAmple_string', False), + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_filter_text, test_text", [ + ('', ''), + ('', 'exAmple_string'), - ('example_string', 'example_string', True), - ('example_string', 'exAmple_string', True), - ('exAmple_string', 'example_string', True), + ('example_string', 'example_string'), + ('example_string', 'exAmple_string'), + ('exAmple_string', 'example_string'), - ('example_string', 'example_string', False), - ('example_string', 'exAmple_string', False), - ('exAmple_string', 'example_string', False), - - ('example_string', 'not_example_string', True), - ('example_string', 'not_eXample_string', True), - ('EXample_string', 'not_eXample_string', True), - - ('example_string', 'not_example_string', False), - ('example_string', 'not_eXample_string', False), - ('EXample_string', 'not_example_string', False), - ]) + ('example_string', 'not_example_string'), + ('example_string', 'not_eXample_string'), + ('EXample_string', 'not_example_string'), + ]) async def test_equals_string(self, test_filter_text, test_text, ignore_case): test_filter = Text(equals=test_filter_text, ignore_case=ignore_case) @@ -321,50 +220,30 @@ class TestTextFilter: _test_text = test_text return result is (_test_text == _test_filter_text) - assert await check(Message(text=test_text)) - assert await check(CallbackQuery(data=test_text)) - assert await check(InlineQuery(query=test_text)) - assert await check(Poll(question=test_text)) + await self._run_check(check, test_text) @pytest.mark.asyncio - @pytest.mark.parametrize("test_filter_list, test_text, ignore_case", - [(['', 'new_string'], '', True), - (['new_string', ''], 'exAmple_string', True), - (['new_string', ''], '', False), - (['', 'new_string'], 'exAmple_string', False), + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_filter_list, test_text", [ + (['new_string', ''], ''), + (['', 'new_string'], 'exAmple_string'), - (['example_string'], 'example_string', True), - (['example_string'], 'exAmple_string', True), - (['exAmple_string'], 'example_string', True), + (['example_string'], 'example_string'), + (['example_string'], 'exAmple_string'), + (['exAmple_string'], 'example_string'), - (['example_string'], 'example_string', False), - (['example_string'], 'exAmple_string', False), - (['exAmple_string'], 'example_string', False), + (['example_string'], 'not_example_string'), + (['example_string'], 'not_eXample_string'), + (['EXample_string'], 'not_example_string'), - (['example_string'], 'not_example_string', True), - (['example_string'], 'not_eXample_string', True), - (['EXample_string'], 'not_eXample_string', True), + (['example_string', 'new_string'], 'example_string'), + (['new_string', 'example_string'], 'exAmple_string'), + (['exAmple_string', 'new_string'], 'example_string'), - (['example_string'], 'not_example_string', False), - (['example_string'], 'not_eXample_string', False), - (['EXample_string'], 'not_example_string', False), - - (['example_string', 'new_string'], 'example_string', True), - (['new_string', 'example_string'], 'exAmple_string', True), - (['exAmple_string', 'new_string'], 'example_string', True), - - (['example_string', 'new_string'], 'example_string', False), - (['new_string', 'example_string'], 'exAmple_string', False), - (['exAmple_string', 'new_string'], 'example_string', False), - - (['example_string', 'new_string'], 'not_example_string', True), - (['new_string', 'example_string'], 'not_eXample_string', True), - (['EXample_string', 'new_string'], 'not_eXample_string', True), - - (['example_string', 'new_string'], 'not_example_string', False), - (['new_string', 'example_string'], 'not_eXample_string', False), - (['EXample_string', 'new_string'], 'not_example_string', False), - ]) + (['example_string', 'new_string'], 'not_example_string'), + (['new_string', 'example_string'], 'not_eXample_string'), + (['EXample_string', 'new_string'], 'not_example_string'), + ]) async def test_equals_list(self, test_filter_list, test_text, ignore_case): test_filter = Text(equals=test_filter_list, ignore_case=ignore_case) From be622ca5590156749feba4783a566fb846468210 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 23:42:18 +0300 Subject: [PATCH 31/35] Refactor some redundant elifs --- aiogram/types/message.py | 56 ++++++++++++++++----------------- aiogram/types/message_entity.py | 12 +++---- aiogram/utils/helper.py | 10 +++--- aiogram/utils/payload.py | 8 ++--- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 67fd07fa..0097fe58 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -94,60 +94,60 @@ class Message(base.TelegramObject): def content_type(self): if self.text: return ContentType.TEXT - elif self.audio: + if self.audio: return ContentType.AUDIO - elif self.animation: + if self.animation: return ContentType.ANIMATION - elif self.document: + if self.document: return ContentType.DOCUMENT - elif self.game: + if self.game: return ContentType.GAME - elif self.photo: + if self.photo: return ContentType.PHOTO - elif self.sticker: + if self.sticker: return ContentType.STICKER - elif self.video: + if self.video: return ContentType.VIDEO - elif self.video_note: + if self.video_note: return ContentType.VIDEO_NOTE - elif self.voice: + if self.voice: return ContentType.VOICE - elif self.contact: + if self.contact: return ContentType.CONTACT - elif self.venue: + if self.venue: return ContentType.VENUE - elif self.location: + if self.location: return ContentType.LOCATION - elif self.new_chat_members: + if self.new_chat_members: return ContentType.NEW_CHAT_MEMBERS - elif self.left_chat_member: + if self.left_chat_member: return ContentType.LEFT_CHAT_MEMBER - elif self.invoice: + if self.invoice: return ContentType.INVOICE - elif self.successful_payment: + if self.successful_payment: return ContentType.SUCCESSFUL_PAYMENT - elif self.connected_website: + if self.connected_website: return ContentType.CONNECTED_WEBSITE - elif self.migrate_from_chat_id: + if self.migrate_from_chat_id: return ContentType.MIGRATE_FROM_CHAT_ID - elif self.migrate_to_chat_id: + if self.migrate_to_chat_id: return ContentType.MIGRATE_TO_CHAT_ID - elif self.pinned_message: + if self.pinned_message: return ContentType.PINNED_MESSAGE - elif self.new_chat_title: + if self.new_chat_title: return ContentType.NEW_CHAT_TITLE - elif self.new_chat_photo: + if self.new_chat_photo: return ContentType.NEW_CHAT_PHOTO - elif self.delete_chat_photo: + if self.delete_chat_photo: return ContentType.DELETE_CHAT_PHOTO - elif self.group_chat_created: + if self.group_chat_created: return ContentType.GROUP_CHAT_CREATED - elif self.passport_data: + if self.passport_data: return ContentType.PASSPORT_DATA - elif self.poll: + if self.poll: return ContentType.POLL - else: - return ContentType.UNKNOWN + + return ContentType.UNKNOWN def is_command(self): """ diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index abb4f060..8b2e62d4 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -52,27 +52,27 @@ class MessageEntity(base.TelegramObject): if as_html: return markdown.hbold(entity_text) return markdown.bold(entity_text) - elif self.type == MessageEntityType.ITALIC: + if self.type == MessageEntityType.ITALIC: if as_html: return markdown.hitalic(entity_text) return markdown.italic(entity_text) - elif self.type == MessageEntityType.PRE: + if self.type == MessageEntityType.PRE: if as_html: return markdown.hpre(entity_text) return markdown.pre(entity_text) - elif self.type == MessageEntityType.CODE: + if self.type == MessageEntityType.CODE: if as_html: return markdown.hcode(entity_text) return markdown.code(entity_text) - elif self.type == MessageEntityType.URL: + if self.type == MessageEntityType.URL: if as_html: return markdown.hlink(entity_text, entity_text) return markdown.link(entity_text, entity_text) - elif self.type == MessageEntityType.TEXT_LINK: + if self.type == MessageEntityType.TEXT_LINK: if as_html: return markdown.hlink(entity_text, self.url) return markdown.link(entity_text, self.url) - elif self.type == MessageEntityType.TEXT_MENTION and self.user: + if self.type == MessageEntityType.TEXT_MENTION and self.user: return self.user.get_mention(entity_text) return entity_text diff --git a/aiogram/utils/helper.py b/aiogram/utils/helper.py index eeabca7c..443a2ffe 100644 --- a/aiogram/utils/helper.py +++ b/aiogram/utils/helper.py @@ -120,15 +120,15 @@ class HelperMode(Helper): """ if mode == cls.SCREAMING_SNAKE_CASE: return cls._screaming_snake_case(text) - elif mode == cls.snake_case: + if mode == cls.snake_case: return cls._snake_case(text) - elif mode == cls.lowercase: + if mode == cls.lowercase: return cls._snake_case(text).replace('_', '') - elif mode == cls.lowerCamelCase: + if mode == cls.lowerCamelCase: return cls._camel_case(text) - elif mode == cls.CamelCase: + if mode == cls.CamelCase: return cls._camel_case(text, True) - elif callable(mode): + if callable(mode): return mode(text) return text diff --git a/aiogram/utils/payload.py b/aiogram/utils/payload.py index 45643553..0c5e8ae9 100644 --- a/aiogram/utils/payload.py +++ b/aiogram/utils/payload.py @@ -52,14 +52,14 @@ def prepare_arg(value): """ if value is None: return value - elif isinstance(value, (list, dict)) or hasattr(value, 'to_python'): + if isinstance(value, (list, dict)) or hasattr(value, 'to_python'): return json.dumps(_normalize(value)) - elif isinstance(value, datetime.timedelta): + if isinstance(value, datetime.timedelta): now = datetime.datetime.now() return int((now + value).timestamp()) - elif isinstance(value, datetime.datetime): + if isinstance(value, datetime.datetime): return round(value.timestamp()) - elif isinstance(value, LazyProxy): + if isinstance(value, LazyProxy): return str(value) return value From dfc334ef2016b06974ad39b9fcd11b14b438fcfe Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 23:45:31 +0300 Subject: [PATCH 32/35] Minor refactor MessageEntity#parse --- aiogram/types/message_entity.py | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index 8b2e62d4..b9a9103e 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -49,29 +49,23 @@ class MessageEntity(base.TelegramObject): entity_text = self.get_text(text) if self.type == MessageEntityType.BOLD: - if as_html: - return markdown.hbold(entity_text) - return markdown.bold(entity_text) + method = markdown.hbold if as_html else markdown.bold + return method(entity_text) if self.type == MessageEntityType.ITALIC: - if as_html: - return markdown.hitalic(entity_text) - return markdown.italic(entity_text) + method = markdown.hitalic if as_html else markdown.italic + return method(entity_text) if self.type == MessageEntityType.PRE: - if as_html: - return markdown.hpre(entity_text) - return markdown.pre(entity_text) + method = markdown.hpre if as_html else markdown.pre + return method(entity_text) if self.type == MessageEntityType.CODE: - if as_html: - return markdown.hcode(entity_text) - return markdown.code(entity_text) + method = markdown.hcode if as_html else markdown.code + return method(entity_text) if self.type == MessageEntityType.URL: - if as_html: - return markdown.hlink(entity_text, entity_text) - return markdown.link(entity_text, entity_text) + method = markdown.hlink if as_html else markdown.link + return method(entity_text, entity_text) if self.type == MessageEntityType.TEXT_LINK: - if as_html: - return markdown.hlink(entity_text, self.url) - return markdown.link(entity_text, self.url) + method = markdown.hlink if as_html else markdown.link + return method(entity_text, self.url) if self.type == MessageEntityType.TEXT_MENTION and self.user: return self.user.get_mention(entity_text) return entity_text From 9ea22a29fc3a14beedeafd22d7f250a1aa45368a Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 12 Aug 2019 14:16:05 +0300 Subject: [PATCH 33/35] fix docs --- aiogram/utils/deprecated.py | 6 ++---- docs/source/utils/deprecated.rst | 7 ++----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 89081c8b..ecb66e6a 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -1,7 +1,3 @@ -""" -Source: https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically -""" - import functools import inspect import warnings @@ -13,6 +9,8 @@ def deprecated(reason): This is a decorator which can be used to mark functions as deprecated. It will result in a warning being emitted when the function is used. + + Source: https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically """ if isinstance(reason, str): diff --git a/docs/source/utils/deprecated.rst b/docs/source/utils/deprecated.rst index ce80bb0e..7f2f07cc 100644 --- a/docs/source/utils/deprecated.rst +++ b/docs/source/utils/deprecated.rst @@ -1,8 +1,5 @@ ========== Deprecated ========== -.. literalinclude:: ../../../aiogram/utils/deprecated.py - :caption: deprecated.py - :language: python3 - :linenos: - +.. automodule:: aiogram.utils.deprecated + :members: From 026416a66882ed445f71918df1b12872deb15e25 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 12 Aug 2019 14:16:38 +0300 Subject: [PATCH 34/35] refactoring --- aiogram/utils/deprecated.py | 5 ++--- examples/webhook_example.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index ecb66e6a..8a527420 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -40,7 +40,7 @@ def deprecated(reason): return decorator - elif inspect.isclass(reason) or inspect.isfunction(reason): + if inspect.isclass(reason) or inspect.isfunction(reason): # The @deprecated is used without any 'reason'. # @@ -64,8 +64,7 @@ def deprecated(reason): return wrapper1 - else: - raise TypeError(repr(type(reason))) + raise TypeError(repr(type(reason))) def warn_deprecated(message, warning=DeprecationWarning, stacklevel=2): diff --git a/examples/webhook_example.py b/examples/webhook_example.py index 0f6ae3cd..ca8028fd 100644 --- a/examples/webhook_example.py +++ b/examples/webhook_example.py @@ -62,7 +62,7 @@ async def cmd_about(message: types.Message): async def cancel(message: types.Message): # Get current state context - state = dp.current_state(chat=message.chat.id, user=message.from_user.id) + state = dp.current_state(chat_id=message.chat.id, user_id=message.from_user.id) # If current user in any state - cancel it. if await state.get_state() is not None: From 5a29eb09600f5e6b7b2f9b869178d3c3781184d0 Mon Sep 17 00:00:00 2001 From: birdi Date: Thu, 15 Aug 2019 01:37:55 +0300 Subject: [PATCH 35/35] Add relax argument in executor.start_polling --- aiogram/utils/executor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index 854facae..33f80684 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -23,7 +23,7 @@ def _setup_callbacks(executor: 'Executor', on_startup=None, on_shutdown=None): def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=True, - on_startup=None, on_shutdown=None, timeout=20, fast=True): + on_startup=None, on_shutdown=None, timeout=20, relax=0.1, fast=True): """ Start bot in long-polling mode @@ -38,7 +38,7 @@ def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=Tr executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop) _setup_callbacks(executor, on_startup, on_shutdown) - executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, fast=fast) + executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, relax=relax, fast=fast) def set_webhook(dispatcher: Dispatcher, webhook_path: str, *, loop: Optional[asyncio.AbstractEventLoop] = None, @@ -291,7 +291,7 @@ class Executor: self.set_webhook(webhook_path=webhook_path, request_handler=request_handler, route_name=route_name) self.run_app(**kwargs) - def start_polling(self, reset_webhook=None, timeout=20, fast=True): + def start_polling(self, reset_webhook=None, timeout=20, relax=0.1, fast=True): """ Start bot in long-polling mode @@ -303,7 +303,8 @@ class Executor: try: loop.run_until_complete(self._startup_polling()) - loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook, timeout=timeout, fast=fast)) + loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook, timeout=timeout, + relax=relax, fast=fast)) loop.run_forever() except (KeyboardInterrupt, SystemExit): # loop.stop()