Skip to content

Commit

Permalink
Better parser API (#289)
Browse files Browse the repository at this point in the history
  • Loading branch information
norinorin committed Dec 5, 2022
1 parent aad80ed commit 972237b
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 32 deletions.
2 changes: 1 addition & 1 deletion lightbulb/commands/prefix.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ async def invoke(self, context: context_.base.Context, **kwargs: t.Any) -> None:
await self.evaluate_checks(context)
await self.evaluate_cooldowns(context)
assert isinstance(context, context_.prefix.PrefixContext)
await context._parser.inject_args_to_context()
context._options = await context._parser.parse()
await self(context, **kwargs)
except Exception:
raise
Expand Down
91 changes: 60 additions & 31 deletions lightbulb/utils/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,40 +79,66 @@


class BaseParser(abc.ABC):
__slots__ = ()
__slots__ = ("ctx", "_buffer", "len", "options")

len: int
ctx: context_.base.Context
_buffer: str
options: t.List[OptionLike]

@abc.abstractmethod
def __init__(self, context: context_.prefix.PrefixContext, args: t.Optional[str]) -> None:
...
def __init__(
self,
context: context_.base.Context,
buffer: t.Optional[str] = None,
options: t.Optional[t.List[OptionLike]] = None,
) -> None:
self.ctx = context

if buffer is None:
if not isinstance(self.ctx, context_.prefix.PrefixContext):
raise RuntimeError("Please provide the buffer to parse") # todo: proper error

message = self.ctx.event.message
assert message.content is not None
buffer = message.content[len(self.ctx.prefix) + len(self.ctx.invoked_with) :]

self.buffer = buffer

if options is not None:
self.options = options
else:
self.options = list(self.ctx.command.options.values()) if self.ctx.command else []

@property
def buffer(self) -> str:
return self._buffer

@buffer.setter
def buffer(self, val: str) -> None:
self._buffer = val
self.len = len(val)

@abc.abstractmethod
async def inject_args_to_context(self) -> None:
async def parse(self) -> t.Dict[str, t.Any]:
...


class Parser(BaseParser):
__slots__ = ("ctx", "_idx", "buffer", "n", "prev", "options")
__slots__ = ("_idx", "prev")

def __init__(self, context: context_.prefix.PrefixContext, buffer: t.Optional[str]):
self.ctx = context
def __init__(
self,
context: context_.base.Context,
buffer: t.Optional[str] = None,
options: t.Optional[t.List[OptionLike]] = None,
) -> None:
self._idx = 0

if buffer is None:
message = self.ctx.event.message
assert message.content is not None
buffer = message.content
self._idx += len(self.ctx.prefix) + len(self.ctx.invoked_with)

self.buffer = buffer
self.n = len(buffer)
self.prev = 0
self.options = list(context.command.options.values()) if context.command else []
super().__init__(context, buffer, options)

@property
def is_eof(self) -> bool:
return self.idx >= self.n
return self.idx >= self.len

@property
def idx(self) -> int:
Expand Down Expand Up @@ -175,7 +201,7 @@ def get_quoted_word(self) -> str:

def read_rest(self) -> str:
self.skip_ws()
self.idx = self.n
self.idx = self.len
return self.buffer[self.prev :]

def get_option(self) -> t.Optional[commands.base.OptionLike]:
Expand All @@ -184,18 +210,19 @@ def get_option(self) -> t.Optional[commands.base.OptionLike]:

return None

async def inject_args_to_context(self) -> None:
attachments = list(self.ctx.attachments)
async def parse(self) -> t.Dict[str, t.Any]:
ret = {}
attachments = list(self.ctx.attachments) if isinstance(self.ctx, context_.prefix.PrefixContext) else []
while option := self.get_option():
if option.arg_type in (hikari.OptionType.ATTACHMENT, hikari.Attachment):
if not attachments:
if not option.required:
self.ctx._options[option.name] = option.default
ret[option.name] = option.default
continue
raise errors.MissingRequiredAttachmentArgument(
"Command invocation expects an attachment but none were found.", missing=option
)
self.ctx._options[option.name] = attachments.pop(0)
ret[option.name] = attachments.pop(0)
continue

_LOGGER.debug("Getting arg for %s with type %s", option.name, option.arg_type)
Expand All @@ -212,14 +239,16 @@ async def inject_args_to_context(self) -> None:
missing=[option, *(o for o in self.options if o.required)],
)

self.ctx._options[option.name] = option.default
ret[option.name] = option.default
continue

_LOGGER.debug("Got raw arg %s", raw_arg)
convert = self._greedy_convert if option.modifier is OptionModifier.GREEDY else self._try_convert
await convert(raw_arg, option)
await convert(raw_arg, option, ret)

return ret

async def _try_convert(self, raw: str, option: commands.base.OptionLike) -> None:
async def _try_convert(self, raw: str, option: commands.base.OptionLike, out: t.Dict[str, t.Any]) -> None:
try:
arg = await self._convert(raw, option.arg_type)
except Exception as e:
Expand All @@ -229,15 +258,15 @@ async def _try_convert(self, raw: str, option: commands.base.OptionLike) -> None
f"Conversion failed for option {option.name!r}", opt=option, raw=raw
) from e

self.ctx._options[option.name] = option.default
out[option.name] = option.default
_LOGGER.debug("Option has a default value, shifting to the next parameter")
self.undo()
else:
_LOGGER.debug("Successfully converted %s to %s", raw, arg)
self.ctx._options[option.name] = arg
out[option.name] = arg

async def _greedy_convert(self, raw: str, option: commands.base.OptionLike) -> None:
self.ctx._options[option.name] = args = []
async def _greedy_convert(self, raw: str, option: commands.base.OptionLike, out: t.Dict[str, t.Any]) -> None:
out[option.name] = args = []
_LOGGER.debug("Attempting to greedy convert %s to %s", raw, option.arg_type)
while raw:
try:
Expand Down

0 comments on commit 972237b

Please sign in to comment.