mirror of
https://github.com/weechat/weechat.git
synced 2026-06-19 09:34:47 +02:00
1448 lines
41 KiB
Python
Executable File
1448 lines
41 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# SPDX-FileCopyrightText: 2017-2026 Sébastien Helleu <flashcode@flashtux.org>
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
"""Unparse AST tree to generate script.
|
|
|
|
All languages supported by WeeChat can be used: Python, Perl, Ruby, etc.
|
|
"""
|
|
|
|
# ruff: noqa: B905,T201,UP006,UP007,UP035
|
|
|
|
import argparse
|
|
import ast
|
|
import inspect
|
|
import io
|
|
import os
|
|
import select
|
|
import sys
|
|
from typing import Any, List, Union
|
|
|
|
sys.dont_write_bytecode = True
|
|
|
|
|
|
class Unparse:
|
|
"""Unparse AST to generate script code."""
|
|
|
|
|
|
class UnparsePython(Unparse):
|
|
"""Unparse AST to generate Python script code.
|
|
|
|
This class is inspired from _Unparser class in cpython:
|
|
https://github.com/python/cpython/blob/main/Lib/ast.py
|
|
|
|
Note: only part of AST types are supported (just the types used by
|
|
the script to test WeeChat scripting API).
|
|
"""
|
|
|
|
__lineno__ = inspect.currentframe().f_lineno
|
|
|
|
def __init__(self, output: io.TextIOBase = sys.stdout) -> None:
|
|
"""Initialize Python parser."""
|
|
self.output = output
|
|
self.indent_string = " " * 4
|
|
self._indent_level = 0
|
|
self._prefix = [] # not used in Python, only in other languages
|
|
self.binop = {
|
|
"Add": "+",
|
|
"Sub": "-",
|
|
"Mult": "*",
|
|
"MatMult": "@",
|
|
"Div": "/",
|
|
"Mod": "%",
|
|
"LShift": "<<",
|
|
"RShift": ">>",
|
|
"BitOr": "|",
|
|
"BitXor": "^",
|
|
"BitAnd": "&",
|
|
"FloorDiv": "//",
|
|
"Pow": "**",
|
|
}
|
|
self.unaryop = {
|
|
"Invert": "~",
|
|
"Not": "not ",
|
|
"UAdd": "+",
|
|
"USub": "-",
|
|
}
|
|
self.cmpop = {
|
|
"Eq": "==",
|
|
"NotEq": "!=",
|
|
"Lt": "<",
|
|
"LtE": "<=",
|
|
"Gt": ">",
|
|
"GtE": ">=",
|
|
"Is": "is",
|
|
"IsNot": "is not",
|
|
"In": "in",
|
|
"NotIn": "not in",
|
|
}
|
|
|
|
def fill(self, string: str = "") -> None:
|
|
"""Add a new line and an indented string."""
|
|
self.add(f"\n{self.indent_string * self._indent_level}{string}")
|
|
|
|
def indent(self) -> None:
|
|
"""Indent code."""
|
|
self._indent_level += 1
|
|
|
|
def unindent(self) -> None:
|
|
"""Unindent code."""
|
|
self._indent_level -= 1
|
|
|
|
def prefix(self, prefix: Union[str, None]) -> None:
|
|
"""Add or remove a prefix from list."""
|
|
if prefix:
|
|
self._prefix.append(prefix)
|
|
else:
|
|
self._prefix.pop()
|
|
|
|
def add(self, *args) -> None: # noqa: ANN002
|
|
"""Add string/node(s) to the output file."""
|
|
for arg in args:
|
|
if callable(arg):
|
|
arg()
|
|
elif isinstance(arg, tuple):
|
|
arg[0](*arg[1:])
|
|
elif isinstance(arg, list):
|
|
for item in arg:
|
|
self.add(item)
|
|
elif isinstance(arg, ast.AST):
|
|
method = getattr(self, f"_ast_{arg.__class__.__name__.lower()}", None)
|
|
if method is None:
|
|
raise NotImplementedError(arg)
|
|
method(arg)
|
|
elif isinstance(arg, str):
|
|
self.output.write(arg)
|
|
|
|
@staticmethod
|
|
def make_list(values: List[Any], sep: str = ", ") -> list:
|
|
"""Add multiple values using a custom method and separator."""
|
|
result = []
|
|
for value in values:
|
|
if result:
|
|
result.append(sep)
|
|
result.append(value)
|
|
return result
|
|
|
|
@staticmethod
|
|
def is_bool(node: ast.AST) -> bool:
|
|
"""Check if the node is a boolean."""
|
|
return isinstance(node, ast.Name) and node.id in ("False", "True")
|
|
|
|
@staticmethod
|
|
def is_number(node: ast.AST) -> bool:
|
|
"""Check if the node is a number."""
|
|
return (isinstance(node, ast.Constant) and isinstance(node.value, int)) or (
|
|
isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub))
|
|
)
|
|
|
|
def _ast_alias(self, node: ast.AST) -> None:
|
|
"""Add an AST alias in output."""
|
|
# ignore alias
|
|
pass # noqa: PIE790
|
|
|
|
def _ast_arg(self, node: ast.AST) -> None:
|
|
"""Add an AST arg in output."""
|
|
prefix = self._prefix[-1] if self._prefix else ""
|
|
self.add(f"{prefix}{node.arg}")
|
|
|
|
def _ast_assign(self, node: ast.Assign) -> None:
|
|
"""Add an AST Assign in output."""
|
|
self.add(
|
|
self.fill,
|
|
[[target, " = "] for target in node.targets],
|
|
node.value,
|
|
)
|
|
|
|
def _ast_attribute(self, node: ast.Attribute) -> None:
|
|
"""Add an AST Attribute in output."""
|
|
self.add(node.value, ".", node.attr)
|
|
|
|
def _ast_binop(self, node: ast.BinOp) -> None:
|
|
"""Add an AST BinOp in output."""
|
|
self.add(
|
|
node.left,
|
|
f" {self.binop[node.op.__class__.__name__]} ",
|
|
node.right,
|
|
)
|
|
|
|
def _ast_call(self, node: ast.Call) -> None:
|
|
"""Add an AST Call in output."""
|
|
self.add(
|
|
node.func,
|
|
"(",
|
|
self.make_list(node.args),
|
|
")",
|
|
)
|
|
|
|
def _ast_compare(self, node: ast.Compare) -> None:
|
|
"""Add an AST Compare in output."""
|
|
self.add(node.left)
|
|
for operator, comparator in zip(node.ops, node.comparators):
|
|
self.add(
|
|
f" {self.cmpop[operator.__class__.__name__]} ",
|
|
comparator,
|
|
)
|
|
|
|
def _ast_constant(self, node: ast.Constant) -> None:
|
|
"""Add an AST Constant in output."""
|
|
self.add(repr(node.value))
|
|
|
|
def _ast_dict(self, node: ast.Dict) -> None:
|
|
"""Add an AST Dict in output."""
|
|
self.add(
|
|
"{",
|
|
self.make_list([[key, ": ", value] for key, value in zip(node.keys, node.values)]),
|
|
"}",
|
|
)
|
|
|
|
def _ast_expr(self, node: ast.Expr) -> None:
|
|
"""Add an AST Expr in output."""
|
|
if not isinstance(node.value, ast.Constant): # ignore docstrings
|
|
self.add(
|
|
self.fill,
|
|
node.value,
|
|
)
|
|
|
|
def _ast_functiondef(self, node: ast.FunctionDef) -> None:
|
|
"""Add an AST FunctionDef in output."""
|
|
self.add(
|
|
self.fill,
|
|
self.fill,
|
|
self.fill if self._indent_level == 0 else None,
|
|
f"def {node.name}(",
|
|
self.make_list(node.args.args),
|
|
"):",
|
|
self.indent,
|
|
node.body,
|
|
self.unindent,
|
|
)
|
|
|
|
def _ast_if(self, node: ast.If) -> None:
|
|
"""Add an AST If in output."""
|
|
self.add(
|
|
self.fill,
|
|
"if ",
|
|
node.test,
|
|
":",
|
|
self.indent,
|
|
node.body,
|
|
self.unindent,
|
|
)
|
|
if node.orelse:
|
|
self.add(
|
|
self.fill,
|
|
"else:",
|
|
self.indent,
|
|
node.orelse,
|
|
self.unindent,
|
|
)
|
|
|
|
def _ast_index(self, node: ast.Index) -> None:
|
|
"""Add an AST Subscript in output."""
|
|
# note: deprecated since Python 3.9
|
|
self.add(node.value)
|
|
|
|
def _ast_import(self, node: ast.AST) -> None:
|
|
"""Add an AST Import in output."""
|
|
# ignore import
|
|
pass # noqa: PIE790
|
|
|
|
def _ast_list(self, node: ast.AST) -> None:
|
|
"""Add an AST List in output."""
|
|
self.add(
|
|
"[",
|
|
self.make_list(node.elts),
|
|
"]",
|
|
)
|
|
|
|
def _ast_module(self, node: ast.Module) -> None:
|
|
"""Add an AST Module in output."""
|
|
self.add(node.body)
|
|
|
|
def _ast_name(self, node: ast.Name) -> None:
|
|
"""Add an AST Name in output."""
|
|
prefix = self._prefix[-1] if self._prefix else ""
|
|
self.add(f"{prefix}{node.id}")
|
|
|
|
def _ast_num(self, node: ast.Num) -> None:
|
|
"""Add an AST Num in output."""
|
|
self.add(repr(node.n))
|
|
|
|
def _ast_pass(self, node: ast.Pass) -> None: # noqa: ARG002
|
|
"""Add an AST Pass in output."""
|
|
self.fill("pass")
|
|
|
|
def _ast_return(self, node: ast.Return) -> None:
|
|
"""Add an AST Return in output."""
|
|
self.fill("return")
|
|
if node.value:
|
|
self.add(" ", node.value)
|
|
|
|
def _ast_str(self, node: ast.Str) -> None:
|
|
"""Add an AST Str in output."""
|
|
self._ast_constant(node)
|
|
|
|
def _ast_subscript(self, node: ast.Subscript) -> None:
|
|
"""Add an AST Subscript in output."""
|
|
self.add(
|
|
node.value,
|
|
"[",
|
|
node.slice,
|
|
"]",
|
|
)
|
|
|
|
def _ast_tuple(self, node: ast.Tuple) -> None:
|
|
"""Add an AST Tuple in output."""
|
|
self.add(
|
|
"(",
|
|
self.make_list(node.elts),
|
|
"," if len(node.elts) == 1 else None,
|
|
")",
|
|
)
|
|
|
|
def _ast_unaryop(self, node: ast.UnaryOp) -> None:
|
|
"""Add an AST UnaryOp in output."""
|
|
self.add(
|
|
self.unaryop[node.op.__class__.__name__],
|
|
node.operand,
|
|
)
|
|
|
|
|
|
class UnparsePerl(UnparsePython):
|
|
"""Unparse AST to generate Perl script code.
|
|
|
|
Note: only part of AST types are supported (just the types used by
|
|
the script to test WeeChat scripting API).
|
|
"""
|
|
|
|
__lineno__ = inspect.currentframe().f_lineno
|
|
|
|
def _ast_assign(self, node: ast.Assign) -> None:
|
|
"""Add an AST Assign in output."""
|
|
self.add(
|
|
self.fill,
|
|
(self.prefix, "$"),
|
|
[[target, " = "] for target in node.targets],
|
|
(self.prefix, None),
|
|
node.value,
|
|
";",
|
|
)
|
|
|
|
def _ast_attribute(self, node: ast.Attribute) -> None:
|
|
"""Add an AST Attribute in output."""
|
|
saved_prefix = self._prefix
|
|
self._prefix = []
|
|
self.add(node.value, "::", node.attr)
|
|
self._prefix = saved_prefix
|
|
|
|
def _ast_binop(self, node: ast.BinOp) -> None:
|
|
"""Add an AST BinOp in output."""
|
|
if isinstance(node.op, ast.Add) and (not self.is_number(node.left) or not self.is_number(node.right)):
|
|
str_op = "."
|
|
else:
|
|
str_op = self.binop[node.op.__class__.__name__]
|
|
self.add(
|
|
(self.prefix, "$"),
|
|
node.left,
|
|
f" {str_op} ",
|
|
node.right,
|
|
(self.prefix, None),
|
|
)
|
|
|
|
def _ast_call(self, node: ast.Call) -> None:
|
|
"""Add an AST Call in output."""
|
|
self.add(
|
|
node.func,
|
|
"(",
|
|
(self.prefix, "$"),
|
|
self.make_list(node.args),
|
|
(self.prefix, None),
|
|
")",
|
|
)
|
|
|
|
def _ast_compare(self, node: ast.Compare) -> None:
|
|
"""Add an AST Compare in output."""
|
|
self.add(node.left)
|
|
for operator, comparator in zip(node.ops, node.comparators):
|
|
if (
|
|
isinstance(operator, (ast.Eq, ast.NotEq))
|
|
and not self.is_number(node.left)
|
|
and not self.is_bool(node.left)
|
|
and not self.is_number(comparator)
|
|
and not self.is_bool(comparator)
|
|
):
|
|
custom_cmpop = {
|
|
"Eq": "eq",
|
|
"NotEq": "ne",
|
|
}
|
|
else:
|
|
custom_cmpop = self.cmpop
|
|
self.add(
|
|
f" {custom_cmpop[operator.__class__.__name__]} ",
|
|
comparator,
|
|
)
|
|
|
|
def _ast_constant(self, node: ast.Constant) -> None:
|
|
"""Add an AST Constant in output."""
|
|
if isinstance(node.value, str):
|
|
str_node = node.value.replace("$", "\\$").replace("@", "\\@")
|
|
self.add(f'"{str_node}"')
|
|
elif node.value is None:
|
|
self.add("undef")
|
|
else:
|
|
self.add(repr(node.value))
|
|
|
|
def _ast_dict(self, node: ast.Dict) -> None:
|
|
"""Add an AST Dict in output."""
|
|
self.add(
|
|
"{",
|
|
self.make_list([[key, " => ", value] for key, value in zip(node.keys, node.values)]),
|
|
"}",
|
|
)
|
|
|
|
def _ast_expr(self, node: ast.Expr) -> None:
|
|
"""Add an AST Expr in output."""
|
|
if not isinstance(node.value, ast.Constant): # ignore docstrings
|
|
self.add(
|
|
self.fill,
|
|
node.value,
|
|
";",
|
|
)
|
|
|
|
def _ast_functiondef(self, node: ast.FunctionDef) -> None:
|
|
"""Add an AST FunctionDef in output."""
|
|
self.add(
|
|
self.fill,
|
|
self.fill,
|
|
f"sub {node.name}",
|
|
self.fill,
|
|
"{",
|
|
self.indent,
|
|
)
|
|
if node.args.args:
|
|
self.add(
|
|
self.fill,
|
|
"my (",
|
|
(self.prefix, "$"),
|
|
self.make_list(node.args.args),
|
|
(self.prefix, None),
|
|
") = @_;",
|
|
)
|
|
self.add(
|
|
node.body,
|
|
self.unindent,
|
|
self.fill,
|
|
"}",
|
|
)
|
|
|
|
def _ast_if(self, node: ast.If) -> None:
|
|
"""Add an AST If in output."""
|
|
self.add(
|
|
self.fill,
|
|
"if (",
|
|
(self.prefix, "$"),
|
|
node.test,
|
|
(self.prefix, None),
|
|
")",
|
|
self.fill,
|
|
"{",
|
|
self.indent,
|
|
node.body,
|
|
self.unindent,
|
|
self.fill,
|
|
"}",
|
|
)
|
|
if node.orelse:
|
|
self.add(
|
|
self.fill,
|
|
"else",
|
|
self.fill,
|
|
"{",
|
|
self.indent,
|
|
node.orelse,
|
|
self.unindent,
|
|
self.fill,
|
|
"}",
|
|
)
|
|
|
|
def _ast_list(self, node: ast.List) -> None:
|
|
"""Add an AST List in output."""
|
|
self.add(
|
|
"(",
|
|
self.make_list(node.elts),
|
|
")",
|
|
)
|
|
|
|
def _ast_pass(self, node: ast.Pass) -> None:
|
|
"""Add an AST Pass in output."""
|
|
pass # noqa: PIE790
|
|
|
|
def _ast_return(self, node: ast.Return) -> None:
|
|
"""Add an AST Return in output."""
|
|
self.fill("return")
|
|
if node.value:
|
|
self.add(
|
|
" ",
|
|
(self.prefix, "%" if isinstance(node.value, ast.Dict) else "$"),
|
|
node.value,
|
|
(self.prefix, None),
|
|
";",
|
|
)
|
|
|
|
def _ast_str(self, node: ast.Str) -> None:
|
|
"""Add an AST Str in output."""
|
|
self._ast_constant(node)
|
|
|
|
def _ast_subscript(self, node: ast.Subscript) -> None:
|
|
"""Add an AST Subscript in output."""
|
|
self.add(
|
|
(self.prefix, "$"),
|
|
node.value,
|
|
(self.prefix, None),
|
|
"->{",
|
|
node.slice,
|
|
"}",
|
|
)
|
|
|
|
|
|
class UnparseRuby(UnparsePython):
|
|
"""Unparse AST to generate Ruby script code.
|
|
|
|
Note: only part of AST types are supported (just the types used by
|
|
the script to test WeeChat scripting API).
|
|
"""
|
|
|
|
__lineno__ = inspect.currentframe().f_lineno
|
|
|
|
def _ast_attribute(self, node: ast.Attribute) -> None:
|
|
"""Add an AST Attribute in output."""
|
|
self.add(
|
|
node.value,
|
|
"::" if node.attr.startswith("WEECHAT_") else ".",
|
|
node.attr,
|
|
)
|
|
|
|
def _ast_constant(self, node: ast.Constant) -> None:
|
|
"""Add an AST Constant in output."""
|
|
if isinstance(node.value, str):
|
|
str_node = node.value.replace("#{", "\\#{")
|
|
self.add(f'"{str_node}"')
|
|
elif node.value is None:
|
|
self.add("nil")
|
|
else:
|
|
self.add(repr(node.value))
|
|
|
|
def _ast_dict(self, node: ast.Dict) -> None:
|
|
"""Add an AST Dict in output."""
|
|
self.add(
|
|
"Hash[",
|
|
self.make_list([[key, " => ", value] for key, value in zip(node.keys, node.values)]),
|
|
"]",
|
|
)
|
|
|
|
def _ast_functiondef(self, node: ast.FunctionDef) -> None:
|
|
"""Add an AST FunctionDef in output."""
|
|
self.add(
|
|
self.fill,
|
|
self.fill,
|
|
f"def {node.name}",
|
|
)
|
|
if node.args.args:
|
|
self.add(
|
|
"(",
|
|
self.make_list(node.args.args),
|
|
")",
|
|
)
|
|
self.add(
|
|
self.indent,
|
|
node.body,
|
|
self.unindent,
|
|
self.fill,
|
|
"end",
|
|
)
|
|
|
|
def _ast_if(self, node: ast.If) -> None:
|
|
"""Add an AST If in output."""
|
|
self.add(
|
|
self.fill,
|
|
"if ",
|
|
node.test,
|
|
self.indent,
|
|
node.body,
|
|
self.unindent,
|
|
)
|
|
if node.orelse:
|
|
self.add(
|
|
self.fill,
|
|
"else",
|
|
self.indent,
|
|
node.orelse,
|
|
self.unindent,
|
|
self.fill,
|
|
"end",
|
|
)
|
|
|
|
def _ast_list(self, node: ast.List) -> None:
|
|
"""Add an AST List in output."""
|
|
self.add(
|
|
"Array[",
|
|
self.make_list(node.elts),
|
|
"]",
|
|
)
|
|
|
|
def _ast_pass(self, node: ast.Pass) -> None:
|
|
"""Add an AST Pass in output."""
|
|
pass # noqa: PIE790
|
|
|
|
def _ast_str(self, node: ast.Str) -> None:
|
|
"""Add an AST Str in output."""
|
|
self._ast_constant(node)
|
|
|
|
|
|
class UnparseLua(UnparsePython):
|
|
"""Unparse AST to generate Lua script code.
|
|
|
|
Note: only part of AST types are supported (just the types used by
|
|
the script to test WeeChat scripting API).
|
|
"""
|
|
|
|
__lineno__ = inspect.currentframe().f_lineno
|
|
|
|
def __init__(self, *args, **kwargs) -> None: # noqa: ANN002,ANN003
|
|
"""Initialize Lua parser."""
|
|
super().__init__(*args, **kwargs)
|
|
self.cmpop = {
|
|
"Eq": "==",
|
|
"NotEq": "~=",
|
|
"Lt": "<",
|
|
"LtE": "<=",
|
|
"Gt": ">",
|
|
"GtE": ">=",
|
|
}
|
|
|
|
def _ast_binop(self, node: ast.BinOp) -> None:
|
|
"""Add an AST BinOp in output."""
|
|
if isinstance(node.op, ast.Add) and (not self.is_number(node.left) or not self.is_number(node.right)):
|
|
str_op = ".."
|
|
else:
|
|
str_op = self.binop[node.op.__class__.__name__]
|
|
self.add(
|
|
node.left,
|
|
f" {str_op} ",
|
|
node.right,
|
|
)
|
|
|
|
def _ast_constant(self, node: ast.Constant) -> None:
|
|
"""Add an AST Constant in output."""
|
|
if node.value is None:
|
|
self.add("nil")
|
|
else:
|
|
self.add(repr(node.value))
|
|
|
|
def _ast_dict(self, node: ast.Dict) -> None:
|
|
"""Add an AST Dict in output."""
|
|
self.add(
|
|
"{",
|
|
self.make_list([["[", key, "]", "=", value] for key, value in zip(node.keys, node.values)]),
|
|
"}",
|
|
)
|
|
|
|
def _ast_functiondef(self, node: ast.FunctionDef) -> None:
|
|
"""Add an AST FunctionDef in output."""
|
|
self.add(
|
|
self.fill,
|
|
self.fill,
|
|
f"function {node.name}",
|
|
)
|
|
self.add(
|
|
"(",
|
|
self.make_list(node.args.args),
|
|
")",
|
|
self.indent,
|
|
node.body,
|
|
self.unindent,
|
|
self.fill,
|
|
"end",
|
|
)
|
|
|
|
def _ast_if(self, node: ast.If) -> None:
|
|
"""Add an AST If in output."""
|
|
self.add(
|
|
self.fill,
|
|
"if ",
|
|
node.test,
|
|
" then",
|
|
self.indent,
|
|
node.body,
|
|
self.unindent,
|
|
)
|
|
if node.orelse:
|
|
self.add(
|
|
self.fill,
|
|
"else",
|
|
self.indent,
|
|
node.orelse,
|
|
self.unindent,
|
|
self.fill,
|
|
"end",
|
|
)
|
|
|
|
def _ast_list(self, node: ast.List) -> None:
|
|
"""Add an AST List in output."""
|
|
self.add(
|
|
"{",
|
|
self.make_list(node.elts),
|
|
"}",
|
|
)
|
|
|
|
def _ast_pass(self, node: ast.Pass) -> None:
|
|
"""Add an AST Pass in output."""
|
|
pass # noqa: PIE790
|
|
|
|
|
|
class UnparseTcl(UnparsePython):
|
|
"""Unparse AST to generate Tcl script code.
|
|
|
|
Note: only part of AST types are supported (just the types used by
|
|
the script to test WeeChat scripting API).
|
|
"""
|
|
|
|
__lineno__ = inspect.currentframe().f_lineno
|
|
|
|
def __init__(self, *args, **kwargs) -> None: # noqa: ANN002,ANN003
|
|
"""Initialize Tcl parser."""
|
|
super().__init__(*args, **kwargs)
|
|
self._call = 0
|
|
|
|
def _ast_assign(self, node: ast.Assign) -> None:
|
|
"""Add an AST Assign in output."""
|
|
exclude_types = (ast.Dict, ast.List, ast.Constant, ast.Subscript)
|
|
self.add(
|
|
self.fill,
|
|
"set ",
|
|
node.targets[0],
|
|
" ",
|
|
"[" if not isinstance(node.value, exclude_types) else "",
|
|
node.value,
|
|
"]" if not isinstance(node.value, exclude_types) else "",
|
|
)
|
|
|
|
def _ast_attribute(self, node: ast.Attribute) -> None:
|
|
"""Add an AST Attribute in output."""
|
|
saved_prefix = self._prefix
|
|
self._prefix = []
|
|
if node.attr.startswith("WEECHAT_"):
|
|
self.add("$::")
|
|
self.add(node.value, "::", node.attr)
|
|
self._prefix = saved_prefix
|
|
|
|
def _ast_binop(self, node: ast.BinOp) -> None:
|
|
"""Add an AST BinOp in output."""
|
|
self.add(
|
|
"[join [list ",
|
|
(self.prefix, "$"),
|
|
node.left,
|
|
" ",
|
|
node.right,
|
|
(self.prefix, None),
|
|
'] ""]',
|
|
)
|
|
|
|
def _ast_call(self, node: ast.Call) -> None:
|
|
"""Add an AST Call in output."""
|
|
if self._call:
|
|
self.add("[")
|
|
self._call += 1
|
|
self.add(
|
|
node.func,
|
|
" " if node.args else None,
|
|
(self.prefix, "$"),
|
|
self.make_list(node.args, sep=" "),
|
|
(self.prefix, None),
|
|
)
|
|
self._call -= 1
|
|
if self._call:
|
|
self.add("]")
|
|
|
|
def _ast_compare(self, node: ast.Compare) -> None:
|
|
"""Add an AST Compare in output."""
|
|
self.prefix("$")
|
|
if self._call:
|
|
self.add("[expr {")
|
|
self.add(node.left)
|
|
for operator, comparator in zip(node.ops, node.comparators):
|
|
if (
|
|
isinstance(operator, (ast.Eq, ast.NotEq))
|
|
and not self.is_number(node.left)
|
|
and not self.is_bool(node.left)
|
|
and not self.is_number(comparator)
|
|
and not self.is_bool(comparator)
|
|
):
|
|
custom_cmpop = {
|
|
"Eq": "eq",
|
|
"NotEq": "ne",
|
|
}
|
|
else:
|
|
custom_cmpop = self.cmpop
|
|
self.add(
|
|
f" {custom_cmpop[operator.__class__.__name__]} ",
|
|
comparator,
|
|
)
|
|
if self._call:
|
|
self.add("}]")
|
|
self.prefix(None)
|
|
|
|
def _ast_constant(self, node: ast.Constant) -> None:
|
|
"""Add an AST Constant in output."""
|
|
if isinstance(node.value, str):
|
|
str_node = node.value.replace("$", "\\$")
|
|
self.add(f'"{str_node}"')
|
|
elif node.value is None:
|
|
self.add("$::weechat::WEECHAT_NULL")
|
|
else:
|
|
self.add(repr(node.value))
|
|
|
|
def _ast_dict(self, node: ast.Dict) -> None:
|
|
"""Add an AST Dict in output."""
|
|
self.add(
|
|
"[dict create ",
|
|
self.make_list([[key, " ", value] for key, value in zip(node.keys, node.values)], sep=" "),
|
|
"]",
|
|
)
|
|
|
|
def _ast_functiondef(self, node: ast.FunctionDef) -> None:
|
|
"""Add an AST FunctionDef in output."""
|
|
self.add(
|
|
self.fill,
|
|
self.fill,
|
|
f"proc {node.name} {{",
|
|
(self.make_list(node.args.args, sep=" ") if node.args.args else None),
|
|
"} {",
|
|
self.indent,
|
|
node.body,
|
|
self.unindent,
|
|
self.fill,
|
|
"}",
|
|
)
|
|
|
|
def _ast_if(self, node: ast.If) -> None:
|
|
"""Add an AST If in output."""
|
|
self.add(
|
|
self.fill,
|
|
"if {",
|
|
(self.prefix, "$"),
|
|
node.test,
|
|
(self.prefix, None),
|
|
"} {",
|
|
self.indent,
|
|
node.body,
|
|
self.unindent,
|
|
self.fill,
|
|
"}",
|
|
)
|
|
if node.orelse:
|
|
self.add(
|
|
" else {",
|
|
self.indent,
|
|
node.orelse,
|
|
self.unindent,
|
|
self.fill,
|
|
"}",
|
|
)
|
|
|
|
def _ast_list(self, node: ast.List) -> None:
|
|
"""Add an AST List in output."""
|
|
self.add(
|
|
"[",
|
|
self.make_list(node.elts, sep=" "),
|
|
"]",
|
|
)
|
|
|
|
def _ast_pass(self, node: ast.Pass) -> None:
|
|
"""Add an AST Pass in output."""
|
|
pass # noqa: PIE790
|
|
|
|
def _ast_return(self, node: ast.Return) -> None:
|
|
"""Add an AST Return in output."""
|
|
self.fill("return")
|
|
if node.value:
|
|
self.add(
|
|
" ",
|
|
(self.prefix, "$"),
|
|
node.value,
|
|
(self.prefix, None),
|
|
)
|
|
|
|
def _ast_str(self, node: ast.Str) -> None:
|
|
"""Add an AST Str in output."""
|
|
self._ast_constant(node)
|
|
|
|
def _ast_subscript(self, node: ast.Subscript) -> None:
|
|
"""Add an AST Subscript in output."""
|
|
self.add(
|
|
"[dict get ",
|
|
(self.prefix, "$"),
|
|
node.value,
|
|
(self.prefix, None),
|
|
" ",
|
|
node.slice,
|
|
"]",
|
|
)
|
|
|
|
|
|
class UnparseGuile(UnparsePython):
|
|
"""Unparse AST to generate Guile script code.
|
|
|
|
Note: only part of AST types are supported (just the types used by
|
|
the script to test WeeChat scripting API).
|
|
"""
|
|
|
|
__lineno__ = inspect.currentframe().f_lineno
|
|
|
|
def __init__(self, *args, **kwargs) -> None: # noqa: ANN002,ANN003
|
|
"""Initialize Guile parser."""
|
|
super().__init__(*args, **kwargs)
|
|
self.cmpop = {
|
|
"Eq": "=",
|
|
"NotEq": "<>",
|
|
"Lt": "<",
|
|
"LtE": "<=",
|
|
"Gt": ">",
|
|
"GtE": ">=",
|
|
}
|
|
self._call = 0
|
|
self._let = 0
|
|
|
|
def _ast_assign(self, node: ast.Assign) -> None:
|
|
"""Add an AST Assign in output."""
|
|
self.add(
|
|
self.fill,
|
|
"(let ((",
|
|
node.targets[0],
|
|
" ",
|
|
node.value,
|
|
"))",
|
|
self.indent,
|
|
self.fill,
|
|
"(begin",
|
|
self.indent,
|
|
)
|
|
self._let += 1
|
|
|
|
def _ast_attribute(self, node: ast.Attribute) -> None:
|
|
"""Add an AST Attribute in output."""
|
|
self.add(node.value, ":", node.attr)
|
|
|
|
def _ast_binop(self, node: ast.BinOp) -> None:
|
|
"""Add an AST BinOp in output."""
|
|
if isinstance(node.op, ast.Add) and (
|
|
isinstance(node.left, (ast.Name, ast.Constant)) or isinstance(node.right, (ast.Name, ast.Constant))
|
|
):
|
|
self.add(
|
|
"(string-append ",
|
|
node.left,
|
|
" ",
|
|
node.right,
|
|
")",
|
|
)
|
|
else:
|
|
self.add(
|
|
node.left,
|
|
f" {self.binop[node.op.__class__.__name__]} ",
|
|
node.right,
|
|
)
|
|
|
|
def _ast_call(self, node: ast.Call) -> None:
|
|
"""Add an AST Call in output."""
|
|
self._call += 1
|
|
self.add(
|
|
"(",
|
|
node.func,
|
|
" " if node.args else None,
|
|
self.make_list(node.args, sep=" "),
|
|
")",
|
|
)
|
|
self._call -= 1
|
|
|
|
def _ast_compare(self, node: ast.Compare) -> None:
|
|
"""Add an AST Compare in output."""
|
|
for operator, comparator in zip(node.ops, node.comparators):
|
|
if (
|
|
isinstance(operator, (ast.Eq, ast.NotEq))
|
|
and not self.is_number(node.left)
|
|
and not self.is_bool(node.left)
|
|
and not self.is_number(comparator)
|
|
and not self.is_bool(comparator)
|
|
):
|
|
prefix = "string"
|
|
else:
|
|
prefix = ""
|
|
self.add(
|
|
f"({prefix}{self.cmpop[operator.__class__.__name__]} ",
|
|
node.left,
|
|
" ",
|
|
comparator,
|
|
")",
|
|
)
|
|
|
|
def _ast_constant(self, node: ast.Constant) -> None:
|
|
"""Add an AST Constant in output."""
|
|
if isinstance(node.value, str):
|
|
self.add(f'"{node.value}"')
|
|
elif node.value is None:
|
|
self.add("#nil")
|
|
else:
|
|
self.add(repr(node.value))
|
|
|
|
def _ast_dict(self, node: ast.Dict) -> None:
|
|
"""Add an AST Dict in output."""
|
|
self.add(
|
|
"'(",
|
|
self.make_list(
|
|
[["(", key, " ", value, ")"] for key, value in zip(node.keys, node.values)], sep=" ",
|
|
),
|
|
")",
|
|
)
|
|
|
|
def _ast_functiondef(self, node: ast.FunctionDef) -> None:
|
|
"""Add an AST FunctionDef in output."""
|
|
self.add(
|
|
self.fill,
|
|
self.fill,
|
|
f"(define ({node.name}",
|
|
" " if node.args.args else None,
|
|
(self.make_list(node.args.args, sep=" ") if node.args.args else None),
|
|
")",
|
|
self.indent,
|
|
node.body,
|
|
)
|
|
while self._let > 0:
|
|
self.add(
|
|
self.unindent,
|
|
self.fill,
|
|
")",
|
|
self.unindent,
|
|
self.fill,
|
|
")",
|
|
)
|
|
self._let -= 1
|
|
self.add(
|
|
self.unindent,
|
|
self.fill,
|
|
")",
|
|
)
|
|
|
|
def _ast_if(self, node: ast.If) -> None:
|
|
"""Add an AST If in output."""
|
|
self.add(
|
|
self.fill,
|
|
"(if " if isinstance(node.test, ast.Name) else "(",
|
|
node.test,
|
|
"" if isinstance(node.test, ast.Name) else ")",
|
|
self.indent,
|
|
self.fill,
|
|
"(begin",
|
|
self.indent,
|
|
node.body,
|
|
self.unindent,
|
|
self.fill,
|
|
")",
|
|
self.unindent,
|
|
)
|
|
if node.orelse:
|
|
self.add(
|
|
self.indent,
|
|
self.fill,
|
|
"(begin",
|
|
self.indent,
|
|
node.orelse,
|
|
self.unindent,
|
|
self.fill,
|
|
")",
|
|
self.unindent,
|
|
)
|
|
self.add(self.fill, ")")
|
|
|
|
def _ast_list(self, node: ast.List) -> None:
|
|
"""Add an AST List in output."""
|
|
self.add(
|
|
"'(",
|
|
self.make_list(node.elts, sep=" "),
|
|
")",
|
|
)
|
|
|
|
def _ast_pass(self, node: ast.Pass) -> None: # noqa: ARG002
|
|
"""Add an AST Pass in output."""
|
|
self.add("#t")
|
|
|
|
def _ast_return(self, node: ast.Return) -> None:
|
|
"""Add an AST Return in output."""
|
|
if node.value:
|
|
self.add(self.fill, node.value)
|
|
|
|
def _ast_str(self, node: ast.Str) -> None:
|
|
"""Add an AST Str in output."""
|
|
self._ast_constant(node)
|
|
|
|
def _ast_subscript(self, node: ast.Subscript) -> None:
|
|
"""Add an AST Subscript in output."""
|
|
self.add(
|
|
"(assoc-ref ",
|
|
node.value,
|
|
" ",
|
|
node.slice,
|
|
")",
|
|
)
|
|
|
|
|
|
class UnparseJavaScript(UnparsePython):
|
|
"""Unparse AST to generate JavaScript script code.
|
|
|
|
Note: only part of AST types are supported (just the types used by
|
|
the script to test WeeChat scripting API).
|
|
"""
|
|
|
|
__lineno__ = inspect.currentframe().f_lineno
|
|
|
|
def _ast_dict(self, node: ast.Dict) -> None:
|
|
"""Add an AST Dict in output."""
|
|
self.add(
|
|
"{",
|
|
self.make_list([[key, ": ", value] for key, value in zip(node.keys, node.values)]),
|
|
"}",
|
|
)
|
|
|
|
def _ast_constant(self, node: ast.Constant) -> None:
|
|
"""Add an AST Constant in output."""
|
|
if node.value is None:
|
|
self.add("null")
|
|
else:
|
|
self.add(repr(node.value))
|
|
|
|
def _ast_functiondef(self, node: ast.FunctionDef) -> None:
|
|
"""Add an AST FunctionDef in output."""
|
|
self.add(
|
|
self.fill,
|
|
self.fill,
|
|
f"function {node.name}(",
|
|
self.make_list(node.args.args),
|
|
") {",
|
|
self.indent,
|
|
node.body,
|
|
self.unindent,
|
|
self.fill,
|
|
"}",
|
|
)
|
|
|
|
def _ast_if(self, node: ast.If) -> None:
|
|
"""Add an AST If in output."""
|
|
self.add(
|
|
self.fill,
|
|
"if (",
|
|
node.test,
|
|
") {",
|
|
self.indent,
|
|
node.body,
|
|
self.unindent,
|
|
self.fill,
|
|
"}",
|
|
)
|
|
if node.orelse:
|
|
self.add(
|
|
" else {",
|
|
self.indent,
|
|
node.orelse,
|
|
self.unindent,
|
|
self.fill,
|
|
"}",
|
|
)
|
|
|
|
def _ast_pass(self, node: ast.Pass) -> None:
|
|
"""Add an AST Pass in output."""
|
|
pass # noqa: PIE790
|
|
|
|
|
|
class UnparsePhp(UnparsePython):
|
|
"""Unparse AST to generate PHP script code.
|
|
|
|
Note: only part of AST types are supported (just the types used by
|
|
the script to test WeeChat scripting API).
|
|
"""
|
|
|
|
__lineno__ = inspect.currentframe().f_lineno
|
|
|
|
def _ast_assign(self, node: ast.Assign) -> None:
|
|
"""Add an AST Assign in output."""
|
|
self.add(
|
|
self.fill,
|
|
(self.prefix, "$"),
|
|
[[target, " = "] for target in node.targets],
|
|
(self.prefix, None),
|
|
node.value,
|
|
";",
|
|
)
|
|
|
|
def _ast_attribute(self, node: ast.Attribute) -> None:
|
|
"""Add an AST Attribute in output."""
|
|
saved_prefix = self._prefix
|
|
self._prefix = []
|
|
if not node.attr.startswith("WEECHAT_"):
|
|
self.add(node.value, "_")
|
|
self.add(node.attr)
|
|
self._prefix = saved_prefix
|
|
|
|
def _ast_binop(self, node: ast.BinOp) -> None:
|
|
"""Add an AST BinOp in output."""
|
|
if isinstance(node.op, ast.Add) and (
|
|
isinstance(node.left, (ast.Name, ast.Constant)) or isinstance(node.right, (ast.Name, ast.Constant))
|
|
):
|
|
str_op = "."
|
|
else:
|
|
str_op = self.binop[node.op.__class__.__name__]
|
|
self.add(
|
|
(self.prefix, "$"),
|
|
node.left,
|
|
f" {str_op} ",
|
|
node.right,
|
|
(self.prefix, None),
|
|
)
|
|
|
|
def _ast_call(self, node: ast.Call) -> None:
|
|
"""Add an AST Call in output."""
|
|
self.add(
|
|
node.func,
|
|
"(",
|
|
(self.prefix, "$"),
|
|
self.make_list(node.args),
|
|
(self.prefix, None),
|
|
")",
|
|
)
|
|
|
|
def _ast_constant(self, node: ast.Constant) -> None:
|
|
"""Add an AST Constant in output."""
|
|
if isinstance(node.value, str):
|
|
str_node = node.value.replace("$", "\\$")
|
|
self.add(f'"{str_node}"')
|
|
elif node.value is None:
|
|
self.add("NULL")
|
|
else:
|
|
self.add(repr(node.value))
|
|
|
|
def _ast_dict(self, node: ast.Dict) -> None:
|
|
"""Add an AST Dict in output."""
|
|
self.add(
|
|
"array(",
|
|
self.make_list([[key, " => ", value] for key, value in zip(node.keys, node.values)]),
|
|
")",
|
|
)
|
|
|
|
def _ast_expr(self, node: ast.Expr) -> None:
|
|
"""Add an AST Expr in output."""
|
|
if not isinstance(node.value, ast.Constant): # ignore docstrings
|
|
self.add(
|
|
self.fill,
|
|
node.value,
|
|
";",
|
|
)
|
|
|
|
def _ast_functiondef(self, node: ast.FunctionDef) -> None:
|
|
"""Add an AST FunctionDef in output."""
|
|
self.add(
|
|
self.fill,
|
|
self.fill,
|
|
f"function {node.name}(",
|
|
(self.prefix, "$"),
|
|
self.make_list(node.args.args),
|
|
(self.prefix, None),
|
|
")",
|
|
self.fill,
|
|
"{",
|
|
self.indent,
|
|
node.body,
|
|
self.unindent,
|
|
self.fill,
|
|
"}",
|
|
)
|
|
|
|
def _ast_list(self, node: ast.List) -> None:
|
|
"""Add an AST List in output."""
|
|
self.add(
|
|
"array(",
|
|
self.make_list(node.elts),
|
|
")",
|
|
)
|
|
|
|
def _ast_if(self, node: ast.If) -> None:
|
|
"""Add an AST If in output."""
|
|
self.add(
|
|
self.fill,
|
|
"if (",
|
|
(self.prefix, "$"),
|
|
node.test,
|
|
(self.prefix, None),
|
|
")",
|
|
self.fill,
|
|
"{",
|
|
self.indent,
|
|
node.body,
|
|
self.unindent,
|
|
self.fill,
|
|
"}",
|
|
)
|
|
if node.orelse:
|
|
self.add(
|
|
self.fill,
|
|
"else",
|
|
self.fill,
|
|
"{",
|
|
self.indent,
|
|
node.orelse,
|
|
self.unindent,
|
|
self.fill,
|
|
"}",
|
|
)
|
|
|
|
def _ast_pass(self, node: ast.Pass) -> None:
|
|
"""Add an AST Pass in output."""
|
|
pass # noqa: PIE790
|
|
|
|
def _ast_return(self, node: ast.Return) -> None:
|
|
"""Add an AST Return in output."""
|
|
self.fill("return")
|
|
if node.value:
|
|
self.add(
|
|
" ",
|
|
(self.prefix, "$"),
|
|
node.value,
|
|
(self.prefix, None),
|
|
";",
|
|
)
|
|
|
|
def _ast_str(self, node: ast.Str) -> None:
|
|
"""Add an AST Str in output."""
|
|
self._ast_constant(node)
|
|
|
|
def _ast_subscript(self, node: ast.Subscript) -> None:
|
|
"""Add an AST Subscript in output."""
|
|
self.add(
|
|
"$",
|
|
node.value,
|
|
"[",
|
|
node.slice,
|
|
"]",
|
|
)
|
|
|
|
|
|
def get_languages() -> List[str]:
|
|
"""Return a list of supported languages: ['python', 'perl', ...]."""
|
|
members = [
|
|
member
|
|
for member in inspect.getmembers(sys.modules[__name__], predicate=inspect.isclass)
|
|
if inspect.isclass(member[1]) and member[0].startswith("Unparse") and member[0] != "Unparse"
|
|
]
|
|
return [name[7:].lower() for name, _ in sorted(members, key=lambda member: member[1].__lineno__)]
|
|
|
|
|
|
LANGUAGES = get_languages()
|
|
|
|
|
|
def get_parser() -> argparse.ArgumentParser:
|
|
"""Get parser arguments."""
|
|
all_languages = [*LANGUAGES, "all"]
|
|
default_language = LANGUAGES[0]
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
"Unparse Python code from stdin and generate code in "
|
|
"another language (to stdout).\n\n"
|
|
"The code is read from stdin and generated code is "
|
|
"written on stdout."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"-l",
|
|
"--language",
|
|
default=default_language,
|
|
choices=all_languages,
|
|
help="output language (default: {default_language})",
|
|
)
|
|
return parser
|
|
|
|
|
|
def get_stdin() -> str:
|
|
"""Return data from standard input.
|
|
|
|
If there is nothing in stdin, wait for data until ctrl-d (EOF)
|
|
is received.
|
|
"""
|
|
data = ""
|
|
inr = select.select([sys.stdin], [], [], 0)[0]
|
|
if not inr:
|
|
print("Enter the code to convert (Enter + ctrl-d to end)")
|
|
while True:
|
|
inr = select.select([sys.stdin], [], [], 0.1)[0]
|
|
if not inr:
|
|
continue
|
|
new_data = os.read(sys.stdin.fileno(), 4096)
|
|
if not new_data: # EOF?
|
|
break
|
|
data += new_data.decode("utf-8")
|
|
return data
|
|
|
|
|
|
def convert_to_language(code: str, language: str, prefix: str = "") -> None:
|
|
"""Convert Python code to a language."""
|
|
class_name = f"Unparse{language.capitalize()}"
|
|
unparse_class = getattr(sys.modules[__name__], class_name)
|
|
if prefix:
|
|
print(prefix)
|
|
output = io.StringIO()
|
|
unparse_class(output=output).add(ast.parse(code))
|
|
print(output.getvalue().lstrip())
|
|
|
|
|
|
def convert(code: str, language: str) -> None:
|
|
"""Convert Python code to one or all languages."""
|
|
if language == "all":
|
|
for lang in LANGUAGES:
|
|
convert_to_language(code, lang, "\n{lang}:")
|
|
else:
|
|
convert_to_language(code, language)
|
|
|
|
|
|
def main() -> None:
|
|
"""Generate scripts."""
|
|
parser = get_parser()
|
|
args = parser.parse_args()
|
|
|
|
code = get_stdin()
|
|
if not code:
|
|
print("ERROR: missing input")
|
|
print()
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
convert(code, args.language)
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|