Skip to content
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

Auto Pagination for answer_inline_query #2072

Merged
merged 7 commits into from
Sep 27, 2020
Merged

Conversation

Bibo-Joshi
Copy link
Member

As discussed in #2071.

Closes #2071

@Bibo-Joshi Bibo-Joshi changed the title Auto Pagination Auto Pagination for answer_inline_query Aug 27, 2020
@Bibo-Joshi
Copy link
Member Author

We might want to allow for pagination with less then 50 items per page. That should be useful for use cases where the computation of each results takes long.

@Bibo-Joshi
Copy link
Member Author

We might want to allow for pagination with less then 50 items per page. That should be useful for use cases where the computation of each results takes long.

Actually, in the case results is a callable, that's already covered and that's the interesting case for this use case anyway. I tweaked the tests to make that clear.

**kwargs):
"""
Use this method to send answers to an inline query. No more than 50 results per query are
allowed.

Args:
inline_query_id (:obj:`str`): Unique identifier for the answered query.
results (List[:class:`telegram.InlineQueryResult`)]: A list of results for the inline
query.
results (List[:class:`telegram.InlineQueryResult`] | Callable): A list of results for
Copy link
Contributor

Choose a reason for hiding this comment

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

Are generic type annotations possible for callable in docstrings?

Callable[[int], Optional[List[:class:`telegram.InlineResult`]]]

?

Copy link
Contributor

Choose a reason for hiding this comment

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

What happens when user returns empty list?

  • Clarify

Copy link
Member Author

Choose a reason for hiding this comment

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

E.g. for callbacks we also just state Callable and state the signature explicitly in the docstring (see Handler). Personally, I don't find Callable[[int], Optional[List[:class:telegram.InlineResult]]] overly readible and newbies will not understand it.

What happens when user returns empty list?

»or :obj:None, if there are no more results.« Is that ambiguous

Copy link
Contributor

Choose a reason for hiding this comment

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

There's also Protocol:

class ResultsGenerator(typing.Protocol):
    def __call__(self, current_index: int) -> typing.List[telegram.InlineResult]:
        ...

But would be quite revolutionary to add that, and even more verbose ;) So just FYI

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't find Callable[[int], Optional[List[:class:telegram.InlineResult]]] overly readible

Yea, generally that syntax is awful. Takes a bit of getting used to

Copy link
Contributor

Choose a reason for hiding this comment

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

»or :obj:None, if there are no more results.« Is that ambiguous

Maybe something along the lines of "or an expression that evaluates to False" (bad wording, I know, but you get the gist)

Copy link
Member Author

Choose a reason for hiding this comment

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

But would be quite revolutionary to add that, and even more verbose ;) So just FYI

good to know :)

Maybe something along the lines of "or an expression that evaluates to False" (bad wording, I know, but you get the gist)

I'd rather leave that as implementation detail. Documenting it might lead to problems, if the implementation needs to be changed and None is easier to graps for newbies

telegram/bot.py Outdated Show resolved Hide resolved
if current_offset == '':
current_offset = 0
else:
current_offset = int(current_offset)
Copy link
Contributor

Choose a reason for hiding this comment

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

Account for negative value with max(current_offset, 0)?

Copy link
Member Author

Choose a reason for hiding this comment

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

In what case would we have a negative value here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Defensive coding

Copy link
Member Author

Choose a reason for hiding this comment

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

IMHO it would just make debugging very hard, if invalid negative input lead to completely unexpected behaivour instead of leading to an exception thrown … I'll add a warning, that current_offset shouldn't usually be passed manually bot InlineQuery.answer(, auto_pagination=True) should take care of it.

telegram/bot.py Outdated Show resolved Hide resolved
telegram/bot.py Outdated Show resolved Hide resolved
telegram/bot.py Outdated
next_offset = current_offset + 1
results = results[
current_offset * MAX_INLINE_QUERY_RESULTS:
current_offset * MAX_INLINE_QUERY_RESULTS + MAX_INLINE_QUERY_RESULTS]
Copy link
Contributor

Choose a reason for hiding this comment

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

This could just use next_offset * MAX, right?

Copy link
Contributor

@JosXa JosXa Sep 13, 2020

Choose a reason for hiding this comment

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

Actually next_offset could be used in a few places here

Copy link
Member Author

Choose a reason for hiding this comment

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

I see how to use it here, but I don't see where else to use it …

@@ -385,6 +400,97 @@ def test(_, url, data, *args, **kwargs):
switch_pm_text='switch pm',
switch_pm_parameter='start_pm')

def test_answer_inline_query_current_offset_error(self, bot, inline_results):
Copy link
Contributor

@JosXa JosXa Sep 13, 2020

Choose a reason for hiding this comment

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

Looks like this could become a lot more concise using @pytest.mark.parametrize

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think I understand you here

Copy link
Contributor

Choose a reason for hiding this comment

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

https://docs.pytest.org/en/latest/parametrize.html#pytest-mark-parametrize

This generates new tests given a bunch of parameter combinations so that you don't need to make multiple assertions in a single test.

Copy link
Contributor

@JosXa JosXa Sep 20, 2020

Choose a reason for hiding this comment

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

Here's an example for classs-based test method parametrization:

class TestCase:

    @pytest.mark.parametrize("test_input,expected", [
    ("3+5", 8),
    ("2+4", 6),
    ("6*9", 42),  # fails
    ])
    def test_eval_combinations(self, test_input, expected):
        assert eval(test_input) == expected

Copy link
Member Author

Choose a reason for hiding this comment

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

I know that ;) I just don't see what you'd want to parametrize on this test

telegram/bot.py Outdated Show resolved Hide resolved
telegram/bot.py Outdated Show resolved Hide resolved
Comment on lines 81 to 84
bot.answer_inline_query(update.inline_query.id,
*args,
current_offset = self.offset if auto_pagination else None,
**kwargs)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
bot.answer_inline_query(update.inline_query.id,
*args,
current_offset = self.offset if auto_pagination else None,
**kwargs)
bot.answer_inline_query(
update.inline_query.id,
*args,
current_offset=self.offset if auto_pagination else None,
**kwargs,
)

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess that is a matter of taste … And I'd prefer to stick to the style we use in all the other docstrings

telegram/bot.py Outdated
Comment on lines 1599 to 1602
if not results:
results = []
else:
next_offset = current_offset + 1
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand why there is an else block here.

  • Explain

Copy link
Member Author

Choose a reason for hiding this comment

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

# callback returned None, i.e. there are no more results. We still need to answer the query to tell TG that.
# so we pass an empty results list along with next_offset = '', which tells TG to not ask again
if not results:    
    results = []
# callback returned more results, so we need to assume that there are results for the next page as well
# hence, increase next_offset. If there are no more results fo the next page, the next query will trigger 
# the `if not results` clause above
else:
    next_offset = current_offset + 1

Comment on lines +405 to +408
bot.answer_inline_query(1234,
results=inline_results,
next_offset=42,
current_offset=51)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
bot.answer_inline_query(1234,
results=inline_results,
next_offset=42,
current_offset=51)
bot.answer_inline_query(
1234,
results=inline_results,
next_offset=42,
current_offset=51
)

Copy link
Member Author

Choose a reason for hiding this comment

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

again, we use that style all over the place. Either we change the style completely or not at all, IMHO

next_offset=42,
current_offset=51)

def test_answer_inline_query_current_offset_1(self, monkeypatch, bot, inline_results):
Copy link
Contributor

Choose a reason for hiding this comment

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

Again a case for parametrization. There are mutliple assertions in this test.

Copy link
Member Author

Choose a reason for hiding this comment

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

here i see it

tests/test_inlinequery.py Outdated Show resolved Hide resolved
tests/test_inlinequery.py Outdated Show resolved Hide resolved
Copy link
Contributor

@JosXa JosXa left a comment

Choose a reason for hiding this comment

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

How about amending the inline query example? 😄

  • Think about adding an example

next_offset=42,
current_offset=51)

def test_answer_inline_query_current_offset_1(self, monkeypatch, bot, inline_results):
Copy link
Contributor

Choose a reason for hiding this comment

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

What's with the 1 / 2 numbers in the test names?

Copy link
Member Author

Choose a reason for hiding this comment

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

in test 2 I test that everything works out, if there are less than 50 results already in the first call, which is not covered by 1. Seemed a bit long to describe in a function name …

tests/test_inlinequery.py Outdated Show resolved Hide resolved
@Bibo-Joshi
Copy link
Member Author

How about amending the inline query example? 😄

  • Think about adding an example

Thanks for the thorough review! I left replies and resolved a few of the more obvious ones.
To the example: Integrating something into inlinebot.py is probably not a good idea, as it will confuse newbies, and I'm not sure if a standalone example is worth it … maybe it is. I'll think about it a bit

@Bibo-Joshi Bibo-Joshi merged commit 97adcdf into master Sep 27, 2020
@Bibo-Joshi Bibo-Joshi deleted the inline-pagination branch September 27, 2020 12:11
@github-actions github-actions bot locked and limited conversation to collaborators Sep 28, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[FEATURE] Auto-pagination for inline results
2 participants