Repository: Rapptz/discord.py

Issue:
Member.top_role not updating when positions change
### Summary

If a role position is changed, calling member.top_role does not update the top role if it has changed. It still shows whatever the old top position was.

Only similar issue still open is #2142 but that involved role creation and the cache, where these roles are being edited and the new position is set correctly. 

### Expected Results

The top_role property to always return the role with the highest position.

### Actual Results
The old top role is still returned as the top role, despite it being moved to a lower position or another role being moved higher. The output of the following code in an on_message(message) event shows the issue.

    msg = "Roles:"
    for myrole in message.guild.me.roles:
        msg += " Name: " + myrole.name + " Position:" + str(myrole.position)
    print(msg)
    msg = "Top Role: " + message.guild.me.top_role.name + " " str(message.guild.me.top_role.position)
    print(msg)

> Roles: Name: @everyone Position:0 Name: PicartoBot-Dev Position:2 Name: Trixie Position:5
> Top Role: Trixie 5

The highest role is Trixie, as it should be. After moving the Trixie role to just above @everyone (Position 1) and rerunning:

> Roles: Name: @everyone Position:0 Name: PicartoBot-Dev Position:3 Name: Trixie Position:1
> Top Role: Trixie 1

It still lists Trixie as the highest role, despite having updated the role with it's new position.

Looking at the code for Member.top_role, it seems like the list of roles should be sorted in ascending position order, but it's not being resorted when the role position is updated, thus causing top_role to return the wrong role.

### Checklist

<!-- Put an x inside [ ] to check it, like so: [x] -->

- [x] I have searched the open issues for duplicates.
- [x] I have shown the entire traceback, if possible.
- [x] I have removed my token from display, if visible.

### System Information

- Python v3.7.2-final
- discord.py v1.3.3-final
- aiohttp v3.6.2
- websockets v6.0
- system info: Windows 10 10.0.18362



Discussion:
I believe this happens because the cache for roles is not invalidated upon roles being updated. Invalidating the cache at this stage becomes kind of expensive so I may have to reconsider the cache layer here to properly fix this. Unsure at the moment.

Resolving patch:
diff --git a/discord/member.py b/discord/member.py
--- a/discord/member.py
+++ b/discord/member.py
@@ -161,7 +161,7 @@ class Member(discord.abc.Messageable, _BaseUser):
         Nitro boost on the guild, if available. This could be ``None``.
     """
 
-    __slots__ = ('_roles', '_cs_roles', 'joined_at', 'premium_since', '_client_status',
+    __slots__ = ('_roles', 'joined_at', 'premium_since', '_client_status',
                  'activities', 'guild', 'nick', '_user', '_state')
 
     def __init__(self, *, data, guild, state):
@@ -234,11 +234,6 @@ def _copy(cls, member):
         self.activities = member.activities
         self._state = member._state
 
-        try:
-            del self._cs_roles
-        except AttributeError:
-            pass
-
         # Reference will not be copied unless necessary by PRESENCE_UPDATE
         # See below
         self._user = member._user
@@ -250,10 +245,6 @@ async def _get_channel(self):
 
     def _update_roles(self, data):
         self._roles = utils.SnowflakeList(map(int, data['roles']))
-        try:
-            del self._cs_roles
-        except AttributeError:
-            pass
 
     def _update(self, data):
         # the nickname change is optional,
@@ -344,7 +335,7 @@ def color(self):
         """
         return self.colour
 
-    @utils.cached_slot_property('_cs_roles')
+    @property
     def roles(self):
         """List[:class:`Role`]: A :class:`list` of :class:`Role` that the member belongs to. Note
         that the first element of this list is always the default '@everyone'
@@ -434,7 +425,11 @@ def top_role(self):
         This is useful for figuring where a member stands in the role
         hierarchy chain.
         """
-        return self.roles[-1]
+        guild = self.guild
+        if len(self._roles) == 0:
+            return guild.default_role
+        
+        return max(guild.get_role(rid) or guild.default_role for rid in self._roles)
 
     @property
     def guild_permissions(self):


Test patch: