mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-22 19:14:20 +00:00
✨ Create Command and PartialCommand
This commit is contained in:
parent
6faf278eb3
commit
0c0ce7b0db
4 changed files with 181 additions and 10 deletions
|
@ -10,6 +10,12 @@
|
|||
.. automodule:: royalnet.engineer.bullet
|
||||
|
||||
|
||||
``command``
|
||||
-----------
|
||||
|
||||
.. automodule:: royalnet.engineer.command
|
||||
|
||||
|
||||
``conversation``
|
||||
-----------
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ All names are inspired by the `Engineer Class of Team Fortress 2 <https://wiki.t
|
|||
"""
|
||||
|
||||
from .bullet import *
|
||||
from .command import *
|
||||
from .conversation import *
|
||||
from .discard import *
|
||||
from .dispenser import *
|
||||
|
|
142
royalnet/engineer/command.py
Normal file
142
royalnet/engineer/command.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
# Module docstring
|
||||
"""
|
||||
|
||||
"""
|
||||
|
||||
# Special imports
|
||||
from __future__ import annotations
|
||||
import royalnet.royaltyping as t
|
||||
|
||||
# External imports
|
||||
import logging
|
||||
import re
|
||||
|
||||
# Internal imports
|
||||
from . import conversation as c
|
||||
from . import sentry as s
|
||||
from . import bullet as b
|
||||
|
||||
# Special global objects
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Code
|
||||
class Command(c.Conversation):
|
||||
"""
|
||||
A Command is a :class:`~.c.Conversation` which is started by a :class:`~.b.Message` having a text matching
|
||||
the :attr:`.pattern`; the named capture groups of the pattern are then passed as keyword arguments to :attr:`.f`.
|
||||
"""
|
||||
|
||||
def __init__(self, f: c.ConversationProtocol, *, name: str, pattern: re.Pattern):
|
||||
super().__init__(f)
|
||||
|
||||
self.name: str = name
|
||||
"""
|
||||
The name of the command, as a :class:`str`.
|
||||
"""
|
||||
|
||||
self.pattern: re.Pattern = pattern
|
||||
"""
|
||||
The pattern that should be matched by the command.
|
||||
"""
|
||||
|
||||
async def run(self, *, _sentry: s.Sentry, **base_kwargs) -> t.Optional[c.ConversationProtocol]:
|
||||
log.debug(f"Awaiting a bullet...")
|
||||
bullet: b.Bullet = await _sentry
|
||||
|
||||
log.debug(f"Received: {bullet!r}")
|
||||
|
||||
log.debug(f"Ensuring a message was received: {bullet!r}")
|
||||
if not isinstance(bullet, b.Message):
|
||||
log.debug(f"Returning: {bullet!r} is not a message")
|
||||
return
|
||||
|
||||
log.debug(f"Getting message text of: {bullet!r}")
|
||||
if not (text := await bullet.text()):
|
||||
log.debug(f"Returning: {bullet!r} has no text")
|
||||
return
|
||||
|
||||
log.debug(f"Searching for pattern: {text!r}")
|
||||
if not (match := self.pattern.search(text)):
|
||||
log.debug(f"Returning: Couldn't find pattern in {text!r}")
|
||||
return
|
||||
|
||||
log.debug(f"Match successful, getting capture groups of: {match!r}")
|
||||
message_kwargs: t.Dict[str, str] = match.groupdict()
|
||||
|
||||
log.debug(f"Passing args to function: {message_kwargs!r}")
|
||||
return await super().run(_sentry=_sentry, **base_kwargs, **message_kwargs)
|
||||
|
||||
def help(self) -> t.Optional[str]:
|
||||
"""
|
||||
Get help about this command. This defaults to returning the docstring of :field:`.f` .
|
||||
|
||||
:return: The help :class:`str` for this command, or :data:`None` if the command has no docstring.
|
||||
"""
|
||||
return self.f.__doc__
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__qualname__}: {self.name}>"
|
||||
|
||||
|
||||
class PartialCommand:
|
||||
"""
|
||||
A PartialCommand is a :class:`.Command` having an unknown :attr:`~.Command.pattern` at the moment of creation.
|
||||
|
||||
The pattern can specified later using :meth:`.complete`.
|
||||
"""
|
||||
def __init__(self, f: c.ConversationProtocol, name: str, syntax: str):
|
||||
self.f: c.ConversationProtocol = f
|
||||
"""
|
||||
The function to pass to :attr:`.c.Conversation.f`.
|
||||
"""
|
||||
|
||||
if not self.name.isalnum():
|
||||
raise ValueError("Name should be alphanumeric")
|
||||
if not self.name.islower():
|
||||
raise ValueError("Name should be lowercase")
|
||||
|
||||
self.name: str = name
|
||||
"""
|
||||
The name of the command to pass to :attr`.Command.name`. Should be lowercase and containing only alphanumeric
|
||||
characters.
|
||||
"""
|
||||
|
||||
self.syntax: str = syntax
|
||||
"""
|
||||
Part of the pattern from where the arguments should be captured.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def new(cls, *args, **kwargs):
|
||||
"""
|
||||
A decorator that instantiates a new :class:`Conversation` object using the decorated function.
|
||||
|
||||
:return: The created :class:`Conversation` object.
|
||||
It can still be called in the same way as the previous function!
|
||||
"""
|
||||
def decorator(f: c.ConversationProtocol):
|
||||
partial_command = cls(f=f, *args, **kwargs)
|
||||
log.debug(f"Created: {partial_command!r}")
|
||||
return decorator
|
||||
|
||||
def complete(self, pattern: str) -> Command:
|
||||
"""
|
||||
Complete the PartialCommand with a pattern, creating a :class:`Command` object.
|
||||
|
||||
:param pattern: The pattern to add to the PartialCommand. It is first :meth:`str.format`\\ ted with the keyword
|
||||
arguments ``name=self.name, syntax=self.syntax`` and later :func:`re.compile`\\ d.
|
||||
:return: The complete :class:`Command`.
|
||||
"""
|
||||
pattern: re.Pattern = re.compile(pattern.format(name=self.name, syntax=self.syntax))
|
||||
return Command(f=self.f, name=self.name, pattern=pattern)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__qualname__}: {self.name}>"
|
||||
|
||||
|
||||
# Objects exported by this module
|
||||
__all__ = (
|
||||
"Command",
|
||||
"PartialCommand",
|
||||
)
|
|
@ -1,24 +1,30 @@
|
|||
# Module docstring
|
||||
"""
|
||||
Conversations are wrapper classes that can be applied to functions which await
|
||||
:class:`~royalnet.engineer.bullet.Bullet`\\ s from a :class:`~royalnet.engineer.sentry.Sentry`.
|
||||
"""
|
||||
|
||||
# Special imports
|
||||
from __future__ import annotations
|
||||
import royalnet.royaltyping as t
|
||||
|
||||
# External imports
|
||||
import logging
|
||||
|
||||
from . import sentry
|
||||
# Internal imports
|
||||
from . import sentry as s
|
||||
|
||||
|
||||
# Special global objects
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Code
|
||||
class ConversationProtocol(t.Protocol):
|
||||
"""
|
||||
Typing annotation for Conversation functions.
|
||||
"""
|
||||
def __call__(self, *, _sentry: sentry.Sentry, **kwargs) -> t.Awaitable[t.Optional[ConversationProtocol]]:
|
||||
def __call__(self, *, _sentry: s.Sentry, **kwargs) -> t.Awaitable[t.Optional[ConversationProtocol]]:
|
||||
...
|
||||
|
||||
|
||||
|
@ -27,11 +33,18 @@ class Conversation:
|
|||
The base class for Conversations. It does nothing on its own except providing better debug information.
|
||||
"""
|
||||
|
||||
def __init__(self, f: ConversationProtocol):
|
||||
def __init__(self, f: ConversationProtocol, *_, **__):
|
||||
self.f: ConversationProtocol = f
|
||||
"""
|
||||
The function that was wrapped by this conversation.
|
||||
|
||||
It can be called by calling this object as it was a function::
|
||||
|
||||
my_conv(_sentry=_sentry, _msg=msg)
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def new(cls):
|
||||
def new(cls, *args, **kwargs):
|
||||
"""
|
||||
A decorator that instantiates a new :class:`Conversation` object using the decorated function.
|
||||
|
||||
|
@ -39,16 +52,25 @@ class Conversation:
|
|||
It can still be called in the same way as the previous function!
|
||||
"""
|
||||
def decorator(f: ConversationProtocol):
|
||||
c = cls(f=f)
|
||||
log.debug(f"Created: {repr(c)}")
|
||||
c = cls(f=f, *args, **kwargs)
|
||||
log.debug(f"Created: {c!r}")
|
||||
return decorator
|
||||
|
||||
def __call__(self, *, _sentry: sentry.Sentry, **kwargs) -> t.Awaitable[t.Optional[ConversationProtocol]]:
|
||||
log.debug(f"Calling: {repr(self)}")
|
||||
return self.f(_sentry=_sentry, **kwargs)
|
||||
def __call__(self, *, _sentry: s.Sentry, **kwargs) -> t.Awaitable[t.Optional[ConversationProtocol]]:
|
||||
log.debug(f"Calling: {self!r}")
|
||||
return self.run(_sentry=_sentry, **kwargs)
|
||||
|
||||
async def run(self, *, _sentry: s.Sentry, **kwargs) -> t.Optional[ConversationProtocol]:
|
||||
"""
|
||||
The coroutine function that generates the coroutines returned by :meth:`__call__` .
|
||||
|
||||
It is a conversation itself.
|
||||
"""
|
||||
log.debug(f"Running: {self!r}")
|
||||
return await self.f(_sentry=_sentry, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Conversation #{id(self)}>"
|
||||
return f"<{self.__class__.__qualname__} #{id(self)}>"
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
Loading…
Reference in a new issue