From 2f5966b8c690ebda86e7c9e97189457c927eaccf Mon Sep 17 00:00:00 2001 From: japandotorg Date: Sat, 29 Jun 2024 17:48:47 +0530 Subject: [PATCH 1/4] [README] fix package name --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ccf611..bda543d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Additional documentation on the TagScriptEngine library can be [found here](http Download the latest version through pip: ``` -pip(3) install AdvancedTagscriptEngine +pip(3) install AdvancedTagScriptEngine ``` Download from a commit: From 8edfa6a538115f3fd06f73036dbcd0fcccdc2748 Mon Sep 17 00:00:00 2001 From: japandotorg Date: Sun, 28 Jul 2024 06:17:25 +0530 Subject: [PATCH 2/4] Update TagScriptEngine to support dpy 2.4 and much more. --- MANIFEST.in | 2 + README.md | 25 +- TagScriptEngine/__init__.py | 196 +++++++- TagScriptEngine/_warnings.py | 88 ++++ TagScriptEngine/adapter/__init__.py | 46 +- TagScriptEngine/adapter/discordadapters.py | 419 ++++++++++++++++-- TagScriptEngine/adapter/functionadapter.py | 11 +- TagScriptEngine/adapter/intadapter.py | 11 +- TagScriptEngine/adapter/objectadapter.py | 14 +- TagScriptEngine/adapter/redbotadapters.py | 161 +++++++ TagScriptEngine/adapter/stringadapter.py | 11 +- TagScriptEngine/block/__init__.py | 116 ++++- TagScriptEngine/block/allowedmentions.py | 60 +++ TagScriptEngine/block/assign.py | 12 +- TagScriptEngine/block/breakblock.py | 13 +- TagScriptEngine/block/case.py | 15 +- TagScriptEngine/block/command.py | 57 ++- TagScriptEngine/block/comment.py | 29 ++ TagScriptEngine/block/control.py | 35 +- TagScriptEngine/block/cooldown.py | 26 +- TagScriptEngine/block/count.py | 22 +- TagScriptEngine/block/embedblock.py | 83 +++- TagScriptEngine/block/fiftyfifty.py | 13 +- TagScriptEngine/block/helpers.py | 65 ++- TagScriptEngine/block/loosevariablegetter.py | 9 +- TagScriptEngine/block/mathblock.py | 92 ++-- TagScriptEngine/block/randomblock.py | 19 +- TagScriptEngine/block/range.py | 33 +- TagScriptEngine/block/redirect.py | 17 +- TagScriptEngine/block/replaceblock.py | 40 +- TagScriptEngine/block/require_blacklist.py | 21 +- TagScriptEngine/block/shortcutredirect.py | 15 +- TagScriptEngine/block/stopblock.py | 15 +- TagScriptEngine/block/strf.py | 13 +- TagScriptEngine/block/strictvariablegetter.py | 11 +- TagScriptEngine/block/substr.py | 23 +- TagScriptEngine/block/urlencodeblock.py | 18 +- TagScriptEngine/exceptions.py | 12 +- TagScriptEngine/interface/__init__.py | 16 +- TagScriptEngine/interface/adapter.py | 47 +- TagScriptEngine/interface/block.py | 36 +- TagScriptEngine/interpreter.py | 156 +++++-- TagScriptEngine/py.typed | 0 TagScriptEngine/utils.py | 43 +- TagScriptEngine/verb.py | 41 +- Tests/test_adapters.py | 22 +- Tests/test_edgecase.py | 8 +- Tests/test_verbs.py | 18 +- benchmark.py | 2 +- docs/_static/favicon.ico | Bin 0 -> 15406 bytes docs/_static/logo.png | Bin 0 -> 10627 bytes docs/conf.py | 14 +- docs/credits.rst | 1 + docs/getting_started.rst | 2 +- docs/install.rst | 36 ++ docs/requirements.txt | 3 +- playground.py | 14 +- pyproject.toml | 16 + requirements.txt | 3 +- setup.cfg | 22 +- 60 files changed, 1929 insertions(+), 439 deletions(-) create mode 100644 MANIFEST.in create mode 100644 TagScriptEngine/_warnings.py create mode 100644 TagScriptEngine/adapter/redbotadapters.py create mode 100644 TagScriptEngine/block/allowedmentions.py create mode 100644 TagScriptEngine/block/comment.py create mode 100644 TagScriptEngine/py.typed create mode 100644 docs/_static/favicon.ico create mode 100644 docs/_static/logo.png create mode 100644 docs/install.rst diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c1a7121 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include LICENSE +include README.md diff --git a/README.md b/README.md index bda543d..2ab2f42 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,16 @@ ## Information - - Documentation Status + + AdvancedTagScriptEngine - -  yPI + + PyPI - Version + + + Documentation Status + + + PyPI - Downloads + This repository is a fork of JonSnowbd's [TagScript](https://github.com/JonSnowbd/TagScript), a string templating language. @@ -12,7 +19,7 @@ well as multiple utility blocks. Additionally, several tweaks have been made to behavior. This TagScriptEngine is used on [MELON, a Discord bot](https://melonbot.io/invite). -An example implementation can be found its [Tags cog](https://github.com/japandotorg/Seina-Cogs/tree/main/tags). +An example implementation can be found in the [Tags cog](https://github.com/japandotorg/Seina-Cogs/tree/main/tags). Additional documentation on the TagScriptEngine library can be [found here](https://tagscriptengine.readthedocs.io/en/latest/). @@ -34,12 +41,12 @@ Install for editing/development: ``` git clone https://github.com/japandotorg/TagScriptEngine.git -pip(3) install -e ./TagScript +pip(3) install -e ./TagScriptEngine ``` ## What? -TagScript is a drop in easy to use string interpreter that lets you provide users with ways of +AdvancedTagScriptEngine is a drop in easy to use string interpreter that lets you provide users with ways of customizing their profiles or chat rooms with interactive text. For example TagScript comes out of the box with a random block that would let users provide @@ -50,6 +57,8 @@ use. `Python 3.8+` +`pyparsing` + `discord.py` -`pyparsing` +`Red-DiscordBot` [optional] diff --git a/TagScriptEngine/__init__.py b/TagScriptEngine/__init__.py index d8b249b..2f1210d 100644 --- a/TagScriptEngine/__init__.py +++ b/TagScriptEngine/__init__.py @@ -1,17 +1,185 @@ -from collections import namedtuple +from __future__ import annotations -from .adapter import * -from .block import * -from .exceptions import * -from .interface import * -from .interpreter import * -from .utils import * -from .verb import Verb as Verb +from typing import Final, NamedTuple, Tuple -__version__ = "3.1.4" +from .adapter import ( + SafeObjectAdapter as SafeObjectAdapter, + StringAdapter as StringAdapter, + IntAdapter as IntAdapter, + FunctionAdapter as FunctionAdapter, + DiscordAttributeAdapter as DiscordAttributeAdapter, + UserAdapter as UserAdapter, + MemberAdapter as MemberAdapter, + DMChannelAdapter as DMChannelAdapter, + ChannelAdapter as ChannelAdapter, + GuildAdapter as GuildAdapter, + RoleAdapter as RoleAdapter, + AttributeAdapter as AttributeAdapter, + DiscordObjectAdapter as DiscordObjectAdapter, + RedCommandAdapter as RedCommandAdapter, + RedBotAdapter as RedBotAdapter, +) +from .block import ( + implicit_bool as implicit_bool, + helper_parse_if as helper_parse_if, + helper_parse_list_if as helper_parse_list_if, + helper_split as helper_split, + AllowedMentionsBlock as AllowedMentionsBlock, + AllBlock as AllBlock, + AnyBlock as AnyBlock, + AssignmentBlock as AssignmentBlock, + BlacklistBlock as BlacklistBlock, + BreakBlock as BreakBlock, + SequentialGather as SequentialGather, + CommandBlock as CommandBlock, + EmbedBlock as EmbedBlock, + FiftyFiftyBlock as FiftyFiftyBlock, + IfBlock as IfBlock, + LooseVariableGetterBlock as LooseVariableGetterBlock, + MathBlock as MathBlock, + OverrideBlock as OverrideBlock, + PythonBlock as PythonBlock, + RandomBlock as RandomBlock, + RangeBlock as RangeBlock, + RedirectBlock as RedirectBlock, + ReplaceBlock as ReplaceBlock, + RequireBlock as RequireBlock, + ShortCutRedirectBlock as ShortCutRedirectBlock, + StopBlock as StopBlock, + StrfBlock as StrfBlock, + StrictVariableGetterBlock as StrictVariableGetterBlock, + SubstringBlock as SubstringBlock, + URLEncodeBlock as URLEncodeBlock, + UpperBlock as UpperBlock, + LowerBlock as LowerBlock, + CountBlock as CountBlock, + LengthBlock as LengthBlock, + CooldownBlock as CooldownBlock, +) +from .interface import ( + Adapter as Adapter, + SimpleAdapter as SimpleAdapter, + Block as Block, + verb_required_block as verb_required_block, +) +from ._warnings import ( + TagScriptEngineDeprecationWarning as TagScriptEngineDeprecationWarning, +) +from .exceptions import ( + TagScriptError as TagScriptError, + WorkloadExceededError as WorkloadExceededError, + ProcessError as ProcessError, + EmbedParseError as EmbedParseError, + BadColourArgument as BadColourArgument, + StopError as StopError, + CooldownExceeded as CooldownExceeded, +) +from .interpreter import ( + Interpreter as Interpreter, + AsyncInterpreter as AsyncInterpreter, + Context as Context, + Response as Response, + Node as Node, + build_node_tree as build_node_tree, +) +from .utils import ( + truncate as truncate, + escape_content as escape_content, + maybe_await as maybe_await, +) +from .verb import ( + Verb as Verb, +) -class VersionInfo(namedtuple("VersionInfo", "major minor micro")): +__all__: Tuple[str, ...] = ( + "implicit_bool", + "helper_parse_if", + "helper_parse_list_if", + "helper_split", + "AllBlock", + "AnyBlock", + "AssignmentBlock", + "BlacklistBlock", + "BreakBlock", + "SequentialGather", + "CommandBlock", + "CooldownBlock", + "EmbedBlock", + "FiftyFiftyBlock", + "IfBlock", + "LooseVariableGetterBlock", + "MathBlock", + "OverrideBlock", + "PythonBlock", + "RandomBlock", + "RangeBlock", + "RedirectBlock", + "ReplaceBlock", + "RequireBlock", + "ShortCutRedirectBlock", + "StopBlock", + "StrfBlock", + "StrictVariableGetterBlock", + "SubstringBlock", + "URLEncodeBlock", + "UpperBlock", + "LowerBlock", + "CountBlock", + "LengthBlock", + "SafeObjectAdapter", + "StringAdapter", + "IntAdapter", + "FunctionAdapter", + "RedCommandAdapter", + "RedBotAdapter", + "AttributeAdapter", + "DiscordAttributeAdapter", + "UserAdapter", + "MemberAdapter", + "DMChannelAdapter", + "ChannelAdapter", + "GuildAdapter", + "RoleAdapter", + "DiscordObjectAdapter", + "Adapter", + "SimpleAdapter", + "Block", + "verb_required_block", + "TagScriptEngineDeprecationWarning", + "TagScriptError", + "WorkloadExceededError", + "ProcessError", + "EmbedParseError", + "BadColourArgument", + "StopError", + "CooldownExceeded", + "Interpreter", + "AsyncInterpreter", + "Context", + "Response", + "Node", + "build_node_tree", + "truncate", + "escape_content", + "maybe_await", + "Verb", + "__version__", + "VersionInfo", + "version_info", +) + + +__version__: Final[str] = "3.2.0" + + +class VersionNamedTuple(NamedTuple): + major: int + minor: int + micro: int + + +class VersionInfo(VersionNamedTuple): """ Version information. @@ -25,9 +193,9 @@ class VersionInfo(namedtuple("VersionInfo", "major minor micro")): Micro version number. """ - __slots__ = () + __slots__: Tuple[str, ...] = () - def __str__(self): + def __str__(self) -> str: """ Returns a string representation of the version information. @@ -39,7 +207,7 @@ def __str__(self): return "{major}.{minor}.{micro}".format(**self._asdict()) @classmethod - def from_str(cls, version): + def from_str(cls, version: str) -> "VersionInfo": """ Returns a VersionInfo instance from a string. @@ -56,4 +224,4 @@ def from_str(cls, version): return cls(*map(int, version.split("."))) -version_info = VersionInfo.from_str(__version__) +version_info: VersionInfo = VersionInfo.from_str(__version__) diff --git a/TagScriptEngine/_warnings.py b/TagScriptEngine/_warnings.py new file mode 100644 index 0000000..e399d36 --- /dev/null +++ b/TagScriptEngine/_warnings.py @@ -0,0 +1,88 @@ +import warnings +import functools +from typing import Callable, Optional, Tuple, TypeVar +from typing_extensions import ParamSpec + + +_P = ParamSpec("_P") +_T = TypeVar("_T") + + +__all__: Tuple[str, ...] = ( + "TagScriptEngineDeprecationWarning", + "TagScriptEngineAttributeRemovalWarning", +) + + +class TagScriptEngineAttributeRemovalWarning(Warning): + """A class for issuing removal warning for TagScriptEngine class attributes.""" + + +class TagScriptEngineDeprecationWarning(DeprecationWarning): + """A class for issuing deprecation warnings for TagScriptEngine modules.""" + + +def remove( + name: str, *, reason: Optional[str] = None, version: Optional[str] = None, level: int = 2 +) -> None: + warnings.warn( + "Removal: {name}.{reason}{version}".format( + name=name, + reason=" ({})".format(reason) if reason else "", + version=" -- Removal scheduled since version v{}".format(version) if version else "", + ), + category=TagScriptEngineAttributeRemovalWarning, + stacklevel=level, + ) + + +def removal( + *, + name: Optional[str] = None, + reason: Optional[str] = None, + version: Optional[str] = None, + level: int = 3, +) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: + def decorator(func: Callable[_P, _T]) -> Callable[_P, _T]: + functools.wraps(func) + + def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T: + remove(name or func.__name__, reason=reason, version=version, level=level) + return func(*args, **kwargs) + + return wrapper + + return decorator + + +def depricate( + name: str, *, reason: Optional[str] = None, version: Optional[str] = None, level: int = 2 +) -> None: + warnings.warn( + "Deprecated: {name}.{reason}{version}".format( + name=name, + reason=" ({})".format(reason) if reason else "", + version=" -- Deprecated since version v{}.".format(version) if version else "", + ), + category=TagScriptEngineDeprecationWarning, + stacklevel=level, + ) + + +def deprecated( + *, + name: Optional[str] = None, + reason: Optional[str] = None, + version: Optional[str] = None, + level: int = 3, +) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: + def decorator(func: Callable[_P, _T]) -> Callable[_P, _T]: + functools.wraps(func) + + def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T: + depricate(name or func.__name__, reason=reason, version=version, level=level) + return func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/TagScriptEngine/adapter/__init__.py b/TagScriptEngine/adapter/__init__.py index 7985952..079c298 100644 --- a/TagScriptEngine/adapter/__init__.py +++ b/TagScriptEngine/adapter/__init__.py @@ -1,16 +1,50 @@ -from .discordadapters import * -from .functionadapter import FunctionAdapter -from .intadapter import IntAdapter -from .objectadapter import SafeObjectAdapter -from .stringadapter import StringAdapter +from __future__ import annotations -__all__ = ( +from typing import Tuple + +from .discordadapters import ( + AttributeAdapter as AttributeAdapter, + DiscordAttributeAdapter as DiscordAttributeAdapter, + UserAdapter as UserAdapter, + MemberAdapter as MemberAdapter, + DMChannelAdapter as DMChannelAdapter, + ChannelAdapter as ChannelAdapter, + GuildAdapter as GuildAdapter, + RoleAdapter as RoleAdapter, + DiscordObjectAdapter as DiscordObjectAdapter, +) +from .functionadapter import ( + FunctionAdapter as FunctionAdapter, +) +from .intadapter import ( + IntAdapter as IntAdapter, +) +from .objectadapter import ( + SafeObjectAdapter as SafeObjectAdapter, +) +from .redbotadapters import ( + RedCommandAdapter as RedCommandAdapter, + RedBotAdapter as RedBotAdapter, +) +from .stringadapter import ( + StringAdapter as StringAdapter, +) + + +__all__: Tuple[str, ...] = ( "SafeObjectAdapter", "StringAdapter", "IntAdapter", "FunctionAdapter", "AttributeAdapter", + "DiscordAttributeAdapter", + "UserAdapter", "MemberAdapter", + "DMChannelAdapter", "ChannelAdapter", "GuildAdapter", + "RoleAdapter", + "DiscordObjectAdapter", + "RedCommandAdapter", + "RedBotAdapter", ) diff --git a/TagScriptEngine/adapter/discordadapters.py b/TagScriptEngine/adapter/discordadapters.py index 3d417a2..0e89b9a 100644 --- a/TagScriptEngine/adapter/discordadapters.py +++ b/TagScriptEngine/adapter/discordadapters.py @@ -1,33 +1,65 @@ +from __future__ import annotations + +import logging +import datetime from random import choice -from typing import Union +from typing import Any, Dict, Union, cast, Tuple import discord -from ..interface import Adapter -from ..utils import DPY2, escape_content from ..verb import Verb +from ..utils import escape_content +from .._warnings import deprecated +from ..interface import Adapter, SimpleAdapter + + +_log: logging.Logger = logging.getLogger(__name__) -__all__ = ( + +__all__: Tuple[str, ...] = ( "AttributeAdapter", + "DiscordAttributeAdapter", + "UserAdapter", "MemberAdapter", + "DMChannelAdapter", "ChannelAdapter", "GuildAdapter", + "RoleAdapter", + "DiscordObjectAdapter", ) class AttributeAdapter(Adapter): - __slots__ = ("object", "_attributes", "_methods") + """ + .. deprecated:: 3.2.0 + AttributeAdapter has been deprecated and will be removed in favor of + ``TagScriptEngine.adapter.discordadpaters.DiscordAttributeAdapter`` or + consider using ``TagScriptEngine.interface.adapter.SimpleAdapter`` instead. + """ + __slots__: Tuple[str, ...] = ("object", "_attributes", "_methods") + + @deprecated( + name="TagScriptEngine.adapter.discordadpaters.AttributeAdapter", + reason=( + "AttributeAdapter has been deprecated and will be removed in favor of " + "``TagScriptEngine.adapter.discordadpaters.DiscordAttributeAdapter`` or " + "consider using ``TagScriptEngine.interface.adapter.SimpleAdapter`` instead." + ), + version="3.2.0", + ) def __init__(self, base: Union[discord.TextChannel, discord.Member, discord.Guild]) -> None: - self.object = base - created_at = getattr(base, "created_at", None) or discord.utils.snowflake_time(base.id) - self._attributes = { + self.object: Union[discord.TextChannel, discord.Member, discord.Guild] = base + created_at: datetime.datetime = getattr( + base, "created_at", None + ) or discord.utils.snowflake_time(base.id) + self._attributes: Dict[str, Any] = { "id": base.id, "created_at": created_at, "timestamp": int(created_at.timestamp()), "name": getattr(base, "name", str(base)), } - self._methods = {} + self._methods: Dict[str, Any] = {} self.update_attributes() self.update_methods() @@ -42,7 +74,6 @@ def update_methods(self) -> None: def get_value(self, ctx: Verb) -> str: should_escape = False - if ctx.parameter is None: return_value = str(self.object) else: @@ -52,17 +83,136 @@ def get_value(self, ctx: Verb) -> str: if method := self._methods.get(ctx.parameter): value = method() else: - return - + return # type: ignore if isinstance(value, tuple): value, should_escape = value + return_value: str = str(value) if value is not None else None # type: ignore + return escape_content(return_value) if should_escape else return_value + +class DiscordAttributeAdapter( + SimpleAdapter[ + Union[ + discord.TextChannel, + discord.DMChannel, + discord.User, + discord.Member, + discord.Guild, + discord.Role, + ] + ] +): + """ + .. versionadded:: 3.2.0 + """ + + def __init__( + self, + base: Union[ + discord.TextChannel, + discord.DMChannel, + discord.User, + discord.Member, + discord.Guild, + discord.Role, + ], + ) -> None: + super().__init__(base=base) + created_at: datetime.datetime = getattr( + base, "created_at", None + ) or discord.utils.snowflake_time(base.id) + self._attributes.update( + { + "id": base.id, + "created_at": created_at, + "timestamp": int(created_at.timestamp()), + "name": getattr(base, "name", str(base)), + } + ) + + def __repr__(self) -> str: + return "<{} object={}>".format(type(self).__qualname__, self.object) + + def get_value(self, ctx: Verb) -> str: # type: ignore + should_escape = False + if ctx.parameter is None: + return_value = str(self.object) + else: + try: + value = self._attributes[ctx.parameter] + except KeyError: + if method := self._methods.get(ctx.parameter): + value = method() + else: + _log.debug( + "No parameter named `{}` found for the `{}` Adapter.".format( + ctx.parameter, self.__class__.__name__ + ) + ) + return # type: ignore + if isinstance(value, tuple): + value, should_escape = value return_value = str(value) if value is not None else None + return escape_content(return_value) if should_escape else return_value # type: ignore - return escape_content(return_value) if should_escape else return_value +class UserAdapter(DiscordAttributeAdapter): + """ + The ``{user}`` block with no parameters returns the user's full username, + but passing the attributes listed below to the block payload will return + that attribute instead. + + **Usage:** ``{user([attribute])}`` + + **Payload:** None + + **Parameter:** attribute, None + + Attributes + ---------- + id + The user's Discord ID. + name + The user's username. + nick + The user's nickname, if they have one, else their username. + avatar + A link to the user's avatar, which can be used in embeds. + created_at + The user's account creation date. + timestamp + The user's account creation date as UTC timestamp. + mention + A formatted text that ping's the user. + bot + Wheather or not the user is a bot. + accent_color + The user's accent color if banner is not present. + avatar_decoration + A link to the user's avatar decoration. + + .. versionadded:: 3.2.0 + """ + + def update_attributes(self) -> None: + object: discord.User = cast(discord.User, self.object) + avatar_url: str = object.display_avatar.url + if asset := object.avatar_decoration: + decoration: Union[str, bool] = asset.with_format("png").url + else: + decoration: Union[str, bool] = False + additional_attributes: Dict[str, Any] = { + "nick": object.display_name, + "mention": object.mention, + "avatar": (avatar_url, False), + "bot": object.bot, + "accent_color": getattr(object, "accent_color", False), + "avatar_decoration": decoration, + } + self._attributes.update(additional_attributes) -class MemberAdapter(AttributeAdapter): + +class MemberAdapter(DiscordAttributeAdapter): """ The ``{author}`` block with no parameters returns the tag invoker's full username and discriminator, but passing the attributes listed below to the block payload @@ -117,29 +267,66 @@ class MemberAdapter(AttributeAdapter): """ def update_attributes(self) -> None: - avatar_url = self.object.display_avatar.url if DPY2 else self.object.avatar_url - joined_at = getattr(self.object, "joined_at", self.object.created_at) - additional_attributes = { - "color": self.object.color, - "colour": self.object.color, - "nick": self.object.display_name, + object: discord.Member = cast(discord.Member, self.object) + avatar_url: str = object.display_avatar.url + joined_at: datetime.datetime = getattr(object, "joined_at", self.object.created_at) + additional_attributes: Dict[str, Any] = { + "color": object.color, + "colour": object.color, + "nick": object.display_name, "avatar": (avatar_url, False), - "discriminator": self.object.discriminator, + "discriminator": object.discriminator, "joined_at": joined_at, "joinstamp": int(joined_at.timestamp()), - "mention": self.object.mention, - "bot": self.object.bot, - "top_role": getattr(self.object, "top_role", ""), - "boost": getattr(self.object, "premium_since", ""), - "timed_out": getattr(self.object, "timed_out_until", ""), - "banner": self.object.banner.url if self.object.banner else "", + "mention": object.mention, + "bot": object.bot, + "top_role": getattr(object, "top_role", ""), + "boost": getattr(object, "premium_since", ""), + "timed_out": getattr(object, "timed_out_until", ""), + "banner": object.banner.url if object.banner else "", } if roleids := getattr(self.object, "_roles", None): additional_attributes["roleids"] = " ".join(str(r) for r in roleids) self._attributes.update(additional_attributes) -class ChannelAdapter(AttributeAdapter): +class DMChannelAdapter(DiscordAttributeAdapter): + """ + The ``{channel}`` block with no parameters returns the channel's full name + but passing the attributes listed below to the block payload will return + the attribute instead. + + **Usage:** ``{channel([attribute])`` + + **Payload:** None + + **Parameter:** attribute, None + + Attributes + ---------- + id + The channel's ID. + name + The channel's name. + created_at + The channel's creation date. + timestamp + The channel's creation date as a UTC timestamp. + jump_url + A link to the channel. + + .. versionadded:: 3.2.0 + """ + + def update_attributes(self) -> None: + if isinstance(self.object, discord.DMChannel): + additional_attributes: Dict[str, Any] = { + "jump_url": getattr(self.object, "jump_url", None) + } + self._attributes.update(additional_attributes) + + +class ChannelAdapter(DiscordAttributeAdapter): """ The ``{channel}`` block with no parameters returns the channel's full name but passing the attributes listed below to the block payload @@ -170,21 +357,27 @@ class ChannelAdapter(AttributeAdapter): category_id The category the channel is associated with. If no category channel, this will return empty. + jump_url + A link to the channel. + + .. versionchanged:: 3.2.0 + Added ``jump_url`` as a parameter. """ def update_attributes(self) -> None: if isinstance(self.object, discord.TextChannel): - additional_attributes = { + additional_attributes: Dict[str, Any] = { "nsfw": self.object.nsfw, "mention": self.object.mention, "topic": self.object.topic or "", "slowmode": self.object.slowmode_delay, "category_id": self.object.category_id or "", + "jump_url": self.object.jump_url or None, } self._attributes.update(additional_attributes) -class GuildAdapter(AttributeAdapter): +class GuildAdapter(DiscordAttributeAdapter): """ The ``{server}`` block with no parameters returns the server's name but passing the attributes listed below to the block payload @@ -224,34 +417,180 @@ class GuildAdapter(AttributeAdapter): If guild has a vanity, this returns the vanity else empty. owner_id The server owner's id. + mfa + The server's mfa level. + boosters + The server's active booster count. + boost_level + The server's current boost level/tier. + discovery_splash + A link to the server's discovery splash. + invite_splash + A link to the server's invite splash. + banner + A link to the server's banner. + + .. versionchanged:: 3.2.0 + Added ``mfa``, ``boosters``, ``boost_level``, + ``discovery_splash``, ``invite_splash`` & ``banner``. """ def update_attributes(self) -> None: - guild = self.object - bots = 0 - humans = 0 + object: discord.Guild = cast(discord.Guild, self.object) + guild: discord.Guild = object + bots: int = 0 + humans: int = 0 for m in guild.members: if m.bot: bots += 1 else: humans += 1 - member_count = guild.member_count - icon_url = getattr(guild.icon, "url", "") if DPY2 else guild.icon_url - additional_attributes = { + member_count: int = getattr(guild, "member_count", 0) + icon_url: str = getattr(guild.icon, "url", "") + additional_attributes: Dict[str, Any] = { "icon": (icon_url, False), "member_count": member_count, "members": member_count, "bots": bots, "humans": humans, "description": guild.description or "No description.", - "vanity": self.object.vanity_url_code or "No Vanity URL.", - "owner_id": self.object.owner_id or "", + "vanity": guild.vanity_url_code or "No Vanity URL.", + "owner_id": guild.owner_id or "", + "mfa": guild.mfa_level, + "boosters": guild.premium_subscription_count, + "boost_level": guild.premium_tier, + "discovery_splash": getattr(guild.discovery_splash, "url", False), + "invite_splash": getattr(guild.splash, "url", False), + "banner": getattr(guild.banner, "url", False), } self._attributes.update(additional_attributes) def update_methods(self) -> None: - additional_methods = {"random": self.random_member} + additional_methods: Dict[str, Any] = {"random": self.random_member} self._methods.update(additional_methods) def random_member(self) -> discord.Member: - return choice(self.object.members) + object: discord.Guild = cast(discord.Guild, self.object) + return choice(object.members) + + +class RoleAdapter(DiscordAttributeAdapter): + """ + The ``{role}`` block with no parameters returns the role's full name + but passing the attributes listed below to the block payload will + return that attribute instead. + + **Usage:** ``{role([attribute])}`` + + **Payload:** None + + **Parameter:** attribute, None + + Attributes + ---------- + id + The role's ID. + name + The role's name. + created_at + The role's creation date. + timestamp + The role's creation date as a UTC timestamp. + color + The role's color. + display_icon + The role's icon. + hoist + Wheather the role is hoisted or not. + managed + Wheather the role is managed or not. + mention + A formatted text that pings the role. + position + The role's position. + + .. versionadded:: 3.2.0 + """ + + def update_attributes(self) -> None: + object: discord.Role = cast(discord.Role, self.object) + additional_attributes: Dict[str, Any] = { + "color": object.color, + "display_icon": getattr(object.display_icon, "url", False), + "hoist": object.hoist, + "managed": object.managed, + "mention": object.mention, + "position": object.position, + } + self._attributes.update(additional_attributes) + + +class DiscordObjectAdapter(Adapter): + """ + The ``{object}`` block with no parameters returs the discord object's ID, + but passing the attributes listed below to the block payload will return + that attribute instead. + + **Usage:** ``{object([attribute])}`` + + **Payload:** None + + **Parameter:** attribute, None + + Attributes + ---------- + id + The object's Discord ID. + created_at + The object's creation date. + timestamp + The object's creation date as a UTC timestamp. + + .. versionadded:: 3.2.0 + """ + + __slots__: Tuple[str, ...] = ("object", "_attributes", "_methods") + + def __init__(self, base: discord.Object) -> None: + self.object: discord.Object = base + created_at: datetime.datetime = getattr( + base, "created_at", None + ) or discord.utils.snowflake_time(base.id) + self._attributes: Dict[str, Any] = { + "id": base.id, + "created_at": created_at, + "timestamp": int(created_at.timestamp()), + } + self._methods: Dict[str, Any] = {} + self.update_attributes() + self.update_methods() + + def __repr__(self) -> str: + return f"<{type(self).__qualname__} object={self.object!r}>" + + def update_attributes(self) -> None: + pass + + def update_methods(self) -> None: + pass + + def get_value(self, ctx: Verb) -> str: # type: ignore + should_escape = False + + if ctx.parameter is None: + return_value = str(self.object.id) + else: + try: + value = self._attributes[ctx.parameter] + except KeyError: + if method := self._methods.get(ctx.parameter): + value = method() + else: + return # type: ignore + + if isinstance(value, tuple): + value, should_escape = value + + return_value = str(value) if value is not None else None + + return escape_content(return_value) if should_escape else return_value # type: ignore diff --git a/TagScriptEngine/adapter/functionadapter.py b/TagScriptEngine/adapter/functionadapter.py index 9ddee90..352f967 100644 --- a/TagScriptEngine/adapter/functionadapter.py +++ b/TagScriptEngine/adapter/functionadapter.py @@ -1,11 +1,16 @@ -from typing import Callable +from __future__ import annotations + +from typing import Callable, Tuple from ..interface import Adapter from ..verb import Verb +__all__: Tuple[str, ...] = ("FunctionAdapter",) + + class FunctionAdapter(Adapter): - __slots__ = ("fn",) + __slots__: Tuple[str, ...] = ("fn",) def __init__(self, function_pointer: Callable[[], str]) -> None: self.fn = function_pointer @@ -14,5 +19,5 @@ def __init__(self, function_pointer: Callable[[], str]) -> None: def __repr__(self) -> str: return f"<{type(self).__qualname__} fn={self.fn!r}>" - def get_value(self, ctx: Verb) -> str: + def get_value(self, ctx: Verb) -> str: # type: ignore return str(self.fn()) diff --git a/TagScriptEngine/adapter/intadapter.py b/TagScriptEngine/adapter/intadapter.py index 8cc2530..adfee5a 100644 --- a/TagScriptEngine/adapter/intadapter.py +++ b/TagScriptEngine/adapter/intadapter.py @@ -1,9 +1,16 @@ +from __future__ import annotations + +from typing import Tuple + from ..interface import Adapter from ..verb import Verb +__all__: Tuple[str, ...] = ("IntAdapter",) + + class IntAdapter(Adapter): - __slots__ = ("integer",) + __slots__: Tuple[str, ...] = ("integer",) def __init__(self, integer: int) -> None: self.integer: int = int(integer) @@ -11,5 +18,5 @@ def __init__(self, integer: int) -> None: def __repr__(self) -> str: return f"<{type(self).__qualname__} integer={repr(self.integer)}>" - def get_value(self, ctx: Verb) -> str: + def get_value(self, ctx: Verb) -> str: # type: ignore return str(self.integer) diff --git a/TagScriptEngine/adapter/objectadapter.py b/TagScriptEngine/adapter/objectadapter.py index 901dd92..b59f3b7 100644 --- a/TagScriptEngine/adapter/objectadapter.py +++ b/TagScriptEngine/adapter/objectadapter.py @@ -1,11 +1,17 @@ +from __future__ import annotations + +from typing import Tuple from inspect import ismethod from ..interface import Adapter from ..verb import Verb +__all__: Tuple[str, ...] = ("SafeObjectAdapter",) + + class SafeObjectAdapter(Adapter): - __slots__ = ("object",) + __slots__: Tuple[str, ...] = ("object",) def __init__(self, base) -> None: self.object = base @@ -17,13 +23,13 @@ def get_value(self, ctx: Verb) -> str: if ctx.parameter is None: return str(self.object) if ctx.parameter.startswith("_") or "." in ctx.parameter: - return + return # type: ignore try: attribute = getattr(self.object, ctx.parameter) except AttributeError: - return + return # type: ignore if ismethod(attribute): - return + return # type: ignore if isinstance(attribute, float): attribute = int(attribute) return str(attribute) diff --git a/TagScriptEngine/adapter/redbotadapters.py b/TagScriptEngine/adapter/redbotadapters.py new file mode 100644 index 0000000..bdbea23 --- /dev/null +++ b/TagScriptEngine/adapter/redbotadapters.py @@ -0,0 +1,161 @@ +import datetime +from typing import Any, Dict, Optional, Tuple, cast + +import discord + +from ..verb import Verb +from ..interface import SimpleAdapter +from ..utils import escape_content + +try: + import redbot # noqa: F401 +except ModuleNotFoundError: + _has_redbot = False +else: + _has_redbot = True + + from redbot.core.bot import Red + from redbot.core.commands import Command + from redbot.core.utils.chat_formatting import humanize_number, humanize_list + + +__all__: Tuple[str, ...] = ("RedCommandAdapter", "RedBotAdapter") + + +class RedCommandAdapter(SimpleAdapter["Command"]): + if not _has_redbot: + raise ImportError("A Red-DiscordBot instance is required to use this.", name="redbot") + + def __init__(self, base: Command, *, signature: Optional[str] = None) -> None: + super().__init__(base=base) + self.signature: Optional[str] = signature + + def update_attributes(self) -> None: + command: Command = self.object + self._attributes.update( + { + "name": command.name, + "cog_name": getattr(command, "cog_name", None), + "description": getattr(command, "description", None), + "aliases": humanize_list(list(getattr(command, "aliases", []))) or "None", + "qualified_name": command.qualified_name, + "signature": self.signature, + } + ) + + def get_value(self, ctx: Verb) -> str: + should_escape: bool = False + if ctx.parameter is None: + return_value: str = self.object.qualified_name + else: + try: + value: Any = self._attributes[ctx.parameter] + except KeyError: + return # type: ignore + if isinstance(value, tuple): + value, should_escape = value + return_value: str = str(value) if value is not None else None # type: ignore + return escape_content(return_value) if should_escape else return_value + + +class RedBotAdapter(SimpleAdapter["Red"]): + """ + The ``{bot}`` block with no parameters returns the bot's name & discriminator, + but passing the attributes listed below to the block payload will return that attribute instead. + + **Usage:** ``{bot([attribute])}`` + + **Payload:** None + + **Parameter:** attribute, None + + Attributes + ---------- + id + The bot's Discord ID. + name + The bot's username. + discriminator + The bot's discriminator. + nick + The bot's nickname, if they have one, else their username. + created_at + The bot's creation date. + timestamp + The bot's creation date as a UTC timestamp. + mention + A formatted text that pings the bot. + verified + If the bot is verified or not. + shard_count (*) + The bot's total shard count. + servers (*) + Total server/guild count of the bot. + channels (*) + Total number of channels visible to the bot. + visible_users (*) + Total number of users visible to the bot. + total_users (*) + The bot's total user count. + unique_users (*) + The bot's unique user count. + percentage_chunked (*) + Percentage of chunked guilds the bot has. + + .. warning:: + Attributes denoting `(*)` can only be used by the bot owner. + """ + + if not _has_redbot: + raise ImportError("A Red-DiscordBot instance is required to use this.") + + def __init__(self, base: Red, *, owner: bool = True) -> None: + super().__init__(base=base) + self.is_owner: bool = owner + + def update_attributes(self) -> None: + self.user: discord.ClientUser = cast(discord.ClientUser, self.object.user) + created_at: datetime.datetime = getattr( + self.user, "created_at", None + ) or discord.utils.snowflake_time(self.user.id) + self._attributes.update( + { + "id": self.user.id, + "name": self.user.name, + "discriminator": self.user.discriminator, + "nick": self.user.display_name, + "mention": self.user.display_avatar.url, + "created_at": created_at, + "timestamp": int(created_at.timestamp()), + "verified": self.user.verified, + } + ) + if self.is_owner: + visible_users: int = sum(len(g.members) for g in self.object.guilds) + total_users: int = sum( + g.member_count if g.member_count else 0 for g in self.object.guilds + ) + owner_attributes: Dict[str, Any] = { + "shard_count": humanize_number(self.object.bot.shard_count), + "servers": humanize_number(len(self.object.guilds)), + "channels": humanize_number(sum(len(g.channels) for g in self.object.guilds)), + "visible_users": humanize_number(visible_users), + "total_users": humanize_number(total_users), + "unique_users": humanize_number(len(self.object.users)), + "percentage_chunked": visible_users / total_users * 100, + } + self._attributes.update(owner_attributes) + + def get_value(self, ctx: Verb) -> str: + should_escape: bool = False + if ctx.parameter is None: + return_value: str = "{0.name}#{0.discriminator}".format(self.user) + else: + try: + value: Any = self._attributes[ctx.parameter] + except KeyError: + return # type: ignore + if isinstance(value, tuple): + value, should_escape = value + return_value: str = str(value) if value is not None else None # type: ignore + return escape_content(return_value) if should_escape else return_value diff --git a/TagScriptEngine/adapter/stringadapter.py b/TagScriptEngine/adapter/stringadapter.py index 134caa0..b08f1ee 100644 --- a/TagScriptEngine/adapter/stringadapter.py +++ b/TagScriptEngine/adapter/stringadapter.py @@ -1,10 +1,17 @@ +from __future__ import annotations + +from typing import Tuple + from ..interface import Adapter from ..utils import escape_content from ..verb import Verb +__all__: Tuple[str, ...] = ("StringAdapter",) + + class StringAdapter(Adapter): - __slots__ = ("string", "escape_content") + __slots__: Tuple[str, ...] = ("string", "escape_content") def __init__(self, string: str, *, escape: bool = False) -> None: self.string: str = str(string) @@ -33,7 +40,7 @@ def handle_ctx(self, ctx: Verb) -> str: return splitter.join(self.string.split(splitter)[index:]) else: return self.string.split(splitter)[index] - except: + except Exception: return self.string def return_value(self, string: str) -> str: diff --git a/TagScriptEngine/block/__init__.py b/TagScriptEngine/block/__init__.py index 3a628c8..82fb000 100644 --- a/TagScriptEngine/block/__init__.py +++ b/TagScriptEngine/block/__init__.py @@ -1,40 +1,108 @@ +from __future__ import annotations + +from typing import Tuple + # isort: off -from .helpers import * +from .helpers import ( + implicit_bool as implicit_bool, + helper_parse_if as helper_parse_if, + helper_parse_list_if as helper_parse_list_if, + helper_split as helper_split, + easier_helper_split as easier_helper_split, +) # isort: on -from .assign import AssignmentBlock -from .breakblock import BreakBlock -from .command import CommandBlock, OverrideBlock -from .control import AllBlock, AnyBlock, IfBlock -from .cooldown import CooldownBlock -from .embedblock import EmbedBlock -from .fiftyfifty import FiftyFiftyBlock -from .loosevariablegetter import LooseVariableGetterBlock -from .mathblock import MathBlock -from .randomblock import RandomBlock -from .range import RangeBlock -from .redirect import RedirectBlock -from .replaceblock import PythonBlock, ReplaceBlock -from .require_blacklist import BlacklistBlock, RequireBlock -from .shortcutredirect import ShortCutRedirectBlock -from .stopblock import StopBlock -from .strf import StrfBlock -from .strictvariablegetter import StrictVariableGetterBlock -from .substr import SubstringBlock -from .urlencodeblock import URLEncodeBlock -from .case import UpperBlock, LowerBlock -from .count import CountBlock, LengthBlock +from .allowedmentions import ( + AllowedMentionsBlock as AllowedMentionsBlock, +) +from .assign import ( + AssignmentBlock as AssignmentBlock, +) +from .breakblock import ( + BreakBlock as BreakBlock, +) +from .command import ( + CommandBlock as CommandBlock, + OverrideBlock as OverrideBlock, + SequentialGather as SequentialGather, +) +from .control import ( + AllBlock as AllBlock, + AnyBlock as AnyBlock, + IfBlock as IfBlock, +) +from .cooldown import ( + CooldownBlock as CooldownBlock, +) +from .embedblock import ( + EmbedBlock as EmbedBlock, +) +from .fiftyfifty import ( + FiftyFiftyBlock as FiftyFiftyBlock, +) +from .loosevariablegetter import ( + LooseVariableGetterBlock as LooseVariableGetterBlock, +) +from .mathblock import ( + MathBlock as MathBlock, +) +from .randomblock import ( + RandomBlock as RandomBlock, +) +from .range import ( + RangeBlock as RangeBlock, +) +from .redirect import ( + RedirectBlock as RedirectBlock, +) +from .replaceblock import ( + PythonBlock as PythonBlock, + ReplaceBlock as ReplaceBlock, +) +from .require_blacklist import ( + BlacklistBlock as BlacklistBlock, + RequireBlock as RequireBlock, +) +from .shortcutredirect import ( + ShortCutRedirectBlock as ShortCutRedirectBlock, +) +from .stopblock import ( + StopBlock as StopBlock, +) +from .strf import ( + StrfBlock as StrfBlock, +) +from .strictvariablegetter import ( + StrictVariableGetterBlock as StrictVariableGetterBlock, +) +from .substr import ( + SubstringBlock as SubstringBlock, +) +from .urlencodeblock import ( + URLEncodeBlock as URLEncodeBlock, +) +from .case import ( + UpperBlock as UpperBlock, + LowerBlock as LowerBlock, +) +from .count import ( + CountBlock as CountBlock, + LengthBlock as LengthBlock, +) -__all__ = ( +__all__: Tuple[str, ...] = ( "implicit_bool", "helper_parse_if", "helper_parse_list_if", "helper_split", + "easier_helper_split", + "AllowedMentionsBlock", "AllBlock", "AnyBlock", "AssignmentBlock", "BlacklistBlock", "BreakBlock", + "SequentialGather", "CommandBlock", "CooldownBlock", "EmbedBlock", diff --git a/TagScriptEngine/block/allowedmentions.py b/TagScriptEngine/block/allowedmentions.py new file mode 100644 index 0000000..2bd42c7 --- /dev/null +++ b/TagScriptEngine/block/allowedmentions.py @@ -0,0 +1,60 @@ +from typing import Dict, List, Optional, Tuple, Union + +from ..interface import Block +from ..interpreter import Context + + +class AllowedMentionsBlock(Block): + """ + The ``{allowedmentions}`` block attempts to enable mentioning of roles. + Passing no parameter enables mentioning of all roles within the message + content. However passing a role name or ID to the block parameter allows + mentioning of that specific role only. Multiple role name or IDs can be + included, separated by a comma ",". By default, mentioning is only + triggered if the execution author has "manage server" permissions. However, + using the "override" keyword as a payload allows mentioning to be triggered + by anyone. + + **Usage:** ``{allowedmentions():["override", None]}`` + + **Aliases:** ``mentions`` + + **Payload:** "override", None + + **Parameter:** role, None + + **Examples:** :: + + {allowedmentions} + {allowedmentions:override} + {allowedmentions(@Admin, Moderator):override} + {allowedmentions(763522431151112265, 812949167190048769)} + {mentions(763522431151112265, 812949167190048769):override} + """ + + ACCEPTED_NAMES: Tuple[str, ...] = ("allowedmentions", "mentions") + PAYLOADS: Tuple[str, ...] = ("override",) + + @classmethod + def will_accept(cls, ctx: Context) -> bool: + if ctx.verb.payload and ctx.verb.payload not in cls.PAYLOADS: + return False + return super().will_accept(ctx) + + def process(self, ctx: Context) -> Optional[str]: + actions: Optional[Dict[str, Union[bool, List[str]]]] = ctx.response.actions.get( + "allowed_mentions", None + ) + if actions: + return None + if not (param := ctx.verb.parameter): + ctx.response.actions["allowed_mentions"] = { + "mentions": True, + "override": True if ctx.verb.payload else False, + } + return "" + ctx.response.actions["allowed_mentions"] = { + "mentions": [r.strip() for r in param.split(",")], + "override": True if ctx.verb.payload else False, + } + return "" diff --git a/TagScriptEngine/block/assign.py b/TagScriptEngine/block/assign.py index 127e7df..5bcf756 100644 --- a/TagScriptEngine/block/assign.py +++ b/TagScriptEngine/block/assign.py @@ -1,11 +1,17 @@ -from typing import Optional +from __future__ import annotations + +from typing import Optional, Tuple, Type, cast from ..adapter import StringAdapter from ..interface import verb_required_block from ..interpreter import Context +from ..interface.block import Block + + +__all__: Tuple[str, ...] = ("AssignmentBlock",) -class AssignmentBlock(verb_required_block(False, parameter=True)): +class AssignmentBlock(cast(Type[Block], verb_required_block(False, parameter=True))): """ Variables are useful for choosing a value and referencing it later in a tag. Variables can be referenced using brackets as any other block. @@ -29,7 +35,7 @@ class AssignmentBlock(verb_required_block(False, parameter=True)): # The day is Monday. """ - ACCEPTED_NAMES = ("=", "assign", "let", "var") + ACCEPTED_NAMES: Tuple[str, ...] = ("=", "assign", "let", "var") def process(self, ctx: Context) -> Optional[str]: if ctx.verb.parameter is None: diff --git a/TagScriptEngine/block/breakblock.py b/TagScriptEngine/block/breakblock.py index f286a2e..2ece77b 100644 --- a/TagScriptEngine/block/breakblock.py +++ b/TagScriptEngine/block/breakblock.py @@ -1,10 +1,15 @@ -from typing import Optional +from __future__ import annotations + +from typing import Optional, Tuple, cast from ..interface import Block from ..interpreter import Context from . import helper_parse_if +__all__: Tuple[str, ...] = ("BreakBlock",) + + class BreakBlock(Block): """ The break block will force the tag output to only be the payload of this block, if the passed @@ -28,9 +33,9 @@ class BreakBlock(Block): {break({args}==):You did not provide any input.} """ - ACCEPTED_NAMES = ("break", "shortcircuit", "short") + ACCEPTED_NAMES: Tuple[str, ...] = ("break", "shortcircuit", "short") def process(self, ctx: Context) -> Optional[str]: - if helper_parse_if(ctx.verb.parameter): - ctx.response.body = ctx.verb.payload if ctx.verb.payload != None else "" + if helper_parse_if(cast(str, ctx.verb.parameter)): + ctx.response.body = ctx.verb.payload if ctx.verb.payload != None else "" # noqa: E711 return "" diff --git a/TagScriptEngine/block/case.py b/TagScriptEngine/block/case.py index 3e82835..99224e3 100644 --- a/TagScriptEngine/block/case.py +++ b/TagScriptEngine/block/case.py @@ -1,6 +1,14 @@ +from __future__ import annotations + +from typing import Tuple + from ..interface import Block from ..interpreter import Context + +__all__: Tuple[str, ...] = ("UpperBlock", "LowerBlock") + + class UpperBlock(Block): """Converts the given text to uppercase. @@ -21,12 +29,13 @@ class UpperBlock(Block): # You have entered HELLO WORLD! """ - ACCEPTED_NAMES = ("upper","uppercase") + ACCEPTED_NAMES: Tuple[str, ...] = ("upper", "uppercase") def process(self, ctx: Context) -> str: text = str(ctx.verb.parameter).upper() return "" if text == "NONE" else text + class LowerBlock(Block): """Converts the given text to lowercase. @@ -47,8 +56,8 @@ class LowerBlock(Block): # You have entered hello world! """ - ACCEPTED_NAMES = ("lower","lowercase") + ACCEPTED_NAMES: Tuple[str, ...] = ("lower", "lowercase") def process(self, ctx: Context) -> str: text = str(ctx.verb.parameter).lower() - return "" if text == "none" else text \ No newline at end of file + return "" if text == "none" else text diff --git a/TagScriptEngine/block/command.py b/TagScriptEngine/block/command.py index a216c8a..4e505a1 100644 --- a/TagScriptEngine/block/command.py +++ b/TagScriptEngine/block/command.py @@ -1,10 +1,19 @@ -from typing import Optional +from __future__ import annotations + +import asyncio +from types import TracebackType +from typing import Any, Awaitable, Generator, Iterator, List, Optional, Tuple, Type, TypeVar, cast from ..interface import Block, verb_required_block from ..interpreter import Context +T = TypeVar("T") + + +__all__: Tuple[str, ...] = ("CommandBlock", "OverrideBlock", "SequentialGather") -class CommandBlock(verb_required_block(True, payload=True)): + +class CommandBlock(cast(Type[Block], verb_required_block(True, payload=True))): """ Run a command as if the tag invoker had ran it. Only 3 command blocks can be used in a tag. @@ -26,14 +35,14 @@ class CommandBlock(verb_required_block(True, payload=True)): # invokes ban command on the pinged user with the reason as "Chatflood/spam" """ - ACCEPTED_NAMES = ("c", "com", "command") + ACCEPTED_NAMES: Tuple[str, ...] = ("c", "com", "command") def __init__(self, limit: int = 3): self.limit = limit super().__init__() def process(self, ctx: Context) -> Optional[str]: - command = ctx.verb.payload.strip() + command = ctx.verb.payload.strip() # type: ignore actions = ctx.response.actions.get("commands") if actions: if len(actions) >= self.limit: @@ -75,7 +84,7 @@ class OverrideBlock(Block): # overrides commands that require the mod role or have user permission requirements """ - ACCEPTED_NAMES = ("override",) + ACCEPTED_NAMES: Tuple[str, ...] = ("override",) def process(self, ctx: Context) -> Optional[str]: param = ctx.verb.parameter @@ -92,3 +101,41 @@ def process(self, ctx: Context) -> Optional[str]: overrides[param] = True ctx.response.actions["overrides"] = overrides return "" + + +class SequentialGather(Awaitable[T]): + """ + Use this to run commands sequentially. + + Parameters + ---------- + awaitables : Tuple[Awaitable[T]] + the awaitables to be run sequentially. + + Returns + ------- + `List[T]` + the result object. + """ + + def __init__(self, *awaitables: Awaitable[T]) -> None: + self.__awaitables: Tuple[Awaitable[T], ...] = awaitables + self.__iterator: Iterator[Awaitable[T]] = iter(self.__awaitables) + self.__results: List[T] = [] + self.__lock: asyncio.Lock = asyncio.Lock() + + def __await__(self) -> Generator[Any, None, List[T]]: + return self.__aenter__().__await__() + + async def __aenter__(self) -> List[T]: + async with self.__lock: + for coro in self.__iterator: + await asyncio.sleep(0.10) + result: T = await coro + self.__results.append(result) + return self.__results + + async def __aexit__( + self, exc_type: Type[BaseException], exc_value: BaseException, traceback: TracebackType + ) -> None: + pass diff --git a/TagScriptEngine/block/comment.py b/TagScriptEngine/block/comment.py new file mode 100644 index 0000000..8927031 --- /dev/null +++ b/TagScriptEngine/block/comment.py @@ -0,0 +1,29 @@ +from typing import Optional, Tuple +from ..interface import Block +from ..interpreter import Context + + +class CommentBlock(Block): + """ + The comment block is just for comments, it will not be parsed, + however it will be removed from your tag's output. + + **Usage:** ``{comment([other]):[text]}`` + + **Aliases:** /, Comment, comment, //, # + + **Payload:** ``text`` + + **Parameter:** ``other`` + + .. tagscript:: + + {#:Comment!} + + {Comment(Something):Comment!} + """ + + ACCEPTED_NAMES: Tuple[str, ...] = ("/", "Comment", "comment", "//", "#") + + def process(self, ctx: Context) -> Optional[str]: + return "" diff --git a/TagScriptEngine/block/control.py b/TagScriptEngine/block/control.py index 4902584..b379a88 100644 --- a/TagScriptEngine/block/control.py +++ b/TagScriptEngine/block/control.py @@ -1,16 +1,21 @@ -from typing import Optional +from __future__ import annotations -from ..interface import verb_required_block +from typing import Optional, Tuple, Type, cast + +from ..interface import verb_required_block, Block from ..interpreter import Context from . import helper_parse_if, helper_parse_list_if, helper_split +__all__: Tuple[str, ...] = ("AnyBlock", "AllBlock", "IfBlock") + + def parse_into_output(payload: str, result: Optional[bool]) -> Optional[str]: if result is None: return try: output = helper_split(payload, False) - if output != None and len(output) == 2: + if output is not None and len(output) == 2: if result: return output[0] else: @@ -19,14 +24,14 @@ def parse_into_output(payload: str, result: Optional[bool]) -> Optional[str]: return payload else: return "" - except: + except: # noqa: E722 return -ImplicitPPRBlock = verb_required_block(True, payload=True, parameter=True) +ImplicitPPRBlock: Block = verb_required_block(True, payload=True, parameter=True) -class AnyBlock(ImplicitPPRBlock): +class AnyBlock(cast(Type[Block], ImplicitPPRBlock)): """ The any block checks that any of the passed expressions are true. Multiple expressions can be passed to the parameter by splitting them with ``|``. @@ -52,14 +57,14 @@ class AnyBlock(ImplicitPPRBlock): How rude. """ - ACCEPTED_NAMES = ("any", "or") + ACCEPTED_NAMES: Tuple[str, ...] = ("any", "or") def process(self, ctx: Context) -> Optional[str]: result = any(helper_parse_list_if(ctx.verb.parameter) or []) - return parse_into_output(ctx.verb.payload, result) + return parse_into_output(cast(str, ctx.verb.payload), result) -class AllBlock(ImplicitPPRBlock): +class AllBlock(cast(Type[Block], ImplicitPPRBlock)): """ The all block checks that all of the passed expressions are true. Multiple expressions can be passed to the parameter by splitting them with ``|``. @@ -85,14 +90,14 @@ class AllBlock(ImplicitPPRBlock): You picked 282. """ - ACCEPTED_NAMES = ("all", "and") + ACCEPTED_NAMES: Tuple[str, ...] = ("all", "and") def process(self, ctx: Context) -> Optional[str]: result = all(helper_parse_list_if(ctx.verb.parameter) or []) - return parse_into_output(ctx.verb.payload, result) + return parse_into_output(cast(str, ctx.verb.payload), result) -class IfBlock(ImplicitPPRBlock): +class IfBlock(cast(Type[Block], ImplicitPPRBlock)): """ The if block returns a message based on the passed expression to the parameter. An expression is represented by two values compared with an operator. @@ -137,8 +142,8 @@ class IfBlock(ImplicitPPRBlock): # Too high, try again. """ - ACCEPTED_NAMES = ("if",) + ACCEPTED_NAMES: Tuple[str, ...] = ("if",) def process(self, ctx: Context) -> Optional[str]: - result = helper_parse_if(ctx.verb.parameter) - return parse_into_output(ctx.verb.payload, result) + result = helper_parse_if(cast(str, ctx.verb.parameter)) + return parse_into_output(cast(str, ctx.verb.payload), result) diff --git a/TagScriptEngine/block/cooldown.py b/TagScriptEngine/block/cooldown.py index d86dfca..785b9f3 100644 --- a/TagScriptEngine/block/cooldown.py +++ b/TagScriptEngine/block/cooldown.py @@ -1,17 +1,19 @@ +from __future__ import annotations + import time -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional, Tuple, Type, cast -from discord.ext.commands import CooldownMapping +from discord.ext.commands import Cooldown, CooldownMapping from ..exceptions import CooldownExceeded from ..interface import verb_required_block -from ..interpreter import Context +from ..interpreter import Context, Block from .helpers import helper_split -__all__ = ("CooldownBlock",) +__all__: Tuple[str, ...] = ("CooldownBlock",) -class CooldownBlock(verb_required_block(True, payload=True, parameter=True)): +class CooldownBlock(cast(Type[Block], verb_required_block(True, payload=True, parameter=True))): """ The cooldown block implements cooldowns when running a tag. The parameter requires 2 values to be passed: ``rate`` and ``per`` integers. @@ -43,11 +45,11 @@ class CooldownBlock(verb_required_block(True, payload=True, parameter=True)): # Slow down! This tag can only be used 3 times per 3 seconds per channel. Try again in **0.74** seconds. """ - ACCEPTED_NAMES = ("cooldown",) + ACCEPTED_NAMES: Tuple[str, ...] = ("cooldown",) COOLDOWNS: Dict[Any, CooldownMapping] = {} @classmethod - def create_cooldown(cls, key: Any, rate: int, per: int) -> CooldownMapping: + def create_cooldown(cls, key: Any, rate: float, per: int) -> CooldownMapping: cooldown = CooldownMapping.from_cooldown(rate, per, lambda x: x) cls.COOLDOWNS[key] = cooldown return cooldown @@ -55,13 +57,13 @@ def create_cooldown(cls, key: Any, rate: int, per: int) -> CooldownMapping: def process(self, ctx: Context) -> Optional[str]: verb = ctx.verb try: - rate, per = helper_split(verb.parameter, maxsplit=1) + rate, per = cast(List[str], helper_split(cast(str, verb.parameter), maxsplit=1)) per = int(per) rate = float(rate) except (ValueError, TypeError): return - if split := helper_split(verb.payload, False, maxsplit=1): + if split := helper_split(cast(str, verb.payload), False, maxsplit=1): key, message = split else: key = verb.payload @@ -72,14 +74,14 @@ def process(self, ctx: Context) -> Optional[str]: cooldown_key = ctx.original_message try: cooldown = self.COOLDOWNS[cooldown_key] - base = cooldown._cooldown + base = cast(Cooldown, cooldown._cooldown) if (rate, per) != (base.rate, base.per): cooldown = self.create_cooldown(cooldown_key, rate, per) except KeyError: cooldown = self.create_cooldown(cooldown_key, rate, per) current = time.time() - bucket = cooldown.get_bucket(key, current) + bucket = cast(Cooldown, cooldown.get_bucket(key, current)) retry_after = bucket.update_rate_limit(current) if retry_after: retry_after = round(retry_after, 2) @@ -89,5 +91,5 @@ def process(self, ctx: Context) -> Optional[str]: ) else: message = f"The bucket for {key} has reached its cooldown. Retry in {retry_after} seconds." - raise CooldownExceeded(message, bucket, key, retry_after) + raise CooldownExceeded(message, bucket, cast(str, key), retry_after) return "" diff --git a/TagScriptEngine/block/count.py b/TagScriptEngine/block/count.py index a20dd2e..0a7f21a 100644 --- a/TagScriptEngine/block/count.py +++ b/TagScriptEngine/block/count.py @@ -1,10 +1,15 @@ -from typing import Optional +from __future__ import annotations + +from typing import Optional, Tuple, Type, cast from ..interface import verb_required_block -from ..interpreter import Context +from ..interpreter import Context, Block + + +__all__: Tuple[str, ...] = ("CountBlock", "LengthBlock") -class CountBlock(verb_required_block(True, payload=True)): +class CountBlock(cast(Type[Block], verb_required_block(True, payload=True))): """ The count block will count how much of text is in message. This is case sensitive and will include substrings, if you @@ -27,15 +32,16 @@ class CountBlock(verb_required_block(True, payload=True)): # 2 """ - ACCEPTED_NAMES = ("count",) + ACCEPTED_NAMES: Tuple[str, ...] = ("count",) def process(self, ctx: Context) -> Optional[str]: if ctx.verb.parameter: - return ctx.verb.payload.count(ctx.verb.parameter) - return len(ctx.verb.payload) + 1 + payload: str = cast(str, ctx.verb.payload) + return str(payload.count(ctx.verb.parameter)) + return str(len(cast(str, ctx.verb.payload)) + 1) -class LengthBlock(verb_required_block(True, payload=True)): +class LengthBlock(cast(Type[Block], verb_required_block(True, payload=True))): """ The length block will check the length of the given String. If a parameter is passed in, the block will check the length @@ -56,7 +62,7 @@ class LengthBlock(verb_required_block(True, payload=True)): 15 """ - ACCEPTED_NAMES = ("length", "len") + ACCEPTED_NAMES: Tuple[str, ...] = ("length", "len") def process(self, ctx: Context) -> Optional[str]: return str(len(ctx.verb.parameter)) if ctx.verb.parameter else "-1" diff --git a/TagScriptEngine/block/embedblock.py b/TagScriptEngine/block/embedblock.py index 49e3602..3711f01 100644 --- a/TagScriptEngine/block/embedblock.py +++ b/TagScriptEngine/block/embedblock.py @@ -1,13 +1,32 @@ +from __future__ import annotations + import json from inspect import ismethod -from typing import Optional, Union +from typing import Any, Dict, List, Optional, Tuple, Union, cast from discord import Colour, Embed -from ..exceptions import BadColourArgument, EmbedParseError from ..interface import Block from ..interpreter import Context -from .helpers import helper_split, implicit_bool +from .helpers import helper_split, easier_helper_split, implicit_bool +from ..exceptions import BadColourArgument, EmbedParseError +from .._warnings import removal + +try: + import orjson # noqa: F401 +except ModuleNotFoundError: + _has_orjson: bool = False +else: + _has_orjson: bool = True + + +__all__: Tuple[str, ...] = ("EmbedBlock",) + + +if _has_orjson: + _from_json = orjson.loads # type: ignore +else: + _from_json = json.loads def string_to_color(argument: str) -> Colour: @@ -28,18 +47,18 @@ def string_to_color(argument: str) -> Colour: return method() -def set_color(embed: Embed, attribute: str, value: str): - value = string_to_color(value) +def set_color(embed: Embed, attribute: str, value: str) -> None: + value = string_to_color(value) # type: ignore setattr(embed, attribute, value) -def set_dynamic_url(embed: Embed, attribute: str, value: str): +def set_dynamic_url(embed: Embed, attribute: str, value: str) -> None: method = getattr(embed, f"set_{attribute}") method(url=value) -def add_field(embed: Embed, _: str, payload: str): - if (data := helper_split(payload, 3)) is None: +def add_field(embed: Embed, _: str, payload: str) -> None: + if (data := easier_helper_split(payload, maxsplit=3)) is None: # type: ignore raise EmbedParseError("`add_field` payload was not split by |") try: name, value, _inline = data @@ -49,13 +68,13 @@ def add_field(embed: Embed, _: str, payload: str): "`inline` argument for `add_field` is not a boolean value (_inline)" ) except ValueError: - name, value = helper_split(payload, 2) + name, value = cast(List[str], helper_split(payload, 2)) # type: ignore inline = False embed.add_field(name=name, value=value, inline=inline) -def set_footer(embed: Embed, _: str, payload: str): - data = helper_split(payload, 2) +def set_footer(embed: Embed, _: str, payload: str) -> None: + data = easier_helper_split(payload, maxsplit=2) # type: ignore if data is None: embed.set_footer(text=payload) else: @@ -104,10 +123,10 @@ class EmbedBlock(Block): * ``footer`` * ``field`` - (See below) - Adding a field to an embed requires the payload to be split by ``|``, into - either 2 or 3 parts. The first part is the name of the field, the second is - the text of the field, and the third optionally specifies whether the field - should be inline. + Adding a field to an embed requires the payload to be split by ``|``, + ``;`` or ``,`` into either 2 or 3 parts. The first part is the name + of the field, the second is the text of the field, and the third + optionally specifies whether the field should be inline. **Usage:** ``{embed():}`` @@ -129,13 +148,21 @@ class EmbedBlock(Block): :: - {embed({{"fields":[{"name":"Field 1","value":"field description","inline":false}]})} {embed(title):my embed title} + {embed({{ + "fields": [ + { + "name": "Field 1", + "value": "field description", + "inline": false + } + ] + })} """ - ACCEPTED_NAMES = ("embed",) + ACCEPTED_NAMES: Tuple[str, ...] = ("embed",) - ATTRIBUTE_HANDLERS = { + ATTRIBUTE_HANDLERS: Dict[str, Any] = { "description": setattr, "title": setattr, "color": set_color, @@ -147,6 +174,20 @@ class EmbedBlock(Block): "footer": set_footer, } + @removal( + name="EmbedBlock", + reason=( + "One of EmbedBlock's trait is scheduled to be removed in the next minor release, " + "A minor exception handling which would restrict the embed from getting sent and " + "would raise TagScriptEngine.exceptions.EmbedParseError incase it had more than " + "6000 characters, to know more about the limitations of discord embeds refer to the " + "[Official Discord API Docs](https://discord.com/developers/docs/resources/channel#embed-object-embed-limits)." + ), + version="3.2.0", + ) + def __init__(self) -> None: + super().__init__() + @staticmethod def get_embed(ctx: Context) -> Embed: return ctx.response.actions.get("embed", Embed()) @@ -154,7 +195,7 @@ def get_embed(ctx: Context) -> Embed: @staticmethod def value_to_color(value: Optional[Union[int, str]]) -> Colour: if value is None or isinstance(value, Colour): - return value + return value # type: ignore if isinstance(value, int): return Colour(value) elif isinstance(value, str): @@ -164,8 +205,8 @@ def value_to_color(value: Optional[Union[int, str]]) -> Colour: def text_to_embed(self, text: str) -> Embed: try: - data = json.loads(text) - except json.decoder.JSONDecodeError as error: + data = _from_json(text) + except (json.decoder.JSONDecodeError, ValueError) as error: raise EmbedParseError(error) from error if data.get("embed"): diff --git a/TagScriptEngine/block/fiftyfifty.py b/TagScriptEngine/block/fiftyfifty.py index 35ef954..d4b66c5 100644 --- a/TagScriptEngine/block/fiftyfifty.py +++ b/TagScriptEngine/block/fiftyfifty.py @@ -1,11 +1,16 @@ +from __future__ import annotations + import random -from typing import Optional +from typing import Optional, Tuple, Type, cast from ..interface import verb_required_block -from ..interpreter import Context +from ..interpreter import Context, Block + + +__all__: Tuple[str, ...] = ("FiftyFiftyBlock",) -class FiftyFiftyBlock(verb_required_block(True, payload=True)): +class FiftyFiftyBlock(cast(Type[Block], verb_required_block(True, payload=True))): """ The fifty-fifty block has a 50% change of returning the payload, and 50% chance of returning null. @@ -23,7 +28,7 @@ class FiftyFiftyBlock(verb_required_block(True, payload=True)): # I pick heads """ - ACCEPTED_NAMES = ("5050", "50", "?") + ACCEPTED_NAMES: Tuple[str, ...] = ("5050", "50", "?") def process(self, ctx: Context) -> Optional[str]: return random.choice(["", ctx.verb.payload]) diff --git a/TagScriptEngine/block/helpers.py b/TagScriptEngine/block/helpers.py index c147dc7..dbc9801 100644 --- a/TagScriptEngine/block/helpers.py +++ b/TagScriptEngine/block/helpers.py @@ -1,10 +1,35 @@ -import re -from typing import List, Optional - -__all__ = ("implicit_bool", "helper_parse_if", "helper_split", "helper_parse_list_if") +from __future__ import annotations -SPLIT_REGEX = re.compile(r"(? Optional[bool]: @@ -82,12 +107,12 @@ def helper_parse_if(string: str) -> Optional[bool]: if "<" in string: spl = string.split("<") return float(spl[0].strip()) < float(spl[1].strip()) - except: + except Exception: pass def helper_split( - split_string: str, easy: bool = True, *, maxsplit: int = None + split_string: str, easy: bool = True, *, maxsplit: Optional[int] = None ) -> Optional[List[str]]: """ A helper method to universalize the splitting logic used in multiple @@ -108,6 +133,30 @@ def helper_split( return +def easier_helper_split( + split_string: str, *, maxsplit: Optional[int] = None +) -> Optional[List[str]]: + """ + A helper method to universalize the splitting logic used in blocks + and adapters. Please use this wherever a verb needs content to be + chopped at `|`, `,` or `;`. + + >>> easier_helper_split("this, should|work") + ["this, should", "work"] + + >>> easier_helper_split("this, should;work~as well") + ["this, should", "work", "as well"] + """ + args = (maxsplit,) if maxsplit is not None else () + if "|" in split_string: + return SPLIT_REGEX.split(split_string, *args) + if "~" in split_string: + return EASIER_SPLIT_REGEX.split(split_string, *args) + if ";" in split_string: + return EASIER_SPLIT_REGEX.split(split_string, *args) + return + + def helper_parse_list_if(if_string): split = helper_split(if_string, False) if split is None: diff --git a/TagScriptEngine/block/loosevariablegetter.py b/TagScriptEngine/block/loosevariablegetter.py index 116a86e..cefb9d4 100644 --- a/TagScriptEngine/block/loosevariablegetter.py +++ b/TagScriptEngine/block/loosevariablegetter.py @@ -1,9 +1,14 @@ -from typing import Optional +from __future__ import annotations + +from typing import Optional, Tuple from ..interface import Block from ..interpreter import Context +__all__: Tuple[str, ...] = ("LooseVariableGetterBlock",) + + class LooseVariableGetterBlock(Block): """ The loose variable block represents the adapters for any seeded or defined variables. @@ -25,7 +30,7 @@ class LooseVariableGetterBlock(Block): # This is my variable. """ - def will_accept(self, ctx: Context) -> bool: + def will_accept(self, ctx: Context) -> bool: # type: ignore return True def process(self, ctx: Context) -> Optional[str]: diff --git a/TagScriptEngine/block/mathblock.py b/TagScriptEngine/block/mathblock.py index 48db63e..a5d7905 100644 --- a/TagScriptEngine/block/mathblock.py +++ b/TagScriptEngine/block/mathblock.py @@ -1,8 +1,8 @@ -from __future__ import division +from __future__ import division, annotations import math import operator -from typing import Optional +from typing import Any, Callable, Dict, List, Tuple, cast, Optional as TypingOptional from pyparsing import ( CaselessLiteral, @@ -11,6 +11,7 @@ Group, Literal, Optional, + ParserElement, Word, ZeroOrMore, alphas, @@ -22,20 +23,23 @@ from ..interpreter import Context +__all__: Tuple[str, ...] = ("MathBlock",) + + class NumericStringParser(object): """ Most of this code comes from the fourFn.py pyparsing example """ - def pushFirst(self, strg, loc, toks): + def pushFirst(self, strg: Any, loc: Any, toks: Any) -> Any: self.exprStack.append(toks[0]) - def pushUMinus(self, strg, loc, toks): + def pushUMinus(self, strg: Any, loc: Any, toks: Any) -> Any: if toks and toks[0] == "-": self.exprStack.append("unary -") - def __init__(self): + def __init__(self) -> None: """ expop :: '^' multop :: '*' | '/' @@ -46,32 +50,32 @@ def __init__(self): term :: factor [ multop factor ]* expr :: term [ addop term ]* """ - point = Literal(".") - e = CaselessLiteral("E") - fnumber = Combine( + point: Literal = Literal(".") + e: CaselessLiteral = CaselessLiteral("E") + fnumber: Combine = Combine( Word("+-" + nums, nums) + Optional(point + Optional(Word(nums))) + Optional(e + Word("+-" + nums, nums)) ) - ident = Word(alphas, alphas + nums + "_$") - mod = Literal("%") - plus = Literal("+") - minus = Literal("-") - mult = Literal("*") - iadd = Literal("+=") - imult = Literal("*=") - idiv = Literal("/=") - isub = Literal("-=") - div = Literal("/") - lpar = Literal("(").suppress() - rpar = Literal(")").suppress() - addop = plus | minus - multop = mult | div | mod - iop = iadd | isub | imult | idiv - expop = Literal("^") - pi = CaselessLiteral("PI") - expr = Forward() - atom = ( + ident: Word = Word(alphas, alphas + nums + "_$") + mod: Literal = Literal("%") + plus: Literal = Literal("+") + minus: Literal = Literal("-") + mult: Literal = Literal("*") + iadd: Literal = Literal("+=") + imult: Literal = Literal("*=") + idiv: Literal = Literal("/=") + isub: Literal = Literal("-=") + div: Literal = Literal("/") + lpar: ParserElement = Literal("(").suppress() + rpar: ParserElement = Literal(")").suppress() + addop: ParserElement = plus | minus + multop: ParserElement = mult | div | mod + iop: ParserElement = iadd | isub | imult | idiv + expop: Literal = Literal("^") + pi: CaselessLiteral = CaselessLiteral("PI") + expr: Forward = Forward() + atom: ParserElement = ( ( Optional(oneOf("- +")) + (ident + lpar + expr + rpar | pi | e | fnumber).setParseAction(self.pushFirst) @@ -81,18 +85,18 @@ def __init__(self): # by defining exponentiation as "atom [ ^ factor ]..." instead of # "atom [ ^ atom ]...", we get right-to-left exponents, instead of left-to-right # that is, 2^3^2 = 2^(3^2), not (2^3)^2. - factor = Forward() - factor << atom + ZeroOrMore((expop + factor).setParseAction(self.pushFirst)) - term = factor + ZeroOrMore((multop + factor).setParseAction(self.pushFirst)) - expr << term + ZeroOrMore((addop + term).setParseAction(self.pushFirst)) - final = expr + ZeroOrMore((iop + expr).setParseAction(self.pushFirst)) + factor: Forward = Forward() + factor << atom + ZeroOrMore((expop + factor).setParseAction(self.pushFirst)) # type: ignore + term: ParserElement = factor + ZeroOrMore((multop + factor).setParseAction(self.pushFirst)) + expr << term + ZeroOrMore((addop + term).setParseAction(self.pushFirst)) # type: ignore + final: ParserElement = expr + ZeroOrMore((iop + expr).setParseAction(self.pushFirst)) # addop_term = ( addop + term ).setParseAction( self.pushFirst ) # general_term = term + ZeroOrMore( addop_term ) | OneOrMore( addop_term) # expr << general_term - self.bnf = final + self.bnf: ParserElement = final # map operator symbols to corresponding arithmetic operations - epsilon = 1e-12 - self.opn = { + epsilon: float = 1e-12 + self.opn: Dict[str, Callable[[Any, Any], Any]] = { "+": operator.add, "-": operator.sub, "+=": operator.iadd, @@ -104,7 +108,7 @@ def __init__(self): "^": operator.pow, "%": operator.mod, } - self.fn = { + self.fn: Dict[str, Any] = { "sin": math.sin, "cos": math.cos, "tan": math.tan, @@ -122,7 +126,7 @@ def __init__(self): "sqrt": math.sqrt, } - def evaluateStack(self, s): + def evaluateStack(self, s: List[Any]) -> Any: op = s.pop() if op == "unary -": return -self.evaluateStack(s) @@ -141,20 +145,20 @@ def evaluateStack(self, s): else: return float(op) - def eval(self, num_string, parseAll=True): + def eval(self, num_string: str, parseAll: bool = True) -> Any: self.exprStack = [] - results = self.bnf.parseString(num_string, parseAll) + results = self.bnf.parseString(num_string, parseAll) # noqa: F841 return self.evaluateStack(self.exprStack[:]) -NSP = NumericStringParser() +NSP: NumericStringParser = NumericStringParser() class MathBlock(Block): - ACCEPTED_NAMES = ("math", "m", "+", "calc") + ACCEPTED_NAMES: Tuple[str, ...] = ("math", "m", "+", "calc") - def process(self, ctx: Context): + def process(self, ctx: Context) -> TypingOptional[str]: try: - return str(NSP.eval(ctx.verb.payload.strip(" "))) - except: + return str(NSP.eval(cast(str, ctx.verb.payload).strip(" "))) + except Exception: return None diff --git a/TagScriptEngine/block/randomblock.py b/TagScriptEngine/block/randomblock.py index 44fabd5..e2a05bd 100644 --- a/TagScriptEngine/block/randomblock.py +++ b/TagScriptEngine/block/randomblock.py @@ -1,11 +1,16 @@ +from __future__ import annotations + import random -from typing import Optional +from typing import Optional, Tuple, Type, cast -from ..interface import verb_required_block +from ..interface import verb_required_block, Block from ..interpreter import Context -class RandomBlock(verb_required_block(True, payload=True)): +__all__: Tuple[str, ...] = ("RandomBlock",) + + +class RandomBlock(cast(Type[Block], verb_required_block(True, payload=True))): """ Pick a random item from a list of strings, split by either ``~`` or ``,``. An optional seed can be provided to the parameter to @@ -33,14 +38,14 @@ class RandomBlock(verb_required_block(True, payload=True)): # Assigns a random insult to the insult variable """ - ACCEPTED_NAMES = ("random", "#", "rand") + ACCEPTED_NAMES: Tuple[str, ...] = ("random", "#", "rand") def process(self, ctx: Context) -> Optional[str]: spl = [] - if "~" in ctx.verb.payload: - spl = ctx.verb.payload.split("~") + if "~" in (payload := cast(str, ctx.verb.payload)): + spl = payload.split("~") else: - spl = ctx.verb.payload.split(",") + spl = payload.split(",") random.seed(ctx.verb.parameter) return random.choice(spl) diff --git a/TagScriptEngine/block/range.py b/TagScriptEngine/block/range.py index b3bfb3b..cbf756b 100644 --- a/TagScriptEngine/block/range.py +++ b/TagScriptEngine/block/range.py @@ -1,11 +1,16 @@ +from __future__ import annotations + import random -from typing import Optional +from typing import Optional, Tuple, Type, cast -from ..interface import verb_required_block +from ..interface import verb_required_block, Block from ..interpreter import Context -class RangeBlock(verb_required_block(True, payload=True)): +__all__: Tuple[str, ...] = ("RangeBlock",) + + +class RangeBlock(cast(Type[Block], verb_required_block(True, payload=True))): """ The range block picks a random number from a range of numbers seperated by ``-``. The number range is inclusive, so it can pick the starting/ending number as well. @@ -32,28 +37,20 @@ class RangeBlock(verb_required_block(True, payload=True)): # I am guessing your height is 5.3ft. """ - ACCEPTED_NAMES = ("rangef", "range") + ACCEPTED_NAMES: Tuple[str, ...] = ("rangef", "range") def process(self, ctx: Context) -> Optional[str]: try: - spl = ctx.verb.payload.split("-") + spl = cast(str, ctx.verb.payload).split("-") random.seed(ctx.verb.parameter) - if ctx.verb.declaration.lower() == "rangef": - lower = float(spl[0]) - upper = float(spl[1]) - base = random.randint(lower * 10, upper * 10) / 10 + if cast(str, ctx.verb.declaration).lower() == "rangef": + lower: float = float(spl[0]) + upper: float = float(spl[1]) + base: float = random.randint(int(lower) * 10, int(upper) * 10) / 10 return str(base) - # base = random.randint(lower, upper) - # if base == upper: - # return str(base) - # if ctx.verb.parameter != None: - # random.seed(ctx.verb.parameter+"float") - # else: - # random.seed(None) - # return str(str(base)+"."+str(random.randint(1,9))) else: lower = int(float(spl[0])) upper = int(float(spl[1])) return str(random.randint(lower, upper)) - except: + except Exception: return None diff --git a/TagScriptEngine/block/redirect.py b/TagScriptEngine/block/redirect.py index a195cb3..de35ae6 100644 --- a/TagScriptEngine/block/redirect.py +++ b/TagScriptEngine/block/redirect.py @@ -1,10 +1,15 @@ -from typing import Optional +from __future__ import annotations -from ..interface import verb_required_block +from typing import Optional, Tuple, Type, cast + +from ..interface import verb_required_block, Block from ..interpreter import Context -class RedirectBlock(verb_required_block(True, parameter=True)): +__all__: Tuple[str, ...] = ("RedirectBlock",) + + +class RedirectBlock(cast(Type[Block], verb_required_block(True, parameter=True))): """ Redirects the tag response to either the given channel, the author's DMs, or uses a reply based on what is passed to the parameter. @@ -26,11 +31,11 @@ class RedirectBlock(verb_required_block(True, parameter=True)): ACCEPTED_NAMES = ("redirect",) def process(self, ctx: Context) -> Optional[str]: - param = ctx.verb.parameter.strip() + param: str = cast(str, ctx.verb.parameter).strip() if param.lower() == "dm": - target = "dm" + target: str = "dm" elif param.lower() == "reply": - target = "reply" + target: str = "reply" else: target = param ctx.response.actions["target"] = target diff --git a/TagScriptEngine/block/replaceblock.py b/TagScriptEngine/block/replaceblock.py index d1d83f3..c24a8e5 100644 --- a/TagScriptEngine/block/replaceblock.py +++ b/TagScriptEngine/block/replaceblock.py @@ -1,8 +1,15 @@ -from ..interface import verb_required_block +from __future__ import annotations + +from typing import Optional, Tuple, Type, cast + +from ..interface import verb_required_block, Block from ..interpreter import Context -class ReplaceBlock(verb_required_block(True, payload=True, parameter=True)): +__all__: Tuple[str, ...] = ("ReplaceBlock", "PythonBlock") + + +class ReplaceBlock(cast(Type[Block], verb_required_block(True, payload=True, parameter=True))): """ The replace block will replace specific characters in a string. The parameter should split by a ``,``, containing the characters to find @@ -29,18 +36,18 @@ class ReplaceBlock(verb_required_block(True, payload=True, parameter=True)): # T e s t """ - ACCEPTED_NAMES = ("replace",) + ACCEPTED_NAMES: Tuple[str, ...] = ("replace",) - def process(self, ctx: Context): + def process(self, ctx: Context) -> Optional[str]: try: - before, after = ctx.verb.parameter.split(",", 1) + before, after = cast(str, ctx.verb.parameter).split(",", 1) except ValueError: return - return ctx.verb.payload.replace(before, after) + return cast(str, ctx.verb.payload).replace(before, after) -class PythonBlock(verb_required_block(True, payload=True, parameter=True)): +class PythonBlock(cast(Type[Block], verb_required_block(True, payload=True, parameter=True))): """ The in block serves three different purposes depending on the alias that is used. @@ -81,18 +88,23 @@ class PythonBlock(verb_required_block(True, payload=True, parameter=True)): # -1 """ - def will_accept(self, ctx: Context): - dec = ctx.verb.declaration.lower() + def will_accept(self, ctx: Context) -> bool: # type: ignore + dec = cast(str, ctx.verb.declaration).lower() return dec in ("contains", "in", "index") - def process(self, ctx: Context): - dec = ctx.verb.declaration.lower() + def process(self, ctx: Context) -> str: + dec: str = cast(str, ctx.verb.declaration).lower() if dec == "contains": - return str(bool(ctx.verb.parameter in ctx.verb.payload.split())).lower() + return str(bool(ctx.verb.parameter in cast(str, ctx.verb.payload).split())).lower() elif dec == "in": - return str(bool(ctx.verb.parameter in ctx.verb.payload)).lower() + return str(bool(cast(str, ctx.verb.parameter) in cast(str, ctx.verb.payload))).lower() else: try: - return str(ctx.verb.payload.strip().split().index(ctx.verb.parameter)) + return str( + cast(str, ctx.verb.payload) + .strip() + .split() + .index(cast(str, ctx.verb.parameter)) + ) except ValueError: return "-1" diff --git a/TagScriptEngine/block/require_blacklist.py b/TagScriptEngine/block/require_blacklist.py index 27a15d2..513f820 100644 --- a/TagScriptEngine/block/require_blacklist.py +++ b/TagScriptEngine/block/require_blacklist.py @@ -1,10 +1,15 @@ -from typing import Optional +from __future__ import annotations -from ..interface import verb_required_block +from typing import Optional, Tuple, Type, cast + +from ..interface import verb_required_block, Block from ..interpreter import Context -class RequireBlock(verb_required_block(True, parameter=True)): +__all__: Tuple[str, ...] = ("RequireBlock", "BlacklistBlock") + + +class RequireBlock(cast(Type[Block], verb_required_block(True, parameter=True))): """ The require block will attempt to convert the given parameter into a channel or role, using name or ID. If the user running the tag is not in the targeted @@ -27,20 +32,20 @@ class RequireBlock(verb_required_block(True, parameter=True)): {require(757425366209134764, 668713062186090506, 737961895356792882):You aren't allowed to use this tag.} """ - ACCEPTED_NAMES = ("require", "whitelist") + ACCEPTED_NAMES: Tuple[str, ...] = ("require", "whitelist") def process(self, ctx: Context) -> Optional[str]: actions = ctx.response.actions.get("requires") if actions: return None ctx.response.actions["requires"] = { - "items": [i.strip() for i in ctx.verb.parameter.split(",")], + "items": [i.strip() for i in cast(str, ctx.verb.parameter).split(",")], "response": ctx.verb.payload, } return "" -class BlacklistBlock(verb_required_block(True, parameter=True)): +class BlacklistBlock(cast(Type[Block], verb_required_block(True, parameter=True))): """ The blacklist block will attempt to convert the given parameter into a channel or role, using name or ID. If the user running the tag is in the targeted @@ -61,14 +66,14 @@ class BlacklistBlock(verb_required_block(True, parameter=True)): {blacklist(Tag Blacklist, 668713062186090506):You are blacklisted from using tags.} """ - ACCEPTED_NAMES = ("blacklist",) + ACCEPTED_NAMES: Tuple[str, ...] = ("blacklist",) def process(self, ctx: Context) -> Optional[str]: actions = ctx.response.actions.get("blacklist") if actions: return None ctx.response.actions["blacklist"] = { - "items": [i.strip() for i in ctx.verb.parameter.split(",")], + "items": [i.strip() for i in cast(str, ctx.verb.parameter).split(",")], "response": ctx.verb.payload, } return "" diff --git a/TagScriptEngine/block/shortcutredirect.py b/TagScriptEngine/block/shortcutredirect.py index d54af48..41042ad 100644 --- a/TagScriptEngine/block/shortcutredirect.py +++ b/TagScriptEngine/block/shortcutredirect.py @@ -1,19 +1,22 @@ -from typing import Optional +from typing import Optional, Tuple, cast from ..interface import Block from ..interpreter import Context from ..verb import Verb +__all__: Tuple[str, ...] = ("ShortCutRedirectBlock",) + + class ShortCutRedirectBlock(Block): - def __init__(self, var_name): - self.redirect_name = var_name + def __init__(self, var_name: str) -> None: + self.redirect_name: str = var_name - def will_accept(self, ctx: Context) -> bool: - return ctx.verb.declaration.isdigit() + def will_accept(self, ctx: Context) -> bool: # type: ignore + return cast(str, ctx.verb.declaration).isdigit() def process(self, ctx: Context) -> Optional[str]: - blank = Verb() + blank: Verb = Verb() blank.declaration = self.redirect_name blank.parameter = ctx.verb.declaration ctx.verb = blank diff --git a/TagScriptEngine/block/stopblock.py b/TagScriptEngine/block/stopblock.py index a4f9525..8e84391 100644 --- a/TagScriptEngine/block/stopblock.py +++ b/TagScriptEngine/block/stopblock.py @@ -1,12 +1,17 @@ -from typing import Optional +from __future__ import annotations + +from typing import Optional, Tuple, Type, cast from ..exceptions import StopError -from ..interface import verb_required_block +from ..interface import verb_required_block, Block from ..interpreter import Context from . import helper_parse_if -class StopBlock(verb_required_block(True, parameter=True)): +__all__: Tuple[str, ...] = ("StopBlock",) + + +class StopBlock(cast(Type[Block], verb_required_block(True, parameter=True))): """ The stop block stops tag processing if the given parameter is true. If a message is passed to the payload it will return that message. @@ -25,9 +30,9 @@ class StopBlock(verb_required_block(True, parameter=True)): # enforces providing arguments for a tag """ - ACCEPTED_NAMES = ("stop", "halt", "error") + ACCEPTED_NAMES: Tuple[str, ...] = ("stop", "halt", "error") def process(self, ctx: Context) -> Optional[str]: - if helper_parse_if(ctx.verb.parameter): + if helper_parse_if(cast(str, ctx.verb.parameter)): raise StopError("" if ctx.verb.payload is None else ctx.verb.payload) return "" diff --git a/TagScriptEngine/block/strf.py b/TagScriptEngine/block/strf.py index 99de583..821bd61 100644 --- a/TagScriptEngine/block/strf.py +++ b/TagScriptEngine/block/strf.py @@ -1,10 +1,15 @@ +from __future__ import annotations + from datetime import datetime, timezone -from typing import Optional +from typing import Optional, Tuple, cast from ..interface import Block from ..interpreter import Context +__all__: Tuple[str, ...] = ("StrfBlock",) + + class StrfBlock(Block): """ The strf block converts and formats timestamps based on `strftime formatting spec `_. @@ -39,10 +44,10 @@ class StrfBlock(Block): # 1629182008 """ - ACCEPTED_NAMES = ("strf", "unix") + ACCEPTED_NAMES: Tuple[str, ...] = ("strf", "unix") def process(self, ctx: Context) -> Optional[str]: - if ctx.verb.declaration.lower() == "unix": + if cast(str, ctx.verb.declaration).lower() == "unix": return str(int(datetime.now(timezone.utc).timestamp())) if not ctx.verb.payload: return None @@ -50,7 +55,7 @@ def process(self, ctx: Context) -> Optional[str]: if ctx.verb.parameter.isdigit(): try: t = datetime.fromtimestamp(int(ctx.verb.parameter)) - except: + except Exception: return else: try: diff --git a/TagScriptEngine/block/strictvariablegetter.py b/TagScriptEngine/block/strictvariablegetter.py index ab324fa..4b0c26a 100644 --- a/TagScriptEngine/block/strictvariablegetter.py +++ b/TagScriptEngine/block/strictvariablegetter.py @@ -1,9 +1,14 @@ -from typing import Optional +from __future__ import annotations + +from typing import Optional, Tuple, cast from ..interface import Block from ..interpreter import Context +__all__: Tuple[str, ...] = ("StrictVariableGetterBlock",) + + class StrictVariableGetterBlock(Block): """ The strict variable block represents the adapters for any seeded or defined variables. @@ -26,8 +31,8 @@ class StrictVariableGetterBlock(Block): # This is my variable. """ - def will_accept(self, ctx: Context) -> bool: + def will_accept(self, ctx: Context) -> bool: # type: ignore return ctx.verb.declaration in ctx.response.variables def process(self, ctx: Context) -> Optional[str]: - return ctx.response.variables[ctx.verb.declaration].get_value(ctx.verb) + return ctx.response.variables[cast(str, ctx.verb.declaration)].get_value(ctx.verb) diff --git a/TagScriptEngine/block/substr.py b/TagScriptEngine/block/substr.py index 1957775..42e85f6 100644 --- a/TagScriptEngine/block/substr.py +++ b/TagScriptEngine/block/substr.py @@ -1,20 +1,25 @@ -from typing import Optional +from __future__ import annotations -from ..interface import verb_required_block +from typing import Optional, Tuple, Type, cast + +from ..interface import verb_required_block, Block from ..interpreter import Context -class SubstringBlock(verb_required_block(True, parameter=True)): - ACCEPTED_NAMES = ("substr", "substring") +__all__: Tuple[str, ...] = ("SubstringBlock",) + + +class SubstringBlock(cast(Type[Block], verb_required_block(True, parameter=True))): + ACCEPTED_NAMES: Tuple[str, ...] = ("substr", "substring") def process(self, ctx: Context) -> Optional[str]: try: - if "-" not in ctx.verb.parameter: - return ctx.verb.payload[int(float(ctx.verb.parameter)) :] + if "-" not in cast(str, ctx.verb.parameter): + return cast(str, ctx.verb.payload)[int(float(cast(str, ctx.verb.parameter))) :] - spl = ctx.verb.parameter.split("-") + spl = cast(str, ctx.verb.parameter).split("-") start = int(float(spl[0])) end = int(float(spl[1])) - return ctx.verb.payload[start:end] - except: + return cast(str, ctx.verb.payload)[start:end] + except Exception: return diff --git a/TagScriptEngine/block/urlencodeblock.py b/TagScriptEngine/block/urlencodeblock.py index 46439cb..31a0d0d 100644 --- a/TagScriptEngine/block/urlencodeblock.py +++ b/TagScriptEngine/block/urlencodeblock.py @@ -1,10 +1,14 @@ +from typing import Tuple, Type, cast from urllib.parse import quote, quote_plus -from ..interface import verb_required_block +from ..interface import verb_required_block, Block from ..interpreter import Context -class URLEncodeBlock(verb_required_block(True, payload=True)): +__all__: Tuple[str, ...] = ("URLEncodeBlock",) + + +class URLEncodeBlock(cast(Type[Block], verb_required_block(True, payload=True))): """ This block will encode a given string into a properly formatted url with non-url compliant characters replaced. Using ``+`` as the parameter @@ -26,12 +30,12 @@ class URLEncodeBlock(verb_required_block(True, payload=True)): # the following tagscript can be used to search up tag blocks # assume {args} = "command block" - - # + # + # """ - ACCEPTED_NAMES = ("urlencode",) + ACCEPTED_NAMES: Tuple[str, ...] = ("urlencode",) - def process(self, ctx: Context): + def process(self, ctx: Context) -> str: method = quote_plus if ctx.verb.parameter == "+" else quote - return method(ctx.verb.payload) + return method(cast(str, ctx.verb.payload)) diff --git a/TagScriptEngine/exceptions.py b/TagScriptEngine/exceptions.py index e7fffc5..1adf49c 100644 --- a/TagScriptEngine/exceptions.py +++ b/TagScriptEngine/exceptions.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Tuple from discord.ext.commands import Cooldown @@ -8,7 +8,7 @@ from .interpreter import Interpreter, Response -__all__ = ( +__all__: Tuple[str, ...] = ( "TagScriptError", "WorkloadExceededError", "ProcessError", @@ -41,7 +41,7 @@ class ProcessError(TagScriptError): The interpreter used for processing. """ - def __init__(self, error: Exception, response: Response, interpreter: Interpreter): + def __init__(self, error: Exception, response: Response, interpreter: Interpreter) -> None: self.original: Exception = error self.response: Response = response self.interpreter: Interpreter = interpreter @@ -62,7 +62,7 @@ class BadColourArgument(EmbedParseError): The invalid input. """ - def __init__(self, argument: str): + def __init__(self, argument: str) -> None: self.argument: str = argument super().__init__(f'Colour "{argument}" is invalid.') @@ -77,7 +77,7 @@ class StopError(TagScriptError): The stop error message. """ - def __init__(self, message: str): + def __init__(self, message: str) -> None: self.message: str = message super().__init__(message) @@ -98,7 +98,7 @@ class CooldownExceeded(StopError): The seconds left til the cooldown ends. """ - def __init__(self, message: str, cooldown: Cooldown, key: str, retry_after: float): + def __init__(self, message: str, cooldown: Cooldown, key: str, retry_after: float) -> None: self.cooldown: Cooldown = cooldown self.key: str = key self.retry_after: float = retry_after diff --git a/TagScriptEngine/interface/__init__.py b/TagScriptEngine/interface/__init__.py index c4cc7d9..98e401d 100644 --- a/TagScriptEngine/interface/__init__.py +++ b/TagScriptEngine/interface/__init__.py @@ -1,4 +1,14 @@ -from .adapter import Adapter -from .block import Block, verb_required_block +from __future__ import annotations -__all__ = ("Adapter", "Block", "verb_required_block") +from typing import Tuple + +from .adapter import ( + Adapter as Adapter, + SimpleAdapter as SimpleAdapter, +) +from .block import ( + Block as Block, + verb_required_block as verb_required_block, +) + +__all__: Tuple[str, ...] = ("Adapter", "SimpleAdapter", "Block", "verb_required_block") diff --git a/TagScriptEngine/interface/adapter.py b/TagScriptEngine/interface/adapter.py index 1537e87..ade95a6 100644 --- a/TagScriptEngine/interface/adapter.py +++ b/TagScriptEngine/interface/adapter.py @@ -1,22 +1,34 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Protocol, Tuple, TypeVar if TYPE_CHECKING: - from ..interpreter import Context + from ..verb import Verb -class Adapter: +_T = TypeVar("_T", bound=object) + + +__all__: Tuple[str, ...] = ("Adapter", "SimpleAdapter") + + +class _Adapter(Protocol): + def __repr__(self) -> str: ... + + def get_value(self, ctx: Verb) -> Optional[str]: ... + + +class Adapter(_Adapter): """ The base class for TagScript blocks. Implementations must subclass this to create adapters. """ - def __repr__(self): + def __repr__(self) -> str: return f"<{type(self).__qualname__} at {hex(id(self))}>" - def get_value(self, ctx: Context) -> Optional[str]: + def get_value(self, ctx: Verb) -> Optional[str]: """ Processes the adapter's actions for a given :class:`~TagScriptEngine.interpreter.Context`. @@ -24,7 +36,7 @@ def get_value(self, ctx: Context) -> Optional[str]: Parameters ---------- - ctx: Context + ctx: Verb The context object containing the TagScript :class:`~TagScriptEngine.verb.Verb`. Returns @@ -38,3 +50,26 @@ def get_value(self, ctx: Context) -> Optional[str]: The subclass did not implement this required method. """ raise NotImplementedError + + +class SimpleAdapter(Adapter, Generic[_T]): + __slots__: Tuple[str, ...] = ("object", "attributes", "_methods") + + def __init__(self, *, base: _T) -> None: + self.object: _T = base + self._attributes: Dict[str, Any] = {} + self._methods: Dict[str, Any] = {} + self.update_attributes() + self.update_methods() + + def __repr__(self) -> str: + return f"<{type(self).__qualname__} object={self.object!r}>" + + def update_attributes(self) -> None: + pass + + def update_methods(self) -> None: + pass + + def get_value(self, ctx: Verb) -> Optional[str]: + raise NotImplementedError diff --git a/TagScriptEngine/interface/block.py b/TagScriptEngine/interface/block.py index 77bb414..605a6b0 100644 --- a/TagScriptEngine/interface/block.py +++ b/TagScriptEngine/interface/block.py @@ -1,16 +1,31 @@ from __future__ import annotations from functools import lru_cache -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Any, Optional, Protocol, Tuple, cast if TYPE_CHECKING: from ..interpreter import Context -__all__ = ("Block", "verb_required_block") +__all__: Tuple[str, ...] = ("Block", "verb_required_block") -class Block: +class _Block(Protocol): + ACCEPTED_NAMES: Tuple[str, ...] + + def __repr__(self) -> str: ... + + @classmethod + def will_accept(cls, ctx: Context) -> bool: ... + + def pre_process(self, ctx: Context) -> Any: ... + + def process(self, ctx: Context) -> Optional[str]: ... + + def post_process(self, ctx: "Context") -> Any: ... + + +class Block(_Block): """ The base class for TagScript blocks. @@ -22,9 +37,9 @@ class Block: The accepted names for this block. This ideally should be set as a class attribute. """ - ACCEPTED_NAMES = () + ACCEPTED_NAMES: Tuple[str, ...] = () - def __repr__(self): + def __repr__(self) -> str: return f"<{type(self).__qualname__} at {hex(id(self))}>" @classmethod @@ -42,10 +57,10 @@ def will_accept(cls, ctx: Context) -> bool: bool Whether the block should be processed for this :class:`~TagScriptEngine.interpreter.Context`. """ - dec = ctx.verb.declaration.lower() + dec: str = cast(str, ctx.verb.declaration).lower() return dec in cls.ACCEPTED_NAMES - def pre_process(self, ctx: Context): + def pre_process(self, ctx: Context) -> Any: return None def process(self, ctx: Context) -> Optional[str]: @@ -71,7 +86,7 @@ def process(self, ctx: Context) -> Optional[str]: """ raise NotImplementedError - def post_process(self, ctx: "interpreter.Context"): + def post_process(self, ctx: "Context") -> Any: return None @@ -102,7 +117,8 @@ def __repr__(self): return f"VerbRequiredBlock(implicit={implicit!r}, payload={payload!r}, parameter={parameter!r})" class VerbRequiredBlock(Block, metaclass=RequireMeta): - def will_accept(self, ctx: Context) -> bool: + @classmethod + def will_accept(cls, ctx: Context) -> bool: verb = ctx.verb if payload and not check(verb.payload): return False @@ -110,4 +126,4 @@ def will_accept(self, ctx: Context) -> bool: return False return super().will_accept(ctx) - return VerbRequiredBlock + return VerbRequiredBlock # type: ignore diff --git a/TagScriptEngine/interpreter.py b/TagScriptEngine/interpreter.py index a3cdda1..7c53c02 100644 --- a/TagScriptEngine/interpreter.py +++ b/TagScriptEngine/interpreter.py @@ -2,7 +2,8 @@ import logging from itertools import islice -from typing import Any, Dict, List, Optional, Tuple, TypeAlias +from typing import Any, Dict, List, Optional, Protocol, Tuple, cast +from typing_extensions import TypeAlias from .exceptions import ( ProcessError, @@ -14,7 +15,7 @@ from .utils import maybe_await from .verb import Verb -__all__ = ( +__all__: Tuple[str, ...] = ( "Interpreter", "AsyncInterpreter", "Context", @@ -29,7 +30,15 @@ AnyDict: TypeAlias = Dict[str, Any] -class Node: +class _Node(Protocol): + def __init__(self, coordinates: Tuple[int, int], verb: Optional[Verb] = None) -> None: ... + + def __str__(self) -> str: ... + + def __repr__(self) -> str: ... + + +class Node(_Node): """ A low-level object representing a bracketed block. @@ -43,12 +52,12 @@ class Node: The `Block` processed output for this node. """ - __slots__ = ("output", "verb", "coordinates") + __slots__: Tuple[str, ...] = ("output", "verb", "coordinates") def __init__(self, coordinates: Tuple[int, int], verb: Optional[Verb] = None) -> None: self.output: Optional[str] = None - self.verb = verb - self.coordinates = coordinates + self.verb: Optional[Verb] = verb + self.coordinates: Tuple[int, int] = coordinates def __str__(self) -> str: return str(self.verb) + " at " + str(self.coordinates) @@ -66,25 +75,34 @@ def build_node_tree(message: str) -> List[Node]: List[Node] A list of all possible text bracket blocks. """ - nodes = [] - previous = r"" - - starts = [] - for i, ch in enumerate(message): - if ch == "{" and previous != r"\\": - starts.append(i) - if ch == "}" and previous != r"\\": + nodes: List[Node] = [] + previous: str = r"" + starts: List[int] = [] + for idx, char in enumerate(message): + if char == "{" and previous != r"\\": + starts.append(idx) + if char == "}" and previous != r"\\": if not starts: continue - coords = (starts.pop(), i) - n = Node(coords) - nodes.append(n) - - previous = ch + coords: Tuple[int, int] = (starts.pop(), idx) + node: Node = Node(coords) + nodes.append(node) + previous: str = char return nodes -class Response: +class _Response(Protocol): + def __init__( + self, + *, + variables: Optional[AdapterDict] = None, + extra_kwargs: Optional[AnyDict] = None, + ) -> None: ... + + def __repr__(self) -> str: ... + + +class Response(_Response): """ An object containing information on a completed TagScript process. @@ -100,7 +118,7 @@ class Response: A dictionary of extra keyword arguments that blocks can use to define their own behavior. """ - __slots__ = ("body", "actions", "variables", "extra_kwargs") + __slots__: Tuple[str, ...] = ("body", "actions", "variables", "extra_kwargs") def __init__( self, @@ -119,7 +137,13 @@ def __repr__(self) -> str: ) -class Context: +class _Context(Protocol): + def __init__(self, verb: Verb, res: Response, interpreter: Interpreter, og: str) -> None: ... + + def __repr__(self) -> str: ... + + +class Context(_Context): """ An object containing data on the TagScript block processed by the interpreter. This class is passed to adapters and blocks during processing. @@ -134,7 +158,7 @@ class Context: The interpreter processing the TagScript. """ - __slots__ = ("verb", "original_message", "interpreter", "response") + __slots__: Tuple[str, ...] = ("verb", "original_message", "interpreter", "response") def __init__(self, verb: Verb, res: Response, interpreter: Interpreter, og: str) -> None: self.verb: Verb = verb @@ -146,7 +170,63 @@ def __repr__(self) -> str: return f"" -class Interpreter: +class _Interpreter(Protocol): + def __init__(self, blocks: List[Block]) -> None: ... + + def __repr__(self) -> str: ... + + def _get_context( + self, + node: Node, + final: str, + *, + response: Response, + original_message: str, + verb_limit: int, + dot_parameter: bool, + ) -> Context: ... + + def _get_acceptors(self, ctx: Context) -> List[Block]: ... + + def _process_blocks(self, ctx: Context, node: Node) -> Optional[str]: ... + + @staticmethod + def _check_workload(charlimit: int, total_work: int, output: str) -> Optional[int]: ... + + @staticmethod + def _text_deform(start: int, end: int, final: str, output: str) -> Tuple[str, int]: ... + + @staticmethod + def _translate_nodes( + node_ordered_list: List[Node], index: int, start: int, differential: int + ) -> None: ... + + def _solve( + self, + message: str, + node_ordered_list: List[Node], + response: Response, + *, + charlimit: int, + verb_limit: int = 2000, + dot_parameter: bool, + ) -> str: ... + + @staticmethod + def _return_response(response: Response, output: str) -> Response: ... + + def process( + self, + message: str, + seed_variables: Optional[AdapterDict] = None, + *, + charlimit: Optional[int] = None, + dot_parameter: bool = False, + **kwargs: Any, + ) -> Response: ... + + +class Interpreter(_Interpreter): """ The TagScript interpreter. @@ -216,7 +296,9 @@ def _text_deform(start: int, end: int, final: str, output: str) -> Tuple[str, in return final, differential @staticmethod - def _translate_nodes(node_ordered_list: List[Node], index: int, start: int, differential: int): + def _translate_nodes( + node_ordered_list: List[Node], index: int, start: int, differential: int + ) -> None: for future_n in islice(node_ordered_list, index + 1, None): new_start = None new_end = None @@ -240,7 +322,7 @@ def _solve( charlimit: int, verb_limit: int = 2000, dot_parameter: bool, - ): + ) -> str: final = message total_work = 0 for index, node in enumerate(node_ordered_list): @@ -262,7 +344,7 @@ def _solve( if output is None: continue # If there was no value output, no need to text deform. - total_work = self._check_workload(charlimit, total_work, output) + total_work = self._check_workload(charlimit, cast(int, total_work), output) final, differential = self._text_deform(start, end, final, output) self._translate_nodes(node_ordered_list, index, start, differential) return final @@ -283,7 +365,7 @@ def process( *, charlimit: Optional[int] = None, dot_parameter: bool = False, - **kwargs, + **kwargs: Any, ) -> Response: """ Processes a given TagScript string. @@ -322,7 +404,7 @@ def process( message, node_ordered_list, response, - charlimit=charlimit, + charlimit=cast(int, charlimit), dot_parameter=dot_parameter, ) except TagScriptError: @@ -341,10 +423,10 @@ class AsyncInterpreter(Interpreter): See `Interpreter` for full documentation. """ - async def _get_acceptors(self, ctx: Context) -> List[Block]: + async def _get_acceptors(self, ctx: Context) -> List[Block]: # type: ignore return [b for b in self.blocks if await maybe_await(b.will_accept, ctx)] - async def _process_blocks(self, ctx: Context, node: Node) -> Optional[str]: + async def _process_blocks(self, ctx: Context, node: Node) -> Optional[str]: # type: ignore acceptors = await self._get_acceptors(ctx) for b in acceptors: value = await maybe_await(b.process, ctx) @@ -353,7 +435,7 @@ async def _process_blocks(self, ctx: Context, node: Node) -> Optional[str]: node.output = value return value - async def _solve( + async def _solve( # type: ignore self, message: str, node_ordered_list: List[Node], @@ -362,7 +444,7 @@ async def _solve( charlimit: int, verb_limit: int = 2000, dot_parameter: bool, - ): + ) -> str: final = message total_work = 0 @@ -383,19 +465,19 @@ async def _solve( if output is None: continue # If there was no value output, no need to text deform. - total_work = self._check_workload(charlimit, total_work, output) + total_work = self._check_workload(charlimit, cast(int, total_work), output) final, differential = self._text_deform(start, end, final, output) self._translate_nodes(node_ordered_list, index, start, differential) return final - async def process( + async def process( # type: ignore self, message: str, seed_variables: Optional[AdapterDict] = None, *, charlimit: Optional[int] = None, dot_parameter: bool = False, - **kwargs, + **kwargs: Any, ) -> Response: """ Asynchronously process a given TagScript string. @@ -410,7 +492,7 @@ async def process( message, node_ordered_list, response, - charlimit=charlimit, + charlimit=cast(int, charlimit), dot_parameter=dot_parameter, ) except TagScriptError: diff --git a/TagScriptEngine/py.typed b/TagScriptEngine/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/TagScriptEngine/utils.py b/TagScriptEngine/utils.py index baea37c..dea5060 100644 --- a/TagScriptEngine/utils.py +++ b/TagScriptEngine/utils.py @@ -1,22 +1,47 @@ +from __future__ import annotations + import re from inspect import isawaitable -from typing import Any, Awaitable, Callable, T, TypeVar, Union +from typing import Any, Awaitable, Callable, Tuple, TypeVar, Union -import discord -__all__ = ("escape_content", "maybe_await", "DPY2") +__all__: Tuple[str, ...] = ("truncate", "escape_content", "maybe_await") T = TypeVar("T") -DPY2 = discord.version_info >= (2, 0, 0, "alpha", 0) - -pattern = re.compile(r"(? str: return "\\" + match[1] +def truncate(text: str, *, max: int = 2000, var: str = "...") -> str: + """ + Truncate the given string to avoid hitting the character limit. + + Parameters + ---------- + text + The string to be truncated. + max + On what character length the string should be truncated. + var + The custom string used for trunication (defaults to '...'). + + Returns + ------- + str + The truncated content. + + .. versionadded:: 3.2.0 + """ + if len(text) <= max: + return text + truncated: str = text[: max - 3] + return truncated + var + + def escape_content(string: str) -> str: """ Escapes given input to avoid tampering with engine/block behavior. @@ -31,7 +56,9 @@ def escape_content(string: str) -> str: return pattern.sub(_sub_match, string) -async def maybe_await(func: Callable[..., Union[T, Awaitable[T]]], *args: Any, **kwargs: Any) -> T: +async def maybe_await( + func: Callable[..., Union[T, Awaitable[T]]], *args: Any, **kwargs: Any +) -> Union[T, Any]: """ Await the given function if it is awaitable or call it synchronously. @@ -40,5 +67,5 @@ async def maybe_await(func: Callable[..., Union[T, Awaitable[T]]], *args: Any, * Any The result of the awaitable function. """ - value = func(*args, **kwargs) + value: Union[T, Awaitable[T]] = func(*args, **kwargs) return await value if isawaitable(value) else value diff --git a/TagScriptEngine/verb.py b/TagScriptEngine/verb.py index ac59320..070321b 100644 --- a/TagScriptEngine/verb.py +++ b/TagScriptEngine/verb.py @@ -1,9 +1,29 @@ -from typing import Optional +from __future__ import annotations -__all__ = ("Verb",) +from typing import Optional, Protocol, Tuple +__all__: Tuple[str, ...] = ("Verb",) -class Verb: + +class _Verb(Protocol): + def __init__( + self, verb_string: Optional[str] = None, *, limit: int = 2000, dot_parameter: bool = False + ) -> None: ... + + def __parse(self, verb_string: str, limit: int) -> None: ... + + def _parse_paranthesis_parameter(self, i: int, v: str) -> bool: ... + + def _parse_dot_parameter(self, i: int, v: str) -> bool: ... + + def set_payload(self) -> None: ... + + def open_parameter(self, i: int) -> None: ... + + def close_parameter(self, i: int) -> bool: ... + + +class Verb(_Verb): """ Represents the passed TagScript block. @@ -35,7 +55,7 @@ class Verb: {declaration.parameter:payload} """ - __slots__ = ( + __slots__: Tuple[str, ...] = ( "declaration", "parameter", "payload", @@ -49,7 +69,7 @@ class Verb: def __init__( self, verb_string: Optional[str] = None, *, limit: int = 2000, dot_parameter: bool = False - ): + ) -> None: self.declaration: Optional[str] = None self.parameter: Optional[str] = None self.payload: Optional[str] = None @@ -58,7 +78,7 @@ def __init__( return self.__parse(verb_string, limit) - def __str__(self): + def __str__(self) -> str: """This makes Verb compatible with str(x)""" response = "{" if self.declaration is not None: @@ -69,12 +89,12 @@ def __str__(self): response += ":" + self.payload return response + "}" - def __repr__(self): + def __repr__(self) -> str: attrs = ("declaration", "parameter", "payload") inner = " ".join(f"{attr}={getattr(self, attr)!r}" for attr in attrs) return f"" - def __parse(self, verb_string: str, limit: int): + def __parse(self, verb_string: str, limit: int) -> None: self.parsed_string = verb_string[1:-1][:limit] self.parsed_length = len(self.parsed_string) self.dec_depth = 0 @@ -94,7 +114,6 @@ def __parse(self, verb_string: str, limit: int): continue if v == ":" and not self.dec_depth: - # if v == ":" and not dec_depth: self.set_payload() return elif parse_parameter(i, v): @@ -116,13 +135,13 @@ def _parse_dot_parameter(self, i: int, v: str) -> bool: return self.close_parameter(i + 1) return False - def set_payload(self): + def set_payload(self) -> None: res = self.parsed_string.split(":", 1) if len(res) == 2: self.payload = res[1] self.declaration = res[0] - def open_parameter(self, i: int): + def open_parameter(self, i: int) -> None: self.dec_depth += 1 if not self.dec_start: self.dec_start = i diff --git a/Tests/test_adapters.py b/Tests/test_adapters.py index 199af3b..6d0917a 100644 --- a/Tests/test_adapters.py +++ b/Tests/test_adapters.py @@ -1,5 +1,7 @@ +from typing import List import unittest +import TagScriptEngine as tse from TagScriptEngine import Interpreter, adapter, block @@ -9,33 +11,33 @@ def dummy_function(): class TestVerbParsing(unittest.TestCase): def setUp(self): - self.blocks = [block.StrictVariableGetterBlock()] - self.engine = Interpreter(self.blocks) + self.blocks: List[tse.Block] = [block.StrictVariableGetterBlock()] + self.engine: Interpreter = Interpreter(self.blocks) def tearDown(self): - self.blocks = None - self.engine = None + self.blocks: List[tse.Block] = None # type: ignore + self.engine: Interpreter = None # type: ignore def test_string_adapter(self): # Basic string adapter get data = {"test": adapter.StringAdapter("Hello World, How are you")} - result = self.engine.process("{test}", data).body + result = self.engine.process("{test}", data).body # type: ignore self.assertEqual(result, "Hello World, How are you") # Slice - result = self.engine.process("{test(1)}", data).body + result = self.engine.process("{test(1)}", data).body # type: ignore self.assertEqual(result, "Hello") # Plus - result = self.engine.process("{test(3+)}", data).body + result = self.engine.process("{test(3+)}", data).body # type: ignore self.assertEqual(result, "How are you") # up to - result = self.engine.process("{test(+2)}", data).body + result = self.engine.process("{test(+2)}", data).body # type: ignore self.assertEqual(result, "Hello World,") def test_function_adapter(self): # Basic string adapter get - data = {"fn": adapter.FunctionAdapter(dummy_function)} - result = self.engine.process("{fn}", data).body + data = {"fn": adapter.FunctionAdapter(dummy_function)} # type: ignore + result = self.engine.process("{fn}", data).body # type: ignore self.assertEqual(result, "500") diff --git a/Tests/test_edgecase.py b/Tests/test_edgecase.py index ab420aa..b6da9ed 100644 --- a/Tests/test_edgecase.py +++ b/Tests/test_edgecase.py @@ -1,6 +1,6 @@ import unittest -from TagScriptEngine import Interpreter, WorkloadExceededError, adapter, block, interface +from TagScriptEngine import Interpreter, WorkloadExceededError, adapter, block class TestEdgeCases(unittest.TestCase): @@ -115,9 +115,9 @@ def test_specific_duplication(self): {c:{if({target(id)}=={user(id)}):choose {error},{error}|setnick {target(id)} {join():{username}}}} """ data = {"target": adapter.StringAdapter("Basic Username")} - result = self.engine.process(script, data).body + result = self.engine.process(script, data).body # type: ignore print(result) - self.assertTrue(len(result) < 150) + self.assertTrue(len(result) < 150) # type: ignore def test_recursion(self): data = {"target": adapter.StringAdapter("Basic Username")} @@ -156,4 +156,4 @@ def test_recursion(self): {recursion} """ - self.engine.process(script, data, charlimit=2000) + self.engine.process(script, data, charlimit=2000) # type: ignore diff --git a/Tests/test_verbs.py b/Tests/test_verbs.py index 936b2a4..6fad747 100644 --- a/Tests/test_verbs.py +++ b/Tests/test_verbs.py @@ -26,7 +26,7 @@ def seen_all(self, string, outcomes, tries=100): unique_outcomes = set(outcomes) seen_outcomes = set() for _ in range(tries): - outcome = self.engine.process(string).body + outcome = self.engine.process(string).body # type: ignore seen_outcomes.add(outcome) result = unique_outcomes == seen_outcomes @@ -82,15 +82,15 @@ def test_range(self): self.assertTrue(self.seen_all(test, expect)) # Test simple float range test = "{rangef:1.5-2.5} cows" - self.assertTrue("." in self.engine.process(test).body) + self.assertTrue("." in self.engine.process(test).body) # type: ignore def test_math(self): test = "{math:100/2}" expect = "50.0" # division implies float - self.assertEqual(self.engine.process(test).body, expect) + self.assertEqual(self.engine.process(test).body, expect) # type: ignore test = "{math:100**100**100}" # should 'fail' - self.assertEqual(self.engine.process(test).body, test) + self.assertEqual(self.engine.process(test).body, test) # type: ignore def test_misc(self): # Test using a variable to get a variable @@ -99,19 +99,19 @@ def test_misc(self): "message": adapter.StringAdapter("Hello"), } test = "{{pointer}}" - self.assertEqual(self.engine.process(test, data).body, "Hello") + self.assertEqual(self.engine.process(test, data).body, "Hello") # type: ignore test = r"\{{pointer}\}" - self.assertEqual(self.engine.process(test, data).body, r"\{message\}") + self.assertEqual(self.engine.process(test, data).body, r"\{message\}") # type: ignore test = "{break(10==10):Override.} This is my actual tag!" - self.assertEqual(self.engine.process(test, data).body, "Override.") + self.assertEqual(self.engine.process(test, data).body, "Override.") # type: ignore def test_cuddled_strf(self): t = time.gmtime() huggle_wuggle = time.strftime("%y%y%y%y") - self.assertEqual(self.engine.process("{strf:%y%y%y%y}").body, huggle_wuggle) + self.assertEqual(self.engine.process("{strf:%y%y%y%y}").body, huggle_wuggle) # type: ignore def test_basic_strf(self): year = time.strftime("%Y") - self.assertEqual(self.engine.process("Hehe, it's {strf:%Y}").body, f"Hehe, it's {year}") + self.assertEqual(self.engine.process("Hehe, it's {strf:%Y}").body, f"Hehe, it's {year}") # type: ignore diff --git a/benchmark.py b/benchmark.py index 82a9992..96c5c31 100644 --- a/benchmark.py +++ b/benchmark.py @@ -42,7 +42,7 @@ def v2_test(): for _ in range(1000): x.process( "{message} {#:1,2,3,4,5,6,7,8,9,10} {range:1-9} {#:1,2,3,4,5} {message} {strf:Its %A}", - dummy, + dummy, # type: ignore ) diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9eb147ec18a118b7b597d8f11954458ab7122856 GIT binary patch literal 15406 zcmeI2Z)g-p7{)h$&%0c5dx|dv@|V4({+usRIAI>v{y7uD=mF;UKZ7~7RnYZFzR2_v>ysJt&BF9 zVDY4!YZf=!fJBqLU zU?29YDJxIpBRxHRYi4HV<3J#=*RWR_;wip1JNd}S$T%kUq|NXh)1%P~r02me${+c! z6WD(v_J)sNNiPV*FTBm-1N&nM>^F<89ddGVRG;2=vNy`h;ag2Cno0NLYgGJYXJ=Qz zy__}@<7CHc*w&Th(1fs+D>QEDt>tb@ES zw}HL9>o(Z%4^!4C<)8sAAAs#Z=0)2ns~qEPoF{O$sP{DauY||a|M(biDo4leanh>h zC2$X$_>bais}qk?IW|M#aCnAt>NcgnYZbSVUgDlF6Ms%zPUXbyEw{X?oy?Og{G({v z$oN`5aZ87NjA$sofCvWyIx@IR^B9t@1mLbOwHhOumzG$LfjOR{DEX@sl48 zg+kAbgE!t3?%mJ8FVBH5P5do!htH6Kzf#7c2|HwL07)CEkKd!BRq7a{8^EXN*al_r z8E9=y7!&KkNdu?g>jtrPh;S=@TVoFj>DwMSdf=}H_Z6o(rOi$csBB%}dl$T4P?=L~qL_)O^_`A;y&uo_Lk`E0mW(${qY@)dA|CxFLy=|{kiz<)Rb zvQNSmt@aM^_eltXfA|sbBj88CkANS6#1ROv#><~ReR@fLe*P>$K|#Sx*=wOJ7t?=Z z9gtX|O>JReVVHI48X(`jj+34vy&@prOWFnG+vHCAGS9}vWEO?ccm#d2-}%%&4`s3a zINHb_RUheZ><6c`2k2#I885q(MyC|b*Mq^}^X!={qRkD^g9g5&Px_ILwhkxVrt%Xg zm2Z-Hw7)1IKW)<0^L-XAGPaZL**>6MEA86B;1trImPG$u>Tjd@mKl_FOj7)149K2@ z{O%#o!$k2P;@)_j{pAX<3@jB;wgk+@R(vUto$#ZWeU*dkH(dr-$#=r|LW1~@@Vq=r zeW!sg(r(ZJPMbly!{AN)Nq;8dC}2P6dl28RMgI+m=A*;S_)m6!NWFab`AjP*33=Rs zG_=;BeZWC~C){rIxoOik0oNfH{cUFLlBets;);G}_ANxlB5vhC^g_3Vax_-}#KH%q$@qF^6 z3;ato5B-1J=$A2c3z90i-tfK*Z#Cn0OC%bti$U^{$h*0^UPFBidHF8-E%}iIeH~zq z_mU^#aboBnbMIREGD1I2Q`VT3l~qf=mh?Ta1=J}}E_?X0EhT-eLchGz?CyzvPv@wC zz+AZRSTtM5lB;w4{0W-qgU)r>gZ@W0eYi>89NKtw%tZTnFZ3HehUr5MV{H}MpSowb zHs@sh)$akGqxW|0cN~J&eJA%%I=)VcZj3LU~jJ29qU6A*ee2?i%MYGu!fAY?r z4`eN1)<1y$O$hgT9ub1G2@RfH1AKL}1}nGfM|%%*yaKNRZ}Ahm4sag^-&xQhnn0Hc zXX)>VIqoViZMjEf?$=3rmGm+=Fp>WmS^HvKW4X@!MXfD0VHx=fG`@je+i&%vpEmMu zs?5P223Bj86Mg$Vou#olAz literal 0 HcmV?d00001 diff --git a/docs/_static/logo.png b/docs/_static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..72613830917b674fa6e3e3bae052fb6f76ef5dea GIT binary patch literal 10627 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 +`Tags cog `_ until developer documentation is written. diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 0000000..10587b6 --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,36 @@ +.. meta:: + :description: How to install AdvancedTagScriptEngine using pip, from source or git repository + :keywords: python, advancedtagscriptengine, tagscriptengine, tagscript, install + +.. title:: AdvancedTagScriptEngine installation options + + +========== +Installing +========== + + +pip +^^^ + +`package `_:: + + $ pip install AdvancedTagScriptEngine + +Latest version of `AdvancedTagScriptEngine` supports only python3. + + +Source +^^^^^^ + +Download `source `_:: + + $ pip install -e . + + +Git Repository +^^^^^^^^^^^^^^ + +Get latest development version:: + + $ git clone https://github.com/japandotorg/TagScriptEngine.git diff --git a/docs/requirements.txt b/docs/requirements.txt index 7869925..055f7fd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,8 +1,7 @@ discord.py Jinja2<3.1 recommonmark -karma-sphinx-theme Sphinx sphinx-book-theme sphinx-material -sphinx_rtd_theme +sphinx-press-theme diff --git a/playground.py b/playground.py index e6e9bef..4c83436 100644 --- a/playground.py +++ b/playground.py @@ -1,8 +1,10 @@ -from appJar import gui +from typing import Any, List, Optional +from appJar import gui as GUI +import TagScriptEngine as tse from TagScriptEngine import Interpreter, block -blocks = [ +blocks: List[tse.Block] = [ block.MathBlock(), block.RandomBlock(), block.RangeBlock(), @@ -18,16 +20,16 @@ block.LooseVariableGetterBlock(), block.SubstringBlock(), ] -x = Interpreter(blocks) +x: Interpreter = Interpreter(blocks) -def press(button): - o = x.process(app.getTextArea("input")).body +def press(button: Any) -> None: + o: Optional[str] = x.process(app.getTextArea("input")).body app.clearTextArea("output") app.setTextArea("output", o) -app = gui("TSE Playground", "750x450") +app: GUI = GUI("TSE Playground", "750x450") app.setPadding([2, 2]) app.setInPadding([2, 2]) app.addTextArea("input", text="I see {rand:1,2,3,4} new items!", row=0, column=0) diff --git a/pyproject.toml b/pyproject.toml index 8dcc88c..105342c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,3 +8,19 @@ line-length = 99 [tool.isort] profile = "black" line_length = 99 + +[tool.pyright] +include = [ + "TagScriptEngine", + "TagScriptEngine/adapter", + "TagScriptEngine/block", + "TagScriptEngine/interface", +] +exclude = [ + "**/__pycache__", + "build", + "dist", + "docs", +] +pythonVersion = "3.8" +typeCheckingMode = "basic" diff --git a/requirements.txt b/requirements.txt index 85d2d7b..c9808e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ autoflake black -discord.py==2.3.2 +discord.py>=2.4.0 isort pyparsing +typing_extensions diff --git a/setup.cfg b/setup.cfg index d39aeff..ad7f115 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,21 +14,39 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Topic :: Internet + Topic :: Software Development :: Libraries + Topic :: Software Development :: Libraries :: Python Modules + Topic :: Utilities + Typing :: Typed license = Creative Commons Attribution 4.0 International License license_file = LICENSE description = An easy drop in user-provided Templating system. long_description = file: README.md long_description_content_type = text/markdown; charset=UTF-8; variant=GFM -keywords = tagscript, +keywords = tagscript, string-templating, discord.py [options] packages = find_namespace: install_requires = - discord.py==2.3.2 pyparsing + typing_extensions python_requires = >=3.8 +[options.extras_require] +discord = + discord.py>=2.4.0 + [options.packages.find] include = TagScriptEngine TagScriptEngine.* +exclude = + docs* + tests* + +[flake8] +exclude = + build + dist + docs/conf.py From 2863dc1d0bdaa03a0d03133ff6058d7565b40070 Mon Sep 17 00:00:00 2001 From: japandotorg Date: Sun, 28 Jul 2024 07:27:25 +0530 Subject: [PATCH 3/4] Fix metaclass error and bump version --- TagScriptEngine/__init__.py | 2 +- TagScriptEngine/block/assign.py | 5 ++--- TagScriptEngine/block/command.py | 4 ++-- TagScriptEngine/block/control.py | 8 ++++---- TagScriptEngine/block/cooldown.py | 6 +++--- TagScriptEngine/block/count.py | 8 ++++---- TagScriptEngine/block/fiftyfifty.py | 6 +++--- TagScriptEngine/block/randomblock.py | 6 +++--- TagScriptEngine/block/range.py | 6 +++--- TagScriptEngine/block/redirect.py | 6 +++--- TagScriptEngine/block/replaceblock.py | 8 ++++---- TagScriptEngine/block/require_blacklist.py | 8 ++++---- TagScriptEngine/block/stopblock.py | 6 +++--- TagScriptEngine/block/substr.py | 6 +++--- TagScriptEngine/block/urlencodeblock.py | 6 +++--- setup.cfg | 3 +++ 16 files changed, 48 insertions(+), 46 deletions(-) diff --git a/TagScriptEngine/__init__.py b/TagScriptEngine/__init__.py index 2f1210d..33b01a1 100644 --- a/TagScriptEngine/__init__.py +++ b/TagScriptEngine/__init__.py @@ -170,7 +170,7 @@ ) -__version__: Final[str] = "3.2.0" +__version__: Final[str] = "3.2.1" class VersionNamedTuple(NamedTuple): diff --git a/TagScriptEngine/block/assign.py b/TagScriptEngine/block/assign.py index 5bcf756..4a44dc0 100644 --- a/TagScriptEngine/block/assign.py +++ b/TagScriptEngine/block/assign.py @@ -1,17 +1,16 @@ from __future__ import annotations -from typing import Optional, Tuple, Type, cast +from typing import Optional, Tuple from ..adapter import StringAdapter from ..interface import verb_required_block from ..interpreter import Context -from ..interface.block import Block __all__: Tuple[str, ...] = ("AssignmentBlock",) -class AssignmentBlock(cast(Type[Block], verb_required_block(False, parameter=True))): +class AssignmentBlock(verb_required_block(False, parameter=True)): # type: ignore """ Variables are useful for choosing a value and referencing it later in a tag. Variables can be referenced using brackets as any other block. diff --git a/TagScriptEngine/block/command.py b/TagScriptEngine/block/command.py index 4e505a1..0cd0ddb 100644 --- a/TagScriptEngine/block/command.py +++ b/TagScriptEngine/block/command.py @@ -2,7 +2,7 @@ import asyncio from types import TracebackType -from typing import Any, Awaitable, Generator, Iterator, List, Optional, Tuple, Type, TypeVar, cast +from typing import Any, Awaitable, Generator, Iterator, List, Optional, Tuple, Type, TypeVar from ..interface import Block, verb_required_block from ..interpreter import Context @@ -13,7 +13,7 @@ __all__: Tuple[str, ...] = ("CommandBlock", "OverrideBlock", "SequentialGather") -class CommandBlock(cast(Type[Block], verb_required_block(True, payload=True))): +class CommandBlock(verb_required_block(True, payload=True)): # type: ignore """ Run a command as if the tag invoker had ran it. Only 3 command blocks can be used in a tag. diff --git a/TagScriptEngine/block/control.py b/TagScriptEngine/block/control.py index b379a88..e0283c5 100644 --- a/TagScriptEngine/block/control.py +++ b/TagScriptEngine/block/control.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional, Tuple, Type, cast +from typing import Optional, Tuple, cast from ..interface import verb_required_block, Block from ..interpreter import Context @@ -31,7 +31,7 @@ def parse_into_output(payload: str, result: Optional[bool]) -> Optional[str]: ImplicitPPRBlock: Block = verb_required_block(True, payload=True, parameter=True) -class AnyBlock(cast(Type[Block], ImplicitPPRBlock)): +class AnyBlock(ImplicitPPRBlock): # type: ignore """ The any block checks that any of the passed expressions are true. Multiple expressions can be passed to the parameter by splitting them with ``|``. @@ -64,7 +64,7 @@ def process(self, ctx: Context) -> Optional[str]: return parse_into_output(cast(str, ctx.verb.payload), result) -class AllBlock(cast(Type[Block], ImplicitPPRBlock)): +class AllBlock(ImplicitPPRBlock): # type: ignore """ The all block checks that all of the passed expressions are true. Multiple expressions can be passed to the parameter by splitting them with ``|``. @@ -97,7 +97,7 @@ def process(self, ctx: Context) -> Optional[str]: return parse_into_output(cast(str, ctx.verb.payload), result) -class IfBlock(cast(Type[Block], ImplicitPPRBlock)): +class IfBlock(ImplicitPPRBlock): # type: ignore """ The if block returns a message based on the passed expression to the parameter. An expression is represented by two values compared with an operator. diff --git a/TagScriptEngine/block/cooldown.py b/TagScriptEngine/block/cooldown.py index 785b9f3..c6345c5 100644 --- a/TagScriptEngine/block/cooldown.py +++ b/TagScriptEngine/block/cooldown.py @@ -1,19 +1,19 @@ from __future__ import annotations import time -from typing import Any, Dict, List, Optional, Tuple, Type, cast +from typing import Any, Dict, List, Optional, Tuple, cast from discord.ext.commands import Cooldown, CooldownMapping from ..exceptions import CooldownExceeded from ..interface import verb_required_block -from ..interpreter import Context, Block +from ..interpreter import Context from .helpers import helper_split __all__: Tuple[str, ...] = ("CooldownBlock",) -class CooldownBlock(cast(Type[Block], verb_required_block(True, payload=True, parameter=True))): +class CooldownBlock(verb_required_block(True, payload=True, parameter=True)): # type: ignore """ The cooldown block implements cooldowns when running a tag. The parameter requires 2 values to be passed: ``rate`` and ``per`` integers. diff --git a/TagScriptEngine/block/count.py b/TagScriptEngine/block/count.py index 0a7f21a..0fb57b3 100644 --- a/TagScriptEngine/block/count.py +++ b/TagScriptEngine/block/count.py @@ -1,15 +1,15 @@ from __future__ import annotations -from typing import Optional, Tuple, Type, cast +from typing import Optional, Tuple, cast from ..interface import verb_required_block -from ..interpreter import Context, Block +from ..interpreter import Context __all__: Tuple[str, ...] = ("CountBlock", "LengthBlock") -class CountBlock(cast(Type[Block], verb_required_block(True, payload=True))): +class CountBlock(verb_required_block(True, payload=True)): # type: ignore """ The count block will count how much of text is in message. This is case sensitive and will include substrings, if you @@ -41,7 +41,7 @@ def process(self, ctx: Context) -> Optional[str]: return str(len(cast(str, ctx.verb.payload)) + 1) -class LengthBlock(cast(Type[Block], verb_required_block(True, payload=True))): +class LengthBlock(verb_required_block(True, payload=True)): # type: ignore """ The length block will check the length of the given String. If a parameter is passed in, the block will check the length diff --git a/TagScriptEngine/block/fiftyfifty.py b/TagScriptEngine/block/fiftyfifty.py index d4b66c5..c202a80 100644 --- a/TagScriptEngine/block/fiftyfifty.py +++ b/TagScriptEngine/block/fiftyfifty.py @@ -1,16 +1,16 @@ from __future__ import annotations import random -from typing import Optional, Tuple, Type, cast +from typing import Optional, Tuple from ..interface import verb_required_block -from ..interpreter import Context, Block +from ..interpreter import Context __all__: Tuple[str, ...] = ("FiftyFiftyBlock",) -class FiftyFiftyBlock(cast(Type[Block], verb_required_block(True, payload=True))): +class FiftyFiftyBlock(verb_required_block(True, payload=True)): # type: ignore """ The fifty-fifty block has a 50% change of returning the payload, and 50% chance of returning null. diff --git a/TagScriptEngine/block/randomblock.py b/TagScriptEngine/block/randomblock.py index e2a05bd..add8126 100644 --- a/TagScriptEngine/block/randomblock.py +++ b/TagScriptEngine/block/randomblock.py @@ -1,16 +1,16 @@ from __future__ import annotations import random -from typing import Optional, Tuple, Type, cast +from typing import Optional, Tuple, cast -from ..interface import verb_required_block, Block +from ..interface import verb_required_block from ..interpreter import Context __all__: Tuple[str, ...] = ("RandomBlock",) -class RandomBlock(cast(Type[Block], verb_required_block(True, payload=True))): +class RandomBlock(verb_required_block(True, payload=True)): # type: ignore """ Pick a random item from a list of strings, split by either ``~`` or ``,``. An optional seed can be provided to the parameter to diff --git a/TagScriptEngine/block/range.py b/TagScriptEngine/block/range.py index cbf756b..e8b3a82 100644 --- a/TagScriptEngine/block/range.py +++ b/TagScriptEngine/block/range.py @@ -1,16 +1,16 @@ from __future__ import annotations import random -from typing import Optional, Tuple, Type, cast +from typing import Optional, Tuple, cast -from ..interface import verb_required_block, Block +from ..interface import verb_required_block from ..interpreter import Context __all__: Tuple[str, ...] = ("RangeBlock",) -class RangeBlock(cast(Type[Block], verb_required_block(True, payload=True))): +class RangeBlock(verb_required_block(True, payload=True)): # type: ignore """ The range block picks a random number from a range of numbers seperated by ``-``. The number range is inclusive, so it can pick the starting/ending number as well. diff --git a/TagScriptEngine/block/redirect.py b/TagScriptEngine/block/redirect.py index de35ae6..163786b 100644 --- a/TagScriptEngine/block/redirect.py +++ b/TagScriptEngine/block/redirect.py @@ -1,15 +1,15 @@ from __future__ import annotations -from typing import Optional, Tuple, Type, cast +from typing import Optional, Tuple, cast -from ..interface import verb_required_block, Block +from ..interface import verb_required_block from ..interpreter import Context __all__: Tuple[str, ...] = ("RedirectBlock",) -class RedirectBlock(cast(Type[Block], verb_required_block(True, parameter=True))): +class RedirectBlock(verb_required_block(True, parameter=True)): # type: ignore """ Redirects the tag response to either the given channel, the author's DMs, or uses a reply based on what is passed to the parameter. diff --git a/TagScriptEngine/block/replaceblock.py b/TagScriptEngine/block/replaceblock.py index c24a8e5..cdbfd35 100644 --- a/TagScriptEngine/block/replaceblock.py +++ b/TagScriptEngine/block/replaceblock.py @@ -1,15 +1,15 @@ from __future__ import annotations -from typing import Optional, Tuple, Type, cast +from typing import Optional, Tuple, cast -from ..interface import verb_required_block, Block +from ..interface import verb_required_block from ..interpreter import Context __all__: Tuple[str, ...] = ("ReplaceBlock", "PythonBlock") -class ReplaceBlock(cast(Type[Block], verb_required_block(True, payload=True, parameter=True))): +class ReplaceBlock(verb_required_block(True, payload=True, parameter=True)): # type: ignore """ The replace block will replace specific characters in a string. The parameter should split by a ``,``, containing the characters to find @@ -47,7 +47,7 @@ def process(self, ctx: Context) -> Optional[str]: return cast(str, ctx.verb.payload).replace(before, after) -class PythonBlock(cast(Type[Block], verb_required_block(True, payload=True, parameter=True))): +class PythonBlock(verb_required_block(True, payload=True, parameter=True)): # type: ignore """ The in block serves three different purposes depending on the alias that is used. diff --git a/TagScriptEngine/block/require_blacklist.py b/TagScriptEngine/block/require_blacklist.py index 513f820..fb4c391 100644 --- a/TagScriptEngine/block/require_blacklist.py +++ b/TagScriptEngine/block/require_blacklist.py @@ -1,15 +1,15 @@ from __future__ import annotations -from typing import Optional, Tuple, Type, cast +from typing import Optional, Tuple, cast -from ..interface import verb_required_block, Block +from ..interface import verb_required_block from ..interpreter import Context __all__: Tuple[str, ...] = ("RequireBlock", "BlacklistBlock") -class RequireBlock(cast(Type[Block], verb_required_block(True, parameter=True))): +class RequireBlock(verb_required_block(True, parameter=True)): # type: ignore """ The require block will attempt to convert the given parameter into a channel or role, using name or ID. If the user running the tag is not in the targeted @@ -45,7 +45,7 @@ def process(self, ctx: Context) -> Optional[str]: return "" -class BlacklistBlock(cast(Type[Block], verb_required_block(True, parameter=True))): +class BlacklistBlock(verb_required_block(True, parameter=True)): # type: ignore """ The blacklist block will attempt to convert the given parameter into a channel or role, using name or ID. If the user running the tag is in the targeted diff --git a/TagScriptEngine/block/stopblock.py b/TagScriptEngine/block/stopblock.py index 8e84391..3796db8 100644 --- a/TagScriptEngine/block/stopblock.py +++ b/TagScriptEngine/block/stopblock.py @@ -1,9 +1,9 @@ from __future__ import annotations -from typing import Optional, Tuple, Type, cast +from typing import Optional, Tuple, cast from ..exceptions import StopError -from ..interface import verb_required_block, Block +from ..interface import verb_required_block from ..interpreter import Context from . import helper_parse_if @@ -11,7 +11,7 @@ __all__: Tuple[str, ...] = ("StopBlock",) -class StopBlock(cast(Type[Block], verb_required_block(True, parameter=True))): +class StopBlock(verb_required_block(True, parameter=True)): # type: ignore """ The stop block stops tag processing if the given parameter is true. If a message is passed to the payload it will return that message. diff --git a/TagScriptEngine/block/substr.py b/TagScriptEngine/block/substr.py index 42e85f6..7bdb2f1 100644 --- a/TagScriptEngine/block/substr.py +++ b/TagScriptEngine/block/substr.py @@ -1,15 +1,15 @@ from __future__ import annotations -from typing import Optional, Tuple, Type, cast +from typing import Optional, Tuple, cast -from ..interface import verb_required_block, Block +from ..interface import verb_required_block from ..interpreter import Context __all__: Tuple[str, ...] = ("SubstringBlock",) -class SubstringBlock(cast(Type[Block], verb_required_block(True, parameter=True))): +class SubstringBlock(verb_required_block(True, parameter=True)): # type: ignore ACCEPTED_NAMES: Tuple[str, ...] = ("substr", "substring") def process(self, ctx: Context) -> Optional[str]: diff --git a/TagScriptEngine/block/urlencodeblock.py b/TagScriptEngine/block/urlencodeblock.py index 31a0d0d..e5ca134 100644 --- a/TagScriptEngine/block/urlencodeblock.py +++ b/TagScriptEngine/block/urlencodeblock.py @@ -1,14 +1,14 @@ -from typing import Tuple, Type, cast +from typing import Tuple, cast from urllib.parse import quote, quote_plus -from ..interface import verb_required_block, Block +from ..interface import verb_required_block from ..interpreter import Context __all__: Tuple[str, ...] = ("URLEncodeBlock",) -class URLEncodeBlock(cast(Type[Block], verb_required_block(True, payload=True))): +class URLEncodeBlock(verb_required_block(True, payload=True)): # type: ignore """ This block will encode a given string into a properly formatted url with non-url compliant characters replaced. Using ``+`` as the parameter diff --git a/setup.cfg b/setup.cfg index ad7f115..14df52d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,6 +36,9 @@ python_requires = >=3.8 [options.extras_require] discord = discord.py>=2.4.0 +all = + orjson + discord.py>=2.4.0 [options.packages.find] include = From 3cef2eca23c61ecb61c67e441b1edfdd86ba2ba5 Mon Sep 17 00:00:00 2001 From: japandotorg Date: Sun, 28 Jul 2024 07:38:58 +0530 Subject: [PATCH 4/4] Remove requiremeta and bump tse --- TagScriptEngine/__init__.py | 2 +- TagScriptEngine/interface/block.py | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/TagScriptEngine/__init__.py b/TagScriptEngine/__init__.py index 33b01a1..581aff7 100644 --- a/TagScriptEngine/__init__.py +++ b/TagScriptEngine/__init__.py @@ -170,7 +170,7 @@ ) -__version__: Final[str] = "3.2.1" +__version__: Final[str] = "3.2.2" class VersionNamedTuple(NamedTuple): diff --git a/TagScriptEngine/interface/block.py b/TagScriptEngine/interface/block.py index 605a6b0..5ec1210 100644 --- a/TagScriptEngine/interface/block.py +++ b/TagScriptEngine/interface/block.py @@ -1,7 +1,7 @@ from __future__ import annotations from functools import lru_cache -from typing import TYPE_CHECKING, Any, Optional, Protocol, Tuple, cast +from typing import TYPE_CHECKING, Any, Optional, Protocol, Tuple, Type, cast if TYPE_CHECKING: from ..interpreter import Context @@ -96,7 +96,7 @@ def verb_required_block( *, parameter: bool = False, payload: bool = False, -) -> Block: +) -> Type[Block]: """ Get a Block subclass that requires a verb to implicitly or explicitly have a parameter or payload passed. @@ -111,12 +111,8 @@ def verb_required_block( Passing True will cause the block to require the payload to be passed. """ check = (lambda x: x) if implicit else (lambda x: x is not None) - - class RequireMeta(type): - def __repr__(self): - return f"VerbRequiredBlock(implicit={implicit!r}, payload={payload!r}, parameter={parameter!r})" - - class VerbRequiredBlock(Block, metaclass=RequireMeta): + + class VerbRequiredBlock(Block): @classmethod def will_accept(cls, ctx: Context) -> bool: verb = ctx.verb @@ -125,5 +121,4 @@ def will_accept(cls, ctx: Context) -> bool: if parameter and not check(verb.parameter): return False return super().will_accept(ctx) - - return VerbRequiredBlock # type: ignore + return VerbRequiredBlock