Module:Lua class explained

local libraryUtil = require('libraryUtil') -- overridden for new types and exceptionslocal warn = require('Module:Warning')

local mode_mt = local _classes, _instances =, -- registry mapping all private classes and instances to their internal counterpartssetmetatable(_classes, mode_mt); setmetatable(_instances, mode_mt)local classes, instances =, -- same but public -> privatesetmetatable(classes, mode_mt); setmetatable(instances, mode_mt)local inst_private_mts, inst_public_mts =, -- for each class since they are mostly immutable

local una_metamethods = local bin_metamethods = local oth_metamethods = local not_metamethods = -- __class and __hash

local function objtostr(obj) local copy = for key, val in pairs(obj) do copy[key] = type(val)

'function' and 'function' or val end return mw.text.jsonEncode(copy, mw.text.JSON_PRETTY)end

local inst_mt =

local function private_read(self_private, key) return _instances[self_private][key] -- instance should be clean of invalid keys so that __index(cls_private, key) handles themend

local function private_read_custom(self_private, key) if not_metamethods[key] then error(("AttributeError: unauthorized read attempt of internal '%s'"):format(key), 2) end local self = _instances[self_private] local value = self.__class.__index(self_private, key) -- custom __index can handle invalid keys if value

nil then return self[key] -- same reason of private_read for not checking key validity end return valueend

local function private_write(self_private, key, value) libraryUtil.checkTypeMultiForIndex(key,) local self = _instances[self_private] if type(key)

'string' then local cls = _classes[self.__class] if cls.__normalmethods[key] or key:sub(1,2)

'__' and key ~= '__hash' then error(("AttributeError: forbidden write attempt to immutable method or invalid key"):format(key, tostring(value)), 2) elseif key:find('[^_%w]') or key:find('^%d') then error(("AttributeError: invalid attribute name '%s'"):format(key), 2) elseif key

'__hash' and self.__hash ~= nil then error("AttributeError: forbidden update attempt to immutable __hash", 2) end end self[key] = valueend

local function private_write_custom(self_private, key, value) local self = _instances[self_private] local cls = _classes[self.__class] local keyType = type(key) if keyType

'string' and (cls.__normalmethods[key] or key:sub(1,2)

'__' and key ~= '__hash') then error(("AttributeError: forbidden write attempt to immutable method or invalid key"):format(key, tostring(value)), 2) end if cls.__newindex(self_private, key, value)

false then -- custom __newindex can handle invalid keys libraryUtil.checkTypeMultiForIndex(key,) if keyType

'string' then if key:find('[^_%w]') or key:find('^%d') then error(("AttributeError: invalid attribute name '%s'"):format(key), 2) elseif key

'__hash' and self.__hash ~= nil then error("AttributeError: forbidden update attempt to immutable __hash", 2) end end self[key] = value endend

local function public_read(self_public, key) if type(key)

'string' and key:sub(1,1)

'_' then error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2) end return _instances[instances[self_public]][key] -- same reason of private_read...end

local function public_read_custom(self_public, key) if type(key)

'string' and key:sub(1,1)

'_' then error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2) end local self = _instances[instances[self_public]] local value = self.__class.__index(instances[self_public], key) if value

nil then return self[key] -- same reason of private_read... end return valueend

local function public_write(self_public, key, value) local self = _instances[instances[self_public]] local cls = _classes[self.__class] if type(key)

'string' then if key:sub(1,1)

'_' then error(("AttributeError: unauthorized write attempt of nonpublic "):format(key, tostring(value)), 2) elseif cls.__normalmethods[key] then error(("AttributeError: forbidden write attempt to immutable method"):format(key, tostring(value)), 2) end end if self[key]

nil and not cls.__slots[key] then -- if instance and __slots are valid, no danger of creating invalid attributes libraryUtil.checkTypeMultiForIndex(key,) -- otherwise error message would not make sense error(("AttributeError: public attribute creation attempt not expected by __slots"):format(key, tostring(value)), 2) end self[key] = valueend

local function public_write_custom(self_public, key, value) local self = _instances[instances[self_public]] local cls = _classes[self.__class] if type(key)

'string' then if key:sub(1,1)

'_' then error(("AttributeError: unauthorized write attempt of nonpublic "):format(key, tostring(value)), 2) elseif cls.__normalmethods[key] then error(("AttributeError: forbidden write attempt to immutable method"):format(key, tostring(value)), 2) end end if cls.__newindex(instances[self_public], key, value)

false then if self[key]

nil and not cls.__slots[key] then libraryUtil.checkTypeMultiForIndex(key,) -- otherwise error message... error(("AttributeError: public attribute creation attempt not expected by __slots"):format(key, tostring(value)), 2) end self[key] = value endend

local function constructor(wrapper, ...) if select('#', ...) ~= 1 or type(...) ~= 'table' then error("SyntaxError: incorrect instance constructor syntax, should be: Class", 2) end local self = -- __new local cls_private = classes[wrapper] or wrapper self.__class = cls_private setmetatable(self, inst_mt)

local self_private = -- wrapper local cls = _classes[cls_private]

local mt = inst_private_mts[cls] if not mt then mt = mt.__index = cls.__index and private_read_custom or private_read mt.__newindex = cls.__newindex and private_write_custom or private_write for key in pairs(una_metamethods) do mt[key] = cls[key] end mt.__call = cls.__call mt.__metatable = "unauthorized access attempt of wrapper object metatable"

inst_private_mts[cls] = mt end

setmetatable(self_private, mt) _instances[self_private] = self

local __init = cls.__init if __init and __init(self_private, ...) then error("TypeError: __init must not return a var-list") end

for key in pairs(cls.__methods) do local func = cls[key] -- index once to save time in future calls self[key] = function (...) return func(self_private, ...) end end

if cls._hash then -- not inheritable self.hash = function return cls._hash(self_private) end self.hash -- construction of self is finalized at this point, so immutable hash can be safely set end

local self_public =

mt = inst_public_mts[cls] if not mt then mt = mt.__index = cls.__index and public_read_custom or public_read mt.__newindex = cls.__newindex and public_write_custom or public_write for key in pairs(una_metamethods) do if cls[key] then local func = cls[key] mt[key] = function (a) return func(instances[a]) end end end for key in pairs(bin_metamethods) do if cls[key] then local func = cls[key] mt[key] = function (a, b) return func(instances[a], instances[b]) end end end if cls.__call then local func = cls.__call mt.__call = function (self_public, ...) return func(instances[self_public], ...) end end mt.__metatable = "unauthorized access attempt of wrapper object metatable"

inst_public_mts[cls] = mt end

setmetatable(self_public, mt) instances[self_public] = self_private return self_public, not classes[wrapper] and self_private or nil -- so that constructions in private scopes have access to private instancesend

local function multi_inheritance(cls, key) for _, base in ipairs(cls.__bases) do if key:sub(1,1) ~= '_' or base.__protected[key] or key:sub(1,2)

'__' and key ~= '__name' and key ~= '__hash' then local value = base[key] if value ~= nil then return value end end endend

local cls_mt =

local cls_private_mt =

local cls_public_mt =

local function default_hash(obj_private) if obj_private.__hash

nil then -- not inheritable obj_private.__hash = tonumber('0x' .. mw.hash.hashValue('fnv1a32', tostring(os.time + math.random))) end return obj_private.__hashend

function class(...) local args = local cls = -- internal

local idx if type(args[1])

'string' then local __name = args[1] if __name:find('%W') or __name:find('^%d') then error(("ValueError: class '%s' must be a valid Lua name without '_'s"):format(__name), 2) end cls.__name = __name idx = 2 else idx = 1 end

cls.__bases = for i = idx, #args-1 do libraryUtil.checkType('class', i, args[i], 'class') cls.__bases[#cls.__bases+1] = _classes[classes[args[i]]] end setmetatable(cls, cls_mt)

local kwargs = args[#args] libraryUtil.checkType('class', #args, kwargs, 'table') if kwargs.__name ~= nil or kwargs.__bases ~= nil then error("ValueError: __name and unpacked __bases must be passed as optional first args to 'class'", 2) end

cls.__slots = local mt = setmetatable(cls.__slots, mt) if kwargs.__slots ~= nil then libraryUtil.checkTypeForNamedArg('class', '__slots', kwargs.__slots, 'table') for i, slot in ipairs(kwargs.__slots) do libraryUtil.checkType('__slots', i, slot, 'string') if slot:find('[^_%w]') or slot:find('^%d') then error(("ValueError: invalid slot name '%s'"):format(slot), 2) elseif slot:sub(1,2)

'__' then error(("ValueError: slot '%s' has forbidden namespace"):format(slot), 2) elseif rawget(cls.__slots, slot) then warn(("ValueWarning: duplicated slot '%s'"):format(slot), 2) elseif kwargs[slot] ~= nil or cls.__slots[slot] then error(("ValueError: slot '%s' is predefined in class or allocated in __slots of bases"):format(slot), 2) end cls.__slots[slot] = true end kwargs.__slots = nil end

cls.__protected = mt = setmetatable(cls.__protected, mt) if kwargs.__protected ~= nil then libraryUtil.checkTypeForNamedArg('class', '__protected', kwargs.__protected, 'table') for i, key in ipairs(kwargs.__protected) do libraryUtil.checkType('__protected', i, key, 'string') if key:sub(1,1) ~= '_' or key:sub(2,2)

'_' then error(("ValueError: the namespace of '%s' is not manually protectable"):format(key), 2) elseif key

'_hash' then error("ValueError: forbidden attempt to protect _hash which is not inheritable", 2) elseif rawget(cls.__protected, key) then warn(("ValueWarning: duplicated '%s' in __protected"):format(key), 2) elseif cls.__protected[key] then error(("ValueError: '%s' is already allocated in __protected of bases"):format(key), 2) elseif kwargs[key]

nil then -- key validity will be checked further ahead error(("ValueError: attempt to protect undefined '%s'"):format(key), 2) end cls.__protected[key] = true end kwargs.__protected = nil end

if kwargs.__methods ~= nil then error("ValueError: __classmethods and __staticmethods should be passed as optional attributes instead of __methods", 2) elseif kwargs.hash ~= nil or kwargs.__hash ~= nil then error("ValueError: forbidden attempt to define hash or __hash which are set internally", 2) end

cls.__normalmethods = -- used in instance write methods mt = setmetatable(cls.__normalmethods, mt)

local cls_private = -- wrapper setmetatable(cls_private, cls_private_mt) _classes[cls_private] = cls

cls.__classmethods = mt = setmetatable(cls.__classmethods, mt) if kwargs.__classmethods ~= nil then libraryUtil.checkTypeForNamedArg('class', '__classmethods', kwargs.__classmethods, 'table') for i, key in ipairs(kwargs.__classmethods) do libraryUtil.checkType('__classmethods', i, key, 'string') if key:find('[^_%w]') or key:find('^%d') then error(("ValueError: invalid classmethod name '%s'"):format(key), 2) elseif key:sub(1,2)

'__' then error(("ValueError: classmethod '%s' has forbidden namespace"):format(key), 2) elseif key

'_hash' then error("ValueError: invalid classmethod _hash, classes have their own hash classmethod", 2) elseif rawget(cls.__classmethods, key) then error(("ValueError: duplicated '%s' in __classmethods"):format(key), 2) elseif not cls.__classmethods[key] and cls[key] ~= nil then error(("ValueError: forbidden attempt to convert '%s' non-classmethod to classmethod"):format(key), 2) end libraryUtil.checkTypeForNamedArg('class', key, kwargs[key], 'function')

cls.__classmethods[key] = true local func = kwargs[key] cls[key] = function (...) return func(cls_private, ...) end kwargs[key] = nil end kwargs.__classmethods = nil end

cls.__normalmethods.hash = true cls.hash = function return default_hash(cls_private) end -- classes are always hashable so this is independent from _hash cls.hash

-- see https://docs.python.org/3/reference/datamodel.html#object.__hash__ if kwargs.__eq

nil then if kwargs._hash then warn("ValueWarning: _hash is defined but not __eq, which is expected", 2) elseif kwargs._hash

false then kwargs._hash = nil else kwargs._hash = default_hash end end if kwargs._hash ~= nil then libraryUtil.checkTypeForNamedArg('class', '_hash', kwargs._hash, 'function') cls.__normalmethods._hash = true cls._hash = kwargs._hash kwargs._hash = nil end

cls.__staticmethods = mt = setmetatable(cls.__staticmethods, mt) if kwargs.__staticmethods ~= nil then libraryUtil.checkTypeForNamedArg('class', '__staticmethods', kwargs.__staticmethods, 'table') for i, key in ipairs(kwargs.__staticmethods) do libraryUtil.checkType('__staticmethods', i, key, 'string') if key:sub(1,2)

'__' then error(("ValueError: staticmethod '%s' has forbidden namespace"):format(key), 2) elseif rawget(cls.__staticmethods, key) then warn(("ValueWarning: duplicated staticmethod '%s'"):format(key), 2) elseif not cls.__staticmethods[key] and cls[key] ~= nil then error(("ValueError: forbidden attempt to convert '%s' non-staticmethod to staticmethod"):format(key), 2) end libraryUtil.checkTypeForNamedArg('class', key, kwargs[key], 'function') cls.__staticmethods[key] = true end kwargs.__staticmethods = nil end

cls.__methods = for _, base in ipairs(cls.__bases) do for key in pairs(base.__methods) do if key:sub(1,1) ~= '_' or base.__protected[key] then cls.__methods[key] = true end end end

local valid = false for key, val in pairs(kwargs) do if type(key) ~= 'string' then error(("TypeError: invalid attribute name '%s' (string expected, got %s)"):format(tostring(key), type(key)), 2) elseif key:find('[^_%w]') or key:find('^%d') then error(("ValueError: invalid attribute name '%s'"):format(key), 2) elseif key:sub(1,2)

'__' and not una_metamethods[key] and not bin_metamethods[key] and not oth_metamethods[key] then error(("ValueError: unrecognized metamethod or unauthorized internal attribute "):format(key, tostring(val)), 2) end cls[key] = val if type(val)

'function' then if not cls.__staticmethods[key] and key:sub(1,2) ~= '__' then -- classmethods and _hash were already removed from kwargs cls.__methods[key] = true end if key ~= '__init' then -- __init does not qualify to a functional/proper class valid = true end end end if not valid then error("ValueError: a (sub)class must have at least one functional method", 2) end

local cls_public = setmetatable(cls_public, cls_public_mt) classes[cls_public] = cls_private return cls_public, cls_privateend

local function rissubclass2(class, classinfo) if class

classinfo then return true end for _, base in ipairs(class.__bases) do if rissubclass2(base, classinfo) then return true end end return falseend

local function rissubclass1(class, classinfo, parent, level) libraryUtil.checkTypeMulti(parent, 2, classinfo,, level) if classes[classinfo] then return rissubclass2(class, _classes[classes[classinfo]]) elseif _classes[classinfo] then return rissubclass2(class, _classes[classinfo]) end for i = 1, #classinfo do if rissubclass1(class, classinfo[i], parent, level+1) then return true end end return falseend

function issubclass(class, classinfo) libraryUtil.checkType('issubclass', 1, class, 'class') class = classes[class] or class return rissubclass1(_classes[class], classinfo, 'issubclass', 4)end

function isinstance(instance, classinfo) if not instances[instance] and not _instances[instance] then -- because named (ClassName) instances would fail with checkType if classinfo

nil then return false end error(("TypeError: bad argument #1 to 'isinstance' (instance expected, got %s)"):format(type(instance)), 2) end if classinfo

nil then return true end instance = instances[instance] or instance return rissubclass1(_classes[instance.__class], classinfo, 'isinstance', 4)end

local _type = typetype = function (value) local t = _type(value) if t

'table' then if classes[value] or _classes[value] then return 'class' elseif instances[value] or _instances[value] then value = instances[value] or value return _classes[value.__class].__name or 'instance' -- should __name be directly readable instead? end end return tend

libraryUtil.checkType = function (name, argIdx, arg, expectType, nilOk, level) if arg

nil and nilOk then return end if type(arg) ~= expectType then error(("TypeError: bad argument #%d to '%s' (%s expected, got %s)"):format(argIdx, name, expectType, type(arg)), level or 3) endend

libraryUtil.checkTypeMulti = function (name, argIdx, arg, expectTypes, level) local argType = type(arg) for _, expectType in ipairs(expectTypes) do if argType

expectType then return end end local n = #expectTypes local typeList if n > 1 then typeList = table.concat(expectTypes, ', ', 1, n-1) .. ' or ' .. expectTypes[n] else typeList = expectTypes[1] end error(("TypeError: bad argument #%d to '%s' (%s expected, got %s)"):format(argIdx, name, typeList, type(arg)), level or 3)end

libraryUtil.checkTypeForIndex = function (index, value, expectType, level) if type(value) ~= expectType then error(("TypeError: value for index '%s' must be %s, %s given"):format(index, expectType, type(value)), level or 3) endend

libraryUtil.checkTypeMultiForIndex = function (index, expectTypes, level) local indexType = type(index) for _, expectType in ipairs(expectTypes) do if indexType

expectType then return end end local n = #expectTypes local typeList if n > 1 then typeList = table.concat(expectTypes, ', ', 1, n-1) .. ' or ' .. expectTypes[n] else typeList = expectTypes[1] end error(("TypeError: index '%s' must be %s, %s given"):format(index, typeList, type(index)), level or 3)end

libraryUtil.checkTypeForNamedArg = function (name, argName, arg, expectType, nilOk, level) if arg

nil and nilOk then return end if type(arg) ~= expectType then error(("TypeError: bad named argument %s to '%s' (%s expected, got %s)"):format(argName, name, expectType, type(arg)), level or 3) endend

libraryUtil.checkTypeMultiForNamedArg = function (name, argName, arg, expectTypes, level) local argType = type(arg) for _, expectType in ipairs(expectTypes) do if argType

expectType then return end end local n = #expectTypes local typeList if n > 1 then typeList = table.concat(expectTypes, ', ', 1, n-1) .. ' or ' .. expectTypes[n] else typeList = expectTypes[1] end error(("TypeError: bad named argument %s to '%s' (%s expected, got %s)"):format(argName, name, typeList, type(arg)), level or 3)end

local function try_parser(...) local args = libraryUtil.checkType('try', 1, args[1], 'function', nil, 4) local try_clause = args[1]

if args[2] ~= 'except' then error("SyntaxError: missing required except clause", 3) end local except_clauses = local i = 3 local argType, exceptionTypes = nil, repeat libraryUtil.checkTypeMulti('try', i, args[i],, 4) argType = type(args[i]) if exceptionTypes[argType] then libraryUtil.checkType('try', i+1, args[i+1], 'function', nil, 4) except_clauses[#except_clauses+1] = if argType

'string' then except_clauses[#except_clauses].exceptions[args[i]] = true else for _, exception in ipairs(args[i]) do if type(exception) ~= 'string' then error(("TypeError: invalid exception type in except (string expected, got %s)"):format(type(exception)), 3) end except_clauses[#except_clauses].exceptions[exception] = true end end i = i + 3 else except_clauses[#except_clauses+1] = i = i + 2 break end until args[i-1] ~= 'except'

local else_clause, finally_clause if args[i-1]

'except' then error("SyntaxError: except after except clause without specific exceptions, which should be the last", 3) elseif args[i-1]

'else' then libraryUtil.checkType('try', i, args[i], 'function', nil, 4) else_clause = args[i] i = i + 2 end if args[i-1]

'finally' then libraryUtil.checkType('try', i, args[i], 'function', nil, 4) finally_clause = args[i] i = i + 2 end

if args[i-1] ~= nil then error(("SyntaxError: unexpected arguments #%d–#%d to 'try'"):format(i-1, #args), 3) end return try_clause, except_clauses, else_clause, finally_clauseend

function try(...) local try_clause, except_clauses, else_clause, finally_clause = try_parser(...)

local function errhandler(message) local errtype = mw.text.split(message, ':')[1] local handled = false for _, except in ipairs(except_clauses) do if except.exceptions[errtype] or #except.exceptions

0 then handled, message = pcall(except.handler) break end end if not handled then return message end end

local success, message = xpcall(try_clause, errhandler) if else_clause and success then success, message = pcall(else_clause) end if finally_clause then finally_clause end if not success and message then error(message, 0) -- what should be the level? endend

local classes_proxy, instances_proxy =,

setmetatable(classes_proxy,)

setmetatable(instances_proxy,)

return