diff --git a/hikari/api/event_factory.py b/hikari/api/event_factory.py index ffa365eb6b..4f77cdcb73 100644 --- a/hikari/api/event_factory.py +++ b/hikari/api/event_factory.py @@ -529,7 +529,7 @@ def deserialize_message_delete_event( def deserialize_message_delete_bulk_event( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject - ) -> message_events.MessageBulkDeleteEvent: + ) -> message_events.MessageDeleteEvent: """Parse a raw payload from Discord into a message delete bulk event object. Parameters @@ -541,8 +541,13 @@ def deserialize_message_delete_bulk_event( Returns ------- - hikari.events.message_events.MessageBulkDeleteEvent + hikari.events.message_events.MessageDeleteEvent The parsed message delete bulk event object. + + Raises + ------ + builtins.NotImplementedError + If a bulk delete occurs in a DM channel. """ def deserialize_message_reaction_add_event( diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index c5c37d0480..8d517d4264 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -48,8 +48,8 @@ from hikari import snowflakes from hikari.utilities import attr_extensions +from hikari.utilities import collections from hikari.utilities import enums -from hikari.utilities import mapping if typing.TYPE_CHECKING: from hikari import channels @@ -347,7 +347,7 @@ def __getitem__(self, slice_: slice, /) -> typing.Sequence[AuditLogEntry]: def __getitem__( self, index_or_slice: typing.Union[int, slice], / ) -> typing.Union[AuditLogEntry, typing.Sequence[AuditLogEntry]]: - return mapping.get_index_or_slice(self.entries, index_or_slice) + return collections.get_index_or_slice(self.entries, index_or_slice) def __iter__(self) -> typing.Iterator[AuditLogEntry]: return iter(self.entries.values()) diff --git a/hikari/events/channel_events.py b/hikari/events/channel_events.py index fc5bb6c65d..223a09adc7 100644 --- a/hikari/events/channel_events.py +++ b/hikari/events/channel_events.py @@ -70,7 +70,7 @@ from hikari.api import shard as gateway_shard -@base_events.requires_intents(intents.Intents.GUILDS, intents.Intents.PRIVATE_MESSAGES) +@base_events.requires_intents(intents.Intents.GUILDS, intents.Intents.DM_MESSAGES) @attr.s(kw_only=True, slots=True, weakref_slot=False) class ChannelEvent(shard_events.ShardEvent, abc.ABC): """Event base for any channel-bound event in guilds or private messages.""" @@ -231,7 +231,7 @@ async def fetch_channel(self) -> channels.PrivateChannel: return channel -@base_events.requires_intents(intents.Intents.GUILDS, intents.Intents.PRIVATE_MESSAGES) +@base_events.requires_intents(intents.Intents.GUILDS, intents.Intents.DM_MESSAGES) @attr.s(kw_only=True, slots=True, weakref_slot=False) class ChannelCreateEvent(ChannelEvent, abc.ABC): """Base event for any channel being created.""" @@ -280,7 +280,7 @@ def guild_id(self) -> snowflakes.Snowflake: return self.channel.guild_id -@base_events.requires_intents(intents.Intents.PRIVATE_MESSAGES) +@base_events.requires_intents(intents.Intents.DM_MESSAGES) @attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class DMChannelCreateEvent(DMChannelEvent, ChannelCreateEvent): @@ -302,7 +302,7 @@ class DMChannelCreateEvent(DMChannelEvent, ChannelCreateEvent): """ -@base_events.requires_intents(intents.Intents.GUILDS, intents.Intents.PRIVATE_MESSAGES) +@base_events.requires_intents(intents.Intents.GUILDS, intents.Intents.DM_MESSAGES) @attr.s(kw_only=True, slots=True, weakref_slot=False) class ChannelUpdateEvent(ChannelEvent, abc.ABC): """Base event for any channel being updated.""" @@ -351,7 +351,7 @@ def guild_id(self) -> snowflakes.Snowflake: return self.channel.guild_id -@base_events.requires_intents(intents.Intents.PRIVATE_MESSAGES) +@base_events.requires_intents(intents.Intents.DM_MESSAGES) @attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class DMChannelUpdateEvent(DMChannelEvent, ChannelUpdateEvent): @@ -373,7 +373,7 @@ class DMChannelUpdateEvent(DMChannelEvent, ChannelUpdateEvent): """ -@base_events.requires_intents(intents.Intents.GUILDS, intents.Intents.PRIVATE_MESSAGES) +@base_events.requires_intents(intents.Intents.GUILDS, intents.Intents.DM_MESSAGES) @attr.s(kw_only=True, slots=True, weakref_slot=False) class ChannelDeleteEvent(ChannelEvent, abc.ABC): """Base event for any channel being deleted.""" @@ -433,7 +433,7 @@ async def fetch_channel(self) -> typing.NoReturn: # TODO: can this actually ever get fired? -@base_events.requires_intents(intents.Intents.PRIVATE_MESSAGES) +@base_events.requires_intents(intents.Intents.DM_MESSAGES) @attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class DMChannelDeleteEvent(DMChannelEvent, ChannelDeleteEvent): diff --git a/hikari/events/message_events.py b/hikari/events/message_events.py index 6a080ffedb..5d04d1dbca 100644 --- a/hikari/events/message_events.py +++ b/hikari/events/message_events.py @@ -24,21 +24,16 @@ from __future__ import annotations __all__: typing.List[str] = [ - "MessagesEvent", "MessageEvent", - "GuildMessageEvent", - "DMMessageEvent", "MessageCreateEvent", - "GuildMessageCreateEvent", - "DMMessageCreateEvent", "MessageUpdateEvent", - "GuildMessageUpdateEvent", - "DMMessageUpdateEvent", "MessageDeleteEvent", + "GuildMessageCreateEvent", + "GuildMessageUpdateEvent", "GuildMessageDeleteEvent", + "DMMessageCreateEvent", + "DMMessageUpdateEvent", "DMMessageDeleteEvent", - "MessageBulkDeleteEvent", - "GuildMessageBulkDeleteEvent", ] import abc @@ -47,7 +42,6 @@ import attr from hikari import channels -from hikari import guilds from hikari import intents from hikari import snowflakes from hikari import undefined @@ -58,26 +52,16 @@ if typing.TYPE_CHECKING: from hikari import embeds as embeds_ + from hikari import guilds from hikari import messages from hikari import traits - from hikari.api import shard as gateway_shard + from hikari.api import shard as shard_ -@base_events.requires_intents(intents.Intents.GUILD_MESSAGES, intents.Intents.PRIVATE_MESSAGES) @attr.s(kw_only=True, slots=True, weakref_slot=False) -class MessagesEvent(shard_events.ShardEvent, abc.ABC): - """Event base for any message-bound event.""" - - @property - @abc.abstractmethod - def channel_id(self) -> snowflakes.Snowflake: - """ID of the channel that this event concerns. - - Returns - ------- - hikari.snowflakes.Snowflake - The ID of the channel that this event concerns. - """ +@base_events.requires_intents(intents.Intents.DM_MESSAGES, intents.Intents.GUILD_MESSAGES) +class MessageEvent(shard_events.ShardEvent, abc.ABC): + """Any event that concerns manipulation of messages.""" @property @abc.abstractmethod @@ -90,130 +74,60 @@ def channel(self) -> typing.Optional[channels.TextChannel]: The cached channel, if known. """ - -@base_events.requires_intents(intents.Intents.GUILD_MESSAGES, intents.Intents.PRIVATE_MESSAGES) -@attr.s(kw_only=True, slots=True, weakref_slot=False) -class MessageEvent(MessagesEvent, abc.ABC): - """Event base for any event that concerns a single message.""" - @property @abc.abstractmethod - def message_id(self) -> snowflakes.Snowflake: - """ID of the message that this event concerns. + def channel_id(self) -> snowflakes.Snowflake: + """ID of the channel that this event concerns. Returns ------- hikari.snowflakes.Snowflake - The ID of the message that this event concerns. + The ID of the channel that this event concerns. """ - -@base_events.requires_intents(intents.Intents.PRIVATE_MESSAGES) -@attr.s(kw_only=True, slots=True, weakref_slot=False) -class DMMessageEvent(MessageEvent, abc.ABC): - """Event base for any message-bound event in private messages.""" - - -@base_events.requires_intents(intents.Intents.GUILD_MESSAGES) -@attr.s(kw_only=True, slots=True, weakref_slot=False) -class GuildMessageEvent(MessageEvent, abc.ABC): - """Event base for any message-bound event in guild messages.""" - @property @abc.abstractmethod - def guild_id(self) -> snowflakes.Snowflake: - """ID of the guild that this event concerns. + def message_id(self) -> snowflakes.Snowflake: + """ID of the message that this event concerns. Returns ------- hikari.snowflakes.Snowflake - The ID of the guild that this event concerns. - """ - - @property - def channel(self) -> typing.Union[None, channels.GuildTextChannel, channels.GuildNewsChannel]: - """Channel that the message was sent in, if known. - - Returns - ------- - typing.Union[builtins.None, hikari.channels.GuildTextChannel, hikari.channels.GuildNewsChannel] - The channel the message was sent in, or `builtins.None` if not - known/cached. - - This otherwise will always be a `hikari.channels.GuildTextChannel` - if it is a normal message, or `hikari.channels.GuildNewsChannel` if - sent in an announcement channel. - """ - channel = self.app.cache.get_guild_channel(self.channel_id) - assert channel is None or isinstance( - channel, (channels.GuildTextChannel, channels.GuildNewsChannel) - ), f"expected cached channel to be None or a GuildTextChannel/GuildNewsChannel, not {channel}" - return channel - - @property - def guild(self) -> typing.Optional[guilds.GatewayGuild]: - """Get the cached guild this event corresponds to, if known. - - !!! note - You will need `hikari.intents.Intents.GUILDS` enabled to receive this - information. - - Returns - ------- - hikari.guilds.GatewayGuild - The gateway guild that this event corresponds to, if known and - cached. + The ID of the message that this event concerns. """ - return self.app.cache.get_guild(self.guild_id) -@base_events.requires_intents(intents.Intents.GUILD_MESSAGES, intents.Intents.PRIVATE_MESSAGES) @attr.s(kw_only=True, slots=True, weakref_slot=False) +@base_events.requires_intents(intents.Intents.DM_MESSAGES, intents.Intents.GUILD_MESSAGES) class MessageCreateEvent(MessageEvent, abc.ABC): - """Event base for any message creation event.""" + """Event that is fired when a message is created.""" @property - def message_id(self) -> snowflakes.Snowflake: - # <>. - return self.message.id - - @property - @abc.abstractmethod - def message(self) -> messages.Message: - """Message that was sent in the event. + def author(self) -> users.User: + """User that sent the message. Returns ------- - hikari.messages.Message - The message object that was sent with this event. + hikari.users.User + The user that sent the message. """ - - @property - def channel_id(self) -> snowflakes.Snowflake: - # <>. - return self.message.channel_id + return self.message.author @property def author_id(self) -> snowflakes.Snowflake: - """ID of the author that triggered this event. + """ID of the author of the message this event concerns. Returns ------- hikari.snowflakes.Snowflake - The ID of the author that triggered this event concerns. + The ID of the author. """ - return self.message.author.id + return self.author.id @property - @abc.abstractmethod - def author(self) -> users.User: - """User that sent the message. - - Returns - ------- - hikari.users.User - The user that sent the message. - """ + def channel_id(self) -> snowflakes.Snowflake: + # <> + return self.message.channel_id @property def content(self) -> typing.Optional[str]: @@ -274,383 +188,427 @@ def is_webhook(self) -> bool: """ return self.message.webhook_id is not None - -@base_events.requires_intents(intents.Intents.GUILD_MESSAGES, intents.Intents.PRIVATE_MESSAGES) -@attr.s(kw_only=True, slots=True, weakref_slot=False) -class MessageUpdateEvent(MessageEvent, abc.ABC): - """Event base for any message update event.""" - @property @abc.abstractmethod - def message(self) -> messages.PartialMessage: - """Partial message that was sent in the event. - - !!! warning - Unlike `MessageCreateEvent`, `MessageUpdateEvent.message` is an - arbitrarily partial version of `hikari.messages.Message` - where any field except `id` and `channel_id` may be set to - `hikari.undefined.UndefinedType` (a singleton) to indicate - that it has not been changed. + def message(self) -> messages.Message: + """Message that was sent in the event. Returns ------- - hikari.messages.PartialMessage - The partially populated message object that was sent with this - event. + hikari.messages.Message + The message object that was sent with this event. """ @property def message_id(self) -> snowflakes.Snowflake: - # <>. + """ID of the message that this event concerns. + + Returns + ------- + hikari.snowflakes.Snowflake + The ID of the message that this event concerns. + """ return self.message.id + +@attr_extensions.with_copy +@attr.s(kw_only=True, slots=True, weakref_slot=False) +@base_events.requires_intents(intents.Intents.GUILD_MESSAGES) +class GuildMessageCreateEvent(MessageCreateEvent): + """Event that is fired when a message is created within a guild. + + This contains the full message in the internal `message` attribute. + """ + + app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <> + + message: messages.Message = attr.ib() + # <> + + shard: shard_.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <> + @property - def channel_id(self) -> snowflakes.Snowflake: - # <>. - return self.message.channel_id + def author(self) -> guilds.Member: + """Member or user that sent the message. + + Returns + ------- + hikari.guilds.Member + The member that sent the message. + """ + member = self.message.member + assert member is not None, "no member given for guild message create event!" + return member @property - @abc.abstractmethod - def channel(self) -> typing.Optional[channels.TextChannel]: + def channel(self) -> typing.Union[None, channels.GuildTextChannel, channels.GuildNewsChannel]: """Channel that the message was sent in, if known. Returns ------- - typing.Optional[hikari.channels.TextChannel] - The text channel that the message was sent in, if known and cached, + typing.Union[builtins.None, hikari.channels.GuildTextChannel, hikari.channels.GuildNewsChannel] + The channel that the message was sent in, if known and cached, otherwise, `builtins.None`. """ + channel = self.app.cache.get_guild_channel(self.channel_id) + assert isinstance( + channel, (channels.GuildNewsChannel, channels.GuildTextChannel) + ), f"Cached channel ID is not a GuildNewsChannel or a GuildTextChannel, but a {type(channel).__name__}!" + return channel @property - def author_id(self) -> snowflakes.Snowflake: - """ID of the author that triggered this event. + def guild(self) -> typing.Optional[guilds.GatewayGuild]: + """Get the cached guild that this event occurred in, if known. + + !!! note + This will require the `GUILDS` intent to be specified on start-up + in order to be known. Returns ------- - hikari.snowflakes.Snowflake - The ID of the author that triggered this event concerns. + typing.Optional[hikari.guilds.GatewayGuild] + The guild that this event occurred in, if cached. Otherwise, + `builtins.None` instead. """ - # Looks like `author` is always present in this event variant. - author = self.message.author - assert isinstance(author, users.User), "message.author was expected to be present" - return author.id + return self.app.cache.get_guild(self.guild_id) @property - @abc.abstractmethod - def author(self) -> typing.Optional[users.User]: - """User that sent the message. + def guild_id(self) -> snowflakes.Snowflake: + """ID of the guild that this event occurred in. Returns ------- - typing.Optional[hikari.users.User] - The user that sent the message, if known and cached, otherwise - `builtins.None`. + hikari.snowflakes.Snowflake + The ID of the guild that this event occurred in. """ - author = self.message.author - assert isinstance(author, users.User), "message.author was expected to be present" - return author + guild_id = self.message.guild_id + # Always present on guild events + assert isinstance(guild_id, snowflakes.Snowflake), "no guild_id attribute set" + return guild_id -@base_events.requires_intents(intents.Intents.GUILD_MESSAGES, intents.Intents.PRIVATE_MESSAGES) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) -class MessageDeleteEvent(MessageEvent, abc.ABC): - """Event base for any message delete event.""" +@base_events.requires_intents(intents.Intents.DM_MESSAGES) +class DMMessageCreateEvent(MessageCreateEvent): + """Event that is fired when a message is created within a DM. + + This contains the full message in the internal `message` attribute. + """ + + app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <> + + message: messages.Message = attr.ib() + # <> + + shard: shard_.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <> @property - @abc.abstractmethod - def message(self) -> messages.PartialMessage: - """Partial message that was sent in the event. + def channel(self) -> typing.Optional[channels.DMChannel]: + """Channel that the message was sent in, if known. - !!! warning - Unlike `MessageCreateEvent`, `message` is a severely limited partial - version of `hikari.messages.Message`. The only attributes - that will not be `hikari.undefined.UNDEFINED` will be - `id`, `channel_id`, and `guild_id` if the message was in a guild. - This is a limitation of Discord. + Returns + ------- + typing.Optional[hikari.channels.DMChannel] + The channel that the message was sent in, if known and cached, + otherwise, `builtins.None`. + """ + return self.app.cache.get_dm_channel(self.author_id) + + +@attr.s(kw_only=True, slots=True, weakref_slot=False) +@base_events.requires_intents(intents.Intents.DM_MESSAGES, intents.Intents.GUILD_MESSAGES) +class MessageUpdateEvent(MessageEvent, abc.ABC): + """Event that is fired when a message is updated. - Furthermore, this partial message will represent a message that no - longer exists. Thus, attempting to edit/delete/react or un-react to - this message or attempting to fetch the full version will result - in a `hikari.errors.NotFoundError` being raised. + !!! note + Less information will be available here than in the creation event + due to Discord limitations. + """ + + @property + def author(self) -> typing.Optional[users.User]: + """User that sent the message. Returns ------- - hikari.messages.PartialMessage - The partially populated message object that was sent with this - event. + typing.Optional[hikari.users.User] + The user that sent the message. + + This will be `builtins.None` in some cases, such as when Discord + updates a message with an embed for a URL preview. """ + return self.message.author @property - def message_id(self) -> snowflakes.Snowflake: - # <>. - return self.message.id + def author_id(self) -> typing.Optional[snowflakes.Snowflake]: + """ID of the author that triggered this event. + + Returns + ------- + typing.Optional[hikari.snowflakes.Snowflake] + The ID of the author that triggered this event concerns. + + This will be `builtins.None` in some cases, such as + when Discord updates a message with an embed for a URL preview. + """ + author = self.message.author + return author.id if author is not None else None @property def channel_id(self) -> snowflakes.Snowflake: - # <>. + # <>. return self.message.channel_id @property - @abc.abstractmethod - def channel(self) -> typing.Optional[channels.TextChannel]: - """Channel that the message was sent in, if known. + def content(self) -> undefined.UndefinedNoneOr[str]: + """Content of the message. Returns ------- - typing.Optional[hikari.channels.TextChannel] - The text channel that the message was sent in, if known and cached, - otherwise, `builtins.None`. + hikari.undefined.UndefinedNoneOr[builtins.str] + The content of the message, if present. This may be `builtins.None` + or an empty string (or any falsy value) if no content is present + (e.g. if only an embed was sent). If not part of the update, then + this will be `hikari.undefined.UNDEFINED` instead. """ + return self.message.content + @property + def embeds(self) -> undefined.UndefinedOr[typing.Sequence[embeds_.Embed]]: + """Sequence of embeds in the message. -@base_events.requires_intents(intents.Intents.GUILD_MESSAGES) -@attr_extensions.with_copy -@attr.s(kw_only=True, slots=True, weakref_slot=False) -class GuildMessageCreateEvent(GuildMessageEvent, MessageCreateEvent): - """Event triggered when a message is sent to a guild channel.""" - - app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) - # <>. - - shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) - # <>. - - message: messages.Message = attr.ib() - # <>. + Returns + ------- + hikari.undefined.UndefinedOr[typing.Sequence[hikari.embeds.Embed]] + The embeds in the message. If the embeds were not changed in this + event, then this may instead be `hikari.undefined.Undefined`. + """ + return self.message.embeds @property - def guild_id(self) -> snowflakes.Snowflake: - # <>. - # Always present in this event. - guild_id = self.message.guild_id - assert isinstance(guild_id, snowflakes.Snowflake) - return guild_id + def is_bot(self) -> typing.Optional[bool]: + """Return `builtins.True` if the message is from a bot. - @property - def author(self) -> users.User: - """Member that sent the message. + Returns + ------- + typing.Optional[builtins.bool] + `builtins.True` if from a bot, or `builtins.False` otherwise. - !!! note - For webhooks, this will be a `hikari.users.User`. + If the author is not known, due to the update event being caused + by Discord adding an embed preview to accompany a URL, then this + will return `builtins.None` instead. + """ + if (author := self.message.author) is not None: + return author.is_bot + return None - Any code relying on this being a `hikari.guilds.Member` directly - should use an `isinstance` assertion to determine if member info - is available or not. + @property + def is_human(self) -> typing.Optional[bool]: + """Return `builtins.True` if the message was created by a human. Returns ------- - hikari.users.User - The member that sent the message, if known. This is a specialised - implementation of `hikari.users.User`. + typing.Optional[builtins.bool] + `builtins.True` if from a human user, or `builtins.False` otherwise. - If the author was a webhook, then a `hikari.users.User` will be - returned instead, as webhooks do not have member objects. + If the author is not known, due to the update event being caused + by Discord adding an embed preview to accompany a URL, then this + may return `builtins.None` instead. """ - member = self.message.member - if member is not None: - return member - return self.message.author + # Not second-guessing some weird edge case will occur in the future with this, + # so I am being safe rather than sorry. + if self.message.webhook_id is not None: + return False + if (author := self.message.author) is not None: + return not author.is_bot -@base_events.requires_intents(intents.Intents.PRIVATE_MESSAGES) -@attr_extensions.with_copy -@attr.s(kw_only=True, slots=True, weakref_slot=False) -class DMMessageCreateEvent(DMMessageEvent, MessageCreateEvent): - """Event triggered when a message is sent to a private channel.""" - - app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) - # <>. + return None - shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) - # <>. + @property + def is_webhook(self) -> bool: + """Return `builtins.True` if the message was created by a webhook. - message: messages.Message = attr.ib() - # <>. + Returns + ------- + builtins.bool + `builtins.True` if from a webhook, or `builtins.False` otherwise. + """ + return self.message.webhook_id is not None @property - def channel(self) -> typing.Optional[channels.DMChannel]: - """Channel that the message was sent in, if known. + @abc.abstractmethod + def message(self) -> messages.PartialMessage: + """Partial message that was sent in the event. Returns ------- - typing.Optional[hikari.channels.DMChannel] - The DM channel that the message was sent in, if known and cached, - otherwise, `builtins.None`. + hikari.messages.PartialMessage + The partial message object that was sent with this event. """ - return self.app.cache.get_dm_channel(self.author_id) @property - def author(self) -> users.User: - # <>. - return self.message.author + def message_id(self) -> snowflakes.Snowflake: + """ID of the message that this event concerns. + + Returns + ------- + hikari.snowflakes.Snowflake + The ID of the message that this event concerns. + """ + return self.message.id -@base_events.requires_intents(intents.Intents.GUILD_MESSAGES) @attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) -class GuildMessageUpdateEvent(GuildMessageEvent, MessageUpdateEvent): - """Event triggered when a message is updated in a guild channel.""" +@base_events.requires_intents(intents.Intents.GUILD_MESSAGES) +class GuildMessageUpdateEvent(MessageUpdateEvent): + """Event that is fired when a message is updated in a guild. - app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) - # <>. + !!! note + Less information will be available here than in the creation event + due to Discord limitations. + """ - shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) - # <>. + app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <> message: messages.PartialMessage = attr.ib() - # <>. + # <> - @property - def guild_id(self) -> snowflakes.Snowflake: - # <>. - # Always present in this event. - guild_id = self.message.guild_id - assert isinstance(guild_id, snowflakes.Snowflake) - return guild_id + shard: shard_.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <> @property - def author(self) -> typing.Union[guilds.Member, users.User]: - # <>. + def author(self) -> typing.Optional[users.User]: + """Member or user that sent the message. + + Returns + ------- + typing.Union[builtins.None, hikari.users.User, hikari.guilds.Member] + The user that sent the message. If the member is cached + (the intents are enabled), then this will be the corresponding + member object instead (which is a specialization of the + user object you should otherwise expect). + + This will be `builtins.None` in some cases, such as when Discord + updates a message with an embed for a URL preview. + """ member = self.message.member - if member is not undefined.UNDEFINED and member is not None: - return member - member = self.app.cache.get_member(self.guild_id, self.author_id) if member is not None: return member - # This should always be present. author = self.message.author - assert isinstance(author, users.User), "expected author to be present" - return author + if author is not None: + member = self.app.cache.get_member(self.guild_id, author.id) -@base_events.requires_intents(intents.Intents.PRIVATE_MESSAGES) -@attr_extensions.with_copy -@attr.s(kw_only=True, slots=True, weakref_slot=False) -class DMMessageUpdateEvent(DMMessageEvent, MessageUpdateEvent): - """Event triggered when a message is updated in a private channel.""" - - app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) - # <>. + if member is not None: + return member - shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) - # <>. - - message: messages.PartialMessage = attr.ib() - # <>. - - @property - def channel(self) -> typing.Optional[channels.DMChannel]: - # <>. - return self.app.cache.get_dm_channel(self.author_id) - - @property - def author(self) -> typing.Optional[users.User]: - # Always present on an update event. - author = self.message.author - assert isinstance(author, users.User), "expected author to be present on PartialMessage" return author + @property + def channel(self) -> typing.Union[None, channels.GuildTextChannel, channels.GuildNewsChannel]: + """Channel that the message was sent in, if known. -@base_events.requires_intents(intents.Intents.GUILD_MESSAGES) -@attr_extensions.with_copy -@attr.s(kw_only=True, slots=True, weakref_slot=False) -class GuildMessageDeleteEvent(GuildMessageEvent, MessageDeleteEvent): - """Event triggered when a message is deleted from a guild channel.""" + Returns + ------- + typing.Union[builtins.None, hikari.channels.GuildTextChannel, hikari.channels.GuildNewsChannel] + The channel that the message was sent in, if known and cached, + otherwise, `builtins.None`. + """ + channel = self.app.cache.get_guild_channel(self.channel_id) + assert isinstance( + channel, (channels.GuildNewsChannel, channels.GuildTextChannel) + ), f"Cached channel ID is not a GuildNewsChannel or a GuildTextChannel, but a {type(channel).__name__}!" + return channel - app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) - # <>. + @property + def guild(self) -> typing.Optional[guilds.GatewayGuild]: + """Get the cached guild that this event occurred in, if known. - shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) - # <>. + !!! note + This will require the `GUILDS` intent to be specified on start-up + in order to be known. - message: messages.PartialMessage = attr.ib() - # <>. + Returns + ------- + typing.Optional[hikari.guilds.GatewayGuild] + The guild that this event occurred in, if cached. Otherwise, + `builtins.None` instead. + """ + return self.app.cache.get_guild(self.guild_id) @property def guild_id(self) -> snowflakes.Snowflake: - # <>. - # Always present in this event. + """ID of the guild that this event occurred in. + + Returns + ------- + hikari.snowflakes.Snowflake + The ID of the guild that this event occurred in. + """ guild_id = self.message.guild_id - assert isinstance(guild_id, snowflakes.Snowflake), f"expected guild_id to be snowflake, not {guild_id}" + # Always present on guild events + assert isinstance(guild_id, snowflakes.Snowflake), f"expected guild_id, got {guild_id}" return guild_id @attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) -@base_events.requires_intents(intents.Intents.PRIVATE_MESSAGES) -class DMMessageDeleteEvent(DMMessageEvent, MessageDeleteEvent): - """Event triggered when a message is deleted from a private channel.""" +@base_events.requires_intents(intents.Intents.DM_MESSAGES) +class DMMessageUpdateEvent(MessageUpdateEvent): + """Event that is fired when a message is updated in a DM. - app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) - # <>. + !!! note + Less information will be available here than in the creation event + due to Discord limitations. + """ - shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) - # <>. + app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <> message: messages.PartialMessage = attr.ib() - # <>. + # <> + + shard: shard_.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <> @property def channel(self) -> typing.Optional[channels.DMChannel]: - # <>. - # TODO: implement when we can find cached private channel by ID - return None - - -# NOTE: if this ever has a private channel equivalent implemented, this intents -# constraint should be relaxed. -@attr.s(kw_only=True, slots=True, weakref_slot=False) -@base_events.requires_intents(intents.Intents.GUILD_MESSAGES) -class MessageBulkDeleteEvent(MessagesEvent, abc.ABC): - """Event triggered when messages are bulk-deleted from a channel. - - !!! note - There is only a guild equivalent of this event at the time of writing. - However, Discord appear to not be ruling out that this ability may - be implemented for private channels in the future. Thus, this base - exists for future compatibility and consistency. + """Channel that the message was sent in, if known. - If you care about the event occurring in a guild specifically, you - should use the `GuildMessageBulkDeleteEvent`. Otherwise, using this - event base is acceptable. + Returns + ------- + typing.Optional[hikari.channels.DMChannel] + The channel that the message was sent in, if known and cached, + otherwise, `builtins.None`. - See https://github.com/discord/discord-api-docs/issues/1633 for - Discord's response. - """ + Likewise, if the author is not known, this will also return + `builtins.None`. + """ + if (author_id := self.author_id) is not None: + return self.app.cache.get_dm_channel(author_id) + return None -@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) -@base_events.requires_intents(intents.Intents.GUILD_MESSAGES) -class GuildMessageBulkDeleteEvent(MessageBulkDeleteEvent): - """Event triggered when messages are bulk-deleted from a guild channel.""" - - app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) - # <>. - - shard: gateway_shard.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) - # <>. - - channel_id: snowflakes.Snowflake = attr.ib() - # <>. - - guild_id: snowflakes.Snowflake = attr.ib() - """ID of the guild that this event concerns. - - Returns - ------- - hikari.snowflakes.Snowflake - The ID of the guild that this event concerns. - """ +@base_events.requires_intents(intents.Intents.GUILD_MESSAGES, intents.Intents.DM_MESSAGES) +class MessageDeleteEvent(MessageEvent, abc.ABC): + """Special event that is triggered when one or more messages get deleted. - message_ids: typing.Sequence[snowflakes.Snowflake] = attr.ib() - """Sequence of message IDs that were bulk deleted. + !!! note + Due to Discord limitations, most message information is unavailable + during deletion events. - Returns - ------- - typing.Sequence[hikari.snowflakes.Snowflake] - A sequence of message IDs that were bulk deleted. + You can check if the message was in a singular deletion by checking the + `is_bulk` attribute. """ @property @@ -673,6 +631,87 @@ def channel(self) -> typing.Union[None, channels.GuildTextChannel, channels.Guil ), f"expected cached channel to be None or a GuildTextChannel/GuildNewsChannel, not {channel}" return channel + @property + def message_id(self) -> snowflakes.Snowflake: + """Get the ID of the first deleted message. + + This is contextually useful if you know this is not a bulk deletion + event. For all other purposes, this is the same as running + `next(iter(event.message_ids))`. + + Returns + ------- + hikari.snowflakes.Snowflake + The first deleted message ID. + """ + try: + return next(iter(self.message_ids)) + except StopIteration: + raise RuntimeError("No messages were sent in a bulk delete! Please shout at Discord to fix this!") from None + + @property + @abc.abstractmethod + def message_ids(self) -> typing.AbstractSet[snowflakes.Snowflake]: + """Set of message IDs that were bulk deleted. + + Returns + ------- + typing.AbstractSet[hikari.snowflakes.Snowflake] + A sequence of message IDs that were bulk deleted. + """ + + @property + @abc.abstractmethod + def is_bulk(self) -> bool: + """Flag that determines whether this was a bulk deletion or not. + + Returns + ------- + builtins.bool + `builtins.True` if this was a bulk deletion, or `builtins.False` + if it was a regular message deletion. + """ + + +@attr_extensions.with_copy +@attr.s(kw_only=True, slots=True, weakref_slot=False) +@base_events.requires_intents(intents.Intents.GUILD_MESSAGES) +class GuildMessageDeleteEvent(MessageDeleteEvent): + """Event that is triggered if messages are deleted in a guild. + + !!! note + Due to Discord limitations, most message information is unavailable + during deletion events. + + This is triggered for singular message deletion, and bulk message + deletion. You can check if the message was in a singular deletion by + checking the `is_bulk` attribute. + """ + + app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <> + + channel_id: snowflakes.Snowflake = attr.ib() + # <> + + guild_id: snowflakes.Snowflake = attr.ib() + """ID of the guild that this event occurred in. + + Returns + ------- + hikari.snowflakes.Snowflake + The ID of the guild. + """ + + is_bulk: bool = attr.ib() + # <> + + message_ids: typing.AbstractSet[snowflakes.Snowflake] = attr.ib() + # <> + + shard: shard_.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <> + @property def guild(self) -> typing.Optional[guilds.GatewayGuild]: """Get the cached guild this event corresponds to, if known. @@ -687,4 +726,37 @@ def guild(self) -> typing.Optional[guilds.GatewayGuild]: The gateway guild that this event corresponds to, if known and cached. """ - return self.app.cache.get_available_guild(self.guild_id) or self.app.cache.get_unavailable_guild(self.guild_id) + return self.app.cache.get_guild(self.guild_id) + + +@attr_extensions.with_copy +@attr.s(kw_only=True, slots=True, weakref_slot=False) +@base_events.requires_intents(intents.Intents.DM_MESSAGES) +class DMMessageDeleteEvent(MessageDeleteEvent): + """Event that is triggered if messages are deleted in a DM. + + !!! note + Due to Discord limitations, most message information is unavailable + during deletion events. + + This is triggered for singular message deletion, and bulk message + deletion, although the latter is not expected to occur in DMs. + + You can check if the message was in a singular deletion by checking the + `is_bulk` attribute. + """ + + app: traits.RESTAware = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <> + + channel_id: snowflakes.Snowflake = attr.ib() + # <> + + is_bulk: bool = attr.ib() + # <> + + message_ids: typing.AbstractSet[snowflakes.Snowflake] = attr.ib() + # <> + + shard: shard_.GatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) + # <> diff --git a/hikari/events/reaction_events.py b/hikari/events/reaction_events.py index 498a0c217c..123ba8106a 100644 --- a/hikari/events/reaction_events.py +++ b/hikari/events/reaction_events.py @@ -60,7 +60,7 @@ @attr.s(kw_only=True, slots=True, weakref_slot=False) -@base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.PRIVATE_MESSAGE_REACTIONS) +@base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.DM_MESSAGE_REACTIONS) class ReactionEvent(shard_events.ShardEvent, abc.ABC): """Event base for any message reaction event.""" @@ -105,13 +105,13 @@ def guild_id(self) -> snowflakes.Snowflake: @attr.s(kw_only=True, slots=True, weakref_slot=False) -@base_events.requires_intents(intents.Intents.PRIVATE_MESSAGE_REACTIONS) +@base_events.requires_intents(intents.Intents.DM_MESSAGE_REACTIONS) class DMReactionEvent(ReactionEvent, abc.ABC): """Event base for any reaction-bound event in private messages.""" @attr.s(kw_only=True, slots=True, weakref_slot=False) -@base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.PRIVATE_MESSAGE_REACTIONS) +@base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.DM_MESSAGE_REACTIONS) class ReactionAddEvent(ReactionEvent, abc.ABC): """Event base for any reaction that is added to a message.""" @@ -140,7 +140,7 @@ def emoji(self) -> emojis.Emoji: @attr.s(kw_only=True, slots=True, weakref_slot=False) -@base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.PRIVATE_MESSAGE_REACTIONS) +@base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.DM_MESSAGE_REACTIONS) class ReactionDeleteEvent(ReactionEvent, abc.ABC): """Event base for any single reaction that is removed from a message.""" @@ -170,13 +170,13 @@ def emoji(self) -> emojis.Emoji: @attr.s(kw_only=True, slots=True, weakref_slot=False) -@base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.PRIVATE_MESSAGE_REACTIONS) +@base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.DM_MESSAGE_REACTIONS) class ReactionDeleteAllEvent(ReactionEvent, abc.ABC): """Event base fired when all reactions are removed from a message.""" @attr.s(kw_only=True, slots=True, weakref_slot=False) -@base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.PRIVATE_MESSAGE_REACTIONS) +@base_events.requires_intents(intents.Intents.GUILD_MESSAGE_REACTIONS, intents.Intents.DM_MESSAGE_REACTIONS) class ReactionDeleteEmojiEvent(ReactionEvent, abc.ABC): """Event base fired when all reactions are removed for one emoji.""" @@ -313,7 +313,7 @@ class GuildReactionDeleteAllEvent(GuildReactionEvent, ReactionDeleteAllEvent): @attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) -@base_events.requires_intents(intents.Intents.PRIVATE_MESSAGE_REACTIONS) +@base_events.requires_intents(intents.Intents.DM_MESSAGE_REACTIONS) class DMReactionAddEvent(DMReactionEvent, ReactionAddEvent): """Event fired when a reaction is added to a guild message.""" @@ -338,7 +338,7 @@ class DMReactionAddEvent(DMReactionEvent, ReactionAddEvent): @attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) -@base_events.requires_intents(intents.Intents.PRIVATE_MESSAGE_REACTIONS) +@base_events.requires_intents(intents.Intents.DM_MESSAGE_REACTIONS) class DMReactionDeleteEvent(DMReactionEvent, ReactionDeleteEvent): """Event fired when a reaction is removed from a private message.""" @@ -363,7 +363,7 @@ class DMReactionDeleteEvent(DMReactionEvent, ReactionDeleteEvent): @attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) -@base_events.requires_intents(intents.Intents.PRIVATE_MESSAGE_REACTIONS) +@base_events.requires_intents(intents.Intents.DM_MESSAGE_REACTIONS) class DMReactionDeleteEmojiEvent(DMReactionEvent, ReactionDeleteEmojiEvent): """Event fired when an emoji is removed from a private message's reactions.""" @@ -385,7 +385,7 @@ class DMReactionDeleteEmojiEvent(DMReactionEvent, ReactionDeleteEmojiEvent): @attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) -@base_events.requires_intents(intents.Intents.PRIVATE_MESSAGE_REACTIONS) +@base_events.requires_intents(intents.Intents.DM_MESSAGE_REACTIONS) class DMReactionDeleteAllEvent(DMReactionEvent, ReactionDeleteAllEvent): """Event fired when all of a private message's reactions are removed.""" diff --git a/hikari/events/shard_events.py b/hikari/events/shard_events.py index 87606ce94e..ac6d62c00d 100644 --- a/hikari/events/shard_events.py +++ b/hikari/events/shard_events.py @@ -40,7 +40,7 @@ from hikari.events import base_events from hikari.utilities import attr_extensions -from hikari.utilities import mapping +from hikari.utilities import collections if typing.TYPE_CHECKING: from hikari import guilds @@ -249,7 +249,7 @@ def __getitem__(self, index_or_slice: slice, /) -> typing.Sequence[guilds.Member def __getitem__( self, index_or_slice: typing.Union[int, slice], / ) -> typing.Union[guilds.Member, typing.Sequence[guilds.Member]]: - return mapping.get_index_or_slice(self.members, index_or_slice) + return collections.get_index_or_slice(self.members, index_or_slice) def __iter__(self) -> typing.Iterator[guilds.Member]: return iter(self.members.values()) diff --git a/hikari/events/typing_events.py b/hikari/events/typing_events.py index 3dc65b8489..c74ebfccec 100644 --- a/hikari/events/typing_events.py +++ b/hikari/events/typing_events.py @@ -50,7 +50,7 @@ from hikari.api import shard as gateway_shard -@base_events.requires_intents(intents.Intents.GUILD_MESSAGE_TYPING, intents.Intents.PRIVATE_MESSAGE_TYPING) +@base_events.requires_intents(intents.Intents.GUILD_MESSAGE_TYPING, intents.Intents.DM_MESSAGE_TYPING) class TypingEvent(shard_events.ShardEvent, abc.ABC): """Base event fired when a user begins typing in a channel.""" @@ -258,7 +258,7 @@ async def fetch_user(self) -> guilds.Member: return await self.app.rest.fetch_member(self.guild_id, self.user_id) -@base_events.requires_intents(intents.Intents.PRIVATE_MESSAGES) +@base_events.requires_intents(intents.Intents.DM_MESSAGES) @attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class DMTypingEvent(TypingEvent): diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 2c332c183b..abf2a5d338 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -1406,19 +1406,17 @@ def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> ################## def deserialize_partial_message(self, payload: data_binding.JSONObject) -> message_models.PartialMessage: - author: undefined.UndefinedOr[user_models.User] = undefined.UNDEFINED - if "author" in payload: - author = self.deserialize_user(payload["author"]) + author: typing.Optional[user_models.User] = None + if author_pl := payload.get("author"): + author = self.deserialize_user(author_pl) - guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = undefined.UNDEFINED + guild_id: typing.Optional[snowflakes.Snowflake] = None + member: typing.Optional[guild_models.Member] = None if "guild_id" in payload: guild_id = snowflakes.Snowflake(payload["guild_id"]) - member: undefined.UndefinedOr[guild_models.Member] = undefined.UNDEFINED - if "member" in payload: - assert author is not undefined.UNDEFINED - assert guild_id is not undefined.UNDEFINED - member = self.deserialize_member(payload["member"], user=author, guild_id=guild_id) + if author is not None and (member_pl := payload.get("member")): + member = self.deserialize_member(member_pl, user=author, guild_id=guild_id) timestamp: undefined.UndefinedOr[datetime.datetime] = undefined.UNDEFINED if "timestamp" in payload: diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py index 0717f238b8..c4819b2cad 100644 --- a/hikari/impl/event_factory.py +++ b/hikari/impl/event_factory.py @@ -45,6 +45,7 @@ from hikari.events import typing_events from hikari.events import user_events from hikari.events import voice_events +from hikari.utilities import collections from hikari.utilities import data_binding from hikari.utilities import date @@ -361,26 +362,45 @@ def deserialize_message_update_event( # noqa: CFQ001 def deserialize_message_delete_event( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject ) -> message_events.MessageDeleteEvent: - message = self._app.entity_factory.deserialize_partial_message(payload) + channel_id = snowflakes.Snowflake(payload["channel_id"]) + message_ids = collections.SnowflakeSet(int(payload["id"])) - if message.guild_id is None: - return message_events.DMMessageDeleteEvent(app=self._app, shard=shard, message=message) + if "guild_id" in payload: + return message_events.GuildMessageDeleteEvent( + app=self._app, + shard=shard, + channel_id=channel_id, + message_ids=message_ids, + is_bulk=False, + guild_id=snowflakes.Snowflake(payload["guild_id"]), + ) - return message_events.GuildMessageDeleteEvent(app=self._app, shard=shard, message=message) + return message_events.DMMessageDeleteEvent( + app=self._app, + shard=shard, + channel_id=channel_id, + message_ids=message_ids, + is_bulk=False, + ) def deserialize_message_delete_bulk_event( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject - ) -> message_events.MessageBulkDeleteEvent: - if "guild_id" not in payload: - raise NotImplementedError("No implementation for private message bulk delete events") + ) -> message_events.MessageDeleteEvent: - return message_events.GuildMessageBulkDeleteEvent( - app=self._app, - shard=shard, - channel_id=snowflakes.Snowflake(payload["channel_id"]), - guild_id=snowflakes.Snowflake(payload["guild_id"]), - message_ids=[snowflakes.Snowflake(message_id) for message_id in payload["ids"]], - ) + message_ids = collections.SnowflakeSet(*(snowflakes.Snowflake(message_id) for message_id in payload["ids"])) + channel_id = snowflakes.Snowflake(payload["channel_id"]) + + if "guild_id" in payload: + return message_events.GuildMessageDeleteEvent( + app=self._app, + shard=shard, + channel_id=channel_id, + guild_id=snowflakes.Snowflake(payload["guild_id"]), + message_ids=message_ids, + is_bulk=True, + ) + + raise NotImplementedError("DM bulk deletes are not documented behavior") def deserialize_message_reaction_add_event( self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject diff --git a/hikari/impl/stateful_cache.py b/hikari/impl/stateful_cache.py index 7fc2ac9a0a..b62b65fbb6 100644 --- a/hikari/impl/stateful_cache.py +++ b/hikari/impl/stateful_cache.py @@ -30,6 +30,7 @@ import logging import typing +import hikari.utilities.collections from hikari import channels from hikari import emojis from hikari import errors @@ -43,7 +44,7 @@ from hikari import voices from hikari.api import cache from hikari.utilities import cache as cache_utility -from hikari.utilities import mapping +from hikari.utilities import collections if typing.TYPE_CHECKING: from hikari import traits @@ -103,17 +104,17 @@ class StatefulCacheImpl(cache.MutableCache): # For the sake of keeping things clean, the annotations are being kept separate from the assignment here. _app: traits.RESTAware _me: typing.Optional[users.OwnUser] - _dm_entries: mapping.MappedCollection[snowflakes.Snowflake, cache_utility.DMChannelData] - _emoji_entries: mapping.MappedCollection[snowflakes.Snowflake, cache_utility.KnownCustomEmojiData] - _guild_channel_entries: mapping.MappedCollection[snowflakes.Snowflake, channels.GuildChannel] - _guild_entries: mapping.MappedCollection[snowflakes.Snowflake, cache_utility.GuildRecord] - _invite_entries: mapping.MappedCollection[str, cache_utility.InviteData] - _role_entries: mapping.MappedCollection[snowflakes.Snowflake, guilds.Role] - _unknown_custom_emoji_entries: mapping.MappedCollection[ + _dm_entries: collections.ExtendedMutableMapping[snowflakes.Snowflake, cache_utility.DMChannelData] + _emoji_entries: collections.ExtendedMutableMapping[snowflakes.Snowflake, cache_utility.KnownCustomEmojiData] + _guild_channel_entries: collections.ExtendedMutableMapping[snowflakes.Snowflake, channels.GuildChannel] + _guild_entries: collections.ExtendedMutableMapping[snowflakes.Snowflake, cache_utility.GuildRecord] + _invite_entries: collections.ExtendedMutableMapping[str, cache_utility.InviteData] + _role_entries: collections.ExtendedMutableMapping[snowflakes.Snowflake, guilds.Role] + _unknown_custom_emoji_entries: collections.ExtendedMutableMapping[ snowflakes.Snowflake, cache_utility.GenericRefWrapper[emojis.CustomEmoji], ] - _user_entries: mapping.MappedCollection[snowflakes.Snowflake, cache_utility.GenericRefWrapper[users.User]] + _user_entries: collections.ExtendedMutableMapping[snowflakes.Snowflake, cache_utility.GenericRefWrapper[users.User]] _intents: intents_.Intents def __init__(self, app: traits.RESTAware, intents: intents_.Intents) -> None: @@ -122,16 +123,16 @@ def __init__(self, app: traits.RESTAware, intents: intents_.Intents) -> None: # Cached Private Channels channels are a special case as there's no sane way to remove them from cache as we'd # have to go through all the guilds the app is in to see if it shares any of them with the channel's owner # before removing it from the cache so we use a specific MRU implementation to cover private channel de-caching. - self._dm_entries = cache_utility.DMChannelMRUMutableMapping(expiry=datetime.timedelta(minutes=5)) - self._emoji_entries = mapping.DictionaryCollection() - self._guild_channel_entries = mapping.DictionaryCollection() - self._guild_entries = mapping.DictionaryCollection() - self._invite_entries = mapping.DictionaryCollection() - self._role_entries = mapping.DictionaryCollection() + self._dm_entries = cache_utility.DMChannelCacheMapping(expiry=datetime.timedelta(minutes=5)) + self._emoji_entries = collections.FreezableDict() + self._guild_channel_entries = collections.FreezableDict() + self._guild_entries = collections.FreezableDict() + self._invite_entries = collections.FreezableDict() + self._role_entries = collections.FreezableDict() # This is a purely internal cache used for handling the caching and de-duplicating of the unknown custom emojis # found attached to cached presence activities. - self._unknown_custom_emoji_entries = mapping.DictionaryCollection() - self._user_entries = mapping.DictionaryCollection() + self._unknown_custom_emoji_entries = collections.FreezableDict() + self._user_entries = collections.FreezableDict() self._intents = intents def _assert_has_intent(self, intents: intents_.Intents, /) -> None: @@ -160,7 +161,7 @@ def clear_dms(self) -> cache.CacheView[snowflakes.Snowflake, channels.DMChannel] return cache_utility.EmptyCacheView() cached_channels = self._dm_entries - self._dm_entries = mapping.DictionaryCollection() + self._dm_entries = collections.FreezableDict() cached_users = {} for user_id in cached_channels: @@ -368,7 +369,7 @@ def set_emoji(self, emoji: emojis.KnownCustomEmoji, /) -> None: guild_container = self._get_or_create_guild_record(emoji.guild_id) if guild_container.emojis is None: # TODO: add test cases when it is not None? - guild_container.emojis = cache_utility.IDTable() + guild_container.emojis = hikari.utilities.collections.SnowflakeSet() guild_container.emojis.add(emoji.id) @@ -486,7 +487,7 @@ def update_guild( def clear_guild_channels(self) -> cache.CacheView[snowflakes.Snowflake, channels.GuildChannel]: cached_channels = self._guild_channel_entries - self._guild_channel_entries = mapping.DictionaryCollection() + self._guild_channel_entries = collections.FreezableDict() for guild_id, guild_record in self._guild_entries.freeze().items(): if guild_record.channels is not None: @@ -563,7 +564,7 @@ def set_guild_channel(self, channel: channels.GuildChannel, /) -> None: guild_record = self._get_or_create_guild_record(channel.guild_id) if guild_record.channels is None: - guild_record.channels = cache_utility.IDTable() + guild_record.channels = hikari.utilities.collections.SnowflakeSet() guild_record.channels.add(channel.id) @@ -986,7 +987,7 @@ def set_member(self, member: guilds.Member, /) -> None: member_data = cache_utility.MemberData.build_from_entity(member) if guild_record.members is None: # TODO: test when this is not None - guild_record.members = mapping.DictionaryCollection() + guild_record.members = collections.FreezableDict() if member.user.id not in guild_record.members: self._increment_user_ref_count(member.user.id) @@ -1268,7 +1269,7 @@ def set_presence(self, presence: presences.MemberPresence, /) -> None: guild_record = self._get_or_create_guild_record(presence.guild_id) if guild_record.presences is None: - guild_record.presences = mapping.DictionaryCollection() + guild_record.presences = collections.FreezableDict() guild_record.presences[presence.user_id] = presence_data @@ -1284,7 +1285,7 @@ def clear_roles(self) -> cache.CacheView[snowflakes.Snowflake, guilds.Role]: return cache_utility.EmptyCacheView() roles = self._role_entries - self._role_entries = mapping.DictionaryCollection() + self._role_entries = collections.FreezableDict() for guild_id, guild_record in self._guild_entries.freeze().items(): if guild_record.roles is not None: # TODO: test coverage for this @@ -1347,7 +1348,7 @@ def set_role(self, role: guilds.Role, /) -> None: guild_record = self._get_or_create_guild_record(role.guild_id) if guild_record.roles is None: # TODO: test when this is not None - guild_record.roles = cache_utility.IDTable() + guild_record.roles = hikari.utilities.collections.SnowflakeSet() guild_record.roles.add(role.id) @@ -1655,7 +1656,7 @@ def set_voice_state(self, voice_state: voices.VoiceState, /) -> None: guild_record = self._get_or_create_guild_record(voice_state.guild_id) if guild_record.voice_states is None: # TODO: test when this is not None - guild_record.voice_states = mapping.DictionaryCollection() + guild_record.voice_states = collections.FreezableDict() # TODO: account for this method not setting the member in some cases later on self.set_member(voice_state.member) diff --git a/hikari/impl/stateful_guild_chunker.py b/hikari/impl/stateful_guild_chunker.py index 55cee53cae..944d3d62a9 100644 --- a/hikari/impl/stateful_guild_chunker.py +++ b/hikari/impl/stateful_guild_chunker.py @@ -40,9 +40,9 @@ from hikari.api import chunker from hikari.events import shard_events from hikari.utilities import attr_extensions +from hikari.utilities import collections from hikari.utilities import date from hikari.utilities import event_stream -from hikari.utilities import mapping if typing.TYPE_CHECKING: import datetime @@ -217,7 +217,7 @@ class StatefulGuildChunkerImpl(chunker.GuildChunker): def __init__(self, app: traits.BotAware, limit: int = 200) -> None: self._app = app self._limit = limit - self._tracked: typing.MutableMapping[int, mapping.CMRIMutableMapping[str, _TrackedRequests]] = {} + self._tracked: typing.MutableMapping[int, collections.LimitedCapacityCacheMap[str, _TrackedRequests]] = {} def _default_include_presences( self, guild_id: snowflakes.Snowflake, include_presences: undefined.UndefinedOr[bool] @@ -303,7 +303,7 @@ async def request_guild_members( shard_id = snowflakes.calculate_shard_id(self._app, guild_id) nonce = f"{shard_id}.{_random_nonce()}" if shard_id not in self._tracked: - self._tracked[shard_id] = mapping.CMRIMutableMapping(limit=self._limit) + self._tracked[shard_id] = collections.LimitedCapacityCacheMap(limit=self._limit) tracker = _TrackedRequests(guild_id=guild_id, nonce=nonce) self._tracked[shard_id][nonce] = tracker diff --git a/hikari/intents.py b/hikari/intents.py index e31f850da0..2f3ad6d164 100644 --- a/hikari/intents.py +++ b/hikari/intents.py @@ -265,7 +265,7 @@ class Intents(flag.Flag): * `TYPING_START` (in guilds only) """ - PRIVATE_MESSAGES = 1 << 12 + DM_MESSAGES = 1 << 12 """Subscribes to the following events: * `CHANNEL_CREATE` (in private message channels (non-guild bound) only) @@ -274,7 +274,7 @@ class Intents(flag.Flag): * `MESSAGE_DELETE` (in private message channels (non-guild bound) only) """ - PRIVATE_MESSAGE_REACTIONS = 1 << 13 + DM_MESSAGE_REACTIONS = 1 << 13 """Subscribes to the following events: * `MESSAGE_REACTION_ADD` (in private message channels (non-guild bound) only) @@ -283,7 +283,7 @@ class Intents(flag.Flag): * `MESSAGE_REACTION_REMOVE_EMOJI` (in private message channels (non-guild bound) only) """ - PRIVATE_MESSAGE_TYPING = 1 << 14 + DM_MESSAGE_TYPING = 1 << 14 """Subscribes to the following events * `TYPING_START` (in private message channels (non-guild bound) only) @@ -327,19 +327,19 @@ class Intents(flag.Flag): use. """ - ALL_PRIVATE = PRIVATE_MESSAGES | PRIVATE_MESSAGE_TYPING | PRIVATE_MESSAGE_REACTIONS + ALL_DMS = DM_MESSAGES | DM_MESSAGE_TYPING | DM_MESSAGE_REACTIONS """All private message channel (non-guild bound) intents.""" - ALL_MESSAGES = PRIVATE_MESSAGES | GUILD_MESSAGES + ALL_MESSAGES = DM_MESSAGES | GUILD_MESSAGES """All message intents.""" - ALL_MESSAGE_REACTIONS = PRIVATE_MESSAGE_REACTIONS | GUILD_MESSAGE_REACTIONS + ALL_MESSAGE_REACTIONS = DM_MESSAGE_REACTIONS | GUILD_MESSAGE_REACTIONS """All message reaction intents.""" - ALL_MESSAGE_TYPING = PRIVATE_MESSAGE_TYPING | GUILD_MESSAGE_TYPING + ALL_MESSAGE_TYPING = DM_MESSAGE_TYPING | GUILD_MESSAGE_TYPING """All typing indicator intents.""" - ALL_UNPRIVILEGED = ALL_GUILDS_UNPRIVILEGED | ALL_PRIVATE + ALL_UNPRIVILEGED = ALL_GUILDS_UNPRIVILEGED | ALL_DMS """All unprivileged intents.""" ALL_PRIVILEGED = ALL_GUILDS_PRIVILEGED diff --git a/hikari/messages.py b/hikari/messages.py index e7bfe497e4..cc3fee6138 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -278,14 +278,24 @@ class PartialMessage(snowflakes.Unique): channel_id: snowflakes.Snowflake = attr.ib(repr=True) """The ID of the channel that the message was sent in.""" - guild_id: undefined.UndefinedNoneOr[snowflakes.Snowflake] = attr.ib(repr=True) + guild_id: typing.Optional[snowflakes.Snowflake] = attr.ib(repr=True) """The ID of the guild that the message was sent in.""" - author: undefined.UndefinedOr[users.User] = attr.ib(repr=True) - """The author of this message.""" + author: typing.Optional[users.User] = attr.ib(repr=True) + """The author of this message. - member: undefined.UndefinedNoneOr[guilds.Member] = attr.ib(repr=False) - """The member properties for the message's author.""" + This will be `builtins.None` in some cases such as when Discord + updates a message with an embed URL preview. + """ + + member: typing.Optional[guilds.Member] = attr.ib(repr=False) + """The member for the author who created the message. + + If the message is not in a guild, this will be `builtins.None`. + + This will also be `builtins.None` in some cases such as when Discord updates + a message with an embed URL preview. + """ content: undefined.UndefinedNoneOr[str] = attr.ib(repr=False) """The content of the message.""" @@ -306,6 +316,7 @@ class PartialMessage(snowflakes.Unique): is_mentioning_everyone: undefined.UndefinedOr[bool] = attr.ib(repr=False) """Whether the message mentions `@everyone` or `@here`.""" + # TODO: make a mentions object. These type hints are cancer in the documentation. user_mentions: undefined.UndefinedOr[typing.Sequence[snowflakes.Snowflake]] = attr.ib(repr=False) """The users the message mentions.""" diff --git a/hikari/utilities/cache.py b/hikari/utilities/cache.py index 9fd56691a4..deffbf6493 100644 --- a/hikari/utilities/cache.py +++ b/hikari/utilities/cache.py @@ -23,7 +23,6 @@ from __future__ import annotations __all__: typing.List[str] = [ - "IDTable", "StatefulCacheMappingView", "EmptyCacheView", "GuildRecord", @@ -36,7 +35,7 @@ "MemberPresenceData", "VoiceStateData", "GenericRefWrapper", - "DMChannelMRUMutableMapping", + "DMChannelCacheMapping", "copy_guild_channel", "GuildChannelCacheMappingView", "Cache3DMappingView", @@ -46,11 +45,8 @@ ] import abc -import array -import bisect import copy import datetime -import reprlib import typing import attr @@ -66,8 +62,8 @@ from hikari import voices from hikari.api import cache from hikari.utilities import attr_extensions +from hikari.utilities import collections from hikari.utilities import date -from hikari.utilities import mapping DataT = typing.TypeVar("DataT", bound="BaseData[typing.Any]") """Type-hint for "data" objects used for storing and building entities.""" @@ -77,54 +73,6 @@ """Type-hint for mapping values.""" -class IDTable(typing.MutableSet[snowflakes.Snowflake]): - """Compact 64-bit integer bisected-array-set of snowflakes.""" - - __slots__: typing.Sequence[str] = ("_ids",) - - def __init__(self) -> None: - self._ids = array.array("Q") - - def add(self, sf: snowflakes.Snowflake) -> None: - """Add a snowflake to this set.""" - if not self._ids: - self._ids.append(sf) - else: - index = bisect.bisect_left(self._ids, sf) - if len(self._ids) == index or self._ids[index] != sf: - self._ids.insert(index, sf) - - def add_all(self, sfs: typing.Iterable[snowflakes.Snowflake]) -> None: - """Add a collection of snowflakes to this set.""" - for sf in sfs: - self.add(sf) - - def discard(self, sf: snowflakes.Snowflake) -> None: - """Remove a snowflake from this set if it's present.""" - index = self._index_of(sf) - if index != -1: - del self._ids[index] - - def _index_of(self, sf: int) -> int: - index = bisect.bisect_left(self._ids, sf) - return index if index < len(self._ids) or self._ids[index] == sf else -1 - - def __contains__(self, value: typing.Any) -> bool: - if not isinstance(value, int): - return False - - return self._index_of(value) != -1 - - def __len__(self) -> int: - return len(self._ids) - - def __iter__(self) -> typing.Iterator[snowflakes.Snowflake]: - return map(snowflakes.Snowflake, self._ids) - - def __repr__(self) -> str: - return "SnowflakeTable" + reprlib.repr(self._ids)[5:] - - class StatefulCacheMappingView(cache.CacheView[KeyT, ValueT], typing.Generic[KeyT, ValueT]): """A cache mapping view implementation used for representing cached data. @@ -279,14 +227,16 @@ class GuildRecord: `typing.MutableSequence[str]` of invite codes. """ - members: typing.Optional[mapping.MappedCollection[snowflakes.Snowflake, MemberData]] = attr.ib(default=None) + members: typing.Optional[collections.ExtendedMutableMapping[snowflakes.Snowflake, MemberData]] = attr.ib( + default=None + ) """A mapping of user IDs to the objects of members cached for this guild. This will be `builtins.None` if no members are cached for this guild else `hikari.utilities.mapping.MappedCollection[hikari.snowflakes.Snowflake, MemberData]`. """ - presences: typing.Optional[mapping.MappedCollection[snowflakes.Snowflake, MemberPresenceData]] = attr.ib( + presences: typing.Optional[collections.ExtendedMutableMapping[snowflakes.Snowflake, MemberPresenceData]] = attr.ib( default=None ) """A mapping of user IDs to objects of the presences cached for this guild. @@ -302,7 +252,7 @@ class GuildRecord: `typing.MutableSet[hikari.snowflakes.Snowflake]` of role IDs. """ - voice_states: typing.Optional[mapping.MappedCollection[snowflakes.Snowflake, VoiceStateData]] = attr.ib( + voice_states: typing.Optional[collections.ExtendedMutableMapping[snowflakes.Snowflake, VoiceStateData]] = attr.ib( default=None ) """A mapping of user IDs to objects of the voice states cached for this guild. @@ -737,7 +687,7 @@ class GenericRefWrapper(typing.Generic[ValueT]): ref_count: int = attr.ib(default=0) -class DMChannelMRUMutableMapping(mapping.MappedCollection[snowflakes.Snowflake, DMChannelData]): +class DMChannelCacheMapping(collections.ExtendedMutableMapping[snowflakes.Snowflake, DMChannelData]): """A specialised Most-recently-used limited mapping for DMs. This allows us to stop the private message cached from growing @@ -772,8 +722,8 @@ def __init__( self._channels = source or {} self._expiry = expiry - def copy(self) -> DMChannelMRUMutableMapping: - return DMChannelMRUMutableMapping(self._channels.copy(), expiry=self._expiry) + def copy(self) -> DMChannelCacheMapping: + return DMChannelCacheMapping(self._channels.copy(), expiry=self._expiry) def freeze(self) -> typing.Dict[snowflakes.Snowflake, DMChannelData]: return self._channels.copy() @@ -817,7 +767,7 @@ def copy_guild_channel(channel: channels.GuildChannel) -> channels.GuildChannel: """ channel = copy.copy(channel) channel.permission_overwrites = { - sf: copy.copy(overwrite) for sf, overwrite in mapping.copy_mapping(channel.permission_overwrites).items() + sf: copy.copy(overwrite) for sf, overwrite in collections.copy_mapping(channel.permission_overwrites).items() } return channel diff --git a/hikari/utilities/mapping.py b/hikari/utilities/collections.py similarity index 62% rename from hikari/utilities/mapping.py rename to hikari/utilities/collections.py index a1ba7fb88c..e7f9fc5d1b 100644 --- a/hikari/utilities/mapping.py +++ b/hikari/utilities/collections.py @@ -19,28 +19,33 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -"""Mapping utility and implementations used in Hriki.""" +"""Custom data structures used within Hikari's core implementation.""" from __future__ import annotations __all__: typing.List[str] = [ "MapT", "KeyT", "ValueT", - "MappedCollection", - "DictionaryCollection", - "MRIMutableMapping", - "CMRIMutableMapping", + "ExtendedMutableMapping", + "FreezableDict", + "TimedCacheMap", + "LimitedCapacityCacheMap", "get_index_or_slice", "copy_mapping", ] import abc +import array +import bisect import datetime import itertools +import sys import time import typing -MapT = typing.TypeVar("MapT", bound="MappedCollection[typing.Any, typing.Any]") +from hikari import snowflakes + +MapT = typing.TypeVar("MapT", bound="ExtendedMutableMapping[typing.Any, typing.Any]") """Type-hint A type hint used for mapped collection objects.""" KeyT = typing.TypeVar("KeyT", bound=typing.Hashable) """Type-hint A type hint used for the type of a mapping's key.""" @@ -48,10 +53,12 @@ """Type-hint A type hint used for the type of a mapping's value.""" -class MappedCollection(typing.MutableMapping[KeyT, ValueT], abc.ABC): - """The abstract class of mutable mappings used within hikari. +class ExtendedMutableMapping(typing.MutableMapping[KeyT, ValueT], abc.ABC): + """The abstract class of mutable mappings used within Hikari. - This extends `typing.MutableMapping` with "copy" and "freeze" methods. + These are mutable mappings that have a couple of extra methods to allow + for further optimised copy operations, as well as the ability to freeze + the implementation of a mapping to make it read-only. """ __slots__: typing.Sequence[str] = () @@ -83,8 +90,8 @@ def freeze(self) -> typing.MutableMapping[KeyT, ValueT]: This may look like calling `dict.copy`. !!! note - Unlike `MappedCollection.copy`, this should return a pure mapping - with no removal policy at all. + Unlike `ExtendedMutableMapping.copy`, this should return a pure + mapping with no removal policy at all. Returns ------- @@ -93,17 +100,18 @@ def freeze(self) -> typing.MutableMapping[KeyT, ValueT]: """ -class DictionaryCollection(MappedCollection[KeyT, ValueT]): - """A basic mapped collection that acts like a dictionary.""" +class FreezableDict(ExtendedMutableMapping[KeyT, ValueT]): + """A mapping that wraps a dict, but can also be frozen.""" __slots__ = ("_data",) def __init__(self, source: typing.Optional[typing.Dict[KeyT, ValueT]] = None, /) -> None: self._data = source or {} - def copy(self) -> DictionaryCollection[KeyT, ValueT]: - return DictionaryCollection(self._data.copy()) + def copy(self) -> FreezableDict[KeyT, ValueT]: + return FreezableDict(self._data.copy()) + # TODO: name this something different if it is not physically frozen. def freeze(self) -> typing.Dict[KeyT, ValueT]: return self._data.copy() @@ -123,7 +131,7 @@ def __setitem__(self, key: KeyT, value: ValueT) -> None: self._data[key] = value -class _FrozenMRIMapping(typing.MutableMapping[KeyT, ValueT]): +class _FrozenDict(typing.MutableMapping[KeyT, ValueT]): __slots__ = ("_source",) def __init__(self, source: typing.Dict[KeyT, typing.Tuple[float, ValueT]], /) -> None: @@ -145,7 +153,7 @@ def __setitem__(self, key: KeyT, value: ValueT) -> None: self._source[key] = (0.0, value) -class MRIMutableMapping(MappedCollection[KeyT, ValueT]): +class TimedCacheMap(ExtendedMutableMapping[KeyT, ValueT]): """A most-recently-inserted limited mutable mapping implementation. This will remove entries on modification as as they pass the expiry limit. @@ -175,11 +183,11 @@ def __init__( self._data = source or {} self._garbage_collect() - def copy(self) -> MRIMutableMapping[KeyT, ValueT]: - return MRIMutableMapping(self._data.copy(), expiry=datetime.timedelta(seconds=self._expiry)) + def copy(self) -> TimedCacheMap[KeyT, ValueT]: + return TimedCacheMap(self._data.copy(), expiry=datetime.timedelta(seconds=self._expiry)) def freeze(self) -> typing.MutableMapping[KeyT, ValueT]: - return _FrozenMRIMapping(self._data.copy()) + return _FrozenDict(self._data.copy()) def _garbage_collect(self) -> None: current_time = time.perf_counter() @@ -212,7 +220,7 @@ def __setitem__(self, key: KeyT, value: ValueT) -> None: self._garbage_collect() -class CMRIMutableMapping(MappedCollection[KeyT, ValueT]): +class LimitedCapacityCacheMap(ExtendedMutableMapping[KeyT, ValueT]): """Implementation of a capacity-limited most-recently-inserted mapping. This will start removing the oldest entries after it's maximum capacity is @@ -234,8 +242,8 @@ def __init__(self, source: typing.Optional[typing.Dict[KeyT, ValueT]] = None, /, self._limit = limit self._garbage_collect() - def copy(self) -> CMRIMutableMapping[KeyT, ValueT]: - return CMRIMutableMapping(self._data.copy(), limit=self._limit) + def copy(self) -> LimitedCapacityCacheMap[KeyT, ValueT]: + return LimitedCapacityCacheMap(self._data.copy(), limit=self._limit) def freeze(self) -> typing.Dict[KeyT, ValueT]: return self._data.copy() @@ -261,6 +269,117 @@ def __setitem__(self, key: KeyT, value: ValueT) -> None: self._garbage_collect() +# TODO: can this be immutable? +class SnowflakeSet(typing.MutableSet[snowflakes.Snowflake]): + r"""Set of `hikari.snowflakes.Snowflake` objects. + + This internally uses a sorted bisect-array of 64 bit integers to represent + the information. This reduces the space needed to store these objects + significantly down to the size of 8 bytes per item. + + In contrast, a regular list would take generally 8 bytes per item just to + store the memory address, and then a further 28 bytes or more to physically + store the integral value if it does not get interned by the Python + implementation you are using. A regular set would consume even more + space, being a hashtable internally. + + The detail of this implementation has the side effect that searches + will take $$ \mathcal{O} \left( \log n \right) $$ operations in the worst + case, and $$ \Omega \left (k \right) $$ in the best case. Average case + will be $$ \mathcal{O} \left( \log n \right) $$ + + Insertions and removals will take $$ \mathcal{O} \left( \log n \right) $$ + operations in the worst case, due to `bisect` using a binary insertion sort + algorithm internally. Average case will be + $$ \mathcal{O} \left( \log n \right) $$, and best case will be + $$ \Omega \left\( k \right) $$. + + !!! warning + This is not thread-safe and must not be iterated across whilst being + concurrently modified. + + Other Parameters + ---------------- + *ids : builtins.int + The IDs to fill this table with. + """ + + __slots__: typing.Sequence[str] = ("_ids",) + + _LONG_LONG_UNSIGNED: typing.Final[typing.ClassVar[str]] = "Q" + + def __init__(self, *ids: int) -> None: + self._ids = array.array(self._LONG_LONG_UNSIGNED) + + if ids: + self.add_all(ids) + + def add(self, sf: int) -> None: + """Add a snowflake to this set.""" + # Always append the first item, as we cannot compare with nothing. + index = bisect.bisect_left(self._ids, sf) + if index == len(self._ids): + self._ids.append(sf) + elif self._ids[index] != sf: + self._ids.insert(index, sf) + + def add_all(self, sfs: typing.Iterable[int]) -> None: + """Add a collection of snowflakes to this set.""" + if not sfs: + return + + for sf in sfs: + # Yes, this is repeated code, but we want insertions to be as fast + # as possible for caching purposes, so reduce the number of function + # calls as much as possible and reimplement the logic for `add` + # here. + index = bisect.bisect_left(self._ids, sf) + if index == len(self._ids): + self._ids.append(sf) + elif self._ids[index] != sf: + self._ids.insert(index, sf) + + def clear(self) -> None: + """Clear all items from this collection.""" + # Arrays lack a "clear" method. + self._ids = array.array(self._LONG_LONG_UNSIGNED) + + def discard(self, sf: int) -> None: + """Remove a snowflake from this set if it's present.""" + if not self._ids: + return + + index = bisect.bisect_left(self._ids, sf) + + if index < len(self) and self._ids[index] == sf: + del self._ids[index] + + def __contains__(self, value: typing.Any) -> bool: + if not isinstance(value, int): + return False + + index = bisect.bisect_left(self._ids, value) + + if index < len(self._ids): + return self._ids[index] == value + return False + + def __iter__(self) -> typing.Iterator[snowflakes.Snowflake]: + return map(snowflakes.Snowflake, self._ids) + + def __len__(self) -> int: + return len(self._ids) + + def __repr__(self) -> str: + return type(self).__name__ + "(" + ", ".join(map(repr, self._ids)) + ")" + + def __sizeof__(self) -> int: + return super().__sizeof__() + sys.getsizeof(self._ids) + + def __str__(self) -> str: + return "{" + ", ".join(map(repr, self._ids)) + "}" + + def get_index_or_slice( mapping: typing.Mapping[KeyT, ValueT], index_or_slice: typing.Union[int, slice] ) -> typing.Union[ValueT, typing.Sequence[ValueT]]: @@ -310,4 +429,4 @@ def copy_mapping(mapping: typing.Mapping[KeyT, ValueT]) -> typing.MutableMapping try: return mapping.copy() # type: ignore[attr-defined, no-any-return] except (AttributeError, TypeError): - raise NotImplementedError("provided mapping doesn't implement a copy method") from None + return dict(mapping) diff --git a/tests/hikari/events/test_message_events.py b/tests/hikari/events/test_message_events.py index e8a50a58e4..e91fb51b65 100644 --- a/tests/hikari/events/test_message_events.py +++ b/tests/hikari/events/test_message_events.py @@ -23,45 +23,15 @@ import pytest from hikari import channels +from hikari import guilds from hikari import messages from hikari import snowflakes +from hikari import undefined from hikari import users from hikari.events import message_events from tests.hikari import hikari_test_helpers -class TestGuildMessageEvent: - @pytest.fixture() - def event(self): - cls = hikari_test_helpers.mock_class_namespace( - message_events.GuildMessageEvent, - guild_id=mock.PropertyMock(return_value=snowflakes.Snowflake(342123123)), - channel_id=mock.PropertyMock(return_value=snowflakes.Snowflake(54123123123)), - ) - return cls() - - @pytest.mark.parametrize("guild_channel_impl", [channels.GuildTextChannel, channels.GuildNewsChannel]) - def test_channel(self, event, guild_channel_impl): - event.app.cache.get_guild_channel = mock.Mock(return_value=mock.Mock(spec_set=guild_channel_impl)) - - result = event.channel - assert result is event.app.cache.get_guild_channel.return_value - event.app.cache.get_guild_channel.assert_called_once_with(54123123123) - - def test_guild_when_available(self, event): - result = event.guild - - assert result is event.app.cache.get_guild.return_value - event.app.cache.get_guild.assert_called_once_with(342123123) - - def test_guild_when_unavailable(self, event): - event.app.cache.get_available_guild.return_value = None - result = event.guild - - assert result is event.app.cache.get_guild.return_value - event.app.cache.get_guild.assert_called_once_with(342123123) - - class TestMessageCreateEvent: @pytest.fixture() def event(self): @@ -74,24 +44,52 @@ def event(self): spec_set=users.User, ), ), - shard=object(), - channel=object(), - author=object(), + shard=mock.Mock(), ) return cls() - def test_message_id_property(self, event): - event.message.id = 123 - assert event.message_id == 123 + def test_author_property(self, event): + assert event.author is event.message.author + + def test_author_id_property(self, event): + assert event.author_id is event.author.id def test_channel_id_property(self, event): - event.message.channel_id = 123 - assert event.channel_id == 123 + assert event.channel_id is event.message.channel_id + + def test_content_property(self, event): + assert event.content is event.message.content + + def test_embeds_property(self, event): + assert event.embeds is event.message.embeds + + @pytest.mark.parametrize("is_bot", [True, False]) + def test_is_bot_property(self, event, is_bot): + event.message.author.is_bot = is_bot + assert event.is_bot is is_bot + + @pytest.mark.parametrize( + ("author_is_bot", "webhook_id", "expected_is_human"), + [ + (True, 123, False), + (True, None, False), + (False, 123, False), + (False, None, True), + ], + ) + def test_is_human_property(self, event, author_is_bot, webhook_id, expected_is_human): + event.message.author.is_bot = author_is_bot + event.message.webhook_id = webhook_id + assert event.is_human is expected_is_human + + @pytest.mark.parametrize(("webhook_id", "is_webhook"), [(123, True), (None, False)]) + def test_is_webhook_property(self, event, webhook_id, is_webhook): + event.message.webhook_id = webhook_id + assert event.is_webhook is is_webhook - def test_author_id_property(self, event): - event.message.author.id = 123 - assert event.author_id == 123 + def test_message_id_property(self, event): + assert event.message_id is event.message.id class TestMessageUpdateEvent: @@ -106,115 +104,211 @@ def event(self): spec_set=users.User, ), ), - shard=object(), - channel=object(), - author=object(), + shard=mock.Mock(), ) return cls() - def test_message_id_property(self, event): - event.message.id = snowflakes.Snowflake(123) - assert event.message_id == snowflakes.Snowflake(123) - - def test_channel_id_property(self, event): - event.message.channel_id = snowflakes.Snowflake(456) - assert event.channel_id == snowflakes.Snowflake(456) - - def test_author_id_property(self, event): - event.message.author.id = snowflakes.Snowflake(911) - assert event.author_id == snowflakes.Snowflake(911) + @pytest.mark.parametrize("author", [mock.Mock(spec_set=users.User), undefined.UNDEFINED]) + def test_author_property(self, event, author): + event.message.author = author + assert event.author is author + @pytest.mark.parametrize( + ("author", "expected_id"), + [(mock.Mock(spec_set=users.User, id=91827), 91827), (None, None)], + ) + def test_author_id_property(self, event, author, expected_id): + event.message.author = author + assert event.author_id == expected_id -class TestMessageDeleteEvent: - @pytest.fixture() - def event(self): - class MessageDeleteEvent(message_events.MessageDeleteEvent): - app = None - message = mock.Mock(messages.Message) - shard = object() - channel = object() - - return MessageDeleteEvent() + def test_channel_id_property(self, event): + assert event.channel_id is event.message.channel_id + + def test_content_property(self, event): + assert event.content is event.message.content + + def test_embeds_property(self, event): + assert event.embeds is event.message.embeds + + @pytest.mark.parametrize("is_bot", [True, False]) + def test_is_bot_property(self, event, is_bot): + event.message.author.is_bot = is_bot + assert event.is_bot is is_bot + + def test_is_bot_property_if_no_author(self, event): + event.message.author = None + assert event.is_bot is None + + @pytest.mark.parametrize( + ("author", "webhook_id", "expected_is_human"), + [ + (mock.Mock(spec_set=users.User, is_bot=True), 123, False), + (mock.Mock(spec_set=users.User, is_bot=True), None, False), + (mock.Mock(spec_set=users.User, is_bot=False), 123, False), + (mock.Mock(spec_set=users.User, is_bot=False), None, True), + (None, 123, False), + (None, None, None), + ], + ) + def test_is_human_property(self, event, author, webhook_id, expected_is_human): + event.message.author = author + event.message.webhook_id = webhook_id + assert event.is_human is expected_is_human + + @pytest.mark.parametrize(("webhook_id", "is_webhook"), [(123, True), (None, False)]) + def test_is_webhook_property(self, event, webhook_id, is_webhook): + event.message.webhook_id = webhook_id + assert event.is_webhook is is_webhook def test_message_id_property(self, event): - event.message.id = 123 - assert event.message_id == 123 - - def test_channel_id_property(self, event): - event.message.channel_id = 123 - assert event.channel_id == 123 + assert event.message_id is event.message.id class TestGuildMessageCreateEvent: @pytest.fixture() def event(self): return message_events.GuildMessageCreateEvent( - app=None, - message=mock.Mock(spec_set=messages.Message, guild_id=snowflakes.Snowflake(998866)), - shard=object(), + app=mock.Mock(), + message=mock.Mock( + spec_set=messages.Message, + guild_id=snowflakes.Snowflake(342123123), + channel_id=snowflakes.Snowflake(9121234), + ), + shard=mock.Mock(), ) def test_guild_id_property(self, event): - assert event.guild_id == snowflakes.Snowflake(998866) + assert event.guild_id == snowflakes.Snowflake(342123123) + + @pytest.mark.parametrize("guild_channel_impl", [channels.GuildTextChannel, channels.GuildNewsChannel]) + def test_channel_property(self, event, guild_channel_impl): + event.app.cache.get_guild_channel = mock.Mock(return_value=mock.Mock(spec_set=guild_channel_impl)) + + result = event.channel + assert result is event.app.cache.get_guild_channel.return_value + event.app.cache.get_guild_channel.assert_called_once_with(9121234) + + def test_guild_property(self, event): + result = event.guild + + assert result is event.app.cache.get_guild.return_value + event.app.cache.get_guild.assert_called_once_with(342123123) class TestGuildMessageUpdateEvent: @pytest.fixture() def event(self): return message_events.GuildMessageUpdateEvent( - app=None, - message=mock.Mock(spec_set=messages.Message, guild_id=snowflakes.Snowflake(9182736)), - shard=object(), + app=mock.Mock(), + message=mock.Mock( + spec_set=messages.Message, + guild_id=snowflakes.Snowflake(54123123123), + channel_id=snowflakes.Snowflake(800001066), + ), + shard=mock.Mock(), ) + def test_author_property_when_member_defined(self, event): + event.message.member = mock.Mock(spec_set=guilds.Member) + event.message.author = undefined.UNDEFINED + + assert event.author is event.message.member + + def test_author_property_when_member_none_but_cached(self, event): + event.message.member = None + event.message.author = mock.Mock(spec_set=users.User, id=1234321) + event.message.guild_id = snowflakes.Snowflake(696969) + real_member = mock.Mock(spec_set=guilds.Member) + event.app.cache.get_member = mock.Mock(return_value=real_member) + + assert event.author is real_member + + event.app.cache.get_member.assert_called_once_with(696969, 1234321) + + def test_author_property_when_member_none_but_author_also_none(self, event): + event.message.author = None + event.message.member = None + + assert event.author is None + + event.app.cache.get_member.assert_not_called() + + def test_author_property_when_member_none_and_uncached_but_author_defined(self, event): + event.message.member = None + event.app.cache.get_member = mock.Mock(return_value=None) + event.message.author = mock.Mock(spec_set=users.User) + + assert event.author is event.message.author + def test_guild_id_property(self, event): - assert event.guild_id == snowflakes.Snowflake(9182736) + assert event.guild_id == snowflakes.Snowflake(54123123123) + @pytest.mark.parametrize("guild_channel_impl", [channels.GuildTextChannel, channels.GuildNewsChannel]) + def test_channel_property(self, event, guild_channel_impl): + event.app.cache.get_guild_channel = mock.Mock(return_value=mock.Mock(spec_set=guild_channel_impl)) -class TestGuildMessageDeleteEvent: + result = event.channel + assert result is event.app.cache.get_guild_channel.return_value + event.app.cache.get_guild_channel.assert_called_once_with(800001066) + + def test_guild_property(self, event): + result = event.guild + + assert result is event.app.cache.get_guild.return_value + event.app.cache.get_guild.assert_called_once_with(54123123123) + + +class TestDMMessageUpdateEvent: @pytest.fixture() def event(self): - return message_events.GuildMessageDeleteEvent( - app=None, - message=mock.Mock(spec_set=messages.Message, guild_id=snowflakes.Snowflake(9182736)), - shard=object(), + return message_events.DMMessageUpdateEvent( + app=mock.Mock(), + message=mock.Mock( + spec_set=messages.Message, author=mock.Mock(spec_set=users.User, id=snowflakes.Snowflake(8000010662)) + ), + shard=mock.Mock(), ) - def test_guild_id_property(self, event): - assert event.guild_id == snowflakes.Snowflake(9182736) + def test_channel_property(self, event): + channel = event.channel + assert channel is event.app.cache.get_dm_channel.return_value + event.app.cache.get_dm_channel.assert_called_once_with(8000010662) + + +class TestMessageDeleteEvent: + def test_message_id_property(self): + event = hikari_test_helpers.mock_class_namespace(message_events.MessageDeleteEvent, message_ids=[9, 18, 27])() + assert event.message_id == 9 + + def test_message_id_property_if_empty_errors(self): + event = hikari_test_helpers.mock_class_namespace(message_events.MessageDeleteEvent, message_ids=[])() + with pytest.raises(RuntimeError): + _ = event.message_id -class TestGuildMessageBulkDeleteEvent: +class TestGuildMessageDeleteEvent: @pytest.fixture() def event(self): - return message_events.GuildMessageBulkDeleteEvent( + return message_events.GuildMessageDeleteEvent( guild_id=snowflakes.Snowflake(542342354564), channel_id=snowflakes.Snowflake(54213123123), app=mock.Mock(), - shard=None, - message_ids=None, + shard=mock.Mock(), + message_ids={9, 18, 27, 36}, + is_bulk=True, ) @pytest.mark.parametrize("guild_channel_impl", [channels.GuildTextChannel, channels.GuildNewsChannel]) - def test_channel(self, event, guild_channel_impl): + def test_channel_property(self, event, guild_channel_impl): event.app.cache.get_guild_channel = mock.Mock(return_value=mock.Mock(spec_set=guild_channel_impl)) result = event.channel assert result is event.app.cache.get_guild_channel.return_value event.app.cache.get_guild_channel.assert_called_once_with(54213123123) - def test_guild_when_available(self, event): + def test_guild_property(self, event): result = event.guild - assert result is event.app.cache.get_available_guild.return_value - event.app.cache.get_available_guild.assert_called_once_with(542342354564) - event.app.cache.get_unavailable_guild.assert_not_called() - - def test_guild_when_unavailable(self, event): - event.app.cache.get_available_guild.return_value = None - result = event.guild - - assert result is event.app.cache.get_unavailable_guild.return_value - event.app.cache.get_unavailable_guild.assert_called_once_with(542342354564) - event.app.cache.get_available_guild.assert_called_once_with(542342354564) + assert result is event.app.cache.get_guild.return_value + event.app.cache.get_guild.assert_called_once_with(542342354564) diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index c12e5d076a..90ad16fb64 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -2444,33 +2444,35 @@ def test_deserialize_partial_message_with_partial_fields(self, entity_factory_im partial_message = entity_factory_impl.deserialize_partial_message(message_payload) assert partial_message.edited_timestamp is None - def test_deserialize_partial_message_with_unset_fields(self, entity_factory_impl, mock_app): - partial_message = entity_factory_impl.deserialize_partial_message({"id": 123, "channel_id": 456}) + def test_deserialize_partial_message_with_unset_fields(self, entity_factory_impl, mock_app, user_payload): + partial_message = entity_factory_impl.deserialize_partial_message( + {"id": 123, "channel_id": 456, "author": user_payload} + ) assert partial_message.app is mock_app assert partial_message.id == 123 assert partial_message.channel_id == 456 - assert partial_message.guild_id == undefined.UNDEFINED - assert partial_message.author == undefined.UNDEFINED - assert partial_message.member == undefined.UNDEFINED - assert partial_message.content == undefined.UNDEFINED - assert partial_message.timestamp == undefined.UNDEFINED - assert partial_message.edited_timestamp == undefined.UNDEFINED - assert partial_message.is_tts == undefined.UNDEFINED - assert partial_message.is_mentioning_everyone == undefined.UNDEFINED - assert partial_message.user_mentions == undefined.UNDEFINED - assert partial_message.role_mentions == undefined.UNDEFINED - assert partial_message.channel_mentions == undefined.UNDEFINED - assert partial_message.attachments == undefined.UNDEFINED - assert partial_message.embeds == undefined.UNDEFINED - assert partial_message.reactions == undefined.UNDEFINED - assert partial_message.is_pinned == undefined.UNDEFINED - assert partial_message.webhook_id == undefined.UNDEFINED - assert partial_message.type == undefined.UNDEFINED - assert partial_message.activity == undefined.UNDEFINED - assert partial_message.application == undefined.UNDEFINED - assert partial_message.message_reference == undefined.UNDEFINED - assert partial_message.flags == undefined.UNDEFINED - assert partial_message.nonce == undefined.UNDEFINED + assert partial_message.guild_id is None + assert partial_message.author is not None + assert partial_message.member is None + assert partial_message.content is undefined.UNDEFINED + assert partial_message.timestamp is undefined.UNDEFINED + assert partial_message.edited_timestamp is undefined.UNDEFINED + assert partial_message.is_tts is undefined.UNDEFINED + assert partial_message.is_mentioning_everyone is undefined.UNDEFINED + assert partial_message.user_mentions is undefined.UNDEFINED + assert partial_message.role_mentions is undefined.UNDEFINED + assert partial_message.channel_mentions is undefined.UNDEFINED + assert partial_message.attachments is undefined.UNDEFINED + assert partial_message.embeds is undefined.UNDEFINED + assert partial_message.reactions is undefined.UNDEFINED + assert partial_message.is_pinned is undefined.UNDEFINED + assert partial_message.webhook_id is undefined.UNDEFINED + assert partial_message.type is undefined.UNDEFINED + assert partial_message.activity is undefined.UNDEFINED + assert partial_message.application is undefined.UNDEFINED + assert partial_message.message_reference is undefined.UNDEFINED + assert partial_message.flags is undefined.UNDEFINED + assert partial_message.nonce is undefined.UNDEFINED def test_deserialize_full_message( self, diff --git a/tests/hikari/impl/test_stateful_cache.py b/tests/hikari/impl/test_stateful_cache.py index 64bbde8abe..ea2ef32f90 100644 --- a/tests/hikari/impl/test_stateful_cache.py +++ b/tests/hikari/impl/test_stateful_cache.py @@ -23,6 +23,7 @@ import mock import pytest +import hikari.utilities.collections from hikari import channels from hikari import emojis from hikari import guilds @@ -33,7 +34,7 @@ from hikari import voices from hikari.impl import stateful_cache from hikari.utilities import cache -from hikari.utilities import mapping +from hikari.utilities import collections from tests.hikari import hikari_test_helpers @@ -54,7 +55,7 @@ def test__build_dm_with_cached_user(self, cache_impl): recipient_id=snowflakes.Snowflake(2342344), ) mock_user = mock.MagicMock(users.User) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( {snowflakes.Snowflake(2342344): cache.GenericRefWrapper(object=mock_user)} ) channel = cache_impl._build_dm(channel_data) @@ -74,7 +75,7 @@ def test__build_dm_with_passed_through_user(self, cache_impl): recipient_id=snowflakes.Snowflake(2342344), ) mock_user = mock.MagicMock(users.User) - cache_impl._user_entries = mapping.DictionaryCollection({}) + cache_impl._user_entries = collections.FreezableDict({}) channel_channel = cache_impl._build_dm( channel_data, cached_users={snowflakes.Snowflake(2342344): cache.GenericRefWrapper(object=mock_user)}, @@ -89,13 +90,13 @@ def test_clear_dms(self, cache_impl): mock_wrapped_user_2 = mock.Mock(cache.GenericRefWrapper[users.User]) mock_channel_1 = mock.Mock(channels.DMChannel) mock_channel_2 = mock.Mock(channels.DMChannel) - cache_impl._dm_entries = mapping.DictionaryCollection( + cache_impl._dm_entries = collections.FreezableDict( { snowflakes.Snowflake(978655): mock_channel_data_1, snowflakes.Snowflake(2342344): mock_channel_data_2, } ) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(2342344): mock_wrapped_user_1, snowflakes.Snowflake(653451234): mock.Mock(cache.GenericRefWrapper), @@ -143,7 +144,7 @@ def test_delete_dm_for_known_channel(self, cache_impl): mock_channel_data = mock.Mock(cache.DMChannelData, recipient_id=snowflakes.Snowflake(7345234)) mock_channel = mock.Mock(channels.DMChannel) mock_other_channel_data = mock.Mock(cache.DMChannelData) - cache_impl._dm_entries = mapping.DictionaryCollection( + cache_impl._dm_entries = collections.FreezableDict( { snowflakes.Snowflake(7345234): mock_channel_data, snowflakes.Snowflake(531234): mock_other_channel_data, @@ -162,7 +163,7 @@ def test_delete_dm_for_unknown_channel_channel(self, cache_impl): def test_get_dm_channel_for_known_channel(self, cache_impl): mock_channel_data = mock.Mock(cache.DMChannelData) mock_channel = mock.Mock(channels.DMChannel) - cache_impl._dm_entries = mapping.DictionaryCollection( + cache_impl._dm_entries = collections.FreezableDict( { snowflakes.Snowflake(65234123): mock_channel_data, snowflakes.Snowflake(5123): mock.Mock(cache.DMChannelData), @@ -184,7 +185,7 @@ def test_get_dm_channel_view(self, cache_impl): mock_channel_2 = mock.Mock(channels.DMChannel) mock_wrapped_user_1 = mock.Mock(cache.GenericRefWrapper[users.User]) mock_wrapped_user_2 = mock.Mock(cache.GenericRefWrapper[users.User]) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(54213): mock_wrapped_user_1, snowflakes.Snowflake(6764556): mock.Mock(cache.GenericRefWrapper), @@ -192,7 +193,7 @@ def test_get_dm_channel_view(self, cache_impl): } ) cache_impl._build_dm = mock.Mock(side_effect=[mock_channel_1, mock_channel_2]) - cache_impl._dm_entries = mapping.DictionaryCollection( + cache_impl._dm_entries = collections.FreezableDict( { snowflakes.Snowflake(54213): mock_channel_data_1, snowflakes.Snowflake(65656): mock_channel_data_2, @@ -255,7 +256,7 @@ def test_set_dm_doesnt_increment_user_ref_for_pre_cached_channel(self, cache_imp channel = mock.Mock(channels.DMChannel, recipient=mock_recipient) cache_impl.set_user = mock.Mock() cache_impl._increment_user_ref_count = mock.Mock() - cache_impl._dm_entries = mapping.DictionaryCollection( + cache_impl._dm_entries = collections.FreezableDict( {snowflakes.Snowflake(7652341234): mock.Mock(cache.DMChannelData)} ) cache_impl.set_dm(channel) @@ -288,7 +289,7 @@ def test__build_emoji(self, cache_impl): is_available=True, ) mock_user = mock.MagicMock(users.User) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( {snowflakes.Snowflake(56234232): cache.GenericRefWrapper(object=mock_user)} ) emoji = cache_impl._build_emoji(emoji_data) @@ -316,7 +317,7 @@ def test__build_emoji_with_passed_through_users(self, cache_impl): is_available=True, ) mock_user = mock.MagicMock(users.User) - cache_impl._user_entries = mapping.DictionaryCollection({}) + cache_impl._user_entries = collections.FreezableDict({}) emoji = cache_impl._build_emoji( emoji_data, cached_users={snowflakes.Snowflake(56234232): cache.GenericRefWrapper(object=mock_user)}, @@ -350,14 +351,14 @@ def test_clear_emojis(self, cache_impl): mock_emoji_3 = mock.Mock(emojis.Emoji) mock_wrapped_user_1 = mock.Mock(cache.GenericRefWrapper[users.User]) mock_wrapped_user_2 = mock.Mock(cache.GenericRefWrapper[users.User]) - cache_impl._emoji_entries = mapping.DictionaryCollection( + cache_impl._emoji_entries = collections.FreezableDict( { snowflakes.Snowflake(43123123): mock_emoji_data_1, snowflakes.Snowflake(87643523): mock_emoji_data_2, snowflakes.Snowflake(6873451): mock_emoji_data_3, } ) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(123123): mock_wrapped_user_1, snowflakes.Snowflake(123): mock_wrapped_user_2, @@ -405,7 +406,7 @@ def test_clear_emojis_for_guild(self, cache_impl): mock_emoji_data_2 = mock.Mock(cache.KnownCustomEmojiData, user_id=snowflakes.Snowflake(123), ref_count=0) mock_emoji_data_3 = mock.Mock(cache.KnownCustomEmojiData, user_id=None, ref_count=0) mock_other_emoji_data = mock.Mock(cache.KnownCustomEmojiData) - emoji_ids = cache.IDTable() + emoji_ids = hikari.utilities.collections.SnowflakeSet() emoji_ids.add_all( [snowflakes.Snowflake(43123123), snowflakes.Snowflake(87643523), snowflakes.Snowflake(6873451)] ) @@ -414,7 +415,7 @@ def test_clear_emojis_for_guild(self, cache_impl): mock_emoji_3 = mock.Mock(emojis.Emoji) mock_wrapped_user_1 = mock.Mock(cache.GenericRefWrapper[users.User]) mock_wrapped_user_2 = mock.Mock(cache.GenericRefWrapper[users.User]) - cache_impl._emoji_entries = mapping.DictionaryCollection( + cache_impl._emoji_entries = collections.FreezableDict( { snowflakes.Snowflake(6873451): mock_emoji_data_1, snowflakes.Snowflake(43123123): mock_emoji_data_2, @@ -422,14 +423,14 @@ def test_clear_emojis_for_guild(self, cache_impl): snowflakes.Snowflake(111): mock_other_emoji_data, } ) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(123123): mock_wrapped_user_1, snowflakes.Snowflake(123): mock_wrapped_user_2, snowflakes.Snowflake(99999): mock.Mock(cache.GenericRefWrapper), } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(432123123): cache.GuildRecord(emojis=emoji_ids), snowflakes.Snowflake(1): mock.Mock(cache.GuildRecord), @@ -446,7 +447,7 @@ def test_clear_emojis_for_guild(self, cache_impl): snowflakes.Snowflake(43123123): mock_emoji_2, snowflakes.Snowflake(87643523): mock_emoji_3, } - assert cache_impl._emoji_entries == mapping.DictionaryCollection( + assert cache_impl._emoji_entries == collections.FreezableDict( {snowflakes.Snowflake(111): mock_other_emoji_data} ) assert cache_impl._guild_entries[snowflakes.Snowflake(432123123)].emojis is None @@ -478,7 +479,7 @@ def test_clear_emojis_for_guild(self, cache_impl): def test_clear_emojis_for_guild_for_unknown_emoji_cache(self, cache_impl): cache_impl._emoji_entries = {snowflakes.Snowflake(3123): mock.Mock(cache.KnownCustomEmojiData)} - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(432123123): cache.GuildRecord(), snowflakes.Snowflake(1): mock.Mock(cache.GuildRecord), @@ -495,9 +496,7 @@ def test_clear_emojis_for_guild_for_unknown_emoji_cache(self, cache_impl): def test_clear_emojis_for_guild_for_unknown_record(self, cache_impl): cache_impl._emoji_entries = {snowflakes.Snowflake(123124): mock.Mock(cache.KnownCustomEmojiData)} - cache_impl._guild_entries = mapping.DictionaryCollection( - {snowflakes.Snowflake(1): mock.Mock(cache.GuildRecord)} - ) + cache_impl._guild_entries = collections.FreezableDict({snowflakes.Snowflake(1): mock.Mock(cache.GuildRecord)}) cache_impl._build_emoji = mock.Mock() cache_impl._remove_guild_record_if_empty = mock.Mock() cache_impl._garbage_collect_user = mock.Mock() @@ -516,15 +515,15 @@ def test_delete_emoji(self, cache_impl): ) mock_other_emoji_data = mock.Mock(cache.KnownCustomEmojiData) mock_emoji = mock.Mock(emojis.KnownCustomEmoji) - emoji_ids = cache.IDTable() + emoji_ids = hikari.utilities.collections.SnowflakeSet() emoji_ids.add_all([snowflakes.Snowflake(12354123), snowflakes.Snowflake(432123)]) - cache_impl._emoji_entries = mapping.DictionaryCollection( + cache_impl._emoji_entries = collections.FreezableDict( { snowflakes.Snowflake(12354123): mock_emoji_data, snowflakes.Snowflake(999): mock_other_emoji_data, } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( {snowflakes.Snowflake(123333): cache.GuildRecord(emojis=emoji_ids)} ) cache_impl._garbage_collect_user = mock.Mock() @@ -544,15 +543,15 @@ def test_delete_emoji_without_user(self, cache_impl): ) mock_other_emoji_data = mock.Mock(cache.KnownCustomEmojiData) mock_emoji = mock.Mock(emojis.KnownCustomEmoji) - emoji_ids = cache.IDTable() + emoji_ids = hikari.utilities.collections.SnowflakeSet() emoji_ids.add_all([snowflakes.Snowflake(12354123), snowflakes.Snowflake(432123)]) - cache_impl._emoji_entries = mapping.DictionaryCollection( + cache_impl._emoji_entries = collections.FreezableDict( { snowflakes.Snowflake(12354123): mock_emoji_data, snowflakes.Snowflake(999): mock_other_emoji_data, } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( {snowflakes.Snowflake(123333): cache.GuildRecord(emojis=emoji_ids)} ) cache_impl._garbage_collect_user = mock.Mock() @@ -574,7 +573,7 @@ def test_get_emoji(self, cache_impl): mock_emoji_data = mock.Mock(cache.KnownCustomEmojiData) mock_emoji = mock.Mock(emojis.KnownCustomEmoji) cache_impl._build_emoji = mock.Mock(return_value=mock_emoji) - cache_impl._emoji_entries = mapping.DictionaryCollection({snowflakes.Snowflake(3422123): mock_emoji_data}) + cache_impl._emoji_entries = collections.FreezableDict({snowflakes.Snowflake(3422123): mock_emoji_data}) assert cache_impl.get_emoji(snowflakes.Snowflake(3422123)) is mock_emoji cache_impl._build_emoji.assert_called_once_with(mock_emoji_data) @@ -589,13 +588,13 @@ def test_get_emojis_view(self, cache_impl): mock_emoji_1 = mock.Mock(emojis.KnownCustomEmoji) mock_emoji_2 = mock.Mock(emojis.KnownCustomEmoji) mock_wrapped_user = mock.Mock(cache.GenericRefWrapper[users.User]) - cache_impl._emoji_entries = mapping.DictionaryCollection( + cache_impl._emoji_entries = collections.FreezableDict( { snowflakes.Snowflake(123123123): mock_emoji_data_1, snowflakes.Snowflake(43156234): mock_emoji_data_2, } ) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(564123): mock.Mock(cache.GenericRefWrapper), snowflakes.Snowflake(43123): mock_wrapped_user, @@ -618,23 +617,23 @@ def test_get_emojis_view_for_guild(self, cache_impl): mock_emoji_data_2 = mock.Mock(cache.KnownCustomEmojiData, user_id=None) mock_emoji_1 = mock.Mock(emojis.KnownCustomEmoji) mock_emoji_2 = mock.Mock(emojis.KnownCustomEmoji) - emoji_ids = cache.IDTable() + emoji_ids = hikari.utilities.collections.SnowflakeSet() emoji_ids.add_all([snowflakes.Snowflake(65123), snowflakes.Snowflake(43156234)]) mock_wrapped_user = mock.Mock(cache.GenericRefWrapper[users.User]) - cache_impl._emoji_entries = mapping.DictionaryCollection( + cache_impl._emoji_entries = collections.FreezableDict( { snowflakes.Snowflake(65123): mock_emoji_data_1, snowflakes.Snowflake(942123): mock.Mock(cache.KnownCustomEmojiData), snowflakes.Snowflake(43156234): mock_emoji_data_2, } ) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(564123): mock.Mock(cache.GenericRefWrapper), snowflakes.Snowflake(32124123): mock_wrapped_user, } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(99999): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(9342123): cache.GuildRecord(emojis=emoji_ids), @@ -653,10 +652,10 @@ def test_get_emojis_view_for_guild(self, cache_impl): ) def test_get_emojis_view_for_guild_for_unknown_emoji_cache(self, cache_impl): - cache_impl._emoji_entries = mapping.DictionaryCollection( + cache_impl._emoji_entries = collections.FreezableDict( {snowflakes.Snowflake(9999): mock.Mock(cache.KnownCustomEmojiData)} ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(99999): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(9342123): cache.GuildRecord(), @@ -667,10 +666,10 @@ def test_get_emojis_view_for_guild_for_unknown_emoji_cache(self, cache_impl): cache_impl._build_emoji.assert_not_called() def test_get_emojis_view_for_guild_for_unknown_record(self, cache_impl): - cache_impl._emoji_entries = mapping.DictionaryCollection( + cache_impl._emoji_entries = collections.FreezableDict( {snowflakes.Snowflake(12354345): mock.Mock(cache.KnownCustomEmojiData)} ) - cache_impl._guild_entries = mapping.DictionaryCollection({snowflakes.Snowflake(9342123): cache.GuildRecord()}) + cache_impl._guild_entries = collections.FreezableDict({snowflakes.Snowflake(9342123): cache.GuildRecord()}) cache_impl._build_emoji = mock.Mock() assert cache_impl.get_emojis_view_for_guild(snowflakes.Snowflake(9342123)) == {} cache_impl._build_emoji.assert_not_called() @@ -724,7 +723,7 @@ def test_set_emoji_with_pre_cached_emoji(self, cache_impl): is_managed=True, is_available=False, ) - cache_impl._emoji_entries = mapping.DictionaryCollection( + cache_impl._emoji_entries = collections.FreezableDict( {snowflakes.Snowflake(5123123): mock.Mock(cache.KnownCustomEmojiData)} ) cache_impl.set_user = mock.Mock() @@ -747,7 +746,7 @@ def test_update_emoji(self, cache_impl): cache_impl.set_emoji.assert_called_once_with(mock_emoji) def test_clear_guilds_when_no_guilds_cached(self, cache_impl): - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(423123): cache.GuildRecord(), snowflakes.Snowflake(675345): cache.GuildRecord(), @@ -764,13 +763,13 @@ def test_clear_guilds(self, cache_impl): mock_guild_2 = mock.MagicMock(guilds.GatewayGuild) mock_member = mock.MagicMock(guilds.Member) mock_guild_3 = mock.MagicMock(guilds.GatewayGuild) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(423123): cache.GuildRecord(), snowflakes.Snowflake(675345): cache.GuildRecord(guild=mock_guild_1), snowflakes.Snowflake(32142): cache.GuildRecord( guild=mock_guild_2, - members=mapping.DictionaryCollection({snowflakes.Snowflake(3241123): mock_member}), + members=collections.FreezableDict({snowflakes.Snowflake(3241123): mock_member}), ), snowflakes.Snowflake(765345): cache.GuildRecord(guild=mock_guild_3), snowflakes.Snowflake(321132): cache.GuildRecord(), @@ -786,13 +785,13 @@ def test_clear_guilds(self, cache_impl): def test_delete_guild_for_known_guild(self, cache_impl): mock_guild = mock.Mock(guilds.GatewayGuild) mock_member = mock.Mock(guilds.Member) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(354123): cache.GuildRecord(), snowflakes.Snowflake(543123): cache.GuildRecord( guild=mock_guild, is_available=True, - members=mapping.DictionaryCollection({snowflakes.Snowflake(43123): mock_member}), + members=collections.FreezableDict({snowflakes.Snowflake(43123): mock_member}), ), } ) @@ -804,7 +803,7 @@ def test_delete_guild_for_known_guild(self, cache_impl): def test_delete_guild_for_removes_emptied_record(self, cache_impl): mock_guild = mock.Mock(guilds.GatewayGuild) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(354123): cache.GuildRecord(), snowflakes.Snowflake(543123): cache.GuildRecord(guild=mock_guild, is_available=True), @@ -814,7 +813,7 @@ def test_delete_guild_for_removes_emptied_record(self, cache_impl): assert cache_impl._guild_entries == {snowflakes.Snowflake(354123): cache.GuildRecord()} def test_delete_guild_for_unknown_guild(self, cache_impl): - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(354123): cache.GuildRecord(), snowflakes.Snowflake(543123): cache.GuildRecord(), @@ -827,13 +826,13 @@ def test_delete_guild_for_unknown_guild(self, cache_impl): } def test_delete_guild_for_unknown_record(self, cache_impl): - cache_impl._guild_entries = mapping.DictionaryCollection({snowflakes.Snowflake(354123): cache.GuildRecord()}) + cache_impl._guild_entries = collections.FreezableDict({snowflakes.Snowflake(354123): cache.GuildRecord()}) assert cache_impl.delete_guild(snowflakes.Snowflake(543123)) is None assert cache_impl._guild_entries == {snowflakes.Snowflake(354123): cache.GuildRecord()} def test_get_guild_first_tries_get_available_guilds(self, cache_impl): mock_guild = mock.MagicMock(guilds.GatewayGuild) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54234123): cache.GuildRecord(), snowflakes.Snowflake(543123): cache.GuildRecord(guild=mock_guild, is_available=True), @@ -845,7 +844,7 @@ def test_get_guild_first_tries_get_available_guilds(self, cache_impl): def test_get_guild_then_tries_get_unavailable_guilds(self, cache_impl): mock_guild = mock.MagicMock(guilds.GatewayGuild) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(543123): cache.GuildRecord(is_available=True), snowflakes.Snowflake(54234123): cache.GuildRecord(guild=mock_guild, is_available=False), @@ -857,7 +856,7 @@ def test_get_guild_then_tries_get_unavailable_guilds(self, cache_impl): def test_get_available_guild_for_known_guild_when_available(self, cache_impl): mock_guild = mock.MagicMock(guilds.GatewayGuild) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54234123): cache.GuildRecord(), snowflakes.Snowflake(543123): cache.GuildRecord(guild=mock_guild, is_available=True), @@ -869,7 +868,7 @@ def test_get_available_guild_for_known_guild_when_available(self, cache_impl): def test_get_available_guild_for_known_guild_when_unavailable(self, cache_impl): mock_guild = mock.Mock(guilds.GatewayGuild) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54234123): cache.GuildRecord(), snowflakes.Snowflake(543123): cache.GuildRecord(guild=mock_guild, is_available=False), @@ -881,7 +880,7 @@ def test_get_available_guild_for_known_guild_when_unavailable(self, cache_impl): assert result is None def test_get_available_guild_for_unknown_guild(self, cache_impl): - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54234123): cache.GuildRecord(), snowflakes.Snowflake(543123): cache.GuildRecord(), @@ -890,7 +889,7 @@ def test_get_available_guild_for_unknown_guild(self, cache_impl): assert cache_impl.get_available_guild(snowflakes.Snowflake(543123)) is None def test_get_available_guild_for_unknown_guild_record(self, cache_impl): - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54234123): cache.GuildRecord(), } @@ -899,7 +898,7 @@ def test_get_available_guild_for_unknown_guild_record(self, cache_impl): def test_get_unavailable_guild_for_known_guild_when_unavailable(self, cache_impl): mock_guild = mock.MagicMock(guilds.GatewayGuild) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54234123): cache.GuildRecord(), snowflakes.Snowflake(452131): cache.GuildRecord(guild=mock_guild, is_available=False), @@ -911,7 +910,7 @@ def test_get_unavailable_guild_for_known_guild_when_unavailable(self, cache_impl def test_get_unavailable_guild_for_known_guild_when_available(self, cache_impl): mock_guild = mock.Mock(guilds.GatewayGuild) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54234123): cache.GuildRecord(), snowflakes.Snowflake(654234): cache.GuildRecord(guild=mock_guild, is_available=True), @@ -923,7 +922,7 @@ def test_get_unavailable_guild_for_known_guild_when_available(self, cache_impl): assert result is None def test_get_unavailable_guild_for_unknown_guild(self, cache_impl): - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54234123): cache.GuildRecord(), snowflakes.Snowflake(543123): cache.GuildRecord(), @@ -932,7 +931,7 @@ def test_get_unavailable_guild_for_unknown_guild(self, cache_impl): assert cache_impl.get_unavailable_guild(snowflakes.Snowflake(543123)) is None def test_get_unavailable_guild_for_unknown_guild_record(self, cache_impl): - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54234123): cache.GuildRecord(), } @@ -942,7 +941,7 @@ def test_get_unavailable_guild_for_unknown_guild_record(self, cache_impl): def test_get_available_guilds_view(self, cache_impl): mock_guild_1 = mock.MagicMock(guilds.GatewayGuild) mock_guild_2 = mock.MagicMock(guilds.GatewayGuild) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(4312312): cache.GuildRecord(guild=mock_guild_1, is_available=True), snowflakes.Snowflake(34123): cache.GuildRecord(), @@ -956,7 +955,7 @@ def test_get_available_guilds_view(self, cache_impl): } def test_get_available_guilds_view_when_no_guilds_cached(self, cache_impl): - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(4312312): cache.GuildRecord(), snowflakes.Snowflake(34123): cache.GuildRecord(), @@ -968,7 +967,7 @@ def test_get_available_guilds_view_when_no_guilds_cached(self, cache_impl): def test_get_unavailable_guilds_view(self, cache_impl): mock_guild_1 = mock.MagicMock(guilds.GatewayGuild) mock_guild_2 = mock.MagicMock(guilds.GatewayGuild) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(4312312): cache.GuildRecord(guild=mock_guild_1, is_available=False), snowflakes.Snowflake(34123): cache.GuildRecord(), @@ -982,7 +981,7 @@ def test_get_unavailable_guilds_view(self, cache_impl): } def test_get_unavailable_guilds_view_when_no_guilds_cached(self, cache_impl): - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(4312312): cache.GuildRecord(), snowflakes.Snowflake(34123): cache.GuildRecord(), @@ -1060,7 +1059,7 @@ def test__build_invite(self, cache_impl): ) mock_inviter = mock.MagicMock(users.User) mock_target_user = mock.MagicMock(users.User) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(123123): cache.GenericRefWrapper(object=mock_inviter), snowflakes.Snowflake(9999): mock.Mock(cache.GenericRefWrapper), @@ -1130,13 +1129,13 @@ def test_clear_invites(self, cache_impl): mock_invite_2 = mock.Mock(invites.InviteWithMetadata) mock_wrapped_target_user = mock.Mock(cache.GenericRefWrapper[users.User], ref_count=5) mock_wrapped_inviter = mock.Mock(cache.GenericRefWrapper[users.User], ref_count=3) - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( { "hiBye": mock_invite_data_1, "Lblalbla": mock_invite_data_2, } ) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(5341231): mock_wrapped_target_user, snowflakes.Snowflake(12354123): mock_wrapped_inviter, @@ -1184,21 +1183,21 @@ def test_clear_invites_for_guild(self, cache_impl): mock_invite_2 = mock.Mock(invites.InviteWithMetadata) mock_wrapped_target_user = mock.Mock(cache.GenericRefWrapper[users.User], ref_count=4) mock_wrapped_inviter = mock.Mock(cache.GenericRefWrapper[users.User], ref_count=42) - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( { "oeoeoeoeooe": mock_invite_data_1, "owowowowoowowow": mock_invite_data_2, "oeoeoeoeoeoeoe": mock_other_invite_data, } ) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(5341231): mock_wrapped_target_user, snowflakes.Snowflake(12354123): mock_wrapped_inviter, snowflakes.Snowflake(65345352): mock.Mock(cache.GenericRefWrapper), } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54123): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(999888777): cache.GuildRecord(invites=["oeoeoeoeooe", "owowowowoowowow"]), @@ -1234,7 +1233,7 @@ def test_clear_invites_for_guild_unknown_invite_cache(self, cache_impl): cache_impl._invite_entries = { "oeoeoeoeoeoeoe": mock_other_invite_data, } - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54123): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(999888777): cache.GuildRecord(invites=None), @@ -1247,12 +1246,12 @@ def test_clear_invites_for_guild_unknown_invite_cache(self, cache_impl): def test_clear_invites_for_guild_unknown_record(self, cache_impl): mock_other_invite_data = mock.Mock(cache.InviteData) - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( { "oeoeoeoeoeoeoe": mock_other_invite_data, } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54123): mock.Mock(cache.GuildRecord), } @@ -1281,7 +1280,7 @@ def test_clear_invites_for_channel(self, cache_impl): mock_invite_2 = mock.Mock(invites.InviteWithMetadata) mock_wrapped_target_user = mock.Mock(cache.GenericRefWrapper[users.User], ref_count=42) mock_wrapped_inviter = mock.Mock(cache.GenericRefWrapper[users.User], ref_count=280) - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( { "oeoeoeoeooe": mock_invite_data_1, "owowowowoowowow": mock_invite_data_2, @@ -1289,14 +1288,14 @@ def test_clear_invites_for_channel(self, cache_impl): "oeo": mock_other_invite_data_2, } ) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(5341231): mock_wrapped_target_user, snowflakes.Snowflake(12354123): mock_wrapped_inviter, snowflakes.Snowflake(65345352): mock.Mock(cache.GenericRefWrapper), } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54123): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(999888777): cache.GuildRecord( @@ -1338,17 +1337,17 @@ def test_clear_invites_for_channel(self, cache_impl): def test_clear_invites_for_channel_unknown_invite_cache(self, cache_impl): mock_other_invite_data = mock.Mock(cache.InviteData) - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( { "oeoeoeoeoeoeoe": mock_other_invite_data, } ) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(65345352): mock.Mock(cache.GenericRefWrapper), } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54123): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(999888777): cache.GuildRecord(invites=None), @@ -1363,17 +1362,17 @@ def test_clear_invites_for_channel_unknown_invite_cache(self, cache_impl): def test_clear_invites_for_channel_unknown_record(self, cache_impl): mock_other_invite_data = mock.Mock(cache.InviteData) - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( { "oeoeoeoeoeoeoe": mock_other_invite_data, } ) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(65345352): mock.Mock(cache.GenericRefWrapper), } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(54123): mock.Mock(cache.GuildRecord), } @@ -1394,10 +1393,10 @@ def test_delete_invite(self, cache_impl): target_user=mock.Mock(users.User, id=snowflakes.Snowflake(9191919)), guild_id=snowflakes.Snowflake(999999999), ) - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( {"blamSpat": mock_other_invite_data, "oooooooooooooo": mock_invite_data} ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(1234312): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(999999999): cache.GuildRecord(invites=["ok", "blat", "oooooooooooooo"]), @@ -1423,7 +1422,7 @@ def test_delete_invite_when_guild_id_is_None(self, cache_impl): mock_invite_data = mock.Mock(cache.InviteData) mock_other_invite_data = mock.Mock(cache.InviteData) mock_invite = mock.Mock(invites.InviteWithMetadata, inviter=None, target_user=None, guild_id=None) - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( {"blamSpat": mock_other_invite_data, "oooooooooooooo": mock_invite_data} ) cache_impl._build_invite = mock.Mock(return_value=mock_invite) @@ -1440,10 +1439,10 @@ def test_delete_invite_without_users(self, cache_impl): mock_invite = mock.Mock( invites.InviteWithMetadata, inviter=None, target_user=None, guild_id=snowflakes.Snowflake(999999999) ) - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( {"blamSpat": mock_other_invite_data, "oooooooooooooo": mock_invite_data} ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(1234312): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(999999999): cache.GuildRecord(invites=["ok", "blat", "oooooooooooooo"]), @@ -1472,7 +1471,7 @@ def test_get_invite(self, cache_impl): mock_invite_data = mock.Mock(cache.InviteData) mock_invite = mock.Mock(invites.InviteWithMetadata) cache_impl._build_invite = mock.Mock(return_value=mock_invite) - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( {"blam": mock.Mock(cache.InviteData), "okokok": mock_invite_data} ) assert cache_impl.get_invite("okokok") is mock_invite @@ -1480,7 +1479,7 @@ def test_get_invite(self, cache_impl): def test_get_invite_for_unknown_invite(self, cache_impl): cache_impl._build_invite = mock.Mock() - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( { "blam": mock.Mock(cache.InviteData), } @@ -1499,14 +1498,14 @@ def test_get_invites_view(self, cache_impl): mock_invite_2 = mock.Mock(invites.InviteWithMetadata) mock_wrapped_inviter = mock.Mock(cache.GenericRefWrapper[users.User]) mock_wrapped_target_user = mock.Mock(cache.GenericRefWrapper[users.User]) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(987): mock_wrapped_inviter, snowflakes.Snowflake(34123): mock_wrapped_target_user, snowflakes.Snowflake(6599): mock.Mock(cache.GenericRefWrapper), } ) - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( {"okok": mock_invite_data_1, "blamblam": mock_invite_data_2} ) cache_impl._build_invite = mock.Mock(side_effect=[mock_invite_1, mock_invite_2]) @@ -1541,21 +1540,21 @@ def test_get_invites_view_for_guild(self, cache_impl): mock_invite_2 = mock.Mock(invites.InviteWithMetadata) mock_wrapped_inviter = mock.Mock(cache.GenericRefWrapper[users.User]) mock_wrapped_target_user = mock.Mock(cache.GenericRefWrapper[users.User]) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(987): mock_wrapped_inviter, snowflakes.Snowflake(34123): mock_wrapped_target_user, snowflakes.Snowflake(6599): mock.Mock(cache.GenericRefWrapper), } ) - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( { "okok": mock_invite_data_1, "dsaytert": mock_invite_data_2, "bitsbits ": mock.Mock(cache.InviteData), } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(9544994): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(4444444): cache.GuildRecord(invites=["okok", "dsaytert"]), @@ -1586,13 +1585,13 @@ def test_get_invites_view_for_guild(self, cache_impl): ) def test_get_invites_view_for_guild_unknown_emoji_cache(self, cache_impl): - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( { "okok": mock.Mock(cache.InviteData), "dsaytert": mock.Mock(cache.InviteData), } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(9544994): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(4444444): cache.GuildRecord(invites=None), @@ -1603,13 +1602,13 @@ def test_get_invites_view_for_guild_unknown_emoji_cache(self, cache_impl): cache_impl._build_invite.assert_not_called() def test_get_invites_view_for_guild_unknown_record(self, cache_impl): - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( { "okok": mock.Mock(cache.InviteData), "dsaytert": mock.Mock(cache.InviteData), } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(9544994): mock.Mock(cache.GuildRecord), } @@ -1629,14 +1628,14 @@ def test_get_invites_view_for_channel(self, cache_impl): mock_invite_2 = mock.Mock(invites.InviteWithMetadata) mock_wrapped_inviter = mock.Mock(cache.GenericRefWrapper[users.User]) mock_wrapped_target_user = mock.Mock(cache.GenericRefWrapper[users.User]) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(4312365): mock_wrapped_inviter, snowflakes.Snowflake(65643213): mock_wrapped_target_user, snowflakes.Snowflake(999875673): mock.Mock(cache.GenericRefWrapper), } ) - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( { "blamBang": mock_invite_data_1, "bingBong": mock_invite_data_2, @@ -1644,7 +1643,7 @@ def test_get_invites_view_for_channel(self, cache_impl): "Fam": mock.Mock(cache.InviteData, channel_id=snowflakes.Snowflake(2123)), } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(31423): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(83452134): cache.GuildRecord(invites=["blamBang", "bingBong", "Pop"]), @@ -1677,13 +1676,13 @@ def test_get_invites_view_for_channel(self, cache_impl): ) def test_get_invites_view_for_channel_unknown_emoji_cache(self, cache_impl): - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( { "okok": mock.Mock(cache.InviteData), "dsaytert": mock.Mock(cache.InviteData), } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(9544994): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(4444444): cache.GuildRecord(invites=None), @@ -1695,13 +1694,13 @@ def test_get_invites_view_for_channel_unknown_emoji_cache(self, cache_impl): cache_impl._build_invite.assert_not_called() def test_get_invites_view_for_channel_unknown_record(self, cache_impl): - cache_impl._invite_entries = mapping.DictionaryCollection( + cache_impl._invite_entries = collections.FreezableDict( { "okok": mock.Mock(cache.InviteData), "dsaytert": mock.Mock(cache.InviteData), } ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(9544994): mock.Mock(cache.GuildRecord), } @@ -1771,7 +1770,7 @@ def test__build_member(self, cache_impl): is_mute=True, ) mock_user = mock.MagicMock(users.User) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( {snowflakes.Snowflake(512312354): cache.GenericRefWrapper(object=mock_user)} ) member = cache_impl._build_member(member_data) @@ -1797,7 +1796,7 @@ def test__build_member_for_passed_through_user(self, cache_impl): is_mute=True, ) mock_user = mock.MagicMock(users.User) - cache_impl._user_entries = mapping.DictionaryCollection({}) + cache_impl._user_entries = collections.FreezableDict({}) member = cache_impl._build_member( member_data, cached_users={snowflakes.Snowflake(512312354): cache.GenericRefWrapper(object=mock_user)}, @@ -1837,10 +1836,10 @@ def test_clear_members(self, cache_impl): mock_wrapped_user_3 = object() mock_wrapped_user_4 = object() mock_wrapped_user_5 = object() - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(43123123): cache.GuildRecord( - members=mapping.DictionaryCollection( + members=collections.FreezableDict( { snowflakes.Snowflake(2123123): mock_data_member_1, snowflakes.Snowflake(212314423): mock_data_member_2, @@ -1850,7 +1849,7 @@ def test_clear_members(self, cache_impl): snowflakes.Snowflake(35123): cache.GuildRecord(members={}), snowflakes.Snowflake(76345123): cache.GuildRecord(members=None), snowflakes.Snowflake(65234): cache.GuildRecord( - members=mapping.DictionaryCollection( + members=collections.FreezableDict( { snowflakes.Snowflake(2123166623): mock_data_member_3, snowflakes.Snowflake(21237777123): mock_data_member_4, @@ -1860,7 +1859,7 @@ def test_clear_members(self, cache_impl): ), } ) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(2123123): mock_wrapped_user_1, snowflakes.Snowflake(212314423): mock_wrapped_user_2, @@ -1926,7 +1925,7 @@ def test_delete_member_for_known_member(self, cache_impl): mock_member_data = mock.Mock( cache.MemberData, id=snowflakes.Snowflake(67876), guild_id=snowflakes.Snowflake(42123) ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( {snowflakes.Snowflake(42123): cache.GuildRecord(members={snowflakes.Snowflake(67876): mock_member_data})} ) cache_impl._remove_guild_record_if_empty = mock.Mock() @@ -1939,13 +1938,13 @@ def test_delete_member_for_known_member(self, cache_impl): cache_impl._remove_guild_record_if_empty.assert_called_once_with(snowflakes.Snowflake(42123)) def test_delete_member_for_known_hard_referenced_member(self, cache_impl): - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( {snowflakes.Snowflake(67876): cache.GenericRefWrapper(object=object(), ref_count=4)} ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(42123): cache.GuildRecord( - members=mapping.DictionaryCollection( + members=collections.FreezableDict( { snowflakes.Snowflake(67876): mock.Mock( cache.MemberData, @@ -1954,20 +1953,18 @@ def test_delete_member_for_known_hard_referenced_member(self, cache_impl): ) } ), - voice_states=mapping.DictionaryCollection( - {snowflakes.Snowflake(67876): mock.Mock(voices.VoiceState)} - ), + voice_states=collections.FreezableDict({snowflakes.Snowflake(67876): mock.Mock(voices.VoiceState)}), ) } ) assert cache_impl.delete_member(snowflakes.Snowflake(42123), snowflakes.Snowflake(67876)) is None def test_get_member_for_unknown_member_cache(self, cache_impl): - cache_impl._guild_entries = mapping.DictionaryCollection({snowflakes.Snowflake(1234213): cache.GuildRecord()}) + cache_impl._guild_entries = collections.FreezableDict({snowflakes.Snowflake(1234213): cache.GuildRecord()}) assert cache_impl.get_member(snowflakes.Snowflake(1234213), snowflakes.Snowflake(512312354)) is None def test_get_member_for_unknown_member(self, cache_impl): - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(1234213): cache.GuildRecord( members={snowflakes.Snowflake(43123): mock.Mock(cache.MemberData)} @@ -1982,10 +1979,10 @@ def test_get_member_for_unknown_guild_record(self, cache_impl): def test_get_member_for_known_member(self, cache_impl): mock_member_data = mock.Mock(cache.MemberData) mock_member = mock.Mock(guilds.Member) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(1234213): cache.GuildRecord( - members=mapping.DictionaryCollection( + members=collections.FreezableDict( { snowflakes.Snowflake(512312354): mock_member_data, snowflakes.Snowflake(321): mock.Mock(cache.MemberData), @@ -1994,13 +1991,13 @@ def test_get_member_for_known_member(self, cache_impl): ) } ) - cache_impl._user_entries = mapping.DictionaryCollection({}) + cache_impl._user_entries = collections.FreezableDict({}) cache_impl._build_member = mock.Mock(return_value=mock_member) assert cache_impl.get_member(snowflakes.Snowflake(1234213), snowflakes.Snowflake(512312354)) is mock_member cache_impl._build_member.assert_called_once_with(mock_member_data) def test_get_members_view(self, cache_impl): - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(345123): object(), snowflakes.Snowflake(65345): object(), @@ -2021,17 +2018,17 @@ def test_get_members_view(self, cache_impl): cache_impl._build_member = mock.Mock( side_effect=[mock_member_1, mock_member_2, mock_member_3, mock_member_4, mock_member_5] ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(543123): cache.GuildRecord(), snowflakes.Snowflake(54123123): cache.GuildRecord( - members=mapping.DictionaryCollection( + members=collections.FreezableDict( {snowflakes.Snowflake(321): mock_member_data_1, snowflakes.Snowflake(6324): mock_member_data_2} ) ), - snowflakes.Snowflake(54234): cache.GuildRecord(members=mapping.DictionaryCollection({})), + snowflakes.Snowflake(54234): cache.GuildRecord(members=collections.FreezableDict({})), snowflakes.Snowflake(783452): cache.GuildRecord( - members=mapping.DictionaryCollection( + members=collections.FreezableDict( { snowflakes.Snowflake(54123): mock_member_data_3, snowflakes.Snowflake(786234): mock_member_data_4, @@ -2069,7 +2066,7 @@ def test_get_members_view_for_guild_unknown_record(self, cache_impl): assert members_mapping == {} def test_get_members_view_for_guild_unknown_member_cache(self, cache_impl): - cache_impl._guild_entries = mapping.DictionaryCollection({snowflakes.Snowflake(42334): cache.GuildRecord()}) + cache_impl._guild_entries = collections.FreezableDict({snowflakes.Snowflake(42334): cache.GuildRecord()}) members_mapping = cache_impl.get_members_view_for_guild(snowflakes.Snowflake(42334)) assert members_mapping == {} @@ -2081,7 +2078,7 @@ def test_get_members_view_for_guild(self, cache_impl): mock_wrapped_user_1 = mock.Mock(cache.GenericRefWrapper[users.User]) mock_wrapped_user_2 = mock.Mock(cache.GenericRefWrapper[users.User]) guild_record = cache.GuildRecord( - members=mapping.DictionaryCollection( + members=collections.FreezableDict( { snowflakes.Snowflake(3214321): mock_member_data_1, snowflakes.Snowflake(53224): mock_member_data_2, @@ -2089,8 +2086,8 @@ def test_get_members_view_for_guild(self, cache_impl): } ) ) - cache_impl._guild_entries = mapping.DictionaryCollection({snowflakes.Snowflake(42334): guild_record}) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict({snowflakes.Snowflake(42334): guild_record}) + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(3214321): mock_wrapped_user_1, snowflakes.Snowflake(53224): mock_wrapped_user_2, @@ -2164,10 +2161,10 @@ def test_set_member_doesnt_increment_user_ref_count_for_pre_cached_member(self, member_model = mock.MagicMock(guilds.Member, user=mock_user, guild_id=snowflakes.Snowflake(67345234)) cache_impl.set_user = mock.Mock() cache_impl._increment_user_ref_count = mock.Mock() - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(67345234): cache.GuildRecord( - members=mapping.DictionaryCollection({snowflakes.Snowflake(645234123): mock.Mock(cache.MemberData)}) + members=collections.FreezableDict({snowflakes.Snowflake(645234123): mock.Mock(cache.MemberData)}) ) } ) @@ -2256,7 +2253,7 @@ def test_update_role(self, cache_impl): def test_clear_users_for_cached_users(self, cache_impl): mock_user_1 = mock.MagicMock(users.User) mock_user_2 = mock.MagicMock(users.User) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(53422132): cache.GenericRefWrapper(object=mock_user_1), snowflakes.Snowflake(7654433245): cache.GenericRefWrapper(object=mock_user_2), @@ -2270,7 +2267,7 @@ def test_clear_users_for_cached_users(self, cache_impl): def test_clear_users_ignores_hard_referenced_users(self, cache_impl): wrapped_user = cache.GenericRefWrapper(object=mock.Mock(users.User), ref_count=2) - cache_impl._user_entries = mapping.DictionaryCollection({snowflakes.Snowflake(53422132): wrapped_user}) + cache_impl._user_entries = collections.FreezableDict({snowflakes.Snowflake(53422132): wrapped_user}) assert cache_impl.clear_users() == {} assert cache_impl._user_entries == {snowflakes.Snowflake(53422132): wrapped_user} @@ -2281,7 +2278,7 @@ def test_clear_users_for_empty_user_cache(self, cache_impl): def test_delete_user_for_known_unreferenced_user(self, cache_impl): mock_user = mock.Mock(users.User) mock_wrapped_other_user = cache.GenericRefWrapper(object=mock.Mock(users.User)) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(21231234): cache.GenericRefWrapper(object=mock_user), snowflakes.Snowflake(645234): mock_wrapped_other_user, @@ -2293,7 +2290,7 @@ def test_delete_user_for_known_unreferenced_user(self, cache_impl): def test_delete_user_for_referenced_user(self, cache_impl): mock_wrapped_user = mock.Mock(cache.GenericRefWrapper, ref_count=2) mock_other_wrapped_user = mock.Mock(cache.GenericRefWrapper) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(21231234): mock_wrapped_user, snowflakes.Snowflake(645234): mock_other_wrapped_user, @@ -2307,7 +2304,7 @@ def test_delete_user_for_referenced_user(self, cache_impl): def test_delete_user_for_unknown_user(self, cache_impl): mock_wrapped_user = mock.Mock(cache.GenericRefWrapper) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(21231234): mock_wrapped_user, } @@ -2319,7 +2316,7 @@ def test_delete_user_for_unknown_user(self, cache_impl): def test_get_user_for_known_user(self, cache_impl): mock_user = mock.MagicMock(users.User) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(21231234): cache.GenericRefWrapper(object=mock_user), snowflakes.Snowflake(645234): mock.Mock(cache.GenericRefWrapper), @@ -2331,7 +2328,7 @@ def test_get_user_for_known_user(self, cache_impl): def test_get_users_view_for_filled_user_cache(self, cache_impl): mock_user_1 = mock.MagicMock(users.User) mock_user_2 = mock.MagicMock(users.User) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(54123): cache.GenericRefWrapper(object=mock_user_1), snowflakes.Snowflake(76345): cache.GenericRefWrapper(object=mock_user_2), @@ -2347,7 +2344,7 @@ def test_get_users_view_for_empty_user_cache(self, cache_impl): def test_set_user(self, cache_impl): mock_user = mock.MagicMock(users.User, id=snowflakes.Snowflake(6451234123)) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( {snowflakes.Snowflake(542143): mock.Mock(cache.GenericRefWrapper)} ) assert cache_impl.set_user(mock_user) is None @@ -2358,7 +2355,7 @@ def test_set_user(self, cache_impl): def test_set_user_carries_over_ref_count(self, cache_impl): mock_user = mock.MagicMock(users.User, id=snowflakes.Snowflake(6451234123)) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(542143): mock.Mock(cache.GenericRefWrapper), snowflakes.Snowflake(6451234123): mock.Mock(cache.GenericRefWrapper, ref_count=42), @@ -2397,14 +2394,14 @@ def test__build_voice_state(self, cache_impl): mock_member_data = mock.Mock(cache.MemberData) mock_member = mock.Mock(guilds.Member) record = cache.GuildRecord( - members=mapping.DictionaryCollection( + members=collections.FreezableDict( { snowflakes.Snowflake(7512312): mock_member_data, snowflakes.Snowflake(43123123): mock.Mock(cache.MemberData), } ), ) - cache_impl._guild_entries = mapping.DictionaryCollection({snowflakes.Snowflake(54123123): record}) + cache_impl._guild_entries = collections.FreezableDict({snowflakes.Snowflake(54123123): record}) cache_impl._build_member = mock.Mock(return_value=mock_member) current_voice_state = cache_impl._build_voice_state(voice_state_data) cache_impl._build_member.assert_called_once_with(mock_member_data, cached_users=None) @@ -2441,16 +2438,16 @@ def test__build_voice_state_with_pass_through_member_and_user_data(self, cache_i cache_impl._build_member = mock.Mock(return_value=mock_member) current_voice_state = cache_impl._build_voice_state( voice_state_data, - cached_members=mapping.DictionaryCollection( + cached_members=collections.FreezableDict( { snowflakes.Snowflake(7512312): mock_member_data, snowflakes.Snowflake(63123): mock.Mock(cache.MemberData), } ), - cached_users=mapping.DictionaryCollection({snowflakes.Snowflake(7512312): mock_user}), + cached_users=collections.FreezableDict({snowflakes.Snowflake(7512312): mock_user}), ) cache_impl._build_member.assert_called_once_with( - mock_member_data, cached_users=mapping.DictionaryCollection({snowflakes.Snowflake(7512312): mock_user}) + mock_member_data, cached_users=collections.FreezableDict({snowflakes.Snowflake(7512312): mock_user}) ) assert current_voice_state.app is cache_impl._app assert current_voice_state.channel_id == snowflakes.Snowflake(4651234123) @@ -2487,13 +2484,13 @@ def test_clear_voice_states_for_guild(self, cache_impl): id=snowflakes.Snowflake(43123123), ) record = cache.GuildRecord( - voice_states=mapping.DictionaryCollection( + voice_states=collections.FreezableDict( { snowflakes.Snowflake(7512312): mock_voice_state_data_1, snowflakes.Snowflake(43123123): mock_voice_state_data_2, } ), - members=mapping.DictionaryCollection( + members=collections.FreezableDict( { snowflakes.Snowflake(7512312): mock_member_data_1, snowflakes.Snowflake(43123123): mock_member_data_2, @@ -2504,14 +2501,14 @@ def test_clear_voice_states_for_guild(self, cache_impl): cache_impl._remove_guild_record_if_empty = mock.Mock() mock_wrapped_user_1 = mock.Mock(cache.GenericRefWrapper[users.User]) mock_wrapped_user_2 = mock.Mock(cache.GenericRefWrapper[users.User]) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( { snowflakes.Snowflake(7512312): mock_wrapped_user_1, snowflakes.Snowflake(43123123): mock_wrapped_user_2, snowflakes.Snowflake(56234): mock.Mock(cache.GenericRefWrapper), } ) - cache_impl._guild_entries = mapping.DictionaryCollection({snowflakes.Snowflake(54123123): record}) + cache_impl._guild_entries = collections.FreezableDict({snowflakes.Snowflake(54123123): record}) cache_impl._build_voice_state = mock.Mock(side_effect=[mock_voice_state_1, mock_voice_state_2]) assert cache_impl.clear_voice_states_for_guild(snowflakes.Snowflake(54123123)) == { snowflakes.Snowflake(7512312): mock_voice_state_1, @@ -2559,20 +2556,20 @@ def test_delete_voice_state(self, cache_impl): mock_member_data = object() cache_impl._build_voice_state = mock.Mock(return_value=mock_voice_state) guild_record = cache.GuildRecord( - voice_states=mapping.DictionaryCollection( + voice_states=collections.FreezableDict( { snowflakes.Snowflake(12354345): mock_voice_state_data, snowflakes.Snowflake(6541234): mock_other_voice_state_data, } ), - members=mapping.DictionaryCollection( + members=collections.FreezableDict( {snowflakes.Snowflake(12354345): mock_member_data, snowflakes.Snowflake(9955959): object()} ), ) - cache_impl._user_entries = mapping.DictionaryCollection( + cache_impl._user_entries = collections.FreezableDict( {snowflakes.Snowflake(12354345): object(), snowflakes.Snowflake(9393): object()} ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(65234): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(43123): guild_record, @@ -2592,9 +2589,9 @@ def test_delete_voice_state_unknown_state(self, cache_impl): mock_other_voice_state_data = mock.Mock(cache.VoiceStateData) cache_impl._build_voice_state = mock.Mock() guild_record = cache.GuildRecord( - voice_states=mapping.DictionaryCollection({snowflakes.Snowflake(6541234): mock_other_voice_state_data}) + voice_states=collections.FreezableDict({snowflakes.Snowflake(6541234): mock_other_voice_state_data}) ) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(65234): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(43123): guild_record, @@ -2610,7 +2607,7 @@ def test_delete_voice_state_unknown_state(self, cache_impl): def test_delete_voice_state_unknown_state_cache(self, cache_impl): cache_impl._build_voice_state = mock.Mock() guild_record = cache.GuildRecord(voice_states=None) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(65234): mock.Mock(cache.GuildRecord), snowflakes.Snowflake(43123): guild_record, @@ -2622,7 +2619,7 @@ def test_delete_voice_state_unknown_state_cache(self, cache_impl): def test_delete_voice_state_unknown_record(self, cache_impl): cache_impl._build_voice_state = mock.Mock() - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(65234): mock.Mock(cache.GuildRecord), } @@ -2636,7 +2633,7 @@ def test_get_voice_state_for_known_voice_state(self, cache_impl): mock_voice_state = mock.Mock(voices.VoiceState) cache_impl._build_voice_state = mock.Mock(return_value=mock_voice_state) guild_record = cache.GuildRecord(voice_states={snowflakes.Snowflake(43124): mock_voice_state_data}) - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(1235123): guild_record, snowflakes.Snowflake(73245): mock.Mock(cache.GuildRecord), @@ -2647,10 +2644,10 @@ def test_get_voice_state_for_known_voice_state(self, cache_impl): cache_impl._build_voice_state.assert_called_once_with(mock_voice_state_data) def test_get_voice_state_for_unknown_voice_state(self, cache_impl): - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(1235123): cache.GuildRecord( - voice_states=mapping.DictionaryCollection( + voice_states=collections.FreezableDict( {snowflakes.Snowflake(54123): mock.Mock(cache.VoiceStateData)} ) ), @@ -2660,7 +2657,7 @@ def test_get_voice_state_for_unknown_voice_state(self, cache_impl): assert cache_impl.get_voice_state(snowflakes.Snowflake(1235123), snowflakes.Snowflake(43124)) is None def test_get_voice_state_for_unknown_voice_state_cache(self, cache_impl): - cache_impl._guild_entries = mapping.DictionaryCollection( + cache_impl._guild_entries = collections.FreezableDict( { snowflakes.Snowflake(1235123): cache.GuildRecord(), snowflakes.Snowflake(73245): mock.Mock(cache.GuildRecord), diff --git a/tests/hikari/utilities/test_mapping.py b/tests/hikari/utilities/test_collections.py similarity index 50% rename from tests/hikari/utilities/test_mapping.py rename to tests/hikari/utilities/test_collections.py index 1b2258a5f7..a76c6e8cb6 100644 --- a/tests/hikari/utilities/test_mapping.py +++ b/tests/hikari/utilities/test_collections.py @@ -18,118 +18,121 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import array as array_ import asyncio import datetime +import operator +import sys import time import pytest -from hikari.utilities import mapping +from hikari.utilities import collections from tests.hikari import hikari_test_helpers -class TestDictionaryCollection: +class TestFreezableDict: def test___init___with_source(self): - mock_map = mapping.DictionaryCollection({"o": "NO", "bye": "blam", "foo": "bar"}) + mock_map = collections.FreezableDict({"o": "NO", "bye": "blam", "foo": "bar"}) assert mock_map == {"o": "NO", "bye": "blam", "foo": "bar"} def test_copy(self): - mock_map = mapping.DictionaryCollection({"foo": "bar", "crash": "balloon"}) + mock_map = collections.FreezableDict({"foo": "bar", "crash": "balloon"}) result = mock_map.copy() assert result == {"foo": "bar", "crash": "balloon"} - assert isinstance(result, mapping.DictionaryCollection) + assert isinstance(result, collections.FreezableDict) assert result is not mock_map def test_freeze(self): - mock_map = mapping.DictionaryCollection({"hikari": "shinji", "gendo": "san"}) + mock_map = collections.FreezableDict({"hikari": "shinji", "gendo": "san"}) result = mock_map.freeze() assert result == {"hikari": "shinji", "gendo": "san"} assert isinstance(result, dict) def test___delitem__(self): - mock_map = mapping.DictionaryCollection({"hikari": "shinji", "gendo": "san", "screwed": "up"}) + mock_map = collections.FreezableDict({"hikari": "shinji", "gendo": "san", "screwed": "up"}) del mock_map["hikari"] assert mock_map == {"gendo": "san", "screwed": "up"} def test___getitem__(self): - mock_map = mapping.DictionaryCollection({"curiosity": "rover", "ok": "bye"}) + mock_map = collections.FreezableDict({"curiosity": "rover", "ok": "bye"}) assert mock_map["ok"] == "bye" def test____iter__(self): - mock_map = mapping.DictionaryCollection({"curiosity": "rover", "cat": "bag", "ok": "bye"}) + mock_map = collections.FreezableDict({"curiosity": "rover", "cat": "bag", "ok": "bye"}) assert list(mock_map) == ["curiosity", "cat", "ok"] def test___len__(self): - mock_map = mapping.DictionaryCollection({"hmm": "blam", "cat": "bag", "ok": "bye"}) + mock_map = collections.FreezableDict({"hmm": "blam", "cat": "bag", "ok": "bye"}) assert len(mock_map) == 3 def test___setitem__(self): - mock_map = mapping.DictionaryCollection({"hmm": "forearm", "cat": "bag", "ok": "bye"}) + mock_map = collections.FreezableDict({"hmm": "forearm", "cat": "bag", "ok": "bye"}) mock_map["bye"] = 4 assert mock_map == {"hmm": "forearm", "cat": "bag", "ok": "bye", "bye": 4} -class TestFrozenMRIMapping: +class TestFrozenDict: def test___init__(self): - mock_map = mapping._FrozenMRIMapping({"foo": (0.432, "bar"), "blam": (0.111, "okok")}) + mock_map = collections._FrozenDict({"foo": (0.432, "bar"), "blam": (0.111, "okok")}) assert mock_map == {"foo": "bar", "blam": "okok"} def test___getitem__(self): - mock_map = mapping._FrozenMRIMapping({"blam": (0.432, "bar"), "obar": (0.111, "okok")}) + mock_map = collections._FrozenDict({"blam": (0.432, "bar"), "obar": (0.111, "okok")}) assert mock_map["obar"] == "okok" def test___iter__(self): - mock_map = mapping._FrozenMRIMapping({"bye": (0.33, "bye"), "111": (0.2, "222"), "45949": (0.5, "020202")}) + mock_map = collections._FrozenDict({"bye": (0.33, "bye"), "111": (0.2, "222"), "45949": (0.5, "020202")}) assert list(mock_map) == ["bye", "111", "45949"] def test___len__(self): - mock_map = mapping._FrozenMRIMapping({"wsw": (0.3, "3"), "fdsa": (0.55, "ewqwe"), "45949": (0.23, "fsasd")}) + mock_map = collections._FrozenDict({"wsw": (0.3, "3"), "fdsa": (0.55, "ewqwe"), "45949": (0.23, "fsasd")}) assert len(mock_map) == 3 def test___delitem__(self): - mock_map = mapping._FrozenMRIMapping({"rororo": (0.55, "bye bye"), "raw": (0.999, "ywywyw")}) + mock_map = collections._FrozenDict({"rororo": (0.55, "bye bye"), "raw": (0.999, "ywywyw")}) del mock_map["raw"] assert mock_map == {"rororo": "bye bye"} def test___setitem__(self): - mock_map = mapping._FrozenMRIMapping({"rororo": (0.55, "bye 3231"), "2121": (0.999, "4321")}) + mock_map = collections._FrozenDict({"rororo": (0.55, "bye 3231"), "2121": (0.999, "4321")}) mock_map["foo bar"] = 42 assert mock_map == {"rororo": "bye 3231", "2121": "4321", "foo bar": 42} -class TestMRIMutableMapping: +class TestTimedCacheMap: def test___init___with_source(self): raw_map = { "not_in": (time.perf_counter() - 50, "goodbye"), "ok": (time.perf_counter() + 30, "no"), "blam": (time.perf_counter() + 20, "bye"), } - mocK_map = mapping.MRIMutableMapping(raw_map, expiry=datetime.timedelta(seconds=42)) + mock_map = collections.TimedCacheMap(raw_map, expiry=datetime.timedelta(seconds=42)) - assert mocK_map == {"blam": "bye", "ok": "no"} + assert mock_map == {"blam": "bye", "ok": "no"} def test___init___raises_value_error_on_invalid_expiry(self): with pytest.raises(ValueError, match="expiry time must be greater than 0 microseconds."): - mapping.MRIMutableMapping(expiry=datetime.timedelta(seconds=0)) + collections.TimedCacheMap(expiry=datetime.timedelta(seconds=0)) with pytest.raises(ValueError, match="expiry time must be greater than 0 microseconds."): - mapping.MRIMutableMapping(expiry=datetime.timedelta(seconds=-50)) + collections.TimedCacheMap(expiry=datetime.timedelta(seconds=-50)) def test_copy(self): raw_map = { "floom": (999999999999999999999999, "buebue"), "bash": (999999999999999999999999, "bunny_time"), } - mock_map = mapping.MRIMutableMapping(raw_map, expiry=datetime.timedelta(seconds=4523412)) + mock_map = collections.TimedCacheMap(raw_map, expiry=datetime.timedelta(seconds=4523412)) result = mock_map.copy() assert result is not mock_map - assert isinstance(result, mapping.MRIMutableMapping) + assert isinstance(result, collections.TimedCacheMap) # 158 mediation? # TODO: fix this properly so the cast isn't needed, if we can find out what went wrong? @@ -141,21 +144,21 @@ def test_freeze(self): "blam": (999999999999999999999999, "poke"), "owowo": (999999999999999999999999, "no you"), } - mock_map = mapping.MRIMutableMapping(raw_map, expiry=datetime.timedelta(seconds=6523423)) + mock_map = collections.TimedCacheMap(raw_map, expiry=datetime.timedelta(seconds=6523423)) result = mock_map.freeze() assert result == {"bash": "gtuutueu", "blam": "poke", "owowo": "no you"} - assert isinstance(result, mapping._FrozenMRIMapping) + assert isinstance(result, collections._FrozenDict) def test___delitem__(self): - mock_map = mapping.MRIMutableMapping(expiry=datetime.timedelta(seconds=100)) + mock_map = collections.TimedCacheMap(expiry=datetime.timedelta(seconds=100)) mock_map.update({"ok": "no", "ayanami": "rei qt"}) del mock_map["ok"] assert mock_map == {"ayanami": "rei qt"} @pytest.mark.asyncio async def test___delitem___garbage_collection(self): - mock_map = mapping.MRIMutableMapping( + mock_map = collections.TimedCacheMap( expiry=datetime.timedelta(seconds=hikari_test_helpers.REASONABLE_QUICK_RESPONSE_TIME * 3) ) mock_map.update({"nyaa": "see", "awwo": "awoo2"}) @@ -168,35 +171,35 @@ async def test___delitem___garbage_collection(self): assert mock_map == {"rei": "aww"} def test___getitem___for_valid_entry(self): - mock_map = mapping.MRIMutableMapping(expiry=datetime.timedelta(seconds=100)) + mock_map = collections.TimedCacheMap(expiry=datetime.timedelta(seconds=100)) mock_map["OK"] = 42 mock_map["blam"] = 8 assert mock_map["OK"] == 42 def test___getitem___for_unknown_entry(self): - mock_map = mapping.MRIMutableMapping(expiry=datetime.timedelta(seconds=100)) + mock_map = collections.TimedCacheMap(expiry=datetime.timedelta(seconds=100)) mock_map["blam"] = 8 with pytest.raises(KeyError): mock_map["OK"] def test___iter__(self): - mock_map = mapping.MRIMutableMapping(expiry=datetime.timedelta(seconds=100)) + mock_map = collections.TimedCacheMap(expiry=datetime.timedelta(seconds=100)) mock_map.update({"o": "k", "k": "o", "awoo": "blam", "hikari": "rei"}) assert list(mock_map) == ["o", "k", "awoo", "hikari"] def test___len__(self): - mock_map = mapping.MRIMutableMapping(expiry=datetime.timedelta(seconds=100)) + mock_map = collections.TimedCacheMap(expiry=datetime.timedelta(seconds=100)) mock_map.update({"o": "k", "boop": "bop", "k": "o", "awoo": "blam", "rei": "cute", "hikari": "rei"}) assert len(mock_map) == 6 def test___setitem__(self): - mock_map = mapping.MRIMutableMapping(expiry=datetime.timedelta(seconds=100)) + mock_map = collections.TimedCacheMap(expiry=datetime.timedelta(seconds=100)) mock_map["blat"] = 42 assert mock_map == {"blat": 42} def test___setitem___removes_old_entry_instead_of_replacing(self): - mock_map = mapping.MRIMutableMapping( + mock_map = collections.TimedCacheMap( { "ok": (time.perf_counter() + 50, "no"), "bar": (time.perf_counter() + 60, "bat"), @@ -212,7 +215,7 @@ def test___setitem___removes_old_entry_instead_of_replacing(self): @pytest.mark.skip("flaky test, might fail on Windows runners.") @pytest.mark.asyncio async def test___setitem___garbage_collection(self): - mock_map = mapping.MRIMutableMapping( + mock_map = collections.TimedCacheMap( expiry=datetime.timedelta(seconds=hikari_test_helpers.REASONABLE_QUICK_RESPONSE_TIME * 3) ) mock_map.update({"OK": "no", "blam": "booga"}) @@ -225,88 +228,254 @@ async def test___setitem___garbage_collection(self): assert mock_map == {"ayanami": "rei", "owo": "awoo", "nyaa": "qt"} -class TestCMRIMutableMapping: +class TestLimitedCapacityCacheMap: def test___init___with_source(self): raw_map = {"voo": "doo", "blam": "blast", "foo": "bye"} - mock_map = mapping.CMRIMutableMapping(raw_map, limit=2) + mock_map = collections.LimitedCapacityCacheMap(raw_map, limit=2) assert mock_map == {"blam": "blast", "foo": "bye"} def test_copy(self): - mock_map = mapping.CMRIMutableMapping({"o": "n", "b": "a", "a": "v"}, limit=42) + mock_map = collections.LimitedCapacityCacheMap({"o": "n", "b": "a", "a": "v"}, limit=42) result = mock_map.copy() assert result is not mock_map - assert isinstance(result, mapping.CMRIMutableMapping) + assert isinstance(result, collections.LimitedCapacityCacheMap) assert result == {"o": "n", "b": "a", "a": "v"} def test_freeze(self): - mock_map = mapping.CMRIMutableMapping({"o": "no", "good": "bye"}, limit=5) + mock_map = collections.LimitedCapacityCacheMap({"o": "no", "good": "bye"}, limit=5) result = mock_map.freeze() assert isinstance(result, dict) assert result == {"o": "no", "good": "bye"} def test___delitem___for_existing_entry(self): - mock_map = mapping.CMRIMutableMapping(limit=50) + mock_map = collections.LimitedCapacityCacheMap(limit=50) mock_map["Ok"] = 42 del mock_map["Ok"] assert "Ok" not in mock_map def test___delitem___for_non_existing_entry(self): - mock_map = mapping.CMRIMutableMapping(limit=50) + mock_map = collections.LimitedCapacityCacheMap(limit=50) with pytest.raises(KeyError): del mock_map["Blam"] def test___getitem___for_existing_entry(self): - mock_map = mapping.CMRIMutableMapping(limit=50) + mock_map = collections.LimitedCapacityCacheMap(limit=50) mock_map["blat"] = 42 assert mock_map["blat"] == 42 def test___getitem___for_non_existing_entry(self): - mock_map = mapping.CMRIMutableMapping(limit=50) + mock_map = collections.LimitedCapacityCacheMap(limit=50) with pytest.raises(KeyError): mock_map["CIA"] def test___iter___(self): - mock_map = mapping.CMRIMutableMapping(limit=50) + mock_map = collections.LimitedCapacityCacheMap(limit=50) mock_map.update({"OK": "blam", "blaaa": "neoeo", "neon": "genesis", "evangelion": None}) assert list(mock_map) == ["OK", "blaaa", "neon", "evangelion"] def test___len___(self): - mock_map = mapping.CMRIMutableMapping(limit=50) + mock_map = collections.LimitedCapacityCacheMap(limit=50) mock_map.update({"ooga": "blam", "blaaa": "neoeo", "the": "boys", "neon": "genesis", "evangelion": None}) assert len(mock_map) == 5 def test___setitem___when_limit_not_reached(self): - mock_map = mapping.CMRIMutableMapping(limit=50) + mock_map = collections.LimitedCapacityCacheMap(limit=50) mock_map["OK"] = 523 mock_map["blam"] = 512387 mock_map.update({"bll": "no", "ieiei": "lslsl"}) assert mock_map == {"OK": 523, "blam": 512387, "bll": "no", "ieiei": "lslsl"} def test___setitem___when_limit_reached(self): - mock_map = mapping.CMRIMutableMapping(limit=4) + mock_map = collections.LimitedCapacityCacheMap(limit=4) mock_map.update({"bll": "no", "ieiei": "lslsl", "pacify": "me", "qt": "pie"}) mock_map["eva"] = "Rei" mock_map.update({"shinji": "ikari"}) assert mock_map == {"pacify": "me", "qt": "pie", "eva": "Rei", "shinji": "ikari"} +class TestSnowflakeSet: + def test_init_creates_empty_array(self): + # given + array = collections.SnowflakeSet()._ids + # then + assert len(array) == 0, "not empty" + assert isinstance(array, array_.array) + + def test_init_creates_array(self): + # given + array = collections.SnowflakeSet(9, 8, 7, 8)._ids + # then + assert len(array) == 3, "wrong size" + assert isinstance(array, array_.array) + assert array.tolist() == [7, 8, 9] + + @pytest.mark.parametrize( + ("start_with", "add_items", "expect"), + [ + ([], [123], [123]), + ([123], [123], [123]), + ([123], [122], [122, 123]), + ([123], [124], [123, 124]), + ([0, 99999], [124], [0, 124, 99999]), + ([0, 124, 99999], [124], [0, 124, 99999]), + ([0, 122, 124, 99999], [122], [0, 122, 124, 99999]), + ([0, 122, 124, 99999], [0, 123, 121], [0, 121, 122, 123, 124, 99999]), + ([0, 122], [123, 121, 999991, 121, 121, 124, 120], [0, 120, 121, 122, 123, 124, 999991]), + ], + ) + def test_add_inserts_items(self, start_with, add_items, expect): + # given + sfs = collections.SnowflakeSet() + sfs._ids.extend(start_with) + # when + for item in add_items: + sfs.add(item) + # then + assert sfs._ids.tolist() == expect + + @pytest.mark.parametrize( + ("start_with", "add_items", "expect"), + [ + ([], [123], [123]), + ([123], [123], [123]), + ([123], [122], [122, 123]), + ([123], [124], [123, 124]), + ([0, 99999], [124], [0, 124, 99999]), + ([0, 124, 99999], [124], [0, 124, 99999]), + ([0, 122, 124, 99999], [122], [0, 122, 124, 99999]), + ([0, 122, 124, 99999], [0, 123, 121], [0, 121, 122, 123, 124, 99999]), + ([0, 122], [123, 121, 999991, 121, 121, 124, 120], [0, 120, 121, 122, 123, 124, 999991]), + ], + ) + def test_add_all_inserts_items(self, start_with, add_items, expect): + # given + sfs = collections.SnowflakeSet() + sfs._ids.extend(start_with) + # when + sfs.add_all(add_items) + # then + assert sfs._ids.tolist() == expect + + def test_clear_empties_buffer(self): + # given + sfs = collections.SnowflakeSet(123, 121, 999991, 121, 121, 124, 120) + assert len(sfs._ids) != 0 + # when + sfs.clear() + # then + assert len(sfs._ids) == 0 + + def test_clear_empties_empty_buffer(self): + # given + sfs = collections.SnowflakeSet() + assert len(sfs._ids) == 0 + # when + sfs.clear() + # then + assert len(sfs._ids) == 0 + + @pytest.mark.parametrize( + ("start_with", "discard", "expect"), + [ + ([], [1], []), + ([1], [1], []), + ([0], [1], [0]), + ([0, 1], [1], [0]), + ([0, 1], [0], [1]), + ([9, 18, 27, 36, 45, 54, 63], [18, 27, 18, 18, 36, 64, 63], [9, 45, 54]), + ], + ) + def test_discard(self, start_with, discard, expect): + # given + sfs = collections.SnowflakeSet() + sfs._ids.extend(start_with) + # when + for item in discard: + sfs.discard(item) + # then + assert sfs._ids.tolist() == expect + + @pytest.mark.parametrize( + ("start_with", "look_for", "expect"), + [ + ([], 12, False), + ([12], 12, True), + ([9, 18, 27, 36, 45, 54, 63], 9, True), + ([9, 18, 27, 36, 45, 54, 63], 18, True), + ([9, 18, 27, 36, 45, 54, 63], 27, True), + ([9, 18, 27, 36, 45, 54, 63], 36, True), + ([9, 18, 27, 36, 45, 54, 63], 45, True), + ([9, 18, 27, 36, 45, 54, 63], 54, True), + ([9, 18, 27, 36, 45, 54, 63], 62, False), + ([9, 18, 27, 36, 45, 54, 63], 63, True), + ([9, 18, 27, 36, 45, 54, 63], 64, False), + ([9, 18, 27, 36, 45, 54, 63], 72, False), + ], + ) + def test_contains(self, start_with, look_for, expect): + # given + sfs = collections.SnowflakeSet() + sfs._ids.extend(start_with) + # then + assert (look_for in sfs) is expect + + def test_iter(self): + # given + sfs = collections.SnowflakeSet() + sfs._ids.extend([9, 18, 27, 36, 45, 54, 63]) + # then + assert list(iter(sfs)) == [9, 18, 27, 36, 45, 54, 63] + assert list(sfs) == [9, 18, 27, 36, 45, 54, 63] + + @pytest.mark.parametrize("items", [*range(0, 10)]) + def test_len(self, items): + # given + sfs = collections.SnowflakeSet() + sfs._ids.extend(i for i in range(items)) + # then + assert len(sfs) == items + + @pytest.mark.parametrize("items", [*range(0, 10)]) + def test_length_hint(self, items): + # given + sfs = collections.SnowflakeSet() + sfs._ids.extend(i for i in range(items)) + # then + assert operator.length_hint(sfs) == items + + def test_repr(self): + sfs = collections.SnowflakeSet() + sfs._ids.extend([1, 3, 5, 7, 8]) + assert repr(sfs) == "SnowflakeSet(1, 3, 5, 7, 8)" + + def test_str(self): + sfs = collections.SnowflakeSet() + sfs._ids.extend([1, 3, 5, 7, 8]) + assert str(sfs) == "{1, 3, 5, 7, 8}" + + def test_sizeof(self): + sfs = collections.SnowflakeSet() + sfs._ids.extend([1, 3, 5, 7, 8]) + assert sfs.__sizeof__() == object.__sizeof__(sfs) + sys.getsizeof(sfs._ids) + + def test_get_index_or_slice_with_index_within_range(): - result = mapping.get_index_or_slice({"i": "e", "n": "o", "b": "a", "hikari": "Rei", "p": "a", "o": "o"}, 3) + result = collections.get_index_or_slice({"i": "e", "n": "o", "b": "a", "hikari": "Rei", "p": "a", "o": "o"}, 3) assert result == "Rei" def test_get_index_or_slice_with_index_outside_range(): with pytest.raises(IndexError): - mapping.get_index_or_slice({"i": "e", "n": "o", "b": "a", "hikari": "noa"}, 77) + collections.get_index_or_slice({"i": "e", "n": "o", "b": "a", "hikari": "noa"}, 77) def test_get_index_or_slice_with_slice(): test_map = {"o": "b", "b": "o", "a": "m", "arara": "blam", "oof": "no", "rika": "may"} - assert mapping.get_index_or_slice(test_map, slice(1, 5, 2)) == ("o", "blam") + assert collections.get_index_or_slice(test_map, slice(1, 5, 2)) == ("o", "blam") def test_get_index_or_slice_with_invalid_type(): with pytest.raises(TypeError): - mapping.get_index_or_slice({}, object()) + collections.get_index_or_slice({}, object()) diff --git a/tests/hikari/utilities/test_ux.py b/tests/hikari/utilities/test_ux.py index 39fbf70548..e6230d13b4 100644 --- a/tests/hikari/utilities/test_ux.py +++ b/tests/hikari/utilities/test_ux.py @@ -259,11 +259,13 @@ def test_when_plat_is_Pocket_PC(self): with stack: assert ux.supports_color(True, False) is False - assert getenv.call_count == 2 - getenv.assert_has_calls([mock.call("CLICOLOR_FORCE", "0"), mock.call("CLICOLOR", "0")]) + assert getenv.call_count == 3 + getenv.assert_has_calls( + [mock.call("CLICOLOR_FORCE", "0"), mock.call("CLICOLOR", "0"), mock.call("COLORTERM", "")] + ) @pytest.mark.parametrize( - ("term_program", "asicon", "isatty", "expected"), + ("term_program", "ansicon", "isatty", "expected"), [ ("mintty", False, True, True), ("Terminus", False, True, True), @@ -274,21 +276,22 @@ def test_when_plat_is_Pocket_PC(self): ("Terminus", True, False, False), ], ) - def test_when_plat_is_win32(self, term_program, asicon, isatty, expected): + def test_when_plat_is_win32(self, term_program, ansicon, isatty, expected): stack = contextlib.ExitStack() - getenv = stack.enter_context(mock.patch.object(os, "getenv", side_effect=["0", "0", term_program, ""])) + getenv = stack.enter_context(mock.patch.object(os, "getenv", side_effect=["0", "0", "", term_program, ""])) stack.enter_context(mock.patch.object(sys.stdout, "isatty", return_value=isatty)) stack.enter_context(mock.patch.object(sys, "platform", new="win32")) - stack.enter_context(mock.patch.object(os, "environ", new=["ANSICON"] if asicon else [])) + stack.enter_context(mock.patch.object(os, "environ", new=["ANSICON"] if ansicon else [])) with stack: assert ux.supports_color(True, False) is expected - assert getenv.call_count == 4 + assert getenv.call_count == 5 getenv.assert_has_calls( [ mock.call("CLICOLOR_FORCE", "0"), mock.call("CLICOLOR", "0"), + mock.call("COLORTERM", ""), mock.call("TERM_PROGRAM", None), mock.call("PYCHARM_HOSTED", ""), ] @@ -297,16 +300,21 @@ def test_when_plat_is_win32(self, term_program, asicon, isatty, expected): @pytest.mark.parametrize("isatty", [True, False]) def test_when_plat_is_not_win32(self, isatty): stack = contextlib.ExitStack() - getenv = stack.enter_context(mock.patch.object(os, "getenv", side_effect=["0", "0", ""])) + getenv = stack.enter_context(mock.patch.object(os, "getenv", side_effect=["0", "0", "", ""])) stack.enter_context(mock.patch.object(sys.stdout, "isatty", return_value=isatty)) stack.enter_context(mock.patch.object(sys, "platform", new="linux")) with stack: assert ux.supports_color(True, False) is isatty - assert getenv.call_count == 3 + assert getenv.call_count == 4 getenv.assert_has_calls( - [mock.call("CLICOLOR_FORCE", "0"), mock.call("CLICOLOR", "0"), mock.call("PYCHARM_HOSTED", "")] + [ + mock.call("CLICOLOR_FORCE", "0"), + mock.call("CLICOLOR", "0"), + mock.call("COLORTERM", ""), + mock.call("PYCHARM_HOSTED", ""), + ] ) @pytest.mark.parametrize("isatty", [True, False]) @@ -321,19 +329,25 @@ def test_when_PYCHARM_HOSTED(self, isatty, plat): assert ux.supports_color(True, False) is True if plat == "win32": - assert getenv.call_count == 4 + assert getenv.call_count == 5 getenv.assert_has_calls( [ mock.call("CLICOLOR_FORCE", "0"), mock.call("CLICOLOR", "0"), + mock.call("COLORTERM", ""), mock.call("TERM_PROGRAM", None), mock.call("PYCHARM_HOSTED", ""), ] ) else: - assert getenv.call_count == 3 + assert getenv.call_count == 4 getenv.assert_has_calls( - [mock.call("CLICOLOR_FORCE", "0"), mock.call("CLICOLOR", "0"), mock.call("PYCHARM_HOSTED", "")] + [ + mock.call("CLICOLOR_FORCE", "0"), + mock.call("CLICOLOR", "0"), + mock.call("COLORTERM", ""), + mock.call("PYCHARM_HOSTED", ""), + ] )