Add lua.py

master
luk3yx 2019-07-12 15:21:55 +12:00
parent adda651e0f
commit 355ba596a5
2 changed files with 261 additions and 0 deletions

View File

@ -22,6 +22,42 @@ are voiced, and will allow IRC users to run `.players` to get a list of players
on all the servers without flooding the channel (as badly as requesting a player
list from every server). Currently not cross-channel and will ignore devoices.
## lua.py
A wrapper around [lupa](https://github.com/scoder/lupa) to make creating
miniirc bots with lua easier. This may move eventually.
Dependencies: `sudo pip3 install lupa>=1.8 miniirc_extras miniirc>=1.4.0`
### Usage
```
./lua.py <path to lua file>
```
This lua file will be able to use the `miniirc` global variable. `IRC` objects
are not called with `:` (`irc.msg(...)` instead of `irc:msg(...)`). *Remember
that lua table indexes start with 1 and not 0.*
#### `await`
Handlers are executed inside coroutines and an `await` function is created, so
that Python functions can be called without blocking the lua thread. The main
lua file can return a function that will be called inside a coroutine.
```lua
await(blocking_python_function, parameters)
await{blocking_python_function, parameters, keyword='argument'}
```
*Note: `await`ing a lua function is pointless and will still block the lua
thread.*
#### `sleep`
The same syntax as `time.sleep`, doesn't block the lua thread if called inside
a coroutine.
# miniirc
A simple IRC client framework.

225
lua.py Executable file
View File

@ -0,0 +1,225 @@
#!/usr/bin/env python3
#
# Lua miniirc wrapper
#
# © 2019 by luk3yx.
#
import miniirc, os, sys, threading
from miniirc_extras import AbstractIRC, utils
from typing import Any, Callable, Optional, Union
import lupa # type: ignore
@lupa.unpacks_lua_table
def _unpack_lua_table(*args, **kwargs):
return args, kwargs
# Wrap functions so they work nicely
class _FunctionWrapper:
def _lua_args(self, args: tuple):
for arg in args:
if isinstance(arg, (tuple, list, dict)):
yield self._lua.table_from(arg)
else:
yield arg
# Make await() work
def __call__(self, *args):
co = self._func.coroutine(*self._lua_args(args))
try:
res = co.send(None)
while res and isinstance(res, tuple) and callable(res[0]):
if not res[0]:
print(res[-1], file=sys.stderr)
return
args, kwargs = _unpack_lua_table(*res[1:])
try:
data = res[0](*args, **kwargs)
except Exception as e:
res = co.send((False, type(e).__name__ + ': ' + str(e)))
else:
res = co.send((True, data))
del data
except lupa.LuaError as e:
print(e, file=sys.stderr)
try:
print(self._tb(co), file=sys.stderr)
except:
pass
except (RuntimeError, StopIteration):
pass
def __init__(self, lua: lupa.LuaRuntime, func):
if not callable(func):
raise TypeError('_FunctionWrapper expects a callable object.')
self._lua = lua
self._func = func
self._tb = self._lua.globals().debug.traceback
assert hasattr(func, 'coroutine') and callable(func.coroutine)
# Wrap Handler functions
class _HandlerFuncWrapper:
__slots__ = ('_lua', '_func')
@lupa.unpacks_lua_table_method
def __call__(self, *events, colon: bool = False,
func: Optional[Callable] = None, **kwargs) -> Optional[Callable]:
add_handler = self._func(*events, colon=colon, **kwargs)
if func is None:
return lambda func : add_handler(_FunctionWrapper(self._lua, func))
else:
add_handler(_FunctionWrapper(self._lua, func))
return func
def __eq__(self, other):
return self._func == other
def __ne__(self, other):
return self._func != other
def __init__(self, lua: lupa.LuaRuntime, func: Callable) -> None:
self._lua = lua
self._func = func
# Wrap lua runtimes
class RuntimeWrapper:
__slots__ = ('dofile', 'loadstring', 'lua', '_handlers', '_await')
# Some inital code
_code = rb"""
if table.unpack then
unpack = table.unpack
else
table.unpack = assert(unpack)
end
if loadstring then
load = loadstring
else
loadstring = load
end
function import(...)
local n = select('#', ...)
local res = {}
for i = 1, n do
local name = select(i, ...)
if name == 'miniirc' and miniirc then
table.insert(res, miniirc)
else
table.insert(res, python.builtins.__import__(name))
end
end
return table.unpack(res)
end
local _miniirc, lupa, time = import('miniirc', 'lupa', 'time')
miniirc = {
Handler = _miniirc.Handler,
CmdHandler = _miniirc.CmdHandler,
IRC = lupa.unpacks_lua_table(_miniirc.IRC),
ver = {},
}
do
for i in python.iter(_miniirc.ver) do table.insert(miniirc.ver, i) end
end
-- Create await()
function await(func, ...)
local good, msg
local n = select('#', ...)
if n == 0 and type(func) == 'table' then
good, msg = coroutine.yield(table.remove(func, 1), func)
elseif n < 2 then
good, msg = coroutine.yield(func, {...})
end
if not good then error(msg, 2) end
return msg
end
-- A nicer time.sleep() wrapper
function sleep(seconds)
if type(seconds) ~= 'number' then
error('sleep() expects a number, not ' .. type(seconds) .. '.', 2)
elseif seconds < 0 then
error('sleep() length must be non-negative.', 2)
elseif seconds == 0 then
return
end
local thread, main = coroutine.running()
if not thread or main then
time.sleep(seconds)
else
await(time.sleep, seconds)
end
end
""".replace(b'\n ', b'\n')
# Override Handlers
def _getter(self, obj, attr_name):
if attr_name in ('Handler', 'CmdHandler') and (obj is miniirc
or isinstance(obj, (AbstractIRC, utils.HandlerGroup))):
return _HandlerFuncWrapper(self.lua, getattr(obj, attr_name))
res = getattr(obj, attr_name)
return res
@property
def globals(self):
return self.lua.globals
# Create a new class right away
@classmethod
def run(cls, file: str) -> None:
cls().dofile(file)
@property
def eval(self) -> Callable[[Union[str, bytes]], Any]:
return self.lua.eval
@property
def exec(self) -> Callable[[Union[str, bytes]], Any]:
return self.lua.execute
def wrap_lua_function(self, func: Callable) -> Callable:
""" Wraps a lua function so await() will work. """
if not callable(getattr(func, 'coroutine', None)):
raise TypeError('wrap_lua_function() expects a Lua function.')
return _FunctionWrapper(self.lua, func)
def call_async(self, func: Callable, *args, **kwargs):
return self.wrap_lua_function(func)(*args, **kwargs)
def __init__(self, clone: bool = False) -> None:
self.lua = lupa.LuaRuntime(attribute_handlers=(self._getter, setattr))
globs = self.lua.globals()
assert __file__.endswith('.py')
self.dofile = globs.dofile # type: Callable[[str], Any]
self.exec(self._code)
self.loadstring = globs.loadstring # type: Callable[[Union[str, bytes]], Callable]
run = RuntimeWrapper.run
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('file', help='The lua file to run.')
args = parser.parse_args()
wrapper = RuntimeWrapper()
try:
res = wrapper.dofile(args.file)
if callable(res):
wrapper.call_async(res)
except lupa.LuaError as e:
print(e, file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()