From e456a9e2324b39e8087725aa94cace5455119192 Mon Sep 17 00:00:00 2001 From: EdVraz <88881326+EdVraz@users.noreply.github.com> Date: Fri, 18 Mar 2022 16:03:56 +0100 Subject: [PATCH] feat: implement magic methods (#633) * feat: ``__setattr__`` implementation for components to allow editing * feat: ``__str__`` and ``__int__`` implementation for models. * feat: ``__repr`` implementation for models. * feat: setattr method for embeds * Update gateway.py * Update component.py * ci: correct from checks. Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- interactions/api/gateway.py | 3 +- interactions/api/models/channel.py | 3 + interactions/api/models/channel.pyi | 1 + interactions/api/models/guild.py | 3 + interactions/api/models/guild.pyi | 1 + interactions/api/models/gw.py | 2 +- interactions/api/models/gw.pyi | 1 + interactions/api/models/member.py | 3 + interactions/api/models/member.pyi | 1 + interactions/api/models/message.py | 118 ++++++++++++++++++--------- interactions/api/models/message.pyi | 7 ++ interactions/api/models/user.py | 3 + interactions/api/models/user.pyi | 1 + interactions/models/component.py | 119 ++++++++++++++++++++-------- 14 files changed, 197 insertions(+), 69 deletions(-) diff --git a/interactions/api/gateway.py b/interactions/api/gateway.py index 0cf679ef7..99d7e37b8 100644 --- a/interactions/api/gateway.py +++ b/interactions/api/gateway.py @@ -365,7 +365,8 @@ def _dispatch_event(self, event: str, data: dict) -> None: # sourcery no-metric _name: str = _event_path[0] if len(_event_path) < 3 else "".join(_event_path[:-1]) __obj: object = getattr(__import__(path), _name) - if name in {"_create", "_add"}: + # name in {"_create", "_add"} returns False (tested w message_create) + if any(_ in name for _ in {"_create", "_update", "_add", "_remove", "_delete"}): data["_client"] = self._http self._dispatch.dispatch(f"on_{name}", __obj(**data)) # noqa diff --git a/interactions/api/models/channel.py b/interactions/api/models/channel.py index 0a68e7843..62849660c 100644 --- a/interactions/api/models/channel.py +++ b/interactions/api/models/channel.py @@ -181,6 +181,9 @@ def __init__(self, **kwargs): else None ) + def __repr__(self) -> str: + return self.name + @property def mention(self) -> str: """ diff --git a/interactions/api/models/channel.pyi b/interactions/api/models/channel.pyi index d226da1b4..a3ec3f3f6 100644 --- a/interactions/api/models/channel.pyi +++ b/interactions/api/models/channel.pyi @@ -68,6 +68,7 @@ class Channel(DictSerializerMixin): default_auto_archive_duration: Optional[int] permissions: Optional[str] def __init__(self, **kwargs): ... + def __repr__(self) -> str: ... @property def mention(self) -> str: ... async def send( diff --git a/interactions/api/models/guild.py b/interactions/api/models/guild.py index 19ea3a5c1..e534f0567 100644 --- a/interactions/api/models/guild.py +++ b/interactions/api/models/guild.py @@ -328,6 +328,9 @@ def __init__(self, **kwargs): else None ) + def __repr__(self) -> str: + return self.name + async def ban( self, member_id: int, diff --git a/interactions/api/models/guild.pyi b/interactions/api/models/guild.pyi index 513c4a7cd..02610758f 100644 --- a/interactions/api/models/guild.pyi +++ b/interactions/api/models/guild.pyi @@ -131,6 +131,7 @@ class Guild(DictSerializerMixin): lazy: Any application_command_counts: Any def __init__(self, **kwargs): ... + def __repr__(self) -> str: ... async def ban( self, member_id: int, diff --git a/interactions/api/models/gw.py b/interactions/api/models/gw.py index ecdde38ca..413acad84 100644 --- a/interactions/api/models/gw.py +++ b/interactions/api/models/gw.py @@ -726,7 +726,7 @@ class Presence(DictSerializerMixin): :ivar ClientStatus client_status: The client status across platforms in the event. """ - __slots__ = ("_json", "user", "guild_id", "status", "activities", "client_status") + __slots__ = ("_json", "user", "guild_id", "status", "activities", "client_status", "_client") def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/interactions/api/models/gw.pyi b/interactions/api/models/gw.pyi index 60a024a68..085f8f921 100644 --- a/interactions/api/models/gw.pyi +++ b/interactions/api/models/gw.pyi @@ -181,6 +181,7 @@ class Integration(DictSerializerMixin): def __init__(self, **kwargs): ... class Presence(DictSerializerMixin): + _client: HTTPClient _json: dict user: User guild_id: Snowflake diff --git a/interactions/api/models/member.py b/interactions/api/models/member.py index 8231f2c71..b7152b369 100644 --- a/interactions/api/models/member.py +++ b/interactions/api/models/member.py @@ -81,6 +81,9 @@ def __init__(self, **kwargs): if not self.avatar and self.user: self.avatar = self.user.avatar + def __repr__(self) -> str: + return self.user.username if self.user else self.nick + @property def id(self) -> Snowflake: """ diff --git a/interactions/api/models/member.pyi b/interactions/api/models/member.pyi index 09bdb33a0..5bc145a46 100644 --- a/interactions/api/models/member.pyi +++ b/interactions/api/models/member.pyi @@ -27,6 +27,7 @@ class Member(DictSerializerMixin): communication_disabled_until: Optional[datetime.isoformat] hoisted_role: Any # TODO: post-v4: Investigate what this is for when documented by Discord. def __init__(self, **kwargs): ... + def __repr__(self) -> str: ... @property def mention(self) -> str: ... @property diff --git a/interactions/api/models/message.py b/interactions/api/models/message.py index 901c38392..ff3040d66 100644 --- a/interactions/api/models/message.py +++ b/interactions/api/models/message.py @@ -299,6 +299,9 @@ def __init__(self, **kwargs): ) self.thread = Channel(**self.thread) if self._json.get("thread") else None + def __repr__(self) -> str: + return self.content + async def get_channel(self) -> Channel: """ Gets the channel where the message was sent. @@ -854,6 +857,15 @@ class EmbedImageStruct(DictSerializerMixin): def __init__(self, **kwargs): super().__init__(**kwargs) + def __setattr__(self, key, value) -> None: + super().__setattr__(key, value) + if key != "_json" and (key not in self._json or value != self._json.get(key)): + if value is not None and value is not MISSING: + self._json.update({key: value}) + + elif value is None and key in self._json.keys(): + del self._json[key] + class EmbedProvider(DictSerializerMixin): """ @@ -868,6 +880,15 @@ class EmbedProvider(DictSerializerMixin): def __init__(self, **kwargs): super().__init__(**kwargs) + def __setattr__(self, key, value) -> None: + super().__setattr__(key, value) + if key != "_json" and (key not in self._json or value != self._json.get(key)): + if value is not None and value is not MISSING: + self._json.update({key: value}) + + elif value is None and key in self._json.keys(): + del self._json[key] + class EmbedAuthor(DictSerializerMixin): """ @@ -892,6 +913,15 @@ class EmbedAuthor(DictSerializerMixin): def __init__(self, **kwargs): super().__init__(**kwargs) + def __setattr__(self, key, value) -> None: + super().__setattr__(key, value) + if key != "_json" and (key not in self._json or value != self._json.get(key)): + if value is not None and value is not MISSING: + self._json.update({key: value}) + + elif value is None and key in self._json.keys(): + del self._json[key] + class EmbedFooter(DictSerializerMixin): """ @@ -915,6 +945,15 @@ class EmbedFooter(DictSerializerMixin): def __init__(self, **kwargs): super().__init__(**kwargs) + def __setattr__(self, key, value) -> None: + super().__setattr__(key, value) + if key != "_json" and (key not in self._json or value != self._json.get(key)): + if value is not None and value is not MISSING: + self._json.update({key: value}) + + elif value is None and key in self._json.keys(): + del self._json[key] + class EmbedField(DictSerializerMixin): """ @@ -940,6 +979,15 @@ class EmbedField(DictSerializerMixin): def __init__(self, **kwargs): super().__init__(**kwargs) + def __setattr__(self, key, value) -> None: + super().__setattr__(key, value) + if key != "_json" and (key not in self._json or value != self._json.get(key)): + if value is not None and value is not MISSING: + self._json.update({key: value}) + + elif value is None and key in self._json.keys(): + del self._json[key] + class Embed(DictSerializerMixin): """ @@ -1037,30 +1085,36 @@ def __init__(self, **kwargs): else None ) - # TODO: Complete partial fix. + # (Complete partial fix.) # The issue seems to be that this itself is not updating # JSON result correctly. After numerous attempts I seem to # have the attribute to do it, but _json won't budge at all. # a genexpr is a poor way to go about this, but I know later # on we'll be refactoring this anyhow. What the fuck is breaking # it? - if self.fields: - self._json.update({"fields": [field._json for field in self.fields]}) - - if self.author: - self._json.update({"author": self.author._json}) - - if self.footer: - self._json.update({"footer": self.footer._json}) - if self.thumbnail: - self._json.update({"thumbnail": self.thumbnail._json}) + # the __setattr__ method fixes this issue :) - if self.image: - self._json.update({"image": self.image._json}) - - if self.video: - self._json.update({"video": self.video._json}) + def __setattr__(self, key, value) -> None: + super().__setattr__(key, value) + if key != "_json" and ( + key not in self._json + or ( + value != self._json.get(key) + or not isinstance(value, dict) + # we don't need this instance check in components because serialisation works for them + ) + ): + if value is not None and value is not MISSING: + try: + value = [val._json for val in value] if isinstance(value, list) else value._json + except AttributeError: + if isinstance(value, datetime): + value = value.isoformat() + self._json.update({key: value}) + + elif value is None and key in self._json.keys(): + del self._json[key] def add_field(self, name: str, value: str, inline: Optional[bool] = False) -> None: """ @@ -1074,11 +1128,13 @@ def add_field(self, name: str, value: str, inline: Optional[bool] = False) -> No :type inline?: Optional[bool] """ - if self.fields is None: - self.fields = [] + fields = self.fields or [] + fields.append(EmbedField(name=name, value=value, inline=inline)) - self.fields.append(EmbedField(name=name, value=value, inline=inline)) - self._json.update({"fields": [field._json for field in self.fields]}) + self.fields = fields + # We must use "=" here to call __setattr__. Append does not call any magic, making it impossible to modify the + # json when using it, so the object what would be sent wouldn't be modified. + # Imo this is still better than doing a `self._json.update({"fields": [field._json for ...]})` def clear_fields(self) -> None: """ @@ -1086,7 +1142,6 @@ def clear_fields(self) -> None: """ self.fields = [] - self._json.update({"fields": []}) def insert_field_at( self, index: int, name: str = None, value: str = None, inline: Optional[bool] = False @@ -1104,13 +1159,9 @@ def insert_field_at( :type inline?: Optional[bool] """ - try: - self.fields.insert(index, EmbedField(name=name, value=value, inline=inline)) - - except AttributeError: - self.fields = [EmbedField(name=name, value=value, inline=inline)] - - self._json.update({"fields": [field._json for field in self.fields]}) + fields = self.fields or [] + fields.insert(index, EmbedField(name=name, value=value, inline=inline)) + self.fields = fields def set_field_at( self, index: int, name: str, value: str, inline: Optional[bool] = False @@ -1130,7 +1181,6 @@ def set_field_at( try: self.fields[index] = EmbedField(name=name, value=value, inline=inline) - self._json.update({"fields": [field._json for field in self.fields]}) except AttributeError as e: raise AttributeError("No fields found in Embed") from e @@ -1147,8 +1197,9 @@ def remove_field(self, index: int) -> None: """ try: - self.fields.pop(index) - self._json.update({"fields": [field._json for field in self.fields]}) + fields = self.fields + fields.pop(index) + self.fields = fields except AttributeError as e: raise AttributeError("No fields found in Embed") from e @@ -1163,7 +1214,6 @@ def remove_author(self) -> None: try: del self.author - self._json.update({"author": None}) except AttributeError: pass @@ -1190,7 +1240,6 @@ def set_author( self.author = EmbedAuthor( name=name, url=url, icon_url=icon_url, proxy_icon_url=proxy_icon_url ) - self._json.update({"author": self.author._json}) def set_footer( self, text: str, icon_url: Optional[str] = None, proxy_icon_url: Optional[str] = None @@ -1207,7 +1256,6 @@ def set_footer( """ self.footer = EmbedFooter(text=text, icon_url=icon_url, proxy_icon_url=proxy_icon_url) - self._json.update({"footer": self.footer._json}) def set_image( self, @@ -1230,7 +1278,6 @@ def set_image( """ self.image = EmbedImageStruct(url=url, proxy_url=proxy_url, height=height, width=width) - self._json.update({"image": self.image._json}) def set_thumbnail( self, @@ -1253,4 +1300,3 @@ def set_thumbnail( """ self.thumbnail = EmbedImageStruct(url=url, proxy_url=proxy_url, height=height, width=width) - self._json.update({"thumbnail": self.thumbnail._json}) diff --git a/interactions/api/models/message.pyi b/interactions/api/models/message.pyi index 71eea0ef7..4542cb6ed 100644 --- a/interactions/api/models/message.pyi +++ b/interactions/api/models/message.pyi @@ -92,6 +92,7 @@ class Message(DictSerializerMixin): sticker_items: Optional[List["PartialSticker"]] stickers: Optional[List["Sticker"]] # deprecated def __init__(self, **kwargs): ... + def __repr__(self) -> str: ... async def delete(self, reason: Optional[str] = None) -> None: ... async def edit( self, @@ -235,12 +236,14 @@ class EmbedImageStruct(DictSerializerMixin): height: Optional[str] width: Optional[str] def __init__(self, **kwargs): ... + def __setattr__(self, key, value) -> None: ... class EmbedProvider(DictSerializerMixin): _json: dict name: Optional[str] url: Optional[str] def __init__(self, **kwargs): ... + def __setattr__(self, key, value) -> None: ... class EmbedAuthor(DictSerializerMixin): _json: dict @@ -249,6 +252,7 @@ class EmbedAuthor(DictSerializerMixin): icon_url: Optional[str] proxy_icon_url: Optional[str] def __init__(self, **kwargs): ... + def __setattr__(self, key, value) -> None: ... class EmbedFooter(DictSerializerMixin): _json: dict @@ -256,6 +260,7 @@ class EmbedFooter(DictSerializerMixin): icon_url: Optional[str] proxy_icon_url: Optional[str] def __init__(self, **kwargs): ... + def __setattr__(self, key, value) -> None: ... class EmbedField(DictSerializerMixin): _json: dict @@ -263,6 +268,7 @@ class EmbedField(DictSerializerMixin): inline: Optional[bool] value: str def __init__(self, **kwargs): ... + def __setattr__(self, key, value) -> None: ... class Embed(DictSerializerMixin): _json: dict @@ -280,6 +286,7 @@ class Embed(DictSerializerMixin): author: Optional[EmbedAuthor] fields: Optional[List[EmbedField]] def __init__(self, **kwargs): ... + def __setattr__(self, key, value) -> None: ... def add_field(self, name: str, value: str, inline: Optional[bool] = False) -> None: ... def clear_fields(self) -> None: ... def insert_field_at(self, index: int, name: str, value: str, inline: Optional[bool] = False) -> None: ... diff --git a/interactions/api/models/user.py b/interactions/api/models/user.py index 4f742757e..abfb23956 100644 --- a/interactions/api/models/user.py +++ b/interactions/api/models/user.py @@ -60,6 +60,9 @@ def __init__(self, **kwargs): self.flags = UserFlags(int(self._json.get("flags"))) if self._json.get("flags") else None + def __repr__(self) -> str: + return self.username + @property def mention(self) -> str: """ diff --git a/interactions/api/models/user.pyi b/interactions/api/models/user.pyi index 25c41a7c2..5f0f70185 100644 --- a/interactions/api/models/user.pyi +++ b/interactions/api/models/user.pyi @@ -22,6 +22,7 @@ class User(DictSerializerMixin): premium_type: Optional[int] public_flags: Optional[UserFlags] def __init__(self, **kwargs): ... + def __repr__(self) -> str: ... @property def mention(self) -> str: ... @property diff --git a/interactions/models/component.py b/interactions/models/component.py index 9bafe9547..d650e9a38 100644 --- a/interactions/models/component.py +++ b/interactions/models/component.py @@ -2,24 +2,20 @@ from ..api.error import InteractionException from ..api.models.message import Emoji -from ..api.models.misc import DictSerializerMixin +from ..api.models.misc import MISSING, DictSerializerMixin from ..enums import ButtonStyle, ComponentType, TextStyleType class SelectOption(DictSerializerMixin): """ A class object representing the select option of a select menu. - The structure for a select option: - .. code-block:: python - interactions.SelectOption( label="I'm a cool option. :)", value="internal_option_value", description="some extra info about me! :D", ) - :ivar str label: The label of the select option. :ivar str value: The returned value of the select option. :ivar Optional[str] description?: The description of the select option. @@ -44,21 +40,29 @@ def __init__(self, **kwargs): if self.emoji: self._json.update({"emoji": self.emoji._json}) + def __setattr__(self, key, value) -> None: + super().__setattr__(key, value) + if key != "_json" and (key not in self._json or value != self._json.get(key)): + if value is not None and value is not MISSING: + try: + value = [val._json for val in value] if isinstance(value, list) else value._json + except AttributeError: + pass + self._json.update({key: value}) + elif value is None and key in self._json.keys(): + del self._json[key] + class SelectMenu(DictSerializerMixin): """ A class object representing the select menu of a component. - The structure for a select menu: - .. code-block:: python - interactions.SelectMenu( options=[interactions.SelectOption(...)], placeholder="Check out my options. :)", custom_id="menu_component", ) - :ivar ComponentType type: The type of select menu. Always defaults to ``3``. :ivar str custom_id: The customized "ID" of the select menu. :ivar List[SelectOption] options: The list of select options in the select menu. @@ -102,21 +106,29 @@ def __init__(self, **kwargs) -> None: self._json.update({"type": self.type.value}) self._json.update({"options": [option._json for option in self.options]}) + def __setattr__(self, key, value) -> None: + super().__setattr__(key, value) + if key != "_json" and (key not in self._json or value != self._json.get(key)): + if value is not None and value is not MISSING: + try: + value = [val._json for val in value] if isinstance(value, list) else value._json + except AttributeError: + pass + self._json.update({key: value}) + elif value is None and key in self._json.keys(): + del self._json[key] + class Button(DictSerializerMixin): """ A class object representing the button of a component. - The structure for a button: - .. code-block:: python - interactions.Button( style=interactions.ButtonStyle.DANGER, label="Delete", custom_id="delete_message", ) - :ivar ComponentType type: The type of button. Always defaults to ``2``. :ivar ButtonStyle style: The style of the button. :ivar str label: The label of the button. @@ -143,19 +155,28 @@ def __init__(self, **kwargs) -> None: if self.emoji: self._json.update({"emoji": self.emoji._json}) + def __setattr__(self, key, value) -> None: + super().__setattr__(key, value) + if key != "_json" and (key not in self._json or value != self._json.get(key)): + if value is not None and value is not MISSING: + try: + value = [val._json for val in value] if isinstance(value, list) else value._json + except AttributeError: + pass + self._json.update({key: value}) + elif value is None and key in self._json.keys(): + del self._json[key] + class Component(DictSerializerMixin): """ A class object representing the component in an interaction response/followup. - .. note:: ``components`` is only applicable if an ActionRow is supported, otherwise ActionRow-less will be opted. ``list`` is in reference to the class. - .. warning:: This object object class is only inferred upon when the gateway is processing back information involving a component. Do not use this object for sending. - :ivar ComponentType type: The type of component. :ivar Optional[str] custom_id?: The customized "ID" of the component. :ivar Optional[bool] disabled?: Whether the component is unable to be used. @@ -200,7 +221,7 @@ class Component(DictSerializerMixin): label: Optional[str] emoji: Optional[Emoji] url: Optional[str] - options: Optional[List[SelectMenu]] + options: Optional[List[SelectOption]] placeholder: Optional[str] min_values: Optional[int] max_values: Optional[int] @@ -227,15 +248,25 @@ def __init__(self, **kwargs) -> None: if self._json.get("components"): self._json["components"] = [component._json for component in self.components] + def __setattr__(self, key, value) -> None: + super().__setattr__(key, value) + if key != "_json" and (key not in self._json or value != self._json.get(key)): + if value is not None and value is not MISSING: + try: + value = [val._json for val in value] if isinstance(value, list) else value._json + except AttributeError: + if key == "style": + value = value.value + self._json.update({key: value}) + elif value is None and key in self._json.keys(): + del self._json[key] + class TextInput(DictSerializerMixin): """ A class object representing the text input of a modal. - The structure for a text input: - .. code-block:: python - interactions.TextInput( style=interactions.TextStyleType.SHORT, label="Let's get straight to it: what's 1 + 1?", @@ -243,7 +274,6 @@ class TextInput(DictSerializerMixin): min_length=2, max_length=3, ) - :ivar ComponentType type: The type of input. Always defaults to ``4``. :ivar TextStyleType style: The style of the input. :ivar str custom_id: The custom Id of the input. @@ -283,21 +313,30 @@ def __init__(self, **kwargs): self.style = TextStyleType(self.style) self._json.update({"type": self.type.value, "style": self.style.value}) + def __setattr__(self, key, value) -> None: + super().__setattr__(key, value) + if key != "_json" and (key not in self._json or value != self._json.get(key)): + if value is not None and value is not MISSING: + try: + value = [val._json for val in value] if isinstance(value, list) else value._json + except AttributeError: + if key == "style": + value = value.value + self._json.update({key: value}) + elif value is None and key in self._json.keys(): + del self._json[key] + class Modal(DictSerializerMixin): """ A class object representing a modal. - The structure for a modal: - .. code-block:: python - interactions.Modal( title="Application Form", custom_id="mod_app_form", components=[interactions.TextInput(...)], ) - :ivar str custom_id: The custom ID of the modal. :ivar str title: The title of the modal. :ivar List[Component] components: The components of the modal. @@ -323,27 +362,33 @@ def __init__(self, **kwargs): } ) + def __setattr__(self, key, value) -> None: + super().__setattr__(key, value) + if key != "_json" and (key not in self._json or value != self._json.get(key)): + if value is not None and value is not MISSING: + try: + value = [val._json for val in value] if isinstance(value, list) else value._json + except AttributeError: + pass + self._json.update({key: value}) + elif value is None and key in self._json.keys(): + del self._json[key] + class ActionRow(DictSerializerMixin): """ A class object representing the action row for interaction responses holding components. - .. note:: A message cannot have more than 5 ActionRow's supported. An ActionRow may also support only 1 text input component only. - The structure for an action row: - .. code-block:: python - # "..." represents a component object. # Method 1: interactions.ActionRow(...) - # Method 2: interactions.ActionRow(components=[...]) - :ivar int type: The type of component. Always defaults to ``1``. :ivar Optional[List[Component]] components?: A list of components the ActionRow has, if any. """ @@ -370,6 +415,18 @@ def __init__(self, **kwargs) -> None: if self._json.get("components"): self._json["components"] = [component._json for component in self.components] + def __setattr__(self, key, value) -> None: + super().__setattr__(key, value) + if key != "_json" and (key not in self._json or value != self._json.get(key)): + if value is not None and value is not MISSING: + try: + value = [val._json for val in value] if isinstance(value, list) else value._json + except AttributeError: + pass + self._json.update({key: value}) + elif value is None and key in self._json.keys(): + del self._json[key] + def _build_components(components) -> List[dict]: def __check_action_row():