diff --git a/discord/client.py b/discord/client.py index 7518dca0e..b9c1d4135 100644 --- a/discord/client.py +++ b/discord/client.py @@ -370,18 +370,18 @@ 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 """ @@ -396,8 +396,8 @@ class Client: return utils.SequenceProxy(self._connection._messages or []) @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. .. note:: diff --git a/discord/guild.py b/discord/guild.py index 0abf4376a..a3accfc70 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -562,17 +562,17 @@ class Guild(Hashable): self._add_thread(Thread(guild=self, state=self._state, data=thread)) @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 threads that you have permission to view. + def threads(self) -> Sequence[Thread]: + """Sequence[:class:`Thread`]: A list of 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: @@ -817,9 +817,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. @@ -846,13 +846,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. @@ -904,13 +904,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. @@ -930,12 +930,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 86b5c9b2f..7eb71af7c 100644 --- a/discord/role.py +++ b/discord/role.py @@ -347,7 +347,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 21e93439d..25eda0632 100644 --- a/discord/state.py +++ b/discord/state.py @@ -378,8 +378,8 @@ class ConnectionState: return self._view_store.persistent_views @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 @@ -403,12 +403,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 @@ -419,8 +419,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()) def _get_private_channel(self, channel_id: Optional[int]) -> Optional[PrivateChannel]: try: diff --git a/discord/utils.py b/discord/utils.py index 484b179e5..6b56f4535 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -31,6 +31,7 @@ from typing import ( AsyncIterator, Awaitable, Callable, + Collection, Coroutine, Dict, ForwardRef, @@ -205,31 +206,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 d3402ce53..7b149133f 100644 --- a/docs/migrating.rst +++ b/docs/migrating.rst @@ -864,6 +864,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`` ---------------------------