From d2aac609076f3a31b5910985b552e1f02d0f6e31 Mon Sep 17 00:00:00 2001 From: Dorukyum Date: Mon, 11 Apr 2022 20:52:05 +0300 Subject: [PATCH 1/5] Initial commit for the rewrite of options --- discord/commands/core.py | 52 ++++++++++++++++--------------------- discord/commands/options.py | 9 ++++--- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/discord/commands/core.py b/discord/commands/core.py index a6686fe3a6..b799ef70af 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -646,22 +646,23 @@ def __init__(self, func: Callable, *args, **kwargs) -> None: if self.permissions and self.default_permission: self.default_permission = False - def _parse_options(self, params) -> List[Option]: - if list(params.items())[0][0] == "self": - temp = list(params.items()) - temp.pop(0) - params = dict(temp) + def _check_required_params(self, params): params = iter(params.items()) + required_params = ["self", "context"] if self.attached_to_group or self.cog else ["context"] + for p in required_params: + try: + next(params) + except StopIteration: + raise ClientException(f'Callback for {self.name} command is missing "{p}" parameter.') - # next we have the 'ctx' as the next parameter - try: - next(params) - except StopIteration: - raise ClientException(f'Callback for {self.name} command is missing "ctx" parameter.') + return params + + def _parse_options(self, params, *, check_params: bool = True) -> List[Option]: + if check_params: + params = self._check_required_params(params) final_options = [] for p_name, p_obj in params: - option = p_obj.annotation if option == inspect.Parameter.empty: option = str @@ -673,38 +674,32 @@ def _parse_options(self, params) -> List[Option]: option = Option(option.__args__, "No description provided") if not isinstance(option, Option): - option = Option(option, "No description provided") + if isinstance(p_obj.default, Option): # arg: type = Option(...) + p_obj.default.input_type = SlashCommandOptionType.from_datatype(option) + option = p_obj.default + else: # arg: Option(...) = default + option = Option(option, "No description provided") if option.default is None: - if p_obj.default == inspect.Parameter.empty: - option.default = None - else: + if not p_obj.default == inspect.Parameter.empty and not isinstance(p_obj.default, Option): option.default = p_obj.default option.required = False if option.name is None: option.name = p_name - option._parameter_name = p_name + if option.name != p_name: + option._parameter_name = p_name validate_chat_input_name(option.name) validate_chat_input_description(option.description) + print(option.name, option.default, option.required) final_options.append(option) return final_options def _match_option_param_names(self, params, options): - if list(params.items())[0][0] == "self": - temp = list(params.items()) - temp.pop(0) - params = dict(temp) - params = iter(params.items()) - - # next we have the 'ctx' as the next parameter - try: - next(params) - except StopIteration: - raise ClientException(f'Callback for {self.name} command is missing "ctx" parameter.') + params = self._check_required_params(params) check_annotations = [ lambda o, a: o.input_type == SlashCommandOptionType.string @@ -728,10 +723,9 @@ def _match_option_param_names(self, params, options): o._parameter_name = p_name left_out_params = OrderedDict() - left_out_params[""] = "" # bypass first iter (ctx) for k, v in params: left_out_params[k] = v - options.extend(self._parse_options(left_out_params)) + options.extend(self._parse_options(left_out_params, check_params=False)) return options diff --git a/discord/commands/options.py b/discord/commands/options.py index d1c7262edf..7b21571597 100644 --- a/discord/commands/options.py +++ b/discord/commands/options.py @@ -80,12 +80,12 @@ async def hello( ---------- input_type: :class:`Any` The type of input that is expected for this option. - description: :class:`str` - The description of this option. - Must be 100 characters or fewer. name: :class:`str` The name of this option visible in the UI. Inherits from the variable name if not provided as a parameter. + description: Optional[:class:`str`] + The description of this option. + Must be 100 characters or fewer. choices: Optional[List[Union[:class:`Any`, :class:`OptionChoice`]]] The list of available choices for this option. Can be a list of values or :class:`OptionChoice` objects (which represent a name:value pair). @@ -115,10 +115,11 @@ async def hello( See `here `_ for a list of valid locales. """ - def __init__(self, input_type: Any, /, description: str = None, **kwargs) -> None: + def __init__(self, input_type: Any = str, /, description: Optional[str] = None, **kwargs) -> None: self.name: Optional[str] = kwargs.pop("name", None) if self.name is not None: self.name = str(self.name) + self._parameter_name = self.name # default self.description = description or "No description provided" self.converter = None self._raw_type = input_type From 5ba94783804d89b3e961aa5701ae7dbea014d8c2 Mon Sep 17 00:00:00 2001 From: Dorukyum Date: Tue, 12 Apr 2022 15:45:05 +0300 Subject: [PATCH 2/5] Minor code changes --- discord/commands/core.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/discord/commands/core.py b/discord/commands/core.py index b799ef70af..a01a049713 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -669,9 +669,9 @@ def _parse_options(self, params, *, check_params: bool = True) -> List[Option]: if self._is_typing_union(option): if self._is_typing_optional(option): - option = Option(option.__args__[0], "No description provided", required=False) + option = Option(option.__args__[0], "No description provided", required=False) # type: ignore # union type else: - option = Option(option.__args__, "No description provided") + option = Option(option.__args__, "No description provided") # type: ignore # union type if not isinstance(option, Option): if isinstance(p_obj.default, Option): # arg: type = Option(...) @@ -693,7 +693,6 @@ def _parse_options(self, params, *, check_params: bool = True) -> List[Option]: validate_chat_input_name(option.name) validate_chat_input_description(option.description) - print(option.name, option.default, option.required) final_options.append(option) return final_options @@ -701,13 +700,13 @@ def _parse_options(self, params, *, check_params: bool = True) -> List[Option]: def _match_option_param_names(self, params, options): params = self._check_required_params(params) - check_annotations = [ + check_annotations: List[Callable[[Option, Type], bool]] = [ lambda o, a: o.input_type == SlashCommandOptionType.string and o.converter is not None, # pass on converters lambda o, a: isinstance(o.input_type, SlashCommandOptionType), # pass on slash cmd option type enums lambda o, a: isinstance(o._raw_type, tuple) and a == Union[o._raw_type], # type: ignore # union types lambda o, a: self._is_typing_optional(a) and not o.required and o._raw_type in a.__args__, # optional - lambda o, a: inspect.isclass(a) and issubclass(a, o._raw_type), # 'normal' types + lambda o, a: isinstance(a, type) and issubclass(a, o._raw_type), # 'normal' types ] for o in options: validate_chat_input_name(o.name) @@ -718,7 +717,7 @@ def _match_option_param_names(self, params, options): raise ClientException(f"Too many arguments passed to the options kwarg.") p_obj = p_obj.annotation - if not any(c(o, p_obj) for c in check_annotations): + if not any(check(o, p_obj) for check in check_annotations): raise TypeError(f"Parameter {p_name} does not match input type of {o.name}.") o._parameter_name = p_name From f5c1243735615b8cfefbef27b609441673b5be21 Mon Sep 17 00:00:00 2001 From: Dorukyum Date: Mon, 18 Apr 2022 11:58:05 +0300 Subject: [PATCH 3/5] Support passing option types as Union --- discord/commands/options.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/commands/options.py b/discord/commands/options.py index 57774b71c5..e60c53c440 100644 --- a/discord/commands/options.py +++ b/discord/commands/options.py @@ -141,7 +141,10 @@ def __init__(self, input_type: Any = str, /, description: Optional[str] = None, else: if _type == SlashCommandOptionType.channel: if not isinstance(input_type, tuple): - input_type = (input_type,) + if hasattr(input_type, "__args__"): # Union + input_type = input_type.__args__ + else: + input_type = (input_type,) for i in input_type: if i.__name__ == "GuildChannel": continue From 48fd30ac826a950a591f88bb5669b0066e313074 Mon Sep 17 00:00:00 2001 From: Dorukyum Date: Mon, 18 Apr 2022 22:03:12 +0300 Subject: [PATCH 4/5] Fix an issue with the self parameter being ignored --- discord/commands/core.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/discord/commands/core.py b/discord/commands/core.py index f5e4d01852..a7a79af1ce 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -696,7 +696,7 @@ def _parse_options(self, params, *, check_params: bool = True) -> List[Option]: if option.name is None: option.name = p_name - if option.name != p_name: + if option.name != p_name or option._parameter_name is None: option._parameter_name = p_name _validate_names(option) @@ -745,6 +745,12 @@ def _is_typing_union(self, annotation): def _is_typing_optional(self, annotation): return self._is_typing_union(annotation) and type(None) in annotation.__args__ # type: ignore + def _set_cog(self, cog): + prev = self.cog + super()._set_cog(cog) + if (prev is None and cog is not None) or (prev is not None and cog is None): + self.options = self._parse_options(self._get_signature_parameters()) # parse again to leave out self + @property def is_subcommand(self) -> bool: return self.parent is not None @@ -1155,7 +1161,7 @@ def _update_copy(self, kwargs: Dict[str, Any]): return self.copy() def _set_cog(self, cog): - self.cog = cog + super()._set_cog(cog) for subcommand in self.subcommands: subcommand._set_cog(cog) From 2ee1b068c0e8a2a00157eeb8c0465435c703870c Mon Sep 17 00:00:00 2001 From: Dorukyum Date: Tue, 19 Apr 2022 09:53:05 +0300 Subject: [PATCH 5/5] Ignore ApplicationContext typehints --- discord/enums.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 4d9805e02e..7a9230de11 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -695,8 +695,9 @@ def from_datatype(cls, datatype): if issubclass(datatype, float): return cls.number - # TODO: Improve the error message - raise TypeError(f"Invalid class {datatype} used as an input type for an Option") + from .commands.context import ApplicationContext + if not issubclass(datatype, ApplicationContext): # TODO: prevent ctx being passed here in cog commands + raise TypeError(f"Invalid class {datatype} used as an input type for an Option") # TODO: Improve the error message class EmbeddedActivity(Enum):