Skip to content

Commit

Permalink
refactor(): move to 0.3.x fixes ()
Browse files Browse the repository at this point in the history
* fix(minor): minor fixes

remove support for events.([un]/register). clean up code

* featref(): rename "file" module to "json"
Breaking(!):
. remove FSMProxy avoid races
. fix frozen_list decorator to reuse decorated functions
. update docs

* chore(): remove print statement :)

* handle commands with bot username (#12)

Co-authored-by: GauthamramRavichandran <[email protected]>
  • Loading branch information
uwinx and GauthamramRavichandran authored Jun 7, 2020
1 parent f0fb14e commit a4520b1
Show file tree
Hide file tree
Showing 19 changed files with 640 additions and 651 deletions.
58 changes: 35 additions & 23 deletions examples/fsm.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
from telethon import custom
from functools import partial

from telethon import custom, events
from garnet import TelegramClient, FSMContext, MessageText, CurrentState
from garnet.router import Router
from garnet.helpers import var

POSSIBLE_PETS = ("Doggggee |}", "Cat >.<", "Human <|", "Goose 8D")
PETS = tuple([
(custom.Button.text(_petty, resize=True),)
for _petty in POSSIBLE_PETS
])

# // genders in parallel universe buttons. just imagine 🤔
buttons = (
(custom.Button.text("Abuser", resize=True),),
(custom.Button.text("Dishwasher"),),
(custom.Button.text("Cat"),),
)

class States:
_auto = partial(var.Var, prefix="state:")

name: str = _auto() # state:name
age: str = _auto() # state:age
pet: str = _auto() # state:pet


# /*
Expand All @@ -17,63 +26,66 @@
# */
bot = TelegramClient.from_env()

router = Router(event=events.NewMessage())


@bot.on(MessageText.commands("start"))
@router.on(MessageText.commands("start"))
async def handler(update: custom.Message, context: FSMContext):
await update.reply(f"Hello, please enter your name!")
await context.set_state("stateName")
await context.set_state(States.name)


# // handle all /cancel's from any state if only state is not None
@bot.on(MessageText.commands("cancel"), CurrentState == any)
@router.on(MessageText.commands("cancel"), CurrentState == any)
async def cancel_handler(update: custom.Message, context: FSMContext):
await update.reply("Ok. Resetting!")
await context.reset_state(with_data=True)


@bot.on(CurrentState == "stateName")
@router.on(CurrentState == States.name)
async def name_handler(update: custom.Message, context: FSMContext):
await context.set_data({"name": update.raw_text})
await update.reply(
f"{update.raw_text}, please enter your age:",
buttons=custom.Button.force_reply(),
)
await context.set_state("stateAge")
await context.set_state(States.age)


# if we can use one Filter, then let's create filter which will be cached by garnet. Optimizations 8>
ageStateFilter = CurrentState.exact("stateAge")
ageStateFilter = CurrentState.exact(States.age)


@bot.on(ageStateFilter & MessageText.isdigit())
@router.on(ageStateFilter & MessageText.isdigit())
async def age_correct_handler(update: custom.Message, context: FSMContext):
await context.update_data(age=int(update.raw_text))
await update.reply(f"Cool! Now please select your gender:", buttons=buttons)
await context.set_state("stateGender")
await update.reply(f"Cool! Now please select your favourite pet:", buttons=PETS)
await context.set_state(States.pet)


@bot.on(ageStateFilter & ~MessageText.isdigit())
@router.on(ageStateFilter & ~MessageText.isdigit())
async def age_incorrect_handler(update: custom.Message):
await update.reply(f"Please try again! Age must be digit :D")


@bot.on(CurrentState.exact("stateGender") & MessageText.between("Abuser", "Dishwasher", "Cat"))
async def gender_correct_handler(update: custom.Message, context: FSMContext):
await context.update_data(gender=update.raw_text)
@router.on(CurrentState.exact(States.pet) & MessageText.between(*POSSIBLE_PETS))
async def pet_correct_handler(update: custom.Message, context: FSMContext):
await context.update_data(pet=update.raw_text)
data = await context.get_data()
await update.reply(
f"Your age: {data['age']}\n"
f"Name: {data['name']}\n"
f"Gender: {data['gender']}\n"
f"Thank you! You can participate again!",
f"Favourite pert: {data['pet']}\n"
f"Thank you! To participate again send /start",
buttons=custom.Button.clear(),
)
await context.reset_state(with_data=True)


if __name__ == '__main__':
@bot.on_start
@bot.on_start()
async def main(_):
bot.bind_routers(router)
await bot.start_as_bot()


Expand Down
4 changes: 2 additions & 2 deletions examples/locker.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ async def menu(_):
await messages.reply("Sorry, I'm not ukrainian cat-pic sender...")


@bot.on_start
@bot.on_start()
async def on_start(client: TelegramClient):
await client.start()
await client.send_message("smn", "Bot is starting")


@bot.on_finish
@bot.on_finish()
async def on_finish(client: TelegramClient):
await client.send_message("smn", "GoodBye!")

Expand Down
3 changes: 1 addition & 2 deletions garnet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
#

from .client import TelegramClient
from .storages.base import BaseStorage, FSMContext, FSMContextProxy
from .storages.base import BaseStorage, FSMContext
from .filters import Filter, text, CurrentState
from .jsonlib import json
from .callbacks.base import Callback
Expand All @@ -38,7 +38,6 @@
"TelegramClient",
"events",
"FSMContext",
"FSMContextProxy",
"CurrentState",
"text",
"MessageText", # bc&c
Expand Down
38 changes: 17 additions & 21 deletions garnet/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import (
Union,
Callable,
Dict,
Sequence,
List,
Optional,
Expand All @@ -15,12 +16,10 @@

from telethon.client.telegramclient import TelegramClient as _TelethonTelegramClient

from garnet.events import StopPropagation, NewMessage, Raw

from .events import EventBuilderDict
from .events import EventBuilderDict, StopPropagation, NewMessage, Raw
from .filters import state
from .filters.base import Filter
from .storages import base, file, memory
from .storages import base, memory
from .callbacks.base import (
Callback,
CURRENT_CLIENT_KEY,
Expand All @@ -35,7 +34,10 @@


class TelegramClient(_TelethonTelegramClient, ctx.ContextInstanceMixin):
storage: base.BaseStorage = None
# todo how hard would be using forwardable library to use composition instead of inheritance.

storage: base.BaseStorage
conf: Dict[str, Any]

class Env:
default_session_dsn_key = "SESSION"
Expand All @@ -57,19 +59,13 @@ def __init__(

super().__init__(session, api_id, api_hash, *args, **kwargs)

self.conf: Dict[str, Any] = {}
self.storage = storage
self.__bot_token = None

_action_containerT = PseudoFrozenList[Callable]
self.on_start: _action_containerT
self.on_finish: _action_containerT
self.on_background: _action_containerT

self.on_start, self.on_finish, self.on_background = (
PseudoFrozenList(),
PseudoFrozenList(),
PseudoFrozenList(),
)
self.on_start: PseudoFrozenList[Callable] = PseudoFrozenList()
self.on_finish: PseudoFrozenList[Callable] = PseudoFrozenList()
self.on_background: PseudoFrozenList[Callable] = PseudoFrozenList()

self.set_current(self)

Expand Down Expand Up @@ -100,7 +96,7 @@ def from_env(
obj.__bot_token = os.getenv(cls.Env.default_bot_token_key, bot_token)
obj.set_current(obj)

if isinstance(storage, (file.JSONStorage,)):
if isinstance(storage, base.FileStorageProto):

async def close_storage(*_):
await storage.close()
Expand Down Expand Up @@ -134,12 +130,12 @@ async def start_as_bot(self, token: str = None) -> TelegramClient:
)

@classmethod
def make_fsm_key(cls, update, *, _check=True) -> dict:
def make_fsm_key(cls, event) -> dict:
try:
return {"chat": update.chat_id, "user": update.from_id}
return {"chat": event.chat_id, "user": event.from_id}
except AttributeError:
raise Warning(
f"Standard make_fsm_key method was not designed for {update!r}"
f"Standard make_fsm_key method was not designed for {event!r}"
)

def current_state(self, *, user, chat):
Expand Down Expand Up @@ -171,7 +167,7 @@ def decorator(f):
def add_event_handler(
self,
callback: Union[Callable, Callback],
event=None,
event: Any = None,
*filters: Union[Callable, Filter],
):
if filters is not None and not isinstance(filters, Sequence):
Expand Down Expand Up @@ -293,7 +289,7 @@ async def _dispatch_update(self, update, others, channel_id, pts_date):

coro = callback.__call__(event, **kwargs)
else:
coro = callback.__call__(**kwargs)
coro = callback.__call__()

if not callback.continue_prop:
return await coro
Expand Down
64 changes: 0 additions & 64 deletions garnet/events/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from typing import Type

from warnings import warn

from telethon.events.raw import Raw
from telethon.events.album import Album
from telethon.events.chataction import ChatAction
Expand Down Expand Up @@ -85,15 +83,6 @@ def __getitem__(self, builder):
)


def _deprecated():
warn(
message="event.Register/Unregister is deprecated in "
"Garnet, use garnet::router::Router",
category=Exception,
stacklevel=5,
)


class StopPropagation(Exception):
"""
If this exception is raised in any of the handlers for a given event,
Expand All @@ -120,56 +109,3 @@ class StopPropagation(Exception):
# For some reason Sphinx wants the silly >>> or
# it will show warnings and look bad when generated.
pass


def register(event=None):
"""
DEPRECATED FUNCTION
"""
_deprecated()


def unregister(*_):
"""
Inverse operation of `register` (though not a decorator). Client-less
`remove_event_handler
<telethon.client.updates.UpdateMethods.remove_event_handler>`
variant. **Note that this won't remove handlers from the client**,
because it simply can't, so you would generally use this before
adding the handlers to the client.
This method is here for symmetry. You will rarely need to
unregister events, since you can simply just not add them
to any client.
If no event is given, all events for this callback are removed.
Returns how many callbacks were removed.
"""
_deprecated()


def is_handler(*_):
"""
DEPRECATED
Returns `True` if the given callback is an
event handler (i.e. you used `register` on it).
"""
_deprecated()


def list(*_):
"""
DEPRECATED
Returns a list containing the registered event
builders inside the specified callback handler.
"""

_deprecated()


def _get_handlers(*_):
"""
DEPRECATED
Like ``list`` but returns `None` if the callback was never registered.
"""
_deprecated()
1 change: 1 addition & 0 deletions garnet/filters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .base import Filter
from .state import CurrentState
from . import text
from .file_ext import File
12 changes: 6 additions & 6 deletions garnet/filters/state.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Callable, Dict, Any, Tuple, Union, Sequence
from typing import Callable, Dict, Any, Tuple, Union, Container

from telethon.events import common

Expand Down Expand Up @@ -27,13 +27,13 @@ def __rmatmul__(mcs, key_maker: Tuple[str, KeyMakerType]) -> Filter:
return CurrentState @ key_maker

@classmethod
def __eq__(mcs, _s: Union[str, List[str]]) -> Filter:
def __eq__(mcs, _s: Union[str, Container[str]]) -> Filter:
if id(_s) in ANY_STATE:

async def _f(_, context):
return (await context.get_state()) is not None

elif isinstance(_s, Sequence):
elif not isinstance(_s, str) and isinstance(_s, Container):

async def _f(_, context):
return (await context.get_state()) in _s
Expand All @@ -46,13 +46,13 @@ async def _f(_, context):
return Filter(_f, requires_context=True, state_op=True)

@classmethod
def exact(mcs, state: Union[str, List[str]]) -> Filter:
def exact(mcs, state: Union[str, Container[str]]) -> Filter:
# noinspection PyTypeChecker
return CurrentState == state # type: ignore

@classmethod
def with_key(mcs, state: str, key_maker: KeyMakerType):
return CurrentState @ (state, key_maker)
return mcs.__matmul__((state, key_maker))


class CurrentState(metaclass=_MetaCurrentState):
Expand All @@ -63,7 +63,7 @@ class CurrentState(metaclass=_MetaCurrentState):
key_maker is a callable with signature: (event) -> Dict[str, Any]
For instance:
>>> def my_key_maker(event) -> Dict[str, Any]:
... return {"chat": event.chat.id, event.user.id}
... return {"chat": event.chat.id, "user": event.user.id}
...
>>> CurrentState @ ("equlas_to_the_state", my_key_maker)
... <garnet.filters.base.Filter object at 0x7f8bebe996>
Expand Down
2 changes: 1 addition & 1 deletion garnet/filters/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def commands(
return Filter(
lambda update: isinstance(update.text, str)
and any(update.text.startswith(prefix) for prefix in prefixes)
and update.text.split()[0][1:] in cmd
and update.text.split()[0][1:].split('@')[0] in cmd
)


Expand Down
Loading

0 comments on commit a4520b1

Please sign in to comment.