Skip to content

Commit

Permalink
#4_filter_rewrite: Migrated the @on_update to be using Filters now.
Browse files Browse the repository at this point in the history
  • Loading branch information
luckydonald committed Jun 25, 2020
1 parent 430c7f3 commit d9a95e1
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 86 deletions.
52 changes: 52 additions & 0 deletions teleflask/server/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
__author__ = 'luckydonald'

from pytgbot.api_types.receivable.updates import Update, Message

from teleflask import Teleflask, TBlueprint
from ..messages import Message as OldSendableMessage
from ..new_messages import SendableMessageBase

Expand All @@ -21,6 +23,9 @@ class DEFAULT: pass
# end if


_HANDLERS_ATTRIBUTE = '__teleflask.__handlers'


class NoMatch(Exception):
"""
Raised by a filter if it denies to process the update.
Expand Down Expand Up @@ -146,6 +151,53 @@ def call_handler(self, update: Update, match_result: MATCH_RESULT_TYPE) -> OPTIO
return self.func(update)
# end def

@classmethod
def decorator(cls, teleflask_or_tblueprint: Union[Teleflask, TBlueprint, None], *required_keywords):
"""
Decorator to register a function to receive updates.
Usage:
>>> app = Teleflask(API_KEY)
>>> @app.on_update
>>> @app.on_update("update_id", "message", "whatever")
>>> def foo(update):
... assert isinstance(update, Update)
... # do stuff with the update
... # you can use app.bot to access the bot's messages functions
Or, if you wanna go do it directly for some strange reason:
>>> @UpdateFilter.decorator(app)
>>> @UpdateFilter.decorator(app)("update_id", "message", "whatever")
>>> def foo(update):
... pass
"""

def decorator_inner(function):
if teleflask_or_tblueprint:
filter = cls(func=function, required_update_keywords=required_keywords)
teleflask_or_tblueprint.register_handler(filter)
# end if
handlers = getattr(function, _HANDLERS_ATTRIBUTE, [])
filter = cls(func=function, required_update_keywords=required_keywords)
handlers.append(filter)
setattr(function, _HANDLERS_ATTRIBUTE, handlers)
return function
# end def

if (
len(required_keywords) == 1 and # given could be the function, or a single required_keyword.
not isinstance(required_keywords[0], str) # not string -> must be function
):
# @on_update
function = required_keywords[0]
required_keywords = None
return decorator_inner(function) # not string -> must be function
# end if
# -> else: all `*required_keywords` are the strings
# @on_update("update_id", "message", "whatever")
return decorator_inner # let that function be called again with the function.
# end def

# noinspection SqlNoDataSourceInspection
def __str__(self):
if not self.required_update_keywords:
Expand Down
125 changes: 39 additions & 86 deletions teleflask/server/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
import logging
from abc import abstractmethod
from collections import OrderedDict
from typing import List

from pytgbot.api_types.receivable.updates import Update

from .filters import UpdateFilter, Filter, NoMatch
from ..exceptions import AbortProcessingPlease
from .abstact import AbstractUpdates, AbstractBotCommands, AbstractMessages, AbstractRegisterBlueprints, AbstractStartup
from .base import TeleflaskMixinBase
Expand Down Expand Up @@ -42,16 +44,13 @@ class UpdatesMixin(TeleflaskMixinBase, AbstractUpdates):
"""
def __init__(self, *args, **kwargs):
self.update_listeners = OrderedDict() # Python3.6, dicts are sorted # Schema:
# Schema: {func: [ ["message", "key", "..."] ]} or {func: None} for wildcard.
# [ ['A', 'B'], ['C'] ] == 'A' and 'B' or 'C'
# [ ]  means 'allow all'.
self.update_listeners: List[Filter] = []

super(UpdatesMixin, self).__init__(*args, **kwargs)
# end def

def on_update(self, *required_keywords):
"""
on_update = UpdateFilter.decorator
on_update.__doc__ = """
Decorator to register a function to receive updates.
Usage:
Expand All @@ -61,38 +60,24 @@ def on_update(self, *required_keywords):
>>> # do stuff with the update
>>> # you can use app.bot to access the bot's messages functions
"""
def on_update_inner(function):
return self.add_update_listener(function, required_keywords=required_keywords)
# end def
if (
len(required_keywords) == 1 and # given could be the function, or a single required_keyword.
not isinstance(required_keywords[0], str) # not string -> must be function
):
# @on_update
function = required_keywords[0]
required_keywords = None
return on_update_inner(function) # not string -> must be function
# end if
# -> else: *required_keywords are the strings
# @on_update("update_id", "message", "whatever")
return on_update_inner # let that function be called again with the function.
# end def

def add_update_listener(self, function, required_keywords=None):
def register_handler(self, event_handler: Filter):
"""
Adds an listener for updates.
You can filter them if you supply a list of names of attributes which all need to be present.
Adds an listener for any update type.
You provide a Filter for them as parameter, it also contains the function.
No error will be raised if it is already registered. In that case a warning will be logged,
but nothing else will happen, and the function is not added.
Examples:
>>> add_update_listener(func, required_keywords=["update_id", "message"])
>>> register_handler(UpdateFilter(func, required_keywords=["update_id", "message"]))
# will call func(msg) for all updates which are message (have the message attribute) and have a update_id.
>>> add_update_listener(func, required_keywords=["inline_query"])
>>> register_handler(UpdateFilter(func, required_keywords=["inline_query"]))
# calls func(msg) for all updates which are inline queries (have the inline_query attribute)
>>> add_update_listener(func)
>>> register_handler(UpdateFilter(func, required_keywords=None))
>>> register_handler(UpdateFilter(func))
# allows all messages.
:param function: The function to call. Will be called with the update and the message as arguments
Expand All @@ -101,45 +86,13 @@ def add_update_listener(self, function, required_keywords=None):
:return: the function, unmodified
"""

if required_keywords is None:
self.update_listeners[function] = [None]
logging.debug("listener required keywords set to allow all.")
return function
# end def

# check input, make a list out of what we might get.
if isinstance(required_keywords, str):
required_keywords = [required_keywords] # str => [str]
elif isinstance(required_keywords, tuple):
required_keywords = list(required_keywords) # (str,str) => [str,str]
# end if
assert isinstance(required_keywords, list)
for keyword in required_keywords:
assert isinstance(keyword, str) # required_keywords must all be type str
# end if

if function not in self.update_listeners:
# function does not exists, create the keywords.
logging.debug("adding function to listeners")
self.update_listeners[function] = [required_keywords] # list of lists. Outer list = OR, inner = AND
else:
# function already exists, add/merge the keywords.
if None in self.update_listeners[function]:
# None => allow all, so we don't need to add a filter
logger.debug('listener not updated, as it is already wildcard')
elif required_keywords in self.update_listeners[function]:
# the keywords already are required, we don't need to add a filter
logger.debug("listener required keywords already in {!r}".format(self.update_listeners[function]))
else:
# add another case
self.update_listeners[function].append(required_keywords) # Outer list = OR, required_keywords = AND
logger.debug("listener required keywords updated to {!r}".format(self.update_listeners[function]))
# end if
# end if
return function
logging.debug("adding handler to listeners")
self.update_listeners.append(event_handler) # list of lists. Outer list = OR, inner = AND
return event_handler
# end def add_update_listener

def remove_update_listener(self, func):
def remove_update_listener(self, event_handler):
"""
Removes an function from the update listener list.
No error will be raised if it is already registered. In that case a warning will be logged,
Expand All @@ -149,12 +102,11 @@ def remove_update_listener(self, func):
:param function: The function to remove
:return: the function, unmodified
"""
if func in self.update_listeners:
del self.update_listeners[func]
else:
try:
self.update_listeners.remove(event_handler)
except ValueError:
logger.warning("listener already removed.")
# end if
return func
# end def

def process_update(self, update):
Expand All @@ -167,25 +119,26 @@ def process_update(self, update):
:return: nothing.
"""
assert isinstance(update, Update) # Todo: non python objects
for listener, required_fields_array in self.update_listeners.items():
for required_fields in required_fields_array:
try:
if not required_fields or all([hasattr(update, f) and getattr(update, f) for f in required_fields]):
# either filters evaluates to False, (None, empty list etc) which means it should not filter
# or it has filters, than we need to check if that attributes really exist.
self.process_result(update, listener(update)) # this will be TeleflaskMixinBase.process_result()
break # stop processing other required_fields combinations
# end if
except AbortProcessingPlease as e:
logger.debug('Asked to stop processing updates.')
if e.return_value:
self.process_result(update, e.return_value)
# end if
return # not calling super().process_update(update)
except Exception:
logger.exception("Error executing the update listener {func}.".format(func=listener))
# end try
# end for
filter: Filter
for filter in self.update_listeners:
try:
# check if the Filter matches
match_result = filter.match(update)
# call the handler
result = filter.call_handler(update=update, match_result=match_result)
# send the message
self.process_result(update, result) # this will be TeleflaskMixinBase.process_result()
except NoMatch as e:
logger.debug(f'not matching filter {filter!s}.')
except AbortProcessingPlease as e:
logger.debug('Asked to stop processing updates.')
if e.return_value:
self.process_result(update, e.return_value) # this will be TeleflaskMixinBase.process_result()
# end if
return # not calling super().process_update(update)
except Exception:
logger.exception(f"Error executing the update listener with {filter!s}: {filter!r}")
# end try
# end for
super().process_update(update)
# end def process_update
Expand Down

0 comments on commit d9a95e1

Please sign in to comment.