Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Automatic pass_* for Handlers using reflection: Autowiring #859

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 18 commits into from

Conversation

JosXa
Copy link
Contributor

@JosXa JosXa commented Oct 6, 2017

Whenever I build bots or fix other peoples' code, I run into the problem that the pass_something arguments for Handlers are so explicit. Say I need chat_data in one handler, this handler is used by other callbacks, so the other handler needs chat_data too and this process escalates to a lot of places that need adjustment. It is also quite unnecessary information to have in your routing definitions (the dispatcher.add_handler part) since it is more a kind of implementation details than an actual targeting directive.

My proposed solution to this is to do introspection on the callback so that the multitude of pass_* parameters is no longer required, in favor of a new parameter: autowire=True.
In addition to the pass_* instructions, bot and update can also be used arbitrarily through this PR.

This allows for a much cleaner syntax when adding handlers, please refer to the new autowiring.py example.

@JosXa JosXa requested review from tsnoam and jh0ker October 6, 2017 23:48
@JosXa JosXa self-assigned this Oct 6, 2017
@JosXa JosXa added the πŸ“‹ pending-review work status: pending-review label Oct 7, 2017
@tsnoam
Copy link
Member

tsnoam commented Oct 7, 2017

I started reading the PR and it's very clear you've put a lot of effort into it. However, that made me think that maybe we need a non-backward compat change here.

I'm opening this for discussion, what about:
All callbacks will receive all parameters (job queue, user data, chat data, etc.) as attributes to a context object.
That is:

class CallbackCtx:
    job_queue = ...
    user_data = ...
    chat_data = ...
    bot = ...


def callback_prototype(update, cb_ctx):
    ...

What do you think?

@JosXa
Copy link
Contributor Author

JosXa commented Oct 7, 2017

@tsnoam This would just hand over complexity to the end user again who would be required to pick the objects from the context object (which is context sensitive in itself) and it's impossible to know what attributes are available in the type of callback you're working with which also prohibits IDE support.
There's a reason why arguments exist, and frameworks like Flask do great injecting their parameters automatically. You get instantaneous feedback if you use a parameter that is not defined in this context, as opposed to errors that lurk behind conditional statements when accessing the context object. Example with this PR:

def f(bot, update, chat_data, groups):
    pass  # groups is not passed --> missing positional argument

dp.add_handler(CommandHandler("error", f, autowire=True))

Yields UserWarning: The argument groups cannot be autowired since it is not available on CommandHandlers.

In order to get the same kind of immediate error reporting with a context object, we would need to inspect the full method for its usage... and we're back at square one.

As an end user I'd like my callback handlers to be somewhat explicit as to which arguments I'm using in the method, so I can see at a glance what library entities are being worked with, which serves maintainability. What autowiring does is basically just "syntactic sugar for explicitness".

What you suggest is more of a Java solution than the pythonic way of doing it, and I still believe passing context sensitive arguments is superior, especially in terms of flexibility. A pro that I can see is free type hinting for the attributes of the context object.

@tsnoam
Copy link
Member

tsnoam commented Oct 7, 2017

  1. I'll ignore the comment about Java as I've never developed in Java.
  2. I beg to differ about moving complexity to the user. I think it makes things very simple and straight forward.
    The user needs to implement a very simple callback prototype: def callback_prototype(update, cb_ctx) (also update can be moved into the context as well).
    To pick the right attribute from the context object is very simple: cb_ctx.bot, cb_ctx.job_queue, etc.
  3. Regarding different arguments for different handlers:
    We can have a basic context class:
class Ctx:
    update_queue = ...
    job_queue = ...
    user_data = ...
    chat_data = ...

and for some handlers there be sub-classes providing groups & groupsdict or args as well (depending on the handler).

@Lonami
Copy link

Lonami commented Oct 7, 2017

I agree with @JosXa. I think it's just easier to call an argument "groups" and it magically gets assigned. The variable name makes sense and there's no need to rely on a variable "context" with more or less members. And if one doesn't like the autowiring, or prefers other parameter names, then the old mechanism would still be there.

@JosXa JosXa added question πŸ“‹ help-wanted work status: help-wanted and removed question labels Oct 7, 2017
@Eldinnie
Copy link
Member

Eldinnie commented Oct 7, 2017

I'm have a bit of mixed feelings about this. I'm trying to put it in words.

I think it's not bad that users know what they're doing. If they want to use the job_queue in their handler, they'll have to pass it when adding the handler. I think it makes code more readable and more obvious to understand. Adding behind the scene inspection and taking this understanding and knowledge away from the user might end up on the other side of what you're trying to do. Granted they find the concept hard to grasp now and then, but usually understand how and why it works the way it does when it's explained to them.

Also, I personally know this library pretty well, that's why I know I can use shortcut methods like Message.reply_text(), hence I do not really have a use for a bot argument in handlers. However, most users starting with coding bot's start by using our library in conjunction with Telegram's documentation, they will use bot.send_message(update.message.chat_id, ) instead. Having a simple, reliable, always available bot, update is nice in those situations.

That said, I also agree the way it is done now is a bit archaic and might be due for an overhaul. I lean towards @tsnoam 's suggestion of a context-manager more, bot, update, job_queue and update_queue can always be present. But then we're stuck on stuff that's not always present.
There could be some stuck-in-the-middle solution (still have to add pass_groups to add_handler()) that I don;t really like, or default them to None in the context manager. I'm not completely convinced either way. I think my first point is valid for both this PR and a context manager.

In the end I think it's not bad if complex things are complex to code.
You want it easy? Then take the easy road and just use handlers with bot, update.
You want more complex stuff? Then dive in, learn the syntax of the library and use it.

@JosXa
Copy link
Contributor Author

JosXa commented Oct 7, 2017

@Eldinnie

However, most users starting with coding bot's start by using our library in conjunction with Telegram's documentation, they will use bot.send_message(update.message.chat_id, ) instead. Having a simple, reliable, always available bot, update is nice in those situations.

Well the way the autowiring is designed in this PR doesn't hurt new users in any way. It is just syntactic sugar that more advanced users can opt-in to leverage and keep their code short. It is not intended as a beginner-friendly feature, but it also doesn't hurt. After all, few beginners use chat_data or user_data for the first couple months with the library.

bot, update, job_queue and update_queue can always be present.

Special attention again to StringCommandHandler, RegexCommandHandler and TypeHandler which don't have chat_data and user_data.

You want it easy? Then take the easy road and just use handlers with bot, update.
You want more complex stuff? Then dive in, learn the syntax of the library and use it.

Hmm I still don't really see how this hurts adding some ease of use for seasoned developers (only). For me personally in 6 big, active bots, it hurts like hell having to modify several places every time I need a new object from the library. A lib should support and endorse ease of use, not make it cumbersome.

To pick the right attribute from the context object is very simple: cb_ctx.bot, cb_ctx.job_queue, etc.

To keep picking on @tsnoam's approach (sorry, please don't take it personal πŸ˜™), yes, it is simple. But it is in no way convenient. I don't know if you have worked on a chatbot with a huge code base, but having to prefix most (!) of the library objects with a context access is just not bearable - from a user perspective. The number of characters and complexity we save by removing the pass_my_object flag would be overhauled by far through this indirect access. If it came to that, I would probably make a decorator that served me the arguments directly at function-level namespace, just to avoid the prefixing Β―_(ツ)_/Β―. Also, let's not forget backwards compatibility. Food for thought.

@python-telegram-bot python-telegram-bot deleted a comment from codecov bot Oct 7, 2017
@JosXa JosXa changed the title Magic pass_* for Handlers using reflection: Autowiring Automatic pass_* for Handlers using reflection: Autowiring Oct 7, 2017
@JosXa
Copy link
Contributor Author

JosXa commented Oct 9, 2017

If naming is a problem, the flag could also be called pass_by_name=True or pass_auto(matically)=True

@tsnoam
Copy link
Member

tsnoam commented Oct 9, 2017

@JosXa The naming is not the issue here.

@tsnoam
Copy link
Member

tsnoam commented Oct 9, 2017

@JosXa I have thought about it long and went back and forth with my opinion. One time taking one way then taking the other.
Eventually I ended up with this:

  1. There's no argue that the current API is cumbersome and needs replacement.
  2. ptb as a project takes high value not only in quality, but also with simplicity to the user. This what makes it so great and easy for beginners but still gives all the power needed for the advanced users.
  3. Given the current API autowiring gives great improvement. It allows the user to decide what arguments from the entire context they require and simply put only what they need in their callback prototype.
  4. But that is exactly the problem. It makes the entire interface non deterministic with different behaviour for different users. It prevents simple debugging by adding a layer of meta programming which hides the call tree behind string manipulations and it perpetuates part of the current API problems by making more assumptions on kwargs naming.
    I can easily see how beginners and power users will hit trouble with this kind of API.
  5. OTOH, it seems like any other solution will not be backward compatible. But then the question which we should ask ourselves is: does it worth it to keep backward compatibility at all costs? (I'll get back to this question down below)
  6. Back to the simplicity of use. As a software developer I'm always looking for simplicity. I want to understand what's going on simply by looking at the API. For me, a callback which receives an update object and a context object is the most intuitive thing. I know what it is, I know what I can do with it and I can easily figure out how it got there. Then I can choose to use only part, all or none of the attributes of the context object.
  7. Regarding backward compatibility: yes, it is very important value to keep. However, if for the mere sake of backward compatibility we'll make our code base more complicated and error prone, I think that it will be a mistake to keep it.

To make a long story short, after considering it very thoroughly my personal opinion is that the autowiring approach is not the way we should go. I prefer a context object, even at the price of breaking backward compatibility.

p.s.
I know that my alternative design suggestion is still a bit rough. There is more than one way to do it. Some more puristic some holistic. We can discuss them if and when we'll decide it's relevant.

@JosXa JosXa removed the πŸ“‹ pending-review work status: pending-review label Oct 20, 2017
@JosXa
Copy link
Contributor Author

JosXa commented Feb 19, 2018

Going back to the case of replacing the pass_* hell with a context object. The Telethon library by @Lonami has gotten some awesome upgrades in the last couple of days and an event handling was built on top of the Updates Telegram sends. I've grown accomplished very fast and I quite like the naming of it, instead of an Update you get an Event in your callbacks, which is an abstraction that combines multiple types of Updates from the user api.

What do you think about naming the replacement for (bot, update) Events?

On a related note, we will obviously break backwards compatibility. But I think to build a reliable tool to convert entire user projects to the new syntax, which would presumably help a lot in the transition, aside from the mandatory deprecation period.

@tsnoam
Copy link
Member

tsnoam commented Feb 19, 2018

The subject of "autowiring" had been discussed in the developers group several time and we've recently came to the decision that this will not be the way we go.
The current mindset is to pass a context object to the callback function which will contain everything the user needs. The exact implementation details are still to be determined.

I'm closing this PR for now. Further design discussions will take place in the developers group.

@tsnoam tsnoam closed this Feb 19, 2018
@python-telegram-bot python-telegram-bot locked and limited conversation to collaborators Feb 19, 2018
@JosXa JosXa deleted the magic-arg-passing branch November 30, 2020 23:49
@Bibo-Joshi Bibo-Joshi added πŸ”Œ enhancement pr description: enhancement and removed enhancement labels Nov 3, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
πŸ”Œ enhancement pr description: enhancement πŸ“‹ help-wanted work status: help-wanted
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants