Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add parsing and command-related functionality to command_handler module #18

Merged
merged 13 commits into from
May 20, 2022
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