From 804922babe3cc6e61491598182944625db28c802 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Tue, 4 Nov 2025 22:37:48 -0500 Subject: [PATCH 01/27] fix: Initial Commit To Fix Command Auto Syncing --- discord/bot.py | 223 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 157 insertions(+), 66 deletions(-) diff --git a/discord/bot.py b/discord/bot.py index d50c5cf2ff..8922423f05 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -277,81 +277,172 @@ async def get_desynced_commands( """ # We can suggest the user to upsert, edit, delete, or bulk upsert the commands + class DefaultComparison: + """ + Comparison rule for when there are multiple default values that should be considered equivalent when comparing 2 objects. + Allows for a custom check to be passed for further control over equality. + + Attributes + ---------- + defaults: :class:`tuple` + The values that should be considered equivalent to each other + callback: Callable[[Any, Any], bool] + A callable that will do additional comparison on the objects if neither are a default value. + Defaults to a `!=` comparison. + It should accept the 2 objects as arguments and return True if they should be considered equivalent + and False otherwise. + """ + + def __init__( + self, + defaults: tuple[Any, ...], + callback: Callable[[Any, Any], bool] = lambda x, y: x != y, + ): + self.defaults = defaults + self.callback = callback + + def _check_defaults(self, local, remote) -> bool | None: + defaults = (local in self.defaults) + (remote in self.defaults) + if defaults == 2: + # Both are defaults, so they can be counted as the same + return False + elif defaults == 0: + # Neither are defaults so the callback has to be used + return None + else: + # Only one is a default, so the command must be out of sync + return True + + def check(self, local, remote) -> bool: + if (rtn := self._check_defaults(local, remote)) is not None: + return rtn + else: + return self.callback(local, remote) + + class DefaultSetComparison(DefaultComparison): + def check(self, local, remote) -> bool: + try: + local = set(local) + except TypeError: + pass + try: + remote = set(remote) + except TypeError: + pass + return super().check(local, remote) + + type NestedComparison = dict[str, NestedComparison | DefaultComparison] + + def _compare_defaults( + obj: Mapping[str, Any] | Any, + match: Mapping[str, Any] | Any, + schema: NestedComparison, + ) -> bool: + if not isinstance(match, Mapping) or not isinstance(obj, Mapping): + return obj != match + for field, comparison in schema.items(): + remote = match.get(field, MISSING) + local = obj.get(field, MISSING) + if isinstance(comparison, dict): + _compare_defaults(local, remote, comparison) + elif isinstance(comparison, DefaultComparison): + if comparison.check(local, remote): + return True + return False def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: + cmd = cmd.to_dict() + + option_default_values = ([], MISSING) + + def _option_comparison_check(local, remote) -> bool: + matching = (local in option_default_values) + ( + remote in option_default_values + ) + if matching == 2: + return False + elif matching == 1: + return True + else: + return len(local) != len(remote) or any( + [ + _compare_defaults(local[x], remote[x], option_defaults) + for x in range(len(local)) + ] + ) + + choices_default_values = ([], MISSING) + + def _choices_comparison_check(local, remote) -> bool: + matching = (local in choices_default_values) + ( + remote in choices_default_values + ) + if matching == 2: + return False + elif matching == 1: + return True + else: + return len(local) != len(remote) or any( + [ + _compare_defaults(local[x], remote[x], choices_defaults) + for x in range(len(local)) + ] + ) + + defaults: NestedComparison = { + "type": DefaultComparison((1, MISSING)), + "name": DefaultComparison(()), + "description": DefaultComparison((MISSING,)), + "name_localizations": DefaultComparison((None, {}, MISSING)), + "description_localizations": DefaultComparison((None, {}, MISSING)), + "options": DefaultComparison( + option_default_values, _option_comparison_check + ), + "default_member_permissions": DefaultComparison((None, MISSING)), + "nsfw": DefaultComparison((False, MISSING)), + # TODO: Change the below default if needed to use the correct default integration types and contexts + "integration_types": DefaultSetComparison( + (MISSING, {0, 1}), lambda x, y: set(x) != set(y) + ), + # Discord States That This Defaults To "your app's configured contexts" + "contexts": DefaultSetComparison( + (None, {0, 1, 2}, MISSING), lambda x, y: set(x) != set(y) + ), + } + option_defaults: NestedComparison = { + "type": DefaultComparison(()), + "name": DefaultComparison(()), + "description": DefaultComparison(()), + "name_localizations": DefaultComparison((None, {}, MISSING)), + "description_localizations": DefaultComparison((None, {}, MISSING)), + "required": DefaultComparison((False, MISSING)), + "choices": DefaultComparison( + choices_default_values, _choices_comparison_check + ), + "channel_types": DefaultComparison(([], MISSING)), + "min_value": DefaultComparison((MISSING,)), + "max_value": DefaultComparison((MISSING,)), + "min_length": DefaultComparison((MISSING,)), + "max_length": DefaultComparison((MISSING,)), + "autocomplete": DefaultComparison((MISSING, False)), + } + choices_defaults: NestedComparison = { + "name": DefaultComparison(()), + "name_localizations": DefaultComparison((None, {}, MISSING)), + "value": DefaultComparison(()), + } + if isinstance(cmd, SlashCommandGroup): if len(cmd.subcommands) != len(match.get("options", [])): return True for i, subcommand in enumerate(cmd.subcommands): - match_ = next( - ( - data - for data in match["options"] - if data["name"] == subcommand.name - ), - MISSING, + match_ = find( + lambda x: x["name"] == subcommand.name, match["options"] ) - if match_ is not MISSING and _check_command(subcommand, match_): + if match_ is not None and _check_command(subcommand, match_): return True else: - as_dict = cmd.to_dict() - to_check = { - "nsfw": None, - "default_member_permissions": None, - "name": None, - "description": None, - "name_localizations": None, - "description_localizations": None, - "options": [ - "type", - "name", - "description", - "autocomplete", - "choices", - "name_localizations", - "description_localizations", - ], - "contexts": None, - "integration_types": None, - } - for check, value in to_check.items(): - if type(to_check[check]) == list: - # We need to do some falsy conversion here - # The API considers False (autocomplete) and [] (choices) to be falsy values - falsy_vals = (False, []) - for opt in value: - cmd_vals = ( - [val.get(opt, MISSING) for val in as_dict[check]] - if check in as_dict - else [] - ) - for i, val in enumerate(cmd_vals): - if val in falsy_vals: - cmd_vals[i] = MISSING - if match.get( - check, MISSING - ) is not MISSING and cmd_vals != [ - val.get(opt, MISSING) for val in match[check] - ]: - # We have a difference - return True - elif (attr := getattr(cmd, check, None)) != ( - found := match.get(check) - ): - # We might have a difference - if "localizations" in check and bool(attr) == bool(found): - # unlike other attrs, localizations are MISSING by default - continue - elif ( - check == "default_permission" - and attr is True - and found is None - ): - # This is a special case - # TODO: Remove for perms v2 - continue - return True - return False + return _compare_defaults(cmd, match, defaults) return_value = [] cmds = self.pending_application_commands.copy() From ae4e17178f8c2e6fc1437a7e9272cb99f602b9ee Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Mon, 16 Feb 2026 22:02:58 -0500 Subject: [PATCH 02/27] fix: Apply Suggestions From Code Review --- discord/bot.py | 317 +++++++++++++++++++++++++------------------------ 1 file changed, 162 insertions(+), 155 deletions(-) diff --git a/discord/bot.py b/discord/bot.py index 8922423f05..8e9b2e0378 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -42,9 +42,13 @@ Generator, Literal, Mapping, + TypeAlias, TypeVar, + Union, ) +from typing_extensions import override + from .client import Client from .cog import CogMixin from .commands import ( @@ -87,6 +91,160 @@ _log = logging.getLogger(__name__) +class DefaultComparison: + """ + Comparison rule for when there are multiple default values that should be considered equivalent when comparing 2 objects. + Allows for a custom check to be passed for further control over equality. + + Attributes + ---------- + defaults: :class:`tuple` + The values that should be considered equivalent to each other + callback: Callable[[Any, Any], bool] + A callable that will do additional comparison on the objects if neither are a default value. + Defaults to a `!=` comparison. + It should accept the 2 objects as arguments and return True if they should be considered equivalent + and False otherwise. + """ + + def __init__( + self, + defaults: tuple[Any, ...], + callback: Callable[[Any, Any], bool] = lambda x, y: x != y, + ): + self.defaults = defaults + self.callback = callback + + def _check_defaults(self, local, remote) -> bool | None: + defaults = (local in self.defaults) + (remote in self.defaults) + if defaults == 2: + # Both are COMMAND_DEFAULTS, so they can be counted as the same + return False + elif defaults == 0: + # Neither are COMMAND_DEFAULTS so the callback has to be used + return None + else: + # Only one is a default, so the command must be out of sync + return True + + def check(self, local, remote) -> bool: + if (rtn := self._check_defaults(local, remote)) is not None: + return rtn + else: + return self.callback(local, remote) + + +class DefaultSetComparison(DefaultComparison): + @override + def check(self, local, remote) -> bool: + try: + local = set(local) + except TypeError: + pass + try: + remote = set(remote) + except TypeError: + pass + return super().check(local, remote) + + +NestedComparison: TypeAlias = dict[str, Union["NestedComparison", DefaultComparison]] + + +def _compare_defaults( + obj: Mapping[str, Any] | Any, + match: Mapping[str, Any] | Any, + schema: NestedComparison, +) -> bool: + if not isinstance(match, Mapping) or not isinstance(obj, Mapping): + return obj != match + for field, comparison in schema.items(): + remote = match.get(field, MISSING) + local = obj.get(field, MISSING) + if isinstance(comparison, dict): + _compare_defaults(local, remote, comparison) + elif isinstance(comparison, DefaultComparison): + if comparison.check(local, remote): + return True + return False + + +option_default_values = ([], MISSING) + + +def _option_comparison_check(local, remote) -> bool: + matching = (local in option_default_values) + (remote in option_default_values) + if matching == 2: + return False + elif matching == 1: + return True + else: + return len(local) != len(remote) or any( + [ + _compare_defaults(local[x], remote[x], COMMAND_OPTION_DEFAULTS) + for x in range(len(local)) + ] + ) + + +choices_default_values = ([], MISSING) + + +def _choices_comparison_check(local, remote) -> bool: + matching = (local in choices_default_values) + (remote in choices_default_values) + if matching == 2: + return False + elif matching == 1: + return True + else: + return len(local) != len(remote) or any( + [ + _compare_defaults(local[x], remote[x], OPTIONS_CHOICES_DEFAULTS) + for x in range(len(local)) + ] + ) + + +COMMAND_DEFAULTS: NestedComparison = { + "type": DefaultComparison((1, MISSING)), + "name": DefaultComparison(()), + "description": DefaultComparison((MISSING,)), + "name_localizations": DefaultComparison((None, {}, MISSING)), + "description_localizations": DefaultComparison((None, {}, MISSING)), + "options": DefaultComparison(option_default_values, _option_comparison_check), + "default_member_permissions": DefaultComparison((None, MISSING)), + "nsfw": DefaultComparison((False, MISSING)), + # TODO: Change the below default if needed to use the correct default integration types and contexts + "integration_types": DefaultSetComparison( + (MISSING, {0, 1}), lambda x, y: set(x) != set(y) + ), + # Discord States That This Defaults To "your app's configured contexts" + "contexts": DefaultSetComparison( + (None, {0, 1, 2}, MISSING), lambda x, y: set(x) != set(y) + ), +} +COMMAND_OPTION_DEFAULTS: NestedComparison = { + "type": DefaultComparison(()), + "name": DefaultComparison(()), + "description": DefaultComparison(()), + "name_localizations": DefaultComparison((None, {}, MISSING)), + "description_localizations": DefaultComparison((None, {}, MISSING)), + "required": DefaultComparison((False, MISSING)), + "choices": DefaultComparison(choices_default_values, _choices_comparison_check), + "channel_types": DefaultComparison(([], MISSING)), + "min_value": DefaultComparison((MISSING,)), + "max_value": DefaultComparison((MISSING,)), + "min_length": DefaultComparison((MISSING,)), + "max_length": DefaultComparison((MISSING,)), + "autocomplete": DefaultComparison((MISSING, False)), +} +OPTIONS_CHOICES_DEFAULTS: NestedComparison = { + "name": DefaultComparison(()), + "name_localizations": DefaultComparison((None, {}, MISSING)), + "value": DefaultComparison(()), +} + + class ApplicationCommandMixin(ABC): """A mixin that implements common functionality for classes that need application command compatibility. @@ -277,161 +435,9 @@ async def get_desynced_commands( """ # We can suggest the user to upsert, edit, delete, or bulk upsert the commands - class DefaultComparison: - """ - Comparison rule for when there are multiple default values that should be considered equivalent when comparing 2 objects. - Allows for a custom check to be passed for further control over equality. - - Attributes - ---------- - defaults: :class:`tuple` - The values that should be considered equivalent to each other - callback: Callable[[Any, Any], bool] - A callable that will do additional comparison on the objects if neither are a default value. - Defaults to a `!=` comparison. - It should accept the 2 objects as arguments and return True if they should be considered equivalent - and False otherwise. - """ - - def __init__( - self, - defaults: tuple[Any, ...], - callback: Callable[[Any, Any], bool] = lambda x, y: x != y, - ): - self.defaults = defaults - self.callback = callback - - def _check_defaults(self, local, remote) -> bool | None: - defaults = (local in self.defaults) + (remote in self.defaults) - if defaults == 2: - # Both are defaults, so they can be counted as the same - return False - elif defaults == 0: - # Neither are defaults so the callback has to be used - return None - else: - # Only one is a default, so the command must be out of sync - return True - - def check(self, local, remote) -> bool: - if (rtn := self._check_defaults(local, remote)) is not None: - return rtn - else: - return self.callback(local, remote) - - class DefaultSetComparison(DefaultComparison): - def check(self, local, remote) -> bool: - try: - local = set(local) - except TypeError: - pass - try: - remote = set(remote) - except TypeError: - pass - return super().check(local, remote) - - type NestedComparison = dict[str, NestedComparison | DefaultComparison] - - def _compare_defaults( - obj: Mapping[str, Any] | Any, - match: Mapping[str, Any] | Any, - schema: NestedComparison, - ) -> bool: - if not isinstance(match, Mapping) or not isinstance(obj, Mapping): - return obj != match - for field, comparison in schema.items(): - remote = match.get(field, MISSING) - local = obj.get(field, MISSING) - if isinstance(comparison, dict): - _compare_defaults(local, remote, comparison) - elif isinstance(comparison, DefaultComparison): - if comparison.check(local, remote): - return True - return False - def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: cmd = cmd.to_dict() - option_default_values = ([], MISSING) - - def _option_comparison_check(local, remote) -> bool: - matching = (local in option_default_values) + ( - remote in option_default_values - ) - if matching == 2: - return False - elif matching == 1: - return True - else: - return len(local) != len(remote) or any( - [ - _compare_defaults(local[x], remote[x], option_defaults) - for x in range(len(local)) - ] - ) - - choices_default_values = ([], MISSING) - - def _choices_comparison_check(local, remote) -> bool: - matching = (local in choices_default_values) + ( - remote in choices_default_values - ) - if matching == 2: - return False - elif matching == 1: - return True - else: - return len(local) != len(remote) or any( - [ - _compare_defaults(local[x], remote[x], choices_defaults) - for x in range(len(local)) - ] - ) - - defaults: NestedComparison = { - "type": DefaultComparison((1, MISSING)), - "name": DefaultComparison(()), - "description": DefaultComparison((MISSING,)), - "name_localizations": DefaultComparison((None, {}, MISSING)), - "description_localizations": DefaultComparison((None, {}, MISSING)), - "options": DefaultComparison( - option_default_values, _option_comparison_check - ), - "default_member_permissions": DefaultComparison((None, MISSING)), - "nsfw": DefaultComparison((False, MISSING)), - # TODO: Change the below default if needed to use the correct default integration types and contexts - "integration_types": DefaultSetComparison( - (MISSING, {0, 1}), lambda x, y: set(x) != set(y) - ), - # Discord States That This Defaults To "your app's configured contexts" - "contexts": DefaultSetComparison( - (None, {0, 1, 2}, MISSING), lambda x, y: set(x) != set(y) - ), - } - option_defaults: NestedComparison = { - "type": DefaultComparison(()), - "name": DefaultComparison(()), - "description": DefaultComparison(()), - "name_localizations": DefaultComparison((None, {}, MISSING)), - "description_localizations": DefaultComparison((None, {}, MISSING)), - "required": DefaultComparison((False, MISSING)), - "choices": DefaultComparison( - choices_default_values, _choices_comparison_check - ), - "channel_types": DefaultComparison(([], MISSING)), - "min_value": DefaultComparison((MISSING,)), - "max_value": DefaultComparison((MISSING,)), - "min_length": DefaultComparison((MISSING,)), - "max_length": DefaultComparison((MISSING,)), - "autocomplete": DefaultComparison((MISSING, False)), - } - choices_defaults: NestedComparison = { - "name": DefaultComparison(()), - "name_localizations": DefaultComparison((None, {}, MISSING)), - "value": DefaultComparison(()), - } - if isinstance(cmd, SlashCommandGroup): if len(cmd.subcommands) != len(match.get("options", [])): return True @@ -442,7 +448,7 @@ def _choices_comparison_check(local, remote) -> bool: if match_ is not None and _check_command(subcommand, match_): return True else: - return _compare_defaults(cmd, match, defaults) + return _compare_defaults(cmd, match, COMMAND_DEFAULTS) return_value = [] cmds = self.pending_application_commands.copy() @@ -473,9 +479,10 @@ def _choices_comparison_check(local, remote) -> bool: # First let's check if the commands we have locally are the same as the ones on discord for cmd in pending: match = registered_commands_dict.get(cmd.name) + # We don't have this command registered if match is None: - # We don't have this command registered return_value.append({"command": cmd, "action": "upsert"}) + # We have a different version of the command then Discord elif _check_command(cmd, match): return_value.append( { @@ -485,7 +492,7 @@ def _choices_comparison_check(local, remote) -> bool: } ) else: - # We have this command registered but it's the same + # We have this command registered and it's the same return_value.append( {"command": cmd, "action": None, "id": int(match["id"])} ) From a08ebef2139cedf81e49e9be0900d7ce0ad54e13 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Sat, 21 Feb 2026 23:39:43 -0500 Subject: [PATCH 03/27] refactor: Apply Suggestions From Code Review --- discord/bot.py | 71 +++++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/discord/bot.py b/discord/bot.py index 8e9b2e0378..f8cdc03849 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -102,7 +102,7 @@ class DefaultComparison: The values that should be considered equivalent to each other callback: Callable[[Any, Any], bool] A callable that will do additional comparison on the objects if neither are a default value. - Defaults to a `!=` comparison. + Defaults to a `==` comparison. It should accept the 2 objects as arguments and return True if they should be considered equivalent and False otherwise. """ @@ -110,7 +110,7 @@ class DefaultComparison: def __init__( self, defaults: tuple[Any, ...], - callback: Callable[[Any, Any], bool] = lambda x, y: x != y, + callback: Callable[[Any, Any], bool] = lambda x, y: x == y, ): self.defaults = defaults self.callback = callback @@ -119,15 +119,23 @@ def _check_defaults(self, local, remote) -> bool | None: defaults = (local in self.defaults) + (remote in self.defaults) if defaults == 2: # Both are COMMAND_DEFAULTS, so they can be counted as the same - return False + return True elif defaults == 0: # Neither are COMMAND_DEFAULTS so the callback has to be used return None else: # Only one is a default, so the command must be out of sync - return True + return False def check(self, local, remote) -> bool: + """ + Compares the local and remote objects. + + Returns + ------- + bool + True if local and remote are deemed to be equivalent. False otherwise. + """ if (rtn := self._check_defaults(local, remote)) is not None: return rtn else: @@ -157,29 +165,29 @@ def _compare_defaults( schema: NestedComparison, ) -> bool: if not isinstance(match, Mapping) or not isinstance(obj, Mapping): - return obj != match + return obj == match for field, comparison in schema.items(): remote = match.get(field, MISSING) local = obj.get(field, MISSING) if isinstance(comparison, dict): _compare_defaults(local, remote, comparison) elif isinstance(comparison, DefaultComparison): - if comparison.check(local, remote): - return True - return False + if not comparison.check(local, remote): + return False + return True -option_default_values = ([], MISSING) +OPTION_DEFAULT_VALUES = ([], MISSING) def _option_comparison_check(local, remote) -> bool: - matching = (local in option_default_values) + (remote in option_default_values) + matching = (local in OPTION_DEFAULT_VALUES) + (remote in OPTION_DEFAULT_VALUES) if matching == 2: - return False - elif matching == 1: return True + elif matching == 1: + return False else: - return len(local) != len(remote) or any( + return len(local) == len(remote) and all( [ _compare_defaults(local[x], remote[x], COMMAND_OPTION_DEFAULTS) for x in range(len(local)) @@ -187,17 +195,17 @@ def _option_comparison_check(local, remote) -> bool: ) -choices_default_values = ([], MISSING) +CHOICES_DEFAULT_VALUES = ([], MISSING) def _choices_comparison_check(local, remote) -> bool: - matching = (local in choices_default_values) + (remote in choices_default_values) + matching = (local in CHOICES_DEFAULT_VALUES) + (remote in CHOICES_DEFAULT_VALUES) if matching == 2: - return False - elif matching == 1: return True + elif matching == 1: + return False else: - return len(local) != len(remote) or any( + return len(local) == len(remote) and all( [ _compare_defaults(local[x], remote[x], OPTIONS_CHOICES_DEFAULTS) for x in range(len(local)) @@ -211,17 +219,15 @@ def _choices_comparison_check(local, remote) -> bool: "description": DefaultComparison((MISSING,)), "name_localizations": DefaultComparison((None, {}, MISSING)), "description_localizations": DefaultComparison((None, {}, MISSING)), - "options": DefaultComparison(option_default_values, _option_comparison_check), + "options": DefaultComparison(OPTION_DEFAULT_VALUES, _option_comparison_check), "default_member_permissions": DefaultComparison((None, MISSING)), "nsfw": DefaultComparison((False, MISSING)), - # TODO: Change the below default if needed to use the correct default integration types and contexts - "integration_types": DefaultSetComparison( - (MISSING, {0, 1}), lambda x, y: set(x) != set(y) - ), + # TODO: Change the below default if needed to use the correct default integration types # Discord States That This Defaults To "your app's configured contexts" - "contexts": DefaultSetComparison( - (None, {0, 1, 2}, MISSING), lambda x, y: set(x) != set(y) + "integration_types": DefaultSetComparison( + (MISSING, {0, 1}), lambda x, y: set(x) == set(y) ), + "contexts": DefaultSetComparison((None, MISSING), lambda x, y: set(x) == set(y)), } COMMAND_OPTION_DEFAULTS: NestedComparison = { "type": DefaultComparison(()), @@ -230,7 +236,7 @@ def _choices_comparison_check(local, remote) -> bool: "name_localizations": DefaultComparison((None, {}, MISSING)), "description_localizations": DefaultComparison((None, {}, MISSING)), "required": DefaultComparison((False, MISSING)), - "choices": DefaultComparison(choices_default_values, _choices_comparison_check), + "choices": DefaultComparison(CHOICES_DEFAULT_VALUES, _choices_comparison_check), "channel_types": DefaultComparison(([], MISSING)), "min_value": DefaultComparison((MISSING,)), "max_value": DefaultComparison((MISSING,)), @@ -436,19 +442,18 @@ async def get_desynced_commands( # We can suggest the user to upsert, edit, delete, or bulk upsert the commands def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: - cmd = cmd.to_dict() - + """Returns True If Commands Are Equivalent""" if isinstance(cmd, SlashCommandGroup): if len(cmd.subcommands) != len(match.get("options", [])): - return True + return False for i, subcommand in enumerate(cmd.subcommands): match_ = find( lambda x: x["name"] == subcommand.name, match["options"] ) - if match_ is not None and _check_command(subcommand, match_): - return True + if match_ is not None and not _check_command(subcommand, match_): + return False else: - return _compare_defaults(cmd, match, COMMAND_DEFAULTS) + return _compare_defaults(cmd.to_dict(), match, COMMAND_DEFAULTS) return_value = [] cmds = self.pending_application_commands.copy() @@ -483,7 +488,7 @@ def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: if match is None: return_value.append({"command": cmd, "action": "upsert"}) # We have a different version of the command then Discord - elif _check_command(cmd, match): + elif not _check_command(cmd, match): return_value.append( { "command": cmd, From a5b3225268cd8144566b509d9d91ca9dabb489a0 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Sun, 22 Feb 2026 17:06:36 -0500 Subject: [PATCH 04/27] fix: Subcommand Checking --- discord/bot.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/discord/bot.py b/discord/bot.py index f8cdc03849..68d211effb 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -170,7 +170,8 @@ def _compare_defaults( remote = match.get(field, MISSING) local = obj.get(field, MISSING) if isinstance(comparison, dict): - _compare_defaults(local, remote, comparison) + if not _compare_defaults(local, remote, comparison): + return False elif isinstance(comparison, DefaultComparison): if not comparison.check(local, remote): return False @@ -229,6 +230,14 @@ def _choices_comparison_check(local, remote) -> bool: ), "contexts": DefaultSetComparison((None, MISSING), lambda x, y: set(x) == set(y)), } +SUBCOMMAND_DEFAULTS: NestedComparison = { + "type": DefaultComparison(()), + "name": DefaultComparison(()), + "description": DefaultComparison(()), + "name_localizations": DefaultComparison((None, {}, MISSING)), + "description_localizations": DefaultComparison((None, {}, MISSING)), + "options": DefaultComparison(OPTION_DEFAULT_VALUES, _option_comparison_check), +} COMMAND_OPTION_DEFAULTS: NestedComparison = { "type": DefaultComparison(()), "name": DefaultComparison(()), @@ -450,10 +459,13 @@ def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: match_ = find( lambda x: x["name"] == subcommand.name, match["options"] ) - if match_ is not None and not _check_command(subcommand, match_): - return False + if match_ is not None: + return _check_command(subcommand, match_) else: - return _compare_defaults(cmd.to_dict(), match, COMMAND_DEFAULTS) + if cmd.parent is None: + return _compare_defaults(cmd.to_dict(), match, COMMAND_DEFAULTS) + else: + return _compare_defaults(cmd.to_dict(), match, SUBCOMMAND_DEFAULTS) return_value = [] cmds = self.pending_application_commands.copy() From d02ec72b202838286335c9c10cbfc868fb3c9d94 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Sun, 22 Feb 2026 17:12:46 -0500 Subject: [PATCH 05/27] refactor: Use Pipe Union --- discord/bot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/discord/bot.py b/discord/bot.py index 68d211effb..f0a11357c4 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -44,7 +44,6 @@ Mapping, TypeAlias, TypeVar, - Union, ) from typing_extensions import override @@ -156,7 +155,7 @@ def check(self, local, remote) -> bool: return super().check(local, remote) -NestedComparison: TypeAlias = dict[str, Union["NestedComparison", DefaultComparison]] +NestedComparison: TypeAlias = dict[str, "NestedComparison | DefaultComparison"] def _compare_defaults( From 24dd6f7b520ad45965f8e0d6c45b688863b004f6 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Mon, 2 Mar 2026 17:17:59 -0600 Subject: [PATCH 06/27] feat: Support IntegrationTypes Defaults Via AppInfo --- discord/bot.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/discord/bot.py b/discord/bot.py index f0a11357c4..1c6c5b3212 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -417,6 +417,16 @@ def get_application_command( return return command + async def _get_command_defaults(self): + app_info = await self._bot.application_info() + integration_contexts = app_info.integration_types_config._to_payload().keys() + + command_defaults = COMMAND_DEFAULTS.copy() + command_defaults["integration_types"] = DefaultSetComparison( + (MISSING, integration_contexts), lambda x, y: set(x) == set(y) + ) + return command_defaults + async def get_desynced_commands( self, guild_id: int | None = None, @@ -448,6 +458,8 @@ async def get_desynced_commands( the action, including ``id``. """ + updated_command_defaults = await self._get_command_defaults() + # We can suggest the user to upsert, edit, delete, or bulk upsert the commands def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: """Returns True If Commands Are Equivalent""" @@ -462,7 +474,9 @@ def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: return _check_command(subcommand, match_) else: if cmd.parent is None: - return _compare_defaults(cmd.to_dict(), match, COMMAND_DEFAULTS) + return _compare_defaults( + cmd.to_dict(), match, updated_command_defaults + ) else: return _compare_defaults(cmd.to_dict(), match, SUBCOMMAND_DEFAULTS) From 5723354a150d42beaa27d4327fc4e5212b72f5e1 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Mon, 2 Mar 2026 17:20:22 -0600 Subject: [PATCH 07/27] chore: Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8ecf9a5c0..a7b377dc85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -218,6 +218,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#3105](https://github.com/Pycord-Development/pycord/pull/3105)) - Fixed the update of a user's `avatar_decoration` to now cause an `on_user_update` event to fire. ([#3103](https://github.com/Pycord-Development/pycord/pull/3103)) +- Fixed backend logic for `sync_commands` to only sync when needed. + ([#2990](https://github.com/Pycord-Development/pycord/pull/2990)) ### Deprecated From 2bc087d4aa33036c40e79211892a56292710e0d3 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Tue, 3 Mar 2026 11:36:27 -0600 Subject: [PATCH 08/27] feat: Implement Tests For Command Comparison --- tests/test_command_syncing.py | 1082 +++++++++++++++++++++++++++++++++ 1 file changed, 1082 insertions(+) create mode 100644 tests/test_command_syncing.py diff --git a/tests/test_command_syncing.py b/tests/test_command_syncing.py new file mode 100644 index 0000000000..862c711032 --- /dev/null +++ b/tests/test_command_syncing.py @@ -0,0 +1,1082 @@ +import copy +from typing import Any + +import pytest + +import discord +from discord import MISSING, Bot, SlashCommandGroup +from discord.types.interactions import ApplicationCommand, ApplicationCommandOption + +pytestmark = pytest.mark.asyncio + + +class SlashCommand(discord.SlashCommand): + def __init__(self, **kwargs): + if (r := kwargs.pop("func", None)) is not None: + callback = r + else: + + async def dummy_callback(ctx): + pass + + callback = dummy_callback + if (desc := kwargs.get("description")) is not None: + kwargs.pop("description") + else: + desc = "desc" + if (name := kwargs.get("name")) is not None: + kwargs.pop("name") + else: + name = "testing" + super().__init__(callback, name=name, description=desc, **kwargs) + if self.integration_types is None: + self.integration_types = {discord.IntegrationType.guild_install} + if self.contexts is None: + self.contexts = { + discord.InteractionContextType.private_channel, + discord.InteractionContextType.bot_dm, + discord.InteractionContextType.guild, + } + + +remote_dummy_base: dict = { + "id": "1", + "application_id": "1", + "version": "1", + "default_member_permissions": None, + "type": 1, + "name": "testing", + "name_localizations": None, + "description": "desc", + "description_localizations": None, + "dm_permission": True, + "contexts": [0, 1, 2], + "integration_types": [0], + "nsfw": False, +} + + +async def edit_needed( + local: SlashCommand | SlashCommandGroup, remote: ApplicationCommand +): + b = Bot() + b.add_application_command(local) + r = await b.get_desynced_commands(prefetched=[remote]) + return r[0]["action"] == "edit" + + +class TestCommandSyncing: + @staticmethod + def dict_factory(**kwargs) -> ApplicationCommand: + remote_dummy = copy.deepcopy(remote_dummy_base) + for key, value in kwargs.items(): + if value == MISSING: + del remote_dummy[key] + else: + remote_dummy.update({key: value}) + return remote_dummy + + async def test_default(self): + assert not await edit_needed(SlashCommand(), TestCommandSyncing.dict_factory()) + + async def test_default_member_permissions_defaults(self): + assert not await edit_needed( + SlashCommand(), + TestCommandSyncing.dict_factory(default_member_permissions=None), + ) + assert not await edit_needed( + SlashCommand(), + TestCommandSyncing.dict_factory(default_member_permissions=MISSING), + ) + + async def test_default_member_permissions(self): + assert await edit_needed( + SlashCommand(default_member_permissions=discord.Permissions(8)), + TestCommandSyncing.dict_factory(), + ) + assert await edit_needed( + SlashCommand(), + TestCommandSyncing.dict_factory(default_member_permissions="8"), + ) + + async def test_type_defaults(self): + assert not await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(type=1) + ) + assert not await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(type=MISSING) + ) + + async def test_name_localizations_defaults(self): + assert not await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(name_localizations={}) + ) + assert not await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(name_localizations=None) + ) + assert not await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(name_localizations=MISSING) + ) + + async def test_name_localizations(self): + assert await edit_needed( + SlashCommand(name_localizations={"no": "testing_no"}), + TestCommandSyncing.dict_factory(), + ) + assert await edit_needed( + SlashCommand(name_localizations={"no": "testing_no", "ja": "testing_ja"}), + TestCommandSyncing.dict_factory(), + ) + assert await edit_needed( + SlashCommand(name_localizations={"ja": "testing_ja", "no": "testing_no"}), + TestCommandSyncing.dict_factory(), + ) + assert await edit_needed( + SlashCommand(), + TestCommandSyncing.dict_factory(name_localizations={"no": "testing_no"}), + ) + assert await edit_needed( + SlashCommand(), + TestCommandSyncing.dict_factory( + name_localizations={"no": "testing_no", "ja": "testing_ja"} + ), + ) + assert await edit_needed( + SlashCommand(), + TestCommandSyncing.dict_factory( + name_localizations={"ja": "testing_ja", "no": "testing_no"} + ), + ) + + async def test_description_localizations_defaults(self): + assert not await edit_needed( + SlashCommand(), + TestCommandSyncing.dict_factory(description_localizations={}), + ) + assert not await edit_needed( + SlashCommand(), + TestCommandSyncing.dict_factory(description_localizations=None), + ) + assert not await edit_needed( + SlashCommand(), + TestCommandSyncing.dict_factory(description_localizations=MISSING), + ) + + async def test_description_localizations(self): + assert await edit_needed( + SlashCommand(description_localizations={"no": "testing_desc_es"}), + TestCommandSyncing.dict_factory(), + ) + assert await edit_needed( + SlashCommand( + description_localizations={ + "no": "testing_desc_es", + "ja": "testing_desc_jp", + } + ), + TestCommandSyncing.dict_factory(), + ) + assert await edit_needed( + SlashCommand( + description_localizations={ + "ja": "testing_desc_jp", + "no": "testing_desc_es", + } + ), + TestCommandSyncing.dict_factory(), + ) + assert await edit_needed( + SlashCommand(), + TestCommandSyncing.dict_factory( + description_localizations={"no": "testing_desc_es"} + ), + ) + assert await edit_needed( + SlashCommand(), + TestCommandSyncing.dict_factory( + description_localizations={ + "no": "testing_desc_es", + "ja": "testing_desc_jp", + } + ), + ) + assert await edit_needed( + SlashCommand(), + TestCommandSyncing.dict_factory( + description_localizations={ + "ja": "testing_desc_jp", + "no": "testing_desc_es", + } + ), + ) + + async def test_description(self): + assert await edit_needed( + SlashCommand(description="different"), TestCommandSyncing.dict_factory() + ) + assert await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(description="different") + ) + + async def test_contexts_defaults(self): + assert not await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(contexts=[2, 1, 0]) + ) + + async def test_contexts(self): + assert await edit_needed( + SlashCommand(contexts=set()), TestCommandSyncing.dict_factory() + ) + assert await edit_needed( + SlashCommand(contexts={discord.InteractionContextType.guild}), + TestCommandSyncing.dict_factory(), + ) + assert await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(contexts=[]) + ) + assert await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(contexts=[1]) + ) + + async def test_integration_types_defaults(self): + assert not await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(integration_types=MISSING) + ) + + async def test_integration_types(self): + assert await edit_needed( + SlashCommand(integration_types=set()), TestCommandSyncing.dict_factory() + ) + assert await edit_needed( + SlashCommand(integration_types={discord.IntegrationType.user_install}), + TestCommandSyncing.dict_factory(), + ) + assert await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(integration_types=[]) + ) + assert await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(integration_types=[1]) + ) + + async def test_nsfw_defaults(self): + assert not await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(nsfw=False) + ) + assert not await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(nsfw=MISSING) + ) + + async def test_nsfw(self): + assert await edit_needed( + SlashCommand(nsfw=True), TestCommandSyncing.dict_factory() + ) + assert await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(nsfw=True) + ) + + async def test_options_defaults(self): + assert not await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(options=[]) + ) + assert not await edit_needed( + SlashCommand(), TestCommandSyncing.dict_factory(options=MISSING) + ) + + +class TestCommandSyncingWithOption: + @staticmethod + def dict_factory(**kwargs) -> dict[str, Any]: + remote_dummy = copy.deepcopy(remote_dummy_base) + remote_dummy["options"] = [ + { + "type": 3, + "name": "user", + "name_localizations": None, + "description": "name", + "description_localizations": None, + "required": True, + } + ] + for key, value in kwargs.items(): + if value == MISSING: + del remote_dummy["options"][0][key] + else: + remote_dummy["options"][0].update({key: value}) + return remote_dummy + + class SlashOptionCommand(SlashCommand): + @staticmethod + def default_option() -> discord.Option: + return discord.Option(str, name="user", description="name") + + def __init__( + self, options: list[discord.Option | dict] | None = None, **kwargs + ): + async def dummy_callback(ctx, user): + pass + + if options is None: + options = [self.default_option()] + for n, i in enumerate(options): + if isinstance(i, dict): + d = self.default_option() + for key, value in i.items(): + d.__setattr__(key, value) + options[n] = d + super().__init__(func=dummy_callback, options=options, **kwargs) + + async def test_type(self): + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"input_type": discord.SlashCommandOptionType(5)}] + ), + TestCommandSyncingWithOption.dict_factory(), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(type=5), + ) + + async def test_name(self): + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"name": "pycord"}]), + TestCommandSyncingWithOption.dict_factory(), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(name="pycord"), + ) + + async def test_description(self): + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"description": "pycord_desc"}] + ), + TestCommandSyncingWithOption.dict_factory(), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(description="pycord_desc"), + ) + + async def test_name_localizations_defaults(self): + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(name_localizations={}), + ) + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(name_localizations=None), + ) + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(name_localizations=MISSING), + ) + + async def test_name_localizations(self): + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"name_localizations": {"no": "testing_no"}}] + ), + TestCommandSyncingWithOption.dict_factory(), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"name_localizations": {"no": "testing_no", "ja": "testing_ja"}}] + ), + TestCommandSyncingWithOption.dict_factory(), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"name_localizations": {"ja": "testing_ja", "no": "testing_no"}}] + ), + TestCommandSyncingWithOption.dict_factory(), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory( + name_localizations={"no": "testing_no"} + ), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory( + name_localizations={"no": "testing_no", "ja": "testing_ja"} + ), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory( + name_localizations={"ja": "testing_ja", "no": "testing_no"} + ), + ) + + async def test_description_localizations_defaults(self): + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(description_localizations={}), + ) + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(description_localizations=None), + ) + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory( + description_localizations=MISSING + ), + ) + + async def test_description_localizations(self): + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"description_localizations": {"no": "testing_desc_es"}}] + ), + TestCommandSyncingWithOption.dict_factory(), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [ + { + "description_localizations": { + "no": "testing_desc_es", + "ja": "testing_desc_jp", + } + } + ] + ), + TestCommandSyncingWithOption.dict_factory(), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [ + { + "description_localizations": { + "ja": "testing_desc_jp", + "no": "testing_desc_es", + } + } + ] + ), + TestCommandSyncingWithOption.dict_factory(), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory( + description_localizations={"no": "testing_desc_es"} + ), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory( + description_localizations={ + "no": "testing_desc_es", + "ja": "testing_desc_jp", + } + ), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory( + description_localizations={ + "ja": "testing_desc_jp", + "no": "testing_desc_es", + } + ), + ) + + async def test_required_defaults(self): + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"required": False}]), + TestCommandSyncingWithOption.dict_factory(required=MISSING), + ) + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(required=True), + ) + + async def test_required(self): + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(required=MISSING), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(required=False), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"required": False}]), + TestCommandSyncingWithOption.dict_factory(required=True), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"required": None}]), + TestCommandSyncingWithOption.dict_factory(required=MISSING), + ) + + async def test_choices_defaults(self): + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(choices=MISSING), + ) + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(choices=[]), + ) + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [ + { + "choices": [ + discord.OptionChoice("a"), + discord.OptionChoice("b"), + discord.OptionChoice("c"), + ] + } + ] + ), + TestCommandSyncingWithOption.dict_factory( + choices=[ + {"name": "a", "value": "a"}, + {"name": "b", "value": "b"}, + {"name": "c", "value": "c"}, + ] + ), + ) + + async def test_choices(self): + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"choices": [discord.OptionChoice("a"), discord.OptionChoice("b")]}] + ), + TestCommandSyncingWithOption.dict_factory( + choices=[ + {"name": "a", "value": "a"}, + {"name": "b", "value": "b"}, + {"name": "c", "value": "c"}, + ] + ), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [ + { + "choices": [ + discord.OptionChoice("a"), + discord.OptionChoice("b"), + discord.OptionChoice("c"), + ] + } + ] + ), + TestCommandSyncingWithOption.dict_factory( + choices=[{"name": "a", "value": "a"}, {"name": "b", "value": "b"}] + ), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [ + { + "choices": [ + discord.OptionChoice("a"), + discord.OptionChoice("x"), + discord.OptionChoice("c"), + ] + } + ] + ), + TestCommandSyncingWithOption.dict_factory( + choices=[ + {"name": "a", "value": "a"}, + {"name": "b", "value": "b"}, + {"name": "c", "value": "c"}, + ] + ), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [ + { + "choices": [ + discord.OptionChoice("a"), + discord.OptionChoice("b"), + discord.OptionChoice("c"), + ] + } + ] + ), + TestCommandSyncingWithOption.dict_factory( + choices=[ + {"name": "a", "value": "a"}, + {"name": "x", "value": "x"}, + {"name": "c", "value": "c"}, + ] + ), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"choices": [discord.OptionChoice("a"), discord.OptionChoice("c")]}] + ), + TestCommandSyncingWithOption.dict_factory( + choices=[ + {"name": "a", "value": "a"}, + {"name": "b", "value": "b"}, + {"name": "c", "value": "c"}, + ] + ), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [ + { + "choices": [ + discord.OptionChoice("a"), + discord.OptionChoice("b"), + discord.OptionChoice("c"), + ] + } + ] + ), + TestCommandSyncingWithOption.dict_factory( + choices=[{"name": "a", "value": "a"}, {"name": "c", "value": "c"}] + ), + ) + + async def test_channel_type_defaults(self): + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(channel_types=[]), + ) + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(channel_types=MISSING), + ) + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"channel_types": [discord.ChannelType(0), discord.ChannelType(1)]}] + ), + TestCommandSyncingWithOption.dict_factory(channel_types=[0, 1]), + ) + + async def test_channel_types(self): + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"channel_types": [discord.ChannelType(0)]}] + ), + TestCommandSyncingWithOption.dict_factory(channel_types=[0, 1]), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"channel_types": [discord.ChannelType(0), discord.ChannelType(1)]}] + ), + TestCommandSyncingWithOption.dict_factory(channel_types=[0]), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(channel_types=[0, 1]), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"channel_types": [discord.ChannelType(0), discord.ChannelType(1)]}] + ), + TestCommandSyncingWithOption.dict_factory(channel_types=MISSING), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"channel_types": [discord.ChannelType(0), discord.ChannelType(5)]}] + ), + TestCommandSyncingWithOption.dict_factory(channel_types=[0, 1]), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"channel_types": [discord.ChannelType(0), discord.ChannelType(1)]}] + ), + TestCommandSyncingWithOption.dict_factory(channel_types=[0, 5]), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [ + { + "channel_types": [ + discord.ChannelType(0), + discord.ChannelType(1), + discord.ChannelType(15), + ] + } + ] + ), + TestCommandSyncingWithOption.dict_factory(channel_types=[0, 1]), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"channel_types": [discord.ChannelType(0), discord.ChannelType(1)]}] + ), + TestCommandSyncingWithOption.dict_factory(channel_types=[0, 1, 15]), + ) + + async def test_min_value_defaults(self): + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(min_value=MISSING), + ) + + async def test_min_value(self): + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_value": 5}]), + TestCommandSyncingWithOption.dict_factory(min_value=5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_value": 6}]), + TestCommandSyncingWithOption.dict_factory(min_value=5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_value": 5}]), + TestCommandSyncingWithOption.dict_factory(min_value=6), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(min_value=5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_value": 5}]), + TestCommandSyncingWithOption.dict_factory(), + ) + + # Floats + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_value": 5.5}]), + TestCommandSyncingWithOption.dict_factory(min_value=5.5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_value": 6.0}]), + TestCommandSyncingWithOption.dict_factory(min_value=5.0), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_value": 5.77}]), + TestCommandSyncingWithOption.dict_factory(min_value=6.123), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(min_value=5.333), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_value": 5.333}]), + TestCommandSyncingWithOption.dict_factory(), + ) + + # Mixed + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_value": 5.0}]), + TestCommandSyncingWithOption.dict_factory(min_value=5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_value": 6}]), + TestCommandSyncingWithOption.dict_factory(min_value=5.0), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_value": 5.0}]), + TestCommandSyncingWithOption.dict_factory(min_value=6), + ) + + async def test_max_value_defaults(self): + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(max_value=MISSING), + ) + + async def test_max_value(self): + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_value": 5}]), + TestCommandSyncingWithOption.dict_factory(max_value=5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_value": 6}]), + TestCommandSyncingWithOption.dict_factory(max_value=5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_value": 5}]), + TestCommandSyncingWithOption.dict_factory(max_value=6), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(max_value=5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_value": 5}]), + TestCommandSyncingWithOption.dict_factory(), + ) + + # Floats + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_value": 5.5}]), + TestCommandSyncingWithOption.dict_factory(max_value=5.5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_value": 6.0}]), + TestCommandSyncingWithOption.dict_factory(max_value=5.0), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_value": 5.77}]), + TestCommandSyncingWithOption.dict_factory(max_value=6.123), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(max_value=5.333), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_value": 5.333}]), + TestCommandSyncingWithOption.dict_factory(), + ) + + # Mixed + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_value": 5.0}]), + TestCommandSyncingWithOption.dict_factory(max_value=5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_value": 6}]), + TestCommandSyncingWithOption.dict_factory(max_value=5.0), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_value": 5.0}]), + TestCommandSyncingWithOption.dict_factory(max_value=6), + ) + + async def test_min_length_defaults(self): + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(min_length=MISSING), + ) + + async def test_min_length(self): + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_length": 5}]), + TestCommandSyncingWithOption.dict_factory(min_length=5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_length": 6}]), + TestCommandSyncingWithOption.dict_factory(min_length=5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_length": 5}]), + TestCommandSyncingWithOption.dict_factory(min_length=6), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(min_length=5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"min_length": 5}]), + TestCommandSyncingWithOption.dict_factory(), + ) + + async def test_max_length_defaults(self): + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(max_length=MISSING), + ) + + async def test_max_length(self): + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_length": 5}]), + TestCommandSyncingWithOption.dict_factory(max_length=5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_length": 6}]), + TestCommandSyncingWithOption.dict_factory(max_length=5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_length": 5}]), + TestCommandSyncingWithOption.dict_factory(max_length=6), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(max_length=5), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand([{"max_length": 5}]), + TestCommandSyncingWithOption.dict_factory(), + ) + + async def test_autocomplete_default(self): + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(autocomplete=False), + ) + assert not await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(autocomplete=MISSING), + ) + + async def test_autocomplete(self): + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand(), + TestCommandSyncingWithOption.dict_factory(autocomplete=True), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"autocomplete": lambda x: x}] + ), + TestCommandSyncingWithOption.dict_factory(autocomplete=False), + ) + assert await edit_needed( + TestCommandSyncingWithOption.SlashOptionCommand( + [{"autocomplete": lambda x: x}] + ), + TestCommandSyncingWithOption.dict_factory(autocomplete=MISSING), + ) + + +class TestSubCommandSyncing: + + @staticmethod + def dict_factory(top: dict, command: dict) -> dict[str, Any]: + remote_dummy = TestCommandSyncing.dict_factory(**top) + if "name" not in top: + remote_dummy["name"] = "subcommand" + remote_dummy["options"] = [ + ApplicationCommandOption( + type=1, + name="testing", + name_localizations=None, + description="desc", + description_localizations=None, + required=True, + ) + ] + for key, value in command.items(): + if value == MISSING: + del remote_dummy["options"][0][key] + else: + remote_dummy["options"][0].update({key: value}) + return remote_dummy + + async def test_parent_name(self): + async def edit_needed( + local: SlashCommand | SlashCommandGroup, remote: ApplicationCommand + ): + b = Bot() + b.add_application_command(local) + r = await b.get_desynced_commands(prefetched=[remote]) + return r[0]["action"] == "upsert" and r[1]["action"] == "delete" + + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand()) + assert not await edit_needed(s, self.dict_factory({}, {})) + s = SlashCommandGroup("newgroupname") + s.add_command(SlashCommand()) + assert await edit_needed(s, self.dict_factory({}, {})) + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand()) + assert await edit_needed(s, self.dict_factory({"name": "new_discord_name"}, {})) + + async def test_parent_attrs(self): + s = SlashCommandGroup("subcommand", description="newdescname") + s.add_command(SlashCommand()) + assert await edit_needed(s, self.dict_factory({}, {})) + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand()) + assert await edit_needed( + s, self.dict_factory({"description": "new_discord_desc"}, {}) + ) + + async def test_child_name(self): + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand(parent=s)) + assert not await edit_needed(s, self.dict_factory({}, {})) + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand(name="newsubcommand_name")) + assert await edit_needed(s, self.dict_factory({}, {})) + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand()) + assert await edit_needed(s, self.dict_factory({}, {"name": "new_discord_name"})) + + async def test_child(self): + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand(description="newdescript")) + assert await edit_needed(s, self.dict_factory({}, {})) + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand()) + assert await edit_needed( + s, self.dict_factory({}, {"description": "new_descript"}) + ) + + async def test_subcommand_counts(self): + s = SlashCommandGroup("subcommand") + assert await edit_needed(s, self.dict_factory({}, {})) + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand(parent=s)) + rd = self.dict_factory({}, {}) + rd["options"] = [] + assert await edit_needed(s, rd) + + +class TestSubSubCommandSyncing: + + @staticmethod + def dict_factory(top: dict, command: dict) -> dict[str, Any]: + remote_dummy = TestCommandSyncing.dict_factory(**top) + if "name" not in top: + remote_dummy["name"] = "subcommand" + remote_dummy["options"] = [ + ApplicationCommandOption( + type=2, + name="testing", + name_localizations=None, + description="desc", + description_localizations=None, + required=True, + options=[ + ApplicationCommandOption( + type=1, + name="testing", + name_localizations=None, + description="desc", + description_localizations=None, + required=True, + ) + ], + ) + ] + for key, value in command.items(): + if value == MISSING: + del remote_dummy["options"][0]["options"][0][key] + else: + remote_dummy["options"][0]["options"][0].update({key: value}) + return remote_dummy + + async def test_child_name(self): + s = SlashCommandGroup("subcommand") + ss = s.create_subgroup("testing", description="desc") + ss.add_command(SlashCommand(parent=ss)) + assert not await edit_needed(s, self.dict_factory({}, {})) + s = SlashCommandGroup("subcommand") + ss = s.create_subgroup("testing", description="desc") + ss.add_command(SlashCommand(name="newsubcommand_name")) + assert await edit_needed(s, self.dict_factory({}, {})) + s = SlashCommandGroup("subcommand") + ss = s.create_subgroup("testing", description="desc") + ss.add_command(SlashCommand()) + assert await edit_needed(s, self.dict_factory({}, {"name": "new_discord_name"})) + + async def test_child(self): + s = SlashCommandGroup("subcommand") + ss = s.create_subgroup("testing", description="desc") + ss.add_command(SlashCommand(description="newdescript")) + assert await edit_needed(s, self.dict_factory({}, {})) + s = SlashCommandGroup("subcommand") + ss = s.create_subgroup("testing", description="desc") + ss.add_command(SlashCommand()) + assert await edit_needed( + s, self.dict_factory({}, {"description": "new_descript"}) + ) + + async def test_subsubcommand_counts(self): + s = SlashCommandGroup("subcommand") + ss = s.create_subgroup("testing", description="desc") + assert await edit_needed(s, self.dict_factory({}, {})) + s = SlashCommandGroup("subcommand") + ss = s.create_subgroup("testing", description="desc") + ss.add_command(SlashCommand(parent=ss)) + rd = self.dict_factory({}, {}) + rd["options"][0]["options"] = [] + assert await edit_needed(s, rd) From 93ff9c168071cdbf52e6773e0015ce0dc889e8f8 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Tue, 3 Mar 2026 11:48:12 -0600 Subject: [PATCH 09/27] chore: Apply Minor Changes From Code Review --- discord/bot.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/discord/bot.py b/discord/bot.py index 1c6c5b3212..a464def998 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -114,7 +114,7 @@ def __init__( self.defaults = defaults self.callback = callback - def _check_defaults(self, local, remote) -> bool | None: + def _check_defaults(self, local: Any, remote: Any) -> bool | None: defaults = (local in self.defaults) + (remote in self.defaults) if defaults == 2: # Both are COMMAND_DEFAULTS, so they can be counted as the same @@ -126,7 +126,7 @@ def _check_defaults(self, local, remote) -> bool | None: # Only one is a default, so the command must be out of sync return False - def check(self, local, remote) -> bool: + def check(self, local: Any, remote: Any) -> bool: """ Compares the local and remote objects. @@ -143,7 +143,7 @@ def check(self, local, remote) -> bool: class DefaultSetComparison(DefaultComparison): @override - def check(self, local, remote) -> bool: + def check(self, local: Any, remote: Any) -> bool: try: local = set(local) except TypeError: @@ -180,7 +180,7 @@ def _compare_defaults( OPTION_DEFAULT_VALUES = ([], MISSING) -def _option_comparison_check(local, remote) -> bool: +def _option_comparison_check(local: Any, remote: Any) -> bool: matching = (local in OPTION_DEFAULT_VALUES) + (remote in OPTION_DEFAULT_VALUES) if matching == 2: return True @@ -198,7 +198,7 @@ def _option_comparison_check(local, remote) -> bool: CHOICES_DEFAULT_VALUES = ([], MISSING) -def _choices_comparison_check(local, remote) -> bool: +def _choices_comparison_check(local: Any, remote: Any) -> bool: matching = (local in CHOICES_DEFAULT_VALUES) + (remote in CHOICES_DEFAULT_VALUES) if matching == 2: return True @@ -521,8 +521,8 @@ def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: "id": int(registered_commands_dict[cmd.name]["id"]), } ) + # We have this command registered and it's the same else: - # We have this command registered and it's the same return_value.append( {"command": cmd, "action": None, "id": int(match["id"])} ) From 2bf4228601510effca2a61231937b889798b4f83 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Tue, 3 Mar 2026 20:51:08 -0600 Subject: [PATCH 10/27] fix: Use Dummy Data In Place Of Fetching AppInfo --- tests/test_command_syncing.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/test_command_syncing.py b/tests/test_command_syncing.py index 862c711032..caec296aef 100644 --- a/tests/test_command_syncing.py +++ b/tests/test_command_syncing.py @@ -1,10 +1,11 @@ import copy -from typing import Any +from typing import Any, override import pytest import discord from discord import MISSING, Bot, SlashCommandGroup +from discord.bot import COMMAND_DEFAULTS, DefaultSetComparison from discord.types.interactions import ApplicationCommand, ApplicationCommandOption pytestmark = pytest.mark.asyncio @@ -56,10 +57,20 @@ async def dummy_callback(ctx): } +class DummyBot(Bot): + @override + async def _get_command_defaults(self): + command_defaults = COMMAND_DEFAULTS.copy() + command_defaults["integration_types"] = DefaultSetComparison( + (MISSING, {0}), lambda x, y: set(x) == set(y) + ) + return command_defaults + + async def edit_needed( local: SlashCommand | SlashCommandGroup, remote: ApplicationCommand ): - b = Bot() + b = DummyBot() b.add_application_command(local) r = await b.get_desynced_commands(prefetched=[remote]) return r[0]["action"] == "edit" @@ -954,7 +965,7 @@ async def test_parent_name(self): async def edit_needed( local: SlashCommand | SlashCommandGroup, remote: ApplicationCommand ): - b = Bot() + b = DummyBot() b.add_application_command(local) r = await b.get_desynced_commands(prefetched=[remote]) return r[0]["action"] == "upsert" and r[1]["action"] == "delete" From 3fd30ed233520d01641d517ff979f4ff2991a367 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Wed, 18 Mar 2026 21:17:12 -0400 Subject: [PATCH 11/27] fix: Only Checking First Sub Command --- discord/bot.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/discord/bot.py b/discord/bot.py index a464def998..e01c47ef36 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -470,8 +470,12 @@ def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: match_ = find( lambda x: x["name"] == subcommand.name, match["options"] ) - if match_ is not None: - return _check_command(subcommand, match_) + if match_ is None: + return False + elif not _check_command(subcommand, match_): + return False + else: + return True else: if cmd.parent is None: return _compare_defaults( From 689d86f5682277abcca4d0697a964d068ceb6827 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Wed, 18 Mar 2026 21:19:06 -0400 Subject: [PATCH 12/27] feat(tests): Checks For Multiple Sub Commands --- tests/test_command_syncing.py | 177 ++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/tests/test_command_syncing.py b/tests/test_command_syncing.py index caec296aef..8067e03d19 100644 --- a/tests/test_command_syncing.py +++ b/tests/test_command_syncing.py @@ -1020,6 +1020,92 @@ async def test_subcommand_counts(self): rd["options"] = [] assert await edit_needed(s, rd) + async def test_multi_subcommand_diff(self): + # No Different + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand(parent=s)) + s.add_command(SlashCommand(parent=s, name="another")) + rd = self.dict_factory({}, {}) + rd["options"].append( + ApplicationCommandOption( + type=1, + name="another", + name_localizations=None, + description="desc", + description_localizations=None, + required=True, + ) + ) + assert not await edit_needed(s, rd) + + # First Local Different + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand(parent=s, description="different")) + s.add_command(SlashCommand(parent=s, name="another")) + rd = self.dict_factory({}, {}) + rd["options"].append( + ApplicationCommandOption( + type=1, + name="another", + name_localizations=None, + description="desc", + description_localizations=None, + required=True, + ) + ) + assert await edit_needed(s, rd) + + # Second Local Different + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand(parent=s)) + s.add_command(SlashCommand(parent=s, name="another", description="different")) + rd = self.dict_factory({}, {}) + rd["options"].append( + ApplicationCommandOption( + type=1, + name="another", + name_localizations=None, + description="desc", + description_localizations=None, + required=True, + ) + ) + assert await edit_needed(s, rd) + + # First Remote Different + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand(parent=s)) + s.add_command(SlashCommand(parent=s, name="another")) + rd = self.dict_factory({}, {"description": "different"}) + rd["options"].append( + ApplicationCommandOption( + type=1, + name="another", + name_localizations=None, + description="desc", + description_localizations=None, + required=True, + ) + ) + assert await edit_needed(s, rd) + + # Second Remote Different + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand(parent=s)) + s.add_command(SlashCommand(parent=s, name="another")) + rd = self.dict_factory({}, {}) + rd["options"].append( + ApplicationCommandOption( + type=1, + name="another", + name_localizations=None, + description="different", + description_localizations=None, + required=True, + ) + ) + assert await edit_needed(s, rd) + class TestSubSubCommandSyncing: @@ -1091,3 +1177,94 @@ async def test_subsubcommand_counts(self): rd = self.dict_factory({}, {}) rd["options"][0]["options"] = [] assert await edit_needed(s, rd) + + async def test_multi_subsubcommand_diff(self): + # No Different + s = SlashCommandGroup("subcommand") + ss = s.create_subgroup("testing", description="desc") + ss.add_command(SlashCommand(parent=ss)) + ss.add_command(SlashCommand(parent=ss, name="another")) + rd = self.dict_factory({}, {}) + rd["options"][0]["options"].append( + ApplicationCommandOption( + type=1, + name="another", + name_localizations=None, + description="desc", + description_localizations=None, + required=True, + ) + ) + assert not await edit_needed(s, rd) + + # First Local Different + s = SlashCommandGroup("subcommand") + ss = s.create_subgroup("testing", description="desc") + ss.add_command(SlashCommand(parent=ss, description="different")) + ss.add_command(SlashCommand(parent=ss, name="another")) + rd = self.dict_factory({}, {}) + rd["options"][0]["options"].append( + ApplicationCommandOption( + type=1, + name="another", + name_localizations=None, + description="desc", + description_localizations=None, + required=True, + ) + ) + assert await edit_needed(s, rd) + + # Second Local Different + s = SlashCommandGroup("subcommand") + ss = s.create_subgroup("testing", description="desc") + ss.add_command(SlashCommand(parent=ss)) + ss.add_command(SlashCommand(parent=ss, name="another", description="different")) + rd = self.dict_factory({}, {}) + rd["options"][0]["options"].append( + ApplicationCommandOption( + type=1, + name="another", + name_localizations=None, + description="desc", + description_localizations=None, + required=True, + ) + ) + assert await edit_needed(s, rd) + + # First Remote Different + s = SlashCommandGroup("subcommand") + ss = s.create_subgroup("testing", description="desc") + ss.add_command(SlashCommand(parent=ss)) + ss.add_command(SlashCommand(parent=ss, name="another")) + rd = self.dict_factory({}, {"description": "different"}) + rd["options"][0]["options"].append( + ApplicationCommandOption( + type=1, + name="another", + name_localizations=None, + description="desc", + description_localizations=None, + required=True, + ) + ) + assert await edit_needed(s, rd) + + # Second Remote Different + s = SlashCommandGroup("subcommand") + ss = s.create_subgroup("testing", description="desc") + ss.add_command(SlashCommand(parent=ss)) + ss.add_command(SlashCommand(parent=ss, name="another")) + rd = self.dict_factory({}, {}) + rd["options"][0]["options"].append( + ApplicationCommandOption( + type=1, + name="another", + name_localizations=None, + description="different", + description_localizations=None, + required=True, + ) + ) + assert await edit_needed(s, rd) From 62a5900c5ae06e3da6044c5b50e55ba820660dbb Mon Sep 17 00:00:00 2001 From: Paillat Date: Wed, 18 Mar 2026 11:09:44 +0100 Subject: [PATCH 13/27] Update tests/test_command_syncing.py Signed-off-by: Paillat --- tests/test_command_syncing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_command_syncing.py b/tests/test_command_syncing.py index 8067e03d19..0b577818d9 100644 --- a/tests/test_command_syncing.py +++ b/tests/test_command_syncing.py @@ -1,5 +1,7 @@ import copy -from typing import Any, override +from typing import Any + +from typing_extensions import override import pytest From 748a960134b8e8ee96a940dbb77f7f869e499dd3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:10:11 +0000 Subject: [PATCH 14/27] style(pre-commit): auto fixes from pre-commit.com hooks --- tests/test_command_syncing.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_command_syncing.py b/tests/test_command_syncing.py index 0b577818d9..75673733df 100644 --- a/tests/test_command_syncing.py +++ b/tests/test_command_syncing.py @@ -1,9 +1,8 @@ import copy from typing import Any -from typing_extensions import override - import pytest +from typing_extensions import override import discord from discord import MISSING, Bot, SlashCommandGroup From 085d02f8ff8ed345c0522d5509ca0beca81420d6 Mon Sep 17 00:00:00 2001 From: Paillat Date: Mon, 27 Jul 2026 23:42:36 +0200 Subject: [PATCH 15/27] fix: Wow that's messed up --- tests/conftest.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000..f91d5338e3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,11 @@ +import asyncio + +import pytest + + +@pytest.fixture(autouse=True) +def _reset_event_loop_policy(): + # pytest-asyncio calls per-test asyncio.set_event_loop(None) at the end + # but doesn't reset the event loop policy so we do it here or else it breaks on py <3.14 + yield + asyncio.set_event_loop_policy(None) From 345ba3107a0aea6d1e90759e74a3d34c8a2cf769 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Mon, 27 Jul 2026 18:30:25 -0500 Subject: [PATCH 16/27] chore: Update Changelog Version --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7b377dc85..ed54951d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ These changes are available on the `master` branch, but have not yet been releas ### Changed +- Fixed backend logic for `sync_commands` to only sync when needed. + ([#2990](https://github.com/Pycord-Development/pycord/pull/2990)) + ### Fixed - Fix `TypeError` when accessing `ApplicationCommand.guild_only` or @@ -218,8 +221,6 @@ These changes are available on the `master` branch, but have not yet been releas ([#3105](https://github.com/Pycord-Development/pycord/pull/3105)) - Fixed the update of a user's `avatar_decoration` to now cause an `on_user_update` event to fire. ([#3103](https://github.com/Pycord-Development/pycord/pull/3103)) -- Fixed backend logic for `sync_commands` to only sync when needed. - ([#2990](https://github.com/Pycord-Development/pycord/pull/2990)) ### Deprecated From 455a995f57ca1fb854b4758167730ba9fe2813d6 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Mon, 27 Jul 2026 22:50:02 -0500 Subject: [PATCH 17/27] feat: Add Default Integration Types When None Are Given --- discord/bot.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/discord/bot.py b/discord/bot.py index e01c47ef36..c0407ca213 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -222,11 +222,7 @@ def _choices_comparison_check(local: Any, remote: Any) -> bool: "options": DefaultComparison(OPTION_DEFAULT_VALUES, _option_comparison_check), "default_member_permissions": DefaultComparison((None, MISSING)), "nsfw": DefaultComparison((False, MISSING)), - # TODO: Change the below default if needed to use the correct default integration types - # Discord States That This Defaults To "your app's configured contexts" - "integration_types": DefaultSetComparison( - (MISSING, {0, 1}), lambda x, y: set(x) == set(y) - ), + # integration_types gets set in ApplicationCommandMixin._get_command_defaults "contexts": DefaultSetComparison((None, MISSING), lambda x, y: set(x) == set(y)), } SUBCOMMAND_DEFAULTS: NestedComparison = { @@ -421,6 +417,9 @@ async def _get_command_defaults(self): app_info = await self._bot.application_info() integration_contexts = app_info.integration_types_config._to_payload().keys() + if len(integration_contexts) == 0: + integration_contexts = {0, 1} + command_defaults = COMMAND_DEFAULTS.copy() command_defaults["integration_types"] = DefaultSetComparison( (MISSING, integration_contexts), lambda x, y: set(x) == set(y) From 11b3400335d3d12dac55ab7b3bab404bf2ed9f97 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Tue, 28 Jul 2026 11:14:48 -0500 Subject: [PATCH 18/27] refactor: Use Enum For InteractionTypes --- discord/bot.py | 3 ++- tests/test_command_syncing.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/discord/bot.py b/discord/bot.py index c0407ca213..8d0294ae90 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -48,6 +48,7 @@ from typing_extensions import override +from discord import IntegrationTypesConfig from .client import Client from .cog import CogMixin from .commands import ( @@ -418,7 +419,7 @@ async def _get_command_defaults(self): integration_contexts = app_info.integration_types_config._to_payload().keys() if len(integration_contexts) == 0: - integration_contexts = {0, 1} + integration_contexts = {IntegrationType.guild_install.value, IntegrationType.user_install.value} command_defaults = COMMAND_DEFAULTS.copy() command_defaults["integration_types"] = DefaultSetComparison( diff --git a/tests/test_command_syncing.py b/tests/test_command_syncing.py index 75673733df..feb60a1d57 100644 --- a/tests/test_command_syncing.py +++ b/tests/test_command_syncing.py @@ -5,7 +5,7 @@ from typing_extensions import override import discord -from discord import MISSING, Bot, SlashCommandGroup +from discord import MISSING, Bot, SlashCommandGroup, IntegrationType from discord.bot import COMMAND_DEFAULTS, DefaultSetComparison from discord.types.interactions import ApplicationCommand, ApplicationCommandOption @@ -63,7 +63,7 @@ class DummyBot(Bot): async def _get_command_defaults(self): command_defaults = COMMAND_DEFAULTS.copy() command_defaults["integration_types"] = DefaultSetComparison( - (MISSING, {0}), lambda x, y: set(x) == set(y) + (MISSING, {IntegrationType.guild_install.value}), lambda x, y: set(x) == set(y) ) return command_defaults From af20adb166263eacc8ed177d889a1778a875059f Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Tue, 28 Jul 2026 11:42:10 -0500 Subject: [PATCH 19/27] docs: Add To Changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed54951d65..2f0109a7ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ These changes are available on the `master` branch, but have not yet been releas ### Changed -- Fixed backend logic for `sync_commands` to only sync when needed. +- Refactored backend logic for `sync_commands` to only sync when needed and be easily extensible. ([#2990](https://github.com/Pycord-Development/pycord/pull/2990)) ### Fixed From e93766bea52d007e56f27abb8925f76022709d7d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:43:48 +0000 Subject: [PATCH 20/27] style(pre-commit): auto fixes from pre-commit.com hooks --- CHANGELOG.md | 4 ++-- discord/bot.py | 6 +++++- tests/test_command_syncing.py | 5 +++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f0109a7ac..4c8ccc1c98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,8 @@ These changes are available on the `master` branch, but have not yet been releas ### Changed -- Refactored backend logic for `sync_commands` to only sync when needed and be easily extensible. - ([#2990](https://github.com/Pycord-Development/pycord/pull/2990)) +- Refactored backend logic for `sync_commands` to only sync when needed and be easily + extensible. ([#2990](https://github.com/Pycord-Development/pycord/pull/2990)) ### Fixed diff --git a/discord/bot.py b/discord/bot.py index 8d0294ae90..158ec4c5bb 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -49,6 +49,7 @@ from typing_extensions import override from discord import IntegrationTypesConfig + from .client import Client from .cog import CogMixin from .commands import ( @@ -419,7 +420,10 @@ async def _get_command_defaults(self): integration_contexts = app_info.integration_types_config._to_payload().keys() if len(integration_contexts) == 0: - integration_contexts = {IntegrationType.guild_install.value, IntegrationType.user_install.value} + integration_contexts = { + IntegrationType.guild_install.value, + IntegrationType.user_install.value, + } command_defaults = COMMAND_DEFAULTS.copy() command_defaults["integration_types"] = DefaultSetComparison( diff --git a/tests/test_command_syncing.py b/tests/test_command_syncing.py index feb60a1d57..090048fcae 100644 --- a/tests/test_command_syncing.py +++ b/tests/test_command_syncing.py @@ -5,7 +5,7 @@ from typing_extensions import override import discord -from discord import MISSING, Bot, SlashCommandGroup, IntegrationType +from discord import MISSING, Bot, IntegrationType, SlashCommandGroup from discord.bot import COMMAND_DEFAULTS, DefaultSetComparison from discord.types.interactions import ApplicationCommand, ApplicationCommandOption @@ -63,7 +63,8 @@ class DummyBot(Bot): async def _get_command_defaults(self): command_defaults = COMMAND_DEFAULTS.copy() command_defaults["integration_types"] = DefaultSetComparison( - (MISSING, {IntegrationType.guild_install.value}), lambda x, y: set(x) == set(y) + (MISSING, {IntegrationType.guild_install.value}), + lambda x, y: set(x) == set(y), ) return command_defaults From 894d512255a87163b0fdbdac7553d3e331bd9909 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:33:19 +0000 Subject: [PATCH 21/27] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/bot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discord/bot.py b/discord/bot.py index 112942829a..9591cfc6bb 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -39,7 +39,6 @@ TYPE_CHECKING, Any, Literal, - Mapping, TypeAlias, TypeVar, ) From 5581164bd5d2d01adbae4df6f0e963f77bf92353 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Thu, 30 Jul 2026 17:26:46 -0500 Subject: [PATCH 22/27] fix: Import Location --- discord/bot.py | 2 -- tests/test_command_syncing.py | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/discord/bot.py b/discord/bot.py index 9591cfc6bb..2e33f8d08d 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -45,8 +45,6 @@ from typing_extensions import override -from discord import IntegrationTypesConfig - from .client import Client from .cog import CogMixin from .commands import ( diff --git a/tests/test_command_syncing.py b/tests/test_command_syncing.py index 090048fcae..4ee3f9310f 100644 --- a/tests/test_command_syncing.py +++ b/tests/test_command_syncing.py @@ -5,8 +5,9 @@ from typing_extensions import override import discord -from discord import MISSING, Bot, IntegrationType, SlashCommandGroup +from discord import MISSING, Bot, SlashCommandGroup from discord.bot import COMMAND_DEFAULTS, DefaultSetComparison +from discord.enums import IntegrationType from discord.types.interactions import ApplicationCommand, ApplicationCommandOption pytestmark = pytest.mark.asyncio From aa1ccedfae3e5adbba1914cf2f7411f962b8bffb Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Mon, 10 Aug 2026 17:40:38 -0500 Subject: [PATCH 23/27] fix: Check parent command attributes --- discord/bot.py | 21 +++++++++++++++++++-- tests/test_command_syncing.py | 16 +++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/discord/bot.py b/discord/bot.py index 2e33f8d08d..e13ad7f30e 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -99,7 +99,7 @@ class DefaultComparison: callback: Callable[[Any, Any], bool] A callable that will do additional comparison on the objects if neither are a default value. Defaults to a `==` comparison. - It should accept the 2 objects as arguments and return True if they should be considered equivalent + It should accept local and remote command objects as arguments and return True if they should be considered equivalent and False otherwise. """ @@ -230,6 +230,8 @@ def _choices_comparison_check(local: Any, remote: Any) -> bool: "description_localizations": DefaultComparison((None, {}, MISSING)), "options": DefaultComparison(OPTION_DEFAULT_VALUES, _option_comparison_check), } +PARENT_SUBCOMMAND_DEFAULTS: NestedComparison = SUBCOMMAND_DEFAULTS.copy() +del PARENT_SUBCOMMAND_DEFAULTS["options"] COMMAND_OPTION_DEFAULTS: NestedComparison = { "type": DefaultComparison(()), "name": DefaultComparison(()), @@ -456,13 +458,28 @@ async def get_desynced_commands( respectively contain the command and the action to perform. Other keys may also be present depending on the action, including ``id``. """ + # We can suggest the user to upsert, edit, delete, or bulk upsert the commands updated_command_defaults = await self._get_command_defaults() + updated_parent_command_defaults = updated_command_defaults.copy() + del updated_parent_command_defaults["options"] - # We can suggest the user to upsert, edit, delete, or bulk upsert the commands def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: """Returns True If Commands Are Equivalent""" if isinstance(cmd, SlashCommandGroup): + # Check the fields relevant to the parent command + if cmd.parent is None: + parent_equality = _compare_defaults( + cmd.to_dict(), match, updated_parent_command_defaults + ) + else: + parent_equality = _compare_defaults( + cmd.to_dict(), match, PARENT_SUBCOMMAND_DEFAULTS + ) + # Return early if the parent commands have a difference + if not parent_equality: + return False + # Then check the subcommands if len(cmd.subcommands) != len(match.get("options", [])): return False for i, subcommand in enumerate(cmd.subcommands): diff --git a/tests/test_command_syncing.py b/tests/test_command_syncing.py index 4ee3f9310f..76bd5c5551 100644 --- a/tests/test_command_syncing.py +++ b/tests/test_command_syncing.py @@ -5,7 +5,7 @@ from typing_extensions import override import discord -from discord import MISSING, Bot, SlashCommandGroup +from discord import MISSING, Bot from discord.bot import COMMAND_DEFAULTS, DefaultSetComparison from discord.enums import IntegrationType from discord.types.interactions import ApplicationCommand, ApplicationCommandOption @@ -42,6 +42,15 @@ async def dummy_callback(ctx): } +class SlashCommandGroup(discord.SlashCommandGroup): + def __init__(self, name, **kwargs): + if (desc := kwargs.get("description")) is not None: + kwargs.pop("description") + else: + desc = "desc" + super().__init__(name, description=desc, **kwargs) + + remote_dummy_base: dict = { "id": "1", "application_id": "1", @@ -964,6 +973,11 @@ def dict_factory(top: dict, command: dict) -> dict[str, Any]: remote_dummy["options"][0].update({key: value}) return remote_dummy + async def test_default(self): + s = SlashCommandGroup("subcommand") + s.add_command(SlashCommand()) + assert not await edit_needed(s, self.dict_factory({}, {})) + async def test_parent_name(self): async def edit_needed( local: SlashCommand | SlashCommandGroup, remote: ApplicationCommand From a45f83c51600c9bbc3f42298e2335e036d728e28 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Mon, 10 Aug 2026 18:09:22 -0500 Subject: [PATCH 24/27] refactor: Simplify code --- discord/bot.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/discord/bot.py b/discord/bot.py index e13ad7f30e..b56d759491 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -486,9 +486,7 @@ def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: match_ = find( lambda x: x["name"] == subcommand.name, match["options"] ) - if match_ is None: - return False - elif not _check_command(subcommand, match_): + if match_ is None or not _check_command(subcommand, match_): return False else: return True From 51bee7910cc1b8fe79085a01a6deb9ff833449e5 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Mon, 10 Aug 2026 18:20:00 -0500 Subject: [PATCH 25/27] feat: Documents SlashCommandGroup.add_command --- discord/commands/core.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/discord/commands/core.py b/discord/commands/core.py index e2354e64ad..1221a5abbc 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -1392,9 +1392,22 @@ def to_dict(self) -> dict: return as_dict def add_command(self, command: SlashCommand | SlashCommandGroup) -> None: + """Adds a :class:`.SlashCommand` or :class:`.SlashCommandGroup` as a subcommand. + + This is usually not called, instead the :meth:`command` or + other shortcut decorators are used instead. + + .. versionadded:: 2.9 + + Parameters + ---------- + command: Union[:class:`.SlashCommand`, :class:`.SlashCommandGroup`] + The command to add. + """ if command.cog is None and self.cog is not None: command.cog = self.cog + command.parent = self self.subcommands.append(command) def command( @@ -1410,7 +1423,7 @@ def command( """ def wrap(func) -> T: - command = cls(func, parent=self, **kwargs) + command = cls(func, **kwargs) self.add_command(command) return command @@ -1508,7 +1521,6 @@ def inner(cls: type[SlashCommandGroup]) -> SlashCommandGroup: else "No description provided" ), guild_ids=guild_ids, - parent=self, ) self.add_command(group) return group From 032f556cb958e57e6afd09000905496fe68e7d58 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Mon, 10 Aug 2026 23:20:47 -0500 Subject: [PATCH 26/27] chore: Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c42ff5f256..33830bfb4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ These changes are available on the `master` branch, but have not yet been releas - Added `Member.vr_status` property. ([#3328](https://github.com/Pycord-Development/pycord/pull/3328)) +- Documented `SlashCommandGroup.add_command`. + ([#3346](https://github.com/Pycord-Development/pycord/pull/3346)) ### Changed From 3f12e213d3ed58276ebee76d4c5eed207af3f725 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Sat, 15 Aug 2026 21:36:55 -0500 Subject: [PATCH 27/27] docs: Add docs for _get_command_defaults --- discord/bot.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/discord/bot.py b/discord/bot.py index b56d759491..a5a94fa797 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -412,7 +412,16 @@ def get_application_command( return return command - async def _get_command_defaults(self): + async def _get_command_defaults(self) -> NestedComparison: + """|coro| + + Provides an object containing the dynamic fields of command defaults based on application info. + + Returns + ------- + NestedComparison + The NestedComparison for command defaults with the dynamic fields filled in. + """ app_info = await self._bot.application_info() integration_contexts = app_info.integration_types_config._to_payload().keys()