From a76f7cbf615771c03ba507c3a4e0f44cd0ee96bf Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 11:14:55 +0200 Subject: [PATCH 01/30] Be more consistant with U as shorthand for Union --- src/niobot/attachment.py | 57 ++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/niobot/attachment.py b/src/niobot/attachment.py index 2c1169e..ab34931 100644 --- a/src/niobot/attachment.py +++ b/src/niobot/attachment.py @@ -85,7 +85,7 @@ SUPPORTED_CODECS = SUPPORTED_VIDEO_CODECS + SUPPORTED_AUDIO_CODECS + SUPPORTED_IMAGE_CODECS -def detect_mime_type(file: typing.Union[str, io.BytesIO, pathlib.Path]) -> str: +def detect_mime_type(file: U[str, io.BytesIO, pathlib.Path]) -> str: """ Detect the mime type of a file. @@ -112,7 +112,7 @@ def detect_mime_type(file: typing.Union[str, io.BytesIO, pathlib.Path]) -> str: raise TypeError("File must be a string, BytesIO, or Path object.") -def get_metadata_ffmpeg(file: typing.Union[str, pathlib.Path]) -> typing.Dict[str, typing.Any]: +def get_metadata_ffmpeg(file: U[str, pathlib.Path]) -> typing.Dict[str, typing.Any]: """ Gets metadata for a file via ffprobe. @@ -180,7 +180,9 @@ def get_metadata_imagemagick(file: pathlib.Path) -> typing.Dict[str, typing.Any] return data -def get_metadata(file: typing.Union[str, pathlib.Path], mime_type: str = None) -> typing.Dict[str, typing.Any]: +def get_metadata( + file: U[str, pathlib.Path], mime_type: typing.Optional[str] = None +) -> typing.Dict[str, typing.Any]: """ Gets metadata for a file. @@ -308,7 +310,7 @@ def _file_okay(file: U[pathlib.Path, io.BytesIO]) -> typing.Literal[True]: return True -def _to_path(file: U[str, pathlib.Path, io.BytesIO]) -> typing.Union[pathlib.Path, io.BytesIO]: +def _to_path(file: U[str, pathlib.Path, io.BytesIO]) -> U[pathlib.Path, io.BytesIO]: """Converts a string to a Path object.""" if not isinstance(file, (str, pathlib.PurePath, io.BytesIO)): raise TypeError("File must be a string, BytesIO, or Path object.") @@ -331,7 +333,7 @@ def _size(file: U[pathlib.Path, io.BytesIO]) -> int: def which( file: U[io.BytesIO, pathlib.Path, str], mime_type: str = None -) -> typing.Union[ +) -> U[ typing.Type["FileAttachment"], typing.Type["ImageAttachment"], typing.Type["AudioAttachment"], @@ -423,7 +425,7 @@ class BaseAttachment(abc.ABC): """ if typing.TYPE_CHECKING: - file: typing.Union[pathlib.Path, io.BytesIO] + file: U[pathlib.Path, io.BytesIO] file_name: str mime_type: str size: int @@ -434,7 +436,7 @@ class BaseAttachment(abc.ABC): def __init__( self, - file: typing.Union[str, io.BytesIO, pathlib.Path], + file: U[str, io.BytesIO, pathlib.Path], file_name: str = None, mime_type: str = None, size_bytes: int = None, @@ -482,7 +484,7 @@ def as_body(self, body: str = None) -> dict: @classmethod async def from_file( cls, - file: typing.Union[str, io.BytesIO, pathlib.Path], + file: U[str, io.BytesIO, pathlib.Path], file_name: str = None, ) -> "BaseAttachment": """ @@ -495,11 +497,10 @@ async def from_file( :return: Loaded attachment. """ file = _to_path(file) - if isinstance(file, io.BytesIO): - if not file_name: + if not file_name: + if isinstance(file, io.BytesIO): raise ValueError("file_name must be specified when uploading a BytesIO object.") - else: - if not file_name: + else: file_name = file.name mime_type = await run_blocking(detect_mime_type, file) @@ -588,14 +589,14 @@ async def from_http( else: save_path = tempdir - if save_path is not None: - async with aiofiles.open(save_path, "wb") as fh: - async for chunk in response.content.iter_chunked(1024): - await fh.write(chunk) - return await cls.from_file(save_path, file_name) - else: + if save_path is None: return await cls.from_file(io.BytesIO(await response.read()), file_name) + async with aiofiles.open(save_path, "wb") as fh: + async for chunk in response.content.iter_chunked(1024): + await fh.write(chunk) + return await cls.from_file(save_path, file_name) + @property def size_bytes(self) -> int: """Returns the size of this attachment in bytes.""" @@ -612,7 +613,7 @@ def size_as( "gb", "gib", ], - ) -> typing.Union[int, float]: + ) -> U[int, float]: """ Helper function to convert the size of this attachment into a different unit. @@ -722,7 +723,7 @@ def __init__(self, *args, xyz_amorgan_blurhash: str = None, **kwargs): @classmethod async def from_file( cls, - file: typing.Union[str, io.BytesIO, pathlib.Path], + file: U[str, io.BytesIO, pathlib.Path], file_name: str = None, xyz_amorgan_blurhash: U[str, bool] = None, ) -> "SupportXYZAmorganBlurHash": @@ -822,7 +823,7 @@ class FileAttachment(BaseAttachment): def __init__( self, - file: typing.Union[str, io.BytesIO, pathlib.Path], + file: U[str, io.BytesIO, pathlib.Path], file_name: str = None, mime_type: str = None, size_bytes: int = None, @@ -849,7 +850,7 @@ class ImageAttachment(SupportXYZAmorganBlurHash): def __init__( self, - file: typing.Union[str, io.BytesIO, pathlib.Path], + file: U[str, io.BytesIO, pathlib.Path], file_name: str = None, mime_type: str = None, size_bytes: int = None, @@ -877,7 +878,7 @@ def __init__( @classmethod async def from_file( cls, - file: typing.Union[str, io.BytesIO, pathlib.Path], + file: U[str, io.BytesIO, pathlib.Path], file_name: str = None, height: int = None, width: int = None, @@ -956,7 +957,7 @@ class VideoAttachment(BaseAttachment): def __init__( self, - file: typing.Union[str, io.BytesIO, pathlib.Path], + file: U[str, io.BytesIO, pathlib.Path], file_name: str = None, mime_type: str = None, size_bytes: int = None, @@ -978,7 +979,7 @@ def __init__( @classmethod async def from_file( cls, - file: typing.Union[str, io.BytesIO, pathlib.Path], + file: U[str, io.BytesIO, pathlib.Path], file_name: str = None, duration: int = None, height: int = None, @@ -1051,7 +1052,7 @@ async def from_file( return self @staticmethod - async def generate_thumbnail(video: typing.Union[str, pathlib.Path, "VideoAttachment"]) -> ImageAttachment: + async def generate_thumbnail(video: U[str, pathlib.Path, "VideoAttachment"]) -> ImageAttachment: """ Generates a thumbnail for a video. @@ -1086,7 +1087,7 @@ class AudioAttachment(BaseAttachment): def __init__( self, - file: typing.Union[str, io.BytesIO, pathlib.Path], + file: U[str, io.BytesIO, pathlib.Path], file_name: str = None, mime_type: str = None, size_bytes: int = None, @@ -1102,7 +1103,7 @@ def __init__( @classmethod async def from_file( cls, - file: typing.Union[str, io.BytesIO, pathlib.Path], + file: U[str, io.BytesIO, pathlib.Path], file_name: str = None, duration: int = None, ) -> "AudioAttachment": From 73c7ac3eb40909f4ae1c2e1093bc5330c30b9d0b Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 11:39:38 +0200 Subject: [PATCH 02/30] Add function overload for _to_path --- src/niobot/attachment.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/niobot/attachment.py b/src/niobot/attachment.py index ab34931..a5c0639 100644 --- a/src/niobot/attachment.py +++ b/src/niobot/attachment.py @@ -17,6 +17,7 @@ import urllib.parse import warnings from typing import Union as U +from typing import overload import aiofiles import aiohttp @@ -180,9 +181,7 @@ def get_metadata_imagemagick(file: pathlib.Path) -> typing.Dict[str, typing.Any] return data -def get_metadata( - file: U[str, pathlib.Path], mime_type: typing.Optional[str] = None -) -> typing.Dict[str, typing.Any]: +def get_metadata(file: U[str, pathlib.Path], mime_type: typing.Optional[str] = None) -> typing.Dict[str, typing.Any]: """ Gets metadata for a file. @@ -310,9 +309,19 @@ def _file_okay(file: U[pathlib.Path, io.BytesIO]) -> typing.Literal[True]: return True +@overload +def _to_path(file: U[str, pathlib.Path]) -> pathlib.Path: + ... + + +@overload +def _to_path(file: io.BytesIO) -> io.BytesIO: + ... + + def _to_path(file: U[str, pathlib.Path, io.BytesIO]) -> U[pathlib.Path, io.BytesIO]: """Converts a string to a Path object.""" - if not isinstance(file, (str, pathlib.PurePath, io.BytesIO)): + if not isinstance(file, (str, pathlib.Path, io.BytesIO)): raise TypeError("File must be a string, BytesIO, or Path object.") if isinstance(file, io.BytesIO): From 501f44b3f6233ac54a7d1a96690a2972dd0f2039 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 13:27:33 +0200 Subject: [PATCH 03/30] Fix type hinting in attachments.py --- src/niobot/attachment.py | 145 ++++++++++++++++++++------------------- 1 file changed, 76 insertions(+), 69 deletions(-) diff --git a/src/niobot/attachment.py b/src/niobot/attachment.py index a5c0639..c1ce5d8 100644 --- a/src/niobot/attachment.py +++ b/src/niobot/attachment.py @@ -341,7 +341,7 @@ def _size(file: U[pathlib.Path, io.BytesIO]) -> int: def which( - file: U[io.BytesIO, pathlib.Path, str], mime_type: str = None + file: U[io.BytesIO, pathlib.Path, str], mime_type: typing.Optional[str] = None ) -> U[ typing.Type["FileAttachment"], typing.Type["ImageAttachment"], @@ -446,9 +446,9 @@ class BaseAttachment(abc.ABC): def __init__( self, file: U[str, io.BytesIO, pathlib.Path], - file_name: str = None, - mime_type: str = None, - size_bytes: int = None, + file_name: typing.Optional[str] = None, + mime_type: typing.Optional[str] = None, + size_bytes: typing.Optional[int] = None, *, attachment_type: AttachmentType = AttachmentType.FILE, ): @@ -457,7 +457,12 @@ def __init__( if not self.file_name: raise ValueError("file_name must be specified when uploading a BytesIO object.") self.mime_type = mime_type or detect_mime_type(self.file) - self.size = size_bytes or os.path.getsize(self.file) + if size_bytes: + self.size = size_bytes + elif isinstance(self.file, io.BytesIO): + self.size = len(self.file.getbuffer()) + else: + os.path.getsize(self.file) self.type = attachment_type self.url = None @@ -469,14 +474,14 @@ def __repr__(self): "mime_type={0.mime_type!r} size={0.size!r} type={0.type!r}>".format(self) ) - def as_body(self, body: str = None) -> dict: + def as_body(self, body: typing.Optional[str] = None) -> dict: """ Generates the body for the attachment for sending. The attachment must've been uploaded first. :param body: The body to use (should be a textual description). Defaults to the file name. :return: """ - body = { + output_body = { "body": body or self.file_name, "info": { "mimetype": self.mime_type, @@ -487,14 +492,14 @@ def as_body(self, body: str = None) -> dict: "url": self.url, } if self.keys: - body["file"] = self.keys - return body + output_body["file"] = self.keys + return output_body @classmethod async def from_file( cls, file: U[str, io.BytesIO, pathlib.Path], - file_name: str = None, + file_name: typing.Optional[str] = None, ) -> "BaseAttachment": """ Creates an attachment from a file. @@ -547,7 +552,7 @@ async def from_mxc( async def from_http( cls, url: str, - client_session: aiohttp.ClientSession = None, + client_session: typing.Optional[aiohttp.ClientSession] = None, *, force_write: U[bool, pathlib.Path] = False, ) -> "BaseAttachment": @@ -725,7 +730,7 @@ class SupportXYZAmorganBlurHash(BaseAttachment): if typing.TYPE_CHECKING: xyz_amorgan_blurhash: str - def __init__(self, *args, xyz_amorgan_blurhash: str = None, **kwargs): + def __init__(self, *args, xyz_amorgan_blurhash: typing.Optional[str] = None, **kwargs): super().__init__(*args, **kwargs) self.xyz_amorgan_blurhash = xyz_amorgan_blurhash @@ -733,8 +738,8 @@ def __init__(self, *args, xyz_amorgan_blurhash: str = None, **kwargs): async def from_file( cls, file: U[str, io.BytesIO, pathlib.Path], - file_name: str = None, - xyz_amorgan_blurhash: U[str, bool] = None, + file_name: typing.Optional[str] = None, + xyz_amorgan_blurhash: typing.Optional[U[str, bool]] = None, ) -> "SupportXYZAmorganBlurHash": file = _to_path(file) if isinstance(file, io.BytesIO): @@ -775,7 +780,9 @@ def thumbnailify_image( return image async def get_blurhash( - self, quality: typing.Tuple[int, int] = (4, 3), file: U[str, pathlib.Path, io.BytesIO, PIL.Image.Image] = None + self, + quality: typing.Tuple[int, int] = (4, 3), + file: typing.Optional[U[str, pathlib.Path, io.BytesIO, PIL.Image.Image]] = None, ) -> str: """ Gets the blurhash of the attachment. See: [woltapp/blurhash](https://github.com/woltapp/blurhash) @@ -808,11 +815,11 @@ async def get_blurhash( self.xyz_amorgan_blurhash = x return x - def as_body(self, body: str = None) -> dict: - body = super().as_body(body) + def as_body(self, body: typing.Optional[str] = None) -> dict: + output_body = super().as_body(body) if isinstance(self.xyz_amorgan_blurhash, str): - body["info"]["xyz.amorgan.blurhash"] = self.xyz_amorgan_blurhash - return body + output_body["info"]["xyz.amorgan.blurhash"] = self.xyz_amorgan_blurhash + return output_body class FileAttachment(BaseAttachment): @@ -833,9 +840,9 @@ class FileAttachment(BaseAttachment): def __init__( self, file: U[str, io.BytesIO, pathlib.Path], - file_name: str = None, - mime_type: str = None, - size_bytes: int = None, + file_name: typing.Optional[str] = None, + mime_type: typing.Optional[str] = None, + size_bytes: typing.Optional[int] = None, ): super().__init__(file, file_name, mime_type, size_bytes, attachment_type=AttachmentType.FILE) @@ -860,13 +867,13 @@ class ImageAttachment(SupportXYZAmorganBlurHash): def __init__( self, file: U[str, io.BytesIO, pathlib.Path], - file_name: str = None, - mime_type: str = None, - size_bytes: int = None, - height: int = None, - width: int = None, - thumbnail: "ImageAttachment" = None, - xyz_amorgan_blurhash: str = None, + file_name: typing.Optional[str] = None, + mime_type: typing.Optional[str] = None, + size_bytes: typing.Optional[int] = None, + height: typing.Optional[int] = None, + width: typing.Optional[int] = None, + thumbnail: typing.Optional["ImageAttachment"] = None, + xyz_amorgan_blurhash: typing.Optional[str] = None, ): super().__init__( file, @@ -888,10 +895,10 @@ def __init__( async def from_file( cls, file: U[str, io.BytesIO, pathlib.Path], - file_name: str = None, - height: int = None, - width: int = None, - thumbnail: "ImageAttachment" = None, + file_name: typing.Optional[str] = None, + height: typing.Optional[int] = None, + width: typing.Optional[int] = None, + thumbnail: typing.Optional["ImageAttachment"] = None, generate_blurhash: bool = True, *, unsafe: bool = False, @@ -939,15 +946,15 @@ async def from_file( await self.get_blurhash() return self - def as_body(self, body: str = None) -> dict: - body = super().as_body(body) - body["info"] = {**body["info"], **self.info} + def as_body(self, body: typing.Optional[str] = None) -> dict: + output_body = super().as_body(body) + output_body["info"] = {**output_body["info"], **self.info} if self.thumbnail: if self.thumbnail.keys: - body["info"]["thumbnail_file"] = self.thumbnail.keys - body["info"]["thumbnail_info"] = self.thumbnail.info - body["info"]["thumbnail_url"] = self.thumbnail.url - return body + output_body["info"]["thumbnail_file"] = self.thumbnail.keys + output_body["info"]["thumbnail_info"] = self.thumbnail.info + output_body["info"]["thumbnail_url"] = self.thumbnail.url + return output_body class VideoAttachment(BaseAttachment): @@ -967,13 +974,13 @@ class VideoAttachment(BaseAttachment): def __init__( self, file: U[str, io.BytesIO, pathlib.Path], - file_name: str = None, - mime_type: str = None, - size_bytes: int = None, - duration: int = None, - height: int = None, - width: int = None, - thumbnail: "ImageAttachment" = None, + file_name: typing.Optional[str] = None, + mime_type: typing.Optional[str] = None, + size_bytes: typing.Optional[int] = None, + duration: typing.Optional[int] = None, + height: typing.Optional[int] = None, + width: typing.Optional[int] = None, + thumbnail: typing.Optional["ImageAttachment"] = None, ): super().__init__(file, file_name, mime_type, size_bytes, attachment_type=AttachmentType.VIDEO) self.info = { @@ -989,11 +996,11 @@ def __init__( async def from_file( cls, file: U[str, io.BytesIO, pathlib.Path], - file_name: str = None, - duration: int = None, - height: int = None, - width: int = None, - thumbnail: U[ImageAttachment, typing.Literal[False]] = None, + file_name: typing.Optional[str] = None, + duration: typing.Optional[int] = None, + height: typing.Optional[int] = None, + width: typing.Optional[int] = None, + thumbnail: typing.Optional[U[ImageAttachment, typing.Literal[False]]] = None, generate_blurhash: bool = True, ) -> "VideoAttachment": """ @@ -1078,15 +1085,15 @@ async def generate_thumbnail(video: U[str, pathlib.Path, "VideoAttachment"]) -> x = await run_blocking(first_frame, video, "webp") return await ImageAttachment.from_file(io.BytesIO(x), file_name="thumbnail.webp") - def as_body(self, body: str = None) -> dict: - body = super().as_body(body) - body["info"] = {**body["info"], **self.info} + def as_body(self, body: typing.Optional[str] = None) -> dict: + output_body = super().as_body(body) + output_body["info"] = {**output_body["info"], **self.info} if self.thumbnail: if self.thumbnail.keys: - body["info"]["thumbnail_file"] = self.thumbnail.keys - body["info"]["thumbnail_info"] = self.thumbnail.info - body["info"]["thumbnail_url"] = self.thumbnail.url - return body + output_body["info"]["thumbnail_file"] = self.thumbnail.keys + output_body["info"]["thumbnail_info"] = self.thumbnail.info + output_body["info"]["thumbnail_url"] = self.thumbnail.url + return output_body class AudioAttachment(BaseAttachment): @@ -1097,10 +1104,10 @@ class AudioAttachment(BaseAttachment): def __init__( self, file: U[str, io.BytesIO, pathlib.Path], - file_name: str = None, - mime_type: str = None, - size_bytes: int = None, - duration: int = None, + file_name: typing.Optional[str] = None, + mime_type: typing.Optional[str] = None, + size_bytes: typing.Optional[int] = None, + duration: typing.Optional[int] = None, ): super().__init__(file, file_name, mime_type, size_bytes, attachment_type=AttachmentType.AUDIO) self.info = { @@ -1113,8 +1120,8 @@ def __init__( async def from_file( cls, file: U[str, io.BytesIO, pathlib.Path], - file_name: str = None, - duration: int = None, + file_name: typing.Optional[str] = None, + duration: typing.Optional[int] = None, ) -> "AudioAttachment": """ Generates an audio attachment @@ -1140,7 +1147,7 @@ async def from_file( self = cls(file, file_name, mime_type, size, duration) return self - def as_body(self, body: str = None) -> dict: - body = super().as_body(body) - body["info"] = {**body["info"], **self.info} - return body + def as_body(self, body: typing.Optional[str] = None) -> dict: + output_body = super().as_body(body) + output_body["info"] = {**output_body["info"], **self.info} + return output_body From a01453407c9905f2f60d924ac126e1ba3c6d37a8 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 13:39:16 +0200 Subject: [PATCH 04/30] Proper type hinting for run_blocking --- src/niobot/attachment.py | 6 ++++-- src/niobot/utils/unblocking.py | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/niobot/attachment.py b/src/niobot/attachment.py index c1ce5d8..b41f354 100644 --- a/src/niobot/attachment.py +++ b/src/niobot/attachment.py @@ -1063,8 +1063,10 @@ async def from_file( if isinstance(self.thumbnail, ImageAttachment): await self.thumbnail.get_blurhash() elif isinstance(file, pathlib.Path) and original_thumbnail is not False: - thumbnail = await run_blocking(first_frame, file) - self.thumbnail = await ImageAttachment.from_file(io.BytesIO(thumbnail), file_name="thumbnail.webp") + thumbnail_bytes = await run_blocking(first_frame, file) + self.thumbnail = await ImageAttachment.from_file( + io.BytesIO(thumbnail_bytes), file_name="thumbnail.webp" + ) return self @staticmethod diff --git a/src/niobot/utils/unblocking.py b/src/niobot/utils/unblocking.py index d5c8f8c..4adf0e8 100644 --- a/src/niobot/utils/unblocking.py +++ b/src/niobot/utils/unblocking.py @@ -2,11 +2,14 @@ import functools import typing from typing import Any +from collections.abc import Callable __all__ = ("run_blocking", "force_await") +T = typing.TypeVar("T") -async def run_blocking(function: typing.Callable, *args: Any, **kwargs: Any) -> Any: + +async def run_blocking(function: Callable[..., T], *args: Any, **kwargs: Any) -> T: """ Takes a blocking function and runs it in a thread, returning the result. From 8325349676f6d52a1bbbd99b20c055060166b791 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 15:56:58 +0200 Subject: [PATCH 05/30] Improve typing in client.py --- src/niobot/client.py | 53 ++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/src/niobot/client.py b/src/niobot/client.py index 7e40f0a..8f5e7df 100644 --- a/src/niobot/client.py +++ b/src/niobot/client.py @@ -52,15 +52,15 @@ def __init__( homeserver: str, user_id: str, device_id: str = "nio-bot", - store_path: str = None, + store_path: typing.Optional[str] = None, *, command_prefix: typing.Union[str, re.Pattern], case_insensitive: bool = True, - owner_id: str = None, - config: nio.AsyncClientConfig = None, + owner_id: typing.Optional[str] = None, + config: typing.Optional[nio.AsyncClientConfig] = None, ssl: bool = True, - proxy: str = None, - help_command: typing.Union[Command, typing.Callable[["Context"], typing.Any]] = None, + proxy: typing.Optional[str] = None, + help_command: typing.Optional[typing.Union[Command, typing.Callable[["Context"], typing.Any]]] = None, global_message_type: str = "m.notice", ignore_old_events: bool = True, auto_join_rooms: bool = True, @@ -104,7 +104,7 @@ def __init__( if command_prefix == "/": self.log.warning("The prefix '/' may interfere with client-side commands on some clients, such as Element.") - if re.match(r"\s", command_prefix): + if isinstance(command_prefix, str) and re.match(r"\s", command_prefix): raise RuntimeError("Command prefix cannot contain whitespace.") self.start_time: typing.Optional[float] = None @@ -184,7 +184,7 @@ async def _auto_join_room_backlog_callback(self, room: nio.MatrixRoom, event: ni await self._auto_join_room_callback(room, event) @staticmethod - def latency(event: nio.Event, *, received_at: float = None) -> float: + def latency(event: nio.Event, *, received_at: typing.Optional[float] = None) -> float: """Returns the latency for a given event in milliseconds :param event: The event to measure latency with @@ -238,8 +238,8 @@ async def update_read_receipts(self, room: U[str, nio.MatrixRoom], event: nio.Ev if self.is_old(event): self.log.debug("Ignoring event %s, sent before bot started.", event.event_id) return - event = event.event_id - result = await self.room_read_markers(room, event, event) + event_id = event.event_id + result = await self.room_read_markers(room, event_id, event_id) if not isinstance(result, nio.RoomReadMarkersResponse): self.log.warning("Failed to update read receipts for %s: %s", room, result.message) else: @@ -443,7 +443,7 @@ def remove_command(self, command: Command) -> None: for alias in command.aliases: self.log.debug("Removed command %r from the register.", self._commands.pop(alias, None)) - def command(self, name: str = None, **kwargs): + def command(self, name: typing.Optional[str] = None, **kwargs): """Registers a command with the bot.""" cls = kwargs.pop("cls", Command) @@ -461,15 +461,15 @@ def add_event_listener(self, event_type: str, func): self._events[event_type].append(func) self.log.debug("Added event listener %r for %r", func, event_type) - def on_event(self, event_type: str = None): + def on_event(self, event_type: typing.Optional[str] = None): """Wrapper that allows you to register an event handler""" - if event_type.startswith("on_"): - self.log.warning("No events start with 'on_' - stripping prefix") - event_type = event_type[3:] def wrapper(func): nonlocal event_type event_type = event_type or func.__name__ + if event_type.startswith("on_"): + self.log.warning("No events start with 'on_' - stripping prefix") + event_type = event_type[3:] self.add_event_listener(event_type, func) return func @@ -524,11 +524,11 @@ async def fetch_message(self, room_id: str, event_id: str): async def wait_for_message( self, - room_id: str = None, - sender: str = None, - check: typing.Callable[[nio.MatrixRoom, nio.RoomMessageText], typing.Any] = None, + room_id: typing.Optional[str] = None, + sender: typing.Optional[str] = None, + check: typing.Optional[typing.Callable[[nio.MatrixRoom, nio.RoomMessageText], typing.Any]] = None, *, - timeout: float = None, + timeout: typing.Optional[float] = None, ) -> typing.Optional[typing.Tuple[nio.MatrixRoom, nio.RoomMessageText]]: """Waits for a message, optionally with a filter. @@ -606,7 +606,7 @@ def generate_mx_reply(room: nio.MatrixRoom, event: nio.RoomMessageText) -> str: ) async def _recursively_upload_attachments( - self, base: "BaseAttachment", encrypted: bool = False, __previous: list = None + self, base: "BaseAttachment", encrypted: bool = False, __previous: typing.Optional[list] = None ) -> list[typing.Union[nio.UploadResponse, nio.UploadError, type(None)]]: """Recursively uploads attachments.""" previous = (__previous or []).copy() @@ -646,20 +646,21 @@ async def get_dm_room(self, user: U[nio.MatrixUser, str]) -> nio.MatrixRoom: if not isinstance(room, nio.RoomCreateResponse): raise NioBotException("Unable to create DM room for %r: %r" % (user_id, room), response=room) self.log.debug("Created DM room for %r: %r", user_id, room) - room = self.rooms.get(room.room_id) + room_id = room.room_id + room = self.rooms.get(room_id) if not room: - raise RuntimeError("DM room %r was created, but could not be found in the room list!" % room.room_id) + raise RuntimeError("DM room %r was created, but could not be found in the room list!" % room_id) self.direct_rooms[user_id] = room return room async def send_message( self, room: U[nio.MatrixRoom, nio.MatrixUser, str], - content: str = None, - file: BaseAttachment = None, - reply_to: U[nio.RoomMessageText, str] = None, - message_type: str = None, - clean_mentions: bool = False, + content: typing.Optional[str] = None, + file: typing.Optional[BaseAttachment] = None, + reply_to: typing.Optional[U[nio.RoomMessageText, str]] = None, + message_type: typing.Optional[str] = None, + clean_mentions: typing.Optional[bool] = False, ) -> nio.RoomSendResponse: """ Sends a message. From 9b17b8653d84b64c0c044a1af09b0d20d3de569e Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 16:04:16 +0200 Subject: [PATCH 06/30] More typing improvements for client.py --- src/niobot/client.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/niobot/client.py b/src/niobot/client.py index 8f5e7df..8216e0e 100644 --- a/src/niobot/client.py +++ b/src/niobot/client.py @@ -740,7 +740,7 @@ async def edit_message( message: U[nio.Event, str], content: str, *, - message_type: str = None, + message_type: typing.Optional[str] = None, clean_mentions: bool = False, ) -> nio.RoomSendResponse: """ @@ -760,7 +760,7 @@ async def edit_message( content = content.replace("@", "@\u200b") event_id = self._get_id(message) message_type = message_type or self.global_message_type - content = { + content_dict = { "msgtype": message_type, "body": content, "format": "org.matrix.custom.html", @@ -769,8 +769,8 @@ async def edit_message( body = { "msgtype": message_type, - "body": " * %s" % content["body"], - "m.new_content": {**content}, + "body": " * %s" % content_dict["body"], + "m.new_content": {**content_dict}, "m.relates_to": { "rel_type": "m.replace", "event_id": event_id, @@ -789,7 +789,7 @@ async def edit_message( return response async def delete_message( - self, room: U[nio.MatrixRoom, str], message_id: U[nio.RoomMessage, str], reason: str = None + self, room: U[nio.MatrixRoom, str], message_id: U[nio.RoomMessage, str], reason: typing.Optional[str] = None ) -> nio.RoomRedactResponse: """ Delete an existing message. You must be the sender of the message. @@ -845,7 +845,12 @@ async def redact_reaction(self, room: U[nio.MatrixRoom, str], reaction: U[nio.Ro raise MessageException("Failed to delete reaction.", response) return response - async def start(self, password: str = None, access_token: str = None, sso_token: str = None) -> None: + async def start( + self, + password: typing.Optional[str] = None, + access_token: typing.Optional[str] = None, + sso_token: typing.Optional[str] = None, + ) -> None: """Starts the bot, running the sync loop.""" self.loop = asyncio.get_event_loop() if password or sso_token: @@ -900,7 +905,13 @@ async def start(self, password: str = None, access_token: str = None, sso_token: self.log.info("Closing http session and logging out.") await self.close() - def run(self, *, password: str = None, access_token: str = None, sso_token: str = None) -> None: + def run( + self, + *, + password: typing.Optional[str] = None, + access_token: typing.Optional[str] = None, + sso_token: typing.Optional[str] = None, + ) -> None: """ Runs the bot, blocking the program until the event loop exists. This should be the last function to be called in your script, as once it exits, the bot will stop running. From 08db0c567c4fd10c1a68087ef77eefa9e8e82efb Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 16:07:10 +0200 Subject: [PATCH 07/30] simplify code in client.py --- src/niobot/client.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/niobot/client.py b/src/niobot/client.py index 8216e0e..813b80b 100644 --- a/src/niobot/client.py +++ b/src/niobot/client.py @@ -145,7 +145,7 @@ def __init__( self.is_ready = asyncio.Event() self._waiting_events = {} - if self.auto_join_rooms is True: + if self.auto_join_rooms: self.log.info("Auto-joining rooms enabled.") # noinspection PyTypeChecker self.add_event_callback(self._auto_join_room_backlog_callback, nio.InviteMemberEvent) @@ -537,14 +537,12 @@ async def wait_for_message( value = None async def event_handler(_room, _event): - if room_id: - if _room.room_id != room_id: - self.log.debug("Ignoring bubbling message from %r (vs %r)", _room.room_id, room_id) - return False - if sender: - if _event.sender != sender: - self.log.debug("Ignoring bubbling message from %r (vs %r)", _event.sender, sender) - return False + if room_id and _room.room_id != room_id: + self.log.debug("Ignoring bubbling message from %r (vs %r)", _room.room_id, room_id) + return False + if sender and _event.sender != sender: + self.log.debug("Ignoring bubbling message from %r (vs %r)", _event.sender, sender) + return False if check: try: result = await force_await(check, _room, _event) @@ -864,10 +862,10 @@ async def start( login_response = await self.login(password=password, token=sso_token, device_name=self.device_id) if isinstance(login_response, nio.LoginError): raise LoginException("Failed to log in.", login_response) - else: - self.log.info("Logged in as %s", login_response.user_id) - self.log.debug("Logged in: {0.access_token}, {0.user_id}".format(login_response)) - self.start_time = time.time() + + self.log.info("Logged in as %s", login_response.user_id) + self.log.debug("Logged in: {0.access_token}, {0.user_id}".format(login_response)) + self.start_time = time.time() elif access_token: self.log.info("Logging in with existing access token.") if self.store_path: From f7e5e017af9fffd24c7b018cb34aee891c31f31f Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 16:16:57 +0200 Subject: [PATCH 08/30] improve typing in commands.py --- src/niobot/commands.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/niobot/commands.py b/src/niobot/commands.py index f3051d8..4ff300d 100644 --- a/src/niobot/commands.py +++ b/src/niobot/commands.py @@ -7,6 +7,7 @@ from .context import Context from .exceptions import * +from collections.abc import Callable if typing.TYPE_CHECKING: from .client import NioBot @@ -46,7 +47,7 @@ def __init__( name: str, arg_type: _T, *, - description: str = None, + description: typing.Optional[str] = None, default: typing.Any = ..., required: bool = ..., parser: typing.Callable[["Context", "Argument", str], typing.Optional[_T]] = ..., @@ -146,10 +147,10 @@ def hello(ctx: niobot.Context): def __init__( self, name: str, - callback: callable, + callback: Callable, *, - aliases: list[str] = None, - description: str = None, + aliases: typing.Optional[list[str]] = None, + description: typing.Optional[str] = None, disabled: bool = False, hidden: bool = False, greedy: bool = False, @@ -325,7 +326,7 @@ def construct_context( return cls(client, room, src_event, self, invoking_string=meta) -def command(name: str = None, **kwargs) -> callable: +def command(name: typing.Optional[str] = None, **kwargs) -> Callable: """ Allows you to register commands later on, by loading modules. @@ -349,8 +350,8 @@ def decorator(func): def check( function: typing.Callable[[Context], typing.Union[bool, typing.Coroutine[None, None, bool]]], - name: str = None, -) -> callable: + name: typing.Optional[str] = None, +) -> Callable: """ Allows you to register checks in modules. @@ -377,7 +378,7 @@ def decorator(command_function): return decorator -def event(name: str) -> callable: +def event(name: str) -> Callable: """ Allows you to register event listeners in modules. From 5357aab16c62f34825acd1e9b88cb7fe0d8be877 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 16:19:04 +0200 Subject: [PATCH 09/30] Some code improvement in commands.py --- src/niobot/commands.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/niobot/commands.py b/src/niobot/commands.py index 4ff300d..7510721 100644 --- a/src/niobot/commands.py +++ b/src/niobot/commands.py @@ -237,16 +237,15 @@ def display_usage(self) -> str: """Returns the usage string for this command, auto-resolved if not pre-defined""" if self.usage: return self.usage - else: - usage = [] - req = "<{!s}>" - opt = "[{!s}]" - for arg in self.arguments[1:]: - if arg.required: - usage.append(req.format(arg.name)) - else: - usage.append(opt.format(arg.name)) - return " ".join(usage) + usage = [] + req = "<{!s}>" + opt = "[{!s}]" + for arg in self.arguments[1:]: + if arg.required: + usage.append(req.format(arg.name)) + else: + usage.append(opt.format(arg.name)) + return " ".join(usage) async def invoke(self, ctx: Context) -> typing.Coroutine: """ @@ -279,9 +278,8 @@ async def invoke(self, ctx: Context) -> typing.Coroutine: if index >= len(ctx.args): if argument.required: raise CommandArgumentsError(f"Missing required argument {argument.name}") - else: - parsed_args.append(argument.default) - continue + parsed_args.append(argument.default) + continue self.log.debug("Resolved argument %s to %r", argument.name, ctx.args[index]) try: From 6095c8d5078698d1bef663eda4901b612797a11c Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 16:36:01 +0200 Subject: [PATCH 10/30] Typing improvements in parsers.py --- src/niobot/utils/parsers.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/niobot/utils/parsers.py b/src/niobot/utils/parsers.py index 4b9ab5f..31fb7e2 100644 --- a/src/niobot/utils/parsers.py +++ b/src/niobot/utils/parsers.py @@ -99,9 +99,7 @@ def __parser(_, __, v) -> typing.Union[int, float]: return __parser -def json_parser( - _: "Context", __: "Argument", value: str -) -> typing.Union[list, dict, str, int, float, type(None), bool]: +def json_parser(_: "Context", __: "Argument", value: str) -> typing.Union[list, dict, str, int, float, None, bool]: """ Converts a given string into a JSON object. @@ -161,7 +159,7 @@ async def room_parser(ctx: "Context", arg: "Argument", value: str) -> nio.Matrix raise CommandParserError(f"Invalid room ID, alias, or matrix.to link: {value!r}.") if room is None: - raise CommandParserError(f"No room with that ID, alias, or matrix.to link found.") + raise CommandParserError("No room with that ID, alias, or matrix.to link found.") return room @@ -244,7 +242,7 @@ async def internal(ctx: "Context", _, value: str) -> MatrixToLink: room = ctx.client.rooms.get(room_id) if room is None: - raise CommandParserError(f"No room with that ID, alias, or matrix.to link found.") + raise CommandParserError("No room with that ID, alias, or matrix.to link found.") if event_id: event: U[nio.RoomGetEventResponse, nio.RoomGetEventError] = await ctx.client.room_get_event( From aace35789a3fb2b8ff6d5a1b30df254a6f7bc37e Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 16:39:56 +0200 Subject: [PATCH 11/30] Code improvement in parsers.py --- src/niobot/utils/parsers.py | 64 ++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/niobot/utils/parsers.py b/src/niobot/utils/parsers.py index 31fb7e2..c9bb833 100644 --- a/src/niobot/utils/parsers.py +++ b/src/niobot/utils/parsers.py @@ -53,9 +53,9 @@ def boolean_parser(_: "Context", __, value: str) -> bool: :return: The parsed boolean """ value = value.lower() - if value in ("1", "y", "yes", "true", "on"): + if value in {"1", "y", "yes", "true", "on"}: return True - if value in ("0", "n", "no", "false", "off"): + if value in {"0", "n", "no", "false", "off"}: return False raise CommandParserError(f"Invalid boolean value: {value}. Should be a sensible value, such as 1, yes, false.") @@ -220,42 +220,42 @@ def matrix_to_parser( """ async def internal(ctx: "Context", _, value: str) -> MatrixToLink: - if m := MATRIX_TO_REGEX.match(value): - # matrix.to link - groups = m.groupdict() - event_id = groups.get("event_id", "") - room_id = groups.get("room_id", "") - event_id = urllib.unquote(event_id) - room_id = urllib.unquote(room_id) + if not (m := MATRIX_TO_REGEX.match(value)): + raise CommandParserError(f"Invalid matrix.to link: {value!r}.") - if require_room and not room_id: - raise CommandParserError(f"Invalid matrix.to link: {value} (no room).") - if require_event and not event_id: - raise CommandParserError(f"Invalid matrix.to link: {value} (no event).") + # matrix.to link + groups = m.groupdict() + event_id = groups.get("event_id", "") + room_id = groups.get("room_id", "") + event_id = urllib.unquote(event_id) + room_id = urllib.unquote(room_id) - if room_id.startswith("@") and not allow_user_as_room: - raise CommandParserError(f"Invalid matrix.to link: {value} (expected room, got user).") + if require_room and not room_id: + raise CommandParserError(f"Invalid matrix.to link: {value} (no room).") + if require_event and not event_id: + raise CommandParserError(f"Invalid matrix.to link: {value} (no event).") + + if room_id.startswith("@") and not allow_user_as_room: + raise CommandParserError(f"Invalid matrix.to link: {value} (expected room, got user).") - if room_id.startswith("@"): - room = await ctx.client.get_dm_room(room_id) - else: - room = ctx.client.rooms.get(room_id) + if room_id.startswith("@"): + room = await ctx.client.get_dm_room(room_id) + else: + room = ctx.client.rooms.get(room_id) - if room is None: - raise CommandParserError("No room with that ID, alias, or matrix.to link found.") + if room is None: + raise CommandParserError("No room with that ID, alias, or matrix.to link found.") - if event_id: - event: U[nio.RoomGetEventResponse, nio.RoomGetEventError] = await ctx.client.room_get_event( - room_id, event_id - ) - if not isinstance(event, nio.RoomGetEventResponse): - raise CommandParserError(f"Invalid event ID: {event_id}.", response=event) - event: nio.Event = event.event - else: - event: None = None - return MatrixToLink(room, event, groups.get("qs")) + if event_id: + event: U[nio.RoomGetEventResponse, nio.RoomGetEventError] = await ctx.client.room_get_event( + room_id, event_id + ) + if not isinstance(event, nio.RoomGetEventResponse): + raise CommandParserError(f"Invalid event ID: {event_id}.", response=event) + event: nio.Event = event.event else: - raise CommandParserError(f"Invalid matrix.to link: {value!r}.") + event: None = None + return MatrixToLink(room, event, groups.get("qs")) return internal From e241f6dd4b139d3c3496932772051bb8be108d99 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 17:01:23 +0200 Subject: [PATCH 12/30] Improve type-hinting in exceptions.py --- src/niobot/exceptions.py | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/niobot/exceptions.py b/src/niobot/exceptions.py index 41380cb..ceab4c9 100644 --- a/src/niobot/exceptions.py +++ b/src/niobot/exceptions.py @@ -47,24 +47,24 @@ class NioBotException(Exception): def __init__( self, - message: str = None, - response: nio.ErrorResponse = None, + message: typing.Optional[str] = None, + response: typing.Optional[nio.ErrorResponse] = None, *, - exception: BaseException = None, - original: typing.Union[nio.ErrorResponse, BaseException] = None, + exception: typing.Optional[BaseException] = None, + original: typing.Optional[typing.Union[nio.ErrorResponse, BaseException]] = None, ): if original: warnings.warn(DeprecationWarning("original is deprecated, use response or exception instead")) self.original = original or response or exception self.response = response - self.exception: typing.Union[nio.ErrorResponse, BaseException] = exception + self.exception: typing.Optional[typing.Union[nio.ErrorResponse, BaseException]] = exception self.message = message if self.original is None and self.message is None: raise ValueError("If there is no error history, at least a human readable message should be provided.") def bottom_of_chain( - self, other: typing.Union[Exception, nio.ErrorResponse] = None + self, other: typing.Optional[typing.Union[Exception, nio.ErrorResponse]] = None ) -> typing.Union[BaseException, nio.ErrorResponse]: """Recursively checks the `original` attribute of the exception until it reaches the bottom of the chain. @@ -200,9 +200,9 @@ class CheckFailure(CommandPreparationError): def __init__( self, - check_name: str = None, - message: str = None, - exception: BaseException = None, + check_name: typing.Optional[str] = None, + message: typing.Optional[str] = None, + exception: typing.Optional[BaseException] = None, ): if not message: message = f"Check {check_name} failed." @@ -224,7 +224,12 @@ class NotOwner(CheckFailure): Exception raised when the command invoker is not the owner of the bot. """ - def __init__(self, check_name: str = None, message: str = None, exception: BaseException = None): + def __init__( + self, + check_name: typing.Optional[str] = None, + message: typing.Optional[str] = None, + exception: typing.Optional[BaseException] = None, + ): if not message: message = "You are not the owner of this bot." super().__init__(check_name, message, exception) @@ -236,7 +241,13 @@ class InsufficientPower(CheckFailure): """ def __init__( - self, check_name: str = None, message: str = None, exception: BaseException = None, *, needed: int, have: int + self, + check_name: typing.Optional[str] = None, + message: typing.Optional[str] = None, + exception: typing.Optional[BaseException] = None, + *, + needed: int, + have: int, ): if not message: message = "Insufficient power level. Needed %d, have %d." % (needed, have) @@ -248,7 +259,12 @@ class NotADirectRoom(CheckFailure): Exception raised when the current room is not `m.direct` (a DM room) """ - def __init__(self, check_name: str = None, message: str = None, exception: BaseException = None): + def __init__( + self, + check_name: typing.Optional[str] = None, + message: typing.Optional[str] = None, + exception: typing.Optional[BaseException] = None, + ): if not message: message = "This command can only be run in a direct message room." super().__init__(check_name, message, exception) From 394416cc4c356468098367379b1a322a613f9296 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 17:02:31 +0200 Subject: [PATCH 13/30] Fix type hinting in check.py --- src/niobot/utils/checks.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/niobot/utils/checks.py b/src/niobot/utils/checks.py index 27cb01c..b208d35 100644 --- a/src/niobot/utils/checks.py +++ b/src/niobot/utils/checks.py @@ -1,6 +1,7 @@ from ..commands import check from ..context import Context from ..exceptions import CheckFailure, InsufficientPower, NotOwner +from typing import Optional __all__ = ( "is_owner", @@ -10,7 +11,7 @@ ) -def is_owner(*extra_owner_ids, name: str = None): +def is_owner(*extra_owner_ids, name: Optional[str] = None): """ Requires the sender owns the bot ([`NioBot.owner_id`][]), or is in `extra_owner_ids`. :param extra_owner_ids: A set of `@localpart:homeserver.tld` strings to check against. @@ -29,7 +30,7 @@ def predicate(ctx): return check(predicate, name) -def is_dm(allow_dual_membership: bool = False, name: str = None): +def is_dm(allow_dual_membership: bool = False, name: Optional[str] = None): """ Requires that the current room is a DM with the sender. @@ -50,7 +51,7 @@ def predicate(ctx: "Context"): return check(predicate, name) -def sender_has_power(level: int, room_creator_bypass: bool = False, name: str = None): +def sender_has_power(level: int, room_creator_bypass: bool = False, name: Optional[str] = None): """ Requires that the sender has a certain power level in the current room before running the command. @@ -70,7 +71,7 @@ def predicate(ctx): return check(predicate, name) -def client_has_power(level: int, name: str = None): +def client_has_power(level: int, name: Optional[str] = None): """ Requires that the bot has a certain power level in the current room before running the command. From 6a37f381eaab3f565d4e37802e30b7247b2ceec6 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 17:21:37 +0200 Subject: [PATCH 14/30] Improve type-hinting in help_command.py --- src/niobot/utils/help_command.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/niobot/utils/help_command.py b/src/niobot/utils/help_command.py index 37be398..05a013f 100644 --- a/src/niobot/utils/help_command.py +++ b/src/niobot/utils/help_command.py @@ -27,7 +27,7 @@ def clean_output( escape_room_references: bool = False, escape_all_periods: bool = False, escape_all_at_signs: bool = False, - escape_method: typing.Callable[[str], str] = None, + escape_method: typing.Optional[typing.Callable[[str], str]] = None, ) -> str: """ Escapes given text and sanitises it, ready for outputting to the user. @@ -51,9 +51,11 @@ def clean_output( """ if escape_method is None: - def escape_method(x: str) -> str: + def default_escape_method(x: str) -> str: return "\u200b".join(x.split()) + escape_method = default_escape_method + if escape_user_mentions: text = re.sub(r"@([A-Za-z0-9\-_=+./]+):([A-Za-z0-9\-_=+./]+)", escape_method("@\\1:\\2"), text) if escape_room_mentions: @@ -79,7 +81,7 @@ def format_command_name(command: "Command") -> str: def format_command_line(prefix: str, command: "Command") -> str: """Formats a command line, including name(s) & usage.""" name = format_command_name(command) - start = "{}{}".format(prefix, name) + start = f"{prefix}{name}" start += " " + command.display_usage.strip().replace("\n", "") return start From 75e83d928b4cd9ca48c0f9f70f3bdf745e85546b Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 17:22:37 +0200 Subject: [PATCH 15/30] Code improvement in string_view.py --- src/niobot/utils/string_view.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/niobot/utils/string_view.py b/src/niobot/utils/string_view.py index c4b7730..667f9db 100644 --- a/src/niobot/utils/string_view.py +++ b/src/niobot/utils/string_view.py @@ -24,7 +24,7 @@ def __init__(self, string: str): self.source = string self.index = 0 - self.arguments = [] + self.arguments: list[str] = [] def add_arg(self, argument: str) -> None: """Adds an argument to the argument list @@ -65,17 +65,14 @@ def parse_arguments(self) -> "ArgumentView": self.add_arg(reconstructed) reconstructed = "" quote_char = None + elif self.index == 0: # cannot be an escaped string + quote_started = True + quote_char = char + elif self.index > 0 and self.source[self.index - 1] != "\\": + quote_started = True + quote_char = char else: - if self.index == 0: # cannot be an escaped string - quote_started = True - quote_char = char - elif self.index > 0 and self.source[self.index - 1] != "\\": - quote_started = True - quote_char = char - # If it is an escaped quote, we can add it to the string. - else: - reconstructed += char - # If the character is a space, we can add the reconstructed string to the arguments list + reconstructed += char elif char.isspace(): if quote_started: reconstructed += char @@ -83,7 +80,6 @@ def parse_arguments(self) -> "ArgumentView": self.add_arg(reconstructed) reconstructed = "" quote_char = None - # Any other character can be added to the current string elif char: # elif ensures the character isn't null reconstructed += char self.index += 1 From 24765a5d3caed8b81b31f17bc1fb4d9748c2496b Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 17:23:17 +0200 Subject: [PATCH 16/30] Fix type-hinting in context.py --- src/niobot/context.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/niobot/context.py b/src/niobot/context.py index 8ff377f..d15b171 100644 --- a/src/niobot/context.py +++ b/src/niobot/context.py @@ -63,7 +63,7 @@ async def edit(self, content: str, **kwargs) -> "ContextualResponse": await self.ctx.client.edit_message(self.ctx.room, self._response.event_id, content, **kwargs) return self - async def delete(self, reason: str = None) -> None: + async def delete(self, reason: typing.Optional[str] = None) -> None: """ Redacts the current response. @@ -83,7 +83,7 @@ def __init__( event: nio.RoomMessageText, command: "Command", *, - invoking_string: str = None, + invoking_string: typing.Optional[str] = None, ): self._init_ts = time.time() self._client = _client @@ -149,7 +149,9 @@ def latency(self) -> float: """Returns the current event's latency in milliseconds.""" return self.client.latency(self.event, received_at=self._init_ts) - async def respond(self, content: str = None, file: "BaseAttachment" = None) -> ContextualResponse: + async def respond( + self, content: typing.Optional[str] = None, file: typing.Optional["BaseAttachment"] = None + ) -> ContextualResponse: """ Responds to the current event. From 63bfebc679020055bfda4a865a29aa76258ccc4f Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 17:23:40 +0200 Subject: [PATCH 17/30] Fixi BaseAttachment import --- src/niobot/client.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/niobot/client.py b/src/niobot/client.py index 813b80b..19a3d82 100644 --- a/src/niobot/client.py +++ b/src/niobot/client.py @@ -13,10 +13,7 @@ import nio from nio.crypto import ENCRYPTION_ENABLED -try: - from .attachment import BaseAttachment -except ImportError: - BaseAttachment = None +from .attachment import BaseAttachment from .commands import Command, Module from .exceptions import * from .utils import Typing, force_await, run_blocking From f427a21c08edfe9bf1dcd37a7d4ab84af5b02b40 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 17:34:08 +0200 Subject: [PATCH 18/30] Code refactoring in __main__.py --- src/niobot/__main__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/niobot/__main__.py b/src/niobot/__main__.py index 9305ceb..a377c8c 100644 --- a/src/niobot/__main__.py +++ b/src/niobot/__main__.py @@ -113,7 +113,7 @@ def version(ctx, no_colour: bool): t = ctx.obj["version_tuple"] if len(t) > 3: - t3 = t[3] or "gN/A.d%s" % (datetime.datetime.now().strftime("%Y%m%d")) + t3 = t[3] or f"gN/A.d{datetime.datetime.now():%Y%m%d}" try: t3_commit, t3_date_raw = t3.split(".", 1) except ValueError: @@ -189,7 +189,7 @@ def test_homeserver(homeserver: str): parsed = urllib.parse.urlparse(homeserver) if not parsed.scheme: logger.info("No scheme found, assuming HTTPS.") - parsed = urllib.parse.urlparse("https://" + homeserver) + parsed = urllib.parse.urlparse(f"https://{homeserver}") if not parsed.netloc: logger.critical("No netloc found, cannot continue.") @@ -200,7 +200,7 @@ def test_homeserver(homeserver: str): logger.info("Trying well-known of %r...", parsed.netloc) base_url = None try: - response = httpx.get("https://%s/.well-known/matrix/client" % parsed.netloc, timeout=30) + response = httpx.get(f"https://{parsed.netloc}/.well-known/matrix/client", timeout=30) except httpx.HTTPError as e: logger.critical("Failed to get well-known: %r", e) return @@ -233,13 +233,13 @@ def test_homeserver(homeserver: str): if not base_url: logger.info("No well-known found. Assuming %r as homeserver.", parsed.netloc) - base_url = urllib.parse.urlparse("https://" + parsed.netloc) + base_url = urllib.parse.urlparse(f"https://{parsed.netloc}") base_url = base_url.geturl() logger.info("Using %r as homeserver.", base_url) logger.info("Validating homeserver...") try: - response = httpx.get(base_url + "/_matrix/client/versions", timeout=30) + response = httpx.get(f"{base_url}/_matrix/client/versions", timeout=30) except httpx.HTTPError as e: logger.critical("Failed to get versions: %r", e) return @@ -305,7 +305,7 @@ def get_access_token(ctx, username: str, password: str, homeserver: str, device_ status_code = None try: response = httpx.post( - homeserver + "/_matrix/client/r0/login", + f"{homeserver}/_matrix/client/r0/login", json={ "type": "m.login.password", "identifier": {"type": "m.id.user", "user": username}, @@ -322,10 +322,10 @@ def get_access_token(ctx, username: str, password: str, homeserver: str, device_ response.raise_for_status() except httpx.HTTPError as e: click.secho("Failed!", fg="red", nl=False) - click.secho(" (%s)" % status_code or str(e), bg="red") + click.secho(f" ({status_code or str(e)})", bg="red") else: click.secho("OK", fg="green") - click.secho("Access token: %s" % response.json()["access_token"], fg="green") + click.secho(f'Access token: {response.json()["access_token"]}', fg="green") @cli_root.group() From bca84495d7f9aaa7e2b78a3eb5dbc44f4005c799 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 19:40:22 +0200 Subject: [PATCH 19/30] Fix typo --- src/niobot/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/niobot/client.py b/src/niobot/client.py index 19a3d82..bda7d36 100644 --- a/src/niobot/client.py +++ b/src/niobot/client.py @@ -209,7 +209,7 @@ def dispatch(self, event_name: str, *args, **kwargs): self.log.debug("%r is not in registered events: %s", event_name, self._events) def is_old(self, event: nio.Event) -> bool: - """Checks if an event was sent before the bot started. Always returns False when ignore_old_evens is False""" + """Checks if an event was sent before the bot started. Always returns False when ignore_old_events is False""" if not self.start_time: self.log.warning("have not started yet, using relative age comparison") start_time = time.time() - 30 # relative @@ -242,7 +242,7 @@ async def update_read_receipts(self, room: U[str, nio.MatrixRoom], event: nio.Ev else: self.log.debug("Updated read receipts for %s to %s.", room, event) - async def process_message(self, room: nio.MatrixRoom, event: nio.RoomMessageText): + async def process_message(self, room: nio.MatrixRoom, event: nio.RoomMessageText) -> None: """Processes a message and runs the command it is trying to invoke if any.""" self.message_cache.append((room, event)) self.dispatch("message", room, event) From bbeb6fdd5e88daab8274158197833a8456617f1d Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 19:52:53 +0200 Subject: [PATCH 20/30] Use casefold for case-insentive string comparing --- src/niobot/client.py | 4 ++-- src/niobot/utils/parsers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/niobot/client.py b/src/niobot/client.py index bda7d36..b70c1bd 100644 --- a/src/niobot/client.py +++ b/src/niobot/client.py @@ -34,7 +34,7 @@ class NioBot(nio.AsyncClient): :param device_id: The device ID to log in as. e.g. nio-bot :param store_path: The path to the store file. Defaults to ./store. Must be a directory. :param command_prefix: The prefix to use for commands. e.g. ! - :param case_insensitive: Whether to ignore case when checking for commands. If True, this lower()s + :param case_insensitive: Whether to ignore case when checking for commands. If True, this casefold()s incoming messages for parsing. :param global_message_type: The message type to default to. Defaults to m.notice :param ignore_old_events: Whether to simply discard events before the bot's login. @@ -255,7 +255,7 @@ async def process_message(self, room: nio.MatrixRoom, event: nio.RoomMessageText return if self.case_insensitive: - content = event.body.lower() + content = event.body.casefold() else: content = event.body diff --git a/src/niobot/utils/parsers.py b/src/niobot/utils/parsers.py index c9bb833..984d32d 100644 --- a/src/niobot/utils/parsers.py +++ b/src/niobot/utils/parsers.py @@ -40,7 +40,7 @@ def boolean_parser(_: "Context", __, value: str) -> bool: """ - Converts a given string into a boolean. Value is lower-cased before being parsed. + Converts a given string into a boolean. Value is casefolded before being parsed. The following resolves to true: * 1, y, yes, true, on @@ -52,7 +52,7 @@ def boolean_parser(_: "Context", __, value: str) -> bool: :return: The parsed boolean """ - value = value.lower() + value = value.casefold() if value in {"1", "y", "yes", "true", "on"}: return True if value in {"0", "n", "no", "false", "off"}: From 64a6467d055e13270706d3612b3acf29cc5d65ff Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 19:54:06 +0200 Subject: [PATCH 21/30] Ignore type issue for importing __version__ --- src/niobot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/niobot/__init__.py b/src/niobot/__init__.py index 8ee6e41..ca3eb4d 100644 --- a/src/niobot/__init__.py +++ b/src/niobot/__init__.py @@ -9,7 +9,7 @@ from .utils import * try: - import __version__ as version_meta + import __version__ as version_meta # type: ignore except ImportError: class __VersionMeta: From abed218b94d17471da635d81562f98ea7ca7eb0f Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 21:23:30 +0200 Subject: [PATCH 22/30] some more fixes to client.py --- src/niobot/client.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/niobot/client.py b/src/niobot/client.py index b70c1bd..0f99ee3 100644 --- a/src/niobot/client.py +++ b/src/niobot/client.py @@ -131,8 +131,7 @@ def __init__( # NOTE: `m.notice` prevents bot messages sending off room notifications, and shows darker text # (In element at least). - # noinspection PyTypeChecker - self.add_event_callback(self.process_message, nio.RoomMessageText) + self.add_event_callback(self.process_message, nio.RoomMessageText) # type: ignore self.add_event_callback(self.update_read_receipts, nio.RoomMessage) self.direct_rooms: typing.Dict[str, nio.MatrixRoom] = {} @@ -144,8 +143,7 @@ def __init__( if self.auto_join_rooms: self.log.info("Auto-joining rooms enabled.") - # noinspection PyTypeChecker - self.add_event_callback(self._auto_join_room_backlog_callback, nio.InviteMemberEvent) + self.add_event_callback(self._auto_join_room_backlog_callback, nio.InviteMemberEvent) # type: ignore async def sync(self, *args, **kwargs) -> U[nio.SyncResponse, nio.SyncError]: sync = await super().sync(*args, **kwargs) @@ -238,12 +236,16 @@ async def update_read_receipts(self, room: U[str, nio.MatrixRoom], event: nio.Ev event_id = event.event_id result = await self.room_read_markers(room, event_id, event_id) if not isinstance(result, nio.RoomReadMarkersResponse): - self.log.warning("Failed to update read receipts for %s: %s", room, result.message) + msg = result.message if isinstance(result, nio.ErrorResponse) else "?" + self.log.warning("Failed to update read receipts for %s: %s", room, msg) else: self.log.debug("Updated read receipts for %s to %s.", room, event) async def process_message(self, room: nio.MatrixRoom, event: nio.RoomMessageText) -> None: """Processes a message and runs the command it is trying to invoke if any.""" + if self.start_time is None: + raise RuntimeError("Bot has not started yet!") + self.message_cache.append((room, event)) self.dispatch("message", room, event) if event.sender == self.user: @@ -259,7 +261,7 @@ async def process_message(self, room: nio.MatrixRoom, event: nio.RoomMessageText else: content = event.body - def get_prefix(c) -> typing.Union[str, None]: + def get_prefix(c: str) -> typing.Union[str, None]: if isinstance(self.command_prefix, re.Pattern): _m = re.match(self.command_prefix, c) if _m: @@ -269,7 +271,7 @@ def get_prefix(c) -> typing.Union[str, None]: return self.command_prefix _p = get_prefix(content) - if get_prefix(content): + if _p: try: command = original_command = content[len(_p) :].splitlines()[0].split(" ")[0] except IndexError: @@ -285,7 +287,7 @@ def get_prefix(c) -> typing.Union[str, None]: self.dispatch("command_error", command, error) return - context = command.construct_context(self, room, event, self.command_prefix + original_command) + context = command.construct_context(self, room, event, _p + original_command) self.dispatch("command", context) def _task_callback(t: asyncio.Task): From ab74d6b128e4322eef8fd1bb61fcd4bc6c8da4d8 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Tue, 3 Oct 2023 21:32:28 +0200 Subject: [PATCH 23/30] Edit readme about case insensitivity --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 62918af..0ffff75 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ bot = Bot( homeserver="https://matrix.org", # your homeserver user_id="@__example__:matrix.org", # the user ID to log in as (Fully qualified) command_prefix="!", # the prefix to respond to (case sensitive, must be lowercase if below is True) - case_insensitive=True, # messages will be lower()cased before being handled. This is recommended. + case_insensitive=True, # messages will be casefold()ed before being handled. This is recommended. owner_id="@owner:homeserver.com" # The user ID who owns this bot. Optional, but required for bot.is_owner(...). ) From 8f1c68a6cf5491f0c1083cc4d0cc2d4f6513e50b Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Wed, 4 Oct 2023 13:51:50 +0200 Subject: [PATCH 24/30] Fix typing in context.py --- src/niobot/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/niobot/context.py b/src/niobot/context.py index eb80def..b22ff41 100644 --- a/src/niobot/context.py +++ b/src/niobot/context.py @@ -84,7 +84,7 @@ def __init__( command: "Command", *, invoking_prefix: typing.Optional[str] = None, - invoking_string: str = None, + invoking_string: typing.Optional[str] = None, ): self._init_ts = time.time() self._client = _client From 9236a84836da61cb0ff271b5e228b90d0100a251 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Wed, 4 Oct 2023 14:00:41 +0200 Subject: [PATCH 25/30] Clean up typing in client.py --- src/niobot/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/niobot/client.py b/src/niobot/client.py index 8d21893..9aa6c41 100644 --- a/src/niobot/client.py +++ b/src/niobot/client.py @@ -610,7 +610,7 @@ def generate_mx_reply(room: nio.MatrixRoom, event: nio.RoomMessageText) -> str: async def _recursively_upload_attachments( self, base: "BaseAttachment", encrypted: bool = False, __previous: typing.Optional[list] = None - ) -> list[typing.Union[nio.UploadResponse, nio.UploadError, type(None)]]: + ) -> list[typing.Union[nio.UploadResponse, nio.UploadError, None]]: """Recursively uploads attachments.""" previous = (__previous or []).copy() if not base.url: @@ -694,7 +694,7 @@ async def send_message( self.log.debug("Send message resolved room to %r", room) - body = { + body: dict[str, typing.Any] = { "msgtype": message_type or self.global_message_type, } @@ -709,7 +709,7 @@ async def send_message( body = file.as_body(content) else: - if clean_mentions: + if clean_mentions and content: content = content.replace("@", "@\u200b") body["body"] = content if self.automatic_markdown_renderer: From 606bf492f4edbc91cd8fff27e66092048e7b57a3 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Wed, 4 Oct 2023 14:16:01 +0200 Subject: [PATCH 26/30] Document and refactor client._get_id --- src/niobot/client.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/niobot/client.py b/src/niobot/client.py index 9aa6c41..761d730 100644 --- a/src/niobot/client.py +++ b/src/niobot/client.py @@ -578,12 +578,29 @@ async def _markdown_to_html(text: str) -> str: return rendered @staticmethod - def _get_id(obj) -> str: - if hasattr(obj, "event_id"): + def _get_id(obj: typing.Union[nio.Event, nio.MatrixRoom, nio.MatrixUser, str]) -> str: + """Gets the id of most objects as a string. + + Parameters + ---------- + obj : typing.Union[nio.Event, nio.MatrixRoom, nio.MatrixUser, str] + The object who's id to get, or the ID itself. + + Returns + ------- + str + The id of the objet. + + Raises + ------ + ValueError + The objet doesn't have an ID. + """ + if isinstance(obj, nio.Event): return obj.event_id - if hasattr(obj, "room_id"): + if isinstance(obj, nio.MatrixRoom): return obj.room_id - if hasattr(obj, "user_id"): + if isinstance(obj, nio.MatrixUser): return obj.user_id if isinstance(obj, str): return obj From fe7d9d8362d0ba24d9ed05575baa7c53f3876c4b Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Wed, 4 Oct 2023 14:20:00 +0200 Subject: [PATCH 27/30] Remove deprecated typing containers (List / Dict) --- src/niobot/attachment.py | 8 ++++---- src/niobot/client.py | 8 ++++---- src/niobot/utils/typing.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/niobot/attachment.py b/src/niobot/attachment.py index b41f354..3a7887f 100644 --- a/src/niobot/attachment.py +++ b/src/niobot/attachment.py @@ -113,7 +113,7 @@ def detect_mime_type(file: U[str, io.BytesIO, pathlib.Path]) -> str: raise TypeError("File must be a string, BytesIO, or Path object.") -def get_metadata_ffmpeg(file: U[str, pathlib.Path]) -> typing.Dict[str, typing.Any]: +def get_metadata_ffmpeg(file: U[str, pathlib.Path]) -> dict[str, typing.Any]: """ Gets metadata for a file via ffprobe. @@ -137,7 +137,7 @@ def get_metadata_ffmpeg(file: U[str, pathlib.Path]) -> typing.Dict[str, typing.A return data -def get_metadata_imagemagick(file: pathlib.Path) -> typing.Dict[str, typing.Any]: +def get_metadata_imagemagick(file: pathlib.Path) -> dict[str, typing.Any]: """The same as `get_metadata_ffmpeg` but for ImageMagick. Only returns a limited subset of the data, such as one stream, which contains the format, and size, @@ -181,7 +181,7 @@ def get_metadata_imagemagick(file: pathlib.Path) -> typing.Dict[str, typing.Any] return data -def get_metadata(file: U[str, pathlib.Path], mime_type: typing.Optional[str] = None) -> typing.Dict[str, typing.Any]: +def get_metadata(file: U[str, pathlib.Path], mime_type: typing.Optional[str] = None) -> dict[str, typing.Any]: """ Gets metadata for a file. @@ -441,7 +441,7 @@ class BaseAttachment(abc.ABC): type: AttachmentType url: typing.Optional[str] - keys: typing.Optional[typing.Dict[str, str]] + keys: typing.Optional[dict[str, str]] def __init__( self, diff --git a/src/niobot/client.py b/src/niobot/client.py index 761d730..38c1541 100644 --- a/src/niobot/client.py +++ b/src/niobot/client.py @@ -133,7 +133,7 @@ def __init__( self.add_event_callback(self.process_message, nio.RoomMessageText) # type: ignore self.add_event_callback(self.update_read_receipts, nio.RoomMessage) - self.direct_rooms: typing.Dict[str, nio.MatrixRoom] = {} + self.direct_rooms: dict[str, nio.MatrixRoom] = {} self.message_cache: typing.Deque[typing.Tuple[nio.MatrixRoom, nio.RoomMessageText]] = deque( maxlen=max_message_cache @@ -355,7 +355,7 @@ def mount_module(self, import_path: str) -> typing.Optional[list[Command]]: populated), but the event loop will be running. :param import_path: The import path (such as modules.file), which would be ./modules/file.py in a file tree. - :returns: Optional[List[Command]] - A list of commands mounted. None if the module's setup() was called. + :returns: Optional[list[Command]] - A list of commands mounted. None if the module's setup() was called. :raise ImportError: The module path is incorrect of there was another error while importing :raise TypeError: The module was not a subclass of Module. :raise ValueError: There was an error registering a command (e.g. name conflict) @@ -390,7 +390,7 @@ def mount_module(self, import_path: str) -> typing.Optional[list[Command]]: return added @property - def commands(self) -> typing.Dict[str, Command]: + def commands(self) -> dict[str, Command]: """Returns the internal command register. !!! warning @@ -405,7 +405,7 @@ def commands(self) -> typing.Dict[str, Command]: return self._commands @property - def modules(self) -> typing.Dict[typing.Type[Module], Module]: + def modules(self) -> dict[typing.Type[Module], Module]: """Returns the internal module register. !!! warning diff --git a/src/niobot/utils/typing.py b/src/niobot/utils/typing.py index 97a7616..c9905e6 100644 --- a/src/niobot/utils/typing.py +++ b/src/niobot/utils/typing.py @@ -9,7 +9,7 @@ __all__ = ("Typing",) log = logging.getLogger(__name__) -_TYPING_STATES: typing.Dict[str, "Typing"] = {} +_TYPING_STATES: dict[str, "Typing"] = {} class Typing: From 0717a6493ecc0eb690b6d0b207d9bfad8476dde0 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Wed, 4 Oct 2023 14:32:57 +0200 Subject: [PATCH 28/30] Ignore some errors that make sense --- src/niobot/attachment.py | 3 ++- src/niobot/client.py | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/niobot/attachment.py b/src/niobot/attachment.py index 3a7887f..2e555b3 100644 --- a/src/niobot/attachment.py +++ b/src/niobot/attachment.py @@ -453,7 +453,8 @@ def __init__( attachment_type: AttachmentType = AttachmentType.FILE, ): self.file = _to_path(file) - self.file_name = self.file.name if isinstance(self.file, pathlib.Path) else file_name + # Ignore type error as the type is checked right afterwards + self.file_name = self.file.name if isinstance(self.file, pathlib.Path) else file_name # type: ignore if not self.file_name: raise ValueError("file_name must be specified when uploading a BytesIO object.") self.mime_type = mime_type or detect_mime_type(self.file) diff --git a/src/niobot/client.py b/src/niobot/client.py index 38c1541..4423056 100644 --- a/src/niobot/client.py +++ b/src/niobot/client.py @@ -578,12 +578,12 @@ async def _markdown_to_html(text: str) -> str: return rendered @staticmethod - def _get_id(obj: typing.Union[nio.Event, nio.MatrixRoom, nio.MatrixUser, str]) -> str: + def _get_id(obj: typing.Union[nio.Event, nio.MatrixRoom, nio.MatrixUser, str, typing.Any]) -> str: """Gets the id of most objects as a string. Parameters ---------- - obj : typing.Union[nio.Event, nio.MatrixRoom, nio.MatrixUser, str] + obj : typing.Union[nio.Event, nio.MatrixRoom, nio.MatrixUser, str, Any] The object who's id to get, or the ID itself. Returns @@ -596,11 +596,11 @@ def _get_id(obj: typing.Union[nio.Event, nio.MatrixRoom, nio.MatrixUser, str]) - ValueError The objet doesn't have an ID. """ - if isinstance(obj, nio.Event): + if hasattr(obj, "event_id"): return obj.event_id - if isinstance(obj, nio.MatrixRoom): + if hasattr(obj, "room_id"): return obj.room_id - if isinstance(obj, nio.MatrixUser): + if hasattr(obj, "user_id"): return obj.user_id if isinstance(obj, str): return obj From dd73de6d95c43c9f6e98fc9e1b66af1f77f2a9ea Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Wed, 4 Oct 2023 14:40:24 +0200 Subject: [PATCH 29/30] Handle str rooms in edit_message --- src/niobot/client.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/niobot/client.py b/src/niobot/client.py index 4423056..ce1a729 100644 --- a/src/niobot/client.py +++ b/src/niobot/client.py @@ -776,6 +776,8 @@ async def edit_message( :raises RuntimeError: If you are not the sender of the message. :raises TypeError: If the message is not text. """ + room = self._get_id(room) + if clean_mentions: content = content.replace("@", "@\u200b") event_id = self._get_id(message) @@ -796,16 +798,16 @@ async def edit_message( "event_id": event_id, }, } - async with Typing(self, room.room_id): + async with Typing(self, room): response = await self.room_send( - self._get_id(room), + room, "m.room.message", body, ) if isinstance(response, nio.RoomSendError): raise MessageException("Failed to edit message.", response) # Forcefully clear typing - await self.room_typing(room.room_id, False) + await self.room_typing(room, False) return response async def delete_message( From c28fdb4a13e4adbd0e6fbfa5c0dd281f2c0ee6c4 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Wed, 4 Oct 2023 15:29:44 +0200 Subject: [PATCH 30/30] Use sphinx-style for docstring instead of numpy --- src/niobot/client.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/niobot/client.py b/src/niobot/client.py index ce1a729..796a54a 100644 --- a/src/niobot/client.py +++ b/src/niobot/client.py @@ -580,21 +580,10 @@ async def _markdown_to_html(text: str) -> str: @staticmethod def _get_id(obj: typing.Union[nio.Event, nio.MatrixRoom, nio.MatrixUser, str, typing.Any]) -> str: """Gets the id of most objects as a string. - - Parameters - ---------- - obj : typing.Union[nio.Event, nio.MatrixRoom, nio.MatrixUser, str, Any] - The object who's id to get, or the ID itself. - - Returns - ------- - str - The id of the objet. - - Raises - ------ - ValueError - The objet doesn't have an ID. + :param obj: The object who's ID to get, or the ID itself. + :type obj: typing.Union[nio.Event, nio.MatrixRoom, nio.MatrixUser, str, Any] + :returns: the ID of the object + :raises: ValueError - the Object doesn't have an ID """ if hasattr(obj, "event_id"): return obj.event_id