Skip to content

Commit 316eb45

Browse files
committed
[commands] custom default arguments
Modeled slightly after Converters, allow specifying Converter-like class for context-based default parameters. e.g. ```py class Author(CustomDefault): async def default(self, ctx): return ctx.author async def my_command(ctx, user: discord.Member=Author): ... ``` Also adds a few common cases (Author, Channel, Guild) for current author, ...
1 parent a136def commit 316eb45

File tree

5 files changed

+178
-4
lines changed

5 files changed

+178
-4
lines changed

discord/ext/commands/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@
1818
from .converter import *
1919
from .cooldowns import *
2020
from .cog import *
21+
from .default import CustomDefault

discord/ext/commands/core.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from .errors import *
3636
from .cooldowns import Cooldown, BucketType, CooldownMapping
3737
from . import converter as converters
38+
from . import default as defaults
3839
from ._types import _BaseCommand
3940
from .cog import Cog
4041

@@ -410,12 +411,25 @@ async def do_conversion(self, ctx, converter, argument, param):
410411
def _get_converter(self, param):
411412
converter = param.annotation
412413
if converter is param.empty:
413-
if param.default is not param.empty:
414-
converter = str if param.default is None else type(param.default)
415-
else:
414+
if param.default is param.empty or param.default is None or (inspect.isclass(param.default) and issubclass(param.default, defaults.CustomDefault)):
416415
converter = str
416+
else:
417+
converter = type(param.default)
417418
return converter
418419

420+
async def _resolve_default(self, ctx, param):
421+
try:
422+
if inspect.isclass(param.default) and issubclass(param.default, defaults.CustomDefault):
423+
instance = param.default()
424+
return await instance.default(ctx=ctx, param=param)
425+
elif isinstance(param.default, defaults.CustomDefault):
426+
return await param.default.default(ctx=ctx, param=param)
427+
except CommandError as e:
428+
raise e
429+
except Exception as e:
430+
raise ConversionError(param.default, e) from e
431+
return param.default
432+
419433
async def transform(self, ctx, param):
420434
required = param.default is param.empty
421435
converter = self._get_converter(param)
@@ -443,7 +457,7 @@ async def transform(self, ctx, param):
443457
if self._is_typing_optional(param.annotation):
444458
return None
445459
raise MissingRequiredArgument(param)
446-
return param.default
460+
return await self._resolve_default(ctx, param)
447461

448462
previous = view.index
449463
if consume_rest_is_special:

discord/ext/commands/default.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
The MIT License (MIT)
5+
6+
Copyright (c) 2015-2019 Rapptz
7+
8+
Permission is hereby granted, free of charge, to any person obtaining a
9+
copy of this software and associated documentation files (the "Software"),
10+
to deal in the Software without restriction, including without limitation
11+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
12+
and/or sell copies of the Software, and to permit persons to whom the
13+
Software is furnished to do so, subject to the following conditions:
14+
15+
The above copyright notice and this permission notice shall be included in
16+
all copies or substantial portions of the Software.
17+
18+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24+
DEALINGS IN THE SOFTWARE.
25+
"""
26+
27+
from .errors import MissingRequiredArgument
28+
29+
__all__ = (
30+
'CustomDefault',
31+
'Author',
32+
'CurrentChannel',
33+
'CurrentGuild',
34+
'Call',
35+
)
36+
37+
class CustomDefault:
38+
"""The base class of custom defaults that require the :class:`.Context`.
39+
40+
Classes that derive from this should override the :meth:`~.CustomDefault.default`
41+
method to do its conversion logic. This method must be a coroutine.
42+
"""
43+
44+
async def default(self, ctx, param):
45+
"""|coro|
46+
47+
The method to override to do conversion logic.
48+
49+
If an error is found while converting, it is recommended to
50+
raise a :exc:`.CommandError` derived exception as it will
51+
properly propagate to the error handlers.
52+
53+
Parameters
54+
-----------
55+
ctx: :class:`.Context`
56+
The invocation context that the argument is being used in.
57+
"""
58+
raise NotImplementedError('Derived classes need to implement this.')
59+
60+
61+
class Author(CustomDefault):
62+
"""Default parameter which returns the author for this context."""
63+
64+
async def default(self, ctx, param):
65+
return ctx.author
66+
67+
class CurrentChannel(CustomDefault):
68+
"""Default parameter which returns the channel for this context."""
69+
70+
async def default(self, ctx, param):
71+
return ctx.channel
72+
73+
class CurrentGuild(CustomDefault):
74+
"""Default parameter which returns the guild for this context."""
75+
76+
async def default(self, ctx, param):
77+
if ctx.guild:
78+
return ctx.guild
79+
raise MissingRequiredArgument(param)
80+
81+
class Call(CustomDefault):
82+
"""Easy wrapper for lambdas/inline defaults."""
83+
84+
def __init__(self, callback):
85+
self._callback = callback
86+
87+
async def default(self, ctx, param):
88+
return self._callback(ctx, param)

docs/ext/commands/api.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,22 @@ Converters
225225

226226
For more information, check :ref:`ext_commands_special_converters`.
227227

228+
.. _ext_commands_api_custom_default:
229+
230+
Default Parameters
231+
-------------------
232+
233+
.. autoclass:: discord.ext.commands.CustomDefault
234+
:members:
235+
236+
.. autoclass:: discord.ext.commands.default.Author
237+
238+
.. autoclass:: discord.ext.commands.default.CurrentChannel
239+
240+
.. autoclass:: discord.ext.commands.default.CurrentGuild
241+
242+
.. autoclass:: discord.ext.commands.default.Call
243+
228244
.. _ext_commands_api_errors:
229245

230246
Exceptions

docs/ext/commands/commands.rst

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,61 @@ handlers that allow us to do just that. First we decorate an error handler funct
586586
The first parameter of the error handler is the :class:`.Context` while the second one is an exception that is derived from
587587
:exc:`~ext.commands.CommandError`. A list of errors is found in the :ref:`ext_commands_api_errors` page of the documentation.
588588

589+
590+
Custom Defaults
591+
---------------
592+
593+
Custom defaults allow us to specify :class:`.Context`-based defaults. Custom defaults are always classes which inherit from
594+
:class:`.CustomDefault`.
595+
596+
The library provides some simple default implementations in ``ext.commands.default`` - :class:`.default.Author`, :class:`.default.CurrentChannel`,
597+
and :class:`.default.CurrentGuild` returning the corresponding properties from the Context. These can be used along with Converters to
598+
simplify your individual commands. You can also use :class:`.default.Call` to quickly wrap existing functions.
599+
600+
A DefaultParam returning ``None`` is valid - if this should be an error, raise :class:`.MissingRequiredArgument`.
601+
602+
.. code-block:: python3
603+
:emphasize-lines: 14,17,32
604+
605+
class Image(Converter):
606+
"""Find images associated with the message."""
607+
608+
async def convert(self, ctx, argument):
609+
if argument.startswith("http://") or argument.startswith("https://"):
610+
return argument
611+
612+
member = await MemberConverter().convert(ctx, argument)
613+
if member:
614+
return str(member.avatar_url_as(format="png"))
615+
616+
raise errors.BadArgument(f"{argument} isn't a member or url.")
617+
618+
class LastImage(CustomDefault):
619+
"""Default param which finds the last image in chat."""
620+
621+
async def default(self, ctx, param):
622+
for attachment in message.attachments:
623+
if attachment.proxy_url:
624+
return attachment.proxy_url
625+
async for message in ctx.history(ctx, limit=100):
626+
for embed in message.embeds:
627+
if embed.thumbnail and embed.thumbnail.proxy_url:
628+
return embed.thumbnail.proxy_url
629+
for attachment in message.attachments:
630+
if attachment.proxy_url:
631+
return attachment.proxy_url
632+
633+
raise errors.MissingRequiredArgument(param)
634+
635+
@bot.command()
636+
async def echo_image(ctx, *, image: Image = LastImage):
637+
async with aiohttp.ClientSession() as sess:
638+
async with sess.get(image) as resp:
639+
resp.raise_for_status()
640+
my_bytes = io.BytesIO(await resp.content.read())
641+
await ctx.send(file=discord.File(filename="your_image", fp=my_bytes))
642+
643+
589644
Checks
590645
-------
591646

0 commit comments

Comments
 (0)