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, }