Repository: hikari-py/hikari (python)

Issue:
Option to only cache the bot member
## Summary

This requests aims to introduce a new option for enabling the members cache ONLY for the bot member.
Similar to `CacheComponents.ME` but for the member.

## Why is this needed?

The bot member is very useful for permissions calculations and enabling the members caches every member, increasing memory consumption and CPU consumption on startup.

## Ideal implementation

see pr :)


Pull request:
feat: add CacheSettings.only_my_member
### Summary
<!-- Small summary of the merge request -->

Add CacheSettings.only_my_member to restrict the member cache to only cache the bot.
This is useful for situations when you only need the bot member (eg permission calculations).

### 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

Closes #1678 


Patch:
diff --git a/README.md b/README.md
index 88f2070c..1ad27581 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ Hikari provides two different default bot implementations to suit your needs:
 
 ### GatewayBot
 
-A [`GatewayBot`](https://docs.hikari-py.dev/en/stable/reference/hikari/impl/bot/#hikari.impl.bot.GatewayBot)
+A [`GatewayBot`](https://docs.hikari-py.dev/en/stable/reference/hikari/impl/gateway_bot/#hikari.impl.gateway_bot.GatewayBot)
 is one which will connect to Discord through the gateway and receive
 events through there. A simple startup example could be the following:
 
@@ -194,7 +194,7 @@ bot.run(
 
 Many other helpful options exist for you to take advantage of if you wish. Links to the respective docs can be seen
 below:
-- [GatewayBot.run](https://docs.hikari-py.dev/en/stable/reference/hikari/impl/bot/#hikari.impl.bot.GatewayBot.run)
+- [GatewayBot.run](https://docs.hikari-py.dev/en/stable/reference/hikari/impl/gateway_bot/#hikari.impl.gateway_bot.GatewayBot.run)
 - [RESTBot.run](https://docs.hikari-py.dev/en/stable/reference/hikari/impl/rest_bot/#hikari.impl.rest_bot.RESTBot.run)
 
 ---
diff --git a/changes/1679.feature.md b/changes/1679.feature.md
new file mode 100644
index 00000000..4e2839f5
--- /dev/null
+++ b/changes/1679.feature.md
@@ -0,0 +1,1 @@
+Add `CacheSettings.only_my_member` to only cache the bot member.
diff --git a/hikari/__init__.py b/hikari/__init__.py
index 45a0066f..c4ae3280 100644
--- a/hikari/__init__.py
+++ b/hikari/__init__.py
@@ -22,7 +22,7 @@
 # SOFTWARE.
 """A sane Python framework for writing modern Discord bots.
 
-To get started, you will want to initialize an instance of `hikari.impl.bot.GatewayBot`
+To get started, you will want to initialize an instance of `hikari.impl.gateway_bot.GatewayBot`
 for writing a gateway based bot, `hikari.impl.rest_bot.RESTBot` for a REST based bot,
 or `hikari.impl.rest.RESTApp` if you only need to use the REST API.
 """
diff --git a/hikari/api/config.py b/hikari/api/config.py
index ef6b05b7..8907783b 100644
--- a/hikari/api/config.py
+++ b/hikari/api/config.py
@@ -77,7 +77,7 @@ class CacheComponents(enums.Flag):
     """Enables the guild stickers cache."""
 
     GUILD_THREADS = 1 << 12
-    """Enabled the guild threads cache."""
+    """Enables the guild threads cache."""
 
     ALL = (
         GUILDS
@@ -199,3 +199,14 @@ class CacheSettings(abc.ABC):
     @abc.abstractmethod
     def components(self) -> CacheComponents:
         """Cache components to use."""
+
+    @property
+    @abc.abstractmethod
+    def only_my_member(self) -> bool:
+        """Reduce the members cache to only the bot itself.
+
+        Useful when only the bot member is required (eg. permission checks).
+        This will have no effect if the members cache is not enabled.
+
+        Defaults to `False`.
+        """
diff --git a/hikari/impl/config.py b/hikari/impl/config.py
index 4f0916f4..c7fc36b3 100644
--- a/hikari/impl/config.py
+++ b/hikari/impl/config.py
@@ -29,6 +29,7 @@
     "ProxySettings",
     "HTTPTimeoutSettings",
     "HTTPSettings",
+    "CacheComponents",
     "CacheSettings",
 )
 
@@ -292,6 +293,10 @@ class that will **NOT** enforce SSL verification. This is then stored
     """
 
 
+# Re-export
+CacheComponents = config.CacheComponents
+
+
 @attrs_extensions.with_copy
 @attrs.define(kw_only=True, weakref_slot=False)
 class CacheSettings(config.CacheSettings):
@@ -320,3 +325,12 @@ class CacheSettings(config.CacheSettings):
 
     Defaults to `50`.
     """
+
+    only_my_member: bool = attrs.field(default=False)
+    """Reduce the members cache to only the bot itself.
+
+    Useful when only the bot member is required (eg. permission checks).
+    This will have no effect if the members cache is not enabled.
+
+    Defaults to `False`.
+    """
diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py
index ef1c6590..e9ec111a 100644
--- a/hikari/impl/event_manager.py
+++ b/hikari/impl/event_manager.py
@@ -343,8 +343,12 @@ async def on_guild_create(  # noqa: C901, CFQ001 - Function too complex and too
             if members:
                 # TODO: do we really want to invalidate these all after an outage.
                 self._cache.clear_members_for_guild(guild_id)
-                for member in members.values():
-                    self._cache.set_member(member)
+                if not self._cache.settings.only_my_member:
+                    for member in members.values():
+                        self._cache.set_member(member)
+                else:
+                    my_member = members[shard.get_user_id()]
+                    self._cache.set_member(my_member)
 
             if presences:
                 self._cache.clear_presences_for_guild(guild_id)
@@ -361,17 +365,34 @@ async def on_guild_create(  # noqa: C901, CFQ001 - Function too complex and too
                 for thread in threads.values():
                     self._cache.set_thread(thread)
 
+        # We only want to chunk if we are allowed and need to:
+        #   Allowed?
+        #       All the following must be true:
+        #           1. `auto_chunk_members` is true (the user wants us to).
+        #           2. We have the necessary intents (`GUILD_MEMBERS`).
+        #           3. The guild is marked as "large" or we do not have `GUILD_PRESENCES` intent
+        #              Discord will only send every other member objects on the `GUILD_CREATE`
+        #              payload if presence intents are also declared, so if this isn't the case then we also
+        #              want to chunk small guilds.
+        #
+        #   Need to?
+        #       One of the following must be true:
+        #           1. We have a cache, and it requires it (it is enabled for `MEMBERS`), but we are
+        #              not limited to only our own member (which is included in the `GUILD_CREATE` payload).
+        #           2. The user is waiting for the member chunks (there is an event listener for it).
         presences_declared = self._intents & intents_.Intents.GUILD_PRESENCES
 
-        # When intents are enabled Discord will only send other member objects on the guild create
-        # payload if presence intents are also declared, so if this isn't the case then we also want
-        # to chunk small guilds.
         if (
             self._auto_chunk_members
             and self._intents & intents_.Intents.GUILD_MEMBERS
             and (payload.get("large") or not presences_declared)
             and (
-                self._cache_enabled_for(config.CacheComponents.MEMBERS)
+                (
+                    self._cache
+                    and self._cache_enabled_for(config.CacheComponents.MEMBERS)
+                    and not self._cache.settings.only_my_member
+                )
+                # This call is a bit expensive, so best to do it last
                 or self._enabled_for_event(shard_events.MemberChunkEvent)
             )
         ):
diff --git a/hikari/impl/gateway_bot.py b/hikari/impl/gateway_bot.py
index 0d3f8843..70bf8bca 100644
--- a/hikari/impl/gateway_bot.py
+++ b/hikari/impl/gateway_bot.py
@@ -183,15 +183,22 @@ class GatewayBot(traits.GatewayBotAware):
         Defaults to `True`. If `False`, then no member chunks
         will be requested automatically, even if there are reasons to do so.
 
-        All following statements must be true to automatically request chunks:
-
-        1. `auto_chunk_members` is `True`.
-        2. The members intent is enabled.
-        3. The server is marked as "large" or the presences intent is not enabled
-           (since Discord only sends other members when presences are declared,
-           we should also chunk small guilds if the presences are not declared).
-        4. The members cache is enabled or there are listeners for the
-           `MemberChunkEvent`.
+        We only want to chunk if we are allowed and need to:
+
+        - Allowed?
+            All the following must be true:
+                1. `auto_chunk_members` is true (the user wants us to).
+                2. We have the necessary intents (`GUILD_MEMBERS`).
+                3. The guild is marked as "large" or we do not have `GUILD_PRESENCES` intent
+                   Discord will only send every other member objects on the `GUILD_CREATE`
+                   payload if presence intents are also declared, so if this isn't the case then we also
+                   want to chunk small guilds.
+
+        - Needed?
+            One of the following must be true:
+                1. We have a cache, and it requires it (it is enabled for `MEMBERS`), but we are
+                   not limited to only our own member (which is included in the `GUILD_CREATE` payload).
+                2. The user is waiting for the member chunks (there is an event listener for it).
     logs : typing.Union[None, str, int, typing.Dict[str, typing.Any], os.PathLike]
         The flavour to set the logging to.
 
@@ -563,11 +570,11 @@ async def on_everyone_mentioned(event):
 
         See Also
         --------
-        Listen : `hikari.impl.bot.GatewayBot.listen`.
-        Stream : `hikari.impl.bot.GatewayBot.stream`.
-        Subscribe : `hikari.impl.bot.GatewayBot.subscribe`.
-        Unsubscribe : `hikari.impl.bot.GatewayBot.unsubscribe`.
-        Wait_for : `hikari.impl.bot.GatewayBot.wait_for`.
+        Listen : `hikari.impl.gateway_bot.GatewayBot.listen`.
+        Stream : `hikari.impl.gateway_bot.GatewayBot.stream`.
+        Subscribe : `hikari.impl.gateway_bot.GatewayBot.subscribe`.
+        Unsubscribe : `hikari.impl.gateway_bot.GatewayBot.unsubscribe`.
+        Wait_for : `hikari.impl.gateway_bot.GatewayBot.wait_for`.
         """
         return self._event_manager.dispatch(event)
 
@@ -626,11 +633,11 @@ def listen(
 
         See Also
         --------
-        Dispatch : `hikari.impl.bot.GatewayBot.dispatch`.
-        Stream : `hikari.impl.bot.GatewayBot.stream`.
-        Subscribe : `hikari.impl.bot.GatewayBot.subscribe`.
-        Unsubscribe : `hikari.impl.bot.GatewayBot.unsubscribe`.
-        Wait_for : `hikari.impl.bot.GatewayBot.wait_for`.
+        Dispatch : `hikari.impl.gateway_bot.GatewayBot.dispatch`.
+        Stream : `hikari.impl.gateway_bot.GatewayBot.stream`.
+        Subscribe : `hikari.impl.gateway_bot.GatewayBot.subscribe`.
+        Unsubscribe : `hikari.impl.gateway_bot.GatewayBot.unsubscribe`.
+        Wait_for : `hikari.impl.gateway_bot.GatewayBot.wait_for`.
         """
         return self._event_manager.listen(*event_types)
 
@@ -1056,11 +1063,11 @@ def stream(
 
         See Also
         --------
-        Dispatch : `hikari.impl.bot.GatewayBot.dispatch`.
-        Listen : `hikari.impl.bot.GatewayBot.listen`.
-        Subscribe : `hikari.impl.bot.GatewayBot.subscribe`.
-        Unsubscribe : `hikari.impl.bot.GatewayBot.unsubscribe`.
-        Wait_for : `hikari.impl.bot.GatewayBot.wait_for`.
+        Dispatch : `hikari.impl.gateway_bot.GatewayBot.dispatch`.
+        Listen : `hikari.impl.gateway_bot.GatewayBot.listen`.
+        Subscribe : `hikari.impl.gateway_bot.GatewayBot.subscribe`.
+        Unsubscribe : `hikari.impl.gateway_bot.GatewayBot.unsubscribe`.
+        Wait_for : `hikari.impl.gateway_bot.GatewayBot.wait_for`.
         """
         self._check_if_alive()
         return self._event_manager.stream(event_type, timeout=timeout, limit=limit)
@@ -1099,11 +1106,11 @@ async def on_message(event):
 
         See Also
         --------
-        Dispatch : `hikari.impl.bot.GatewayBot.dispatch`.
-        Listen : `hikari.impl.bot.GatewayBot.listen`.
-        Stream : `hikari.impl.bot.GatewayBot.stream`.
-        Unsubscribe : `hikari.impl.bot.GatewayBot.unsubscribe`.
-        Wait_for : `hikari.impl.bot.GatewayBot.wait_for`.
+        Dispatch : `hikari.impl.gateway_bot.GatewayBot.dispatch`.
+        Listen : `hikari.impl.gateway_bot.GatewayBot.listen`.
+        Stream : `hikari.impl.gateway_bot.GatewayBot.stream`.
+        Unsubscribe : `hikari.impl.gateway_bot.GatewayBot.unsubscribe`.
+        Wait_for : `hikari.impl.gateway_bot.GatewayBot.wait_for`.
         """
         self._event_manager.subscribe(event_type, callback)
 
@@ -1139,11 +1146,11 @@ async def on_message(event):
 
         See Also
         --------
-        Dispatch : `hikari.impl.bot.GatewayBot.dispatch`.
-        Listen : `hikari.impl.bot.GatewayBot.listen`.
-        Stream : `hikari.impl.bot.GatewayBot.stream`.
-        Subscribe : `hikari.impl.bot.GatewayBot.subscribe`.
-        Wait_for : `hikari.impl.bot.GatewayBot.wait_for`.
+        Dispatch : `hikari.impl.gateway_bot.GatewayBot.dispatch`.
+        Listen : `hikari.impl.gateway_bot.GatewayBot.listen`.
+        Stream : `hikari.impl.gateway_bot.GatewayBot.stream`.
+        Subscribe : `hikari.impl.gateway_bot.GatewayBot.subscribe`.
+        Wait_for : `hikari.impl.gateway_bot.GatewayBot.wait_for`.
         """
         self._event_manager.unsubscribe(event_type, callback)
 
@@ -1190,11 +1197,11 @@ async def wait_for(
 
         See Also
         --------
-        Dispatch : `hikari.impl.bot.GatewayBot.dispatch`.
-        Listen : `hikari.impl.bot.GatewayBot.listen`.
-        Stream : `hikari.impl.bot.GatewayBot.stream`.
-        Subscribe : `hikari.impl.bot.GatewayBot.subscribe`.
-        Unsubscribe : `hikari.impl.bot.GatewayBot.unsubscribe`.
+        Dispatch : `hikari.impl.gateway_bot.GatewayBot.dispatch`.
+        Listen : `hikari.impl.gateway_bot.GatewayBot.listen`.
+        Stream : `hikari.impl.gateway_bot.GatewayBot.stream`.
+        Subscribe : `hikari.impl.gateway_bot.GatewayBot.subscribe`.
+        Unsubscribe : `hikari.impl.gateway_bot.GatewayBot.unsubscribe`.
         """
         self._check_if_alive()
         return await self._event_manager.wait_for(event_type, timeout=timeout, predicate=predicate)
diff --git a/hikari/users.py b/hikari/users.py
index b58e1562..d6e44162 100644
--- a/hikari/users.py
+++ b/hikari/users.py
@@ -168,7 +168,7 @@ def accent_colour(self) -> undefined.UndefinedNoneOr[colors.Color]:
     def discriminator(self) -> undefined.UndefinedOr[str]:
         """Discriminator for the user.
 
-        .. deprecation:: 2.0.0.dev120
+        .. deprecated:: 2.0.0.dev120
             Discriminators are deprecated and being replaced with "0" by Discord
             during username migration. This field will be removed after migration is complete.
             Learn more here: https://dis.gd/usernames
@@ -514,7 +514,7 @@ def display_avatar_url(self) -> files.URL:
     def discriminator(self) -> str:
         """Discriminator for the user.
 
-        .. deprecation:: 2.0.0.dev120
+        .. deprecated:: 2.0.0.dev120
             Discriminators are deprecated and being replaced with "0" by Discord
             during username migration. This field will be removed after migration is complete.
             Learn more here: https://dis.gd/usernames
@@ -665,7 +665,7 @@ class PartialUserImpl(PartialUser):
     discriminator: undefined.UndefinedOr[str] = attrs.field(eq=False, hash=False, repr=True)
     """Four-digit discriminator for the user if unmigrated.
 
-    .. deprecation:: 2.0.0.dev120
+    .. deprecated:: 2.0.0.dev120
         Discriminators are deprecated and being replaced with "0" by Discord
         during username migration. This field will be removed after migration is complete.
         Learn more here: https://dis.gd/usernames
@@ -726,7 +726,7 @@ class UserImpl(PartialUserImpl, User):
     discriminator: str = attrs.field(eq=False, hash=False, repr=True)
     """The user's discriminator.
 
-    .. deprecation:: 2.0.0.dev120
+    .. deprecated:: 2.0.0.dev120
         Discriminators are deprecated and being replaced with "0" by Discord
         during username migration. This field will be removed after migration is complete.
         Learn more here: https://dis.gd/usernames


Test patch:
diff --git a/tests/hikari/impl/test_event_manager.py b/tests/hikari/impl/test_event_manager.py
index 5ea1c46c..3068c416 100644
--- a/tests/hikari/impl/test_event_manager.py
+++ b/tests/hikari/impl/test_event_manager.py
@@ -551,15 +551,19 @@ async def test_on_guild_create_when_not_dispatching_and_not_caching(
 
         event_manager_impl.dispatch.assert_not_called()
 
-    @pytest.mark.parametrize("include_unavailable", [True, False])
+    @pytest.mark.parametrize(
+        ("include_unavailable", "only_my_member"), [(True, True), (True, False), (False, True), (False, False)]
+    )
     @pytest.mark.asyncio()
     async def test_on_guild_create_when_not_dispatching_and_caching(
-        self, event_manager_impl, shard, event_factory, entity_factory, include_unavailable
+        self, event_manager_impl, shard, event_factory, entity_factory, include_unavailable, only_my_member
     ):
         payload = {"unavailable": False} if include_unavailable else {}
         event_manager_impl._intents = intents.Intents.NONE
         event_manager_impl._cache_enabled_for = mock.Mock(return_value=True)
         event_manager_impl._enabled_for_event = mock.Mock(return_value=False)
+        event_manager_impl._cache.settings.only_my_member = only_my_member
+        shard.get_user_id.return_value = 1
         gateway_guild = entity_factory.deserialize_gateway_guild.return_value
         gateway_guild.channels.return_value = {1: "channel1", 2: "channel2"}
         gateway_guild.emojis.return_value = {1: "emoji1", 2: "emoji2"}
@@ -595,13 +599,17 @@ async def test_on_guild_create_when_not_dispatching_and_caching(
         event_manager_impl._cache.clear_roles_for_guild.assert_called_once_with(gateway_guild.id)
         event_manager_impl._cache.set_role.assert_has_calls([mock.call("role1"), mock.call("role2")])
         event_manager_impl._cache.clear_members_for_guild.assert_called_once_with(gateway_guild.id)
-        event_manager_impl._cache.set_member.assert_has_calls([mock.call("member1"), mock.call("member2")])
+        if only_my_member:
+            event_manager_impl._cache.set_member.assert_called_once_with("member1")
+            shard.get_user_id.assert_has_calls([mock.call(), mock.call()])
+        else:
+            event_manager_impl._cache.set_member.assert_has_calls([mock.call("member1"), mock.call("member2")])
+            shard.get_user_id.assert_called_once_with()
         event_manager_impl._cache.clear_presences_for_guild.assert_called_once_with(gateway_guild.id)
         event_manager_impl._cache.set_presence.assert_has_calls([mock.call("presence1"), mock.call("presence2")])
         event_manager_impl._cache.clear_voice_states_for_guild.assert_called_once_with(gateway_guild.id)
         event_manager_impl._cache.set_voice_state.assert_has_calls([mock.call("voice1"), mock.call("voice2")])
         request_guild_members.assert_not_called()
-        shard.get_user_id.assert_called_once_with()
 
         event_manager_impl.dispatch.assert_not_called()
 
@@ -633,25 +641,58 @@ async def test_on_guild_create_when_stateless(
         stateless_event_manager_impl.dispatch.assert_not_called()
 
     @pytest.mark.asyncio()
-    async def test_on_guild_create_when_members_declared_and_member_cache_enabled(
-        self, stateless_event_manager_impl, shard, event_factory, entity_factory
+    async def test_on_guild_create_when_members_declared_and_member_cache_enabled_but_only_my_member_not_enabled(
+        self, event_manager_impl, shard, event_factory, entity_factory
     ):
+        def cache_enabled_for_members_only(component):
+            return component == config.CacheComponents.MEMBERS
+
         shard.id = 123
-        stateless_event_manager_impl._intents = intents.Intents.GUILD_MEMBERS
-        stateless_event_manager_impl._cache_enabled_for = mock.Mock(return_value=True)
-        stateless_event_manager_impl._enabled_for_event = mock.Mock(return_value=False)
+        event_manager_impl._cache.settings.only_my_member = False
+        event_manager_impl._intents = intents.Intents.GUILD_MEMBERS
+        event_manager_impl._cache_enabled_for = cache_enabled_for_members_only
+        event_manager_impl._enabled_for_event = mock.Mock(return_value=False)
+        gateway_guild = entity_factory.deserialize_gateway_guild.return_value
+        gateway_guild.id = 456
+        gateway_guild.members.return_value = {1: "member1", 2: "member2"}
         mock_request_guild_members = mock.Mock()
 
         with mock.patch.object(asyncio, "create_task") as create_task:
             with mock.patch.object(event_manager, "_fixed_size_nonce", return_value="abc"):
                 with mock.patch.object(event_manager, "_request_guild_members", new=mock_request_guild_members):
-                    await stateless_event_manager_impl.on_guild_create(shard, {"id": 456, "large": False})
+                    await event_manager_impl.on_guild_create(shard, {"id": 456, "large": False})
 
         mock_request_guild_members.assert_called_once_with(shard, 456, include_presences=False, nonce="123.abc")
         create_task.assert_called_once_with(
             mock_request_guild_members.return_value, name="123:456 guild create members request"
         )
 
+    @pytest.mark.asyncio()
+    async def test_on_guild_create_when_members_declared_and_member_cache_but_only_my_member_enabled(
+        self, event_manager_impl, shard, event_factory, entity_factory
+    ):
+        def cache_enabled_for_members_only(component):
+            return component == config.CacheComponents.MEMBERS
+
+        shard.id = 123
+        shard.get_user_id.return_value = 1
+        event_manager_impl._cache.settings.only_my_member = True
+        event_manager_impl._intents = intents.Intents.GUILD_MEMBERS
+        event_manager_impl._cache_enabled_for = cache_enabled_for_members_only
+        event_manager_impl._enabled_for_event = mock.Mock(return_value=False)
+        gateway_guild = entity_factory.deserialize_gateway_guild.return_value
+        gateway_guild.members.return_value = {1: "member1", 2: "member2"}
+
+        mock_request_guild_members = mock.Mock()
+
+        with mock.patch.object(asyncio, "create_task") as create_task:
+            with mock.patch.object(event_manager, "_fixed_size_nonce", return_value="abc"):
+                with mock.patch.object(event_manager, "_request_guild_members", new=mock_request_guild_members):
+                    await event_manager_impl.on_guild_create(shard, {"id": 456, "large": False})
+
+        mock_request_guild_members.assert_not_called()
+        create_task.assert_not_called()
+
     @pytest.mark.asyncio()
     async def test_on_guild_create_when_members_declared_and_enabled_for_member_chunk_event(
         self, stateless_event_manager_impl, shard, event_factory, entity_factory