From 92483af1fe4add04778377eb8e2da45de89a85ab Mon Sep 17 00:00:00 2001 From: Bibo-Joshi Date: Sun, 21 Feb 2021 11:13:11 +0100 Subject: [PATCH] Update PTB to v13.3 (#106) * Upgrade PTB version and make adjustments for Persistence * Update PTB to v13.3 * Use auto_pagination * Fix orchestra scores * Artificially up coverage * coverage --- bot/backup.py | 4 +-- bot/check_user_status.py | 4 +-- bot/editing.py | 31 +++++-------------- bot/error.py | 6 ++-- bot/inline.py | 18 +---------- bot/setup.py | 14 ++++++++- components/member.py | 15 ++++++--- components/orchestra.py | 31 +++++++++++++------ components/userscore.py | 26 +++++----------- main.py | 8 ++++- requirements.txt | 4 +-- tests/orchestra.py | 2 +- tests/test_attributemanager.py | 3 -- tests/test_member.py | 1 - tests/test_orchestra.py | 10 ------ tests/test_questioner.py | 56 +++++++++++++++++----------------- tests/test_userscore.py | 13 ++------ 17 files changed, 110 insertions(+), 136 deletions(-) diff --git a/bot/backup.py b/bot/backup.py index 69a9e87..a81deb7 100644 --- a/bot/backup.py +++ b/bot/backup.py @@ -41,9 +41,9 @@ def back_up(context: CallbackContext) -> None: def schedule_daily_job(dispatcher: Dispatcher) -> None: """ - Schedules a job running daily at 2AM UTC which run :meth:`check_users`. + Schedules a job running daily at 2AM which runs :meth:`back_up`. Args: dispatcher: The :class:`telegram.ext.Dispatcher`. """ - dispatcher.job_queue.run_daily(back_up, dtm.time(0, 0)) + dispatcher.job_queue.run_daily(back_up, dtm.time(2, 0)) diff --git a/bot/check_user_status.py b/bot/check_user_status.py index 7d48247..c7b045a 100644 --- a/bot/check_user_status.py +++ b/bot/check_user_status.py @@ -44,9 +44,9 @@ def check_users(context: CallbackContext) -> None: def schedule_daily_job(dispatcher: Dispatcher) -> None: """ - Schedules a job running daily at 2AM UTC which run :meth:`check_users`. + Schedules a job running daily at 2AM which runs :meth:`check_users`. Args: dispatcher: The :class:`telegram.ext.Dispatcher`. """ - dispatcher.job_queue.run_daily(check_users, datetime.time(0, 0)) + dispatcher.job_queue.run_daily(check_users, datetime.time(2, 0)) diff --git a/bot/editing.py b/bot/editing.py index 71a1e13..dd85827 100644 --- a/bot/editing.py +++ b/bot/editing.py @@ -4,7 +4,7 @@ import datetime as dtm import warnings from copy import deepcopy -from typing import Dict, Callable, List, Union +from typing import Dict, Callable, List from telegram import ( Update, @@ -30,7 +30,6 @@ InlineQueryHandler, BaseFilter, ) -from telegram.constants import MAX_INLINE_QUERY_RESULTS from bot import ( ORCHESTRA_KEY, @@ -371,34 +370,17 @@ def choose_member(update: Update, context: CallbackContext) -> str: inline_query = update.inline_query orchestra = context.bot_data[ORCHESTRA_KEY] - if inline_query.offset: - offset = int(inline_query.offset) - else: - offset = 0 - next_offset: Union[str, int] = '' - members = sorted(list(orchestra.members.values()), key=lambda member: member.full_name) - - # Telegram only likes up to 50 results - if len(members) > (offset + 1) * MAX_INLINE_QUERY_RESULTS: - next_offset = offset + 1 - members = members[ - offset * MAX_INLINE_QUERY_RESULTS : offset * MAX_INLINE_QUERY_RESULTS # noqa: E203 - + MAX_INLINE_QUERY_RESULTS - ] - else: - members = members[offset * MAX_INLINE_QUERY_RESULTS :] # noqa: E203 - results = [ InlineQueryResultArticle( - id=m.user_id, + id=f'edit {m.user_id}', title=m.full_name, input_message_content=InputTextMessageContent(m.user_id), ) for m in members ] - inline_query.answer(results=results, next_offset=next_offset) + inline_query.answer(results=results, auto_pagination=True) return CHOOSING_MEMBER @@ -967,9 +949,12 @@ def build_editing_handler(admin: int) -> ConversationHandler: MessageHandler((Filters.text & ~Filters.command), date_of_birth), CallbackQueryHandler(date_of_birth), ], - ADDRESS: [MessageHandler(ADDRESS_FILTER, address), CallbackQueryHandler(address)], + ADDRESS: [ + MessageHandler(ADDRESS_FILTER, address, run_async=True), + CallbackQueryHandler(address), + ], ADDRESS_CONFIRMATION: [ - MessageHandler(ADDRESS_FILTER, address), + MessageHandler(ADDRESS_FILTER, address, run_async=True), CallbackQueryHandler(address), ], PHOTO: [ diff --git a/bot/error.py b/bot/error.py index 4565f6a..8da88b3 100644 --- a/bot/error.py +++ b/bot/error.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) -def handle_error(update: Update, context: CallbackContext) -> None: +def handle_error(update: object, context: CallbackContext) -> None: """ Informs the originator of the update that an error occurred and forwards the traceback to the admin. @@ -29,7 +29,7 @@ def handle_error(update: Update, context: CallbackContext) -> None: logger.error(msg="Exception while handling an update:", exc_info=context.error) # Inform sender of update, that something went wrong - if update and update.effective_message: + if isinstance(update, Update) and update.effective_message: text = emojize( 'Huch, da ist etwas schief gelaufen :worried:. Ich melde es dem Hirsch :nerd_face:.', use_aliases=True, @@ -42,7 +42,7 @@ def handle_error(update: Update, context: CallbackContext) -> None: # Gather information from the update payload = '' - if update: + if isinstance(update, Update): if update.effective_user: payload += ' with the user {}'.format( mention_html(update.effective_user.id, update.effective_user.first_name) diff --git a/bot/inline.py b/bot/inline.py index 446306f..49a5f89 100644 --- a/bot/inline.py +++ b/bot/inline.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """This module contains functions for the inline mode.""" -from typing import Union from telegram import ( Update, @@ -42,12 +41,6 @@ def search_users(update: Update, context: CallbackContext) -> None: admin_id = context.bot_data[ADMIN_KEY] user_id = update.effective_user.id - if inline_query.offset: - offset = int(inline_query.offset) - else: - offset = 0 - next_offset: Union[str, int] = '' - members = [ m for uid, m in orchestra.members.items() @@ -59,15 +52,6 @@ def search_users(update: Update, context: CallbackContext) -> None: else: sorted_members = sorted(members, key=lambda m: m.compare_full_name_to(query), reverse=True) - # Telegram only likes up to 50 results - if len(sorted_members) > (offset + 1) * MEMBERS_PER_PAGE: - next_offset = offset + 1 - sorted_members = sorted_members[ - offset * MEMBERS_PER_PAGE : offset * MEMBERS_PER_PAGE + MEMBERS_PER_PAGE # noqa: E203 - ] - else: - sorted_members = sorted_members[offset * MEMBERS_PER_PAGE :] # noqa: E203 - results = [ InlineQueryResultArticle( id=m.user_id, @@ -84,9 +68,9 @@ def search_users(update: Update, context: CallbackContext) -> None: inline_query.answer( results=results, - next_offset=next_offset, switch_pm_text='Hilfe', switch_pm_parameter=INLINE_HELP, + auto_pagination=True, ) diff --git a/bot/setup.py b/bot/setup.py index f0b3c3f..7b43d8f 100644 --- a/bot/setup.py +++ b/bot/setup.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- """This module contains functions for setting up to bot at start up.""" import re +import warnings from typing import List, Union, Dict from telegram import BotCommand, Update @@ -57,6 +58,10 @@ ] """List[:class:`telegram.BotCommand`]: A list of commands of the bot.""" +warnings.filterwarnings( + 'ignore', message="BasePersistence.", module='telegram.ext.basepersistence' +) + def setup( # pylint: disable=R0913,R0914,R0915 dispatcher: Dispatcher, @@ -198,7 +203,9 @@ def clear_conversation_status(update: Update, context: CallbackContext) -> None: CommandHandler('start', registration.start, filters=Filters.text('/start')) ) dispatcher.add_handler( - CallbackQueryHandler(registration.request_registration, pattern=REGISTRATION_PATTERN) + CallbackQueryHandler( + registration.request_registration, pattern=REGISTRATION_PATTERN, run_async=True + ) ) dispatcher.add_handler(registration.ACCEPT_REGISTRATION_HANDLER) dispatcher.add_handler(registration.DENY_REGISTRATION_HANDLER) @@ -257,6 +264,11 @@ def clear_conversation_status(update: Update, context: CallbackContext) -> None: bot_data = dispatcher.bot_data if not bot_data.get(ORCHESTRA_KEY): bot_data[ORCHESTRA_KEY] = Orchestra() + else: + # We rebuild the orchestra on start up to make sure code changes are applied + old_orchestra = bot_data.pop(ORCHESTRA_KEY) + new_orchestra = old_orchestra.copy() + bot_data[ORCHESTRA_KEY] = new_orchestra if not bot_data.get(PENDING_REGISTRATIONS_KEY): bot_data[PENDING_REGISTRATIONS_KEY] = dict() if not bot_data.get(DENIED_USERS_KEY): diff --git a/components/member.py b/components/member.py index 49fb7b8..13ea83c 100644 --- a/components/member.py +++ b/components/member.py @@ -136,7 +136,7 @@ def __init__( self.date_of_birth = date_of_birth self.photo_file_id = photo_file_id self.allow_contact_sharing = allow_contact_sharing - self.user_score = UserScore(self) + self.user_score = UserScore() # See https://github.com/python/mypy/issues/3004 self._instruments: List[Instrument] = [] @@ -638,15 +638,22 @@ def copy(self) -> 'Member': """ Returns: A (deep) copy of this member. """ + # for backwards compatibility + if not hasattr(self, '_functions'): + self._functions = [] # pylint: disable=W0212 # type: ignore + if hasattr(self.user_score, 'member'): + del self.user_score.member # type: ignore # pylint: disable=E1101 # pragma: no cover + for score in self.user_score._high_score.values(): # pylint: disable=W0212 # type: ignore + if hasattr(score, 'member'): # pragma: no cover + del score.member # pragma: no cover + new_member = copy.deepcopy(self) - # for backwards compatibility@ - if not hasattr(new_member, '_functions'): - new_member._functions = [] # pylint: disable=W0212 # type: ignore if not hasattr(new_member, 'joined'): new_member.joined = None new_member.instruments = [ i for i in new_member.instruments if i in self.ALLOWED_INSTRUMENTS ] + return new_member @classmethod diff --git a/components/orchestra.py b/components/orchestra.py index 53cf344..04c43fb 100644 --- a/components/orchestra.py +++ b/components/orchestra.py @@ -186,14 +186,16 @@ def _score(self, attr: str) -> List[Score]: else: attr = f'{attr}s_score' - return sorted( - [ - getattr(m.user_score, attr) - for m in self.members.values() - if getattr(m.user_score, attr).answers > 0 - ], - reverse=True, - ) # noqa: E126 + scores = { + member: getattr(member.user_score, attr) + for member in self.members.values() + if getattr(member.user_score, attr).answers > 0 + } + membered_scores = [] + for member, score in scores.items(): + score.member = member + membered_scores.append(score) + return sorted(membered_scores, reverse=True) def _score_text(self, attr: str, length: int = None, html: Optional[bool] = False) -> str: sorted_scores = self._score(attr) @@ -342,6 +344,17 @@ def overall_score_text(self, length: int = None, html: Optional[bool] = False) - """ return self._score_text('overall', length=length, html=html) + def copy(self) -> 'Orchestra': + """ + Returns a (deep) copy of this orchestra. + """ + new_orchestra = self.__class__() + + for member in self.members.values(): + new_orchestra.register_member(member.copy()) + + return new_orchestra + def __eq__(self, other: object) -> bool: return False @@ -364,7 +377,7 @@ def __eq__(self, other: object) -> bool: 'birthdays': 'Geburtstag', 'birthday': 'Geburtstag', 'photo_file_ids': 'Foto', - 'photo_file_id': 'Photo', + 'photo_file_id': 'Foto', } """Dict[:obj:`str`, :obj:`str`]: A map from the names of the different properties of this class to the human readable strings.""" diff --git a/components/userscore.py b/components/userscore.py index 0efff67..fcde4bb 100644 --- a/components/userscore.py +++ b/components/userscore.py @@ -4,16 +4,12 @@ import datetime as dt from threading import Lock -from typing import Dict, TYPE_CHECKING +from typing import Dict from collections import defaultdict from components import PicklableBase from components import Score -# We don't like circular imports -if TYPE_CHECKING: - from components import Member - class UserScore(PicklableBase): """ @@ -21,22 +17,16 @@ class UserScore(PicklableBase): instances are subscriptable: For each date ``day``, ``score[day]`` is a :class:`components.Score` instance with the number of answers and correct answers given by the user on that day. To add values, :meth:`add_to_score` should be the preferred method. - - Attributes: - member (:class:`components.Member`): The member, this high score is associated with. - - Args: - member: The member, this high score is associated with. """ - def __init__(self, member: 'Member') -> None: - self.member = member - + def __init__(self) -> None: self._high_score_lock = Lock() - self._high_score: Dict[dt.date, Score] = defaultdict(self._default_factory) + self._high_score: Dict[dt.date, Score] = defaultdict(Score) - def _default_factory(self: 'UserScore') -> Score: - return Score(member=self.member) + @staticmethod + def _default_factory() -> Score: + # needed for backwards compatibility only. Can be dropped in future versions + return Score() # pragma: no cover def __getitem__(self, date: dt.date) -> Score: with self._high_score_lock: @@ -72,7 +62,7 @@ def todays_score(self) -> Score: return self[dt.date.today()] def _cumulative_score(self, start: dt.date = None) -> Score: - c_score = Score(member=self.member) + c_score = Score() with self._high_score_lock: for date, score in self._high_score.items(): diff --git a/main.py b/main.py index 111b8c3..edfe891 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,8 @@ """The script that runs the bot.""" import logging from configparser import ConfigParser + +import pytz from telegram import ParseMode from telegram.ext import Updater, PicklePersistence, Defaults @@ -37,7 +39,11 @@ def main() -> None: # Create the Updater and pass it your bot's token. # Make sure to set use_context=True to use the new context based callbacks # Post version 12 this will no longer be necessary - defaults = Defaults(parse_mode=ParseMode.HTML, disable_notification=True) + defaults = Defaults( + parse_mode=ParseMode.HTML, + disable_notification=True, + tzinfo=pytz.timezone('Europe/Berlin') + ) persistence = PicklePersistence('akanamen_db', single_file=False) updater = Updater(token, use_context=True, persistence=persistence, defaults=defaults) diff --git a/requirements.txt b/requirements.txt index 1c6e531..efe0420 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -python-telegram-bot[json]==12.8 +python-telegram-bot[json]==13.3 emoji geopy==1.21.0 vobject==0.9.6.1 @@ -7,6 +7,6 @@ python-dateutil==2.8.1 camelot-py[cv] numpy pandas -git+https://gitlab.com/HirschHeissIch/ptbstats.git@v1.2 +git+https://gitlab.com/HirschHeissIch/ptbstats.git@v1.3 pyocclient==0.6 git+https://github.com/Bibo-Joshi/yourls-python.git@extensions diff --git a/tests/orchestra.py b/tests/orchestra.py index 97f6a66..967fabd 100644 --- a/tests/orchestra.py +++ b/tests/orchestra.py @@ -190,7 +190,7 @@ def date_of_birth(): def instrument(): number = random.randint(1, 3) instruments_ = random.sample( - [i() for i in instruments.__dict__.values() if isinstance(i, type)], number + [i() for i in list(instruments.__dict__.values()) if isinstance(i, type)], number ) return value_or_none(instruments_) diff --git a/tests/test_attributemanager.py b/tests/test_attributemanager.py index 7fc03e1..76f0fbb 100644 --- a/tests/test_attributemanager.py +++ b/tests/test_attributemanager.py @@ -104,19 +104,16 @@ def test_register_member_with_attribute(self, member): am.register_member(member) assert am.data == {'test1': {member}} assert all(member is not m for m in am.data['test1']) - assert all(member.user_score.member is not m.user_score.member for m in am.data['test1']) member2 = Member(2, last_name='test2') am.register_member(member2) assert am.data == {'test1': {member}, 'test2': {member2}} assert all(member2 is not m for m in am.data['test2']) - assert all(member2.user_score.member is not m.user_score.member for m in am.data['test1']) member3 = Member(4, last_name='test1') am.register_member(member3) assert am.data == {'test1': {member, member3}, 'test2': {member2}} assert all(member3 is not m for m in am.data['test1']) - assert all(member3.user_score.member is not m.user_score.member for m in am.data['test1']) def test_double_register(self, member): member.last_name = 'test' diff --git a/tests/test_member.py b/tests/test_member.py index 1fd8821..1848a06 100644 --- a/tests/test_member.py +++ b/tests/test_member.py @@ -98,7 +98,6 @@ def test_all_args(self, monkeypatch): assert member.functions == self.functions assert member['functions'] == self.functions assert isinstance(member.user_score, UserScore) - assert member.user_score.member == member assert member.address == 'Universitätsplatz 2, 38106 Braunschweig' assert member['address'] == 'Universitätsplatz 2, 38106 Braunschweig' diff --git a/tests/test_orchestra.py b/tests/test_orchestra.py index 99b4f80..8890c27 100644 --- a/tests/test_orchestra.py +++ b/tests/test_orchestra.py @@ -82,7 +82,6 @@ def test_register_and_update_member(self, orchestra, member, today, monkeypatch) orchestra.register_member(member) assert orchestra.members == {123456: member} assert orchestra.members[123456] is not member - assert orchestra.members[123456].user_score.member is not member assert orchestra.attribute_managers['first_name'].male_data == {'first_name': {member}} assert orchestra.attribute_managers['last_name'].data == {'last_name': {member}} assert orchestra.attribute_managers['nickname'].data == {'nickname': {member}} @@ -122,7 +121,6 @@ def test_register_and_update_member(self, orchestra, member, today, monkeypatch) orchestra.update_member(member) assert orchestra.members == {123456: member} assert orchestra.members[123456] is not member - assert orchestra.members[123456].user_score.member is not member assert orchestra.attribute_managers['first_name'].female_data == {'First_name': {member}} assert orchestra.attribute_managers['last_name'].data == {'Last_name': {member}} assert orchestra.attribute_managers['nickname'].data == {'Nickname': {member}} @@ -159,22 +157,14 @@ def test_scores(self, today): overall_score = score_orchestra(today).overall_score for score in [todays_score, weeks_score, months_score, years_score]: - assert score[0].member == Member(1) assert score[0] == Score(8, 4) - assert score[1].member == Member(2) assert score[1] == Score(4, 2) - assert score[2].member == Member(3) assert score[2] == Score(3, 1) - assert score[3].member == Member(4) assert score[3] == Score(4, 1) - assert overall_score[0].member == Member(2) assert overall_score[0] == Score(14, 12) - assert overall_score[1].member == Member(3) assert overall_score[1] == Score(13, 11) - assert overall_score[2].member == Member(4) assert overall_score[2] == Score(14, 11) - assert overall_score[3].member == Member(1) assert overall_score[3] == Score(18, 14) def test_score_texts_empty(self, today, orchestra): diff --git a/tests/test_questioner.py b/tests/test_questioner.py index 0cae637..9800fc4 100644 --- a/tests/test_questioner.py +++ b/tests/test_questioner.py @@ -18,10 +18,10 @@ def empty_orchestra(): def fake_poll(): return Message( - 123, - None, - None, - None, + message_id=123, + from_user=None, + chat=None, + date=None, poll=Poll( random.randint(100, 500), 'question', @@ -304,10 +304,10 @@ def send_pass(*args, **kwargs): update = Update( 123, message=Message( - 123, - User(123, 'foo', False), - None, - Chat(123, Chat.PRIVATE), + message_id=123, + from_user=User(123, 'foo', False), + chat=Chat(123, Chat.PRIVATE), + date=None, text='some very false answer', bot=bot, ), @@ -328,10 +328,10 @@ def send_pass(*args, **kwargs): update = Update( 123, message=Message( - 123, - User(chat_id, 'foo', False), - None, - Chat(chat_id, Chat.PRIVATE), + message_id=123, + from_user=User(chat_id, 'foo', False), + date=None, + chat=Chat(chat_id, Chat.PRIVATE), text='some very false answer', bot=bot, ), @@ -362,10 +362,10 @@ def send_pass(*args, **kwargs): update = Update( 123, message=Message( - 123, - User(chat_id, 'foo', False), - None, - Chat(chat_id, Chat.PRIVATE), + message_id=123, + from_user=User(chat_id, 'foo', False), + date=None, + chat=Chat(chat_id, Chat.PRIVATE), text=text, bot=bot, ), @@ -421,10 +421,10 @@ def send_pass(*args, **kwargs): update = Update( 123, message=Message( - 123, - User(123, 'foo', False), - None, - Chat(123, Chat.PRIVATE), + message_id=123, + from_user=User(123, 'foo', False), + date=None, + chat=Chat(123, Chat.PRIVATE), location=Location(27.988191, 86.924518), bot=bot, ), @@ -435,10 +435,10 @@ def send_pass(*args, **kwargs): update = Update( 123, message=Message( - 123, - User(chat_id, 'foo', False), - None, - Chat(chat_id, Chat.PRIVATE), + message_id=123, + from_user=User(chat_id, 'foo', False), + date=None, + chat=Chat(chat_id, Chat.PRIVATE), location=Location(27.988191, 86.924518), bot=bot, ), @@ -456,10 +456,10 @@ def send_pass(*args, **kwargs): update = Update( 123, message=Message( - 123, - User(chat_id, 'foo', False), - None, - Chat(chat_id, Chat.PRIVATE), + message_id=123, + from_user=User(chat_id, 'foo', False), + date=None, + chat=Chat(chat_id, Chat.PRIVATE), location=Location(longitude, latitude), bot=bot, ), diff --git a/tests/test_userscore.py b/tests/test_userscore.py index 657d7cc..6ef23e7 100644 --- a/tests/test_userscore.py +++ b/tests/test_userscore.py @@ -1,22 +1,18 @@ #!/usr/bin/env python import pytest import datetime as dt -from components import Score, UserScore, Member +from components import Score, UserScore @pytest.fixture(scope='function') def us(): - return UserScore(Member(123)) + return UserScore() class TestUserScore: - def test_init(self, us): - assert us.member == Member(123) - def test_subscriptable(self, us, today): score = us[today] assert isinstance(score, Score) - assert score.member == Member(123) score.answers = 7 score.correct = 5 @@ -59,7 +55,6 @@ def test_todays_score(self, us, today): assert score.answers == 5 assert score.correct == 3 - assert score.member == us.member def test_weeks_score(self, us, today): us.add_to_score(answers=4, correct=3, date=today - dt.timedelta(days=10)) @@ -69,7 +64,6 @@ def test_weeks_score(self, us, today): assert us.weeks_score assert us.weeks_score.answers == 8 assert us.weeks_score.correct == 4 - assert us.weeks_score.member == us.member def test_months_score(self, us, today): us.add_to_score(answers=5, correct=3, date=today - dt.timedelta(weeks=6)) @@ -79,7 +73,6 @@ def test_months_score(self, us, today): assert us.months_score assert us.months_score.answers == 8 assert us.months_score.correct == 4 - assert us.months_score.member == us.member def test_years_score(self, us, today): us.add_to_score(answers=5, correct=3, date=today - dt.timedelta(weeks=53)) @@ -89,11 +82,9 @@ def test_years_score(self, us, today): assert us.years_score assert us.years_score.answers == 8 assert us.years_score.correct == 4 - assert us.years_score.member == us.member def test_overall_score(self, us, today): us.add_to_score(answers=5, correct=3, date=today - dt.timedelta(weeks=53)) assert us.overall_score assert us.overall_score.answers == 5 assert us.overall_score.correct == 3 - assert us.years_score.member == us.member