Repository: hikari-py/hikari
Pull request #644 (2021-06-04 11:44:33)

Add mention properties to GuildChannel and PartialRole
### Summary
Add mention properties to GuildChannel and PartialRole

### 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: Could add a mention property to PartialChannel instead but DM channel mentions aren't clickable on desktop (they do still resolve to the channel's name though) but are on android fwiw

Patch:
diff --git a/hikari/channels.py b/hikari/channels.py
index 52a2f531..12ce2e64 100644
--- a/hikari/channels.py
+++ b/hikari/channels.py
@@ -717,6 +717,21 @@ class GuildChannel(PartialChannel):
     If no parent category is set for the channel, this will be `builtins.None`.
     """
 
+    @property
+    def mention(self) -> str:
+        """Return a raw mention string for the guild channel.
+
+        !!! note
+            As of writing, GuildCategory channels are a special case
+            for this and mentions of them will not resolve as clickable.
+
+        Returns
+        -------
+        builtins.str
+            The mention string to use.
+        """
+        return f"<#{self.id}>"
+
     @property
     def shard_id(self) -> typing.Optional[int]:
         """Return the shard ID for the shard.
diff --git a/hikari/guilds.py b/hikari/guilds.py
index ca3f1349..6a839647 100644
--- a/hikari/guilds.py
+++ b/hikari/guilds.py
@@ -584,6 +584,17 @@ class PartialRole(snowflakes.Unique):
     name: str = attr.field(eq=False, hash=False, repr=True)
     """The role's name."""
 
+    @property
+    def mention(self) -> str:
+        """Return a raw mention string for the role.
+
+        Returns
+        -------
+        builtins.str
+            The mention string to use.
+        """
+        return f"<@&{self.id}>"
+
     def __str__(self) -> str:
         return self.name
 
@@ -1220,6 +1231,61 @@ def discovery_splash_url(self) -> typing.Optional[files.URL]:
         """Discovery splash URL for the guild, if set."""
         return self.make_discovery_splash_url()
 
+    @property
+    def members(self) -> typing.Mapping[snowflakes.Snowflake, Member]:
+        """Get the members cached for the guild.
+
+        typing.Mapping[hikari.snowflakes.Snowflake, Member]
+            A mapping of user IDs to objects of the members cached for the guild.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return {}
+
+        return self.app.cache.get_members_view_for_guild(self.id)
+
+    @property
+    def presences(self) -> typing.Mapping[snowflakes.Snowflake, presences_.MemberPresence]:
+        """Get the presences cached for the guild.
+
+        typing.Mapping[hikari.snowflakes.Snowflake, hikari.presences.MemberPresence]
+            A mapping of user IDs to objects of the presences cached for the
+            guild.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return {}
+
+        return self.app.cache.get_presences_view_for_guild(self.id)
+
+    @property
+    def channels(self) -> typing.Mapping[snowflakes.Snowflake, channels_.GuildChannel]:
+        """Get the channels cached for the guild.
+
+        Returns
+        -------
+        typing.Mapping[hikari.snowflakes.Snowflake, hikari.channels.GuildChannel]
+            A mapping of channel IDs to objects of the channels cached for the
+            guild.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return {}
+
+        return self.app.cache.get_guild_channels_view_for_guild(self.id)
+
+    @property
+    def voice_states(self) -> typing.Mapping[snowflakes.Snowflake, voices_.VoiceState]:
+        """Get the voice states cached for the guild.
+
+        Returns
+        -------
+        typing.Mapping[hikari.snowflakes.Snowflake, hikari.voices.VoiceState]
+            A mapping of user IDs to objects of the voice states cached for the
+            guild.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return {}
+
+        return self.app.cache.get_voice_states_view_for_guild(self.id)
+
     @property
     @abc.abstractmethod
     def emojis(self) -> typing.Mapping[snowflakes.Snowflake, emojis_.KnownCustomEmoji]:
@@ -1346,6 +1412,102 @@ def make_splash_url(self, *, ext: str = "png", size: int = 4096) -> typing.Optio
             file_format=ext,
         )
 
+    def get_channel(
+        self,
+        channel: snowflakes.SnowflakeishOr[channels_.PartialChannel],
+    ) -> typing.Optional[channels_.GuildChannel]:
+        """Get a cached channel that belongs to the guild by it's ID or object.
+
+        Parameters
+        ----------
+        channel : hikari.snowflakes.SnowflakeishOr[hikari.channels.PartialChannel]
+            The object or ID of the guild channel to get from the cache.
+
+        Returns
+        -------
+        typing.Optional[hikari.channels.GuildChannel]
+            The object of the guild channel found in cache or `builtins.None.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return None
+
+        return self.app.cache.get_guild_channel(channel)
+
+    def get_member(self, user: snowflakes.SnowflakeishOr[users.PartialUser]) -> typing.Optional[Member]:
+        """Get a cached member that belongs to the guild by it's user ID or object.
+
+        Parameters
+        ----------
+        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
+            The object or ID of the user to get the cached member for.
+
+        Returns
+        -------
+        typing.Optional[Member]
+            The cached member object if found, else `builtins.None`.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return None
+
+        return self.app.cache.get_member(self.id, user)
+
+    def get_my_member(self) -> typing.Optional[Member]:
+        """Return the cached member for the bot user in this guild, if known.
+
+        Returns
+        -------
+        typing.Optional[Member]
+            The cached member for this guild, or `builtins.None` if not known.
+        """
+        if not isinstance(self.app, traits.ShardAware):
+            return None
+
+        me = self.app.me
+        if me is None:
+            return None
+
+        return self.get_member(me.id)
+
+    def get_presence(
+        self, user: snowflakes.SnowflakeishOr[users.PartialUser]
+    ) -> typing.Optional[presences_.MemberPresence]:
+        """Get a cached presence that belongs to the guild by it's user ID or object.
+
+        Parameters
+        ----------
+        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
+            The object or ID of the user to get the cached presence for.
+
+        Returns
+        -------
+        typing.Optional[hikari.presences.MemberPresence]
+            The cached presence object if found, else `builtins.None`.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return None
+
+        return self.app.cache.get_presence(self.id, user)
+
+    def get_voice_state(
+        self, user: snowflakes.SnowflakeishOr[users.PartialUser]
+    ) -> typing.Optional[voices_.VoiceState]:
+        """Get a cached voice state that belongs to the guild by it's user.
+
+        Parameters
+        ----------
+        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
+            The object or ID of the user to get the cached voice state for.
+
+        Returns
+        -------
+        typing.Optional[hikari.voices.VoiceState]
+            The cached voice state object if found, else `builtins.None`.
+        """
+        if not isinstance(self.app, traits.CacheAware):
+            return None
+
+        return self.app.cache.get_voice_state(self.id, user)
+
     @abc.abstractmethod
     def get_emoji(
         self, emoji: snowflakes.SnowflakeishOr[emojis_.CustomEmoji]
@@ -1441,21 +1603,6 @@ class GatewayGuild(Guild):
     `builtins.None`.
     """
 
-    @property
-    def channels(self) -> typing.Mapping[snowflakes.Snowflake, channels_.GuildChannel]:
-        """Get the channels cached for the guild.
-
-        Returns
-        -------
-        typing.Mapping[hikari.snowflakes.Snowflake, hikari.channels.GuildChannel]
-            A mapping of channel IDs to objects of the channels cached for the
-            guild.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return {}
-
-        return self.app.cache.get_guild_channels_view_for_guild(self.id)
-
     @property
     def emojis(self) -> typing.Mapping[snowflakes.Snowflake, emojis_.KnownCustomEmoji]:
         # <<inherited docstring from Guild>>.
@@ -1464,31 +1611,6 @@ def emojis(self) -> typing.Mapping[snowflakes.Snowflake, emojis_.KnownCustomEmoj
 
         return self.app.cache.get_emojis_view_for_guild(self.id)
 
-    @property
-    def members(self) -> typing.Mapping[snowflakes.Snowflake, Member]:
-        """Get the members cached for the guild.
-
-        typing.Mapping[hikari.snowflakes.Snowflake, Member]
-            A mapping of user IDs to objects of the members cached for the guild.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return {}
-
-        return self.app.cache.get_members_view_for_guild(self.id)
-
-    @property
-    def presences(self) -> typing.Mapping[snowflakes.Snowflake, presences_.MemberPresence]:
-        """Get the presences cached for the guild.
-
-        typing.Mapping[hikari.snowflakes.Snowflake, hikari.presences.MemberPresence]
-            A mapping of user IDs to objects of the presences cached for the
-            guild.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return {}
-
-        return self.app.cache.get_presences_view_for_guild(self.id)
-
     @property
     def roles(self) -> typing.Mapping[snowflakes.Snowflake, Role]:
         # <<inherited docstring from Guild>>.
@@ -1497,42 +1619,6 @@ def roles(self) -> typing.Mapping[snowflakes.Snowflake, Role]:
 
         return self.app.cache.get_roles_view_for_guild(self.id)
 
-    @property
-    def voice_states(self) -> typing.Mapping[snowflakes.Snowflake, voices_.VoiceState]:
-        """Get the voice states cached for the guild.
-
-        Returns
-        -------
-        typing.Mapping[hikari.snowflakes.Snowflake, hikari.voices.VoiceState]
-            A mapping of user IDs to objects of the voice states cached for the
-            guild.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return {}
-
-        return self.app.cache.get_voice_states_view_for_guild(self.id)
-
-    def get_channel(
-        self,
-        channel: snowflakes.SnowflakeishOr[channels_.PartialChannel],
-    ) -> typing.Optional[channels_.GuildChannel]:
-        """Get a cached channel that belongs to the guild by it's ID or object.
-
-        Parameters
-        ----------
-        channel : hikari.snowflakes.SnowflakeishOr[hikari.channels.PartialChannel]
-            The object or ID of the guild channel to get from the cache.
-
-        Returns
-        -------
-        typing.Optional[hikari.channels.GuildChannel]
-            The object of the guild channel found in cache or `builtins.None.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return None
-
-        return self.app.cache.get_guild_channel(channel)
-
     def get_emoji(
         self, emoji: snowflakes.SnowflakeishOr[emojis_.CustomEmoji]
     ) -> typing.Optional[emojis_.KnownCustomEmoji]:
@@ -1554,61 +1640,6 @@ def get_emoji(
 
         return self.app.cache.get_emoji(emoji)
 
-    def get_member(self, user: snowflakes.SnowflakeishOr[users.PartialUser]) -> typing.Optional[Member]:
-        """Get a cached member that belongs to the guild by it's user ID or object.
-
-        Parameters
-        ----------
-        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
-            The object or ID of the user to get the cached member for.
-
-        Returns
-        -------
-        typing.Optional[Member]
-            The cached member object if found, else `builtins.None`.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return None
-
-        return self.app.cache.get_member(self.id, user)
-
-    def get_my_member(self) -> typing.Optional[Member]:
-        """Return the cached member for the bot user in this guild, if known.
-
-        Returns
-        -------
-        typing.Optional[Member]
-            The cached member for this guild, or `builtins.None` if not known.
-        """
-        if not isinstance(self.app, traits.ShardAware):
-            return None
-
-        me = self.app.me
-        if me is None:
-            return None
-
-        return self.get_member(me.id)
-
-    def get_presence(
-        self, user: snowflakes.SnowflakeishOr[users.PartialUser]
-    ) -> typing.Optional[presences_.MemberPresence]:
-        """Get a cached presence that belongs to the guild by it's user ID or object.
-
-        Parameters
-        ----------
-        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
-            The object or ID of the user to get the cached presence for.
-
-        Returns
-        -------
-        typing.Optional[hikari.presences.MemberPresence]
-            The cached presence object if found, else `builtins.None`.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return None
-
-        return self.app.cache.get_presence(self.id, user)
-
     def get_role(self, role: snowflakes.SnowflakeishOr[PartialRole]) -> typing.Optional[Role]:
         """Get a cached role that belongs to the guild by it's ID or object.
 
@@ -1626,23 +1657,3 @@ def get_role(self, role: snowflakes.SnowflakeishOr[PartialRole]) -> typing.Optio
             return None
 
         return self.app.cache.get_role(role)
-
-    def get_voice_state(
-        self, user: snowflakes.SnowflakeishOr[users.PartialUser]
-    ) -> typing.Optional[voices_.VoiceState]:
-        """Get a cached voice state that belongs to the guild by it's user.
-
-        Parameters
-        ----------
-        user : hikari.snowflakes.SnowflakeishOr[hikari.users.PartialUser]
-            The object or ID of the user to get the cached voice state for.
-
-        Returns
-        -------
-        typing.Optional[hikari.voices.VoiceState]
-            The cached voice state object if found, else `builtins.None`.
-        """
-        if not isinstance(self.app, traits.CacheAware):
-            return None
-
-        return self.app.cache.get_voice_state(self.id, user)


Test patch:
diff --git a/tests/hikari/test_channels.py b/tests/hikari/test_channels.py
index 2682a46f..36599200 100644
--- a/tests/hikari/test_channels.py
+++ b/tests/hikari/test_channels.py
@@ -284,3 +284,6 @@ def __getattr__(self, name):
     def test_shard_id_property_when_guild_id_is_not_None(self, model):
         model.app.shard_count = 3
         assert model.shard_id == 2
+
+    def test_mention_property(self, model):
+        assert model.mention == "<#69420>"
diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py
index 47c630c3..c462a3c6 100644
--- a/tests/hikari/test_guilds.py
+++ b/tests/hikari/test_guilds.py
@@ -52,6 +52,9 @@ def model(self, mock_app):
     def test_str_operator(self, model):
         assert str(model) == "The Big Cool"
 
+    def test_mention_property(self, model):
+        assert model.mention == "<@&1106913972>"
+
 
 def test_PartialApplication_str_operator():
     mock_application = mock.Mock(guilds.PartialApplication)
@@ -482,6 +485,38 @@ def model(self, mock_app):
             system_channel_flags=guilds.GuildSystemChannelFlag.SUPPRESS_PREMIUM_SUBSCRIPTION,
         )
 
+    def test_channels(self, model):
+        assert model.channels is model.app.cache.get_guild_channels_view_for_guild.return_value
+        model.app.cache.get_guild_channels_view_for_guild.assert_called_once_with(123)
+
+    def test_channels_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.channels == {}
+
+    def test_members(self, model):
+        assert model.members is model.app.cache.get_members_view_for_guild.return_value
+        model.app.cache.get_members_view_for_guild.assert_called_once_with(123)
+
+    def test_members_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.members == {}
+
+    def test_presences(self, model):
+        assert model.presences is model.app.cache.get_presences_view_for_guild.return_value
+        model.app.cache.get_presences_view_for_guild.assert_called_once_with(123)
+
+    def test_presences_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.presences == {}
+
+    def test_voice_states(self, model):
+        assert model.voice_states is model.app.cache.get_voice_states_view_for_guild.return_value
+        model.app.cache.get_voice_states_view_for_guild.assert_called_once_with(123)
+
+    def test_voice_states_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.voice_states == {}
+
     def test_splash_url(self, model):
         splash = object()
 
@@ -558,6 +593,54 @@ def test_make_banner_url_when_no_hash(self, model):
         model.banner_hash = None
         assert model.make_banner_url(ext="png", size=2048) is None
 
+    def test_get_channel(self, model):
+        assert model.get_channel(456) is model.app.cache.get_guild_channel.return_value
+        model.app.cache.get_guild_channel.assert_called_once_with(456)
+
+    def test_get_channel_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.get_channel(456) is None
+
+    def test_get_member(self, model):
+        assert model.get_member(456) is model.app.cache.get_member.return_value
+        model.app.cache.get_member.assert_called_once_with(123, 456)
+
+    def test_get_member_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.get_member(456) is None
+
+    def test_get_presence(self, model):
+        assert model.get_presence(456) is model.app.cache.get_presence.return_value
+        model.app.cache.get_presence.assert_called_once_with(123, 456)
+
+    def test_get_presence_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.get_presence(456) is None
+
+    def test_get_voice_state(self, model):
+        assert model.get_voice_state(456) is model.app.cache.get_voice_state.return_value
+        model.app.cache.get_voice_state.assert_called_once_with(123, 456)
+
+    def test_get_voice_state_when_no_cache_trait(self, model):
+        model.app = object()
+        assert model.get_voice_state(456) is None
+
+    def test_get_my_member_when_not_shardaware(self, model):
+        model.app = object()
+        assert model.get_my_member() is None
+
+    def test_get_my_member_when_no_me(self, model):
+        model.app.me = None
+        assert model.get_my_member() is None
+
+    def test_get_my_member(self, model):
+        model.app.me = mock.Mock(id=123)
+
+        with mock.patch.object(guilds.Guild, "get_member") as get_member:
+            assert model.get_my_member() is get_member.return_value
+
+        get_member.assert_called_once_with(123)
+
 
 class TestRestGuild:
     @pytest.fixture()
@@ -651,14 +734,6 @@ def model(self, mock_app):
             nsfw_level=guilds.GuildNSFWLevel.AGE_RESTRICTED,
         )
 
-    def test_channels(self, model):
-        assert model.channels is model.app.cache.get_guild_channels_view_for_guild.return_value
-        model.app.cache.get_guild_channels_view_for_guild.assert_called_once_with(123)
-
-    def test_channels_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.channels == {}
-
     def test_emojis(self, model):
         assert model.emojis is model.app.cache.get_emojis_view_for_guild.return_value
         model.app.cache.get_emojis_view_for_guild.assert_called_once_with(123)
@@ -667,22 +742,6 @@ def test_emojis_when_no_cache_trait(self, model):
         model.app = object()
         assert model.emojis == {}
 
-    def test_members(self, model):
-        assert model.members is model.app.cache.get_members_view_for_guild.return_value
-        model.app.cache.get_members_view_for_guild.assert_called_once_with(123)
-
-    def test_members_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.members == {}
-
-    def test_presences(self, model):
-        assert model.presences is model.app.cache.get_presences_view_for_guild.return_value
-        model.app.cache.get_presences_view_for_guild.assert_called_once_with(123)
-
-    def test_presences_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.presences == {}
-
     def test_roles(self, model):
         assert model.roles is model.app.cache.get_roles_view_for_guild.return_value
         model.app.cache.get_roles_view_for_guild.assert_called_once_with(123)
@@ -691,22 +750,6 @@ def test_roles_when_no_cache_trait(self, model):
         model.app = object()
         assert model.roles == {}
 
-    def test_voice_states(self, model):
-        assert model.voice_states is model.app.cache.get_voice_states_view_for_guild.return_value
-        model.app.cache.get_voice_states_view_for_guild.assert_called_once_with(123)
-
-    def test_voice_states_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.voice_states == {}
-
-    def test_get_channel(self, model):
-        assert model.get_channel(456) is model.app.cache.get_guild_channel.return_value
-        model.app.cache.get_guild_channel.assert_called_once_with(456)
-
-    def test_get_channel_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.get_channel(456) is None
-
     def test_get_emoji(self, model):
         assert model.get_emoji(456) is model.app.cache.get_emoji.return_value
         model.app.cache.get_emoji.assert_called_once_with(456)
@@ -715,22 +758,6 @@ def test_get_emoji_when_no_cache_trait(self, model):
         model.app = object()
         assert model.get_emoji(456) is None
 
-    def test_get_member(self, model):
-        assert model.get_member(456) is model.app.cache.get_member.return_value
-        model.app.cache.get_member.assert_called_once_with(123, 456)
-
-    def test_get_member_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.get_member(456) is None
-
-    def test_get_presence(self, model):
-        assert model.get_presence(456) is model.app.cache.get_presence.return_value
-        model.app.cache.get_presence.assert_called_once_with(123, 456)
-
-    def test_get_presence_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.get_presence(456) is None
-
     def test_get_role(self, model):
         assert model.get_role(456) is model.app.cache.get_role.return_value
         model.app.cache.get_role.assert_called_once_with(456)
@@ -738,27 +765,3 @@ def test_get_role(self, model):
     def test_get_role_when_no_cache_trait(self, model):
         model.app = object()
         assert model.get_role(456) is None
-
-    def test_get_voice_state(self, model):
-        assert model.get_voice_state(456) is model.app.cache.get_voice_state.return_value
-        model.app.cache.get_voice_state.assert_called_once_with(123, 456)
-
-    def test_get_voice_state_when_no_cache_trait(self, model):
-        model.app = object()
-        assert model.get_voice_state(456) is None
-
-    def test_get_my_member_when_not_shardaware(self, model):
-        model.app = object()
-        assert model.get_my_member() is None
-
-    def test_get_my_member_when_no_me(self, model):
-        model.app.me = None
-        assert model.get_my_member() is None
-
-    def test_get_my_member(self, model):
-        model.app.me = mock.Mock(id=123)
-
-        with mock.patch.object(guilds.GatewayGuild, "get_member") as get_member:
-            assert model.get_my_member() is get_member.return_value
-
-        get_member.assert_called_once_with(123)