forked from prostomarkeloff/vkwave
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
437 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import vkwave.bots.vkscript.handlers.assignments | ||
import vkwave.bots.vkscript.handlers.blocks | ||
import vkwave.bots.vkscript.handlers.calls | ||
import vkwave.bots.vkscript.handlers.expressions | ||
import vkwave.bots.vkscript.handlers.statements | ||
import vkwave.bots.vkscript.handlers.types | ||
from .converter import VKScriptConverter | ||
from .execute import Execute | ||
from .execute import execute | ||
|
||
|
||
__all__ = ("execute", "Execute", "VKScriptConverter") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import typing | ||
import contextvars | ||
|
||
import pydantic | ||
|
||
from typing import Type | ||
from typing import TypeVar | ||
|
||
|
||
T = TypeVar("T") | ||
|
||
|
||
class ContextInstanceMixin: | ||
def __init_subclass__(cls, **kwargs): | ||
cls.__context_instance = contextvars.ContextVar("instance_" + cls.__name__) | ||
return cls | ||
|
||
@classmethod | ||
def get_current(cls: Type[T], no_error=True) -> T: | ||
if no_error: | ||
return cls.__context_instance.get(None) | ||
return cls.__context_instance.get() | ||
|
||
@classmethod | ||
def set_current(cls: Type[T], value: T): | ||
if not isinstance(value, cls): | ||
raise TypeError( | ||
f"Value should be instance of '{cls.__name__}' not '{type(value).__name__}'" | ||
) | ||
cls.__context_instance.set(value) | ||
|
||
|
||
class Scope(pydantic.BaseModel): | ||
locals: list = [] | ||
globals: dict = {} | ||
|
||
|
||
class VKScriptConverter(ContextInstanceMixin): | ||
handlers: dict = {} | ||
|
||
@classmethod | ||
def register(cls, expr): | ||
def meta(handler: typing.Callable): | ||
cls.handlers[expr] = handler | ||
|
||
return meta | ||
|
||
def __init__(self, scope: Scope = None): | ||
self.scope = scope or Scope() | ||
self.set_current(self) | ||
|
||
def convert_node(self, node): | ||
if node.__class__ in self.handlers: | ||
return self.handlers[node.__class__](node) | ||
raise NotImplementedError(f"Conversion for type {node.__class__} not implemented.") | ||
|
||
def convert_block(self, nodes: list): | ||
return "".join(self.convert_node(child) for child in nodes) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import ast | ||
import inspect | ||
import types | ||
|
||
from vkwave.bots.vkscript.converter import Scope | ||
from vkwave.bots.vkscript.converter import VKScriptConverter | ||
|
||
|
||
def execute(func: types.FunctionType): | ||
e = Execute() | ||
return e.decorate(func) | ||
|
||
|
||
class Execute: | ||
_code = None | ||
_preprocessor = None | ||
_func = None | ||
|
||
def decorate(self, func): | ||
source = inspect.getsource(func) | ||
self._func = func | ||
self._code = ast.parse(source).body[0] | ||
return self | ||
|
||
def preprocessor(self, func): | ||
self._preprocessor = func | ||
|
||
def build(self, *args, **kwargs) -> str: | ||
if self._code.__class__ == ast.FunctionDef: | ||
globals_ = dict(self._func.__globals__) | ||
for i, argument in enumerate(self._code.args.args): | ||
if argument.arg in kwargs: | ||
globals_[argument.arg] = kwargs[argument.arg] | ||
elif i < len(args): | ||
globals_[argument.arg] = args[i] | ||
elif argument.arg.upper() == "API": | ||
continue | ||
else: | ||
raise TypeError(f"missing required argument {argument.arg}") | ||
converter = VKScriptConverter(Scope(globals=globals_)) | ||
return converter.convert_block(self._code.body) | ||
raise NotImplementedError() | ||
|
||
async def __call__(self, *args, **kwargs): | ||
if self._preprocessor is not None: | ||
return await self._preprocessor(*args, **kwargs) | ||
return await self.execute(*args, **kwargs) | ||
|
||
async def execute(self, *args, **kwargs): | ||
vk = _get_vk().get_current() | ||
code = self.build(*args, **kwargs) | ||
response = await vk.api_request("execute", {"code": code}) | ||
return response | ||
|
||
e = execute |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import ast | ||
|
||
from ..converter import VKScriptConverter | ||
from .expressions import OPS | ||
|
||
|
||
@VKScriptConverter.register(ast.Assign) | ||
def assign_handler(node: ast.Assign): | ||
converter = VKScriptConverter.get_current() | ||
left = node.targets | ||
left_ = [] | ||
for target in left: | ||
if target.__class__ == ast.Name: | ||
left_.append(target.id) | ||
converter.scope.locals.append(target.id) | ||
elif target.__class__ == ast.Subscript: | ||
pass | ||
elif target.__class__ == ast.Tuple: | ||
raise NotImplementedError("Tuple assignments are not allowed") | ||
else: | ||
raise NotImplementedError(f"Assignments of {target.__class__} are not implemented") | ||
|
||
right = converter.convert_node(node.value) | ||
return "var " + ",".join(f"{target}={right}" for target in left_) + ";" | ||
|
||
|
||
@VKScriptConverter.register(ast.AugAssign) | ||
def aug_assign_handler(node: ast.AugAssign): | ||
converter = VKScriptConverter.get_current() | ||
|
||
if node.op.__class__ not in OPS: | ||
raise NotImplementedError(f"Operation {node.op} is not implemented.") | ||
|
||
if node.target.__class__ == ast.Name and node.target.id not in converter.scope.locals: | ||
raise NameError(f"name '{node.target.id}' is not defined") | ||
target = converter.convert_node(node.target) | ||
return f"{target}={target}{OPS[node.op.__class__]}({converter.convert_node(node.value)});" | ||
|
||
|
||
@VKScriptConverter.register(ast.Name) | ||
def name_handler(node: ast.Name): | ||
converter = VKScriptConverter.get_current() | ||
if node.id in converter.scope.locals: | ||
return node.id | ||
if node.id not in converter.scope.globals: | ||
raise NameError(f"name '{node.id}' is not defined") | ||
if ( | ||
type(converter.scope.globals[node.id]) not in (str, int, tuple, dict, list) | ||
and converter.scope.globals[node.id] is not None | ||
): | ||
raise NotImplementedError( | ||
f'type "{type(converter.scope.globals[node.id])}" not allowed inside VK Script' | ||
) | ||
return converter.convert_node( | ||
ast.parse(repr(converter.scope.globals[node.id]), mode="eval").body | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import ast | ||
|
||
from ..converter import VKScriptConverter | ||
|
||
WHILE_TEMPLATE = "while(%(test)s){%(body)s};" | ||
IF_TEMPLATE = "if(%(test)s){%(content)s}%(other)s;" | ||
|
||
|
||
@VKScriptConverter.register(ast.While) | ||
def while_handler(node: ast.While): | ||
converter = VKScriptConverter.get_current() | ||
if node.orelse: | ||
raise NotImplementedError("while...else not implemented.") | ||
test = converter.convert_node(node.test) | ||
body = converter.convert_block(node.body) | ||
return WHILE_TEMPLATE % {"test": test, "body": body} | ||
|
||
|
||
@VKScriptConverter.register(ast.If) | ||
def if_handler(node: ast.If): | ||
converter = VKScriptConverter.get_current() | ||
test = converter.convert_node(node.test) | ||
content = converter.convert_block(node.body) | ||
if node.orelse: | ||
other = f"else{{{converter.convert_block(node.orelse)}}}" | ||
else: | ||
other = "" | ||
return IF_TEMPLATE % {"test": test, "content": content, "other": other} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import ast | ||
|
||
from ..converter import VKScriptConverter | ||
|
||
|
||
@VKScriptConverter.register(ast.Call) | ||
def call_handler(node: ast.Call): | ||
converter = VKScriptConverter.get_current() | ||
replacements = {"append": "push", "pop": "pop", "split": "split"} | ||
funcs = [ | ||
"slice", | ||
"push", | ||
"pop", | ||
"shift", | ||
"unshift", | ||
"splice", | ||
"substr", | ||
"split", | ||
"append", | ||
] | ||
attrs = [] | ||
node_ = node.func | ||
while isinstance(node_, ast.Attribute): | ||
attrs.append(node_.attr) | ||
node_ = node_.value | ||
|
||
if node_.id.upper() == "API" and len(attrs) >= 1: | ||
if node.args: | ||
raise TypeError("api calls does not accept positional arguments") | ||
|
||
inner = ",".join( | ||
f"{keyword.arg}:{converter.convert_node(keyword.value)}" for keyword in node.keywords | ||
) | ||
|
||
return "API." + ".".join(attrs[::-1]) + f"({{{inner}}})" | ||
|
||
elif not attrs and node_.id == "len" and "len" not in converter.scope.globals: | ||
return f"{converter.convert_node(node.args[0])}.length" | ||
|
||
# TODO: rewrite? | ||
elif len(attrs) == 1 and attrs[0] in funcs: | ||
if node.keywords: | ||
raise NotImplementedError("keywords not allowed") | ||
|
||
if attrs[0] in replacements: | ||
func = replacements[attrs[0]] | ||
else: | ||
func = attrs[0] | ||
|
||
return ( | ||
f"{converter.convert_node(node_)}.{func}" | ||
f'({",".join(converter.convert_node(arg) for arg in node.args)})' | ||
) | ||
|
||
else: | ||
raise NotImplementedError( | ||
f'function call "{node_.id + ("." if attrs else "") + ".".join(attrs[::-1])}" not allowed inside VK Script!' | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import ast | ||
import string | ||
|
||
from ..converter import VKScriptConverter | ||
|
||
|
||
OPS = { | ||
ast.Add: "+", | ||
ast.Sub: "-", | ||
ast.Mult: "*", | ||
ast.Div: "/", | ||
ast.Pow: "**", | ||
ast.RShift: ">>", | ||
ast.LShift: "<<", | ||
ast.BitOr: "|", | ||
ast.BitAnd: "&", | ||
ast.Mod: "%", | ||
} | ||
|
||
|
||
@VKScriptConverter.register(ast.Expr) | ||
def expr_handler(node: ast.Expr): | ||
converter = VKScriptConverter.get_current() | ||
return converter.convert_node(node.value) + ";" | ||
|
||
|
||
@VKScriptConverter.register(ast.Module) | ||
def module_handler(node: ast.Module): | ||
converter = VKScriptConverter.get_current() | ||
return converter.convert_block(node.body) | ||
|
||
|
||
@VKScriptConverter.register(ast.BinOp) | ||
def bin_op_handler(node: ast.BinOp): | ||
if node.op.__class__ not in OPS: | ||
raise NotImplementedError(f"Operation {node.op} is not implemented.") | ||
converter = VKScriptConverter.get_current() | ||
return f"{converter.convert_node(node.left)}{OPS[node.op.__class__]}{converter.convert_node(node.right)}" | ||
|
||
|
||
@VKScriptConverter.register(ast.Compare) | ||
def compare_handler(node: ast.Compare): | ||
ops = { | ||
ast.Gt: ">", | ||
ast.Lt: "<", | ||
ast.GtE: ">=", | ||
ast.LtE: "<=", | ||
ast.Eq: "==", | ||
ast.NotEq: "!=", | ||
} | ||
|
||
operations = [] | ||
converter = VKScriptConverter.get_current() | ||
left = converter.convert_node(node.left) | ||
for op, comparator in zip(node.ops, node.comparators): | ||
if op.__class__ not in ops: | ||
raise NotImplementedError(f"comparison operator {op} not supported") | ||
operations.append(f"{left}{ops[op.__class__]}{converter.convert_node(comparator)}") | ||
return "&&".join(operations) | ||
|
||
|
||
@VKScriptConverter.register(ast.BoolOp) | ||
def bool_op_handler(node: ast.BoolOp): | ||
ops = {ast.And: "&&", ast.Or: "||"} | ||
if node.op.__class__ not in ops: | ||
raise NotImplementedError(f"operation '{node.op}' not supported") | ||
converter = VKScriptConverter.get_current() | ||
return ops[node.op.__class__].join(converter.convert_node(value) for value in node.values) | ||
|
||
|
||
@VKScriptConverter.register(ast.UnaryOp) | ||
def unary_op_handler(node: ast.UnaryOp): | ||
ops = {ast.UAdd: "+", ast.USub: "-"} | ||
if node.op.__class__ not in ops: | ||
raise NotImplementedError(f"operation '{node.op}' not supported") | ||
converter = VKScriptConverter.get_current() | ||
return f"{ops[node.op.__class__]}{converter.convert_node(node.operand)}" | ||
|
||
|
||
@VKScriptConverter.register(ast.Subscript) | ||
def subscript_handler(node: ast.Subscript): | ||
converter = VKScriptConverter.get_current() | ||
value = converter.convert_node(node.value) | ||
if node.slice.__class__ == ast.Index: | ||
safe = frozenset(string.ascii_letters + string.digits + "_") | ||
if node.slice.value.__class__ == ast.Str and set(node.slice.value.s) <= safe: | ||
# TODO: Improve safety check, first symbol may be digit | ||
return f"{value}.{node.slice.value.s}" | ||
return f"{value}[{converter.convert_node(node.slice.value)}]" | ||
elif node.slice.__class__ == ast.Slice: | ||
if node.slice.step: | ||
raise NotImplementedError("steps in slice not supported") | ||
if node.slice.lower.__class__ != ast.Num and node.slice.upper.__class__ != ast.Num: | ||
raise TypeError("slices must be integers") | ||
lower = node.slice.lower.n or 0 | ||
if node.slice.upper: | ||
return f"{value}.slice({lower},{node.slice.upper.n})" | ||
return f"{value}.slice({lower})" | ||
raise NotImplementedError(f"slice {node.slice} not supported") | ||
|
||
|
||
@VKScriptConverter.register(ast.Attribute) | ||
def attribute_handler(node: ast.Attribute): | ||
converter = VKScriptConverter.get_current() | ||
return f"{converter.convert_node(node.value)}.{node.attr}" |
Oops, something went wrong.