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`` ---------------------------