1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-26 21:14:19 +00:00

Create Command and PartialCommand

This commit is contained in:
Steffo 2020-12-30 17:05:25 +01:00
parent 6faf278eb3
commit 0c0ce7b0db
4 changed files with 181 additions and 10 deletions

View file

@ -10,6 +10,12 @@
.. automodule:: royalnet.engineer.bullet .. automodule:: royalnet.engineer.bullet
``command``
-----------
.. automodule:: royalnet.engineer.command
``conversation`` ``conversation``
----------- -----------

View file

@ -5,6 +5,7 @@ All names are inspired by the `Engineer Class of Team Fortress 2 <https://wiki.t
""" """
from .bullet import * from .bullet import *
from .command import *
from .conversation import * from .conversation import *
from .discard import * from .discard import *
from .dispenser import * from .dispenser import *

View 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",
)

View file

@ -1,24 +1,30 @@
# Module docstring
""" """
Conversations are wrapper classes that can be applied to functions which await 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`. :class:`~royalnet.engineer.bullet.Bullet`\\ s from a :class:`~royalnet.engineer.sentry.Sentry`.
""" """
# Special imports
from __future__ import annotations from __future__ import annotations
import royalnet.royaltyping as t import royalnet.royaltyping as t
# External imports
import logging import logging
from . import sentry # Internal imports
from . import sentry as s
# Special global objects
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Code
class ConversationProtocol(t.Protocol): class ConversationProtocol(t.Protocol):
""" """
Typing annotation for Conversation functions. 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. 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 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 @classmethod
def new(cls): def new(cls, *args, **kwargs):
""" """
A decorator that instantiates a new :class:`Conversation` object using the decorated function. 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! It can still be called in the same way as the previous function!
""" """
def decorator(f: ConversationProtocol): def decorator(f: ConversationProtocol):
c = cls(f=f) c = cls(f=f, *args, **kwargs)
log.debug(f"Created: {repr(c)}") log.debug(f"Created: {c!r}")
return decorator return decorator
def __call__(self, *, _sentry: sentry.Sentry, **kwargs) -> t.Awaitable[t.Optional[ConversationProtocol]]: def __call__(self, *, _sentry: s.Sentry, **kwargs) -> t.Awaitable[t.Optional[ConversationProtocol]]:
log.debug(f"Calling: {repr(self)}") log.debug(f"Calling: {self!r}")
return self.f(_sentry=_sentry, **kwargs) 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): def __repr__(self):
return f"<Conversation #{id(self)}>" return f"<{self.__class__.__qualname__} #{id(self)}>"
__all__ = ( __all__ = (