Skip to content

Add default values #1490

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

Merged
merged 34 commits into from
Feb 6, 2020
Merged

Add default values #1490

merged 34 commits into from
Feb 6, 2020

Conversation

Bibo-Joshi
Copy link
Member

@Bibo-Joshi Bibo-Joshi commented Aug 28, 2019

Supersedes/Closes #1226 and closes #1527

To meet the requirements stated by @Eldinnie in #1226, I added a new class DefaultValue to the helpers module. It's purpose is to allow to differentiate check if a value is just the default or was set specifically:

With def f(arg=None) we don't know, if arg is None results from f() or from f(None).
With def f(arg=DefaultValue(None), we can handle those differently.

This makes it necessary to add some stuff to the InlineQueryResult* for bot.answer_inline_query to parse the new default_parse_mode correctly. But it does the job :) Maybe DefaultValue also comes in handy with a future use case …
How do the @python-telegram-bot/maintainers feel about this?

For the tests, I tried and added them for the send_* and edit_message_* methods. I'm still not too familiar with pytest, so please tell if those can be improved.
However, I couldn't find a clever way to test the default_parse_mode for answer_inline_query since I'd somehow have to get hold of the resulting message. Any idea is appreciated :)

Copy link
Member

@Poolitzer Poolitzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didnt check the tests because I feel nowhere save with them.

Comment on lines +49 to +51
@property
def _has_input_message_content(self):
return hasattr(self, 'input_message_content')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont exactly see what this has to do with the PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question … Don't recall, why I added it …

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mh, actually it seems that the InlineQueryResult… classes don't always have an input_message_content attribute. This is due to the fact, that the base class InlineQueryResult doesn't have one and in the child classes the optionals are parsed in the manner

if input_message_content:
            self.input_message_content = input_message_content

without preceding self.input_message_content = None.
I.e. if the optional input_message_content is not passed, the class instance won't have such an attribute.

I'm not sure that this is acutally wanted behavior, but changing that would surely be the content of another PR …

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, above mentioned issue is solved by #1600. But this is still needed, since e.g. InlineQueryResultGame has neither a parse_mode nor ar input_message_content attribute.

@Poolitzer
Copy link
Member

Poolitzer commented Oct 10, 2019

What I saw from my little dive into your changes is that this is a good solutions which tackels all the problems we had, everything is covered. If the other maintainer agree, we can built the other default ideas we had (timeout, disable web preview mainly) on top of this. Lets use #1527 to track this if we come so far. Good job though, I really like this :)

Since this is such a big change and I bet that there will be a lot of discussion around this, I feel like we should give it it's own version, so I added it to the 12.3 milestone.

@Poolitzer Poolitzer added this to the 12.3 milestone Oct 10, 2019
@jh0ker
Copy link
Member

jh0ker commented Oct 11, 2019

Hey,

I was wondering if it was possible to move this check:

https://github.com/python-telegram-bot/python-telegram-bot/pull/1490/files#diff-3b086505b88889eca9352d47789b348dL101-L102

from all the different Input classes into this check:

https://github.com/python-telegram-bot/python-telegram-bot/pull/1490/files#diff-56fa4ffcaf1325dae7804deba7d447afR1467-R1473

so that we wouldn't have as much code duplication.

@jh0ker
Copy link
Member

jh0ker commented Oct 11, 2019

Regarding the Bot class, i had the idea of using a factory function to make the defaults a bit nicer.

Assuming we want to be able to have defaults for other parameters, I suggest a Defaults class like this:

class Defaults():
    def __init__(parse_mode=None, disable_notification=False):
        self.parse_mode = parse_mode
        self.disable_notification = disable_notification

Then, we can use it like this:

def Bot(token, base_url=None, base_file_url=None, request=None, private_key=None,
        private_key_password=None, defaults=Defaults()):
    class Bot(TelegramObject):
        [...]

        @log
        def send_message(self,
                        chat_id,
                        text,
                        parse_mode=defaults.parse_mode,
                        disable_web_page_preview=None,
                        disable_notification=defaults.disable_notification,
                        reply_to_message_id=None,
                        reply_markup=None,
                        timeout=None,
                        **kwargs):

        [...]

    return Bot(token, base_url, base_file_url, request, private_key, private_key_password)

which means you can create a bot like Bot(..., defaults=Defaults(parse_mode='html')) and don't need any extra checks in all the api methods.

Now, i realize this seems a little janky with the factory and all, but i still wanted to put it out there as an idea.

@jh0ker jh0ker added the 📋 pending-reply work status: pending-reply label Oct 11, 2019
@Bibo-Joshi
Copy link
Member Author

Bibo-Joshi commented Oct 15, 2019

Your idea for a Defaults class looks interesting, @jh0ker, especially since it makes it easy to add more defaults.

My main concern is usability. To me it seems a bit harder to understand

def Bot(token, base_url=None, base_file_url=None, request=None, private_key=None,
        private_key_password=None, defaults=Defaults(parse_mode=MARKDOWN,disable_notification=True))

than

def Bot(token, base_url=None, base_file_url=None, request=None, private_key=None,
        private_key_password=None, default_parse_mode=MARKDOWN, default_disable_notification=True)

Still, I guess I'd put maintainability before usability 😄
How do the other @python-telegram-bot/developers fell about that?

@jsmnbom
Copy link
Member

jsmnbom commented Oct 15, 2019

Here's a solution that does the best of both worlds (maybe?)

import functools
import inspect


class Defaults:
    parse_mode = None
    disable_notification = False

class Bot:
    def __new__(cls, *args, **kwargs):
        # Transform default_x=y kwargs into Defaults.x=y
        defaults = Defaults()
        for kwarg in list(kwargs.keys()):
            if kwarg.startswith('default_'):
                defaults.__dict__[kwarg[8:]] = kwargs[kwarg]
                del kwargs[kwarg]

        # Make an instance of the class
        instance = super(Bot, cls).__new__(cls, *args, **kwargs)
        # For each method
        for method_name, method in inspect.getmembers(instance, predicate=inspect.ismethod):
            # Get kwargs
            argspec = inspect.getfullargspec(method)
            kwarg_names = argspec.args[-len(argspec.defaults or []):]
            # Check if Defaults has a attribute that matches the kwarg name
            needs_default = [kwarg_name for kwarg_name in kwarg_names if kwarg_name in defaults.__dict__.keys()]
            # Make a dict of kwarg name and the default value
            default_kwargs = {kwarg_name: defaults.__dict__[kwarg_name] for kwarg_name in needs_default}
            # Apply the defaults using a partial
            if default_kwargs:
                setattr(instance, method_name, functools.partial(method, **default_kwargs))
        return instance

    def __init__(self, *args, **kwargs):
        pass

    def test(self, parse_mode=None, disable_notification=False):
        print('parse_mode=', parse_mode, ' ', 'disable_notification=', disable_notification)

print('bot1')  # bot1
bot1 = Bot()
bot1.test()  # parse_mode= None   disable_notification= False
bot1.test(parse_mode='md')  # parse_mode= md   disable_notification= False
bot1.test(parse_mode='html')  # parse_mode= html   disable_notification= False
bot1.test(parse_mode=None)  # parse_mode= None   disable_notification= False
bot1.test(disable_notification=False)  # parse_mode= None   disable_notification= False
bot1.test(disable_notification=None)  # parse_mode= None   disable_notification= None
bot1.test(disable_notification=True)  # parse_mode= None   disable_notification= True

print('bot2')  # bot2
bot2 = Bot(default_parse_mode='md')
bot2.test()  # parse_mode= md   disable_notification= False
bot2.test(parse_mode='md')  # parse_mode= md   disable_notification= False
bot2.test(parse_mode='html')  # parse_mode= html   disable_notification= False
bot2.test(parse_mode=None)  # parse_mode= None   disable_notification= False
bot2.test(disable_notification=False)  # parse_mode= md   disable_notification= False
bot2.test(disable_notification=None)  # parse_mode= md   disable_notification= None
bot2.test(disable_notification=True)  # parse_mode= md   disable_notification= True

@jh0ker
Copy link
Member

jh0ker commented Oct 15, 2019

@jsmnbom Seems equally janky, if not more so, but it's really cool ^^
Probably still better than the factory cause we still have the one class so we don't break isinstance checks and similar.

@Bibo-Joshi
Copy link
Member Author

Bibo-Joshi commented Oct 16, 2019

Your approach looks good, @jsmnbom, but it only seems to work for bot methods that get the parse_mode (or other defaultable) argument directly. Most notably, I don't see how your solution would handle answer_inline_query:

default_parse_mode is not passed to answer_inline_query as parse_mode, since it has no such parameter. But say we modify your solution to pass default_parse_mode to all methods as kwarg. Then we still have the problem, that we can't defferentiate between

InlineQueryResultArticle(
            ...,
            input_message_content=InputTextMessageContent(
                `some text',
                parse_mode=None))

and

InlineQueryResultArticle(
            ...,
            input_message_content=InputTextMessageContent(
                `some text'))

within answer_inline_query.

Or am I missing something?

Copy link
Member

jh0ker commented Oct 16, 2019

@Bibo-Joshi We could still save the Defaults object in the Bot for that, couldn't we?

@Bibo-Joshi
Copy link
Member Author

Simply storing default_parse_mode as attribute for Bot (or as attribute for Bot.defaults, if you will) wouldn't suffice (If I'm understanding you correctly @jh0ker). We need to make sure that the default value for parse_mode is different from None, wherever parse_mode is a parameter. That's why I came up with the DefaultValue class …

@jh0ker
Copy link
Member

jh0ker commented Oct 16, 2019

@Bibo-Joshi In my mind, there are two different problems here:

  1. Setting the defaults for the methods on the Bot class
  2. Setting the defaults for the Input classes passed to AnswerInlineQuery

Your PR already handles the second problem pretty well, IMO, with the help of the DefaultValue class you introduced, and the one additional check for this special value inside Bot.answer_inline_query.
But it would be nice to have a similarly neat solution for the Bot class as well, where the code isn't spread out/duplicated as much as it is right now.

Is there a third case or am i missing some other problem with this?

@jsmnbom
Copy link
Member

jsmnbom commented Oct 16, 2019

Yea in my opinion the best way to do it is using the snippet i posted, but then keeping a lot of your code for the InlineQuery stuff ^^

@Bibo-Joshi
Copy link
Member Author

Okidok, then we're all on the same page :) Just wanted to clarify, before starting to work on it ;)
I'll go ahead and implement the changes.

@Bibo-Joshi
Copy link
Member Author

Bibo-Joshi commented Oct 16, 2019

mh … I implemented the changes @jsmnbom suggested for the default parameters, but now I'm having trouble adjusting the tests. Since I can't just alter bot.defaults.parse_mode, I do something like

setattr(bot, 'send_animation', functools.partial(bot.send_animation, **{'parse_mode': 'Markdown'}))

in the routines (and reset to 'None' afterwards). However that causes (at least i thinks that's the connection) other tests to fail. E.g. after test_animation.py fiddles with the bot, test_instance_method_send_animation in test_user.py will fail. As far as I can see this is because the mokeypatch.setattr('telegram.Bot.send_animation', test) doesn't work and user.send_animation still calls the actual method instead of the provided dummy.

Do you have any hint on how to handle this, @jh0ker @jsmnbom ?

Update:
I figured out that I can use

monkeypatch.setattr('telegram.Bot.send_animation, functools.partial(bot.send_animation, **{'parse_mode': 'Markdown'}))

instead of above snipped and that seems to work 💃
Will update the PR tomorrow :)

@Bibo-Joshi Bibo-Joshi requested a review from jh0ker October 17, 2019 09:35
@Bibo-Joshi Bibo-Joshi mentioned this pull request Jan 10, 2020
10 tasks
@Bibo-Joshi
Copy link
Member Author

telegram.Message should get a default value for quote. This can't be handled by the bot class, but we could just define a __new__ for Message as we did for Bot. on my todo list.

@Bibo-Joshi
Copy link
Member Author

Well, a __new__ method won't help at all. But adding an attribute to Message and passing the default_quote argument to Message.de_json (through all the classes the call the method, being WebhookServer, CallbackQuery, Chat, Bot and Update) should work.

@Bibo-Joshi
Copy link
Member Author

Bibo-Joshi commented Jan 20, 2020

Latest commits add default_disable_web_page_preview to InputTextMessagContent and default_quote for Message.reply_text and friends.

Regarding failed tests:

  • Codecov complains but iisc, only lines not changed in this PR are marked red
  • Codacy complains that WebhookHandler._default_quote is set in initialize instead of __init__, but this is also done for WebhookHandler.{bot, update_queue}
  • Pytest fails on test_pin_and_unpin_message even on multiple runs. Though I have added another test that pins and unpins (test_chat_data_default_quote), I don't see how this should be connected. Also, can't reproduce locally
  • test-official fails b/c API 4.5 is not yet merged

@Eldinnie Eldinnie merged commit 960c7a0 into master Feb 6, 2020
@Eldinnie Eldinnie deleted the default_parse_mode branch February 6, 2020 10:23
Eldinnie added a commit that referenced this pull request Feb 6, 2020
* Rename Test suite

* Actually change the badge in readme

* fix download without path arguments (#1591)

* fix download without path arguments

* fix download without path arguments

* solved downloading a file without file_path or custom_path

* if no file_path, download as file_id

* Add test case

* Elaborate doc string

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>

* Add default values (#1490)

* added parse_mode parameter in Updater and in Bot class to set a default parse mode for bot

* DefaultValue

* Add default parse_mode everywhere

* Renome to default_parse_mode

* Test def parse_mode for send_*, edit_message_*

* Remove duplicate code in Input* classes

* Improve handling of def_p_m for Bot methods

* Remove unneeded deletion of kwargs

* Make @log preserve signature, add bots with defaults to tests

* Fix Codacy

* Fix Travis Error

* Add default_disable_notification

* Add default_disable_web_page_preview

* Add default_disable_timeout

* add default_disable_web_page_preview for InputTextMessageContent

* add default_quote

* Try fixing test_pin_and_unpin

* Just run 3.5 tests for paranoia

* add tests for Defaults

* Revert "Just run 3.5 tests for paranoia"

This reverts commit 1baa91a.

* Tidy up parameters, move Defaults to ext

* Stage new files, because im an idiot

* remove debug prints

* change equality checks for DEFAULT_NONE

* Some last changes

* fix S&R error so that tests actually run

Co-authored-by: Ak4zh <agwl.akash@gmail.com>
Co-authored-by: Eldinnie <Eldinnie@users.noreply.github.com>

* Skip test relying on ordered dicts for 3.5 (#1752)

* Rename Test suite

* Actually change the badge in readme

Co-authored-by: Gabriel Simonetto <42247511+GabrielSimonetto@users.noreply.github.com>
Co-authored-by: Ak4zh <agwl.akash@gmail.com>
Co-authored-by: Eldinnie <Eldinnie@users.noreply.github.com>
@github-actions github-actions bot locked and limited conversation to collaborators Aug 19, 2020
@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 📋 pending-review work status: pending-review
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[FEATURE] Enter missing defaults at runtime
6 participants