From 55833054a9fd84d6d9a254596bf125444d3a97c0 Mon Sep 17 00:00:00 2001 From: Alexander Zveruk Date: Sat, 27 Jun 2020 23:31:19 +0300 Subject: [PATCH 1/7] add more strict types checks --- .github/workflows/main.yml | 2 +- tg/controllers.py | 165 ++++++++++++++++++++----------------- tg/main.py | 8 +- tg/models.py | 54 ++++++------ tg/msg.py | 36 ++++---- tg/tdlib.py | 19 +++-- tg/update_handlers.py | 70 ++++++++++------ tg/utils.py | 85 ++++++++++++------- tg/views.py | 20 ++--- 9 files changed, 262 insertions(+), 197 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8ff236e..458ba6c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -44,4 +44,4 @@ jobs: - name: Check types with mypy run: | - mypy tg --warn-redundant-casts --warn-unused-ignores --no-warn-no-return --warn-unreachable --strict-equality --ignore-missing-imports + mypy tg --warn-redundant-casts --warn-unused-ignores --no-warn-no-return --warn-unreachable --strict-equality --ignore-missing-imports --warn-unused-configs --disallow-untyped-calls --disallow-untyped-defs --disallow-incomplete-defs --check-untyped-defs --disallow-untyped-decorators diff --git a/tg/controllers.py b/tg/controllers.py index 43b52ea..3f7d756 100644 --- a/tg/controllers.py +++ b/tg/controllers.py @@ -9,6 +9,8 @@ from queue import Queue from tempfile import NamedTemporaryFile from typing import Any, Callable, Dict, List, Optional +from telegram.utils import AsyncResult + from tg import config from tg.models import Model from tg.msg import MsgProxy @@ -32,26 +34,26 @@ log = logging.getLogger(__name__) # cause blan areas on the msg display screen MSGS_LEFT_SCROLL_THRESHOLD = 2 REPLY_MSG_PREFIX = "# >" -handler_type = Callable[[Any], Any] +HandlerType = Callable[[Any], Optional[str]] -chat_handler: Dict[str, handler_type] = {} -msg_handler: Dict[str, handler_type] = {} +chat_handler: Dict[str, HandlerType] = {} +msg_handler: Dict[str, HandlerType] = {} def bind( - binding: Dict[str, handler_type], + binding: Dict[str, HandlerType], keys: List[str], repeat_factor: bool = False, -): +) -> Callable: """bind handlers to given keys""" - def decorator(fun): + def decorator(fun: Callable) -> HandlerType: @wraps(fun) - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> Any: return fun(*args, **kwargs) @wraps(fun) - def _no_repeat_factor(self, repeat_factor): + def _no_repeat_factor(self: Controller, _: bool) -> Any: return fun(self) for key in keys: @@ -72,9 +74,8 @@ class Controller: self.chat_size = 0.5 @bind(msg_handler, ["o"]) - def open_url(self): - msg = self.model.current_msg - msg = MsgProxy(msg) + def open_url(self) -> None: + msg = MsgProxy(self.model.current_msg) if not msg.is_text: self.present_error("Does not contain urls") return @@ -97,41 +98,42 @@ class Controller: with suspend(self.view) as s: s.run_with_input(config.URL_VIEW, "\n".join(urls)) - def format_help(self, bindings): + @staticmethod + def format_help(bindings: Dict[str, HandlerType]) -> str: return "\n".join( f"{key}\t{fun.__name__}\t{fun.__doc__ or ''}" for key, fun in sorted(bindings.items()) ) @bind(chat_handler, ["?"]) - def show_chat_help(self): + def show_chat_help(self) -> None: _help = self.format_help(chat_handler) with suspend(self.view) as s: s.run_with_input(config.HELP_CMD, _help) @bind(msg_handler, ["?"]) - def show_msg_help(self): + def show_msg_help(self) -> None: _help = self.format_help(msg_handler) with suspend(self.view) as s: s.run_with_input(config.HELP_CMD, _help) @bind(chat_handler, ["bp"]) @bind(msg_handler, ["bp"]) - def breakpoint(self): + def breakpoint(self) -> None: with suspend(self.view): breakpoint() @bind(chat_handler, ["q"]) @bind(msg_handler, ["q"]) - def quit(self): + def quit(self) -> str: return "QUIT" @bind(msg_handler, ["h", "^D"]) - def back(self): + def back(self) -> str: return "BACK" @bind(msg_handler, ["p"]) - def forward_msgs(self): + def forward_msgs(self) -> None: """Paste yanked msgs""" if not self.model.forward_msgs(): self.present_error("Can't forward msg(s)") @@ -139,7 +141,7 @@ class Controller: self.present_info("Forwarded msg(s)") @bind(msg_handler, ["y"]) - def yank_msgs(self): + def yank_msgs(self) -> None: """Copy msgs to clipboard and internal buffer to forward""" chat_id = self.model.chats.id_by_index(self.model.current_chat) if not chat_id: @@ -154,7 +156,7 @@ class Controller: self.present_info(f"Copied {len(msg_ids)} msg(s)") @bind(msg_handler, [" "]) - def toggle_select_msg(self): + def toggle_select_msg(self) -> None: chat_id = self.model.chats.id_by_index(self.model.current_chat) if not chat_id: return @@ -168,7 +170,7 @@ class Controller: self.render_msgs() @bind(msg_handler, ["^G", "^["]) - def discard_selected_msgs(self): + def discard_selected_msgs(self) -> None: chat_id = self.model.chats.id_by_index(self.model.current_chat) if not chat_id: return @@ -177,34 +179,36 @@ class Controller: self.present_info("Discarded selected messages") @bind(msg_handler, ["G"]) - def bottom_msg(self): + def bottom_msg(self) -> None: if self.model.jump_bottom(): self.render_msgs() @bind(msg_handler, ["j", "^B", "^N"], repeat_factor=True) - def next_msg(self, repeat_factor: int = 1): + def next_msg(self, repeat_factor: int = 1) -> None: if self.model.next_msg(repeat_factor): self.render_msgs() @bind(msg_handler, ["J"]) - def jump_10_msgs_down(self): + def jump_10_msgs_down(self) -> None: self.next_msg(10) @bind(msg_handler, ["k", "^C", "^P"], repeat_factor=True) - def prev_msg(self, repeat_factor: int = 1): + def prev_msg(self, repeat_factor: int = 1) -> None: if self.model.prev_msg(repeat_factor): self.render_msgs() @bind(msg_handler, ["K"]) - def jump_10_msgs_up(self): + def jump_10_msgs_up(self) -> None: self.prev_msg(10) @bind(msg_handler, ["r"]) - def reply_message(self): + def reply_message(self) -> None: if not self.can_send_msg(): self.present_info("Can't send msg in this chat") return chat_id = self.model.current_chat_id + if chat_id is None: + return reply_to_msg = self.model.current_msg_id if msg := self.view.status.get_input(): self.tg.reply_message(chat_id, reply_to_msg, msg) @@ -213,11 +217,13 @@ class Controller: self.present_info("Message reply wasn't sent") @bind(msg_handler, ["R"]) - def reply_with_long_message(self): + def reply_with_long_message(self) -> None: if not self.can_send_msg(): self.present_info("Can't send msg in this chat") return chat_id = self.model.current_chat_id + if chat_id is None: + return reply_to_msg = self.model.current_msg_id msg = MsgProxy(self.model.current_msg) with NamedTemporaryFile("w+", suffix=".txt") as f, suspend( @@ -227,14 +233,14 @@ class Controller: f.seek(0) s.call(config.LONG_MSG_CMD.format(file_path=shlex.quote(f.name))) with open(f.name) as f: - if msg := strip_replied_msg(f.read().strip()): - self.tg.reply_message(chat_id, reply_to_msg, msg) + if replied_msg := strip_replied_msg(f.read().strip()): + self.tg.reply_message(chat_id, reply_to_msg, replied_msg) self.present_info("Message sent") else: self.present_info("Message wasn't sent") @bind(msg_handler, ["a", "i"]) - def write_short_msg(self): + def write_short_msg(self) -> None: if not self.can_send_msg(): self.present_info("Can't send msg in this chat") return @@ -245,7 +251,7 @@ class Controller: self.present_info("Message wasn't sent") @bind(msg_handler, ["A", "I"]) - def write_long_msg(self): + def write_long_msg(self) -> None: if not self.can_send_msg(): self.present_info("Can't send msg in this chat") return @@ -259,7 +265,7 @@ class Controller: self.present_info("Message sent") @bind(msg_handler, ["sv"]) - def send_video(self): + def send_video(self) -> None: file_path = self.view.status.get_input() if not file_path or not os.path.isfile(file_path): return @@ -271,7 +277,7 @@ class Controller: self.tg.send_video(file_path, chat_id, width, height, duration) @bind(msg_handler, ["dd"]) - def delete_msgs(self): + def delete_msgs(self) -> None: is_deleted = self.model.delete_msgs() self.discard_selected_msgs() if not is_deleted: @@ -280,26 +286,28 @@ class Controller: self.present_info("Message deleted") @bind(msg_handler, ["sd"]) - def send_document(self): + def send_document(self) -> None: self.send_file(self.tg.send_doc) @bind(msg_handler, ["sp"]) - def send_picture(self): + def send_picture(self) -> None: self.send_file(self.tg.send_photo) @bind(msg_handler, ["sa"]) - def send_audio(self): + def send_audio(self) -> None: self.send_file(self.tg.send_audio) - def send_file(self, send_file_fun, *args, **kwargs): + def send_file( + self, send_file_fun: Callable[[str, int], AsyncResult], *args: Any, **kwargs: Any + ) -> None: file_path = self.view.status.get_input() if file_path and os.path.isfile(file_path): - chat_id = self.model.chats.id_by_index(self.model.current_chat) - send_file_fun(file_path, chat_id, *args, **kwargs) - self.present_info("File sent") + if chat_id := self.model.chats.id_by_index(self.model.current_chat): + send_file_fun(file_path, chat_id) + self.present_info("File sent") @bind(msg_handler, ["v"]) - def record_voice(self): + def record_voice(self) -> None: file_path = f"/tmp/voice-{datetime.now()}.oga" with suspend(self.view) as s: s.call( @@ -327,7 +335,7 @@ class Controller: self.present_info(f"Sent voice msg: {file_path}") @bind(msg_handler, ["D"]) - def download_current_file(self): + def download_current_file(self) -> None: msg = MsgProxy(self.model.current_msg) log.debug("Downloading msg: %s", msg.msg) file_id = msg.file_id @@ -337,7 +345,7 @@ class Controller: self.download(file_id, msg["chat_id"], msg["id"]) self.present_info("File started downloading") - def download(self, file_id: int, chat_id: int, msg_id: int): + def download(self, file_id: int, chat_id: int, msg_id: int) -> None: log.info("Downloading file: file_id=%s", file_id) self.model.downloads[file_id] = (chat_id, msg_id) self.tg.download_file(file_id=file_id) @@ -348,7 +356,7 @@ class Controller: return chat["permissions"]["can_send_messages"] @bind(msg_handler, ["l", "^J"]) - def open_current_msg(self): + def open_current_msg(self) -> None: msg = MsgProxy(self.model.current_msg) if msg.is_text: with NamedTemporaryFile("w", suffix=".txt") as f: @@ -370,7 +378,7 @@ class Controller: s.open_file(path) @bind(msg_handler, ["e"]) - def edit_msg(self): + def edit_msg(self) -> None: msg = MsgProxy(self.model.current_msg) log.info("Editing msg: %s", msg.msg) if not self.model.is_me(msg.sender_id): @@ -392,7 +400,7 @@ class Controller: self.present_info("Message edited") @bind(chat_handler, ["l", "^J", "^E"]) - def handle_msgs(self): + def handle_msgs(self) -> Optional[str]: rc = self.handle(msg_handler, 0.2) if rc == "QUIT": return rc @@ -400,32 +408,32 @@ class Controller: self.resize() @bind(chat_handler, ["g"]) - def top_chat(self): + def top_chat(self) -> None: if self.model.first_chat(): self.render() @bind(chat_handler, ["j", "^B", "^N"], repeat_factor=True) @bind(msg_handler, ["]"]) - def next_chat(self, repeat_factor: int = 1): + def next_chat(self, repeat_factor: int = 1) -> None: if self.model.next_chat(repeat_factor): self.render() @bind(chat_handler, ["k", "^C", "^P"], repeat_factor=True) @bind(msg_handler, ["["]) - def prev_chat(self, repeat_factor: int = 1): + def prev_chat(self, repeat_factor: int = 1) -> None: if self.model.prev_chat(repeat_factor): self.render() @bind(chat_handler, ["J"]) - def jump_10_chats_down(self): + def jump_10_chats_down(self) -> None: self.next_chat(10) @bind(chat_handler, ["K"]) - def jump_10_chats_up(self): + def jump_10_chats_up(self) -> None: self.prev_chat(10) @bind(chat_handler, ["u"]) - def toggle_unread(self): + def toggle_unread(self) -> None: chat = self.model.chats.chats[self.model.current_chat] chat_id = chat["id"] toggle = not chat["is_marked_as_unread"] @@ -433,7 +441,7 @@ class Controller: self.render() @bind(chat_handler, ["r"]) - def read_msgs(self): + def read_msgs(self) -> None: chat = self.model.chats.chats[self.model.current_chat] chat_id = chat["id"] msg_id = chat["last_message"]["id"] @@ -441,7 +449,7 @@ class Controller: self.render() @bind(chat_handler, ["m"]) - def toggle_mute(self): + def toggle_mute(self) -> None: # TODO: if it's msg to yourself, do not change its # notification setting, because we can't by documentation, # instead write about it in status @@ -459,7 +467,7 @@ class Controller: self.render() @bind(chat_handler, ["p"]) - def toggle_pin(self): + def toggle_pin(self) -> None: chat = self.model.chats.chats[self.model.current_chat] chat_id = chat["id"] toggle = not chat["is_pinned"] @@ -473,10 +481,10 @@ class Controller: except Exception: log.exception("Error happened in main loop") - def close(self): + def close(self) -> None: self.is_running = False - def handle(self, handlers: Dict[str, handler_type], size: float): + def handle(self, handlers: Dict[str, HandlerType], size: float) -> str: self.chat_size = size self.resize() @@ -489,15 +497,15 @@ class Controller: elif res == "BACK": return res - def resize_handler(self, signum, frame): + def resize_handler(self, signum: int, frame: Any) -> None: curses.endwin() self.view.stdscr.refresh() self.resize() - def resize(self): + def resize(self) -> None: self.queue.put(self._resize) - def _resize(self): + def _resize(self) -> None: rows, cols = self.view.stdscr.getmaxyx() # If we didn't clear the screen before doing this, # the original window contents would remain on the screen @@ -510,7 +518,7 @@ class Controller: self.view.status.resize(rows, cols) self.render() - def draw(self): + def draw(self) -> None: while self.is_running: try: log.info("Queue size: %d", self.queue.qsize()) @@ -519,16 +527,16 @@ class Controller: except Exception: log.exception("Error happened in draw loop") - def present_error(self, msg: str): + def present_error(self, msg: str) -> None: return self.update_status("Error", msg) - def present_info(self, msg: str): + def present_info(self, msg: str) -> None: return self.update_status("Info", msg) - def update_status(self, level: str, msg: str): + def update_status(self, level: str, msg: str) -> None: self.queue.put(partial(self._update_status, level, msg)) - def _update_status(self, level: str, msg: str): + def _update_status(self, level: str, msg: str) -> None: self.view.status.draw(f"{level}: {msg}") def render(self) -> None: @@ -566,10 +574,9 @@ class Controller: ) self.view.msgs.draw(current_msg_idx, msgs, MSGS_LEFT_SCROLL_THRESHOLD) - def notify_for_message(self, chat_id: int, msg: MsgProxy): + def notify_for_message(self, chat_id: int, msg: MsgProxy) -> None: # do not notify, if muted # TODO: optimize - chat = None for chat in self.model.chats.chats: if chat_id == chat["id"]: break @@ -587,10 +594,10 @@ class Controller: user = self.model.users.get_user(msg.sender_id) name = f"{user['first_name']} {user['last_name']}" - text = msg.text_content if msg.is_text else msg.content_type - notify(text, title=name) + if text := msg.text_content if msg.is_text else msg.content_type: + notify(text, title=name) - def _refresh_current_chat(self, current_chat_id: Optional[int]): + def _refresh_current_chat(self, current_chat_id: Optional[int]) -> None: if current_chat_id is None: return # TODO: we can create for chats, it's faster than sqlite anyway @@ -604,12 +611,16 @@ class Controller: def insert_replied_msg(msg: MsgProxy) -> str: - text = msg.text_content if msg.is_text else msg.content_type - return ( - "\n".join([f"{REPLY_MSG_PREFIX} {line}" for line in text.split("\n")]) - # adding line with whitespace so text editor could start editing from last line - + "\n " - ) + if text := msg.text_content if msg.is_text else msg.content_type: + return ( + "\n".join( + [f"{REPLY_MSG_PREFIX} {line}" for line in text.split("\n")] + ) + # adding line with whitespace so text editor could start editing from last line + + "\n " + ) + else: + return "" def strip_replied_msg(msg: str) -> str: diff --git a/tg/main.py b/tg/main.py index 05ac374..bd5ac5c 100644 --- a/tg/main.py +++ b/tg/main.py @@ -1,9 +1,9 @@ -import logging import logging.handlers import signal import threading from curses import window, wrapper # type: ignore from functools import partial +from types import FrameType from tg import config, update_handlers, utils from tg.controllers import Controller @@ -17,7 +17,7 @@ log = logging.getLogger(__name__) def run(tg: Tdlib, stdscr: window) -> None: # handle ctrl+c, to avoid interrupting tg when subprocess is called - def interrupt_signal_handler(sig, frame): + def interrupt_signal_handler(sig: int, frame: FrameType) -> None: # TODO: draw on status pane: to quite press log.info("Interrupt signal is handled and ignored on purpose.") @@ -36,14 +36,14 @@ def run(tg: Tdlib, stdscr: window) -> None: for msg_type, handler in update_handlers.handlers.items(): tg.add_update_handler(msg_type, partial(handler, controller)) - thread = threading.Thread(target=controller.run,) + thread = threading.Thread(target=controller.run) thread.daemon = True thread.start() controller.draw() -def main(): +def main() -> None: tg = Tdlib( api_id=config.API_ID, api_hash=config.API_HASH, diff --git a/tg/models.py b/tg/models.py index c46fe89..bcf4db0 100644 --- a/tg/models.py +++ b/tg/models.py @@ -1,7 +1,7 @@ import logging import time from collections import defaultdict -from typing import Any, Dict, List, Optional, Set, Tuple +from typing import Any, Dict, List, Optional, Set, Tuple, Union from tg.msg import MsgProxy from tg.tdlib import Tdlib @@ -11,7 +11,7 @@ log = logging.getLogger(__name__) class Model: - def __init__(self, tg: Tdlib): + def __init__(self, tg: Tdlib) -> None: self.tg = tg self.chats = ChatModel(tg) self.msgs = MsgModel(tg) @@ -21,13 +21,13 @@ class Model: self.selected: Dict[int, List[int]] = defaultdict(list) self.copied_msgs: Tuple[int, List[int]] = (0, []) - def get_me(self): + def get_me(self) -> Dict[str, Any]: return self.users.get_me() def is_me(self, user_id: int) -> bool: return self.get_me()["id"] == user_id - def get_user(self, user_id): + def get_user(self, user_id: int) -> Dict: return self.users.get_user(user_id) @property @@ -68,9 +68,10 @@ class Model: def current_msg_id(self) -> int: return self.current_msg["id"] - def jump_bottom(self): - chat_id = self.chats.id_by_index(self.current_chat) - return self.msgs.jump_bottom(chat_id) + def jump_bottom(self) -> bool: + if chat_id := self.chats.id_by_index(self.current_chat): + return self.msgs.jump_bottom(chat_id) + return False def next_chat(self, step: int = 1) -> bool: new_idx = self.current_chat + step @@ -85,17 +86,17 @@ class Model: self.current_chat = max(0, self.current_chat - step) return True - def first_chat(self): + def first_chat(self) -> bool: if self.current_chat != 0: self.current_chat = 0 return True return False - def view_current_msg(self): - chat_id = self.chats.id_by_index(self.current_chat) + def view_current_msg(self) -> None: msg = MsgProxy(self.current_msg) msg_id = msg["id"] - self.tg.view_messages(chat_id, [msg_id]) + if chat_id := self.chats.id_by_index(self.current_chat): + self.tg.view_messages(chat_id, [msg_id]) def next_msg(self, step: int = 1) -> bool: chat_id = self.chats.id_by_index(self.current_chat) @@ -120,7 +121,7 @@ class Model: current_position: int = 0, page_size: int = 10, msgs_left_scroll_threshold: int = 10, - ): + ) -> List[Dict[str, Any]]: chats_left = page_size - current_position offset = max(msgs_left_scroll_threshold - chats_left, 0) limit = offset + page_size @@ -181,7 +182,7 @@ class Model: self.copied_msgs = (0, []) return True - def copy_msgs_text(self): + def copy_msgs_text(self) -> bool: """Copies current msg text or path to file if it's file""" buffer = [] @@ -193,15 +194,16 @@ class Model: if not _msg: return False msg = MsgProxy(_msg) - if msg.file_id: + if msg.file_id and msg.local_path: buffer.append(msg.local_path) elif msg.is_text: buffer.append(msg.text_content) copy_to_clipboard("\n".join(buffer)) + return True class ChatModel: - def __init__(self, tg: Tdlib): + def __init__(self, tg: Tdlib) -> None: self.tg = tg self.chats: List[Dict[str, Any]] = [] self.chat_ids: List[int] = [] @@ -220,7 +222,7 @@ class ChatModel: return self.chats[offset:limit] - def _load_next_chats(self): + def _load_next_chats(self) -> None: """ based on https://github.com/tdlib/td/issues/56#issuecomment-364221408 @@ -282,7 +284,7 @@ class ChatModel: class MsgModel: - def __init__(self, tg: Tdlib): + def __init__(self, tg: Tdlib) -> None: self.tg = tg self.msgs: Dict[int, List[Dict]] = defaultdict(list) self.current_msgs: Dict[int, int] = defaultdict(int) @@ -296,7 +298,7 @@ class MsgModel: self.current_msgs[chat_id] = max(0, current_msg - step) return True - def jump_bottom(self, chat_id: int): + def jump_bottom(self, chat_id: int) -> bool: if self.current_msgs[chat_id] == 0: return False self.current_msgs[chat_id] = 0 @@ -323,7 +325,7 @@ class MsgModel: return result.update return next(iter(m for m in self.msgs[chat_id] if m["id"] == msg_id)) - def remove_message(self, chat_id: int, msg_id: int): + def remove_message(self, chat_id: int, msg_id: int) -> bool: msg_set = self.msg_ids[chat_id] if msg_id not in msg_set: return False @@ -335,7 +337,7 @@ class MsgModel: msg_set.remove(msg_id) return True - def update_msg_content_opened(self, chat_id: int, msg_id: int): + def update_msg_content_opened(self, chat_id: int, msg_id: int) -> None: for message in self.msgs[chat_id]: if message["id"] != msg_id: continue @@ -349,7 +351,9 @@ class MsgModel: # https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1update_message_content_opened.html return - def update_msg(self, chat_id: int, msg_id: int, **fields: Dict[str, Any]): + def update_msg( + self, chat_id: int, msg_id: int, **fields: Dict[str, Any] + ) -> bool: msg = None for message in self.msgs[chat_id]: if message["id"] == msg_id: @@ -472,11 +476,11 @@ class UserModel: def __init__(self, tg: Tdlib) -> None: self.tg = tg - self.me = None + self.me: Dict[str, Any] = {} self.users: Dict[int, Dict] = {} self.not_found: Set[int] = set() - def get_me(self): + def get_me(self) -> Dict[str, Any]: if self.me: return self.me result = self.tg.get_me() @@ -487,12 +491,12 @@ class UserModel: self.me = result.update return self.me - def set_status(self, user_id: int, status: Dict[str, Any]): + def set_status(self, user_id: int, status: Dict[str, Any]) -> None: if user_id not in self.users: self.get_user(user_id) self.users[user_id]["status"] = status - def is_online(self, user_id: int): + def is_online(self, user_id: int) -> bool: user = self.get_user(user_id) if ( user diff --git a/tg/msg.py b/tg/msg.py index 6922504..3343aaf 100644 --- a/tg/msg.py +++ b/tg/msg.py @@ -32,7 +32,7 @@ class MsgProxy: } @classmethod - def get_doc(cls, msg, deep=10): + def get_doc(cls, msg: Dict[str, Any], deep: int = 10) -> Dict[str, Any]: doc = msg["content"] _type = doc["@type"] fields = cls.fields_mapping.get(_type) @@ -48,13 +48,13 @@ class MsgProxy: return {} return doc - def __init__(self, msg: Dict[str, Any]): + def __init__(self, msg: Dict[str, Any]) -> None: self.msg = msg def __getitem__(self, key: str) -> Any: return self.msg[key] - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: self.msg[key] = value @property @@ -66,39 +66,39 @@ class MsgProxy: return datetime.fromtimestamp(self.msg["date"]) @property - def is_message(self): + def is_message(self) -> bool: return self.type == "message" @property - def content_type(self): + def content_type(self) -> Optional[str]: return self.types.get(self.msg["content"]["@type"]) @property - def size(self): + def size(self) -> int: doc = self.get_doc(self.msg) return doc["size"] @property - def human_size(self): + def human_size(self) -> str: doc = self.get_doc(self.msg) return utils.humanize_size(doc["size"]) @property - def duration(self): + def duration(self) -> Optional[str]: if self.content_type not in ("audio", "voice", "video", "recording"): return None doc = self.get_doc(self.msg, deep=1) return utils.humanize_duration(doc["duration"]) @property - def file_name(self): + def file_name(self) -> Optional[str]: if self.content_type not in ("audio", "document", "video"): return None doc = self.get_doc(self.msg, deep=1) return doc["file_name"] @property - def file_id(self): + def file_id(self) -> Optional[int]: if self.content_type not in ( "audio", "document", @@ -113,26 +113,26 @@ class MsgProxy: return doc["id"] @property - def local_path(self): + def local_path(self) -> Optional[str]: if self.msg["content"]["@type"] is None: return None doc = self.get_doc(self.msg) return doc["local"]["path"] @property - def local(self): + def local(self) -> Dict: doc = self.get_doc(self.msg) return doc["local"] @local.setter - def local(self, value): + def local(self, value: Dict) -> None: if self.msg["content"]["@type"] is None: - return None + return doc = self.get_doc(self.msg) doc["local"] = value @property - def is_text(self): + def is_text(self) -> bool: return self.msg["content"]["@type"] == "messageText" @property @@ -140,7 +140,7 @@ class MsgProxy: return self.msg["content"]["text"]["text"] @property - def is_downloaded(self): + def is_downloaded(self) -> bool: doc = self.get_doc(self.msg) return doc["local"]["is_downloading_completed"] @@ -151,7 +151,7 @@ class MsgProxy: return self.msg["content"]["is_listened"] @is_listened.setter - def is_listened(self, value: bool): + def is_listened(self, value: bool) -> None: if self.content_type == "voice": self.msg["content"]["is_listened"] = value @@ -162,7 +162,7 @@ class MsgProxy: return self.msg["content"]["is_viewed"] @is_viewed.setter - def is_viewed(self, value: bool): + def is_viewed(self, value: bool) -> None: if self.content_type == "recording": self.msg["content"]["is_viewed"] = value diff --git a/tg/tdlib.py b/tg/tdlib.py index fe8c425..8f7b32e 100644 --- a/tg/tdlib.py +++ b/tg/tdlib.py @@ -5,8 +5,13 @@ from telegram.client import AsyncResult, Telegram class Tdlib(Telegram): def download_file( - self, file_id, priority=16, offset=0, limit=0, synchronous=False, - ): + self, + file_id: int, + priority: int = 16, + offset: int = 0, + limit: int = 0, + synchronous: bool = False, + ) -> None: result = self.call_method( "downloadFile", params=dict( @@ -90,8 +95,8 @@ class Tdlib(Telegram): return self._send_data(data) def send_voice( - self, file_path: str, chat_id: int, duration: int, waveform: int - ): + self, file_path: str, chat_id: int, duration: int, waveform: str + ) -> AsyncResult: data = { "@type": "sendMessage", "chat_id": chat_id, @@ -104,7 +109,9 @@ class Tdlib(Telegram): } return self._send_data(data) - def edit_message_text(self, chat_id: int, message_id: int, text: str): + def edit_message_text( + self, chat_id: int, message_id: int, text: str + ) -> AsyncResult: data = { "@type": "editMessageText", "message_id": message_id, @@ -138,7 +145,7 @@ class Tdlib(Telegram): def set_chat_nottification_settings( self, chat_id: int, notification_settings: dict - ): + ) -> AsyncResult: data = { "@type": "setChatNotificationSettings", "chat_id": chat_id, diff --git a/tg/update_handlers.py b/tg/update_handlers.py index e530061..8111d66 100644 --- a/tg/update_handlers.py +++ b/tg/update_handlers.py @@ -8,15 +8,15 @@ from tg.msg import MsgProxy log = logging.getLogger(__name__) -_update_handler_type = Callable[[Controller, Dict[str, Any]], None] +UpdateHandler = Callable[[Controller, Dict[str, Any]], None] -handlers: Dict[str, _update_handler_type] = {} +handlers: Dict[str, UpdateHandler] = {} max_download_size: int = utils.parse_size(config.MAX_DOWNLOAD_SIZE) -def update_handler(update_type): - def decorator(fun): +def update_handler(update_type: str) -> Callable: + def decorator(fun: Callable) -> Callable: global handlers assert ( update_type not in handlers @@ -25,7 +25,7 @@ def update_handler(update_type): handlers[update_type] = fun @wraps(fun) - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> Any: try: return fun(*args, **kwargs) except Exception: @@ -37,7 +37,9 @@ def update_handler(update_type): @update_handler("updateMessageContent") -def update_message_content(controller: Controller, update: Dict[str, Any]): +def update_message_content( + controller: Controller, update: Dict[str, Any] +) -> None: chat_id = update["chat_id"] message_id = update["message_id"] controller.model.msgs.update_msg( @@ -50,7 +52,9 @@ def update_message_content(controller: Controller, update: Dict[str, Any]): @update_handler("updateMessageEdited") -def update_message_edited(controller: Controller, update: Dict[str, Any]): +def update_message_edited( + controller: Controller, update: Dict[str, Any] +) -> None: chat_id = update["chat_id"] message_id = update["message_id"] edit_date = update["edit_date"] @@ -62,7 +66,7 @@ def update_message_edited(controller: Controller, update: Dict[str, Any]): @update_handler("updateNewMessage") -def update_new_message(controller: Controller, update: Dict[str, Any]): +def update_new_message(controller: Controller, update: Dict[str, Any]) -> None: msg = MsgProxy(update["message"]) controller.model.msgs.add_message(msg.chat_id, msg.msg) current_chat_id = controller.model.current_chat_id @@ -75,7 +79,7 @@ def update_new_message(controller: Controller, update: Dict[str, Any]): @update_handler("updateChatOrder") -def update_chat_order(controller: Controller, update: Dict[str, Any]): +def update_chat_order(controller: Controller, update: Dict[str, Any]) -> None: log.info("Proccessing updateChatOrder") current_chat_id = controller.model.current_chat_id chat_id = update["chat_id"] @@ -86,7 +90,7 @@ def update_chat_order(controller: Controller, update: Dict[str, Any]): @update_handler("updateChatTitle") -def update_chat_title(controller: Controller, update: Dict[str, Any]): +def update_chat_title(controller: Controller, update: Dict[str, Any]) -> None: log.info("Proccessing updateChatTitle") chat_id = update["chat_id"] title = update["title"] @@ -99,7 +103,7 @@ def update_chat_title(controller: Controller, update: Dict[str, Any]): @update_handler("updateChatIsMarkedAsUnread") def update_chat_is_marked_as_unread( controller: Controller, update: Dict[str, Any] -): +) -> None: log.info("Proccessing updateChatIsMarkedAsUnread") chat_id = update["chat_id"] is_marked_as_unread = update["is_marked_as_unread"] @@ -112,7 +116,9 @@ def update_chat_is_marked_as_unread( @update_handler("updateChatIsPinned") -def update_chat_is_pinned(controller: Controller, update: Dict[str, Any]): +def update_chat_is_pinned( + controller: Controller, update: Dict[str, Any] +) -> None: log.info("Proccessing updateChatIsPinned") chat_id = update["chat_id"] is_pinned = update["is_pinned"] @@ -126,20 +132,24 @@ def update_chat_is_pinned(controller: Controller, update: Dict[str, Any]): @update_handler("updateChatReadOutbox") -def update_chat_read_outbox(controller: Controller, update: Dict[str, Any]): +def update_chat_read_outbox( + controller: Controller, update: Dict[str, Any] +) -> None: log.info("Proccessing updateChatReadOutbox") chat_id = update["chat_id"] last_read_outbox_message_id = update["last_read_outbox_message_id"] current_chat_id = controller.model.current_chat_id if controller.model.chats.update_chat( - chat_id, last_read_outbox_message_id=last_read_outbox_message_id, + chat_id, last_read_outbox_message_id=last_read_outbox_message_id ): controller._refresh_current_chat(current_chat_id) @update_handler("updateChatReadInbox") -def update_chat_read_inbox(controller: Controller, update: Dict[str, Any]): +def update_chat_read_inbox( + controller: Controller, update: Dict[str, Any] +) -> None: log.info("Proccessing updateChatReadInbox") chat_id = update["chat_id"] last_read_inbox_message_id = update["last_read_inbox_message_id"] @@ -155,7 +165,9 @@ def update_chat_read_inbox(controller: Controller, update: Dict[str, Any]): @update_handler("updateChatDraftMessage") -def update_chat_draft_message(controller: Controller, update: Dict[str, Any]): +def update_chat_draft_message( + controller: Controller, update: Dict[str, Any] +) -> None: log.info("Proccessing updateChatDraftMessage") chat_id = update["chat_id"] # FIXME: ignoring draft message itself for now because UI can't show it @@ -168,7 +180,9 @@ def update_chat_draft_message(controller: Controller, update: Dict[str, Any]): @update_handler("updateChatLastMessage") -def update_chat_last_message(controller: Controller, update: Dict[str, Any]): +def update_chat_last_message( + controller: Controller, update: Dict[str, Any] +) -> None: log.info("Proccessing updateChatLastMessage") chat_id = update["chat_id"] last_message = update.get("last_message") @@ -186,7 +200,9 @@ def update_chat_last_message(controller: Controller, update: Dict[str, Any]): @update_handler("updateChatNotificationSettings") -def update_chat_notification_settings(controller: Controller, update): +def update_chat_notification_settings( + controller: Controller, update: Dict[str, Any] +) -> None: log.info("Proccessing update_chat_notification_settings") chat_id = update["chat_id"] notification_settings = update["notification_settings"] @@ -197,7 +213,9 @@ def update_chat_notification_settings(controller: Controller, update): @update_handler("updateMessageSendSucceeded") -def update_message_send_succeeded(controller: Controller, update): +def update_message_send_succeeded( + controller: Controller, update: Dict[str, Any] +) -> None: chat_id = update["message"]["chat_id"] msg_id = update["old_message_id"] controller.model.msgs.add_message(chat_id, update["message"]) @@ -209,7 +227,7 @@ def update_message_send_succeeded(controller: Controller, update): @update_handler("updateFile") -def update_file(controller: Controller, update): +def update_file(controller: Controller, update: Dict[str, Any]) -> None: log.info("update_file: %s", update) file_id = update["file"]["id"] local = update["file"]["local"] @@ -233,7 +251,7 @@ def update_file(controller: Controller, update): @update_handler("updateMessageContentOpened") def update_message_content_opened( controller: Controller, update: Dict[str, Any] -): +) -> None: chat_id = update["chat_id"] message_id = update["message_id"] controller.model.msgs.update_msg_content_opened(chat_id, message_id) @@ -241,7 +259,9 @@ def update_message_content_opened( @update_handler("updateDeleteMessages") -def update_delete_messages(controller: Controller, update: Dict[str, Any]): +def update_delete_messages( + controller: Controller, update: Dict[str, Any] +) -> None: chat_id = update["chat_id"] msg_ids = update["message_ids"] for msg_id in msg_ids: @@ -250,7 +270,9 @@ def update_delete_messages(controller: Controller, update: Dict[str, Any]): @update_handler("updateConnectionState") -def update_connection_state(controller: Controller, update: Dict[str, Any]): +def update_connection_state( + controller: Controller, update: Dict[str, Any] +) -> None: log.info("state:: %s", update) state = update["state"]["@type"] states = { @@ -265,6 +287,6 @@ def update_connection_state(controller: Controller, update: Dict[str, Any]): @update_handler("updateUserStatus") -def update_user_status(controller: Controller, update: Dict[str, Any]): +def update_user_status(controller: Controller, update: Dict[str, Any]) -> None: controller.model.users.set_status(update["user_id"], update["status"]) controller.render_chats() diff --git a/tg/utils.py b/tg/utils.py index c27f1da..8ecc1da 100644 --- a/tg/utils.py +++ b/tg/utils.py @@ -13,7 +13,9 @@ import subprocess import sys from datetime import datetime from functools import wraps -from typing import Optional +from logging.handlers import RotatingFileHandler +from types import TracebackType +from typing import Any, Callable, Optional, TextIO, Tuple, Type from tg import config @@ -33,29 +35,30 @@ units = {"B": 1, "KB": 10 ** 3, "MB": 10 ** 6, "GB": 10 ** 9, "TB": 10 ** 12} class LogWriter: - def __init__(self, level): + def __init__(self, level: Any) -> None: self.level = level - def write(self, message): + def write(self, message: str) -> None: if message != "\n": self.level.log(self.level, message) - def flush(self): + def flush(self) -> None: pass -def setup_log(): +def setup_log() -> None: handlers = [] - for level, filename in zip( - (config.LOG_LEVEL, logging.ERROR), ("all.log", "error.log"), + for level, filename in ( + (config.LOG_LEVEL, "all.log"), + (logging.ERROR, "error.log"), ): - handler = logging.handlers.RotatingFileHandler( + handler = RotatingFileHandler( os.path.join(config.LOG_PATH, filename), maxBytes=parse_size("32MB"), backupCount=1, ) - handler.setLevel(level) + handler.setLevel(level) # type: ignore handlers.append(handler) logging.basicConfig( @@ -63,11 +66,11 @@ def setup_log(): handlers=handlers, ) logging.getLogger().setLevel(config.LOG_LEVEL) - sys.stderr = LogWriter(log.error) + sys.stderr = LogWriter(log.error) # type: ignore logging.captureWarnings(True) -def get_file_handler(file_path, default=None): +def get_file_handler(file_path: str, default: str = None) -> Optional[str]: mtype, _ = mimetypes.guess_type(file_path) if not mtype: return default @@ -87,8 +90,19 @@ def parse_size(size: str) -> int: def humanize_size( - num, suffix="B", suffixes=("", "K", "M", "G", "T", "P", "E", "Z") -): + num: int, + suffix: str = "B", + suffixes: Tuple[str, str, str, str, str, str, str, str] = ( + "", + "K", + "M", + "G", + "T", + "P", + "E", + "Z", + ), +) -> str: magnitude = int(math.floor(math.log(num, 1024))) val = num / math.pow(1024, magnitude) if magnitude > 7: @@ -96,7 +110,7 @@ def humanize_size( return "{:3.1f}{}{}".format(val, suffixes[magnitude], suffix) -def humanize_duration(seconds): +def humanize_duration(seconds: int) -> str: dt = datetime.utcfromtimestamp(seconds) fmt = "%-M:%S" if seconds >= 3600: @@ -111,13 +125,13 @@ def num(value: str, default: Optional[int] = None) -> Optional[int]: return default -def is_yes(resp): +def is_yes(resp: str) -> bool: if resp.strip().lower() == "y" or resp == "": return True return False -def get_duration(file_path): +def get_duration(file_path: str) -> int: cmd = f"ffprobe -v error -i '{file_path}' -show_format" stdout = subprocess.check_output(shlex.split(cmd)).decode().splitlines() line = next((line for line in stdout if "duration" in line), None) @@ -128,14 +142,14 @@ def get_duration(file_path): return 0 -def get_video_resolution(file_path): +def get_video_resolution(file_path: str) -> Tuple[int, int]: cmd = f"ffprobe -v error -show_entries stream=width,height -of default=noprint_wrappers=1 '{file_path}'" lines = subprocess.check_output(shlex.split(cmd)).decode().splitlines() info = {line.split("=")[0]: line.split("=")[1] for line in lines} - return info.get("width"), info.get("height") + return int(str(info.get("width"))), int(str(info.get("height"))) -def get_waveform(file_path): +def get_waveform(file_path: str) -> str: # mock for now waveform = (random.randint(0, 255) for _ in range(100)) packed = struct.pack("100B", *waveform) @@ -143,8 +157,11 @@ def get_waveform(file_path): def notify( - msg, subtitle="", title="tg", cmd=config.NOTIFY_CMD, -): + msg: str, + subtitle: str = "", + title: str = "tg", + cmd: str = config.NOTIFY_CMD, +) -> None: if not cmd: return notify_cmd = cmd.format( @@ -156,9 +173,9 @@ def notify( os.system(notify_cmd) -def handle_exception(fun): +def handle_exception(fun: Callable) -> Callable: @wraps(fun) - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> Any: try: return fun(*args, **kwargs) except Exception: @@ -172,29 +189,30 @@ def truncate_to_len(s: str, target_len: int, encoding: str = "utf-8") -> str: return s[: max(1, target_len - 1)] -def copy_to_clipboard(text): +def copy_to_clipboard(text: str) -> None: subprocess.run( config.COPY_CMD, universal_newlines=True, input=text, shell=True ) class suspend: - def __init__(self, view): + # FIXME: can't explicitly set type "View" due to circular import + def __init__(self, view: Any) -> None: self.view = view - def call(self, cmd): + def call(self, cmd: str) -> None: subprocess.call(cmd, shell=True) - def run_with_input(self, cmd, text): + def run_with_input(self, cmd: str, text: str) -> None: subprocess.run(cmd, universal_newlines=True, input=text, shell=True) - def open_file(self, file_path): + def open_file(self, file_path: str) -> None: cmd = get_file_handler(file_path) if not cmd: return self.call(cmd) - def __enter__(self): + def __enter__(self) -> "suspend": for view in (self.view.chats, self.view.msgs, self.view.status): view._refresh = view.win.noutrefresh curses.echo() @@ -204,7 +222,12 @@ class suspend: curses.endwin() return self - def __exit__(self, exc_type, exc_val, tb): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + tb: Optional[TracebackType], + ) -> None: for view in (self.view.chats, self.view.msgs, self.view.status): view._refresh = view.win.refresh curses.noecho() @@ -214,5 +237,5 @@ class suspend: curses.doupdate() -def set_shorter_esc_delay(delay=25): +def set_shorter_esc_delay(delay: int = 25) -> None: os.environ.setdefault("ESCDELAY", str(delay)) diff --git a/tg/views.py b/tg/views.py index e7914f8..4ba5df2 100644 --- a/tg/views.py +++ b/tg/views.py @@ -2,7 +2,7 @@ import curses import logging from _curses import window # type: ignore from datetime import datetime -from typing import Any, Dict, List, Optional, Tuple, cast +from typing import Any, Dict, List, Optional, Tuple, Union, cast from tg import config from tg.colors import blue, cyan, get_color, magenta, reverse, white, yellow @@ -82,7 +82,7 @@ class StatusView: self.win = stdscr.subwin(self.h, self.w, self.y, self.x) self._refresh = self.win.refresh - def resize(self, rows: int, cols: int): + def resize(self, rows: int, cols: int) -> None: self.w = cols - 1 self.y = rows - 1 self.win.resize(self.h, self.w) @@ -95,7 +95,7 @@ class StatusView: self.win.addstr(0, 0, msg[: self.w]) self._refresh() - def get_input(self, msg="") -> str: + def get_input(self, msg: str = "") -> str: self.draw(msg) curses.curs_set(1) @@ -126,7 +126,7 @@ class StatusView: class ChatView: - def __init__(self, stdscr: window, model: Model): + def __init__(self, stdscr: window, model: Model) -> None: self.stdscr = stdscr self.h = 0 self.w = 0 @@ -243,9 +243,7 @@ class ChatView: class MsgView: - def __init__( - self, stdscr: window, model: Model, - ): + def __init__(self, stdscr: window, model: Model,) -> None: self.model = model self.stdscr = stdscr self.h = 0 @@ -265,7 +263,7 @@ class MsgView: self.win.resize(self.h, self.w) self.win.mvwin(0, self.x) - def _get_flags(self, msg_proxy: MsgProxy): + def _get_flags(self, msg_proxy: MsgProxy) -> str: flags = [] chat = self.model.chats.chats[self.model.current_chat] @@ -314,7 +312,7 @@ class MsgView: msg = f"{reply_line}\n{msg}" return msg - def _format_url(self, msg_proxy: MsgProxy): + def _format_url(self, msg_proxy: MsgProxy) -> str: if not msg_proxy.is_text or "web_page" not in msg_proxy.msg["content"]: return "" web = msg_proxy.msg["content"]["web_page"] @@ -541,11 +539,11 @@ def format_bool(value: Optional[bool]) -> Optional[str]: return "yes" if value else "no" -def get_download(local, size): +def get_download(local: Dict[str, Union[str, bool, int]], size: int) -> str: if local["is_downloading_completed"]: return "yes" elif local["is_downloading_active"]: - d = local["downloaded_size"] + d = int(local["downloaded_size"]) percent = int(d * 100 / size) return f"{percent}%" return "no" From f451473b390c9b94e7b34cbcf9524784c4820743 Mon Sep 17 00:00:00 2001 From: Alexander Zveruk Date: Sat, 27 Jun 2020 23:55:53 +0300 Subject: [PATCH 2/7] make it prettier --- tg/controllers.py | 13 +++++++------ tg/update_handlers.py | 26 ++++++++++++++------------ tg/utils.py | 16 ++-------------- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/tg/controllers.py b/tg/controllers.py index 3f7d756..caf65ed 100644 --- a/tg/controllers.py +++ b/tg/controllers.py @@ -2,7 +2,6 @@ import curses import logging import os import shlex -import threading from datetime import datetime from functools import partial, wraps from queue import Queue @@ -19,7 +18,6 @@ from tg.utils import ( get_duration, get_video_resolution, get_waveform, - handle_exception, is_yes, notify, suspend, @@ -53,7 +51,7 @@ def bind( return fun(*args, **kwargs) @wraps(fun) - def _no_repeat_factor(self: Controller, _: bool) -> Any: + def _no_repeat_factor(self: "Controller", _: bool) -> Any: return fun(self) for key in keys: @@ -298,11 +296,14 @@ class Controller: self.send_file(self.tg.send_audio) def send_file( - self, send_file_fun: Callable[[str, int], AsyncResult], *args: Any, **kwargs: Any + self, + send_file_fun: Callable[[str, int], AsyncResult], ) -> None: file_path = self.view.status.get_input() if file_path and os.path.isfile(file_path): - if chat_id := self.model.chats.id_by_index(self.model.current_chat): + if chat_id := self.model.chats.id_by_index( + self.model.current_chat + ): send_file_fun(file_path, chat_id) self.present_info("File sent") @@ -597,7 +598,7 @@ class Controller: if text := msg.text_content if msg.is_text else msg.content_type: notify(text, title=name) - def _refresh_current_chat(self, current_chat_id: Optional[int]) -> None: + def refresh_current_chat(self, current_chat_id: Optional[int]) -> None: if current_chat_id is None: return # TODO: we can create for chats, it's faster than sqlite anyway diff --git a/tg/update_handlers.py b/tg/update_handlers.py index 8111d66..e1c6618 100644 --- a/tg/update_handlers.py +++ b/tg/update_handlers.py @@ -15,8 +15,10 @@ handlers: Dict[str, UpdateHandler] = {} max_download_size: int = utils.parse_size(config.MAX_DOWNLOAD_SIZE) -def update_handler(update_type: str) -> Callable: - def decorator(fun: Callable) -> Callable: +def update_handler( + update_type: str, +) -> Callable[[UpdateHandler], UpdateHandler]: + def decorator(fun: UpdateHandler) -> UpdateHandler: global handlers assert ( update_type not in handlers @@ -25,9 +27,9 @@ def update_handler(update_type: str) -> Callable: handlers[update_type] = fun @wraps(fun) - def wrapper(*args: Any, **kwargs: Any) -> Any: + def wrapper(controller: Controller, update: Dict[str, Any]) -> None: try: - return fun(*args, **kwargs) + return fun(controller, update) except Exception: log.exception("Error happened in %s handler", fun.__name__) @@ -86,7 +88,7 @@ def update_chat_order(controller: Controller, update: Dict[str, Any]) -> None: order = update["order"] if controller.model.chats.update_chat(chat_id, order=order): - controller._refresh_current_chat(current_chat_id) + controller.refresh_current_chat(current_chat_id) @update_handler("updateChatTitle") @@ -97,7 +99,7 @@ def update_chat_title(controller: Controller, update: Dict[str, Any]) -> None: current_chat_id = controller.model.current_chat_id if controller.model.chats.update_chat(chat_id, title=title): - controller._refresh_current_chat(current_chat_id) + controller.refresh_current_chat(current_chat_id) @update_handler("updateChatIsMarkedAsUnread") @@ -112,7 +114,7 @@ def update_chat_is_marked_as_unread( if controller.model.chats.update_chat( chat_id, is_marked_as_unread=is_marked_as_unread ): - controller._refresh_current_chat(current_chat_id) + controller.refresh_current_chat(current_chat_id) @update_handler("updateChatIsPinned") @@ -128,7 +130,7 @@ def update_chat_is_pinned( if controller.model.chats.update_chat( chat_id, is_pinned=is_pinned, order=order ): - controller._refresh_current_chat(current_chat_id) + controller.refresh_current_chat(current_chat_id) @update_handler("updateChatReadOutbox") @@ -143,7 +145,7 @@ def update_chat_read_outbox( if controller.model.chats.update_chat( chat_id, last_read_outbox_message_id=last_read_outbox_message_id ): - controller._refresh_current_chat(current_chat_id) + controller.refresh_current_chat(current_chat_id) @update_handler("updateChatReadInbox") @@ -161,7 +163,7 @@ def update_chat_read_inbox( last_read_inbox_message_id=last_read_inbox_message_id, unread_count=unread_count, ): - controller._refresh_current_chat(current_chat_id) + controller.refresh_current_chat(current_chat_id) @update_handler("updateChatDraftMessage") @@ -176,7 +178,7 @@ def update_chat_draft_message( current_chat_id = controller.model.current_chat_id if controller.model.chats.update_chat(chat_id, order=order): - controller._refresh_current_chat(current_chat_id) + controller.refresh_current_chat(current_chat_id) @update_handler("updateChatLastMessage") @@ -196,7 +198,7 @@ def update_chat_last_message( if controller.model.chats.update_chat( chat_id, last_message=last_message, order=order ): - controller._refresh_current_chat(current_chat_id) + controller.refresh_current_chat(current_chat_id) @update_handler("updateChatNotificationSettings") diff --git a/tg/utils.py b/tg/utils.py index 8ecc1da..0a62761 100644 --- a/tg/utils.py +++ b/tg/utils.py @@ -12,10 +12,9 @@ import struct import subprocess import sys from datetime import datetime -from functools import wraps from logging.handlers import RotatingFileHandler from types import TracebackType -from typing import Any, Callable, Optional, TextIO, Tuple, Type +from typing import Any, Optional, Tuple, Type from tg import config @@ -173,18 +172,7 @@ def notify( os.system(notify_cmd) -def handle_exception(fun: Callable) -> Callable: - @wraps(fun) - def wrapper(*args: Any, **kwargs: Any) -> Any: - try: - return fun(*args, **kwargs) - except Exception: - log.exception("Error happened in %s handler", fun.__name__) - - return wrapper - - -def truncate_to_len(s: str, target_len: int, encoding: str = "utf-8") -> str: +def truncate_to_len(s: str, target_len: int) -> str: target_len -= sum(map(bool, map(emoji_pattern.findall, s[:target_len]))) return s[: max(1, target_len - 1)] From 346c73d0c21de9bf23e0958c72ff3a8407c5ce94 Mon Sep 17 00:00:00 2001 From: Alexander Zveruk Date: Sun, 28 Jun 2020 00:10:20 +0300 Subject: [PATCH 3/7] update gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index e265369..e6c3b35 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ __pycache__ dist *.log* Makefile +.idea/ +*monkeytype.sqlite3 From b4d373aee501af0a4a08c5854a43be5e2f7c3176 Mon Sep 17 00:00:00 2001 From: Alexander Zveruk Date: Sun, 28 Jun 2020 00:14:50 +0300 Subject: [PATCH 4/7] fix gly type signature --- tg/controllers.py | 3 +-- tg/utils.py | 11 +---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/tg/controllers.py b/tg/controllers.py index caf65ed..3498b52 100644 --- a/tg/controllers.py +++ b/tg/controllers.py @@ -296,8 +296,7 @@ class Controller: self.send_file(self.tg.send_audio) def send_file( - self, - send_file_fun: Callable[[str, int], AsyncResult], + self, send_file_fun: Callable[[str, int], AsyncResult], ) -> None: file_path = self.view.status.get_input() if file_path and os.path.isfile(file_path): diff --git a/tg/utils.py b/tg/utils.py index 0a62761..2345aa9 100644 --- a/tg/utils.py +++ b/tg/utils.py @@ -91,16 +91,7 @@ def parse_size(size: str) -> int: def humanize_size( num: int, suffix: str = "B", - suffixes: Tuple[str, str, str, str, str, str, str, str] = ( - "", - "K", - "M", - "G", - "T", - "P", - "E", - "Z", - ), + suffixes: Tuple[str, ...] = ("", "K", "M", "G", "T", "P", "E", "Z",), ) -> str: magnitude = int(math.floor(math.log(num, 1024))) val = num / math.pow(1024, magnitude) From 19dc0f4c9a2de223c7ed13deb19c04f01905c781 Mon Sep 17 00:00:00 2001 From: Alexander Zveruk Date: Sun, 28 Jun 2020 00:26:47 +0300 Subject: [PATCH 5/7] fix isort --- pyproject.toml | 2 +- tg/controllers.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 85933c5..6fe1cc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,4 +21,4 @@ line-length = 79 [tool.isort] line_length = 79 multi_line_output = 3 -include_trailing_comma = true \ No newline at end of file +include_trailing_comma = true diff --git a/tg/controllers.py b/tg/controllers.py index 3498b52..6ec054e 100644 --- a/tg/controllers.py +++ b/tg/controllers.py @@ -9,7 +9,6 @@ from tempfile import NamedTemporaryFile from typing import Any, Callable, Dict, List, Optional from telegram.utils import AsyncResult - from tg import config from tg.models import Model from tg.msg import MsgProxy From 4a3ebe5b5d6f2be7f9de8ad74b0d9ad378678717 Mon Sep 17 00:00:00 2001 From: Alexander Zveruk Date: Sun, 28 Jun 2020 11:13:16 +0300 Subject: [PATCH 6/7] do not use else in insert_replied_msg --- tg/controllers.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tg/controllers.py b/tg/controllers.py index 6ec054e..9d0372b 100644 --- a/tg/controllers.py +++ b/tg/controllers.py @@ -610,16 +610,14 @@ class Controller: def insert_replied_msg(msg: MsgProxy) -> str: - if text := msg.text_content if msg.is_text else msg.content_type: - return ( - "\n".join( - [f"{REPLY_MSG_PREFIX} {line}" for line in text.split("\n")] - ) - # adding line with whitespace so text editor could start editing from last line - + "\n " - ) - else: + text = msg.text_content if msg.is_text else msg.content_type + if not text: return "" + return ( + "\n".join([f"{REPLY_MSG_PREFIX} {line}" for line in text.split("\n")]) + # adding line with whitespace so text editor could start editing from last line + + "\n " + ) def strip_replied_msg(msg: str) -> str: From 4cea85c0262dbcfc123878c870a1863cdde85921 Mon Sep 17 00:00:00 2001 From: Paul Nameless Date: Tue, 30 Jun 2020 15:08:44 +0800 Subject: [PATCH 7/7] Update type hints after merging master --- tg/controllers.py | 8 ++++---- tg/update_handlers.py | 8 +++++--- tg/views.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/tg/controllers.py b/tg/controllers.py index 22ec1a1..61bf90c 100644 --- a/tg/controllers.py +++ b/tg/controllers.py @@ -238,10 +238,10 @@ class Controller: @bind(msg_handler, ["a", "i"]) def write_short_msg(self) -> None: - if not self.can_send_msg(): + chat_id = self.model.chats.id_by_index(self.model.current_chat) + if not self.can_send_msg() or chat_id is None: self.present_info("Can't send msg in this chat") return - chat_id = self.model.chats.id_by_index(self.model.current_chat) self.tg.send_chat_action(chat_id, ChatAction.chatActionTyping) if msg := self.view.status.get_input(): self.model.send_message(text=msg) @@ -252,13 +252,13 @@ class Controller: @bind(msg_handler, ["A", "I"]) def write_long_msg(self) -> None: - if not self.can_send_msg(): + chat_id = self.model.chats.id_by_index(self.model.current_chat) + if not self.can_send_msg() or chat_id is None: self.present_info("Can't send msg in this chat") return with NamedTemporaryFile("r+", suffix=".txt") as f, suspend( self.view ) as s: - chat_id = self.model.chats.id_by_index(self.model.current_chat) self.tg.send_chat_action(chat_id, ChatAction.chatActionTyping) s.call(config.LONG_MSG_CMD.format(file_path=shlex.quote(f.name))) with open(f.name) as f: diff --git a/tg/update_handlers.py b/tg/update_handlers.py index d1955ac..b31476c 100644 --- a/tg/update_handlers.py +++ b/tg/update_handlers.py @@ -285,21 +285,23 @@ def update_user_status(controller: Controller, update: Dict[str, Any]) -> None: @update_handler("updateBasicGroup") -def update_basic_group(controller: Controller, update: Dict[str, Any]): +def update_basic_group(controller: Controller, update: Dict[str, Any]) -> None: basic_group = update["basic_group"] controller.model.users.groups[basic_group["id"]] = basic_group controller.render_msgs() @update_handler("updateSupergroup") -def update_supergroup(controller: Controller, update: Dict[str, Any]): +def update_supergroup(controller: Controller, update: Dict[str, Any]) -> None: supergroup = update["supergroup"] controller.model.users.supergroups[supergroup["id"]] = supergroup controller.render_msgs() @update_handler("updateUserChatAction") -def update_user_chat_action(controller: Controller, update: Dict[str, Any]): +def update_user_chat_action( + controller: Controller, update: Dict[str, Any] +) -> None: chat_id = update["chat_id"] if update["action"]["@type"] == "chatActionCancel": controller.model.users.actions.pop(chat_id, None) diff --git a/tg/views.py b/tg/views.py index 839400c..9d3beba 100644 --- a/tg/views.py +++ b/tg/views.py @@ -491,7 +491,7 @@ class MsgView: log.error(f"ChatType {chat['type']} not implemented") return None - def _msg_title(self, chat: Dict[str, Any]): + def _msg_title(self, chat: Dict[str, Any]) -> str: chat_type = self._get_chat_type(chat) status = "" if action := self.model.users.get_action(chat["id"]):