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 0ccf611..2ab2f42 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,16 @@
## Information
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
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/).
@@ -21,7 +28,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:
@@ -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..581aff7 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.2"
+
+
+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..4a44dc0 100644
--- a/TagScriptEngine/block/assign.py
+++ b/TagScriptEngine/block/assign.py
@@ -1,11 +1,16 @@
-from typing import Optional
+from __future__ import annotations
+
+from typing import Optional, Tuple
from ..adapter import StringAdapter
from ..interface import verb_required_block
from ..interpreter import Context
-class AssignmentBlock(verb_required_block(False, parameter=True)):
+__all__: Tuple[str, ...] = ("AssignmentBlock",)
+
+
+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.
@@ -29,7 +34,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..0cd0ddb 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
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(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.
@@ -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..e0283c5 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, 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(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 ``|``.
@@ -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(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 ``|``.
@@ -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(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.
@@ -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..c6345c5 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, 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 .helpers import helper_split
-__all__ = ("CooldownBlock",)
+__all__: Tuple[str, ...] = ("CooldownBlock",)
-class CooldownBlock(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.
@@ -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..0fb57b3 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, cast
from ..interface import verb_required_block
from ..interpreter import Context
-class CountBlock(verb_required_block(True, payload=True)):
+__all__: Tuple[str, ...] = ("CountBlock", "LengthBlock")
+
+
+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
@@ -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(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
@@ -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..c202a80 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
from ..interface import verb_required_block
from ..interpreter import Context
-class FiftyFiftyBlock(verb_required_block(True, payload=True)):
+__all__: Tuple[str, ...] = ("FiftyFiftyBlock",)
+
+
+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.
@@ -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..add8126 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, cast
from ..interface import verb_required_block
from ..interpreter import Context
-class RandomBlock(verb_required_block(True, payload=True)):
+__all__: Tuple[str, ...] = ("RandomBlock",)
+
+
+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
@@ -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..e8b3a82 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, cast
from ..interface import verb_required_block
from ..interpreter import Context
-class RangeBlock(verb_required_block(True, payload=True)):
+__all__: Tuple[str, ...] = ("RangeBlock",)
+
+
+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.
@@ -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..163786b 100644
--- a/TagScriptEngine/block/redirect.py
+++ b/TagScriptEngine/block/redirect.py
@@ -1,10 +1,15 @@
-from typing import Optional
+from __future__ import annotations
+
+from typing import Optional, Tuple, cast
from ..interface import verb_required_block
from ..interpreter import Context
-class RedirectBlock(verb_required_block(True, parameter=True)):
+__all__: Tuple[str, ...] = ("RedirectBlock",)
+
+
+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.
@@ -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..cdbfd35 100644
--- a/TagScriptEngine/block/replaceblock.py
+++ b/TagScriptEngine/block/replaceblock.py
@@ -1,8 +1,15 @@
+from __future__ import annotations
+
+from typing import Optional, Tuple, cast
+
from ..interface import verb_required_block
from ..interpreter import Context
-class ReplaceBlock(verb_required_block(True, payload=True, parameter=True)):
+__all__: Tuple[str, ...] = ("ReplaceBlock", "PythonBlock")
+
+
+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
@@ -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(verb_required_block(True, payload=True, parameter=True)): # type: ignore
"""
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..fb4c391 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 typing import Optional, Tuple, cast
from ..interface import verb_required_block
from ..interpreter import Context
-class RequireBlock(verb_required_block(True, parameter=True)):
+__all__: Tuple[str, ...] = ("RequireBlock", "BlacklistBlock")
+
+
+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
@@ -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(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
@@ -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..3796db8 100644
--- a/TagScriptEngine/block/stopblock.py
+++ b/TagScriptEngine/block/stopblock.py
@@ -1,4 +1,6 @@
-from typing import Optional
+from __future__ import annotations
+
+from typing import Optional, Tuple, cast
from ..exceptions import StopError
from ..interface import verb_required_block
@@ -6,7 +8,10 @@
from . import helper_parse_if
-class StopBlock(verb_required_block(True, parameter=True)):
+__all__: Tuple[str, ...] = ("StopBlock",)
+
+
+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.
@@ -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..7bdb2f1 100644
--- a/TagScriptEngine/block/substr.py
+++ b/TagScriptEngine/block/substr.py
@@ -1,20 +1,25 @@
-from typing import Optional
+from __future__ import annotations
+
+from typing import Optional, Tuple, cast
from ..interface import verb_required_block
from ..interpreter import Context
-class SubstringBlock(verb_required_block(True, parameter=True)):
- ACCEPTED_NAMES = ("substr", "substring")
+__all__: Tuple[str, ...] = ("SubstringBlock",)
+
+
+class SubstringBlock(verb_required_block(True, parameter=True)): # type: ignore
+ 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..e5ca134 100644
--- a/TagScriptEngine/block/urlencodeblock.py
+++ b/TagScriptEngine/block/urlencodeblock.py
@@ -1,10 +1,14 @@
+from typing import Tuple, cast
from urllib.parse import quote, quote_plus
from ..interface import verb_required_block
from ..interpreter import Context
-class URLEncodeBlock(verb_required_block(True, payload=True)):
+__all__: Tuple[str, ...] = ("URLEncodeBlock",)
+
+
+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
@@ -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..5ec1210 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, Type, 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
@@ -81,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.
@@ -96,18 +111,14 @@ 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):
- def will_accept(self, ctx: Context) -> bool:
+
+ class VerbRequiredBlock(Block):
+ @classmethod
+ def will_accept(cls, ctx: Context) -> bool:
verb = ctx.verb
if payload and not check(verb.payload):
return False
if parameter and not check(verb.parameter):
return False
return super().will_accept(ctx)
-
return VerbRequiredBlock
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 0000000..9eb147e
Binary files /dev/null and b/docs/_static/favicon.ico differ
diff --git a/docs/_static/logo.png b/docs/_static/logo.png
new file mode 100644
index 0000000..7261383
Binary files /dev/null and b/docs/_static/logo.png differ
diff --git a/docs/conf.py b/docs/conf.py
index e8b74bd..edd325c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -18,7 +18,7 @@
# -- Project information -----------------------------------------------------
-project = "TagScript"
+project = "AdvancedTagScriptEngine"
copyright = "2021, JonSnowbd, PhenoM4n4n & inthedark.org"
author = "JonSnowbd, PhenoM4n4n & inthedark.org"
@@ -51,8 +51,15 @@
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
-# html_theme = "karma_sphinx_theme"
-html_theme = "sphinx_rtd_theme"
+html_theme = "press"
+html_logo = "_static/logo.png"
+html_favicon = "_static/favicon.ico"
+html_theme_options = {
+ "external_links": [
+ ("Github", "https://github.com/japandotorg/AdvancedTagScriptEngine"),
+ ("Discord", "https://discord.gg/AyMrA7KMSp"),
+ ]
+}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
@@ -70,4 +77,5 @@
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"dpy": ("https://discordpy.readthedocs.io/en/stable/", None),
+ "red": ("https://docs.discord.red/en/stable/index.html", None),
}
diff --git a/docs/credits.rst b/docs/credits.rst
index 23f19e1..2a281b9 100644
--- a/docs/credits.rst
+++ b/docs/credits.rst
@@ -4,6 +4,7 @@ Credits
Thank you to the following users who contributed to this documentation!
+* **japandotorg** ``inthedark.org```
* **PhenoM4n4n** ``phenom4n4n``
* **sravan** ``sravan#0001``
* **Anik** ``aniksarker_21``
diff --git a/docs/getting_started.rst b/docs/getting_started.rst
index dec68b6..5abbfb0 100644
--- a/docs/getting_started.rst
+++ b/docs/getting_started.rst
@@ -3,5 +3,5 @@ Getting Started
===============
Please refer to existing TagScript implementations such as my
-`Tags cog `_
+`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..14df52d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -14,21 +14,42 @@ 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
+all =
+ orjson
+ discord.py>=2.4.0
+
[options.packages.find]
include =
TagScriptEngine
TagScriptEngine.*
+exclude =
+ docs*
+ tests*
+
+[flake8]
+exclude =
+ build
+ dist
+ docs/conf.py