231 lines
6.4 KiB
Python
Executable File
231 lines
6.4 KiB
Python
Executable File
# -*- coding: utf-8 -*-
|
|
"""Bindings to write alterator backends in Python
|
|
|
|
Alterator backends use a very simple communication protocol and can be written in any language.
|
|
This library contains a set of functions to simplify development of backend on `Python`.
|
|
Main event loop is provided by `message_loop` function. You must provide your own `on_message` callback
|
|
(that accepts contents of Alterator's message in `message` argument).
|
|
All incoming parameters are provided as-is
|
|
(unlike `alterator-sh-functions`, i.e. without `in_` prefix, exactly like in `alterator-perl-functions`).
|
|
For example, incoming parameter `'foo'` can be found as a key `"foo"` in `message` `dict`.
|
|
|
|
You should write output parameters using functions with prefix `write_`.
|
|
Some parameter names have special meaning. There are: `action`, `_objects`, `language`, `expert_mode`.
|
|
|
|
See also Project's README.md for more thorough description.
|
|
|
|
Attributes:
|
|
ALTERATOR_DEBUG (bool): Indicates if additional debug output
|
|
(to stderr) is needed (messages from alterator, etc.).
|
|
Also with this flag `write_debug` won't be silent
|
|
(will print messages to stderr)
|
|
|
|
TEXTDOMAIN (str): Sets dictionary used for translations.
|
|
If not set manually, warning will be printed and default value
|
|
(running backend file name) will be set
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
import os
|
|
from io import StringIO
|
|
|
|
ALTERATOR_DEBUG = False # May be set from backend for debug-messages
|
|
TEXTDOMAIN = None # Should be set by backend
|
|
|
|
|
|
_LANGUAGE = "en_US" # Will be set from language parameter
|
|
_OUT_BUF = None # Module-local variable
|
|
|
|
|
|
### Internal function
|
|
def _validate_symbol(str: str):
|
|
if not re.match(r"^\w+$", str):
|
|
sys.exit("wrong attribute name: {}".format(str))
|
|
|
|
|
|
### Quote
|
|
def string_quote(str: str):
|
|
"""Escapes \" and \\"""
|
|
escaped = str.translate(str.maketrans({"\\": r"\\", '"': r"\""}))
|
|
return escaped
|
|
|
|
|
|
### Input/Output functions
|
|
|
|
|
|
def write_string(str: str):
|
|
"""Writes string with escaping \" and \\"""
|
|
print(string_quote(str), end="", file=_OUT_BUF)
|
|
|
|
|
|
def get_bool_string(value):
|
|
"""Inteprets value as Scheme's boolean (value can be Python's bool or str)"""
|
|
if (
|
|
value == "yes"
|
|
or value == "true"
|
|
or value == "on"
|
|
or value == "y"
|
|
or value == "1"
|
|
or value == "#t"
|
|
or value == True
|
|
):
|
|
return "#t"
|
|
return "#f"
|
|
|
|
|
|
def test_bool(input: str):
|
|
"""Simple wrapper to cast Scheme's boolean to Python's"""
|
|
return input == "#t"
|
|
|
|
|
|
### High-Level Output
|
|
|
|
|
|
def write_string_param(name: str, value: str):
|
|
"""Writes attribute's value as string"""
|
|
_validate_symbol(name)
|
|
print('{} "{}"'.format(name, string_quote(value)), file=_OUT_BUF)
|
|
|
|
|
|
def write_bool_param(name: str, value):
|
|
"""Writes attribute's value as Scheme's bool"""
|
|
_validate_symbol(name)
|
|
print(name, get_bool_string(value), file=_OUT_BUF)
|
|
|
|
|
|
def write_enum_item(name: str, label: str = None):
|
|
"""Writes enum entry (sets label value to name if not given)"""
|
|
_validate_symbol(name)
|
|
|
|
if label is None:
|
|
label = name
|
|
|
|
print('(name "{}" label "{}")'.format(name, string_quote(label)), file=_OUT_BUF)
|
|
|
|
|
|
def write_table_item(row: dict):
|
|
"""Writes table row (dictionary with column-value pairs)"""
|
|
print("(", end="", file=_OUT_BUF)
|
|
for name, value in row.items():
|
|
_validate_symbol(name)
|
|
|
|
if value is None:
|
|
value = name
|
|
|
|
if value == "#t" or value == "#f":
|
|
print(" {} {}".format(name, string_quote(value)), end="", file=_OUT_BUF)
|
|
else:
|
|
print(' {} "{}"'.format(name, string_quote(value)), end="", file=_OUT_BUF)
|
|
|
|
print(")", end="", file=_OUT_BUF)
|
|
|
|
|
|
def write_type_item(name: str, type: str):
|
|
"""Declares attribute's type for Alterator"""
|
|
write_string_param(name, type)
|
|
|
|
|
|
def write_error(message: str):
|
|
"""Writes an error to Alterator"""
|
|
global _OUT_BUF
|
|
_OUT_BUF = StringIO()
|
|
write_string_param("error", message)
|
|
|
|
|
|
def write_debug(message: str):
|
|
"""Writes debug message in ALTERATOR_DEBUG mode"""
|
|
if ALTERATOR_DEBUG:
|
|
print(message, file=sys.stderr)
|
|
|
|
|
|
### Localization Support
|
|
|
|
|
|
def _(text: str, domain=None):
|
|
"""Provides text translation from given domain (TEXTDOMAIN by default)"""
|
|
if domain is None:
|
|
domain = TEXTDOMAIN
|
|
|
|
lang_list = _LANGUAGE.split(":")
|
|
if len(lang_list) <= 0:
|
|
return text
|
|
|
|
return 'LANGUAGE="{}" LANG="{}.UTF8" gettext {} "{}"'.format(
|
|
_LANGUAGE, lang_list[0], domain, text
|
|
)
|
|
|
|
|
|
### Message Loop
|
|
|
|
|
|
# Unimplemented for now
|
|
def _check_response(response: str):
|
|
return True
|
|
|
|
|
|
def _redirect_stdout():
|
|
saved_stdout_fd = os.dup(sys.stdout.fileno())
|
|
backendout = os.fdopen(saved_stdout_fd, "w")
|
|
os.dup2(sys.stderr.fileno(), sys.stdout.fileno())
|
|
return backendout
|
|
|
|
|
|
def _ensure_textdomain():
|
|
global TEXTDOMAIN
|
|
if TEXTDOMAIN is None:
|
|
backend_name = os.path.splitext(os.path.basename(sys.argv[0]))[0]
|
|
TEXTDOMAIN = "alterator-" + backend_name
|
|
print(
|
|
"alterator_python_functions: TEXTDOMAIN variable is undefined! Setting to {}".format(
|
|
TEXTDOMAIN
|
|
),
|
|
file=sys.stderr,
|
|
)
|
|
|
|
|
|
def _have_language(message: dict):
|
|
if "language" in message and message["language"]:
|
|
global _LANGUAGE
|
|
_LANGUAGE = message["language"].replace(";", ":")
|
|
|
|
|
|
def message_loop(handler):
|
|
"""Main function to communicate with Alterator"""
|
|
_ensure_textdomain()
|
|
|
|
# Redirecting streams (according to Alterator API)
|
|
backendout = _redirect_stdout()
|
|
|
|
message = {}
|
|
reading = False
|
|
for line in sys.stdin:
|
|
write_debug(">>>{}".format(line))
|
|
|
|
if line == "_message:begin\n":
|
|
message = {}
|
|
reading = True
|
|
|
|
elif reading and line == "_message:end\n":
|
|
_have_language(message)
|
|
|
|
reading = False
|
|
global _OUT_BUF
|
|
_OUT_BUF = StringIO()
|
|
|
|
handler(message)
|
|
response = _OUT_BUF.getvalue()
|
|
|
|
# TODO: use _check_response here
|
|
write_debug("response >>>({})<<<".format(response))
|
|
print("({})".format(response), end="", file=backendout, flush=True)
|
|
|
|
elif reading:
|
|
name, value = line.split(":", 1)
|
|
value = value.rstrip("\n")
|
|
|
|
value = re.sub(r"([^\\])\\n", r"\1\n", value)
|
|
value = re.sub(r"\\\\", r"\\", value)
|
|
|
|
message[name] = value
|