Module:Lua class

local libraryUtil = require('libraryUtil') -- overridden for new types and exceptions

local warn = require('Module:Warning')

local mode_mt = {__mode='k'}

local _classes, _instances = {}, {} -- registry mapping all private classes and instances to their internal counterparts

setmetatable(_classes, mode_mt); setmetatable(_instances, mode_mt)

local classes, instances = {}, {} -- same but public -> private

setmetatable(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 = {__ipairs=1, __pairs=1, __tostring=1, __unm=1}

local bin_metamethods = {__add=1, __concat=1, __div=1, __eq=1, __le=1, __lt=1, __mod=1, __mul=1, __pow=1, __sub=1}

local oth_metamethods = {__call=1, __index=1, __newindex=1, __init=1}

local not_metamethods = {__name=1, __bases=1, __methods=1, __classmethods=1, __staticmethods=1, __normalmethods=1, __slots=1, __protected=1}

-- __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 = {

__index = function (self, key)

if tonumber(key) or key == 'hash' or key == '__hash' then -- don't search numeric keys in classes and hash isn't inheritable

return nil

end

return self.__class[key] -- key could be invalid here without issues as __index(cls_private, key) would handle it

end,

__tostring = objtostr--

}

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 them

end

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 value

end

local function private_write(self_private, key, value)

libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'})

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 {%s: %s} 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] = value

end

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 {%s: %s} 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, {'string', 'number'})

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

end

end

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 value

end

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 {%s: %s}"):format(key, tostring(value)), 2)

elseif cls.__normalmethods[key] then

error(("AttributeError: forbidden write attempt {%s: %s} 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, {'string', 'number'}) -- otherwise error message would not make sense

error(("AttributeError: public attribute creation attempt {%s: %s} not expected by __slots"):format(key, tostring(value)), 2)

end

self[key] = value

end

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 {%s: %s}"):format(key, tostring(value)), 2)

elseif cls.__normalmethods[key] then

error(("AttributeError: forbidden write attempt {%s: %s} 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, {'string', 'number'}) -- otherwise error message...

error(("AttributeError: public attribute creation attempt {%s: %s} not expected by __slots"):format(key, tostring(value)), 2)

end

self[key] = value

end

end

local function constructor(wrapper, ...)

if select('#', ...) ~= 1 or type(...) ~= 'table' then

error("SyntaxError: incorrect instance constructor syntax, should be: Class{arg1, arg2..., kw1=kwarg1, kw2=kwarg2...}", 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 instances

end

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

end

end

local cls_mt = {

__index = multi_inheritance,

__tostring = objtostr--

}

local cls_private_mt = {

__call = constructor,

__index = function (cls_private, key)

if type(key) ~= 'string' then

warn(("AttributeWarning: index '%s' type should be string, %s given"):format(tostring(key), type(key)), 2)

elseif not_metamethods[key] then

error(("AttributeError: unauthorized read attempt of internal '%s'"):format(key), 2)

elseif key:find('[^_%w]') or key:find('^%d') then

warn(("AttributeWarning: index '%s' should be a valid Lua name"):format(key), 2)

end

local cls = _classes[cls_private]

local value = cls[key]

if not cls.__slots[key] then

local valueType = type(value)

if valueType == 'table' then

return mw.clone(value) -- because class attributes are immutable by default

elseif valueType == 'set' then --should list be clone or deep copy?

return value.copy()

end

end

return value

end,

__newindex = function (cls_private, key, value)

local cls = _classes[cls_private]

if not cls.__slots[key] and key ~= '__hash' then -- __slots should be valid, so no need to check key validity before

libraryUtil.checkTypeMultiForIndex(key, {'string'}) -- otherwise error message would not make sense

error(("AttributeError: write attempt {%s: %s} not expected by __slots"):format(key, tostring(value)), 2)

elseif key == '__hash' and cls.__hash ~= nil then

error("AttributeError: forbidden update attempt to immutable __hash", 2)

end

cls[key] = value

end,

__metatable = "unauthorized access attempt of wrapper object metatable"

}

local cls_public_mt = {

__call = constructor,

__index = function (cls_public, key)

if type(key) ~= 'string' then

warn(("AttributeWarning: index '%s' type should be string, %s given"):format(tostring(key), type(key)), 2)

elseif key:sub(1,1) == '_' then

error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2)

elseif key:find('[^_%w]') or key:find('^%d') then

warn(("AttributeWarning: index '%s' should be a valid Lua name"):format(key), 2)

end

local value = _classes[classes[cls_public]][key]

local valueType = type(value)

if valueType == 'table' then

return mw.clone(value) -- all class attributes are immutable in the public scope

elseif valueType == 'set' then

return value.copy()

end

return value

end,

__newindex = function (cls_public, key, value)

libraryUtil.checkTypeMultiForIndex(key, {'string'}) -- otherwise error message...

error(("AttributeError: forbidden write attempt of {%s: %s}; a class is immutable in public scope"):format(key, tostring(value)), 2)

end,

__metatable = "unauthorized access attempt of wrapper object metatable"

}

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.__hash

end

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 = {

__index = function (__slots, key) -- multi_inheritance

for _, base in ipairs(cls.__bases) do

if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__slots[key] then

return true

end

end

end

}

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 = {

__index = function (__protected, key)

for _, base in ipairs(cls.__bases) do

if base.__protected[key] then

return true

end

end

end

}

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 = {

__index = function (__normalmethods, key)

return cls.__methods[key] or cls.__classmethods[key] or cls.__staticmethods[key]

end

}

setmetatable(cls.__normalmethods, mt)

local cls_private = {} -- wrapper

setmetatable(cls_private, cls_private_mt)

_classes[cls_private] = cls

cls.__classmethods = {}

mt = {

__index = function (__classmethods, key)

for _, base in ipairs(cls.__bases) do

if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__classmethods[key] then

return true

end

end

end

}

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 = {

__index = function (__staticmethods, key)

for _, base in ipairs(cls.__bases) do

if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__staticmethods[key] then

return true

end

end

end

}

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 {%s: %s}"):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_private

end

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 false

end

local function rissubclass1(class, classinfo, parent, level)

libraryUtil.checkTypeMulti(parent, 2, classinfo, {'class', 'table'}, 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 false

end

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 = type

type = 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 t

end

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)

end

end

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)

end

end

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)

end

end

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, {string=1, table=1}

repeat

libraryUtil.checkTypeMulti('try', i, args[i], {'string', 'table', 'function'}, 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] = {exceptions={}, handler=args[i+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] = {exceptions={}, handler=args[i]}

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_clause

end

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?

end

end

local classes_proxy, instances_proxy = {}, {}

setmetatable(classes_proxy, {

__index = classes, -- create function to limit access only to modules which define the requested classes and testcases pages

__newindex = function () error("KeyError: forbidden write attempt to classes proxy", 2) end,

__metatable = "unauthorized access attempt of classes proxy metatable"

})

setmetatable(instances_proxy, {

__index = instances, -- create function to limit access only to testcases pages

__newindex = function () error("KeyError: forbidden write attempt to instances proxy", 2) end,

__metatable = "unauthorized access attempt of instances proxy metatable"

})

return {classes_proxy, instances_proxy}