Skip to content

Commit

Permalink
Complete argument parsing and converting rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
tandemdude committed Jun 9, 2021
1 parent c193ab6 commit c606133
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 35 deletions.
12 changes: 7 additions & 5 deletions lightbulb/command_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@

from lightbulb import commands
from lightbulb import context as context_
from lightbulb.converters import _DefaultingConverter
from lightbulb import errors
from lightbulb import events
from lightbulb import help as help_
from lightbulb import plugins
from lightbulb.converters import _DefaultingConverter
from lightbulb.utils import maybe_await

_LOGGER = logging.getLogger("lightbulb")
Expand Down Expand Up @@ -755,11 +755,12 @@ async def resolve_args_for_command(
) -> typing.Tuple[typing.List[str], typing.Dict[str, str]]:
"""
Resolve the appropriate command arguments from an unparsed string
containing the raw command arguments.
containing the raw command arguments and attempt to convert them into
the appropriate types given the type hint.
This method can be overridden if you wish to customise how arguments
are parsed for all the commands. If you override this then it is important that
it at least returns a tuple containing an empty list if no positional arguments
are parsed and converted for all the commands. If you override this then it is important
that it at least returns a tuple containing an empty list if no positional arguments
were resolved, and an empty dict if no keyword arguments were resolved.
If you override this method then you may find the :obj:`~.stringview.StringView`
Expand All @@ -781,6 +782,7 @@ class useful for extracting the arguments from the raw string and the property
arguments were supplied by the user.
:obj:`~.errors.NotEnoughArguments`: Not enough arguments were provided by the user to fill
all required argument fields.
:obj:`~.errors.ConverterFailure`: Argument value conversion failed.
"""
converters = command.arg_details.converters[:]
arg_names = command.arg_details.arguments[:]
Expand All @@ -797,7 +799,7 @@ class useful for extracting the arguments from the raw string and the property
try:
conv_out, arg_string = await conv.convert(context, arg_string)
except (ValueError, TypeError, errors.ConverterFailure):
raise errors.ConverterFailure("")
raise errors.ConverterFailure(f"Converting failed for argument: {arg_name}")

if isinstance(conv_out, dict):
kwargs.update(conv_out)
Expand Down
76 changes: 49 additions & 27 deletions lightbulb/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,31 @@
from __future__ import annotations

__all__: typing.Final[typing.List[str]] = [
"ArgInfo",
"SignatureInspector",
"Command",
"Group",
"command",
"group",
]

import dataclasses
import functools
import inspect
import logging
import typing
from itertools import zip_longest

import hikari
from multidict import CIMultiDict

from lightbulb import context as context_
from lightbulb import converters
from lightbulb.converters import _Converter, _UnionConverter, _DefaultingConverter, _GreedyConverter, _ConsumeRestConverter
from lightbulb import cooldowns
from lightbulb import errors
from lightbulb import events
from lightbulb.converters import _ConsumeRestConverter
from lightbulb.converters import _Converter
from lightbulb.converters import _DefaultingConverter
from lightbulb.converters import _GreedyConverter
from lightbulb.converters import _UnionConverter

if typing.TYPE_CHECKING:
from lightbulb import plugins
Expand All @@ -50,6 +51,21 @@

_CommandT = typing.TypeVar("_CommandT", bound="Command")

_CONVERTER_CLASS_MAPPING = {
hikari.User: converters.user_converter,
hikari.Member: converters.member_converter,
hikari.TextChannel: converters.text_channel_converter,
hikari.GuildVoiceChannel: converters.guild_voice_channel_converter,
hikari.GuildCategory: converters.category_converter,
hikari.Role: converters.role_converter,
hikari.Emoji: converters.emoji_converter,
hikari.GuildPreview: converters.guild_converter,
hikari.Message: converters.message_converter,
hikari.Invite: converters.invite_converter,
hikari.Colour: converters.colour_converter,
hikari.Color: converters.colour_converter,
}


class _BoundCommandMarker:
def __init__(self, delegates_to: _CommandT) -> None:
Expand Down Expand Up @@ -102,24 +118,6 @@ async def invoke(self, context: context_.Context, *args: str, **kwargs: str) ->
return typing.cast(_CommandT, prototype)


@dataclasses.dataclass
class ArgInfo:
"""
Dataclass representing information for a single command argument.
"""

ignore: bool
""":obj:`True` if the argument is ``self`` or ``context`` else :obj:`False`."""
argtype: int
"""The type of the argument. See :attr:`inspect.Parameter.kind` for possible types."""
annotation: typing.Any
"""The type annotation of the argument."""
required: bool
"""Whether or not the argument is required during invocation."""
default: typing.Any
"""Default value for an argument."""


class SignatureInspector:
"""
Contains information about the arguments that a command takes when
Expand All @@ -132,28 +130,52 @@ class SignatureInspector:
def __init__(self, command: Command) -> None:
self.command = command
self.has_self = isinstance(command, _BoundCommandMarker)
self.kwarg_name = None
signature = inspect.signature(command._callback)
self.converters = self.parse_signature(signature)
self.arguments = [p.name for p in signature.parameters.values()]
self.arguments.pop(0)
if self.has_self:
self.arguments.pop(0)

def get_converter(self, annotation) -> typing.Union[_Converter, _UnionConverter, _DefaultingConverter, _GreedyConverter]:
def get_converter(
self, annotation
) -> typing.Union[_Converter, _UnionConverter, _DefaultingConverter, _GreedyConverter]:
"""
Resolve the converter order for a given type annotation recursively.
Args:
annotation: The parameter's type annotation.
Returns:
The top level converter for the parameter
"""
annotation = _CONVERTER_CLASS_MAPPING.get(annotation, annotation)

origin = typing.get_origin(annotation)
args = typing.get_args(annotation)

if origin is typing.Union:
if args[1] is type(None):
return _DefaultingConverter(self.get_converter(args[0]), None)
args = [self.get_converter(conv) for conv in args]
return _UnionConverter(*args)
elif origin is converters.Greedy:

if origin is converters.Greedy:
return _GreedyConverter(self.get_converter(args[0]))
else:
return _Converter(annotation if annotation is not inspect.Parameter.empty else str)

return _Converter(annotation if annotation is not inspect.Parameter.empty else str)

def parse_signature(self, signature: inspect.Signature):
"""
Parse the command's callback signature into a list of the converters used
for each command argument.
Args:
signature (:obj:`inspect.Signature`): The signature of the command callback.
Returns:
List of converters for command arguments in the correct order.
"""
arg_converters = []

params = list(signature.parameters.values())
Expand Down
47 changes: 44 additions & 3 deletions lightbulb/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,40 @@ async def to_int(ctx, number: int):
will be an instance of the :obj:`~lightbulb.converters.WrappedArg` class. The arg value and command invocation
context can be accessed through this instance from the attributes ``data`` and ``context`` respectively.
Hikari classes are also available as type hints in place of the lightbulb converters and will be internally
converted into the necessary converter for the command to behave as expected. A list of all available classes
along with the converters they 'replace' can be seen below:
- :obj:`hikari.User` (:obj:`~.user_converter`)
- :obj:`hikari.Member` (:obj:`~.member_converter`)
- :obj:`hikari.TextChannel` (:obj:`~.text_channel_converter`)
- :obj:`hikari.GuildVoiceChannel` (:obj:`~.guild_voice_channel_converter`)
- :obj:`hikari.GuildCategory` (:obj:`~.category_converter`)
- :obj:`hikari.Role` (:obj:`~.role_converter`)
- :obj:`hikari.Emoji` (:obj:`~.emoji_converter`)
- :obj:`hikari.GuildPreview` (:obj:`~.guild_converter`)
- :obj:`hikari.Message` (:obj:`~.message_converter`)
- :obj:`hikari.Invite` (:obj:`~.invite_converter`)
- :obj:`hikari.Colour` (:obj:`~.colour_converter`)
- :obj:`hikari.Color` (:obj:`~.color_converter`)
.. warning:: For the supplied converters, some functionality will not be possible depending on the intents and/or
cache settings of your bot application and object. If the bot does not have a cache then the converters can
only work for arguments of ID or mention and **not** any form of name.
.. warning:: If you use ``from __future__ import annotations`` then you **will not** be able to use converters
in your commands. Instead of converting the arguments, the raw, unconverted arguments will be passed back
to the command.
.. error:: In the current state, var positional arguments **are not** supported. To achieve the same functionality
you should use an argument with the :obj:`~.Greedy` converter as seen below
::
@bot.command()
async def foo(ctx, arg: lightbulb.Greedy[str]):
....
"""
from __future__ import annotations

Expand Down Expand Up @@ -72,8 +99,8 @@ async def to_int(ctx, number: int):

from lightbulb import context as context_
from lightbulb import errors
from lightbulb import utils
from lightbulb import stringview
from lightbulb import utils

T = typing.TypeVar("T")

Expand Down Expand Up @@ -421,12 +448,26 @@ async def color_converter(arg: WrappedArg) -> hikari.Color:


class Greedy(typing.Generic[T]):
"""
A special converter that greedily consumes arguments until it either runs out of arguments, or a parser error
is encountered. Due to this behaviour, most input errors will be silently ignored.
Example:
.. code-block:: python
@bot.command()
async def foo(ctx, foo: Greedy[int]):
# if called with <p>foo 1 2 3 4 5
# then the arg foo would contain [1, 2, 3, 4, 5]
...
"""

pass


_converter_T = typing.Union[
typing.Callable[[WrappedArg], T],
typing.Callable[[WrappedArg], typing.Coroutine[None, None, T]]
typing.Callable[[WrappedArg], T], typing.Callable[[WrappedArg], typing.Coroutine[None, None, T]]
]


Expand Down

0 comments on commit c606133

Please sign in to comment.