How to correctly pass dependencies to handler? #4902
-
|
I recently tried to implement code with something like the following structure: from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes
from telegram import Update
class MyService: ...
async def hello(update: Update, context: ContextTypes.DEFAULT_TYPE):
# somehow get the service and do something useful
service: MyService = ...
...
def main():
# сreate instances of the required classes
service = MyService(...)
app = ApplicationBuilder().token("...").build()
app.add_handler(CommandHandler("hello", hello))
# somehow bind the service to the application
# so that it can be retrieved later in the handler.
...
app.run_polling()I'd be surprised there isn't a recommended way to do this easily, or I didn't read the documentation well. I understand that we can write something like this: # in main
app.service = service # just create attribute dynamically
# in handler
service = typing.cast(MyService, context.application.service)or like this # in main
app.bot_data["service"] = service
# in handler
service = typing.cast(MyService, context.bot_data["service"])or even make a variable at the module level. But these solutions look dirty from the point of view of typing and testability. The closest result I was able to achieve was using custom Application and CallbackContext classes. Full code exampleimport typing as t
from telegram import Update
from telegram.ext import (
ApplicationBuilder,
Application,
CommandHandler,
ContextTypes,
CallbackContext,
ExtBot,
)
ADict = dict[t.Any, t.Any]
class MyService: ...
class MyApplication(Application):
def __init__(self, service: MyService, **kwargs):
super().__init__(**kwargs)
self.service = service
class MyContext(CallbackContext[ExtBot[None], ADict, ADict, ADict]):
@property
def service(self):
return t.cast(MyApplication, self.application).service
async def hello(update: Update, context: MyContext) -> None:
print(context.service) # <__main__.MyService object at 0x7f174d334680>
def main():
service = MyService()
context_types = ContextTypes(context=MyContext)
app = (
ApplicationBuilder()
.application_class(MyApplication, {"service": service})
.context_types(context_types)
.token("...")
.build()
)
app.add_handler(CommandHandler("hello", hello))
app.run_polling()But even here we need to use type cast because in the context the application property is annotated as In general, I would like to know if my last example can be made better, maybe there are other alternatives and I missed something? |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments
-
|
I usually use a custom context, see e.g. https://github.com/python-telegram-bot/ptbcontrib/tree/main/ptbcontrib/username_to_chat_api or https://docs.python-telegram-bot.org/en/stable/examples.contexttypesbot.html Not sure that's fully accommodating of your fully type cast example here |
Beta Was this translation helpful? Give feedback.
-
This binds the service to the instance, making it available via self in your handlers. No globals, no bot_data, and perfect type safety. Python from telegram import Update class MyService: class BotHandlers: def main():
Python import functools Define handler with an EXTRA argument for the serviceasync def hello(update: Update, context: ContextTypes.DEFAULT_TYPE, service: MyService): def main():
Why is the cast still necessary? You asked why t.cast is needed even though you passed MyApplication to the builder. The issue lies in the definition of CallbackContext. In the PTB source code, the application property inside CallbackContext is typed as the base Application class (or a generic that defaults to it). Even though you injected MyApplication at runtime, the static type checker (mypy/pyright) cannot infer that this specific instance of Context is always linked to that specific subclass of Application without a circular generic dependency that is hard to express in Python. Your property override is the correct fix: Python @Property |
Beta Was this translation helpful? Give feedback.
I usually use a custom context, see e.g. https://github.com/python-telegram-bot/ptbcontrib/tree/main/ptbcontrib/username_to_chat_api or https://docs.python-telegram-bot.org/en/stable/examples.contexttypesbot.html
Not sure that's fully accommodating of your fully type cast example here