From 2b03c038910e327b928d0373774fd31cb7175497 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Sat, 9 Jan 2021 22:30:07 -0500 Subject: [PATCH] Add typing to every method definition --- my_project_name/bot_commands.py | 28 +++++++++---- my_project_name/callbacks.py | 60 +++++++++++++++++++--------- my_project_name/chat_functions.py | 10 +++-- my_project_name/config.py | 18 ++++----- my_project_name/errors.py | 7 +++- my_project_name/main.py | 1 + my_project_name/message_responses.py | 32 ++++++++++----- my_project_name/storage.py | 26 ++++++++---- 8 files changed, 124 insertions(+), 58 deletions(-) diff --git a/my_project_name/bot_commands.py b/my_project_name/bot_commands.py index d9a40db..38cdb00 100644 --- a/my_project_name/bot_commands.py +++ b/my_project_name/bot_commands.py @@ -1,22 +1,34 @@ +from nio import AsyncClient, MatrixRoom, RoomMessageText + from my_project_name.chat_functions import react_to_event, send_text_to_room +from my_project_name.config import Config +from my_project_name.storage import Storage -class Command(object): - def __init__(self, client, store, config, command, room, event): +class Command: + def __init__( + self, + client: AsyncClient, + store: Storage, + config: Config, + command: str, + room: MatrixRoom, + event: RoomMessageText, + ): """A command made by a user Args: - client (nio.AsyncClient): The client to communicate to matrix with + client: The client to communicate to matrix with - store (Storage): Bot storage + store: Bot storage - config (Config): Bot configuration parameters + config: Bot configuration parameters - command (str): The command and arguments + command: The command and arguments - room (nio.rooms.MatrixRoom): The room the command was sent in + room: The room the command was sent in - event (nio.events.room_events.RoomMessageText): The event describing the command + event: The event describing the command """ self.client = client self.store = store diff --git a/my_project_name/callbacks.py b/my_project_name/callbacks.py index 3c7406b..8c155f3 100644 --- a/my_project_name/callbacks.py +++ b/my_project_name/callbacks.py @@ -1,37 +1,47 @@ import logging -from nio import JoinError, MatrixRoom, MegolmEvent, RoomGetEventError, UnknownEvent +from nio import ( + AsyncClient, + InviteMemberEvent, + JoinError, + MatrixRoom, + MegolmEvent, + RoomGetEventError, + RoomMessageText, + UnknownEvent, +) from my_project_name.bot_commands import Command from my_project_name.chat_functions import make_pill, react_to_event, send_text_to_room +from my_project_name.config import Config from my_project_name.message_responses import Message +from my_project_name.storage import Storage logger = logging.getLogger(__name__) -class Callbacks(object): - def __init__(self, client, store, config): +class Callbacks: + def __init__(self, client: AsyncClient, store: Storage, config: Config): """ Args: - client (nio.AsyncClient): nio client used to interact with matrix + client: nio client used to interact with matrix - store (Storage): Bot storage + store: Bot storage - config (Config): Bot configuration parameters + config: Bot configuration parameters """ self.client = client self.store = store self.config = config self.command_prefix = config.command_prefix - async def message(self, room, event): + async def message(self, room: MatrixRoom, event: RoomMessageText) -> None: """Callback for when a message event is received Args: - room (nio.rooms.MatrixRoom): The room the event came from - - event (nio.events.room_events.RoomMessageText): The event defining the message + room: The room the event came from + event: The event defining the message """ # Extract the message text msg = event.body @@ -67,8 +77,14 @@ class Callbacks(object): command = Command(self.client, self.store, self.config, msg, room, event) await command.process() - async def invite(self, room, event): - """Callback for when an invite is received. Join the room specified in the invite""" + async def invite(self, room: MatrixRoom, event: InviteMemberEvent) -> None: + """Callback for when an invite is received. Join the room specified in the invite. + + Args: + room: The room that we are invited to. + + event: The invite event. + """ logger.debug(f"Got invite to {room.room_id} from {event.sender}.") # Attempt to join 3 times before giving up @@ -90,7 +106,7 @@ class Callbacks(object): async def _reaction( self, room: MatrixRoom, event: UnknownEvent, reacted_to_id: str - ): + ) -> None: """A reaction was sent to one of our messages. Let's send a reply acknowledging it. Args: @@ -130,8 +146,14 @@ class Callbacks(object): reply_to_event_id=reacted_to_id, ) - async def decryption_failure(self, room: MatrixRoom, event: MegolmEvent): - """Callback for when an event fails to decrypt. Inform the user""" + async def decryption_failure(self, room: MatrixRoom, event: MegolmEvent) -> None: + """Callback for when an event fails to decrypt. Inform the user. + + Args: + room: The room that the event that we were unable to decrypt is in. + + event: The encrypted event that we were unable to decrypt. + """ logger.error( f"Failed to decrypt event '{event.event_id}' in room '{room.room_id}'!" f"\n\n" @@ -152,14 +174,15 @@ class Callbacks(object): red_x_and_lock_emoji, ) - async def unknown(self, room: MatrixRoom, event: UnknownEvent): + async def unknown(self, room: MatrixRoom, event: UnknownEvent) -> None: """Callback for when an event with a type that is unknown to matrix-nio is received. - Currently this is used for reaction events, which are not specced. + Currently this is used for reaction events, which are not yet part of a released + matrix spec (and are thus unknown to nio). Args: room: The room the reaction was sent in. - event: The reaction event. + event: The event itself. """ if event.type == "m.reaction": # Get the ID of the event this was a reaction to @@ -168,6 +191,7 @@ class Callbacks(object): reacted_to = relation_dict.get("event_id") if reacted_to and relation_dict.get("rel_type") == "m.annotation": await self._reaction(room, event, reacted_to) + return logger.debug( f"Got unknown event with type to {event.type} from {event.sender} in {room.room_id}." diff --git a/my_project_name/chat_functions.py b/my_project_name/chat_functions.py index c8959fb..136726f 100644 --- a/my_project_name/chat_functions.py +++ b/my_project_name/chat_functions.py @@ -8,6 +8,7 @@ from nio import ( MatrixRoom, MegolmEvent, Response, + RoomSendResponse, SendRetryError, ) @@ -21,7 +22,7 @@ async def send_text_to_room( notice: bool = True, markdown_convert: bool = True, reply_to_event_id: Optional[str] = None, -): +) -> Union[RoomSendResponse, ErrorResponse]: """Send text to a matrix room. Args: @@ -39,6 +40,9 @@ async def send_text_to_room( reply_to_event_id: Whether this message is a reply to another event. The event ID this is message is a reply to. + + Returns: + A RoomSendResponse if the request was successful, else an ErrorResponse. """ # Determine whether to ping room members or not msgtype = "m.notice" if notice else "m.text" @@ -56,7 +60,7 @@ async def send_text_to_room( content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to_event_id}} try: - await client.room_send( + return await client.room_send( room_id, "m.room.message", content, @@ -125,7 +129,7 @@ async def react_to_event( ) -async def decryption_failure(self, room: MatrixRoom, event: MegolmEvent): +async def decryption_failure(self, room: MatrixRoom, event: MegolmEvent) -> None: """Callback for when an event fails to decrypt. Inform the user""" logger.error( f"Failed to decrypt event '{event.event_id}' in room '{room.room_id}'!" diff --git a/my_project_name/config.py b/my_project_name/config.py index 87c7c83..f7e86bd 100644 --- a/my_project_name/config.py +++ b/my_project_name/config.py @@ -2,7 +2,7 @@ import logging import os import re import sys -from typing import Any, List +from typing import Any, List, Optional import yaml @@ -14,11 +14,11 @@ logging.getLogger("peewee").setLevel( ) # Prevent debug messages from peewee lib -class Config(object): - def __init__(self, filepath): +class Config: + def __init__(self, filepath: str): """ Args: - filepath (str): Path to config file + filepath: Path to a config file """ if not os.path.isfile(filepath): raise ConfigError(f"Config file '{filepath}' does not exist") @@ -104,15 +104,15 @@ class Config(object): def _get_cfg( self, path: List[str], - default: Any = None, - required: bool = True, + default: Optional[Any] = None, + required: Optional[bool] = True, ) -> Any: """Get a config option from a path and option name, specifying whether it is required. Raises: - ConfigError: If required is specified and the object is not found - (and there is no default value provided), this error will be raised + ConfigError: If required is True and the object is not found (and there is + no default value provided), a ConfigError will be raised. """ # Sift through the the config until we reach our option config = self.config @@ -128,5 +128,5 @@ class Config(object): # or return the default value return default - # We found the option. Return it + # We found the option. Return it. return config diff --git a/my_project_name/errors.py b/my_project_name/errors.py index 5b7887c..70720c9 100644 --- a/my_project_name/errors.py +++ b/my_project_name/errors.py @@ -1,9 +1,12 @@ +# This file holds custom error types that you can define for your application. + + class ConfigError(RuntimeError): """An error encountered during reading the config file Args: - msg (str): The message displayed to the user on error + msg: The message displayed to the user on error """ - def __init__(self, msg): + def __init__(self, msg: str): super(ConfigError, self).__init__("%s" % (msg,)) diff --git a/my_project_name/main.py b/my_project_name/main.py index a7d3936..e00d4a5 100644 --- a/my_project_name/main.py +++ b/my_project_name/main.py @@ -112,4 +112,5 @@ async def main(): await client.close() +# Run the main function in an asyncio event loop asyncio.get_event_loop().run_until_complete(main()) diff --git a/my_project_name/message_responses.py b/my_project_name/message_responses.py index 006888d..654407a 100644 --- a/my_project_name/message_responses.py +++ b/my_project_name/message_responses.py @@ -1,26 +1,38 @@ import logging +from nio import AsyncClient, MatrixRoom, RoomMessageText + from my_project_name.chat_functions import send_text_to_room +from my_project_name.config import Config +from my_project_name.storage import Storage logger = logging.getLogger(__name__) -class Message(object): - def __init__(self, client, store, config, message_content, room, event): +class Message: + def __init__( + self, + client: AsyncClient, + store: Storage, + config: Config, + message_content: str, + room: MatrixRoom, + event: RoomMessageText, + ): """Initialize a new Message Args: - client (nio.AsyncClient): nio client used to interact with matrix + client: nio client used to interact with matrix - store (Storage): Bot storage + store: Bot storage - config (Config): Bot configuration parameters + config: Bot configuration parameters - message_content (str): The body of the message + message_content: The body of the message - room (nio.rooms.MatrixRoom): The room the event came from + room: The room the event came from - event (nio.events.room_events.RoomMessageText): The event defining the message + event: The event defining the message """ self.client = client self.store = store @@ -29,12 +41,12 @@ class Message(object): self.room = room self.event = event - async def process(self): + async def process(self) -> None: """Process and possibly respond to the message""" if self.message_content.lower() == "hello world": await self._hello_world() - async def _hello_world(self): + async def _hello_world(self) -> None: """Say hello""" text = "Hello, world!" await send_text_to_room(self.client, self.room.room_id, text) diff --git a/my_project_name/storage.py b/my_project_name/storage.py index 8fd506d..01f9c8c 100644 --- a/my_project_name/storage.py +++ b/my_project_name/storage.py @@ -1,4 +1,5 @@ import logging +from typing import Any, Dict # The latest migration version of the database. # @@ -12,8 +13,8 @@ latest_migration_version = 0 logger = logging.getLogger(__name__) -class Storage(object): - def __init__(self, database_config): +class Storage: + def __init__(self, database_config: Dict[str, str]): """Setup the database Runs an initial setup or migrations depending on whether a database file has already @@ -45,7 +46,10 @@ class Storage(object): logger.info(f"Database initialization of type '{self.db_type}' complete") - def _get_database_connection(self, database_type: str, connection_string: str): + def _get_database_connection( + self, database_type: str, connection_string: str + ) -> Any: + """Creates and returns a connection to the database""" if database_type == "sqlite": import sqlite3 @@ -61,7 +65,7 @@ class Storage(object): return conn - def _initial_setup(self): + def _initial_setup(self) -> None: """Initial setup of the database""" logger.info("Performing initial database setup...") @@ -88,13 +92,13 @@ class Storage(object): logger.info("Database setup complete") - def _run_migrations(self, current_migration_version: int): + def _run_migrations(self, current_migration_version: int) -> None: """Execute database migrations. Migrates the database to the `latest_migration_version` Args: current_migration_version: The migration version that the database is - currently at + currently at. """ logger.debug("Checking for necessary database migrations...") @@ -108,8 +112,14 @@ class Storage(object): # # logger.info("Database migrated to v1") - def _execute(self, *args): - """A wrapper around cursor.execute that transforms placeholder ?'s to %s for postgres""" + def _execute(self, *args) -> None: + """A wrapper around cursor.execute that transforms placeholder ?'s to %s for postgres. + + This allows for the support of queries that are compatible with both postgres and sqlite. + + Args: + args: Arguments passed to cursor.execute. + """ if self.db_type == "postgres": self.cursor.execute(args[0].replace("?", "%s"), *args[1:]) else: