Repository: Pycord-Development/pycord

Issue:
Options with a default value of None still require the "required" kwarg
### Summary

When using `None` as the default value, `required` defaults to True even though there _is_ a default value.

### Reproduction Steps

- Make an application command (slash command) with at least one Option
- Give the option a default value of None, and see that the field is **required** in Discord (shows a red error message when trying to send the command)
- Change the default value to something else, and see that the field is now no longer required in Discord

### Minimal Reproducible Code

```python
import discord
from discord.commands import Option

bot = discord.Bot()

@bot.slash_command()
async def test(ctx, arg: Option(int, default=None)):
    pass

bot.run(...)
```


### Expected Results

If any value at all is passed, `required` should be `False`, no matter what that value may be. `None` is a pretty common use case and shouldn't always signify that there is **no** default value.

### Actual Results

If the value passed is `None`, it is treated as if no default value is present.

Error in Discord itself: **This option is required. Specify a value.**

To allow this default value to be used, the Option would have to be specified as

```py
Option(int, default=None, required=False)
```
which feels off, if `default` is present then `required` shouldn't be.

### Intents

default + members

### System Information

- Python v3.9.5-final
- py-cord v2.0.0-beta
    - py-cord pkg_resources: v2.0.0b1
- aiohttp v3.7.4.post0
- system info: Linux 4.15.0-58-generic #64-Ubuntu SMP Tue Aug 6 11:12:41 UTC 2019

### Checklist

- [X] I have searched the open issues for duplicates.
- [X] I have shown the entire traceback, if possible.
- [X] I have removed my token from display, if visible.

### Additional Context

This is caused by https://github.com/Pycord-Development/pycord/blob/68e9cf3db5c311153899a0c577c68d88c0feb4cf/discord/commands/options.py#L88-L91

where it's merely checked to be `None`. A better solution would be to check if it's **in** the kwargs dict, no matter the value (which would allow `None`), and *then* pop it. When popping it, the default value is `None`, making it impossible to differentiate from actually being *passed* or being *absent*.

```py
        # using "in" instead of popping the value with a default of None
        # which would allow None as a default
        self.required: bool = (
            kwargs.pop("required", True) if "default" not in kwargs else False
        )
        # Popping the default value AFTERWARDS so that it doesn't matter if it was already None or not
        self.default = kwargs.pop("default", None)
```

If you think this would fix the issue, I'd be more than happy to make the PR for it myself if necessary.


Discussion:

Resolving patch:
diff --git a/discord/commands/options.py b/discord/commands/options.py
--- a/discord/commands/options.py
+++ b/discord/commands/options.py
@@ -85,10 +85,10 @@ def __init__(self, input_type: Any, /, description: str = None, **kwargs) -> Non
                         self.channel_types.append(channel_type)
                 input_type = _type
         self.input_type = input_type
-        self.default = kwargs.pop("default", None)
         self.required: bool = (
-            kwargs.pop("required", True) if self.default is None else False
+            kwargs.pop("required", True) if "default" not in kwargs else False
         )
+        self.default = kwargs.pop("default", None)
         self.choices: List[OptionChoice] = [
             o if isinstance(o, OptionChoice) else OptionChoice(o)
             for o in kwargs.pop("choices", list())
diff --git a/discord/ext/pages/pagination.py b/discord/ext/pages/pagination.py
--- a/discord/ext/pages/pagination.py
+++ b/discord/ext/pages/pagination.py
@@ -80,7 +80,7 @@ def __init__(
         )
         self.button_type = button_type
         self.label = label if label or emoji else button_type.capitalize()
-        self.emoji = emoji
+        self.emoji: Union[str, discord.Emoji, discord.PartialEmoji] = emoji
         self.style = style
         self.disabled = disabled
         self.loop_label = self.label if not loop_label else loop_label
@@ -170,8 +170,10 @@ def __init__(
     ):
         self.label = label
         self.description = description
-        self.emoji = emoji
-        self.pages = pages
+        self.emoji: Union[str, discord.Emoji, discord.PartialEmoji] = emoji
+        self.pages: Union[
+            List[str], List[Union[List[discord.Embed], discord.Embed]]
+        ] = pages
         self.show_disabled = show_disabled
         self.show_indicator = show_indicator
         self.author_check = author_check
@@ -179,9 +181,9 @@ def __init__(
         self.use_default_buttons = use_default_buttons
         self.default_button_row = default_button_row
         self.loop_pages = loop_pages
-        self.custom_view = custom_view
-        self.timeout = timeout
-        self.custom_buttons = custom_buttons
+        self.custom_view: discord.ui.View = custom_view
+        self.timeout: float = timeout
+        self.custom_buttons: List = custom_buttons
 
 
 class Paginator(discord.ui.View):
@@ -252,8 +254,10 @@ def __init__(
         custom_buttons: Optional[List[PaginatorButton]] = None,
     ) -> None:
         super().__init__(timeout=timeout)
-        self.timeout = timeout
-        self.pages = pages
+        self.timeout: float = timeout
+        self.pages: Union[
+            List[PageGroup], List[str], List[Union[List[discord.Embed], discord.Embed]]
+        ] = pages
         self.current_page = 0
         self.menu: Optional[PaginatorMenu] = None
         self.show_menu = show_menu
@@ -261,18 +265,20 @@ def __init__(
 
         if all(isinstance(pg, PageGroup) for pg in pages):
             self.page_groups = self.pages if show_menu else None
-            self.pages = self.page_groups[0].pages
+            self.pages: Union[
+                List[str], List[Union[List[discord.Embed], discord.Embed]]
+            ] = self.page_groups[0].pages
 
         self.page_count = len(self.pages) - 1
         self.buttons = {}
-        self.custom_buttons = custom_buttons
+        self.custom_buttons: List = custom_buttons
         self.show_disabled = show_disabled
         self.show_indicator = show_indicator
         self.disable_on_timeout = disable_on_timeout
         self.use_default_buttons = use_default_buttons
         self.default_button_row = default_button_row
         self.loop_pages = loop_pages
-        self.custom_view = custom_view
+        self.custom_view: discord.ui.View = custom_view
         self.message: Union[discord.Message, discord.WebhookMessage, None] = None
 
         if self.custom_buttons and not self.use_default_buttons:
@@ -333,7 +339,9 @@ async def update(
         """
 
         # Update pages and reset current_page to 0 (default)
-        self.pages = pages if pages is not None else self.pages
+        self.pages: Union[
+            List[PageGroup], List[str], List[Union[List[discord.Embed], discord.Embed]]
+        ] = (pages if pages is not None else self.pages)
         self.page_count = len(self.pages) - 1
         self.current_page = 0
         # Apply config changes, if specified
@@ -360,8 +368,8 @@ async def update(
             else self.default_button_row
         )
         self.loop_pages = loop_pages if loop_pages is not None else self.loop_pages
-        self.custom_view = None if custom_view is None else custom_view
-        self.timeout = timeout if timeout is not None else self.timeout
+        self.custom_view: discord.ui.View = None if custom_view is None else custom_view
+        self.timeout: float = timeout if timeout is not None else self.timeout
         if custom_buttons and not self.use_default_buttons:
             self.buttons = {}
             for button in custom_buttons:
@@ -379,6 +387,61 @@ async def on_timeout(self) -> None:
                 item.disabled = True
             await self.message.edit(view=self)
 
+    async def disable(
+        self,
+        include_custom: bool = False,
+        page: Optional[Union[str, Union[List[discord.Embed], discord.Embed]]] = None,
+    ) -> None:
+        """Stops the paginator, disabling all of its components.
+
+        Parameters
+        ----------
+        include_custom: :class:`bool`
+            Whether to disable components added via custom views.
+        page: Optional[Union[:class:`str`, Union[List[:class:`discord.Embed`], :class:`discord.Embed`]]]
+            The page content to show after disabling the paginator.
+        """
+        page = self.get_page_content(page)
+        for item in self.children:
+            if item not in self.custom_view.children or include_custom:
+                item.disabled = True
+        if page:
+            await self.message.edit(
+                content=page if isinstance(page, str) else None,
+                embeds=[] if isinstance(page, str) else page,
+                view=self,
+            )
+        else:
+            await self.message.edit(view=self)
+
+    async def cancel(
+        self,
+        include_custom: bool = False,
+        page: Optional[Union[str, Union[List[discord.Embed], discord.Embed]]] = None,
+    ) -> None:
+        """Cancels the paginator, removing all of its components from the message.
+
+        Parameters
+        ----------
+        include_custom: :class:`bool`
+            Whether to remove components added via custom views.
+        page: Optional[Union[:class:`str`, Union[List[:class:`discord.Embed`], :class:`discord.Embed`]]]
+            The page content to show after canceling the paginator.
+        """
+        items = self.children.copy()
+        page = self.get_page_content(page)
+        for item in items:
+            if item not in self.custom_view.children or include_custom:
+                self.remove_item(item)
+        if page:
+            await self.message.edit(
+                content=page if isinstance(page, str) else None,
+                embeds=[] if isinstance(page, str) else page,
+                view=self,
+            )
+        else:
+            await self.message.edit(view=self)
+
     async def goto_page(self, page_number=0) -> discord.Message:
         """Updates the paginator message to show the specified page number.
 
@@ -559,7 +622,6 @@ def update_buttons(self) -> Dict:
         if self.custom_view:
             for item in self.custom_view.children:
                 self.add_item(item)
-
         return self.buttons
 
     @staticmethod
diff --git a/examples/views/paginator.py b/examples/views/paginator.py
--- a/examples/views/paginator.py
+++ b/examples/views/paginator.py
@@ -182,6 +182,26 @@ async def pagetest_custom_view(self, ctx: discord.ApplicationContext):
         paginator = pages.Paginator(pages=self.get_pages(), custom_view=view)
         await paginator.respond(ctx.interaction, ephemeral=False)
 
+    @pagetest.command(name="disable")
+    async def pagetest_disable(self, ctx: discord.ApplicationContext):
+        """Demonstrates disabling the paginator buttons and showing a custom page when disabled."""
+        paginator = pages.Paginator(pages=self.get_pages())
+        await paginator.respond(ctx.interaction, ephemeral=False)
+        await ctx.respond("Disabling paginator in 5 seconds...")
+        await asyncio.sleep(5)
+        disable_page = discord.Embed(title="Paginator Disabled!", description="This page is only shown when the paginator is disabled.")
+        await paginator.disable(page=disable_page)
+
+    @pagetest.command(name="cancel")
+    async def pagetest_cancel(self, ctx: discord.ApplicationContext):
+        """Demonstrates canceling (stopping) the paginator and showing a custom page when cancelled."""
+        paginator = pages.Paginator(pages=self.get_pages())
+        await paginator.respond(ctx.interaction, ephemeral=False)
+        await ctx.respond("Canceling paginator in 5 seconds...")
+        await asyncio.sleep(5)
+        cancel_page = discord.Embed(title="Paginator Cancelled!", description="This page is only shown when the paginator is cancelled.")
+        await paginator.cancel(page=cancel_page)
+
     @pagetest.command(name="groups")
     async def pagetest_groups(self, ctx: discord.ApplicationContext):
         """Demonstrates using page groups to switch between different sets of pages."""


Test patch: