From 4bf0530f8e1340d27b5d7693842c73de0376a929 Mon Sep 17 00:00:00 2001 From: Zomatree Date: Thu, 11 May 2023 07:02:39 +0100 Subject: [PATCH] clean up code --- pyproject.toml | 1 + revolt/__init__.py | 1 + revolt/client.py | 2 +- revolt/ext/commands/checks.py | 8 ++-- revolt/ext/commands/command.py | 27 +++++++------ revolt/ext/commands/context.py | 14 +++++-- revolt/ext/commands/errors.py | 11 ++++- revolt/ext/commands/group.py | 18 ++++----- revolt/ext/commands/help.py | 51 ++++++++++++------------ revolt/ext/commands/utils.py | 4 +- revolt/http.py | 3 +- revolt/member.py | 50 +++++++++++++++++++++-- revolt/permissions.py | 2 +- revolt/user.py | 37 +++++++++++++++-- revolt/websocket.py | 14 +++++-- typings/sphinx_nameko_theme/__init__.pyi | 1 + 16 files changed, 171 insertions(+), 73 deletions(-) create mode 100644 typings/sphinx_nameko_theme/__init__.pyi diff --git a/pyproject.toml b/pyproject.toml index 6a1eeb8..b0176a5 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ only-packages = true reportPrivateUsage = false reportImportCycles = false reportIncompatibleMethodOverride = false +typeCheckingMode = "strict" [build-system] requires = ["hatchling"] diff --git a/revolt/__init__.py b/revolt/__init__.py index 84f8cc7..5fc1ba5 100755 --- a/revolt/__init__.py +++ b/revolt/__init__.py @@ -19,3 +19,4 @@ from .server import * from .user import * __version__ = "0.1.9" + \ No newline at end of file diff --git a/revolt/client.py b/revolt/client.py index 77ca3ed..fa02345 100755 --- a/revolt/client.py +++ b/revolt/client.py @@ -347,7 +347,7 @@ class Client: background: Optional[:class:`File`] The new background for the profile, passing in ``None`` will remove the profile background """ - remove = [] + remove: list[str] = [] if kwargs.get("content", Missing) is None: del kwargs["content"] diff --git a/revolt/ext/commands/checks.py b/revolt/ext/commands/checks.py index 435483f..84e0130 100755 --- a/revolt/ext/commands/checks.py +++ b/revolt/ext/commands/checks.py @@ -68,7 +68,7 @@ def has_permissions(**permissions: bool): author = context.author if not author.has_permissions(**permissions): - raise MissingPermissionsError + raise MissingPermissionsError(permissions) return inner @@ -77,10 +77,10 @@ def has_channel_permissions(**permissions: bool): def inner(context: Context[ClientT]): author = context.author - if isinstance(author, revolt.User): - raise MissingPermissionsError + if not isinstance(author, revolt.Member): + raise ServerOnly if not author.has_channel_permissions(context.channel, **permissions): - raise MissingPermissionsError + raise MissingPermissionsError(permissions) return inner diff --git a/revolt/ext/commands/command.py b/revolt/ext/commands/command.py index 94c7cab..b127192 100755 --- a/revolt/ext/commands/command.py +++ b/revolt/ext/commands/command.py @@ -9,7 +9,7 @@ from typing import (TYPE_CHECKING, Annotated, Any, Callable, Coroutine, from revolt.utils import copy_doc, maybe_coroutine from .errors import InvalidLiteralArgument, UnionConverterError -from .utils import ClientT, evaluate_parameters +from .utils import ClientCoT, evaluate_parameters if TYPE_CHECKING: from .checks import Check @@ -25,7 +25,7 @@ __all__ = ( NoneType = type(None) -class Command(Generic[ClientT]): +class Command(Generic[ClientCoT]): """Class for holding info about a command. Parameters @@ -52,13 +52,13 @@ class Command(Generic[ClientT]): self.usage = usage self.signature = inspect.signature(self.callback) self.parameters = evaluate_parameters(self.signature.parameters.values(), getattr(callback, "__globals__", {})) - self.checks: list[Check[ClientT]] = getattr(callback, "_checks", []) - self.parent: Optional[Group[ClientT]] = None - self.cog: Optional[Cog[ClientT]] = None - self._error_handler: Callable[[Any, Context[ClientT], Exception], Coroutine[Any, Any, Any]] = type(self)._default_error_handler + self.checks: list[Check[ClientCoT]] = getattr(callback, "_checks", []) + self.parent: Optional[Group[ClientCoT]] = None + self.cog: Optional[Cog[ClientCoT]] = None + self._error_handler: Callable[[Any, Context[ClientCoT], Exception], Coroutine[Any, Any, Any]] = type(self)._default_error_handler self.description = callback.__doc__ - async def invoke(self, context: Context[ClientT], *args: Any, **kwargs: Any) -> Any: + async def invoke(self, context: Context[ClientCoT], *args: Any, **kwargs: Any) -> Any: """Runs the command and calls the error handler if the command errors. Parameters @@ -74,7 +74,7 @@ class Command(Generic[ClientT]): return await self._error_handler(self.cog or context.client, context, err) @copy_doc(invoke) - def __call__(self, context: Context[ClientT], *args: Any, **kwargs: Any) -> Any: + def __call__(self, context: Context[ClientCoT], *args: Any, **kwargs: Any) -> Any: return self.invoke(context, *args, **kwargs) def error(self, func: Callable[..., Coroutine[Any, Any, Any]]): @@ -97,11 +97,11 @@ class Command(Generic[ClientT]): self._error_handler = func return func - async def _default_error_handler(self, ctx: Context[ClientT], error: Exception): + async def _default_error_handler(self, ctx: Context[ClientCoT], error: Exception): traceback.print_exception(type(error), error, error.__traceback__) @classmethod - async def handle_origin(cls, context: Context[ClientT], origin: Any, annotation: Any, arg: str) -> Any: + async def handle_origin(cls, context: Context[ClientCoT], origin: Any, annotation: Any, arg: str) -> Any: if origin is Union: for converter in get_args(annotation): try: @@ -128,11 +128,12 @@ class Command(Generic[ClientT]): raise InvalidLiteralArgument(arg) @classmethod - async def convert_argument(cls, arg: str, annotation: Any, context: Context[ClientT]) -> Any: + async def convert_argument(cls, arg: str, annotation: Any, context: Context[ClientCoT]) -> Any: if annotation is not inspect.Signature.empty: if annotation is str: # no converting is needed - its already a string return arg + origin: Any if origin := get_origin(annotation): return await cls.handle_origin(context, origin, annotation, arg) else: @@ -140,7 +141,7 @@ class Command(Generic[ClientT]): else: return arg - async def parse_arguments(self, context: Context[ClientT]): + async def parse_arguments(self, context: Context[ClientCoT]): # please pr if you can think of a better way to do this for parameter in self.parameters[2:]: @@ -213,7 +214,7 @@ class Command(Generic[ClientT]): return f"{' '.join(parents[::-1])} {self.name} {' '.join(parameters)}" -def command(*, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: type[Command[ClientT]] = Command, usage: Optional[str] = None): +def command(*, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: type[Command[ClientCoT]] = Command, usage: Optional[str] = None): """A decorator that turns a function into a :class:`Command`. Parameters diff --git a/revolt/ext/commands/context.py b/revolt/ext/commands/context.py index 60f03cb..beb5e91 100755 --- a/revolt/ext/commands/context.py +++ b/revolt/ext/commands/context.py @@ -7,7 +7,7 @@ from revolt.utils import maybe_coroutine from .command import Command from .group import Group -from .utils import ClientT +from .utils import ClientCoT if TYPE_CHECKING: from .view import StringView @@ -16,7 +16,7 @@ __all__ = ( "Context", ) -class Context(revolt.Messageable, Generic[ClientT]): +class Context(revolt.Messageable, Generic[ClientCoT]): """Stores metadata the commands execution. Attributes @@ -45,7 +45,7 @@ class Context(revolt.Messageable, Generic[ClientT]): async def _get_channel_id(self) -> str: return self.channel.id - def __init__(self, command: Optional[Command[ClientT]], invoked_with: str, view: StringView, message: revolt.Message, client: ClientT): + def __init__(self, command: Optional[Command[ClientCoT]], invoked_with: str, view: StringView, message: revolt.Message, client: ClientCoT): self.command = command self.invoked_with = invoked_with self.view = view @@ -85,8 +85,14 @@ class Context(revolt.Messageable, Generic[ClientT]): await command.parse_arguments(self) return await command.invoke(self, *self.args, **self.kwargs) - async def can_run(self, command: Optional[Command[ClientT]] = None) -> bool: + async def can_run(self, command: Optional[Command[ClientCoT]] = None) -> bool: """Runs all of the commands checks, and returns true if all of them pass""" command = command or self.command return all([await maybe_coroutine(check, self) for check in (command.checks if command else [])]) + + async def send_help(self, argument: Command[Any] | Group[Any] | ClientCoT | None = None): + argument = argument or self.client + + command = self.client.get_command("help") + await command.invoke(self, argument) diff --git a/revolt/ext/commands/errors.py b/revolt/ext/commands/errors.py index be828f1..bef980f 100755 --- a/revolt/ext/commands/errors.py +++ b/revolt/ext/commands/errors.py @@ -50,7 +50,16 @@ class ServerOnly(CheckError): """Raised when a check requires the command to be ran in a server""" class MissingPermissionsError(CheckError): - """Raised when a check requires permissions the user does not have""" + """Raised when a check requires permissions the user does not have + + Attributes + ----------- + permissions: :class:`dict[str, bool]` + The permissions which the user did not have + """ + + def __init__(self, permissions: dict[str, bool]): + self.permissions = permissions class ConverterError(CommandError): """Base class for all converter errors""" diff --git a/revolt/ext/commands/group.py b/revolt/ext/commands/group.py index 6792eb2..f459119 100755 --- a/revolt/ext/commands/group.py +++ b/revolt/ext/commands/group.py @@ -1,21 +1,17 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional, TypeVar +from typing import Any, Callable, Coroutine, Optional from .command import Command +from .utils import ClientCoT, ClientT -if TYPE_CHECKING: - from .client import CommandsClient __all__ = ( "Group", "group" ) -ClientT = TypeVar("ClientT", bound="CommandsClient") - - -class Group(Command[ClientT]): +class Group(Command[ClientCoT]): """Class for holding info about a group command. Parameters @@ -33,10 +29,10 @@ class Group(Command[ClientT]): __slots__ = ("subcommands",) def __init__(self, callback: Callable[..., Coroutine[Any, Any, Any]], name: str, aliases: list[str]): - self.subcommands: dict[str, Command[ClientT]] = {} + self.subcommands: dict[str, Command[ClientCoT]] = {} super().__init__(callback, name, aliases) - def command(self, *, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: type[Command[ClientT]] = Command[ClientT]): + def command(self, *, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: type[Command[ClientCoT]] = Command[ClientCoT]): """A decorator that turns a function into a :class:`Command` and registers the command as a subcommand. Parameters @@ -61,7 +57,7 @@ class Group(Command[ClientT]): return inner - def group(self, *, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: Optional[type[Group[ClientT]]] = None): + def group(self, *, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: Optional[type[Group[ClientCoT]]] = None): """A decorator that turns a function into a :class:`Group` and registers the command as a subcommand Parameters @@ -92,7 +88,7 @@ class Group(Command[ClientT]): return f"" @property - def commands(self) -> list[Command[ClientT]]: + def commands(self) -> list[Command[ClientCoT]]: return list(self.subcommands.values()) def group(*, name: Optional[str] = None, aliases: Optional[list[str]] = None, cls: type[Group[ClientT]] = Group): diff --git a/revolt/ext/commands/help.py b/revolt/ext/commands/help.py index 42cd876..86423f8 100755 --- a/revolt/ext/commands/help.py +++ b/revolt/ext/commands/help.py @@ -9,7 +9,7 @@ from .cog import Cog from .command import Command from .context import Context from .group import Group -from .utils import ClientT +from .utils import ClientCoT, ClientT if TYPE_CHECKING: from revolt import File, Message, Messageable, MessageReply, SendableEmbed @@ -18,6 +18,7 @@ if TYPE_CHECKING: __all__ = ("MessagePayload", "HelpCommand", "DefaultHelpCommand", "help_command_impl") + class MessagePayload(TypedDict): content: str embed: NotRequired[SendableEmbed] @@ -25,28 +26,28 @@ class MessagePayload(TypedDict): attachments: NotRequired[list[File]] replies: NotRequired[list[MessageReply]] -class HelpCommand(ABC, Generic[ClientT]): +class HelpCommand(ABC, Generic[ClientCoT]): @abstractmethod - async def create_bot_help(self, context: Context[ClientT], commands: dict[Optional[Cog[ClientT]], list[Command[ClientT]]]) -> Union[str, SendableEmbed, MessagePayload]: + async def create_bot_help(self, context: Context[ClientCoT], commands: dict[Optional[Cog[ClientCoT]], list[Command[ClientCoT]]]) -> Union[str, SendableEmbed, MessagePayload]: raise NotImplementedError @abstractmethod - async def create_command_help(self, context: Context[ClientT], command: Command[ClientT]) -> Union[str, SendableEmbed, MessagePayload]: + async def create_command_help(self, context: Context[ClientCoT], command: Command[ClientCoT]) -> Union[str, SendableEmbed, MessagePayload]: raise NotImplementedError @abstractmethod - async def create_group_help(self, context: Context[ClientT], group: Group[ClientT]) -> Union[str, SendableEmbed, MessagePayload]: + async def create_group_help(self, context: Context[ClientCoT], group: Group[ClientCoT]) -> Union[str, SendableEmbed, MessagePayload]: raise NotImplementedError @abstractmethod - async def create_cog_help(self, context: Context[ClientT], cog: Cog[ClientT]) -> Union[str, SendableEmbed, MessagePayload]: + async def create_cog_help(self, context: Context[ClientCoT], cog: Cog[ClientCoT]) -> Union[str, SendableEmbed, MessagePayload]: raise NotImplementedError - async def send_help_command(self, context: Context[ClientT], message_payload: MessagePayload) -> Message: + async def send_help_command(self, context: Context[ClientCoT], message_payload: MessagePayload) -> Message: return await context.send(**message_payload) - async def filter_commands(self, context: Context[ClientT], commands: list[Command[ClientT]]) -> list[Command[ClientT]]: - filtered: list[Command[ClientT]] = [] + async def filter_commands(self, context: Context[ClientCoT], commands: list[Command[ClientCoT]]) -> list[Command[ClientCoT]]: + filtered: list[Command[ClientCoT]] = [] for command in commands: try: @@ -57,34 +58,34 @@ class HelpCommand(ABC, Generic[ClientT]): return filtered - async def group_commands(self, context: Context[ClientT], commands: list[Command[ClientT]]) -> dict[Optional[Cog[ClientT]], list[Command[ClientT]]]: - cogs: dict[Optional[Cog[ClientT]], list[Command[ClientT]]] = {} + async def group_commands(self, context: Context[ClientCoT], commands: list[Command[ClientCoT]]) -> dict[Optional[Cog[ClientCoT]], list[Command[ClientCoT]]]: + cogs: dict[Optional[Cog[ClientCoT]], list[Command[ClientCoT]]] = {} for command in commands: cogs.setdefault(command.cog, []).append(command) return cogs - async def handle_message(self, context: Context[ClientT], message: Message): + async def handle_message(self, context: Context[ClientCoT], message: Message): pass - async def get_channel(self, context: Context[ClientT]) -> Messageable: + async def get_channel(self, context: Context) -> Messageable: return context @abstractmethod - async def handle_no_command_found(self, context: Context[ClientT], name: str) -> Any: + async def handle_no_command_found(self, context: Context[ClientCoT], name: str) -> Any: raise NotImplementedError @abstractmethod - async def handle_no_cog_found(self, context: Context[ClientT], name: str) -> Any: + async def handle_no_cog_found(self, context: Context[ClientCoT], name: str) -> Any: raise NotImplementedError -class DefaultHelpCommand(HelpCommand[ClientT]): +class DefaultHelpCommand(HelpCommand[ClientCoT]): def __init__(self, default_cog_name: str = "No Cog"): self.default_cog_name = default_cog_name - async def create_bot_help(self, context: Context[ClientT], commands: dict[Optional[Cog[ClientT]], list[Command[ClientT]]]) -> Union[str, SendableEmbed, MessagePayload]: + async def create_bot_help(self, context: Context[ClientCoT], commands: dict[Optional[Cog[ClientCoT]], list[Command[ClientCoT]]]) -> Union[str, SendableEmbed, MessagePayload]: lines = ["```"] for cog, cog_commands in commands.items(): @@ -99,7 +100,7 @@ class DefaultHelpCommand(HelpCommand[ClientT]): lines.append("```") return "\n".join(lines) - async def create_cog_help(self, context: Context[ClientT], cog: Cog[ClientT]) -> Union[str, SendableEmbed, MessagePayload]: + async def create_cog_help(self, context: Context[ClientCoT], cog: Cog[ClientCoT]) -> Union[str, SendableEmbed, MessagePayload]: lines = ["```"] lines.append(f"{cog.qualified_name}:") @@ -110,7 +111,7 @@ class DefaultHelpCommand(HelpCommand[ClientT]): lines.append("```") return "\n".join(lines) - async def create_command_help(self, context: Context[ClientT], command: Command[ClientT]) -> Union[str, SendableEmbed, MessagePayload]: + async def create_command_help(self, context: Context[ClientCoT], command: Command[ClientCoT]) -> Union[str, SendableEmbed, MessagePayload]: lines = ["```"] lines.append(f"{command.name}:") @@ -126,7 +127,7 @@ class DefaultHelpCommand(HelpCommand[ClientT]): lines.append("```") return "\n".join(lines) - async def create_group_help(self, context: Context[ClientT], group: Group[ClientT]) -> Union[str, SendableEmbed, MessagePayload]: + async def create_group_help(self, context: Context[ClientCoT], group: Group[ClientCoT]) -> Union[str, SendableEmbed, MessagePayload]: lines = ["```"] lines.append(f"{group.name}:") @@ -144,20 +145,20 @@ class DefaultHelpCommand(HelpCommand[ClientT]): lines.append("```") return "\n".join(lines) - async def handle_no_command_found(self, context: Context[ClientT], name: str): + async def handle_no_command_found(self, context: Context[ClientCoT], name: str): channel = await self.get_channel(context) await channel.send(f"Command `{name}` not found.") - async def handle_no_cog_found(self, context: Context[ClientT], name: str): + async def handle_no_cog_found(self, context: Context[ClientCoT], name: str): channel = await self.get_channel(context) await channel.send(f"Cog `{name}` not found.") -class HelpCommandImpl(Command[ClientT]): - def __init__(self, client: ClientT): +class HelpCommandImpl(Command[ClientCoT]): + def __init__(self, client: ClientCoT): self.client = client - async def callback(_: Union[ClientT, Cog[ClientT]], context: Context[ClientT], *args: str): + async def callback(_: Union[ClientCoT, Cog[ClientCoT]], context: Context[ClientCoT], *args: str): await help_command_impl(context.client, context, *args) super().__init__(callback=callback, name="help", aliases=[]) diff --git a/revolt/ext/commands/utils.py b/revolt/ext/commands/utils.py index 00d679c..c3bb67b 100755 --- a/revolt/ext/commands/utils.py +++ b/revolt/ext/commands/utils.py @@ -7,12 +7,14 @@ from typing_extensions import TypeVar if TYPE_CHECKING: from .client import CommandsClient + from .context import Context __all__ = ("evaluate_parameters",) ClientT = TypeVar("ClientT", bound="CommandsClient", default="CommandsClient") - +ClientCoT = TypeVar("ClientCoT", bound="CommandsClient", default="CommandsClient", covariant=True) +ContextT = TypeVar("ContextT", bound="Context") def evaluate_parameters(parameters: Iterable[Parameter], globals: dict[str, Any]) -> list[Parameter]: new_parameters: list[Parameter] = [] diff --git a/revolt/http.py b/revolt/http.py index 44ed1d7..ab13d2c 100755 --- a/revolt/http.py +++ b/revolt/http.py @@ -8,7 +8,6 @@ import ulid from .errors import Forbidden, HTTPError, ServerError from .file import File -from .utils import Missing try: import ujson as _json @@ -203,7 +202,7 @@ class HttpClient: include_users: bool = False ) -> Request[Union[list[MessagePayload], MessageWithUserData]]: - json = {"sort": sort.value, "include_users": str(include_users)} + json: dict[str, Any] = {"sort": sort.value, "include_users": str(include_users)} if limit: json["limit"] = limit diff --git a/revolt/member.py b/revolt/member.py index 886d8e8..aa85fed 100755 --- a/revolt/member.py +++ b/revolt/member.py @@ -1,19 +1,24 @@ from __future__ import annotations import datetime -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Any, Optional + + +from .utils import _Missing, Missing from .asset import Asset from .permissions import Permissions from .permissions_calculator import calculate_permissions from .user import User +from .file import File if TYPE_CHECKING: from .channel import Channel from .server import Server from .state import State - from .types import File + from .types import File as FilePayload from .types import Member as MemberPayload + from .role import Role __all__ = ("Member",) @@ -42,7 +47,7 @@ class Member(User): # due to not having a user payload and only a user object we have to manually add all the attributes instead of calling User.__init__ flattern_user(self, user) - user._members.add(self) + user._members[server.id] = self self.state = state @@ -77,7 +82,7 @@ class Member(User): """:class:`str`: Returns a string that allows you to mention the given member.""" return f"<@{self.id}>" - def _update(self, *, nickname: Optional[str] = None, avatar: Optional[File] = None, roles: Optional[list[str]] = None): + def _update(self, *, nickname: Optional[str] = None, avatar: Optional[FilePayload] = None, roles: Optional[list[str]] = None): if nickname is not None: self.nickname = nickname @@ -106,6 +111,43 @@ class Member(User): """Unbans the member from the server""" await self.state.http.unban_member(self.server.id, self.id) + async def edit( + self, + *, + nickname: str | None | _Missing = Missing, + roles: list[Role] | None | _Missing = Missing, + avatar: File | None | _Missing = Missing, + timeout: datetime.timedelta | None | _Missing = Missing + ): + remove: list[str] = [] + data: dict[str, Any] = {} + + if nickname is None: + remove.append("Nickname") + elif nickname is not Missing: + data["nickname"] = nickname + + if roles is None: + remove.append("Roles") + elif roles is not Missing: + data["roles"] = roles + + if avatar is None: + remove.append("Avatar") + elif avatar is not Missing: + # pyright cant understand custom singletons - it doesnt know this will never be an instance of _Missing here because Missing is the only instance + assert not isinstance(avatar, _Missing) + + data["avatar"] = (await self.state.http.upload_file(avatar, "avatars"))["id"] + + if timeout is None: + remove.append("Timeout") + elif timeout is not Missing: + assert not isinstance(timeout, _Missing) + data["timeout"] = (datetime.datetime.now(datetime.timezone.utc) + timeout).isoformat() + + await self.state.http.edit_member(self.server.id, self.id, remove, data) + async def timeout(self, length: datetime.timedelta): """Timeouts the member diff --git a/revolt/permissions.py b/revolt/permissions.py index ae32d11..bd5c5de 100755 --- a/revolt/permissions.py +++ b/revolt/permissions.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Optional, cast +from typing import TYPE_CHECKING, Any, Optional from typing_extensions import Self diff --git a/revolt/user.py b/revolt/user.py index d1e9c78..f013f6b 100755 --- a/revolt/user.py +++ b/revolt/user.py @@ -1,7 +1,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, NamedTuple, Optional, Union -from weakref import WeakSet +from weakref import WeakValueDictionary from .asset import Asset, PartialAsset from .channel import DMChannel, GroupDMChannel @@ -18,6 +18,7 @@ if TYPE_CHECKING: from .types import Status as StatusPayload from .types import User as UserPayload from .types import UserProfile as UserProfileData + from .server import Server __all__ = ("User", "Status", "Relation", "UserProfile") @@ -69,7 +70,7 @@ class User(Messageable, Ulid): def __init__(self, data: UserPayload, state: State): self.state = state - self._members: WeakSet[Member] = WeakSet() # we store all member versions of this user to avoid having to check every guild when needing to update. + self._members: WeakValueDictionary[str, Member] = WeakValueDictionary() # we store all member versions of this user to avoid having to check every guild when needing to update. self.id = data["_id"] self.original_name = data["username"] self.dm_channel = None @@ -212,7 +213,7 @@ class User(Messageable, Ulid): # update user infomation for all members if self.__class__ is User: - for member in self._members: + for member in self._members.values(): User._update(member, status=status, profile=profile, avatar=avatar, online=online) async def default_avatar(self) -> bytes: @@ -245,3 +246,33 @@ class User(Messageable, Ulid): self.profile = UserProfile(payload.get("content"), background) return self.profile + + def to_member(self, server: Server) -> Member: + """Gets the member instance for this user for a specific server. + + Roughly equivelent to: + + .. code-block:: python + + member = server.get_member(user.id) + + + Parameters + ----------- + server: :class:`Server` + The server to get the member for + + Returns + -------- + :class:`Member` + The member + + Raises + ------- + :class:`LookupError` + + """ + try: + return self._members[server.id] + except IndexError: + raise LookupError from None diff --git a/revolt/websocket.py b/revolt/websocket.py index 8d268ef..f5912a9 100755 --- a/revolt/websocket.py +++ b/revolt/websocket.py @@ -4,7 +4,7 @@ import asyncio import logging import time from copy import copy -from typing import TYPE_CHECKING, Callable, cast +from typing import TYPE_CHECKING, Callable, NamedTuple, cast from . import utils from .channel import GroupDMChannel, TextChannel, VoiceChannel @@ -29,6 +29,8 @@ from .types import (ServerCreateEventPayload, ServerDeleteEventPayload, UserUpdateEventPayload) from .user import Status, UserProfile +import aiohttp + try: import ujson as json except ImportError: @@ -46,7 +48,11 @@ if TYPE_CHECKING: from .state import State from .types import (AuthenticatePayload, BasePayload, MessageEventPayload, ReadyEventPayload) + from .message import Message +class WSMessage(NamedTuple): + type: aiohttp.WSMsgType + data: str | bytes | aiohttp.WSCloseCode __all__ = ("WebsocketHandler",) @@ -313,7 +319,7 @@ class WebsocketHandler: # remove the member from the user user = self.state.get_user(payload["user"]) - user._members.remove(member) + user._members.pop(server.id) self.dispatch("member_leave", member) @@ -431,7 +437,7 @@ class WebsocketHandler: self.dispatch("raw_bulk_message_delete", payload) - messages = [] + messages: list[Message] = [] for message_id in payload["ids"]: if server_id := channel.server_id: @@ -462,6 +468,8 @@ class WebsocketHandler: asyncio.create_task(self.heartbeat()) async for msg in self.websocket: + msg = cast(WSMessage, msg) # aiohttp doesnt use NamedTuple so the type info is missing + if use_msgpack: data = cast(bytes, msg.data) diff --git a/typings/sphinx_nameko_theme/__init__.pyi b/typings/sphinx_nameko_theme/__init__.pyi new file mode 100644 index 0000000..4fbb77d --- /dev/null +++ b/typings/sphinx_nameko_theme/__init__.pyi @@ -0,0 +1 @@ +def get_html_theme_path() -> str: ...