Skip to content

Commit

Permalink
Add parsing and command-related functionality to command_handler mo…
Browse files Browse the repository at this point in the history
…dule (#18)

* add parsing and converter submodule to command_handler

* improvements to command_handler

* fix CI fails

* rename Range converter to RangeObject

* improve error output of custom parser

* improve command decorators in command_handler
  • Loading branch information
Mega-JC committed May 20, 2022
1 parent b441c62 commit d8705ed
Show file tree
Hide file tree
Showing 10 changed files with 1,388 additions and 12 deletions.
14 changes: 12 additions & 2 deletions snakecore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,12 @@ def init_sync(

success_failure_list = [0, 0]

for module in (events, jobs, utils): # might be extended in the future
for module in (
command_handler,
events,
jobs,
utils,
): # might be extended in the future
if not module.is_init(): # prevent multiple init calls, which can be allowed by
# modules on an individual level

Expand Down Expand Up @@ -190,7 +195,12 @@ def quit_sync():
modules that are still initialized and can be called multiple times.
"""

for module in (events, utils): # might be extended in the future
for module in (
command_handler,
events,
jobs,
utils,
): # might be extended in the future
if module.is_init():
module.quit()

Expand Down
41 changes: 41 additions & 0 deletions snakecore/command_handler/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""This file is a part of the source code for snakecore.
This project has been licensed under the MIT license.
Copyright (c) 2022-present PygameCommunityDiscord
This file defines powerful utilities for developing bot commands.
"""

from typing import Optional

import discord

from snakecore import config
from . import converters, decorators, parser


def init(global_client: Optional[discord.Client] = None):
"""Initialize this module.
Args:
global_client (Optional[discord.Client], optional):
The global `discord.Client` object to set for all modules to use.
Defaults to None.
"""
if global_client is not None and not config.conf.is_set("global_client"):
config.conf.global_client = global_client

config.conf.init_mods[config.ModuleName.COMMAND_HANDLER] = True


def quit():
"""Quit this module."""
config.conf.init_mods[config.ModuleName.COMMAND_HANDLER] = False


def is_init():
"""Whether this module has been sucessfully initialized.
Returns:
bool: True/False
"""
return config.conf.init_mods.get(config.ModuleName.COMMAND_HANDLER, False)
111 changes: 111 additions & 0 deletions snakecore/command_handler/converters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""This file is a part of the source code for snakecore.
This project has been licensed under the MIT license.
Copyright (c) 2022-present PygameCommunityDiscord
This file defines some converters for command argument parsing.
"""

import datetime
from typing import TYPE_CHECKING

from discord.ext import commands

import snakecore
from .parser import CodeBlock, String


class DateTime(commands.Converter):
"""A converter that parses UNIX/ISO timestamps to `datetime.datetime` objects.
Syntax:
- `<t:{6969...}[:t|T|d|D|f|F|R]> -> datetime.datetime(seconds=6969...)`
- `YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]] -> datetime.datetime`
"""

async def convert(self, ctx: commands.Context, argument: str) -> datetime.datetime:
arg = argument.strip()

if snakecore.utils.is_markdown_timestamp(arg):
arg = snakecore.utils.extract_markdown_timestamp(arg)
try:
return datetime.datetime.fromtimestamp(arg)
except ValueError as v:
raise commands.BadArgument(
f"failed to construct datetime: {v.__class__.__name__}:{v!s}"
) from v
else:
arg = arg.removesuffix("Z")

try:
return datetime.datetime.fromisoformat(arg)
except ValueError as v:
raise commands.BadArgument(
f"failed to construct datetime: {v.__class__.__name__}:{v!s}"
) from v


class RangeObject(commands.Converter):
"""A converter that parses integer range values to `range` objects.
Syntax:
- `[start:stop] -> range(start, stop)`
- `[start:stop:step] -> range(start, stop, step)`
"""

async def convert(self, ctx: commands.Context, argument: str) -> range:
if not argument.startswith("[") or not argument.endswith("]"):
raise commands.BadArgument("ranges begin and end with square brackets")

try:
splits = [int(i.strip()) for i in argument[6:-1].split(":")]

if splits and len(splits) <= 3:
return range(*splits)
except (ValueError, TypeError) as v:
raise commands.BadArgument(
f"failed to construct range: {argument!r}"
) from v

raise commands.BadArgument(f"invalid range string: {argument!r}")


class QuotedString(commands.Converter):
"""A simple converter that enforces a quoted string as an argument.
It removes leading and ending single or doble quotes. If those quotes
are not found, exceptions are raised.
Syntax:
- `"abc" -> str("abc")`
- `'abc' -> str('abc')`
"""

async def convert(self, ctx: commands.Context, argument: str) -> range:
passed = False
if argument.startswith('"'):
if argument.endswith('"'):
passed = True
else:
raise commands.BadArgument(
"argument string quote '\"' was not closed with \""
)

elif argument.startswith("'"):
if argument.endswith("'"):
passed = True
else:
raise commands.BadArgument(
"argument string quote \"'\" was not closed with '"
)

if not passed:
raise commands.BadArgument(
"argument string is not properly quoted with '' or \"\""
)

return argument[1:-1]


if TYPE_CHECKING:
DateTime = datetime.datetime
RangeObject = range
QuotedString = str

0 comments on commit d8705ed

Please sign in to comment.