Permalink
Fetching contributors…
Cannot retrieve contributors at this time
266 lines (226 sloc) 6.92 KB
--
-- This modules provides utilities to access Python
-- objects from Lua. It should be used together with
-- utilities in qtrender_lua.
--
-- This function works very much like standard Lua assert, but:
--
-- * the first argument is the stack level to report the error at (1 being
-- current level, like for `error` function)
-- * it strips the flag if it evaluates to true
-- * it does not take a message parameter and thus will always preserve all
-- elements of the tuple
--
local function assertx(nlevels, ok, ...)
if not ok then
-- print("Assertx nlevels=", nlevels, "tb: ", debug.traceback())
local msg = tostring(select(1, ...))
error(msg, 1 + nlevels)
else
return ...
end
end
-- Python Splash commands return
--
-- operation, [ result1, result2, ... ]
--
-- tuples. Operation can be one of the following:
--
-- * "return": return the rest of the tuple
--
-- * "yield": yield the rest of the tuple with coroutine.yield
--
-- * "raise": raise an error using the rest of the tuple
--
local function unwrap_python_result(error_nlevels, op, ...)
if op == 'return' then
return ...
elseif op == 'raise' then
assertx(error_nlevels, nil, ...)
elseif op == 'yield' then
return unwrap_python_result(error_nlevels, coroutine.yield(...))
else
error('Invalid operation: ' .. tostring(op))
end
end
local function unwraps_python_result(func, nlevels)
if nlevels == nil then
-- nlevels is passed straight to the corresponding assertx func.
nlevels = 1
end
return function(...)
return unwrap_python_result(1 + nlevels, func(...))
end
end
--
-- Python methods don't want explicit 'self' argument;
-- this decorator adds a dummy 'self' argument to allow Lua
-- methods syntax.
--
local function drops_self_argument(func)
return function(self, ...)
return func(...)
end
end
--
-- A decorator that fixes an issue with passing callbacks from Lua to Python
-- by putting the callback to a table provided by the caller.
-- See https://github.com/scoder/lupa/pull/49 for more.
--
local function sets_callback(func, storage)
return function(cb, ...)
storage[1] = cb
return func(...)
end
end
local function is_private_name(name)
-- Method/attribute name is private true if it starts with an underscore.
return name:sub(1, 1) == "_"
end
--
-- Create a Lua wrapper for a Python object.
--
-- * Lua methods are created for Python methods wrapped in @command.
-- * Async methods are wrapped with `coroutine.yield`.
-- * Lua <-> Python error handling is fixed.
-- * Private methods are stored in `private_self`, public methods are
-- stored in `self`.
--
local function setup_methods(py_object, self, cls)
-- Create lua_object:<...> methods from py_object methods:
for key, opts in pairs(py_object.commands) do
local command = py_object[key]
if opts.sets_callback then
command = sets_callback(command, py_object.tmp_storage)
end
command = drops_self_argument(command)
local nlevels = 1
if is_private_name(key) then
-- private functions are wrapped, so nlevels is set to 2 to show error
-- line number in user code
nlevels = 2
end
command = unwraps_python_result(command, nlevels)
rawset(self, key, command)
end
for key, value in pairs(cls) do
if type(value) == "function" then
rawset(self, key, drops_self_argument(function(...)
return value(self, ...)
end))
end
end
end
--
-- Handle @lua_property decorators.
--
local function setup_property_access(py_object, self)
rawset(self, '__getters', {})
rawset(self, '__setters', {})
for name, opts in pairs(py_object.lua_properties) do
self.__getters[name] = unwraps_python_result(drops_self_argument(py_object[opts.getter]))
if opts.setter ~= nil then
self.__setters[name] = unwraps_python_result(drops_self_argument(py_object[opts.setter]))
else
self.__setters[name] = function()
error("Attribute " .. name .. " is read-only.", 2)
end
end
end
end
-- This value is used to protect the metatable of an exposed object from being
-- edited and replaced.
local EXPOSED_OBJ_METATABLE_PLACEHOLDER = '<wrapped object>'
--
-- Create a Lua wrapper for a Python object.
--
local function wrap_exposed_object(py_object, private_self, cls)
setmetatable(private_self, cls)
setup_methods(py_object, private_self, cls)
setup_property_access(py_object, private_self)
-- "Public" metatable that prevents access to private elements and to itself.
local public_mt = {
__index = function(self, key)
if is_private_name(key) then
return nil
end
return private_self[key]
end,
__newindex = function(self, key, value)
if is_private_name(key) then
error("Cannot set private field: " .. tostring(key), 2)
end
assertx(2, pcall(function()
private_self[key] = value
end))
end,
__pairs = function(self)
local wrapper = function(t, k)
local v
repeat
k, v = next(private_self, k)
until k == nil or not is_private_name(k)
return k, v
end
return wrapper, self, nil
end,
__metatable = EXPOSED_OBJ_METATABLE_PLACEHOLDER,
}
-- Forward any metatable events not defined in the "public" table to the
-- actual class/metadatable.
setmetatable(public_mt, {__index = cls})
-- public_self should only contain a reference to the public metatable
-- forwarding all actual data to the "real" self object.
local public_self = {
-- Add a function to the "public_self" so that it doesn't serialize cleanly
-- by mistake.
is_object = function() return true end
}
setmetatable(public_self, public_mt)
return public_self
end
--
-- Return a metatable for a wrapped Python object
--
-- WARNING: setting this metatable on an empty table might cause infinite
-- recursion during the lookup of __getters & __setters. To minimize the risk,
-- the calling code should add these fields to the table ASAP, preferably with
-- rawset.
local function create_metatable()
local cls = {}
cls.__index = function(self, index)
if self.__getters[index] then
return self.__getters[index](self)
else
return rawget(cls, index)
end
end
cls.__newindex = function(self, index, value)
if self.__setters[index] then
return self.__setters[index](self, value)
else
return rawset(self, index, value)
end
end
return cls
end
--
-- Return true if an object is a wrapped Python object
--
local function is_wrapped(obj)
local mt = getmetatable(obj)
return mt == EXPOSED_OBJ_METATABLE_PLACEHOLDER
end
-- Exposed API
return {
assertx = assertx,
unwraps_python_result = unwraps_python_result,
drops_self_argument = drops_self_argument,
raises_async = raises_async,
yields_result = yields_result,
sets_callback = sets_callback,
setup_property_access = setup_property_access,
wrap_exposed_object = wrap_exposed_object,
create_metatable = create_metatable,
is_wrapped = is_wrapped,
}