From 088e76330629de4a33caa8452a711f1c2bbe6d8d Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 19 Jul 2022 01:31:33 -0400 Subject: [PATCH] Change certain sequences to use a special proxy type instead of list This is to speed up cases where someone is just querying the length of the underlying sequence. If anything else is done to the sequence then it is copied from the original iterator. This change should be mostly transparent. --- discord/client.py | 40 ++++++++++++++++++++-------------------- discord/guild.py | 39 +++++++++++++++++++-------------------- discord/role.py | 2 +- discord/state.py | 17 +++++++++-------- discord/utils.py | 29 ++++++++++++++++++++--------- docs/migrating.rst | 19 +++++++++++++++++++ 6 files changed, 88 insertions(+), 58 deletions(-) diff --git a/discord/client.py b/discord/client.py index 415ab1491..ffb62d883 100644 --- a/discord/client.py +++ b/discord/client.py @@ -395,32 +395,32 @@ class Client: return self._connection.user @property - def guilds(self) -> List[Guild]: - """List[:class:`.Guild`]: The guilds that the connected client is a member of.""" + def guilds(self) -> Sequence[Guild]: + """Sequence[:class:`.Guild`]: The guilds that the connected client is a member of.""" return self._connection.guilds @property - def emojis(self) -> List[Emoji]: - """List[:class:`.Emoji`]: The emojis that the connected client has.""" + def emojis(self) -> Sequence[Emoji]: + """Sequence[:class:`.Emoji`]: The emojis that the connected client has.""" return self._connection.emojis @property - def stickers(self) -> List[GuildSticker]: - """List[:class:`.GuildSticker`]: The stickers that the connected client has. + def stickers(self) -> Sequence[GuildSticker]: + """Sequence[:class:`.GuildSticker`]: The stickers that the connected client has. .. versionadded:: 2.0 """ return self._connection.stickers @property - def sessions(self) -> List[Session]: - """List[:class:`.Session`]: The gateway sessions that the current user is connected in with. + def sessions(self) -> Sequence[Session]: + """Sequence[:class:`.Session`]: The gateway sessions that the current user is connected in with. When connected, this includes a representation of the library's session and an "all" session representing the user's overall presence. .. versionadded:: 2.0 """ - return list(self._connection._sessions.values()) + return utils.SequenceProxy(self._connection._sessions.values()) @property def cached_messages(self) -> Sequence[Message]: @@ -431,8 +431,8 @@ class Client: return utils.SequenceProxy(self._connection._messages or []) @property - def connections(self) -> List[Connection]: - """List[:class:`.Connection`]: The connections that the connected client has. + def connections(self) -> Sequence[Connection]: + """Sequence[:class:`.Connection`]: The connections that the connected client has. These connections don't have the :attr:`.Connection.metadata` attribute populated. @@ -442,20 +442,20 @@ class Client: Due to a Discord limitation, removed connections may not be removed from this cache. """ - return list(self._connection.connections.values()) + return utils.SequenceProxy(self._connection.connections.values()) @property - def private_channels(self) -> List[PrivateChannel]: - """List[:class:`.abc.PrivateChannel`]: The private channels that the connected client is participating on.""" + def private_channels(self) -> Sequence[PrivateChannel]: + """Sequence[:class:`.abc.PrivateChannel`]: The private channels that the connected client is participating on.""" return self._connection.private_channels @property - def relationships(self) -> List[Relationship]: - """List[:class:`.Relationship`]: Returns all the relationships that the connected client has. + def relationships(self) -> Sequence[Relationship]: + """Sequence[:class:`.Relationship`]: Returns all the relationships that the connected client has. .. versionadded:: 2.0 """ - return list(self._connection._relationships.values()) + return utils.SequenceProxy(self._connection._relationships.values()) @property def friends(self) -> List[Relationship]: @@ -531,12 +531,12 @@ class Client: return self._connection.preferred_regions @property - def pending_payments(self) -> List[Payment]: - """List[:class:`.Payment`]: The pending payments that the connected client has. + def pending_payments(self) -> Sequence[Payment]: + """Sequence[:class:`.Payment`]: The pending payments that the connected client has. .. versionadded:: 2.0 """ - return list(self._connection.pending_payments.values()) + return utils.SequenceProxy(self._connection.pending_payments.values()) def is_ready(self) -> bool: """:class:`bool`: Specifies if the client's internal cache is ready for use.""" diff --git a/discord/guild.py b/discord/guild.py index 283ec376d..ee5cb945a 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -26,7 +26,6 @@ from __future__ import annotations import copy from datetime import datetime -import logging import unicodedata from typing import ( Any, @@ -556,17 +555,17 @@ class Guild(Hashable): state.store_presence(user_id, presence, self.id) @property - def channels(self) -> List[GuildChannel]: - """List[:class:`abc.GuildChannel`]: A list of channels that belongs to this guild.""" - return list(self._channels.values()) + def channels(self) -> Sequence[GuildChannel]: + """Sequence[:class:`abc.GuildChannel`]: A list of channels that belongs to this guild.""" + return utils.SequenceProxy(self._channels.values()) @property - def threads(self) -> List[Thread]: - """List[:class:`Thread`]: A list of active threads that you have permission to view. + def threads(self) -> Sequence[Thread]: + """Sequence[:class:`Thread`]: A list of active threads that you have permission to view. .. versionadded:: 2.0 """ - return list(self._threads.values()) + return utils.SequenceProxy(self._threads.values()) @property def large(self) -> bool: @@ -870,9 +869,9 @@ class Guild(Hashable): return self._PREMIUM_GUILD_LIMITS[self.premium_tier].filesize @property - def members(self) -> List[Member]: - """List[:class:`Member`]: A list of members that belong to this guild.""" - return list(self._members.values()) + def members(self) -> Sequence[Member]: + """Sequence[:class:`Member`]: A list of members that belong to this guild.""" + return utils.SequenceProxy(self._members.values()) def get_member(self, user_id: int, /) -> Optional[Member]: """Returns a member with the given ID. @@ -899,13 +898,13 @@ class Guild(Hashable): return [member for member in self.members if member.premium_since is not None] @property - def roles(self) -> List[Role]: - """List[:class:`Role`]: Returns a :class:`list` of the guild's roles in hierarchy order. + def roles(self) -> Sequence[Role]: + """Sequence[:class:`Role`]: Returns a sequence of the guild's roles in hierarchy order. - The first element of this list will be the lowest role in the + The first element of this sequence will be the lowest role in the hierarchy. """ - return sorted(self._roles.values()) + return utils.SequenceProxy(self._roles.values(), sorted=True) def get_role(self, role_id: int, /) -> Optional[Role]: """Returns a role with the given ID. @@ -944,13 +943,13 @@ class Guild(Hashable): return None @property - def stage_instances(self) -> List[StageInstance]: - """List[:class:`StageInstance`]: Returns a :class:`list` of the guild's stage instances that + def stage_instances(self) -> Sequence[StageInstance]: + """Sequence[:class:`StageInstance`]: Returns a sequence of the guild's stage instances that are currently running. .. versionadded:: 2.0 """ - return list(self._stage_instances.values()) + return utils.SequenceProxy(self._stage_instances.values()) def get_stage_instance(self, stage_instance_id: int, /) -> Optional[StageInstance]: """Returns a stage instance with the given ID. @@ -970,12 +969,12 @@ class Guild(Hashable): return self._stage_instances.get(stage_instance_id) @property - def scheduled_events(self) -> List[ScheduledEvent]: - """List[:class:`ScheduledEvent`]: Returns a :class:`list` of the guild's scheduled events. + def scheduled_events(self) -> Sequence[ScheduledEvent]: + """Sequence[:class:`ScheduledEvent`]: Returns a sequence of the guild's scheduled events. .. versionadded:: 2.0 """ - return list(self._scheduled_events.values()) + return utils.SequenceProxy(self._scheduled_events.values()) def get_scheduled_event(self, scheduled_event_id: int, /) -> Optional[ScheduledEvent]: """Returns a scheduled event with the given ID. diff --git a/discord/role.py b/discord/role.py index 9cf9b66c1..33b8e5b99 100644 --- a/discord/role.py +++ b/discord/role.py @@ -350,7 +350,7 @@ class Role(Hashable): @property def members(self) -> List[Member]: """List[:class:`Member`]: Returns all the members with this role.""" - all_members = self.guild.members + all_members = list(self.guild._members.values()) if self.is_default(): return all_members diff --git a/discord/state.py b/discord/state.py index 00541fc61..bfc566f8d 100644 --- a/discord/state.py +++ b/discord/state.py @@ -43,6 +43,7 @@ from typing import ( Deque, Literal, overload, + Sequence, ) import weakref import inspect @@ -761,8 +762,8 @@ class ConnectionState: return sticker @property - def guilds(self) -> List[Guild]: - return list(self._guilds.values()) + def guilds(self) -> Sequence[Guild]: + return utils.SequenceProxy(self._guilds.values()) def _get_guild(self, guild_id: Optional[int]) -> Optional[Guild]: # The keys of self._guilds are ints @@ -786,12 +787,12 @@ class ConnectionState: del guild @property - def emojis(self) -> List[Emoji]: - return list(self._emojis.values()) + def emojis(self) -> Sequence[Emoji]: + return utils.SequenceProxy(self._emojis.values()) @property - def stickers(self) -> List[GuildSticker]: - return list(self._stickers.values()) + def stickers(self) -> Sequence[GuildSticker]: + return utils.SequenceProxy(self._stickers.values()) def get_emoji(self, emoji_id: Optional[int]) -> Optional[Emoji]: # the keys of self._emojis are ints @@ -802,8 +803,8 @@ class ConnectionState: return self._stickers.get(sticker_id) # type: ignore @property - def private_channels(self) -> List[PrivateChannel]: - return list(self._private_channels.values()) + def private_channels(self) -> Sequence[PrivateChannel]: + return utils.SequenceProxy(self._private_channels.values()) async def call_connect(self, channel_id: int) -> None: if self.ws is None: diff --git a/discord/utils.py b/discord/utils.py index eb42b19f6..9f9f874b8 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -31,6 +31,7 @@ from typing import ( AsyncIterator, Awaitable, Callable, + Collection, Coroutine, Dict, ForwardRef, @@ -219,31 +220,41 @@ def cached_slot_property(name: str) -> Callable[[Callable[[T], T_co]], CachedSlo class SequenceProxy(Sequence[T_co]): - """Read-only proxy of a Sequence.""" + """A proxy of a sequence that only creates a copy when necessary.""" - def __init__(self, proxied: Sequence[T_co]): - self.__proxied = proxied + def __init__(self, proxied: Collection[T_co], *, sorted: bool = False): + self.__proxied: Collection[T_co] = proxied + self.__sorted: bool = sorted + + @cached_property + def __copied(self) -> List[T_co]: + if self.__sorted: + # The type checker thinks the variance is wrong, probably due to the comparison requirements + self.__proxied = sorted(self.__proxied) # type: ignore + else: + self.__proxied = list(self.__proxied) + return self.__proxied def __getitem__(self, idx: int) -> T_co: - return self.__proxied[idx] + return self.__copied[idx] def __len__(self) -> int: return len(self.__proxied) def __contains__(self, item: Any) -> bool: - return item in self.__proxied + return item in self.__copied def __iter__(self) -> Iterator[T_co]: - return iter(self.__proxied) + return iter(self.__copied) def __reversed__(self) -> Iterator[T_co]: - return reversed(self.__proxied) + return reversed(self.__copied) def index(self, value: Any, *args: Any, **kwargs: Any) -> int: - return self.__proxied.index(value, *args, **kwargs) + return self.__copied.index(value, *args, **kwargs) def count(self, value: Any) -> int: - return self.__proxied.count(value) + return self.__copied.count(value) @overload diff --git a/docs/migrating.rst b/docs/migrating.rst index ad2ae254c..ecb99e444 100644 --- a/docs/migrating.rst +++ b/docs/migrating.rst @@ -775,6 +775,25 @@ The return type of the following methods has been changed to an :term:`asynchron The ``NoMoreItems`` exception was removed as calling :func:`anext` or :meth:`~object.__anext__` on an :term:`asynchronous iterator` will now raise :class:`StopAsyncIteration`. +Changing certain lists to be lazy sequences instead +----------------------------------------------------- + +In order to improve performance when calculating the length of certain lists, certain attributes were changed to return a sequence rather than a :class:`list`. + +A sequence is similar to a :class:`list` except it is read-only. In order to get a list again you can call :class:`list` on the resulting sequence. + +The following properties were changed to return a sequence instead of a list: + +- :attr:`Client.guilds` +- :attr:`Client.emojis` +- :attr:`Client.private_channels` +- :attr:`Guild.roles` +- :attr:`Guild.channels` +- :attr:`Guild.members` + +This change should be transparent, unless you are modifying the sequence by doing things such as ``list.append``. + + Removal of ``Embed.Empty`` ---------------------------