From 696dcbfa7682d4b435dfc3fda4f4c3ea1e1ed066 Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 6 Feb 2025 09:48:18 +0100 Subject: [PATCH 01/31] :recycle: Rewrite `RoleTags.bot_id` handling --- discord/role.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/discord/role.py b/discord/role.py index 09423b168c..641dd0ec8d 100644 --- a/discord/role.py +++ b/discord/role.py @@ -33,7 +33,13 @@ from .flags import RoleFlags from .mixins import Hashable from .permissions import Permissions -from .utils import MISSING, _bytes_to_base64_data, _get_as_snowflake, snowflake_time +from .utils import ( + MISSING, + _bytes_to_base64_data, + _get_as_snowflake, + cached_slot_property, + snowflake_time, +) __all__ = ( "RoleTags", @@ -79,16 +85,18 @@ class RoleTags: """ __slots__ = ( - "bot_id", "integration_id", "subscription_listing_id", "_premium_subscriber", "_available_for_purchase", "_guild_connections", + "_bot_id", + "_bot_role", + "_data", ) def __init__(self, data: RoleTagPayload): - self.bot_id: int | None = _get_as_snowflake(data, "bot_id") + self._data: RoleTagPayload = data self.integration_id: int | None = _get_as_snowflake(data, "integration_id") self.subscription_listing_id: int | None = _get_as_snowflake( data, "subscription_listing_id" @@ -104,7 +112,13 @@ def __init__(self, data: RoleTagPayload): ) self._guild_connections: Any | None = data.get("guild_connections", MISSING) - def is_bot_managed(self) -> bool: + @cached_slot_property("_bot_id") + def bot_id(self) -> int | None: + """The bot's user ID that manages this role.""" + return int(self._data.get("bot_id", 0) or 0) or None + + @cached_slot_property("_bot_role") + def is_bot_role(self) -> bool: """Whether the role is associated with a bot.""" return self.bot_id is not None From 54d030dde58e67a68c7eb40904a49d3909d7113b Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 6 Feb 2025 16:49:17 +0100 Subject: [PATCH 02/31] :recycle: Rewrite this mess --- discord/role.py | 223 ++++++++++++++++++++++++++++++------------------ 1 file changed, 142 insertions(+), 81 deletions(-) diff --git a/discord/role.py b/discord/role.py index 641dd0ec8d..19623b68e0 100644 --- a/discord/role.py +++ b/discord/role.py @@ -25,7 +25,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, Final, TypeVar from .asset import Asset from .colour import Colour @@ -36,8 +36,6 @@ from .utils import ( MISSING, _bytes_to_base64_data, - _get_as_snowflake, - cached_slot_property, snowflake_time, ) @@ -57,20 +55,72 @@ from .types.role import RoleTags as RoleTagPayload +def _parse_tag_bool(data: RoleTagPayload, key: str) -> bool | None: + """Parse a boolean from a role tag payload. + + None is returned if the key is not present. + True is returned if the key is present and the value is None. + False is returned if the key is present and the value is not None. + + Parameters + ---------- + data: :class:`RoleTagPayload` + The role tag payload to parse from. + key: :class:`str` + The key to parse from. + + Returns + ------- + :class:`bool` | :class:`None` + The parsed boolean value or None if the key is not present. + """ + try: + # if it is False, False != None -> False + # if it is None, None == None -> True + return data[key] is None + except KeyError: + # if the key is not present, None + return None + + +def _parse_tag_int(data: RoleTagPayload, key: str) -> int | None: + """Parse an integer from a role tag payload. + + An integer is returned if the key is present and the value is an integer string. + None is returned if the key is not present or the value is not an integer string. + + Parameters + ---------- + data: :class:`RoleTagPayload` + The role tag payload to parse from. + key: :class:`str` + The key to parse from. + + Returns + ------- + :class:`int` | :class:`None` + The parsed integer value or None if the key is not present or the value is not an integer string. + """ + try: + return int(data[key]) # pyright: ignore[reportUnknownArgumentType] + except (KeyError, ValueError): + # key error means it's not there + # value error means it's not an number string (None or "") + return None + + class RoleTags: """Represents tags on a role. A role tag is a piece of extra information attached to a managed role that gives it context for the reason the role is managed. - While this can be accessed, a useful interface is also provided in the - :class:`Role` and :class:`Guild` classes as well. - Role tags are a fairly complex topic, since it's usually hard to determine which role tag combination represents which role type. We aim to improve the documentation / introduce new attributes in future. For the meantime read `this `_ if you need detailed information about how role tags work. .. versionadded:: 1.6 + .. versionchanged:: 2.7 Attributes ---------- @@ -90,73 +140,123 @@ class RoleTags: "_premium_subscriber", "_available_for_purchase", "_guild_connections", - "_bot_id", - "_bot_role", + "bot_id", "_data", ) def __init__(self, data: RoleTagPayload): self._data: RoleTagPayload = data - self.integration_id: int | None = _get_as_snowflake(data, "integration_id") - self.subscription_listing_id: int | None = _get_as_snowflake( + self.integration_id: int | None = _parse_tag_int(data, "integration_id") + self.subscription_listing_id: int | None = _parse_tag_int( data, "subscription_listing_id" ) - # NOTE: The API returns "null" for each of the following tags if they are True, and omits them if False. - # However, "null" corresponds to None. - # This is different from other fields where "null" means "not there". - # So in this case, a value of None is the same as True. - # Which means we would need a different sentinel. - self._premium_subscriber: Any | None = data.get("premium_subscriber", MISSING) - self._available_for_purchase: Any | None = data.get( - "available_for_purchase", MISSING + self.bot_id: int | None = _parse_tag_int(data, "bot_id") + self._guild_connections: bool | None = _parse_tag_bool( + data, "guild_connections" + ) + self._premium_subscriber: bool | None = _parse_tag_bool( + data, "premium_subscriber" + ) + self._available_for_purchase: bool | None = _parse_tag_bool( + data, "available_for_purchase" ) - self._guild_connections: Any | None = data.get("guild_connections", MISSING) - - @cached_slot_property("_bot_id") - def bot_id(self) -> int | None: - """The bot's user ID that manages this role.""" - return int(self._data.get("bot_id", 0) or 0) or None - @cached_slot_property("_bot_role") + @property def is_bot_role(self) -> bool: - """Whether the role is associated with a bot.""" + """Whether the role is associated with a bot. + .. versionadded:: 2.7 + """ return self.bot_id is not None - def is_premium_subscriber(self) -> bool: - """Whether the role is the premium subscriber, AKA "boost", role for the guild.""" - return self._premium_subscriber is None + @property + def is_booster_role(self) -> bool: + """Whether the role is the "boost", role for the guild. + .. versionadded:: 2.7 + """ + return self._guild_connections is False and self._premium_subscriber is True + + @property + def is_guild_product_role(self) -> bool: + """Whether the role is a guild product role. + + .. versionadded:: 2.7 + """ + return self._guild_connections is False and self._premium_subscriber is False + @property def is_integration(self) -> bool: """Whether the guild manages the role through some form of integrations such as Twitch or through guild subscriptions. """ return self.integration_id is not None - def is_available_for_purchase(self) -> bool: - """Whether the role is available for purchase. + @property + def is_base_subscription_role(self) -> bool: + """Whether the role is a base subscription role. - Returns ``True`` if the role is available for purchase, and - ``False`` if it is not available for purchase or if the role - is not linked to a guild subscription. + .. versionadded:: 2.7 + """ + return ( + self._guild_connections is False + and self._premium_subscriber is False + and self.integration_id is not None + ) + + @property + def is_subscription_role(self) -> bool: + """Whether the role is a subscription role. .. versionadded:: 2.7 """ - return self._available_for_purchase is None + return ( + self._guild_connections is False + and self._premium_subscriber is None + and self.integration_id is not None + and self.subscription_listing_id is not None + and self._available_for_purchase is True + ) + + @property + def is_draft_subscription_role(self) -> bool: + """Whether the role is a draft subscription role. + + .. versionadded:: 2.7 + """ + return ( + self._guild_connections is False + and self._premium_subscriber is None + and self.subscription_listing_id is not None + and self.integration_id is not None + and self._available_for_purchase is False + ) + @property def is_guild_connections_role(self) -> bool: """Whether the role is a guild connections role. .. versionadded:: 2.7 """ - return self._guild_connections is None + return self._guild_connections is True + + QUALIFIERS: Final = ( + "is_bot_role", + "is_booster_role", + "is_guild_product_role", + "is_integration", + "is_base_subscription_role", + "is_subscription_role", + "is_draft_subscription_role", + "is_guild_connections_role", + ) def __repr__(self) -> str: return ( f"" + + f"subscription_listing_id={self.subscription_listing_id} " + + " ".join( + q.removeprefix("is_") for q in self.QUALIFIERS if getattr(self, q) + ) + + ">" ) @@ -230,7 +330,8 @@ class Role(Hashable): mentionable: :class:`bool` Indicates if the role can be mentioned by users. tags: Optional[:class:`RoleTags`] - The role tags associated with this role. + The role tags associated with this role. Use the tags to determine additional information about the role, + like if it's a bot role, a booster role, etc... unicode_emoji: Optional[:class:`str`] The role's unicode emoji. Only available to guilds that contain ``ROLE_ICONS`` in :attr:`Guild.features`. @@ -330,28 +431,6 @@ def is_default(self) -> bool: """Checks if the role is the default role.""" return self.guild.id == self.id - def is_bot_managed(self) -> bool: - """Whether the role is associated with a bot. - - .. versionadded:: 1.6 - """ - return self.tags is not None and self.tags.is_bot_managed() - - def is_premium_subscriber(self) -> bool: - """Whether the role is the premium subscriber, AKA "boost", role for the guild. - - .. versionadded:: 1.6 - """ - return self.tags is not None and self.tags.is_premium_subscriber() - - def is_integration(self) -> bool: - """Whether the guild manages the role through some form of - integrations such as Twitch or through guild subscriptions. - - .. versionadded:: 1.6 - """ - return self.tags is not None and self.tags.is_integration() - def is_assignable(self) -> bool: """Whether the role is able to be assigned or removed by the bot. @@ -364,24 +443,6 @@ def is_assignable(self) -> bool: and (me.top_role > self or me.id == self.guild.owner_id) ) - def is_available_for_purchase(self) -> bool: - """Whether the role is available for purchase. - - Returns ``True`` if the role is available for purchase, and - ``False`` if it is not available for purchase or if the - role is not linked to a guild subscription. - - .. versionadded:: 2.7 - """ - return self.tags is not None and self.tags.is_available_for_purchase() - - def is_guild_connections_role(self) -> bool: - """Whether the role is a guild connections role. - - .. versionadded:: 2.7 - """ - return self.tags is not None and self.tags.is_guild_connections_role() - @property def permissions(self) -> Permissions: """Returns the role's permissions.""" From 5810fa32b8e1735a9bce8698610b8fc5c1074fd7 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Sat, 15 Feb 2025 14:25:53 +0100 Subject: [PATCH 03/31] :sparkles: Things --- discord/role.py | 225 +++++++++++++++++++++++++++++++----------------- 1 file changed, 145 insertions(+), 80 deletions(-) diff --git a/discord/role.py b/discord/role.py index 19623b68e0..9a79a53ee3 100644 --- a/discord/role.py +++ b/discord/role.py @@ -25,7 +25,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Final, TypeVar +from enum import IntEnum +from typing import TYPE_CHECKING, Any, TypeVar from .asset import Asset from .colour import Colour @@ -36,6 +37,8 @@ from .utils import ( MISSING, _bytes_to_base64_data, + cached_slot_property, + deprecated, snowflake_time, ) @@ -109,6 +112,43 @@ def _parse_tag_int(data: RoleTagPayload, key: str) -> int | None: return None +class RoleType(IntEnum): + """Represents the type of role. + + This is NOT provided by discord but is rather computed by pycord based on the role tags. + + .. versionadded:: 2.7 + + Attributes + ---------- + APPLICATION: :class:`int` + The role is an application role. + BOOSTER: :class:`int` + The role is a guild's booster role. + GUILD_PRODUCT: :class:`int` + The role is a guild product role. + PREMIUM_SUBSCRIPTION_BASE: :class:`int` + The role is a base subscription role. + PREMIUM_SUBSCRIPTION_TIER: :class:`int` + The role is a subscription role. + DRAFT_PREMIUM_SUBSCRIPTION_TIER: :class:`int` + The role is a draft subscription role. + INTEGRATION: :class:`int` + The role is an integration role. + CONNECTION: :class:`int` + The role is a guild connections role. + """ + + APPLICATION = 1 + BOOSTER = 2 + GUILD_PRODUCT = 3 + PREMIUM_SUBSCRIPTION_BASE = 4 # Not possible to determine currently, will be INTEGRATION if it's a base subscription + PREMIUM_SUBSCRIPTION_TIER = 5 + DRAFT_PREMIUM_SUBSCRIPTION_TIER = 5 + INTEGRATION = 7 + CONNECTION = 8 + + class RoleTags: """Represents tags on a role. @@ -142,6 +182,7 @@ class RoleTags: "_guild_connections", "bot_id", "_data", + "_type", ) def __init__(self, data: RoleTagPayload): @@ -161,102 +202,73 @@ def __init__(self, data: RoleTagPayload): data, "available_for_purchase" ) - @property - def is_bot_role(self) -> bool: - """Whether the role is associated with a bot. - .. versionadded:: 2.7 - """ + @cached_slot_property("_type") + def type(self) -> RoleType: + """Determine the role type based on tag flags.""" + # Bot role + if self.bot_id is not None: + return RoleType.APPLICATION + + # Role connection + if self._guild_connections is True: + return RoleType.CONNECTION + + # Paid roles + if self._guild_connections is False: + if self._premium_subscriber is False: + return RoleType.GUILD_PRODUCT + + if self._premium_subscriber is True: + return RoleType.BOOSTER + + # subscription roles + if self.integration_id is not None: + if ( + self._premium_subscriber is None + and self.subscription_listing_id is not None + ): + if self._available_for_purchase is True: + return RoleType.PREMIUM_SUBSCRIPTION_TIER + return RoleType.DRAFT_PREMIUM_SUBSCRIPTION_TIER + + # integration role (Twitch/YouTube) + if self.integration_id is not None: + return RoleType.INTEGRATION + + raise ValueError("Unable to determine the role type based on provided tags.") + + @deprecated("RoleTags.type", "2.7") + def is_bot_managed(self) -> bool: + """Whether the role is associated with a bot.""" return self.bot_id is not None - @property - def is_booster_role(self) -> bool: - """Whether the role is the "boost", role for the guild. - .. versionadded:: 2.7 - """ - return self._guild_connections is False and self._premium_subscriber is True - - @property - def is_guild_product_role(self) -> bool: - """Whether the role is a guild product role. + @deprecated("RoleTags.type", "2.7") + def is_premium_subscriber(self) -> bool: + """Whether the role is the premium subscriber, AKA "boost", role for the guild.""" + return self._premium_subscriber is None - .. versionadded:: 2.7 - """ - return self._guild_connections is False and self._premium_subscriber is False - - @property + @deprecated("RoleTags.type", "2.7") def is_integration(self) -> bool: """Whether the guild manages the role through some form of integrations such as Twitch or through guild subscriptions. """ return self.integration_id is not None - @property - def is_base_subscription_role(self) -> bool: - """Whether the role is a base subscription role. - - .. versionadded:: 2.7 - """ - return ( - self._guild_connections is False - and self._premium_subscriber is False - and self.integration_id is not None - ) - - @property - def is_subscription_role(self) -> bool: - """Whether the role is a subscription role. - - .. versionadded:: 2.7 - """ - return ( - self._guild_connections is False - and self._premium_subscriber is None - and self.integration_id is not None - and self.subscription_listing_id is not None - and self._available_for_purchase is True - ) - - @property - def is_draft_subscription_role(self) -> bool: - """Whether the role is a draft subscription role. - - .. versionadded:: 2.7 - """ - return ( - self._guild_connections is False - and self._premium_subscriber is None - and self.subscription_listing_id is not None - and self.integration_id is not None - and self._available_for_purchase is False - ) + @deprecated("RoleTags.type", "2.7") + def is_available_for_purchase(self) -> bool: + """Whether the role is available for purchase.""" + return self._available_for_purchase is True - @property + @deprecated("RoleTags.type", "2.7") def is_guild_connections_role(self) -> bool: - """Whether the role is a guild connections role. - - .. versionadded:: 2.7 - """ + """Whether the role is a guild connections role.""" return self._guild_connections is True - QUALIFIERS: Final = ( - "is_bot_role", - "is_booster_role", - "is_guild_product_role", - "is_integration", - "is_base_subscription_role", - "is_subscription_role", - "is_draft_subscription_role", - "is_guild_connections_role", - ) - def __repr__(self) -> str: return ( f"" + + f"type={self.type!r}>" ) @@ -431,6 +443,31 @@ def is_default(self) -> bool: """Checks if the role is the default role.""" return self.guild.id == self.id + @deprecated("Role.type", "2.7") + def is_bot_managed(self) -> bool: + """Whether the role is associated with a bot. + + .. versionadded:: 1.6 + """ + return self.tags is not None and self.tags.is_bot_managed() + + @deprecated("Role.type", "2.7") + def is_premium_subscriber(self) -> bool: + """Whether the role is the premium subscriber, AKA "boost", role for the guild. + + .. versionadded:: 1.6 + """ + return self.tags is not None and self.tags.is_premium_subscriber() + + @deprecated("Role.type", "2.7") + def is_integration(self) -> bool: + """Whether the guild manages the role through some form of + integrations such as Twitch or through guild subscriptions. + + .. versionadded:: 1.6 + """ + return self.tags is not None and self.tags.is_integration() + def is_assignable(self) -> bool: """Whether the role is able to be assigned or removed by the bot. @@ -443,6 +480,26 @@ def is_assignable(self) -> bool: and (me.top_role > self or me.id == self.guild.owner_id) ) + @deprecated("Role.type", "2.7") + def is_available_for_purchase(self) -> bool: + """Whether the role is available for purchase. + + Returns ``True`` if the role is available for purchase, and + ``False`` if it is not available for purchase or if the + role is not linked to a guild subscription. + + .. versionadded:: 2.7 + """ + return self.tags is not None and self.tags.is_available_for_purchase() + + @deprecated("Role.type", "2.7") + def is_guild_connections_role(self) -> bool: + """Whether the role is a guild connections role. + + .. versionadded:: 2.7 + """ + return self.tags is not None and self.tags.is_guild_connections_role() + @property def permissions(self) -> Permissions: """Returns the role's permissions.""" @@ -489,6 +546,14 @@ def icon(self) -> Asset | None: return Asset._from_icon(self._state, self.id, self._icon, "role") + @property + def type(self) -> RoleType: + """The type of the role. + + .. versionadded:: 2.7 + """ + return self.tags.type + async def _move(self, position: int, reason: str | None) -> None: if position <= 0: raise InvalidArgument("Cannot move role to position 0 or below") From 6a918755f37d7b481cf4311694da7fbb58bd7c44 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Sat, 15 Feb 2025 14:48:08 +0100 Subject: [PATCH 04/31] :bug: Who tf made this messed up system --- discord/role.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/discord/role.py b/discord/role.py index 9a79a53ee3..96497807f9 100644 --- a/discord/role.py +++ b/discord/role.py @@ -144,9 +144,10 @@ class RoleType(IntEnum): GUILD_PRODUCT = 3 PREMIUM_SUBSCRIPTION_BASE = 4 # Not possible to determine currently, will be INTEGRATION if it's a base subscription PREMIUM_SUBSCRIPTION_TIER = 5 - DRAFT_PREMIUM_SUBSCRIPTION_TIER = 5 + DRAFT_PREMIUM_SUBSCRIPTION_TIER = 6 INTEGRATION = 7 CONNECTION = 8 + UNKNOWN = 9 class RoleTags: @@ -180,6 +181,7 @@ class RoleTags: "_premium_subscriber", "_available_for_purchase", "_guild_connections", + "_is_guild_product_role", "bot_id", "_data", "_type", @@ -201,6 +203,8 @@ def __init__(self, data: RoleTagPayload): self._available_for_purchase: bool | None = _parse_tag_bool( data, "available_for_purchase" ) + # here discord did things in a normal and logical way for once + self._is_guild_product_role: bool | None = data.get("is_guild_product_role") @cached_slot_property("_type") def type(self) -> RoleType: @@ -214,28 +218,29 @@ def type(self) -> RoleType: return RoleType.CONNECTION # Paid roles - if self._guild_connections is False: - if self._premium_subscriber is False: - return RoleType.GUILD_PRODUCT - - if self._premium_subscriber is True: - return RoleType.BOOSTER - - # subscription roles - if self.integration_id is not None: - if ( - self._premium_subscriber is None - and self.subscription_listing_id is not None - ): - if self._available_for_purchase is True: - return RoleType.PREMIUM_SUBSCRIPTION_TIER - return RoleType.DRAFT_PREMIUM_SUBSCRIPTION_TIER + if self._is_guild_product_role is True: + return RoleType.GUILD_PRODUCT + + # Booster role + if self._premium_subscriber is True: + return RoleType.BOOSTER + + # subscription roles + if ( + self.integration_id is not None + and self._premium_subscriber is None + and self.subscription_listing_id is not None + ): + if self._available_for_purchase is True: + return RoleType.PREMIUM_SUBSCRIPTION_TIER + return RoleType.DRAFT_PREMIUM_SUBSCRIPTION_TIER # integration role (Twitch/YouTube) if self.integration_id is not None: return RoleType.INTEGRATION - raise ValueError("Unable to determine the role type based on provided tags.") + # Seeing how messed up this is it wouldn't be a surprise if this happened + return RoleType.UNKNOWN @deprecated("RoleTags.type", "2.7") def is_bot_managed(self) -> bool: From 54f1baa1efea50bdf9e2b1a76d3a57ef11de8858 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Sat, 15 Feb 2025 14:53:04 +0100 Subject: [PATCH 05/31] :memo: Better docs --- discord/role.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/discord/role.py b/discord/role.py index 96497807f9..55854f11a7 100644 --- a/discord/role.py +++ b/discord/role.py @@ -122,21 +122,23 @@ class RoleType(IntEnum): Attributes ---------- APPLICATION: :class:`int` - The role is an application role. + The role is an application (bot) role. BOOSTER: :class:`int` The role is a guild's booster role. GUILD_PRODUCT: :class:`int` The role is a guild product role. PREMIUM_SUBSCRIPTION_BASE: :class:`int` - The role is a base subscription role. + The role is a base subscription role. This is not possible to determine currently, will be INTEGRATION if it's a base subscription. PREMIUM_SUBSCRIPTION_TIER: :class:`int` The role is a subscription role. DRAFT_PREMIUM_SUBSCRIPTION_TIER: :class:`int` The role is a draft subscription role. INTEGRATION: :class:`int` - The role is an integration role. + The role is an integration role, such as Twitch or YouTube, or a base subscription role. CONNECTION: :class:`int` The role is a guild connections role. + UNKNOWN: :class:`int` + The role type is unknown. """ APPLICATION = 1 @@ -157,11 +159,13 @@ class RoleTags: that gives it context for the reason the role is managed. Role tags are a fairly complex topic, since it's usually hard to determine which role tag combination represents which role type. - We aim to improve the documentation / introduce new attributes in future. - For the meantime read `this `_ if you need detailed information about how role tags work. + In order to make your life easier, pycord provides a :attr:`RoleTags.type` attribute that attempts to determine the role type based on the role tags. It's value is not provided by discord but is rather computed by pycord based on the role tags. + If you find an issue, please open an issue on `GitHub `_. + Read `this `_ if you need detailed information about how role tags work. .. versionadded:: 1.6 .. versionchanged:: 2.7 + The type of the role is now determined by the :attr:`RoleTags.type` attribute. Attributes ---------- From 5ac16bbd21681a4627724288d6ed92f91822acaf Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Sun, 16 Feb 2025 13:43:24 +0100 Subject: [PATCH 06/31] :adhesive_bandage: Fix error when tags is None --- discord/role.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index 55854f11a7..04b545665f 100644 --- a/discord/role.py +++ b/discord/role.py @@ -121,6 +121,8 @@ class RoleType(IntEnum): Attributes ---------- + NORMAL: :class:`int` + The role is a normal role. APPLICATION: :class:`int` The role is an application (bot) role. BOOSTER: :class:`int` @@ -141,6 +143,7 @@ class RoleType(IntEnum): The role type is unknown. """ + NORMAL = 0 APPLICATION = 1 BOOSTER = 2 GUILD_PRODUCT = 3 @@ -561,7 +564,7 @@ def type(self) -> RoleType: .. versionadded:: 2.7 """ - return self.tags.type + return self.tags.type if self.tags is not None else RoleType.NORMAL async def _move(self, position: int, reason: str | None) -> None: if position <= 0: From 304d70bd6370eec9add54c8d0321556e0d2b02c6 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Sun, 16 Feb 2025 14:00:04 +0100 Subject: [PATCH 07/31] :memo: CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 647fc311be..2728351144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2579](https://github.com/Pycord-Development/pycord/pull/2579)) - Added new `Subscription` object and related methods/events. ([#2564](https://github.com/Pycord-Development/pycord/pull/2564)) +- Added `RoleType` enum accessible via `Role.type` and `RoleTags.type`. + ([#2708](https://github.com/Pycord-Development/pycord/pull/2708)) ### Fixed @@ -121,6 +123,10 @@ These changes are available on the `master` branch, but have not yet been releas ([#2501](https://github.com/Pycord-Development/pycord/pull/2501)) - Deprecated `Interaction.cached_channel` in favor of `Interaction.channel`. ([#2658](https://github.com/Pycord-Development/pycord/pull/2658)) +- Deprecated for both `Role` and `RoleTags`: `is_bot_managed`, `is_premium_subscriber`, + `is_integration`, `is_available_for_purchase`, and `is_guild_connections_role`, in + favor of `Role.type` and `RoleTags.type`. + ([#2708](https://github.com/Pycord-Development/pycord/pull/2708)) ## [2.6.1] - 2024-09-15 From 3b34892000c3a26be556dcb095ca9f74fbc655fc Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 4 Mar 2025 10:38:00 +0100 Subject: [PATCH 08/31] :label: Add missing typing --- discord/role.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord/role.py b/discord/role.py index 7aea7374bb..366f3f615e 100644 --- a/discord/role.py +++ b/discord/role.py @@ -194,6 +194,8 @@ class RoleTags: "_type", ) + _type: RoleType + def __init__(self, data: RoleTagPayload): self._data: RoleTagPayload = data self.integration_id: int | None = _parse_tag_int(data, "integration_id") From 8a96f8df3f89b4821717d39ffc772afc90550452 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Fri, 13 Feb 2026 14:34:14 +0100 Subject: [PATCH 09/31] :memo: :recycle: Refactor role tag parsing and update versioning to 2.8 --- discord/role.py | 51 +++++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/discord/role.py b/discord/role.py index 46aae81399..dd3bb3592e 100644 --- a/discord/role.py +++ b/discord/role.py @@ -25,6 +25,7 @@ from __future__ import annotations +from contextlib import suppress from enum import IntEnum from typing import TYPE_CHECKING, Any, TypeVar @@ -79,13 +80,10 @@ def _parse_tag_bool(data: RoleTagPayload, key: str) -> bool | None: :class:`bool` | :class:`None` The parsed boolean value or None if the key is not present. """ - try: - # if it is False, False != None -> False - # if it is None, None == None -> True - return data[key] is None - except KeyError: - # if the key is not present, None - return None + # if it is False, False is not None -> False + # if it is None, None is None -> True + # if the key is not present, None + return data[key] is None if key in data else None def _parse_tag_int(data: RoleTagPayload, key: str) -> int | None: @@ -106,12 +104,11 @@ def _parse_tag_int(data: RoleTagPayload, key: str) -> int | None: :class:`int` | :class:`None` The parsed integer value or None if the key is not present or the value is not an integer string. """ - try: - return int(data[key]) # pyright: ignore[reportUnknownArgumentType] - except (KeyError, ValueError): - # key error means it's not there - # value error means it's not an number string (None or "") - return None + if value := data.get(key): + with suppress(ValueError): + # value error means it's not an number string (None or "") + return int(data[key]) # pyright: ignore[reportUnknownArgumentType] + return None class RoleType(IntEnum): @@ -119,7 +116,7 @@ class RoleType(IntEnum): This is NOT provided by discord but is rather computed by pycord based on the role tags. - .. versionadded:: 2.7 + .. versionadded:: 2.8 Attributes ---------- @@ -169,7 +166,7 @@ class RoleTags: Read `this `_ if you need detailed information about how role tags work. .. versionadded:: 1.6 - .. versionchanged:: 2.7 + .. versionchanged:: 2.8 The type of the role is now determined by the :attr:`RoleTags.type` attribute. Attributes @@ -219,7 +216,7 @@ def __init__(self, data: RoleTagPayload): @cached_slot_property("_type") def type(self) -> RoleType: - """Determine the role type based on tag flags.""" + """The type of the role.""" # Bot role if self.bot_id is not None: return RoleType.APPLICATION @@ -253,29 +250,29 @@ def type(self) -> RoleType: # Seeing how messed up this is it wouldn't be a surprise if this happened return RoleType.UNKNOWN - @deprecated("RoleTags.type", "2.7") + @deprecated("RoleTags.type", "2.8") def is_bot_managed(self) -> bool: """Whether the role is associated with a bot.""" return self.bot_id is not None - @deprecated("RoleTags.type", "2.7") + @deprecated("RoleTags.type", "2.8") def is_premium_subscriber(self) -> bool: """Whether the role is the premium subscriber, AKA "boost", role for the guild.""" return self._premium_subscriber is None - @deprecated("RoleTags.type", "2.7") + @deprecated("RoleTags.type", "2.8") def is_integration(self) -> bool: """Whether the guild manages the role through some form of integrations such as Twitch or through guild subscriptions. """ return self.integration_id is not None - @deprecated("RoleTags.type", "2.7") + @deprecated("RoleTags.type", "2.8") def is_available_for_purchase(self) -> bool: """Whether the role is available for purchase.""" return self._available_for_purchase is True - @deprecated("RoleTags.type", "2.7") + @deprecated("RoleTags.type", "2.8") def is_guild_connections_role(self) -> bool: """Whether the role is a guild connections role.""" return self._guild_connections is True @@ -556,7 +553,7 @@ def is_default(self) -> bool: """Checks if the role is the default role.""" return self.guild.id == self.id - @deprecated("Role.type", "2.7") + @deprecated("Role.type", "2.8") def is_bot_managed(self) -> bool: """Whether the role is associated with a bot. @@ -564,7 +561,7 @@ def is_bot_managed(self) -> bool: """ return self.tags is not None and self.tags.is_bot_managed() - @deprecated("Role.type", "2.7") + @deprecated("Role.type", "2.8") def is_premium_subscriber(self) -> bool: """Whether the role is the premium subscriber, AKA "boost", role for the guild. @@ -572,7 +569,7 @@ def is_premium_subscriber(self) -> bool: """ return self.tags is not None and self.tags.is_premium_subscriber() - @deprecated("Role.type", "2.7") + @deprecated("Role.type", "2.8") def is_integration(self) -> bool: """Whether the guild manages the role through some form of integrations such as Twitch or through guild subscriptions. @@ -604,7 +601,7 @@ def is_assignable(self) -> bool: and me.top_role > self ) - @deprecated("Role.type", "2.7") + @deprecated("Role.type", "2.8") def is_available_for_purchase(self) -> bool: """Whether the role is available for purchase. @@ -616,7 +613,7 @@ def is_available_for_purchase(self) -> bool: """ return self.tags is not None and self.tags.is_available_for_purchase() - @deprecated("Role.type", "2.7") + @deprecated("Role.type", "2.8") def is_guild_connections_role(self) -> bool: """Whether the role is a guild connections role. @@ -692,7 +689,7 @@ def icon(self) -> Asset | None: def type(self) -> RoleType: """The type of the role. - .. versionadded:: 2.7 + .. versionadded:: 2.8 """ return self.tags.type if self.tags is not None else RoleType.NORMAL From b228e3eb3c22a4a8a959d795a5b005ee54ce5bdd Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Fri, 13 Feb 2026 14:37:31 +0100 Subject: [PATCH 10/31] :memo: Clarify comment on guild product role parsing in role tags --- discord/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index dd3bb3592e..d52ad64f33 100644 --- a/discord/role.py +++ b/discord/role.py @@ -211,7 +211,7 @@ def __init__(self, data: RoleTagPayload): self._available_for_purchase: bool | None = _parse_tag_bool( data, "available_for_purchase" ) - # here discord did things in a normal and logical way for once + # here discord did things in a normal and logical way for once so we don't have to use _parse_tag_bool self._is_guild_product_role: bool | None = data.get("is_guild_product_role") @cached_slot_property("_type") From f1ac594a9dbc158521fa0d3c758e3168669846aa Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Fri, 13 Feb 2026 14:58:40 +0100 Subject: [PATCH 11/31] :memo: Clarify and expand notes on role tag determination limitations --- discord/role.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/discord/role.py b/discord/role.py index d52ad64f33..dd367be6d9 100644 --- a/discord/role.py +++ b/discord/role.py @@ -128,8 +128,15 @@ class RoleType(IntEnum): The role is a guild's booster role. GUILD_PRODUCT: :class:`int` The role is a guild product role. + + .. note:: + This is not possible to determine at times because role tags seem to be missing altogether when + a role is fetched notably. In such cases :class:`Role.type` and `Role.tags` will both be :class:`None`. PREMIUM_SUBSCRIPTION_BASE: :class:`int` - The role is a base subscription role. This is not possible to determine currently, will be INTEGRATION if it's a base subscription. + The role is a base subscription role. + + .. note:: + This is not possible to determine currently, will be INTEGRATION if it's a base subscription. PREMIUM_SUBSCRIPTION_TIER: :class:`int` The role is a subscription role. DRAFT_PREMIUM_SUBSCRIPTION_TIER: :class:`int` @@ -145,7 +152,7 @@ class RoleType(IntEnum): NORMAL = 0 APPLICATION = 1 BOOSTER = 2 - GUILD_PRODUCT = 3 + GUILD_PRODUCT = 3 # Not possible to determine *at times* because role tags seem to be missing altogether when fetched PREMIUM_SUBSCRIPTION_BASE = 4 # Not possible to determine currently, will be INTEGRATION if it's a base subscription PREMIUM_SUBSCRIPTION_TIER = 5 DRAFT_PREMIUM_SUBSCRIPTION_TIER = 6 From a5bb30e0b05a7733905fb31b43e698291188847b Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Fri, 13 Feb 2026 15:57:28 +0100 Subject: [PATCH 12/31] :memo: Update external link in role tags documentation --- discord/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index dd367be6d9..1eeba2fffe 100644 --- a/discord/role.py +++ b/discord/role.py @@ -170,7 +170,7 @@ class RoleTags: Role tags are a fairly complex topic, since it's usually hard to determine which role tag combination represents which role type. In order to make your life easier, pycord provides a :attr:`RoleTags.type` attribute that attempts to determine the role type based on the role tags. It's value is not provided by discord but is rather computed by pycord based on the role tags. If you find an issue, please open an issue on `GitHub `_. - Read `this `_ if you need detailed information about how role tags work. + Read `this `_ if you need detailed information about how role tags work. .. versionadded:: 1.6 .. versionchanged:: 2.8 From 417523e363ca2ba37fd40b3da97d08b8eb62efed Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Fri, 13 Feb 2026 16:29:36 +0100 Subject: [PATCH 13/31] :memo: Reword issue reporting link in role tags documentation --- discord/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index 1eeba2fffe..97bf33027a 100644 --- a/discord/role.py +++ b/discord/role.py @@ -169,7 +169,7 @@ class RoleTags: Role tags are a fairly complex topic, since it's usually hard to determine which role tag combination represents which role type. In order to make your life easier, pycord provides a :attr:`RoleTags.type` attribute that attempts to determine the role type based on the role tags. It's value is not provided by discord but is rather computed by pycord based on the role tags. - If you find an issue, please open an issue on `GitHub `_. + If you find an issue, please report it on `GitHub `_. Read `this `_ if you need detailed information about how role tags work. .. versionadded:: 1.6 From dfa8012630de1c955590dc7311024b56e77af9f0 Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 17 Feb 2026 23:09:21 +0100 Subject: [PATCH 14/31] Update discord/role.py Co-authored-by: plun1331 Signed-off-by: Paillat --- discord/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index 97bf33027a..8e3a13cb3d 100644 --- a/discord/role.py +++ b/discord/role.py @@ -114,7 +114,7 @@ def _parse_tag_int(data: RoleTagPayload, key: str) -> int | None: class RoleType(IntEnum): """Represents the type of role. - This is NOT provided by discord but is rather computed by pycord based on the role tags. + This is NOT provided by Discord but is rather computed based on the role tags. .. versionadded:: 2.8 From 173af118f7fbc1bd9785b56a5939619333d20c2f Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 17 Feb 2026 23:09:50 +0100 Subject: [PATCH 15/31] Update discord/role.py Co-authored-by: plun1331 Signed-off-by: Paillat --- discord/role.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index 8e3a13cb3d..e996980344 100644 --- a/discord/role.py +++ b/discord/role.py @@ -172,7 +172,6 @@ class RoleTags: If you find an issue, please report it on `GitHub `_. Read `this `_ if you need detailed information about how role tags work. - .. versionadded:: 1.6 .. versionchanged:: 2.8 The type of the role is now determined by the :attr:`RoleTags.type` attribute. From 1c2eb0088772adb3b4e026c214b49324846e9a92 Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 17 Feb 2026 23:10:21 +0100 Subject: [PATCH 16/31] Update CHANGELOG.md Co-authored-by: plun1331 Signed-off-by: Paillat --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6640820f3c..225308c226 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,9 +33,9 @@ These changes are available on the `master` branch, but have not yet been releas ### Deprecated -- Deprecated for both `Role` and `RoleTags`: `is_bot_managed`, `is_premium_subscriber`, - `is_integration`, `is_available_for_purchase`, and `is_guild_connections_role`, in - favor of `Role.type` and `RoleTags.type`. +- Deprecated `is_bot_managed`, `is_premium_subscriber`, + `is_integration`, `is_available_for_purchase`, and `is_guild_connections_role` in + favor of `type` for both `Role` and `RoleTags` . ([#2708](https://github.com/Pycord-Development/pycord/pull/2708)) - Deprecated the `suppress` parameter in all applicable message-related methods in favor of `suppress_embeds`. From 39b6f075c76533d19702f5ad62561bbb9bb5e1cc Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 17 Feb 2026 23:10:40 +0100 Subject: [PATCH 17/31] Update discord/role.py Co-authored-by: plun1331 Signed-off-by: Paillat --- discord/role.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/role.py b/discord/role.py index e996980344..da417f394f 100644 --- a/discord/role.py +++ b/discord/role.py @@ -451,8 +451,8 @@ class Role(Hashable): mentionable: :class:`bool` Indicates if the role can be mentioned by users. tags: Optional[:class:`RoleTags`] - The role tags associated with this role. Use the tags to determine additional information about the role, - like if it's a bot role, a booster role, etc... + The role tags associated with this role. Tags indicate whether the role is a special role, + such as a bot role or the booster role. unicode_emoji: Optional[:class:`str`] The role's unicode emoji. Only available to guilds that contain ``ROLE_ICONS`` in :attr:`Guild.features`. From 4aae72ae18804f6d9a1c491f15d5f0c8fc8b54f2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:10:47 +0000 Subject: [PATCH 18/31] style(pre-commit): auto fixes from pre-commit.com hooks --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 225308c226..afed180739 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,9 +33,9 @@ These changes are available on the `master` branch, but have not yet been releas ### Deprecated -- Deprecated `is_bot_managed`, `is_premium_subscriber`, - `is_integration`, `is_available_for_purchase`, and `is_guild_connections_role` in - favor of `type` for both `Role` and `RoleTags` . +- Deprecated `is_bot_managed`, `is_premium_subscriber`, `is_integration`, + `is_available_for_purchase`, and `is_guild_connections_role` in favor of `type` for + both `Role` and `RoleTags` . ([#2708](https://github.com/Pycord-Development/pycord/pull/2708)) - Deprecated the `suppress` parameter in all applicable message-related methods in favor of `suppress_embeds`. From 77b9d41f5665067873a99b423f8c91caa84ea234 Mon Sep 17 00:00:00 2001 From: Paillat Date: Wed, 18 Feb 2026 13:37:52 +0100 Subject: [PATCH 19/31] Update discord/role.py Co-authored-by: plun1331 Signed-off-by: Paillat --- discord/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index da417f394f..77c3f47b3e 100644 --- a/discord/role.py +++ b/discord/role.py @@ -168,7 +168,7 @@ class RoleTags: that gives it context for the reason the role is managed. Role tags are a fairly complex topic, since it's usually hard to determine which role tag combination represents which role type. - In order to make your life easier, pycord provides a :attr:`RoleTags.type` attribute that attempts to determine the role type based on the role tags. It's value is not provided by discord but is rather computed by pycord based on the role tags. + In order to make your life easier, pycord provides a :attr:`RoleTags.type` attribute that attempts to determine the role type based on the role tags. Its value is not provided by Discord but is rather computed based on the role tags. If you find an issue, please report it on `GitHub `_. Read `this `_ if you need detailed information about how role tags work. From 3cc1c117056e44dd7cd6c62c5e54907e7eee1b56 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 12:38:18 +0000 Subject: [PATCH 20/31] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index 77c3f47b3e..39f30b9991 100644 --- a/discord/role.py +++ b/discord/role.py @@ -451,7 +451,7 @@ class Role(Hashable): mentionable: :class:`bool` Indicates if the role can be mentioned by users. tags: Optional[:class:`RoleTags`] - The role tags associated with this role. Tags indicate whether the role is a special role, + The role tags associated with this role. Tags indicate whether the role is a special role, such as a bot role or the booster role. unicode_emoji: Optional[:class:`str`] The role's unicode emoji. From 0db354f663917e28ef66f599a7142cca09f35b08 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Wed, 18 Feb 2026 13:41:42 +0100 Subject: [PATCH 21/31] :memo: Such as but not limited to --- discord/role.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/role.py b/discord/role.py index 39f30b9991..ea64a0b7b6 100644 --- a/discord/role.py +++ b/discord/role.py @@ -130,8 +130,8 @@ class RoleType(IntEnum): The role is a guild product role. .. note:: - This is not possible to determine at times because role tags seem to be missing altogether when - a role is fetched notably. In such cases :class:`Role.type` and `Role.tags` will both be :class:`None`. + This is not possible to determine at times because role tags seem to be missing altogether, notably when + a role is fetched. In such cases :class:`Role.type` and `Role.tags` will both be :class:`None`. PREMIUM_SUBSCRIPTION_BASE: :class:`int` The role is a base subscription role. @@ -452,7 +452,7 @@ class Role(Hashable): Indicates if the role can be mentioned by users. tags: Optional[:class:`RoleTags`] The role tags associated with this role. Tags indicate whether the role is a special role, - such as a bot role or the booster role. + such as but not limited to a bot role or the booster role. unicode_emoji: Optional[:class:`str`] The role's unicode emoji. Only available to guilds that contain ``ROLE_ICONS`` in :attr:`Guild.features`. From 8cfc3565a93d6e3351156bce0744398f068f3072 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Wed, 18 Feb 2026 13:46:15 +0100 Subject: [PATCH 22/31] :recycle: Plun Co-authored-by: plun1331 <49261529+plun1331@users.noreply.github.com> --- discord/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index ea64a0b7b6..60d3e0bcd2 100644 --- a/discord/role.py +++ b/discord/role.py @@ -107,7 +107,7 @@ def _parse_tag_int(data: RoleTagPayload, key: str) -> int | None: if value := data.get(key): with suppress(ValueError): # value error means it's not an number string (None or "") - return int(data[key]) # pyright: ignore[reportUnknownArgumentType] + return int(value) # pyright: ignore[reportUnknownArgumentType] return None From d870733bd924d6e70ea6a49af08395beddfbace6 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Wed, 18 Feb 2026 13:49:51 +0100 Subject: [PATCH 23/31] :memo: Plun 2 Co-authored-by: plun1331 <49261529+plun1331@users.noreply.github.com> --- discord/role.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index 60d3e0bcd2..1c1498a2d9 100644 --- a/discord/role.py +++ b/discord/role.py @@ -280,7 +280,14 @@ def is_available_for_purchase(self) -> bool: @deprecated("RoleTags.type", "2.8") def is_guild_connections_role(self) -> bool: - """Whether the role is a guild connections role.""" + """Whether the role is available for purchase. + + Returns ``True`` if the role is available for purchase, and + ``False`` if it is not available for purchase or if the role + is not linked to a guild subscription. + + .. versionadded:: 2.7 + """ return self._guild_connections is True def __repr__(self) -> str: From 76f944bb2fdad6d316a43d896fbd817944d8b624 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Wed, 18 Feb 2026 13:53:54 +0100 Subject: [PATCH 24/31] :wastebasket: Deprecation notices in docs --- discord/role.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/discord/role.py b/discord/role.py index 1c1498a2d9..0f04106bb4 100644 --- a/discord/role.py +++ b/discord/role.py @@ -570,6 +570,9 @@ def is_default(self) -> bool: def is_bot_managed(self) -> bool: """Whether the role is associated with a bot. + .. deprecated:: 2.8 + Use :attr:`Role.type` instead. + .. versionadded:: 1.6 """ return self.tags is not None and self.tags.is_bot_managed() @@ -578,6 +581,9 @@ def is_bot_managed(self) -> bool: def is_premium_subscriber(self) -> bool: """Whether the role is the premium subscriber, AKA "boost", role for the guild. + .. deprecated:: 2.8 + Use :attr:`Role.type` instead. + .. versionadded:: 1.6 """ return self.tags is not None and self.tags.is_premium_subscriber() @@ -587,6 +593,9 @@ def is_integration(self) -> bool: """Whether the guild manages the role through some form of integrations such as Twitch or through guild subscriptions. + .. deprecated:: 2.8 + Use :attr:`Role.type` instead. + .. versionadded:: 1.6 """ return self.tags is not None and self.tags.is_integration() @@ -622,6 +631,9 @@ def is_available_for_purchase(self) -> bool: ``False`` if it is not available for purchase or if the role is not linked to a guild subscription. + .. deprecated:: 2.8 + Use :attr:`Role.type` instead. + .. versionadded:: 2.7 """ return self.tags is not None and self.tags.is_available_for_purchase() @@ -630,6 +642,9 @@ def is_available_for_purchase(self) -> bool: def is_guild_connections_role(self) -> bool: """Whether the role is a guild connections role. + .. deprecated:: 2.8 + Use :attr:`Role.type` instead. + .. versionadded:: 2.7 """ return self.tags is not None and self.tags.is_guild_connections_role() From 314bc6e1f547ffe9c9069e9d6ccfd2c6a21b4a1f Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Wed, 18 Feb 2026 14:20:28 +0100 Subject: [PATCH 25/31] :recycle: Cleanup & docs --- discord/enums.py | 51 +++++++++++++++++++++++++++++++++++++ discord/role.py | 63 ++++++++-------------------------------------- docs/api/enums.rst | 3 +++ 3 files changed, 64 insertions(+), 53 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 63557c853b..4f8de18642 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -81,6 +81,7 @@ "PollLayoutType", "MessageReferenceType", "ThreadArchiveDuration", + "RoleType", "SubscriptionStatus", "SeparatorSpacingSize", "SelectDefaultValueType", @@ -1131,6 +1132,56 @@ class SelectDefaultValueType(Enum): user = "user" +class RoleType(IntEnum): + """Represents the type of role. + + This is NOT provided by Discord but is rather computed based on :attr:`Role.tags`. + + .. versionadded:: 2.8 + + Attributes + ---------- + NORMAL: :class:`int` + The role is a normal role. + APPLICATION: :class:`int` + The role is an application (bot) role. + BOOSTER: :class:`int` + The role is a guild's booster role. + GUILD_PRODUCT: :class:`int` + The role is a guild product role. + + .. note:: + This is not possible to determine at times because role tags seem to be missing altogether, notably when + a role is fetched. In such cases :attr:`Role.type` and :attr:`Role.tags` will both be :data:`None`. + PREMIUM_SUBSCRIPTION_BASE: :class:`int` + The role is a base subscription role. + + .. note:: + This is not possible to determine currently, will be :attr:`.INTEGRATION` if it's a base subscription. + PREMIUM_SUBSCRIPTION_TIER: :class:`int` + The role is a subscription role. + DRAFT_PREMIUM_SUBSCRIPTION_TIER: :class:`int` + The role is a draft subscription role. + INTEGRATION: :class:`int` + The role is an integration role, such as Twitch or YouTube, or a base subscription role. + CONNECTION: :class:`int` + The role is a guild connections role. + UNKNOWN: :class:`int` + The role type is unknown. + """ + + NORMAL = 0 + APPLICATION = 1 + BOOSTER = 2 + GUILD_PRODUCT = 3 # Not possible to determine *at times* because role tags seem to be missing altogether when fetched + PREMIUM_SUBSCRIPTION_BASE = 4 # Not possible to determine currently, will be INTEGRATION if it's a base subscription + PREMIUM_SUBSCRIPTION_TIER = 5 + DRAFT_PREMIUM_SUBSCRIPTION_TIER = 6 + INTEGRATION = 7 + CONNECTION = 8 + UNKNOWN = 9 + + T = TypeVar("T") diff --git a/discord/role.py b/discord/role.py index 0f04106bb4..80b539c0b3 100644 --- a/discord/role.py +++ b/discord/role.py @@ -26,13 +26,13 @@ from __future__ import annotations from contextlib import suppress -from enum import IntEnum from typing import TYPE_CHECKING, Any, TypeVar from typing_extensions import Self from .asset import Asset from .colour import Colour +from .enums import RoleType from .errors import InvalidArgument from .flags import RoleFlags from .mixins import Hashable @@ -40,7 +40,6 @@ from .utils import ( MISSING, _bytes_to_base64_data, - _get_as_snowflake, cached_slot_property, deprecated, snowflake_time, @@ -111,56 +110,6 @@ def _parse_tag_int(data: RoleTagPayload, key: str) -> int | None: return None -class RoleType(IntEnum): - """Represents the type of role. - - This is NOT provided by Discord but is rather computed based on the role tags. - - .. versionadded:: 2.8 - - Attributes - ---------- - NORMAL: :class:`int` - The role is a normal role. - APPLICATION: :class:`int` - The role is an application (bot) role. - BOOSTER: :class:`int` - The role is a guild's booster role. - GUILD_PRODUCT: :class:`int` - The role is a guild product role. - - .. note:: - This is not possible to determine at times because role tags seem to be missing altogether, notably when - a role is fetched. In such cases :class:`Role.type` and `Role.tags` will both be :class:`None`. - PREMIUM_SUBSCRIPTION_BASE: :class:`int` - The role is a base subscription role. - - .. note:: - This is not possible to determine currently, will be INTEGRATION if it's a base subscription. - PREMIUM_SUBSCRIPTION_TIER: :class:`int` - The role is a subscription role. - DRAFT_PREMIUM_SUBSCRIPTION_TIER: :class:`int` - The role is a draft subscription role. - INTEGRATION: :class:`int` - The role is an integration role, such as Twitch or YouTube, or a base subscription role. - CONNECTION: :class:`int` - The role is a guild connections role. - UNKNOWN: :class:`int` - The role type is unknown. - """ - - NORMAL = 0 - APPLICATION = 1 - BOOSTER = 2 - GUILD_PRODUCT = 3 # Not possible to determine *at times* because role tags seem to be missing altogether when fetched - PREMIUM_SUBSCRIPTION_BASE = 4 # Not possible to determine currently, will be INTEGRATION if it's a base subscription - PREMIUM_SUBSCRIPTION_TIER = 5 - DRAFT_PREMIUM_SUBSCRIPTION_TIER = 6 - INTEGRATION = 7 - CONNECTION = 8 - UNKNOWN = 9 - - class RoleTags: """Represents tags on a role. @@ -222,7 +171,13 @@ def __init__(self, data: RoleTagPayload): @cached_slot_property("_type") def type(self) -> RoleType: - """The type of the role.""" + """:class:`RoleType`: The type of the role. + + Role tags are a fairly complex topic, since it's usually hard to determine which role tag combination represents which role type. + In order to make your life easier, pycord provides a :attr:`RoleTags.type` attribute that attempts to determine the role type based on the role tags. Its value is not provided by Discord but is rather computed based on the role tags. + If you find an issue, please report it on `GitHub `_. + Read `this `_ if you need detailed information about how role tags work. + """ # Bot role if self.bot_id is not None: return RoleType.APPLICATION @@ -717,6 +672,8 @@ def icon(self) -> Asset | None: def type(self) -> RoleType: """The type of the role. + This is an alias for :attr:`RoleTags.type`. + .. versionadded:: 2.8 """ return self.tags.type if self.tags is not None else RoleType.NORMAL diff --git a/docs/api/enums.rst b/docs/api/enums.rst index 428e04e790..7eb1a26df0 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -2630,3 +2630,6 @@ of :class:`enum.Enum`. .. attribute:: user The default value is a user. + +.. autoclass:: RoleType + :members: From c27c52dca926bb97f8b0e07048c382a0723b5a06 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Wed, 18 Feb 2026 14:28:32 +0100 Subject: [PATCH 26/31] :memo: Comment and types --- discord/types/role.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/discord/types/role.py b/discord/types/role.py index 09e718e173..947d7edf76 100644 --- a/discord/types/role.py +++ b/discord/types/role.py @@ -25,6 +25,8 @@ from __future__ import annotations +from typing import Literal + from typing_extensions import NotRequired, TypedDict from .snowflake import Snowflake @@ -51,6 +53,10 @@ class Role(TypedDict): class RoleTags(TypedDict, total=False): - bot_id: Snowflake - integration_id: Snowflake - premium_subscriber: None + bot_id: NotRequired[Snowflake] + integration_id: NotRequired[Snowflake] + subscription_listing_id: NotRequired[Snowflake] + # For future reference, here a key being present and `None` means `True`, and it being missing means `False` + premium_subscriber: NotRequired[Literal[None]] + available_for_purchase: NotRequired[Literal[None]] + guild_connections: NotRequired[Literal[None]] From d91b218e695d95320a3ce17cfc24e76b01e73cc4 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Wed, 18 Feb 2026 14:34:56 +0100 Subject: [PATCH 27/31] :memo: Fix docstrings --- discord/role.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/role.py b/discord/role.py index 80b539c0b3..af7792f905 100644 --- a/discord/role.py +++ b/discord/role.py @@ -65,7 +65,7 @@ def _parse_tag_bool(data: RoleTagPayload, key: str) -> bool | None: None is returned if the key is not present. True is returned if the key is present and the value is None. - False is returned if the key is present and the value is not None. + False is returned for any other cases, but this should allegedly not happen. Parameters ---------- @@ -235,7 +235,7 @@ def is_available_for_purchase(self) -> bool: @deprecated("RoleTags.type", "2.8") def is_guild_connections_role(self) -> bool: - """Whether the role is available for purchase. + """Whether the role is a guild connections role. Returns ``True`` if the role is available for purchase, and ``False`` if it is not available for purchase or if the role From cab1fe7378dc665c12c8fe1cce2a26261d10660b Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Wed, 18 Feb 2026 14:35:22 +0100 Subject: [PATCH 28/31] :bug: I asked claude and it found a bug --- discord/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index af7792f905..db4499bec9 100644 --- a/discord/role.py +++ b/discord/role.py @@ -219,7 +219,7 @@ def is_bot_managed(self) -> bool: @deprecated("RoleTags.type", "2.8") def is_premium_subscriber(self) -> bool: """Whether the role is the premium subscriber, AKA "boost", role for the guild.""" - return self._premium_subscriber is None + return self._premium_subscriber is True @deprecated("RoleTags.type", "2.8") def is_integration(self) -> bool: From ac183e84257203d97a2a6f916b3e8d1587fcb78e Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Fri, 20 Feb 2026 15:43:49 +0100 Subject: [PATCH 29/31] :wastebasket: Deprecation in docstrings --- discord/role.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/discord/role.py b/discord/role.py index db4499bec9..b699d199cc 100644 --- a/discord/role.py +++ b/discord/role.py @@ -213,33 +213,53 @@ def type(self) -> RoleType: @deprecated("RoleTags.type", "2.8") def is_bot_managed(self) -> bool: - """Whether the role is associated with a bot.""" + """Whether the role is associated with a bot. + + .. deprecated:: 2.8 + Use :attr:`RoleTags.type` instead. + """ return self.bot_id is not None @deprecated("RoleTags.type", "2.8") def is_premium_subscriber(self) -> bool: - """Whether the role is the premium subscriber, AKA "boost", role for the guild.""" + """Whether the role is the premium subscriber, AKA "boost", role for the guild. + + .. deprecated:: 2.8 + Use :attr:`RoleTags.type` instead. + """ return self._premium_subscriber is True @deprecated("RoleTags.type", "2.8") def is_integration(self) -> bool: """Whether the guild manages the role through some form of integrations such as Twitch or through guild subscriptions. + + .. deprecated:: 2.8 + Use :attr:`RoleTags.type` instead. """ return self.integration_id is not None @deprecated("RoleTags.type", "2.8") def is_available_for_purchase(self) -> bool: - """Whether the role is available for purchase.""" + """Whether the role is available for purchase. + + Returns ``True`` if the role is available for purchase, and + ``False`` if it is not available for purchase or if the role + is not linked to a guild subscription. + + .. deprecated:: 2.8 + Use :attr:`RoleTags.type` instead. + + .. versionadded:: 2.7 + """ return self._available_for_purchase is True @deprecated("RoleTags.type", "2.8") def is_guild_connections_role(self) -> bool: """Whether the role is a guild connections role. - Returns ``True`` if the role is available for purchase, and - ``False`` if it is not available for purchase or if the role - is not linked to a guild subscription. + .. deprecated:: 2.8 + Use :attr:`RoleTags.type` instead. .. versionadded:: 2.7 """ From e3d8080e6d9cffa00445e04feb5faf8f2474d586 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Fri, 20 Feb 2026 16:03:25 +0100 Subject: [PATCH 30/31] :recycle: Use PEP 702 Deprecation --- discord/role.py | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/discord/role.py b/discord/role.py index b7a0f9e12c..2b4decab73 100644 --- a/discord/role.py +++ b/discord/role.py @@ -210,7 +210,9 @@ def type(self) -> RoleType: # Seeing how messed up this is it wouldn't be a surprise if this happened return RoleType.UNKNOWN - @deprecated("RoleTags.type", "2.8") + @deprecated( + "RoleTags.is_bot_managed is deprecated since version 2.8, consider using RoleTags.type instead." + ) def is_bot_managed(self) -> bool: """Whether the role is associated with a bot. @@ -219,7 +221,9 @@ def is_bot_managed(self) -> bool: """ return self.bot_id is not None - @deprecated("RoleTags.type", "2.8") + @deprecated( + "RoleTags.is_premium_subscriber is deprecated since version 2.8, consider using RoleTags.type instead." + ) def is_premium_subscriber(self) -> bool: """Whether the role is the premium subscriber, AKA "boost", role for the guild. @@ -228,7 +232,9 @@ def is_premium_subscriber(self) -> bool: """ return self._premium_subscriber is True - @deprecated("RoleTags.type", "2.8") + @deprecated( + "RoleTags.is_integration is deprecated since version 2.8, consider using RoleTags.type instead." + ) def is_integration(self) -> bool: """Whether the guild manages the role through some form of integrations such as Twitch or through guild subscriptions. @@ -238,7 +244,9 @@ def is_integration(self) -> bool: """ return self.integration_id is not None - @deprecated("RoleTags.type", "2.8") + @deprecated( + "RoleTags.is_available_for_purchase is deprecated since version 2.8, consider using RoleTags.type instead." + ) def is_available_for_purchase(self) -> bool: """Whether the role is available for purchase. @@ -253,7 +261,9 @@ def is_available_for_purchase(self) -> bool: """ return self._available_for_purchase is True - @deprecated("RoleTags.type", "2.8") + @deprecated( + "RoleTags.is_guild_connections_role is deprecated since version 2.8, consider using RoleTags.type instead." + ) def is_guild_connections_role(self) -> bool: """Whether the role is a guild connections role. @@ -540,7 +550,9 @@ def is_default(self) -> bool: """Checks if the role is the default role.""" return self.guild.id == self.id - @deprecated("Role.type", "2.8") + @deprecated( + "Role.is_bot_managed is deprecated since version 2.8, consider using Role.type instead." + ) def is_bot_managed(self) -> bool: """Whether the role is associated with a bot. @@ -551,7 +563,9 @@ def is_bot_managed(self) -> bool: """ return self.tags is not None and self.tags.is_bot_managed() - @deprecated("Role.type", "2.8") + @deprecated( + "Role.is_premium_subscriber is deprecated since version 2.8, consider using Role.type instead." + ) def is_premium_subscriber(self) -> bool: """Whether the role is the premium subscriber, AKA "boost", role for the guild. @@ -562,7 +576,9 @@ def is_premium_subscriber(self) -> bool: """ return self.tags is not None and self.tags.is_premium_subscriber() - @deprecated("Role.type", "2.8") + @deprecated( + "Role.is_integration is deprecated since version 2.8, consider using Role.type instead." + ) def is_integration(self) -> bool: """Whether the guild manages the role through some form of integrations such as Twitch or through guild subscriptions. @@ -597,7 +613,9 @@ def is_assignable(self) -> bool: and me.top_role > self ) - @deprecated("Role.type", "2.8") + @deprecated( + "Role.is_available_for_purchase is deprecated since version 2.8, consider using Role.type instead." + ) def is_available_for_purchase(self) -> bool: """Whether the role is available for purchase. @@ -612,7 +630,9 @@ def is_available_for_purchase(self) -> bool: """ return self.tags is not None and self.tags.is_available_for_purchase() - @deprecated("Role.type", "2.8") + @deprecated( + "Role.is_guild_connections_role is deprecated since version 2.8, consider using Role.type instead." + ) def is_guild_connections_role(self) -> bool: """Whether the role is a guild connections role. From fefc77600c4a9fc3aef5be1cb13790293e36a60b Mon Sep 17 00:00:00 2001 From: Paillat Date: Wed, 4 Mar 2026 12:30:34 +0100 Subject: [PATCH 31/31] Apply suggestion from @Paillat-dev Signed-off-by: Paillat --- discord/types/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/types/role.py b/discord/types/role.py index 947d7edf76..5e672eb636 100644 --- a/discord/types/role.py +++ b/discord/types/role.py @@ -56,7 +56,7 @@ class RoleTags(TypedDict, total=False): bot_id: NotRequired[Snowflake] integration_id: NotRequired[Snowflake] subscription_listing_id: NotRequired[Snowflake] - # For future reference, here a key being present and `None` means `True`, and it being missing means `False` + # Here a key being present and `None` means `True`, and it being missing means `False` premium_subscriber: NotRequired[Literal[None]] available_for_purchase: NotRequired[Literal[None]] guild_connections: NotRequired[Literal[None]]