Repository: hikari-py/hikari
Pull request #660 (2021-06-12 22:16:13)

introduce REST filled DM Channel ID cache resource

### Summary
introduce REST filled DM Channel ID cache resource
* Remove caching DM channel objects on user objects in favour of new cache resource

### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [x] I have run `nox` and all the pipelines have passed.
- [x] I have made unittests according to the code I have added/modified/deleted.

### Related issues
<!--
To mention an issue use `#issue-id` and to mention a merge request use `!merge-request-id`
To close/fix an issue use `Close #issue-id` or `Fix #issue-id` (depending on the merge request)
-->


FasterSpeeding: > Looks good, I'm just not so sure if a REST based cache is the way to go. This leaves a lot of open questions, like, should we also be caching channels, emojis, guilds, etc

Except A you're already caching EST data just in a kinda bad way which leads to stale data being returned and doesn't have any formalised interface for it.

B how the cache functions around removing entries is implementation detail and cacheing based on REST would only work for resource implemention which aren't reliant on delete events to remove resources from it (which only covers messages, users and dm channel ids to the standard implementation ) 


davfsa: Sounds good then. just the little comment I made with the review

Patch:
diff --git a/hikari/api/cache.py b/hikari/api/cache.py
index 013fe6a8..d86cd989 100644
--- a/hikari/api/cache.py
+++ b/hikari/api/cache.py
@@ -78,6 +78,34 @@ class Cache(abc.ABC):
 
     __slots__: typing.Sequence[str] = ()
 
+    @abc.abstractmethod
+    def get_dm_channel_id(
+        self, user: snowflakes.SnowflakeishOr[users.PartialUser], /
+    ) -> typing.Optional[snowflakes.Snowflake]:
+        """Get the DM channel ID for a user.
+
+        Parameters
+        ----------
+        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
+            Objet or ID of the user to get the DM channel ID for.
+
+        Returns
+        -------
+        typing.Optional[hikari.snowflakes.Snowflake]
+            ID of the DM channel which was found cached for the supplied user or
+            `builtins.None`.
+        """
+
+    @abc.abstractmethod
+    def get_dm_channel_ids_view(self) -> CacheView[snowflakes.Snowflake, snowflakes.Snowflake]:
+        """Get a view of the cached DM channel IDs.
+
+        Returns
+        -------
+        CacheView[hikari.snowflakes.Snowflake, hikari.snowflakes.Snowflake]
+            Cache view of user IDs to DM channel IDs.
+        """
+
     @abc.abstractmethod
     def get_emoji(
         self, emoji: snowflakes.SnowflakeishOr[emojis.CustomEmoji], /
@@ -638,6 +666,52 @@ class MutableCache(Cache, abc.ABC):
     def clear(self) -> None:
         """Clear the full cache."""
 
+    @abc.abstractmethod
+    def clear_dm_channel_ids(self) -> CacheView[snowflakes.Snowflake, snowflakes.Snowflake]:
+        """Remove all the cached DM channel IDs.
+
+        Returns
+        -------
+        CacheView[hikari.snowflakes.Snowflake, hikari.snowflakes.Snowflake]
+            Cache view of user IDs to DM channel IDs which were cleared from
+            the cache.
+        """
+
+    @abc.abstractmethod
+    def delete_dm_channel_id(
+        self, user: snowflakes.SnowflakeishOr[users.PartialUser], /
+    ) -> typing.Optional[snowflakes.Snowflake]:
+        """Remove a DM channel ID from the cache.
+
+        Parameters
+        ----------
+        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
+            Object or ID of the user to remove the cached DM channel ID for.
+
+        Returns
+        -------
+        typing.Optional[hikari.snowflakes.Snowflake]
+            The DM channel ID which was removed from the cache if found, else
+            `builtins.None`.
+        """
+
+    @abc.abstractmethod
+    def set_dm_channel_id(
+        self,
+        user: snowflakes.SnowflakeishOr[users.PartialUser],
+        channel: snowflakes.SnowflakeishOr[channels.PartialChannel],
+        /,
+    ) -> None:
+        """Add a DM channel ID to the cache.
+
+        Parameters
+        ----------
+        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
+            Object or ID of the user to add a DM channel ID to the cache for.
+        channel : hikari.snowflakes.SnowflakeishOr[hikari.channels.PartialChannel]
+            Object or ID of the DM channel to add to the cache.
+        """
+
     @abc.abstractmethod
     def clear_emojis(self) -> CacheView[snowflakes.Snowflake, emojis.KnownCustomEmoji]:
         """Remove all the known custom emoji objects from the cache.
diff --git a/hikari/applications.py b/hikari/applications.py
index 96fd149a..3596842c 100644
--- a/hikari/applications.py
+++ b/hikari/applications.py
@@ -59,7 +59,6 @@
 if typing.TYPE_CHECKING:
     import datetime
 
-    from hikari import channels
     from hikari import files
     from hikari import permissions as permissions_
     from hikari import traits
@@ -347,10 +346,6 @@ def flags(self) -> users.UserFlag:
     def id(self) -> snowflakes.Snowflake:
         return self.user.id
 
-    @id.setter
-    def id(self, value: snowflakes.Snowflake) -> typing.NoReturn:
-        raise TypeError("Cannot mutate the ID of a member")
-
     @property
     def is_bot(self) -> bool:
         return self.user.is_bot
@@ -367,9 +362,6 @@ def mention(self) -> str:
     def username(self) -> str:
         return self.user.username
 
-    async def fetch_dm_channel(self) -> channels.DMChannel:
-        return await self.user.fetch_dm_channel()
-
     def __str__(self) -> str:
         return str(self.user)
 
diff --git a/hikari/channels.py b/hikari/channels.py
index 4a8dfc65..d0c748fd 100644
--- a/hikari/channels.py
+++ b/hikari/channels.py
@@ -734,12 +734,8 @@ def shard_id(self) -> typing.Optional[int]:
 
         This may be `builtins.None` if the shard count is not known.
         """
-        try:
-            shard_count = getattr(self.app, "shard_count")
-            assert isinstance(shard_count, int), f"shard_count attr was expected to be int, but got {shard_count}"
-            return snowflakes.calculate_shard_id(shard_count, self.guild_id)
-        except (TypeError, AttributeError, NameError):
-            pass
+        if isinstance(self.app, traits.ShardAware):
+            return snowflakes.calculate_shard_id(self.app, self.guild_id)
 
         return None
 
diff --git a/hikari/config.py b/hikari/config.py
index 25ab27a8..c57d3269 100644
--- a/hikari/config.py
+++ b/hikari/config.py
@@ -428,7 +428,21 @@ class CacheComponents(enums.Flag):
     MESSAGES = 1 << 8
     """Enables the messages cache."""
 
-    ALL = GUILDS | GUILD_CHANNELS | MEMBERS | ROLES | INVITES | EMOJIS | PRESENCES | VOICE_STATES | MESSAGES
+    DM_CHANNEL_IDS = 1 << 10
+    """Enables the DM channel IDs cache."""
+
+    ALL = (
+        GUILDS
+        | GUILD_CHANNELS
+        | MEMBERS
+        | ROLES
+        | INVITES
+        | EMOJIS
+        | PRESENCES
+        | VOICE_STATES
+        | MESSAGES
+        | DM_CHANNEL_IDS
+    )
     """Fully enables the cache."""
 
 
diff --git a/hikari/embeds.py b/hikari/embeds.py
index 1c6f587d..c8a6a3ee 100644
--- a/hikari/embeds.py
+++ b/hikari/embeds.py
@@ -733,13 +733,8 @@ def set_author(
         if name is None and url is None and icon is None:
             self._author = None
         else:
-            self._author = EmbedAuthor()
-            self._author.name = name
-            self._author.url = url
-            if icon is not None:
-                self._author.icon = EmbedResourceWithProxy(resource=files.ensure_resource(icon))
-            else:
-                self._author.icon = None
+            real_icon = EmbedResourceWithProxy(resource=files.ensure_resource(icon)) if icon is not None else None
+            self._author = EmbedAuthor(name=name, url=url, icon=real_icon)
         return self
 
     def set_footer(self, *, text: typing.Optional[str], icon: typing.Optional[files.Resourceish] = None) -> Embed:
@@ -787,12 +782,8 @@ def set_footer(self, *, text: typing.Optional[str], icon: typing.Optional[files.
 
             self._footer = None
         else:
-            self._footer = EmbedFooter()
-            self._footer.text = text
-            if icon is not None:
-                self._footer.icon = EmbedResourceWithProxy(resource=files.ensure_resource(icon))
-            else:
-                self._footer.icon = None
+            real_icon = EmbedResourceWithProxy(resource=files.ensure_resource(icon)) if icon is not None else None
+            self._footer = EmbedFooter(icon=real_icon, text=text)
         return self
 
     def set_image(self, image: typing.Optional[files.Resourceish] = None, /) -> Embed:
diff --git a/hikari/guilds.py b/hikari/guilds.py
index 6a839647..c5c84638 100644
--- a/hikari/guilds.py
+++ b/hikari/guilds.py
@@ -383,10 +383,6 @@ def flags(self) -> users.UserFlag:
     def id(self) -> snowflakes.Snowflake:
         return self.user.id
 
-    @id.setter
-    def id(self, value: snowflakes.Snowflake) -> None:
-        raise TypeError("Cannot mutate the ID of a member")
-
     @property
     def is_bot(self) -> bool:
         return self.user.is_bot
@@ -510,9 +506,6 @@ async def fetch_self(self) -> Member:
         """
         return await self.user.app.rest.fetch_member(self.guild_id, self.user.id)
 
-    async def fetch_dm_channel(self) -> channels_.DMChannel:
-        return await self.user.fetch_dm_channel()
-
     async def ban(
         self,
         *,
diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py
index 7a67133e..8fcddca2 100644
--- a/hikari/impl/bot.py
+++ b/hikari/impl/bot.py
@@ -277,6 +277,7 @@ def __init__(
 
         # RESTful API.
         self._rest = rest_impl.RESTClientImpl(
+            cache=self._cache,
             entity_factory=self._entity_factory,
             executor=self._executor,
             http_settings=self._http_settings,
diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py
index 4fe5ad87..451b98d0 100644
--- a/hikari/impl/cache.py
+++ b/hikari/impl/cache.py
@@ -67,6 +67,7 @@ class CacheImpl(cache.MutableCache):
 
     __slots__: typing.Sequence[str] = (
         "_app",
+        "_dm_channel_entries",
         "_emoji_entries",
         "_guild_channel_entries",
         "_guild_entries",
@@ -81,6 +82,7 @@ class CacheImpl(cache.MutableCache):
         "_settings",
     )
 
+    _dm_channel_entries: collections.ExtendedMutableMapping[snowflakes.Snowflake, snowflakes.Snowflake]
     # For the sake of keeping things clean, the annotations are being kept separate from the assignment here.
     _me: typing.Optional[users.OwnUser]
     _emoji_entries: collections.ExtendedMutableMapping[snowflakes.Snowflake, cache_utility.KnownCustomEmojiData]
@@ -112,6 +114,7 @@ def settings(self) -> config.CacheSettings:
         return self._settings
 
     def _create_cache(self) -> None:
+        self._dm_channel_entries = collections.FreezableDict()
         self._emoji_entries = collections.FreezableDict()
         self._guild_channel_entries = collections.FreezableDict()
         self._guild_entries = collections.FreezableDict()
@@ -139,6 +142,34 @@ def clear(self) -> None:
 
         self._create_cache()
 
+    def clear_dm_channel_ids(self) -> cache.CacheView[snowflakes.Snowflake, snowflakes.Snowflake]:
+        result = self._dm_channel_entries
+        self._dm_channel_entries = collections.FreezableDict()
+        return cache_utility.CacheMappingView(result)
+
+    def delete_dm_channel_id(
+        self, user: snowflakes.SnowflakeishOr[users.PartialUser], /
+    ) -> typing.Optional[snowflakes.Snowflake]:
+        return self._dm_channel_entries.pop(snowflakes.Snowflake(user), None)
+
+    def get_dm_channel_id(
+        self, user: snowflakes.SnowflakeishOr[users.PartialUser], /
+    ) -> typing.Optional[snowflakes.Snowflake]:
+        return self._dm_channel_entries.get(snowflakes.Snowflake(user))
+
+    def get_dm_channel_ids_view(self) -> cache.CacheView[snowflakes.Snowflake, snowflakes.Snowflake]:
+        return cache_utility.CacheMappingView(self._dm_channel_entries.freeze())
+
+    def set_dm_channel_id(
+        self,
+        user: snowflakes.SnowflakeishOr[users.PartialUser],
+        channel: snowflakes.SnowflakeishOr[channels.PartialChannel],
+        /,
+    ) -> None:
+        user = snowflakes.Snowflake(user)
+        if self._is_cache_enabled_for(config.CacheComponents.EMOJIS) and user in self._user_entries:
+            self._dm_channel_entries[user] = snowflakes.Snowflake(channel)
+
     def _build_emoji(
         self,
         emoji_data: cache_utility.KnownCustomEmojiData,
@@ -1192,6 +1223,7 @@ def _garbage_collect_user(
 
         if self._can_remove_user(user) and user.object.id in self._user_entries:
             del self._user_entries[user.object.id]
+            self._dm_channel_entries.pop(user.object.id, None)
 
     def get_user(self, user: snowflakes.SnowflakeishOr[users.PartialUser], /) -> typing.Optional[users.User]:
         user = self._user_entries.get(snowflakes.Snowflake(user))
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index 85eeb745..a41e4ddf 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -91,6 +91,7 @@
     from hikari import templates
     from hikari import voices
     from hikari import webhooks
+    from hikari.api import cache as cache_api
     from hikari.api import entity_factory as entity_factory_
 
 _LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.rest")
@@ -342,6 +343,7 @@ def acquire(
         entity_factory = entity_factory_impl.EntityFactoryImpl(provider)
 
         rest_client = RESTClientImpl(
+            cache=None,
             entity_factory=entity_factory,
             executor=self._executor,
             http_settings=self._http_settings,
@@ -390,6 +392,7 @@ class RESTClientImpl(rest_api.RESTClient):
     __slots__: typing.Sequence[str] = (
         "buckets",
         "global_rate_limit",
+        "_cache",
         "_client_session",
         "_closed_event",
         "_tcp_connector",
@@ -414,6 +417,7 @@ class _RetryRequest(RuntimeError):
     def __init__(
         self,
         *,
+        cache: typing.Optional[cache_api.MutableCache],
         entity_factory: entity_factory_.EntityFactory,
         executor: typing.Optional[concurrent.futures.Executor],
         http_settings: config.HTTPSettings,
@@ -427,6 +431,7 @@ def __init__(
         # We've been told in DAPI that this is per token.
         self.global_rate_limit = rate_limits.ManualRateLimiter()
 
+        self._cache = cache
         self._client_session: typing.Optional[aiohttp.ClientSession] = None
         self._tcp_connector: typing.Optional[aiohttp.TCPConnector] = None
         self._closed_event = asyncio.Event()
@@ -791,7 +796,12 @@ async def fetch_channel(
         route = routes.GET_CHANNEL.compile(channel=channel)
         response = await self._request(route)
         assert isinstance(response, dict)
-        return self._entity_factory.deserialize_channel(response)
+        result = self._entity_factory.deserialize_channel(response)
+
+        if self._cache and isinstance(result, channels_.DMChannel):
+            self._cache.set_dm_channel_id(result.recipient.id, result.id)
+
+        return result
 
     async def edit_channel(
         self,
@@ -1827,7 +1837,12 @@ async def create_dm_channel(self, user: snowflakes.SnowflakeishOr[users.PartialU
         body.put_snowflake("recipient_id", user)
         response = await self._request(route, json=body)
         assert isinstance(response, dict)
-        return self._entity_factory.deserialize_dm(response)
+        channel = self._entity_factory.deserialize_dm(response)
+
+        if self._cache:
+            self._cache.set_dm_channel_id(user, channel.id)
+
+        return channel
 
     async def fetch_application(self) -> applications.Application:
         route = routes.GET_MY_APPLICATION.compile()
diff --git a/hikari/internal/cache.py b/hikari/internal/cache.py
index 7e6b0156..fd5f4ea3 100644
--- a/hikari/internal/cache.py
+++ b/hikari/internal/cache.py
@@ -983,7 +983,7 @@ def copy_guild_channel(channel: ChannelT) -> ChannelT:
 
 
 class Cache3DMappingView(CacheMappingView[snowflakes.Snowflake, cache.CacheView[KeyT, ValueT]]):
-    """A special case of the Mapping View which avoids copying the already immutable views contained within it."""
+    """A special case of the Mapping View which avoids copying the immutable values contained within it."""
 
     __slots__: typing.Sequence[str] = ()
 
diff --git a/hikari/snowflakes.py b/hikari/snowflakes.py
index 19d90c1b..e26bc928 100644
--- a/hikari/snowflakes.py
+++ b/hikari/snowflakes.py
@@ -124,11 +124,6 @@ def id(self) -> Snowflake:
             The snowflake ID of this object.
         """
 
-    # TODO: make immutable interface, as this is a major risk to consistent hash codes.
-    @id.setter
-    def id(self, value: Snowflake) -> None:
-        """Set the ID on this entity."""
-
     @property
     def created_at(self) -> datetime.datetime:
         """When the object was created."""
diff --git a/hikari/users.py b/hikari/users.py
index af655589..5f8b65c2 100644
--- a/hikari/users.py
+++ b/hikari/users.py
@@ -32,6 +32,7 @@
 import attr
 
 from hikari import snowflakes
+from hikari import traits
 from hikari import undefined
 from hikari import urls
 from hikari.internal import attr_extensions
@@ -44,7 +45,6 @@
     from hikari import files
     from hikari import guilds
     from hikari import messages
-    from hikari import traits
 
 
 @typing.final
@@ -180,7 +180,6 @@ def mention(self) -> str:
             The mention string to use.
         """
 
-    @abc.abstractmethod
     async def fetch_dm_channel(self) -> channels.DMChannel:
         """Fetch the DM channel for this user.
 
@@ -209,6 +208,7 @@ async def fetch_dm_channel(self) -> channels.DMChannel:
         hikari.errors.InternalServerError
             If an internal error occurs on Discord while handling the request.
         """
+        return await self.app.rest.create_dm_channel(self.id)
 
     async def fetch_self(self) -> User:
         """Get this user's up-to-date object by performing an API call.
@@ -390,10 +390,15 @@ async def send(
         hikari.errors.InternalServerError
             If an internal error occurs on Discord while handling the request.
         """  # noqa: E501 - Line too long
-        channel = await self.fetch_dm_channel()
+        channel_id = None
+        if isinstance(self.app, traits.CacheAware):
+            channel_id = self.app.cache.get_dm_channel_id(self.id)
+
+        if channel_id is None:
+            channel_id = (await self.fetch_dm_channel()).id
 
         return await self.app.rest.create_message(
-            channel=channel.id,
+            channel=channel_id,
             content=content,
             embed=embed,
             attachment=attachment,
@@ -573,16 +578,6 @@ class PartialUserImpl(PartialUser):
     flags: undefined.UndefinedOr[UserFlag] = attr.field(eq=False, hash=False)
     """Public flags for this user."""
 
-    _dm_channel: typing.Optional[channels.DMChannel] = attr.field(
-        eq=False, hash=False, init=False, repr=False, default=None
-    )
-
-    async def fetch_dm_channel(self) -> channels.DMChannel:
-        if self._dm_channel is None:
-            self._dm_channel = await self.app.rest.create_dm_channel(self.id)
-
-        return self._dm_channel
-
     @property
     def mention(self) -> str:
         """Return a raw mention string for the given user.


Test patch:
diff --git a/tests/hikari/impl/test_bot.py b/tests/hikari/impl/test_bot.py
index 23bf67c1..6c1a1a00 100644
--- a/tests/hikari/impl/test_bot.py
+++ b/tests/hikari/impl/test_bot.py
@@ -162,6 +162,7 @@ def test_init(self):
         voice.assert_called_once_with(bot)
         assert bot._rest is rest.return_value
         rest.assert_called_once_with(
+            cache=bot._cache,
             entity_factory=bot._entity_factory,
             executor=executor,
             http_settings=bot._http_settings,
diff --git a/tests/hikari/impl/test_cache.py b/tests/hikari/impl/test_cache.py
index 02809ab6..c59ca1cf 100644
--- a/tests/hikari/impl/test_cache.py
+++ b/tests/hikari/impl/test_cache.py
@@ -83,6 +83,64 @@ def test_clear(self, cache_impl):
 
         cache_impl._create_cache.assert_called_once_with()
 
+    def test_clear_dm_channel_ids(self, cache_impl):
+        cache_impl._dm_channel_entries = collections.FreezableDict({123: 5423, 23123: 54123123})
+
+        result = cache_impl.clear_dm_channel_ids()
+
+        assert result == {123: 5423, 23123: 54123123}
+        assert cache_impl._dm_channel_entries == {}
+
+    def test_delete_dm_channel_id(self, cache_impl):
+        cache_impl._dm_channel_entries = collections.FreezableDict({54123: 2123123, 5434: 1234})
+
+        result = cache_impl.delete_dm_channel_id(54123)
+
+        assert result == 2123123
+        assert cache_impl._dm_channel_entries == {5434: 1234}
+
+    def test_delete_dm_channel_id_for_unknown_user(self, cache_impl):
+        cache_impl._dm_channel_entries = collections.FreezableDict({54123: 2123123, 5434: 1234})
+
+        result = cache_impl.delete_dm_channel_id(65234123123)
+
+        assert result is None
+        assert cache_impl._dm_channel_entries == {54123: 2123123, 5434: 1234}
+
+    def test_get_dm_channel_id(self, cache_impl):
+        cache_impl._dm_channel_entries = collections.FreezableDict({24123123: 453123, 5423: 123, 653: 1223})
+
+        assert cache_impl.get_dm_channel_id(5423) == 123
+
+    def test_get_dm_channel_id_for_unknown_user(self, cache_impl):
+        cache_impl._dm_channel_entries = collections.FreezableDict({24123123: 453123, 5423: 123, 653: 1223})
+
+        assert cache_impl.get_dm_channel_id(65656565) is None
+
+    def test_get_dm_channel_ids_view(self, cache_impl):
+        cache_impl._dm_channel_entries = collections.FreezableDict({222: 333, 643: 213, 54234: 1231321})
+
+        assert cache_impl.get_dm_channel_ids_view() == {222: 333, 643: 213, 54234: 1231321}
+
+    def test_set_dm_channel_id(self, cache_impl):
+        cache_impl._user_entries = collections.FreezableDict({43123123: object()})
+
+        cache_impl.set_dm_channel_id(StubModel(43123123), StubModel(12222))
+
+        assert cache_impl._dm_channel_entries == {43123123: 12222}
+
+    def test_set_dm_channel_id_when_user_not_known(self, cache_impl):
+        cache_impl.set_dm_channel_id(StubModel(43123123), StubModel(12222))
+
+        assert cache_impl._dm_channel_entries == {}
+
+    def test_set_dm_channel_id_when_not_enabled(self, cache_impl):
+        cache_impl._user_entries = collections.FreezableDict({43123123: object()})
+        cache_impl._settings.components = 0
+        cache_impl.set_dm_channel_id(StubModel(43123123), StubModel(12222))
+
+        assert cache_impl._dm_channel_entries == {}
+
     def test__build_emoji(self, cache_impl):
         mock_user = mock.MagicMock(users.User)
         emoji_data = cache_utilities.KnownCustomEmojiData(
@@ -1938,7 +1996,23 @@ def test__garbage_collect_user_for_known_unreferenced_user(self, cache_impl):
 
         cache_impl._garbage_collect_user(mock_user, decrement=1)
 
-        assert dict(cache_impl._user_entries) == {snowflakes.Snowflake(645234): mock_other_user}
+        assert cache_impl._user_entries == {snowflakes.Snowflake(645234): mock_other_user}
+
+    def test__garbage_collect_user_for_known_unreferenced_user_removes_cached_dm_channelo(self, cache_impl):
+        mock_user = cache_utilities.RefCell(mock.Mock(id=snowflakes.Snowflake(21231234)), ref_count=1)
+        cache_impl._dm_channel_entries = collections.FreezableDict({21231234: 123123123})
+        mock_other_user = mock.Mock(cache_utilities.RefCell, ref_count=1)
+        cache_impl._user_entries = collections.FreezableDict(
+            {
+                snowflakes.Snowflake(21231234): mock_user,
+                snowflakes.Snowflake(645234): mock_other_user,
+            }
+        )
+
+        cache_impl._garbage_collect_user(mock_user, decrement=1)
+
+        assert cache_impl._user_entries == {snowflakes.Snowflake(645234): mock_other_user}
+        assert cache_impl._dm_channel_entries == {}
 
     def test_garbage_collect_user_for_referenced_user(self, cache_impl):
         mock_user = cache_utilities.RefCell(mock.Mock(id=snowflakes.Snowflake(21231234)), ref_count=2)
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 3c755b94..1f50c1b3 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -287,6 +287,7 @@ def test_acquire(self, rest_app):
             rest_app.acquire(token="token", token_type="Type")
 
         mock_client.assert_called_once_with(
+            cache=None,
             entity_factory=_entity_factory(),
             executor=rest_app._executor,
             http_settings=rest_app._http_settings,
@@ -309,8 +310,14 @@ def rest_client_class():
 
 
 @pytest.fixture()
-def rest_client(rest_client_class):
+def mock_cache():
+    return mock.Mock()
+
+
+@pytest.fixture()
+def rest_client(rest_client_class, mock_cache):
     obj = rest_client_class(
+        cache=mock_cache,
         http_settings=mock.Mock(spec=config.HTTPSettings),
         max_rate_limit=float("inf"),
         proxy_settings=mock.Mock(spec=config.ProxySettings),
@@ -376,6 +383,7 @@ class TestRESTClientImpl:
     def test__init__passes_max_rate_limit(self):
         with mock.patch.object(buckets, "RESTBucketManager") as bucket:
             rest.RESTClientImpl(
+                cache=None,
                 http_settings=mock.Mock(),
                 max_rate_limit=float("inf"),
                 proxy_settings=mock.Mock(),
@@ -391,6 +399,7 @@ def test__init__passes_max_rate_limit(self):
     def test__init__when_token_strategy_passed_with_token_type(self):
         with pytest.raises(ValueError, match="Token type should be handled by the token strategy"):
             rest.RESTClientImpl(
+                cache=None,
                 http_settings=mock.Mock(),
                 max_rate_limit=float("inf"),
                 proxy_settings=mock.Mock(),
@@ -403,6 +412,7 @@ def test__init__when_token_strategy_passed_with_token_type(self):
 
     def test__init__when_token_is_None_sets_token_to_None(self):
         obj = rest.RESTClientImpl(
+            cache=None,
             http_settings=mock.Mock(),
             max_rate_limit=float("inf"),
             proxy_settings=mock.Mock(),
@@ -416,6 +426,7 @@ def test__init__when_token_is_None_sets_token_to_None(self):
 
     def test__init__when_token_and_token_type_is_not_None_generates_token_with_type(self):
         obj = rest.RESTClientImpl(
+            cache=None,
             http_settings=mock.Mock(),
             max_rate_limit=float("inf"),
             proxy_settings=mock.Mock(),
@@ -429,6 +440,7 @@ def test__init__when_token_and_token_type_is_not_None_generates_token_with_type(
 
     def test__init__when_rest_url_is_None_generates_url_using_default_url(self):
         obj = rest.RESTClientImpl(
+            cache=None,
             http_settings=mock.Mock(),
             max_rate_limit=float("inf"),
             proxy_settings=mock.Mock(),
@@ -442,6 +454,7 @@ def test__init__when_rest_url_is_None_generates_url_using_default_url(self):
 
     def test__init__when_rest_url_is_not_None_generates_url_using_given_url(self):
         obj = rest.RESTClientImpl(
+            cache=None,
             http_settings=mock.Mock(),
             max_rate_limit=float("inf"),
             proxy_settings=mock.Mock(),
@@ -1285,6 +1298,29 @@ async def test_fetch_channel(self, rest_client):
         rest_client._request.assert_awaited_once_with(expected_route)
         rest_client._entity_factory.deserialize_channel.assert_called_once_with(rest_client._request.return_value)
 
+    async def test_fetch_channel_with_dm_channel_when_cacheful(self, rest_client, mock_cache):
+        expected_route = routes.GET_CHANNEL.compile(channel=123)
+        mock_object = mock.Mock(spec=channels.DMChannel, type=channels.ChannelType.DM)
+        rest_client._entity_factory.deserialize_channel = mock.Mock(return_value=mock_object)
+        rest_client._request = mock.AsyncMock(return_value={"payload": "NO"})
+
+        assert await rest_client.fetch_channel(StubModel(123)) == mock_object
+        rest_client._request.assert_awaited_once_with(expected_route)
+        rest_client._entity_factory.deserialize_channel.assert_called_once_with(rest_client._request.return_value)
+        mock_cache.set_dm_channel_id.assert_called_once_with(mock_object.recipient.id, mock_object.id)
+
+    async def test_fetch_channel_with_dm_channel_when_cacheless(self, rest_client, mock_cache):
+        expected_route = routes.GET_CHANNEL.compile(channel=123)
+        mock_object = mock.Mock(spec=channels.DMChannel, type=channels.ChannelType.DM)
+        rest_client._cache = None
+        rest_client._entity_factory.deserialize_channel = mock.Mock(return_value=mock_object)
+        rest_client._request = mock.AsyncMock(return_value={"payload": "NO"})
+
+        assert await rest_client.fetch_channel(StubModel(123)) == mock_object
+        rest_client._request.assert_awaited_once_with(expected_route)
+        rest_client._entity_factory.deserialize_channel.assert_called_once_with(rest_client._request.return_value)
+        mock_cache.set_dm_channel_id.assert_not_called()
+
     async def test_edit_channel(self, rest_client):
         expected_route = routes.PATCH_CHANNEL.compile(channel=123)
         mock_object = mock.Mock()
@@ -2013,16 +2049,31 @@ async def test_leave_guild(self, rest_client):
         await rest_client.leave_guild(StubModel(123))
         rest_client._request.assert_awaited_once_with(expected_route)
 
-    async def test_create_dm_channel(self, rest_client):
-        dm_channel = StubModel(123)
+    async def test_create_dm_channel(self, rest_client, mock_cache):
+        dm_channel = StubModel(43234)
+        user = StubModel(123)
         expected_route = routes.POST_MY_CHANNELS.compile()
         expected_json = {"recipient_id": "123"}
-        rest_client._request = mock.AsyncMock(return_value={"id": "123"})
+        rest_client._request = mock.AsyncMock(return_value={"id": "43234"})
+        rest_client._entity_factory.deserialize_dm = mock.Mock(return_value=dm_channel)
+
+        assert await rest_client.create_dm_channel(user) == dm_channel
+        rest_client._request.assert_awaited_once_with(expected_route, json=expected_json)
+        rest_client._entity_factory.deserialize_dm.assert_called_once_with({"id": "43234"})
+        mock_cache.set_dm_channel_id.assert_called_once_with(user, dm_channel.id)
+
+    async def test_create_dm_channel_when_cacheless(self, rest_client, mock_cache):
+        rest_client._cache = None
+        dm_channel = StubModel(43234)
+        expected_route = routes.POST_MY_CHANNELS.compile()
+        expected_json = {"recipient_id": "123"}
+        rest_client._request = mock.AsyncMock(return_value={"id": "43234"})
         rest_client._entity_factory.deserialize_dm = mock.Mock(return_value=dm_channel)
 
         assert await rest_client.create_dm_channel(StubModel(123)) == dm_channel
         rest_client._request.assert_awaited_once_with(expected_route, json=expected_json)
-        rest_client._entity_factory.deserialize_dm.assert_called_once_with({"id": "123"})
+        rest_client._entity_factory.deserialize_dm.assert_called_once_with({"id": "43234"})
+        mock_cache.set_dm_channel_id.assert_not_called()
 
     async def test_fetch_application(self, rest_client):
         application = StubModel(123)
diff --git a/tests/hikari/test_applications.py b/tests/hikari/test_applications.py
index 327409f3..5368fbc3 100644
--- a/tests/hikari/test_applications.py
+++ b/tests/hikari/test_applications.py
@@ -57,10 +57,6 @@ def test_flags_property(self, model):
     def test_id_property(self, model):
         assert model.id is model.user.id
 
-    def test_id_setter(self, model):
-        with pytest.raises(TypeError, match="Cannot mutate the ID of a member"):
-            model.id = 42
-
     def test_is_bot_property(self, model):
         assert model.is_bot is model.user.is_bot
 
@@ -73,14 +69,6 @@ def test_mention_property(self, model):
     def test_username_property(self, model):
         assert model.username is model.user.username
 
-    @pytest.mark.asyncio
-    async def test_fetch_dm_channel(self, model):
-        model.user.fetch_dm_channel = mock.AsyncMock()
-
-        result = await model.fetch_dm_channel()
-        assert result is model.user.fetch_dm_channel.return_value
-        model.user.fetch_dm_channel.assert_awaited_once()
-
     def test_str_operator(self):
         mock_team_member = mock.Mock(
             applications.TeamMember, user=mock.Mock(users.User, __str__=mock.Mock(return_value="mario#1234"))
diff --git a/tests/hikari/test_channels.py b/tests/hikari/test_channels.py
index 36599200..8731f224 100644
--- a/tests/hikari/test_channels.py
+++ b/tests/hikari/test_channels.py
@@ -269,15 +269,8 @@ def model(self, mock_app):
             parent_id=None,
         )
 
-    @pytest.mark.parametrize("error", [TypeError, AttributeError, NameError])
-    def test_shard_id_property_when_guild_id_error_raised(self, model, error):
-        class BrokenApp:
-            def __getattr__(self, name):
-                if name == "shard_count":
-                    raise error
-                return mock.Mock()
-
-        model.app = BrokenApp()
+    def test_shard_id_property_when_not_shard_aware(self, model):
+        model.app = None
 
         assert model.shard_id is None
 
diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py
index c462a3c6..37f75218 100644
--- a/tests/hikari/test_guilds.py
+++ b/tests/hikari/test_guilds.py
@@ -178,10 +178,6 @@ def test_app_property(self, model, mock_user):
     def test_id_property(self, model, mock_user):
         assert model.id is mock_user.id
 
-    def test_id_setter_property(self, model):
-        with pytest.raises(TypeError):
-            model.id = 456
-
     def test_username_property(self, model, mock_user):
         assert model.username is mock_user.username
 
@@ -208,13 +204,6 @@ def test_make_avatar_url(self, model, mock_user):
         mock_user.make_avatar_url.assert_called_once_with(ext="png", size=4096)
         assert result is mock_user.make_avatar_url.return_value
 
-    @pytest.mark.asyncio
-    async def test_fetch_dm_channel(self, model, mock_user):
-        mock_user.fetch_dm_channel = mock.AsyncMock()
-        result = await model.fetch_dm_channel()
-        mock_user.fetch_dm_channel.assert_awaited_once_with()
-        assert result is mock_user.fetch_dm_channel.return_value
-
     @pytest.mark.asyncio
     async def test_fetch_self(self, model):
         model.user.app.rest.fetch_member = mock.AsyncMock()
diff --git a/tests/hikari/test_users.py b/tests/hikari/test_users.py
index f04e0810..6ca1776e 100644
--- a/tests/hikari/test_users.py
+++ b/tests/hikari/test_users.py
@@ -23,6 +23,7 @@
 import pytest
 
 from hikari import snowflakes
+from hikari import traits
 from hikari import undefined
 from hikari import urls
 from hikari import users
@@ -45,21 +46,113 @@ async def test_fetch_self(self, obj):
         obj.app.rest.fetch_user.assert_awaited_once_with(user=123)
 
     @pytest.mark.asyncio()
-    async def test_send(self, obj):
+    async def test_send_uses_cached_id(self, obj):
+        obj.id = 4123123
         embed = object()
         attachment = object()
         attachments = [object(), object()]
         user_mentions = [object(), object()]
         role_mentions = [object(), object()]
-        mock_channel = mock.Mock(id=456)
-        mock_message = object()
         reply = object()
         mentions_reply = object()
 
-        obj.app = mock.Mock()
-        obj.fetch_dm_channel = mock.AsyncMock(return_value=mock_channel)
-        obj.app.rest.create_message = mock.AsyncMock(return_value=mock_message)
-        obj._dm_channel = None
+        obj.app = mock.Mock(spec=traits.CacheAware, rest=mock.AsyncMock())
+        obj.fetch_dm_channel = mock.AsyncMock()
+
+        returned = await obj.send(
+            content="test",
+            embed=embed,
+            attachment=attachment,
+            attachments=attachments,
+            nonce="nonce",
+            tts=True,
+            reply=reply,
+            mentions_everyone=False,
+            user_mentions=user_mentions,
+            role_mentions=role_mentions,
+            mentions_reply=mentions_reply,
+        )
+
+        assert returned is obj.app.rest.create_message.return_value
+
+        obj.app.cache.get_dm_channel_id.assert_called_once_with(4123123)
+        obj.fetch_dm_channel.assert_not_called()
+        obj.app.rest.create_message.assert_awaited_once_with(
+            channel=obj.app.cache.get_dm_channel_id.return_value,
+            content="test",
+            embed=embed,
+            attachment=attachment,
+            attachments=attachments,
+            nonce="nonce",
+            tts=True,
+            mentions_everyone=False,
+            reply=reply,
+            user_mentions=user_mentions,
+            role_mentions=role_mentions,
+            mentions_reply=mentions_reply,
+        )
+
+    @pytest.mark.asyncio()
+    async def test_send_when_not_cached(self, obj):
+        obj.id = 522234
+        embed = object()
+        attachment = object()
+        attachments = [object(), object()]
+        user_mentions = [object(), object()]
+        role_mentions = [object(), object()]
+        reply = object()
+        mentions_reply = object()
+
+        obj.app = mock.Mock(spec=traits.CacheAware, rest=mock.AsyncMock())
+        obj.app.cache.get_dm_channel_id = mock.Mock(return_value=None)
+        obj.fetch_dm_channel = mock.AsyncMock()
+
+        returned = await obj.send(
+            content="test",
+            embed=embed,
+            attachment=attachment,
+            attachments=attachments,
+            nonce="nonce",
+            tts=True,
+            reply=reply,
+            mentions_everyone=False,
+            user_mentions=user_mentions,
+            role_mentions=role_mentions,
+            mentions_reply=mentions_reply,
+        )
+
+        assert returned is obj.app.rest.create_message.return_value
+
+        obj.app.cache.get_dm_channel_id.assert_called_once_with(522234)
+        obj.fetch_dm_channel.assert_awaited_once()
+        obj.app.rest.create_message.assert_awaited_once_with(
+            channel=obj.fetch_dm_channel.return_value.id,
+            content="test",
+            embed=embed,
+            attachment=attachment,
+            attachments=attachments,
+            nonce="nonce",
+            tts=True,
+            mentions_everyone=False,
+            reply=reply,
+            user_mentions=user_mentions,
+            role_mentions=role_mentions,
+            mentions_reply=mentions_reply,
+        )
+
+    @pytest.mark.asyncio()
+    async def test_send_when_not_cache_aware(self, obj):
+        obj.id = 522234
+        embed = object()
+        attachment = object()
+        attachments = [object(), object()]
+        user_mentions = [object(), object()]
+        role_mentions = [object(), object()]
+        reply = object()
+        mentions_reply = object()
+
+        obj.app = mock.Mock(spec=traits.RESTAware, rest=mock.AsyncMock())
+        obj.fetch_dm_channel = mock.AsyncMock()
 
         returned = await obj.send(
             content="test",
@@ -75,11 +168,11 @@ async def test_send(self, obj):
             mentions_reply=mentions_reply,
         )
 
-        assert returned is mock_message
+        assert returned is obj.app.rest.create_message.return_value
 
-        obj.fetch_dm_channel.assert_awaited_once_with()
+        obj.fetch_dm_channel.assert_awaited_once()
         obj.app.rest.create_message.assert_awaited_once_with(
-            channel=456,
+            channel=obj.fetch_dm_channel.return_value.id,
             content="test",
             embed=embed,
             attachment=attachment,
@@ -93,6 +186,16 @@ async def test_send(self, obj):
             mentions_reply=mentions_reply,
         )
 
+    @pytest.mark.asyncio()
+    async def test_fetch_dm_channel(self, obj):
+        obj.id = 123
+        obj.app = mock.Mock()
+        obj.app.rest.create_dm_channel = mock.AsyncMock()
+
+        assert await obj.fetch_dm_channel() == obj.app.rest.create_dm_channel.return_value
+
+        obj.app.rest.create_dm_channel.assert_awaited_once_with(123)
+
 
 class TestUser:
     @pytest.fixture()
@@ -202,30 +305,6 @@ def test_str_operator_when_partial(self, obj):
     def test_mention_property(self, obj):
         assert obj.mention == "<@123>"
 
-    @pytest.mark.asyncio()
-    async def test_fetch_dm_channel_when_not_cached(self, obj):
-        mock_channel = mock.Mock(id=456)
-
-        obj.id = 123
-        obj.app = mock.Mock()
-        obj.app.rest.create_dm_channel = mock.AsyncMock(return_value=mock_channel)
-        obj._dm_channel = None
-
-        assert await obj.fetch_dm_channel() == mock_channel
-
-        assert obj._dm_channel == mock_channel
-        obj.app.rest.create_dm_channel.assert_awaited_once_with(123)
-
-    @pytest.mark.asyncio()
-    async def test_fetch_dm_channel_when_cached(self, obj):
-        mock_channel = mock.Mock(id=456)
-        obj._dm_channel = mock_channel
-
-        assert await obj.fetch_dm_channel() == mock_channel
-
-        assert obj._dm_channel == mock_channel
-        obj.app.rest.create_dm_channel.assert_not_called()
-
     @pytest.mark.asyncio()
     async def test_fetch_self(self, obj):
         user = object()