跳转到内容

模块:PythonScript/Mw

维基百科,自由的百科全书
-- Module:PythonScript/Mw
-- MediaWiki 橋接主模組:負責雙向資料轉譯 (py2lua/lua2py)、LuaInstance、FrameObject 與 TitleObject 特化
-- (MarkupBuilderBase、HtmlObject 與 SvgObject 拆分至子模組 Module:PythonScript/Mw/Html)
local MwAPI = {}
local PyClass = require("Module:PythonScript/Class")
local Runtime = require("Module:PythonScript/Runtime")
local RUtils = require("Module:PythonScript/Runtime/Utils")
local MwHtml = require("Module:PythonScript/Mw/Html")

local T_FUNC = type(function() end)
local T_TABLE = type({})
local B = Runtime.builtins

-- 私有弱引用表:集中隱藏所有特化物件的底層 Lua 實例與快取,使 Python 端 dir() / vars() 完全不可見
local mw_private = setmetatable({}, { __mode = "k" })

local py2lua, lua2py, wrap_lua_instance, make_lazy_namespace, make_py_iterator
local wrap_frame_object, wrap_title_object

local function is_none(v)
    return v == nil or v == B.None or (type(v) == T_TABLE and v.__class and v.__class.__name == "NoneType")
end

local function check_no_kwargs(kwargs, func_name)
    if kwargs and next(kwargs) ~= nil then
        Runtime.raise("TypeError", func_name .. "() takes no keyword arguments")
    end
end

-- 1. 偵測是否為 Scribunto Frame 物件 (全成員型別比對)
local function is_lua_frame(frame)
    return type(frame) == T_TABLE
        and (frame.args and type(frame.args) == T_TABLE)
        and (frame.callParserFunction and type(frame.callParserFunction) == T_FUNC)
        and (frame.expandTemplate and type(frame.expandTemplate) == T_FUNC)
        and (frame.extensionTag and type(frame.extensionTag) == T_FUNC)
        and (frame.getParent and type(frame.getParent) == T_FUNC)
        and (frame.getTitle and type(frame.getTitle) == T_FUNC)
        and (frame.newChild and type(frame.newChild) == T_FUNC)
        and (frame.preprocess and type(frame.preprocess) == T_FUNC)
        and (frame.getArgument and type(frame.getArgument) == T_FUNC)
        and (frame.newParserValue and type(frame.newParserValue) == T_FUNC)
        and (frame.newTemplateParserValue and type(frame.newTemplateParserValue) == T_FUNC)
        and (frame.argumentPairs and type(frame.argumentPairs) == T_FUNC)
end

-- 2. 偵測是否為 Scribunto Title 物件 (僅檢查非高開銷之方法成員,避免觸發昂貴查詢)
local function is_lua_title(obj)
    if type(obj) ~= T_TABLE or obj.__type ~= nil then return false end
    local ok, res = pcall(function()
        return type(obj.inNamespace) == T_FUNC
            and type(obj.inNamespaces) == T_FUNC
            and type(obj.hasSubjectNamespace) == T_FUNC
            and type(obj.subPageTitle) == T_FUNC
            and type(obj.fullUrl) == T_FUNC
            and type(obj.localUrl) == T_FUNC
            and type(obj.canonicalUrl) == T_FUNC
            and type(obj.getContent) == T_FUNC
    end)
    return ok and res == true
end

local is_lua_html = MwHtml.is_lua_html
local is_lua_svg = MwHtml.is_lua_svg
local wrap_html_object = MwHtml.wrap_html_object
local wrap_svg_object = MwHtml.wrap_svg_object

-- ==========================================
-- 核心轉譯引擎:雙向資料與型態映射
-- ==========================================
py2lua = function(val)
    if type(val) == T_TABLE then
        if val.__is_py_proxy and val.__py_obj ~= nil then
            val = val.__py_obj
        end
        local priv = mw_private[val]
        if priv then
            return priv.lua_frame or priv.lua_title or priv.lua_html or priv.lua_svg or priv.lua_obj
        end
        if val.__class and val.__class.__name == "NoneType" then
            return nil
        elseif val.__type == "LuaInstance" or (val.__class and val.__class.__name == "LuaInstance") then
            return val.__dict._lua_obj
        elseif val.__type == "bytes" or val.__type == "bytearray" or val.__type == "memoryview" then
            return val._data
        elseif val.__type == "int" and val.is_big then
            local MathCore = require("Module:PythonScript/Runtime/Cmath")
            return tonumber(MathCore.BigInt.to_string(val.sign, val.limbs))
        elseif val.__type == "dict" then
            local t = {}
            for _, p in ipairs(val.pairs) do t[py2lua(p.key)] = py2lua(p.value) end
            return t
        elseif val.__type == "list" or val.__type == "tuple" or val.__type == "set" then
            local t = {}
            for i, v in ipairs(val.elements) do t[i] = py2lua(v) end
            return t
        elseif val.__type == "function" or val.__type == "bound_method" or val.__type == "builtin_method" then
            return function(...)
                local Evaluator = require("Module:PythonScript/Evaluator")
                local args = {...}
                local py_args = {}
                for _, a in ipairs(args) do table.insert(py_args, lua2py(a)) end
                local success, res = pcall(Evaluator.invoke, val, py_args, {})
                if not success then error(res, 0) end
                return py2lua(res)
            end
        end
    end
    return val
end

lua2py = function(val)
    if val == nil then return B.None end
    if type(val) == "number" then
        if math.floor(val) == val and math.abs(val) > 9007199254740990 then
            return B.int.__call({val})
        end
        return val
    end

    if type(val) == T_TABLE then
        if val.__is_py_proxy and val.__py_obj ~= nil then
            return val.__py_obj
        end
        if val.__type then return val end
        if is_lua_frame(val) then return wrap_frame_object(val) end
        if val.__type then return val end
        if is_lua_frame(val) then return wrap_frame_object(val) end
        if is_lua_title(val) then return wrap_title_object(val) end
        if is_lua_html(val) then return wrap_html_object(val) end
        if is_lua_svg(val) then return wrap_svg_object(val) end

        local has_func = false
        local max_k, count, is_array = 0, 0, true
        for k, v in pairs(val) do
            if type(v) == T_FUNC then has_func = true end
            if type(k) ~= "number" or k < 1 or math.floor(k) ~= k then is_array = false end
            if type(k) == "number" and k > max_k then max_k = k end
            count = count + 1
        end
        if has_func or getmetatable(val) then
            return wrap_lua_instance(val)
        end
        if is_array and max_k == count and count > 0 then
            local elts = {}
            for i = 1, max_k do table.insert(elts, lua2py(val[i])) end
            return {__type="list", elements=elts}
        else
            local kv_pairs = {}
            for k, v in pairs(val) do table.insert(kv_pairs, {key=lua2py(k), value=lua2py(v)}) end
            return {__type="dict", pairs=kv_pairs}
        end
    end
    return val
end

-- 單向注入共用參照至 Mw/Html 子模組,徹底打破循環依賴
MwHtml.bind({
    mw_private = mw_private,
    py2lua = py2lua,
    is_none = is_none,
    check_no_kwargs = check_no_kwargs
})

-- ==========================================
-- 迭代器包裝器與通用 LuaInstance 代理
-- ==========================================
make_py_iterator = function(lua_iter)
    local dict = {
        __iter__ = { __type = "builtin_method", __call = function(args) return args[1] end },
        __next__ = {
            __type = "builtin_method",
            __call = function(args)
                local res = { args[1].__dict._lua_iter() }
                if res[1] == nil then Runtime.raise("StopIteration") end
                if #res == 1 then return lua2py(res[1]) end
                local tup = {}
                for i = 1, #res do table.insert(tup, lua2py(res[i])) end
                return { __type = "tuple", elements = tup }
            end
        }
    }
    local inst = PyClass.make_instance(PyClass.make_class("LuaIterator", {}, dict))
    inst.__dict._lua_iter = lua_iter
    return inst
end

local LuaInstanceClass = PyClass.make_class("LuaInstance", {}, {
    __getattr__ = {
        __type = "builtin_method",
        __call = function(args)
            local self, attr = args[1], args[2]
            local lua_obj = self.__dict._lua_obj
            local val = lua_obj[attr]
            if type(val) == T_FUNC then
                return {
                    __type = "builtin_method",
                    __call = function(m_args, m_kwargs)
                        local lua_args = { lua_obj }
                        for i = 1, #m_args do table.insert(lua_args, py2lua(m_args[i])) end
                        if m_kwargs and next(m_kwargs) then
                            local kw = {}
                            for key, kw_val in pairs(m_kwargs) do kw[key] = py2lua(kw_val) end
                            table.insert(lua_args, kw)
                        end
                        local results = { pcall(val, unpack(lua_args)) }
                        if not results[1] then error(results[2], 0) end
                        table.remove(results, 1)
                        if #results == 0 then return B.None end
                        if #results == 1 then return lua2py(results[1]) end
                        local tup = {}
                        for _, r in ipairs(results) do table.insert(tup, lua2py(r)) end
                        return {__type="tuple", elements=tup}
                    end
                }
            end
            return lua2py(val)
        end
    },
    __getitem__ = {
        __type = "builtin_method",
        __call = function(args) return lua2py(args[1].__dict._lua_obj[py2lua(args[2])]) end
    },
    __iter__ = {
        __type = "builtin_method",
        __call = function(args)
            local lua_obj = args[1].__dict._lua_obj
            local k
            return make_py_iterator(function()
                k = next(lua_obj, k)
                return k
            end)
        end
    },
    __str__ = {
        __type = "builtin_method",
        __call = function(args)
            local success, res = pcall(tostring, args[1].__dict._lua_obj)
            return success and res or "<LuaInstance>"
        end
    }
})

wrap_lua_instance = function(lua_obj)
    if type(lua_obj) ~= T_TABLE then return lua2py(lua_obj) end
    if is_lua_frame(lua_obj) then return wrap_frame_object(lua_obj) end
    if is_lua_title(lua_obj) then return wrap_title_object(lua_obj) end
    if is_lua_html(lua_obj) then return wrap_html_object(lua_obj) end
    if is_lua_svg(lua_obj) then return wrap_svg_object(lua_obj) end
    local inst = PyClass.make_instance(LuaInstanceClass)
    inst.__dict._lua_obj = lua_obj
    return inst
end

-- ==========================================
-- 特化一:TitleObject、TitleBatchObject 與 TitleFileMetadata
-- ==========================================
local TitleObjectClass, TitleBatchObjectClass, TitleFileMetadataClass

local function get_title_priv(obj, caller)
    local priv = type(obj) == T_TABLE and mw_private[obj] or nil
    if not priv or priv.kind ~= "title" then
        Runtime.raise("TypeError", (caller or "TitleObject") .. " requires a valid 'TitleObject' instance")
    end
    return priv
end

local function extract_title_str(v)
    if type(v) == T_TABLE and mw_private[v] and mw_private[v].kind == "title" then
        return mw_private[v].lua_title.fullText or mw_private[v].lua_title.prefixedText
    end
    return RUtils.to_str(v, false)
end

local TITLE_SCALAR_PROPS = {
    id = true, interwiki = true, namespace = true, fragment = true,
    nsText = true, subjectNsText = true, talkNsText = true,
    text = true, prefixedText = true, fullText = true,
    rootText = true, baseText = true, subpageText = true,
    canTalk = true, exists = true, fileExists = true,
    isContentPage = true, isExternal = true, isLocal = true,
    isRedirect = true, isSpecialPage = true, isSubpage = true,
    isTalkPage = true, contentModel = true, content = true,
    isDisambiguationPage = true
}

local TITLE_OBJ_PROPS = {
    basePageTitle = true, rootPageTitle = true,
    talkPageTitle = true, subjectPageTitle = true
}

local function wrap_title_file_metadata(lua_file)
    if not lua_file then return B.None end
    if not TitleFileMetadataClass then
        local tf_dict = {
            __new__ = {
                __type = "staticmethod",
                __func = {
                    __type = "builtin_method", __name = "__new__",
                    __call = function()
                        Runtime.raise("TypeError", "cannot create 'TitleFileMetadata' instances directly")
                    end
                }
            },
            __getattr__ = {
                __type = "builtin_method", __name = "__getattr__",
                __call = function(args)
                    local priv = mw_private[args[1]]
                    if not priv or priv.kind ~= "title_file" then
                        Runtime.raise("TypeError", "invalid TitleFileMetadata instance")
                    end
                    local k = args[2]
                    local val = priv.lua_file[k]
                    if val == nil then return B.None end
                    return lua2py(val)
                end
            },
            __getitem__ = {
                __type = "builtin_method", __name = "__getitem__",
                __call = function(args)
                    local priv = mw_private[args[1]]
                    local val = priv and priv.lua_file[py2lua(args[2])]
                    if val == nil then return B.None end
                    return lua2py(val)
                end
            },
            __setattr__ = {
                __type = "builtin_method", __name = "__setattr__",
                __call = function()
                    Runtime.raise("AttributeError", "'TitleFileMetadata' object attributes are read-only")
                end
            },
            __repr__ = {
                __type = "builtin_method", __name = "__repr__",
                __call = function(args)
                    local priv = mw_private[args[1]]
                    local lf = priv and priv.lua_file
                    if not lf then return "<TitleFileMetadata>" end
                    if not lf.exists then return "<TitleFileMetadata exists=False>" end
                    return string.format("<TitleFileMetadata exists=True size=%s mimeType=%s width=%s height=%s>",
                        tostring(lf.size), RUtils.to_str(lf.mimeType, true), tostring(lf.width), tostring(lf.height))
                end
            }
        }
        tf_dict.__str__ = tf_dict.__repr__
        TitleFileMetadataClass = PyClass.make_class("TitleFileMetadata", { B.object }, tf_dict)
    end
    local inst = PyClass.make_instance(TitleFileMetadataClass)
    mw_private[inst] = { kind = "title_file", lua_file = lua_file }
    return inst
end

local function ensure_title_class()
    if TitleObjectClass then return TitleObjectClass end

    local function coerce_url_query(q)
        if is_none(q) then return nil end
        if type(q) == "string" then return q end
        if type(q) == T_TABLE and q.__type == "dict" then
            local t = {}
            for _, p in ipairs(q.pairs) do
                t[RUtils.to_str(p.key, false)] = RUtils.to_str(p.value, false)
            end
            return t
        end
        return py2lua(q)
    end

    local title_dict = {
        __new__ = {
            __type = "staticmethod",
            __func = {
                __type = "builtin_method", __name = "__new__",
                __call = function()
                    Runtime.raise("TypeError", "cannot create 'TitleObject' instances directly; use mw.title.new(), mw.title.makeTitle(), or mw.title.getCurrentTitle()")
                end
            }
        },
        isSubpageOf = {
            __type = "builtin_method", __name = "isSubpageOf",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "isSubpageOf")
                if #args ~= 2 then
                    Runtime.raise("TypeError", "isSubpageOf() takes exactly 1 argument (" .. (#args - 1) .. " given)")
                end
                local priv = get_title_priv(args[1], "isSubpageOf")
                local t2 = args[2]
                if type(t2) == T_TABLE and mw_private[t2] and mw_private[t2].kind == "title" then
                    t2 = mw_private[t2].lua_title
                elseif type(t2) == "string" and mw.title and mw.title.new then
                    t2 = mw.title.new(t2) or t2
                end
                return priv.lua_title:isSubpageOf(t2) == true
            end
        },
        inNamespace = {
            __type = "builtin_method", __name = "inNamespace",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "inNamespace")
                if #args ~= 2 then
                    Runtime.raise("TypeError", "inNamespace() takes exactly 1 argument (" .. (#args - 1) .. " given)")
                end
                local priv = get_title_priv(args[1], "inNamespace")
                return priv.lua_title:inNamespace(py2lua(args[2])) == true
            end
        },
        inNamespaces = {
            __type = "builtin_method", __name = "inNamespaces",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "inNamespaces")
                local priv = get_title_priv(args[1], "inNamespaces")
                local ns_list = {}
                for i = 2, #args do ns_list[#ns_list + 1] = py2lua(args[i]) end
                return priv.lua_title:inNamespaces(unpack(ns_list)) == true
            end
        },
        hasSubjectNamespace = {
            __type = "builtin_method", __name = "hasSubjectNamespace",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "hasSubjectNamespace")
                if #args ~= 2 then
                    Runtime.raise("TypeError", "hasSubjectNamespace() takes exactly 1 argument (" .. (#args - 1) .. " given)")
                end
                local priv = get_title_priv(args[1], "hasSubjectNamespace")
                return priv.lua_title:hasSubjectNamespace(py2lua(args[2])) == true
            end
        },
        subPageTitle = {
            __type = "builtin_method", __name = "subPageTitle",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "subPageTitle")
                if #args ~= 2 then
                    Runtime.raise("TypeError", "subPageTitle() takes exactly 1 argument (" .. (#args - 1) .. " given)")
                end
                local priv = get_title_priv(args[1], "subPageTitle")
                local sub = priv.lua_title:subPageTitle(RUtils.to_str(args[2], false))
                return sub and wrap_title_object(sub) or B.None
            end
        },
        partialUrl = {
            __type = "builtin_method", __name = "partialUrl",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "partialUrl")
                local priv = get_title_priv(args[1], "partialUrl")
                return priv.lua_title:partialUrl()
            end
        },
        fullUrl = {
            __type = "builtin_method", __name = "fullUrl",
            __call = function(args, kwargs)
                local priv = get_title_priv(args[1], "fullUrl")
                local q = coerce_url_query(args[2] or (kwargs and kwargs.query))
                local proto = args[3] or (kwargs and kwargs.proto)
                proto = not is_none(proto) and RUtils.to_str(proto, false) or nil
                return priv.lua_title:fullUrl(q, proto)
            end
        },
        localUrl = {
            __type = "builtin_method", __name = "localUrl",
            __call = function(args, kwargs)
                local priv = get_title_priv(args[1], "localUrl")
                local q = coerce_url_query(args[2] or (kwargs and kwargs.query))
                return priv.lua_title:localUrl(q)
            end
        },
        canonicalUrl = {
            __type = "builtin_method", __name = "canonicalUrl",
            __call = function(args, kwargs)
                local priv = get_title_priv(args[1], "canonicalUrl")
                local q = coerce_url_query(args[2] or (kwargs and kwargs.query))
                return priv.lua_title:canonicalUrl(q)
            end
        },
        getContent = {
            __type = "builtin_method", __name = "getContent",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "getContent")
                local priv = get_title_priv(args[1], "getContent")
                local c = priv.lua_title:getContent()
                return c ~= nil and c or B.None
            end
        },
        __getattr__ = {
            __type = "builtin_method", __name = "__getattr__",
            __call = function(args)
                local self, attr = args[1], args[2]
                local priv = get_title_priv(self, "__getattr__")
                local lt = priv.lua_title

                if TITLE_SCALAR_PROPS[attr] then
                    local v = lt[attr]
                    if v == nil then return B.None end
                    return v
                elseif TITLE_OBJ_PROPS[attr] then
                    local t_sub = lt[attr]
                    if not t_sub then return B.None end
                    return wrap_title_object(t_sub)
                elseif attr == "redirectTarget" then
                    local rt = lt.redirectTarget
                    if rt == false or rt == nil then return false end
                    return wrap_title_object(rt)
                elseif attr == "file" then
                    if priv.cached_file == nil then
                        priv.cached_file = wrap_title_file_metadata(lt.file)
                    end
                    return priv.cached_file
                elseif attr == "protectionLevels" or attr == "cascadingProtection" or attr == "categories" or attr == "visibleCategories" or attr == "pageAssessments" or attr == "pageLang" then
                    local v = lt[attr]
                    if v == nil then return B.None end
                    return lua2py(v)
                end
                Runtime.raise("AttributeError", "'TitleObject' object has no attribute '" .. tostring(attr) .. "'")
            end
        },
        __setattr__ = {
            __type = "builtin_method", __name = "__setattr__",
            __call = function(args)
                local self, attr, val = args[1], args[2], args[3]
                local priv = get_title_priv(self, "__setattr__")
                if attr == "fragment" then
                    priv.lua_title.fragment = is_none(val) and "" or RUtils.to_str(val, false)
                    priv.cached_repr = nil
                    return B.None
                end
                Runtime.raise("AttributeError", "'TitleObject' attribute '" .. tostring(attr) .. "' is read-only")
            end
        },
        __eq__ = {
            __type = "builtin_method", __name = "__eq__",
            __call = function(args)
                local p1 = mw_private[args[1]]
                local p2 = type(args[2]) == T_TABLE and mw_private[args[2]] or nil
                if p1 and p1.kind == "title" and p2 and p2.kind == "title" then
                    if mw.title and mw.title.equals then
                        return mw.title.equals(p1.lua_title, p2.lua_title)
                    end
                    return p1.lua_title == p2.lua_title
                end
                return B.NotImplemented
            end
        },
        __ne__ = {
            __type = "builtin_method", __name = "__ne__",
            __call = function(args)
                local p1 = mw_private[args[1]]
                local p2 = type(args[2]) == T_TABLE and mw_private[args[2]] or nil
                if p1 and p1.kind == "title" and p2 and p2.kind == "title" then
                    if mw.title and mw.title.equals then
                        return not mw.title.equals(p1.lua_title, p2.lua_title)
                    end
                    return p1.lua_title ~= p2.lua_title
                end
                return B.NotImplemented
            end
        },
        __lt__ = {
            __type = "builtin_method", __name = "__lt__",
            __call = function(args)
                local p1 = get_title_priv(args[1], "__lt__")
                local p2 = type(args[2]) == T_TABLE and mw_private[args[2]] or nil
                if not p2 or p2.kind ~= "title" then return B.NotImplemented end
                return mw.title.compare(p1.lua_title, p2.lua_title) < 0
            end
        },
        __le__ = {
            __type = "builtin_method", __name = "__le__",
            __call = function(args)
                local p1 = get_title_priv(args[1], "__le__")
                local p2 = type(args[2]) == T_TABLE and mw_private[args[2]] or nil
                if not p2 or p2.kind ~= "title" then return B.NotImplemented end
                return mw.title.compare(p1.lua_title, p2.lua_title) <= 0
            end
        },
        __gt__ = {
            __type = "builtin_method", __name = "__gt__",
            __call = function(args)
                local p1 = get_title_priv(args[1], "__gt__")
                local p2 = type(args[2]) == T_TABLE and mw_private[args[2]] or nil
                if not p2 or p2.kind ~= "title" then return B.NotImplemented end
                return mw.title.compare(p1.lua_title, p2.lua_title) > 0
            end
        },
        __ge__ = {
            __type = "builtin_method", __name = "__ge__",
            __call = function(args)
                local p1 = get_title_priv(args[1], "__ge__")
                local p2 = type(args[2]) == T_TABLE and mw_private[args[2]] or nil
                if not p2 or p2.kind ~= "title" then return B.NotImplemented end
                return mw.title.compare(p1.lua_title, p2.lua_title) >= 0
            end
        },
        __hash__ = {
            __type = "builtin_method", __name = "__hash__",
            __call = function(args)
                local priv = get_title_priv(args[1], "__hash__")
                return RUtils.hash_impl({ priv.lua_title.prefixedText or "" })
            end
        },
        __repr__ = {
            __type = "builtin_method", __name = "__repr__",
            __call = function(args)
                local priv = get_title_priv(args[1], "__repr__")
                if not priv.cached_repr then
                    local ft = priv.lua_title.fullText or priv.lua_title.prefixedText or tostring(priv.lua_title)
                    priv.cached_repr = string.format("<TitleObject title='%s'>", tostring(ft))
                end
                return priv.cached_repr
            end
        }
    }
    title_dict.__str__ = title_dict.__repr__
    TitleObjectClass = PyClass.make_class("TitleObject", { B.object }, title_dict)
    return TitleObjectClass
end

wrap_title_object = function(lua_title)
    if lua_title == nil then return B.None end
    if type(lua_title) == T_TABLE and mw_private[lua_title] and mw_private[lua_title].kind == "title" then
        return lua_title
    end
    local inst = PyClass.make_instance(ensure_title_class())
    mw_private[inst] = {
        kind = "title",
        lua_title = lua_title,
        cached_repr = nil,
        cached_file = nil
    }
    return inst
end

local function wrap_title_batch_object(lua_batch)
    if not TitleBatchObjectClass then
        local tb_dict = {
            __new__ = {
                __type = "staticmethod",
                __func = {
                    __type = "builtin_method", __name = "__new__",
                    __call = function()
                        Runtime.raise("TypeError", "cannot create 'TitleBatchObject' instances directly; use mw.title.newBatch()")
                    end
                }
            },
            lookupExistence = {
                __type = "builtin_method", __name = "lookupExistence",
                __call = function(args, kwargs)
                    check_no_kwargs(kwargs, "lookupExistence")
                    local priv = mw_private[args[1]]
                    if not priv or priv.kind ~= "title_batch" then
                        Runtime.raise("TypeError", "invalid TitleBatchObject instance")
                    end
                    priv.lua_batch:lookupExistence()
                    return args[1]
                end
            },
            getTitles = {
                __type = "builtin_method", __name = "getTitles",
                __call = function(args, kwargs)
                    check_no_kwargs(kwargs, "getTitles")
                    local priv = mw_private[args[1]]
                    if not priv or priv.kind ~= "title_batch" then
                        Runtime.raise("TypeError", "invalid TitleBatchObject instance")
                    end
                    local raw_titles = priv.lua_batch:getTitles() or {}
                    local max_i = #raw_titles
                    for k, _ in pairs(raw_titles) do
                        if type(k) == "number" and k > max_i then max_i = k end
                    end
                    local out = {}
                    for i = 1, max_i do
                        out[i] = raw_titles[i] and wrap_title_object(raw_titles[i]) or B.None
                    end
                    return { __type = "list", elements = out }
                end
            },
            __repr__ = {
                __type = "builtin_method", __name = "__repr__",
                __call = function() return "<TitleBatchObject>" end
            }
        }
        tb_dict.__str__ = tb_dict.__repr__
        TitleBatchObjectClass = PyClass.make_class("TitleBatchObject", { B.object }, tb_dict)
    end
    local inst = PyClass.make_instance(TitleBatchObjectClass)
    mw_private[inst] = { kind = "title_batch", lua_batch = lua_batch }
    return inst
end

-- ==========================================
-- 特化二:FrameObject 與 FrameArgumentType
-- ==========================================
local FrameObjectClass, FrameArgumentTypeClass

local function wrap_frame_argument(lua_arg_obj)
    if lua_arg_obj == nil then return B.None end
    if not FrameArgumentTypeClass then
        local fa_dict = {
            __new__ = {
                __type = "staticmethod",
                __func = {
                    __type = "builtin_method", __name = "__new__",
                    __call = function()
                        Runtime.raise("TypeError", "cannot create 'FrameArgumentType' instances directly")
                    end
                }
            },
            expand = {
                __type = "builtin_method", __name = "expand",
                __call = function(args, kwargs)
                    check_no_kwargs(kwargs, "expand")
                    if #args ~= 1 then
                        Runtime.raise("TypeError", "expand() takes 0 positional arguments (" .. (#args - 1) .. " given)")
                    end
                    local priv = mw_private[args[1]]
                    if not priv or priv.kind ~= "frame_arg" then
                        Runtime.raise("TypeError", "invalid FrameArgumentType instance")
                    end
                    local res = priv.lua_obj:expand()
                    return res ~= nil and res or B.None
                end
            },
            __repr__ = {
                __type = "builtin_method", __name = "__repr__",
                __call = function() return "<FrameArgumentType>" end
            }
        }
        fa_dict.__str__ = fa_dict.__repr__
        FrameArgumentTypeClass = PyClass.make_class("FrameArgumentType", { B.object }, fa_dict)
    end
    local inst = PyClass.make_instance(FrameArgumentTypeClass)
    mw_private[inst] = { kind = "frame_arg", lua_obj = lua_arg_obj }
    return inst
end

local function get_frame_priv(obj, caller)
    local priv = type(obj) == T_TABLE and mw_private[obj] or nil
    if not priv or priv.kind ~= "frame" then
        Runtime.raise("TypeError", (caller or "FrameObject") .. " requires a valid 'FrameObject' instance")
    end
    return priv
end

local function parse_title_and_args(args, kwargs, func_name, default_title)
    kwargs = kwargs or {}
    if #args == 2 and type(args[2]) == T_TABLE and args[2].__type == "dict" and next(kwargs) == nil then
        local d_lua = py2lua(args[2])
        if d_lua.title ~= nil or d_lua.args ~= nil then
            local t_str = d_lua.title ~= nil and extract_title_str(d_lua.title) or default_title
            local a_tbl = {}
            if type(d_lua.args) == T_TABLE then
                for k, v in pairs(d_lua.args) do a_tbl[k] = RUtils.to_str(v, false) end
            end
            return t_str, a_tbl
        end
    end

    local title_val = kwargs.title
    local has_kw_title = not is_none(title_val)
    local explicit_args_tbl = kwargs.args
    local out_args = {}

    if not is_none(explicit_args_tbl) and type(explicit_args_tbl) == T_TABLE then
        if explicit_args_tbl.__type == "dict" then
            for _, p in ipairs(explicit_args_tbl.pairs) do
                out_args[py2lua(p.key)] = RUtils.to_str(p.value, false)
            end
        elseif explicit_args_tbl.__type == "list" or explicit_args_tbl.__type == "tuple" then
            for i, v in ipairs(explicit_args_tbl.elements) do
                out_args[i] = RUtils.to_str(v, false)
            end
        end
    end

    if has_kw_title then
        title_val = extract_title_str(title_val)
        for i = 2, #args do
            out_args[i - 1] = RUtils.to_str(args[i], false)
        end
    else
        if #args >= 2 and not is_none(args[2]) then
            title_val = extract_title_str(args[2])
        else
            title_val = default_title
        end
        if #args == 3 and type(args[3]) == T_TABLE and (args[3].__type == "dict" or args[3].__type == "list" or args[3].__type == "tuple") and is_none(explicit_args_tbl) then
            if args[3].__type == "dict" then
                for _, p in ipairs(args[3].pairs) do
                    out_args[py2lua(p.key)] = RUtils.to_str(p.value, false)
                end
            else
                for i, v in ipairs(args[3].elements) do
                    out_args[i] = RUtils.to_str(v, false)
                end
            end
        else
            for i = 3, #args do
                out_args[i - 2] = RUtils.to_str(args[i], false)
            end
        end
    end

    for k, v in pairs(kwargs) do
        if k ~= "title" and (k ~= "args" or type(explicit_args_tbl) ~= T_TABLE) then
            out_args[k] = RUtils.to_str(v, false)
        end
    end

    return title_val, out_args
end

local function ensure_frame_class()
    if FrameObjectClass then return FrameObjectClass end

    local frame_dict = {
        __new__ = {
            __type = "staticmethod",
            __func = {
                __type = "builtin_method", __name = "__new__",
                __call = function()
                    Runtime.raise("TypeError", "cannot create 'FrameObject' instances directly; use mw.getCurrentFrame() or frame.newChild()")
                end
            }
        },
        args = {
            __type = "property",
            fget = {
                __type = "builtin_method", __name = "args",
                __call = function(args)
                    local priv = get_frame_priv(args[1], "args")
                    if not priv.cached_args then
                        local kv_pairs = {}
                        for k, v in pairs(priv.lua_frame.args or {}) do
                            table.insert(kv_pairs, { key = lua2py(k), value = lua2py(v) })
                        end
                        priv.cached_args = { __type = "dict", pairs = kv_pairs }
                    end
                    return priv.cached_args
                end
            }
        },
        callParserFunction = {
            __type = "builtin_method", __name = "callParserFunction",
            __call = function(args, kwargs)
                local priv = get_frame_priv(args[1], "callParserFunction")
                kwargs = kwargs or {}

                if #args == 2 and type(args[2]) == T_TABLE and args[2].__type == "dict" and next(kwargs) == nil then
                    local d_lua = py2lua(args[2])
                    if d_lua.name ~= nil then
                        local fn_name = RUtils.to_str(d_lua.name, false)
                        local raw_a = d_lua.args
                        if type(raw_a) == T_TABLE then
                            return priv.lua_frame:callParserFunction{ name = fn_name, args = raw_a }
                        elseif raw_a ~= nil then
                            return priv.lua_frame:callParserFunction(fn_name, RUtils.to_str(raw_a, false))
                        else
                            return priv.lua_frame:callParserFunction(fn_name)
                        end
                    end
                end

                local fn_name = args[2] or kwargs.name
                if is_none(fn_name) then
                    Runtime.raise("TypeError", "callParserFunction() missing required argument 'name'")
                end
                fn_name = RUtils.to_str(fn_name, false)

                local pos_args = {}
                local named_args = {}
                local has_named = false

                if #args == 3 and type(args[3]) == T_TABLE and (args[3].__type == "list" or args[3].__type == "tuple" or args[3].__type == "dict") and next(kwargs) == nil then
                    if args[3].__type == "dict" then
                        for _, p in ipairs(args[3].pairs) do
                            local k = py2lua(p.key)
                            local v = RUtils.to_str(p.value, false)
                            if type(k) == "number" then pos_args[k] = v
                            else named_args[tostring(k)] = v; has_named = true end
                        end
                    else
                        for i, v in ipairs(args[3].elements) do
                            pos_args[i] = RUtils.to_str(v, false)
                        end
                    end
                else
                    for i = 3, #args do
                        table.insert(pos_args, RUtils.to_str(args[i], false))
                    end
                end

                for k, v in pairs(kwargs) do
                    if k ~= "name" or args[2] ~= nil then
                        if k == "args" and type(v) == T_TABLE and #pos_args == 0 then
                            if v.__type == "dict" then
                                for _, p in ipairs(v.pairs) do
                                    named_args[RUtils.to_str(p.key, false)] = RUtils.to_str(p.value, false)
                                    has_named = true
                                end
                            elseif v.__type == "list" or v.__type == "tuple" then
                                for i, el in ipairs(v.elements) do pos_args[i] = RUtils.to_str(el, false) end
                            end
                        else
                            named_args[tostring(k)] = RUtils.to_str(v, false)
                            has_named = true
                        end
                    end
                end

                if not has_named then
                    return priv.lua_frame:callParserFunction(fn_name, unpack(pos_args))
                else
                    local combined = {}
                    for i, v in ipairs(pos_args) do combined[i] = v end
                    for k, v in pairs(named_args) do combined[k] = v end
                    return priv.lua_frame:callParserFunction(fn_name, combined)
                end
            end
        },
        expandTemplate = {
            __type = "builtin_method", __name = "expandTemplate",
            __call = function(args, kwargs)
                local priv = get_frame_priv(args[1], "expandTemplate")
                local t_str, a_tbl = parse_title_and_args(args, kwargs, "expandTemplate", nil)
                if not t_str then
                    Runtime.raise("TypeError", "expandTemplate() missing required argument 'title'")
                end
                return priv.lua_frame:expandTemplate{ title = t_str, args = a_tbl }
            end
        },
        extensionTag = {
            __type = "builtin_method", __name = "extensionTag",
            __call = function(args, kwargs)
                local priv = get_frame_priv(args[1], "extensionTag")
                kwargs = kwargs or {}
                if #args == 2 and type(args[2]) == T_TABLE and args[2].__type == "dict" and next(kwargs) == nil then
                    local d_lua = py2lua(args[2])
                    return priv.lua_frame:extensionTag{
                        name = RUtils.to_str(d_lua.name or "", false),
                        content = d_lua.content ~= nil and RUtils.to_str(d_lua.content, false) or nil,
                        args = d_lua.args
                    }
                end
                local name = args[2] or kwargs.name
                if is_none(name) then
                    Runtime.raise("TypeError", "extensionTag() missing required argument 'name'")
                end
                local content = args[3] ~= nil and args[3] or kwargs.content
                local ext_args = args[4] ~= nil and args[4] or kwargs.args
                local lua_ext_args = nil
                if not is_none(ext_args) then
                    lua_ext_args = py2lua(ext_args)
                else
                    local extra_kw = {}
                    for k, v in pairs(kwargs) do
                        if k ~= "name" and k ~= "content" and k ~= "args" then
                            extra_kw[k] = RUtils.to_str(v, false)
                        end
                    end
                    if next(extra_kw) ~= nil then lua_ext_args = extra_kw end
                end
                return priv.lua_frame:extensionTag{
                    name = RUtils.to_str(name, false),
                    content = not is_none(content) and RUtils.to_str(content, false) or nil,
                    args = lua_ext_args
                }
            end
        },
        getParent = {
            __type = "builtin_method", __name = "getParent",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "getParent")
                local priv = get_frame_priv(args[1], "getParent")
                local parent = priv.lua_frame:getParent()
                if not parent then return B.None end
                return wrap_frame_object(parent)
            end
        },
        getTitle = {
            __type = "builtin_method", __name = "getTitle",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "getTitle")
                local priv = get_frame_priv(args[1], "getTitle")
                local raw_t = priv.lua_frame:getTitle()
                if is_lua_title(raw_t) then return wrap_title_object(raw_t) end
                return raw_t
            end
        },
        newChild = {
            __type = "builtin_method", __name = "newChild",
            __call = function(args, kwargs)
                local priv = get_frame_priv(args[1], "newChild")
                local t_str, a_tbl = parse_title_and_args(args, kwargs, "newChild", priv.lua_frame:getTitle())
                local child = priv.lua_frame:newChild{ title = t_str, args = a_tbl }
                return wrap_frame_object(child)
            end
        },
        preprocess = {
            __type = "builtin_method", __name = "preprocess",
            __call = function(args, kwargs)
                local priv = get_frame_priv(args[1], "preprocess")
                local text = args[2] or (kwargs and kwargs.text)
                if type(text) == T_TABLE and text.__type == "dict" then
                    text = py2lua(text).text
                end
                if is_none(text) then
                    Runtime.raise("TypeError", "preprocess() missing required argument 'text'")
                end
                return priv.lua_frame:preprocess(RUtils.to_str(text, false))
            end
        },
        getArgument = {
            __type = "builtin_method", __name = "getArgument",
            __call = function(args, kwargs)
                local priv = get_frame_priv(args[1], "getArgument")
                local arg_key = args[2] or (kwargs and kwargs.name)
                if type(arg_key) == T_TABLE and arg_key.__type == "dict" then
                    arg_key = py2lua(arg_key).name
                end
                if is_none(arg_key) then
                    Runtime.raise("TypeError", "getArgument() missing required argument 'name'")
                end
                local res = priv.lua_frame:getArgument(py2lua(arg_key))
                return wrap_frame_argument(res)
            end
        },
        newParserValue = {
            __type = "builtin_method", __name = "newParserValue",
            __call = function(args, kwargs)
                local priv = get_frame_priv(args[1], "newParserValue")
                local text = args[2] or (kwargs and kwargs.text)
                if type(text) == T_TABLE and text.__type == "dict" then
                    text = py2lua(text).text
                end
                if is_none(text) then
                    Runtime.raise("TypeError", "newParserValue() missing required argument 'text'")
                end
                local res = priv.lua_frame:newParserValue(RUtils.to_str(text, false))
                return wrap_frame_argument(res)
            end
        },
        newTemplateParserValue = {
            __type = "builtin_method", __name = "newTemplateParserValue",
            __call = function(args, kwargs)
                local priv = get_frame_priv(args[1], "newTemplateParserValue")
                local t_str, a_tbl = parse_title_and_args(args, kwargs, "newTemplateParserValue", nil)
                if not t_str then
                    Runtime.raise("TypeError", "newTemplateParserValue() missing required argument 'title'")
                end
                local res = priv.lua_frame:newTemplateParserValue{ title = t_str, args = a_tbl }
                return wrap_frame_argument(res)
            end
        },
        argumentPairs = {
            __type = "builtin_method", __name = "argumentPairs",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "argumentPairs")
                local priv = get_frame_priv(args[1], "argumentPairs")
                local iter_fn, state, var = priv.lua_frame:argumentPairs()
                return make_py_iterator(function()
                    local k, v = iter_fn(state, var)
                    var = k
                    if k == nil then return nil end
                    return k, v
                end)
            end
        },
        __setattr__ = {
            __type = "builtin_method", __name = "__setattr__",
            __call = function()
                Runtime.raise("AttributeError", "'FrameObject' object attributes are read-only")
            end
        },
        __repr__ = {
            __type = "builtin_method", __name = "__repr__",
            __call = function(args)
                local priv = get_frame_priv(args[1], "__repr__")
                if not priv.cached_title then
                    local raw_t = priv.lua_frame:getTitle()
                    local full_text = tostring(raw_t or "")
                    if is_lua_title(raw_t) then
                        full_text = raw_t.fullText or raw_t.prefixedText or full_text
                    elseif mw.title and mw.title.new and full_text ~= "" then
                        local ok, t_obj = pcall(mw.title.new, full_text)
                        if ok and t_obj and t_obj.fullText then
                            full_text = t_obj.fullText
                        end
                    end
                    priv.cached_title = string.format('<FrameObject title="%s">', full_text)
                end
                return priv.cached_title
            end
        }
    }
    frame_dict.__str__ = frame_dict.__repr__
    FrameObjectClass = PyClass.make_class("FrameObject", { B.object }, frame_dict)
    return FrameObjectClass
end

wrap_frame_object = function(lua_frame)
    if lua_frame == nil then return B.None end
    if type(lua_frame) == T_TABLE and mw_private[lua_frame] and mw_private[lua_frame].kind == "frame" then
        return lua_frame
    end
    local inst = PyClass.make_instance(ensure_frame_class())
    mw_private[inst] = {
        kind = "frame",
        lua_frame = lua_frame,
        cached_title = nil,
        cached_args = nil
    }
    return inst
end

-- ==========================================
-- 延遲載入命名空間 (Lazy Namespace)
-- ==========================================
local LazyNamespaceClass = PyClass.make_class("module", {}, {
    __getattr__ = {
        __type = "builtin_method",
        __call = function(args)
            local self, attr = args[1], args[2]
            local raw_tbl = self.__dict._raw_lua_tbl
            local val = raw_tbl[attr]

            if val == nil then
                Runtime.raise("AttributeError", "module '" .. self.__dict._name .. "' has no attribute '" .. attr .. "'")
            end

            local wrapped_val
            if type(val) == T_TABLE and (val.__type == "builtin_method" or val.__type == "class" or val.__type == "instance") then
                wrapped_val = val
            elseif type(val) == T_FUNC then
                wrapped_val = {
                    __type = self.__dict._is_external and "function" or "builtin_method",
                    __name = attr,
                    __qualname__ = attr,
                    __module__ = self.__dict._name,
                    __call = function(py_args, py_kwargs)
                        local lua_args = {}
                        for _, a in ipairs(py_args or {}) do table.insert(lua_args, py2lua(a)) end
                        if py_kwargs and next(py_kwargs) then
                            local kw = {}
                            for key, kw_val in pairs(py_kwargs) do kw[key] = py2lua(kw_val) end
                            table.insert(lua_args, kw)
                        end
                        local results = { pcall(val, unpack(lua_args)) }
                        if not results[1] then error(results[2], 0) end
                        table.remove(results, 1)
                        if #results == 0 then return B.None end
                        if #results == 1 then return lua2py(results[1]) end
                        local tup = {}
                        for _, r in ipairs(results) do table.insert(tup, lua2py(r)) end
                        return {__type="tuple", elements=tup}
                    end
                }
            elseif type(val) == T_TABLE then
                if is_lua_frame(val) then wrapped_val = wrap_frame_object(val)
                elseif is_lua_title(val) then wrapped_val = wrap_title_object(val)
                elseif is_lua_html(val) then wrapped_val = wrap_html_object(val)
                elseif is_lua_svg(val) then wrapped_val = wrap_svg_object(val)
                else wrapped_val = make_lazy_namespace(self.__dict._name .. "." .. attr, val) end
            else
                wrapped_val = lua2py(val)
            end

            self.__dict[attr] = wrapped_val
            return wrapped_val
        end
    }
})

make_lazy_namespace = function(name, tbl, is_external)
    local inst = PyClass.make_instance(LazyNamespaceClass)
    inst.__dict._name = name
    inst.__dict._raw_lua_tbl = tbl
    inst.__dict._is_external = is_external
    return inst
end

-- ==========================================
-- 啟動模組匯出與功能性物件特化註冊
-- ==========================================
function MwAPI.get_module()
    ensure_title_class()
    ensure_frame_class()

    local mw_proxy = {}
    for k, v in pairs(mw) do mw_proxy[k] = v end

    mw_proxy.getCurrentFrame = {
        __type = "builtin_method", __name = "getCurrentFrame",
        __call = function(args, kwargs)
            check_no_kwargs(kwargs, "mw.getCurrentFrame")
            if args and #args > 0 then
                Runtime.raise("TypeError", "mw.getCurrentFrame() takes 0 positional arguments")
            end
            local f = mw.getCurrentFrame()
            return f and wrap_frame_object(f) or B.None
        end
    }

    mw_proxy.log = {
        __type = "builtin_method", __name = "log",
        __call = function(py_args)
            local out = {}
            for _, v in ipairs(py_args or {}) do
                table.insert(out, RUtils.to_str(v, false))
            end
            mw.log(unpack(out))
            return B.None
        end
    }

    local function clean_for_log(val, seen)
        local t = type(val)
        if t == T_TABLE then
            seen = seen or {}
            if seen[val] then
                if val.__type == "list" then return "[...]" end
                if val.__type == "dict" then return "{...}" end
                if val.__type == "set" then return "set(...)" end
                if val.__type == "tuple" then return "(...)" end
                return "<...>"
            end
            seen[val] = true
        end
        if t ~= T_TABLE then return val end
        if val.__type == "list" or val.__type == "tuple" or val.__type == "set" then
            local out = {}
            for i, v in ipairs(val.elements) do out[i] = clean_for_log(v, seen) end
            return out
        elseif val.__type == "dict" then
            local out = {}
            for _, p in ipairs(val.pairs) do
                local k = type(p.key) == T_TABLE and RUtils.to_str(p.key, false) or p.key
                out[k] = clean_for_log(p.value, seen)
            end
            return out
        elseif val.__type == "LuaInstance" then
            return val.__dict._lua_obj
        else
            return RUtils.to_str(val, false)
        end
    end

    mw_proxy.logObject = {
        __type = "builtin_method", __name = "logObject",
        __call = function(py_args)
            if not py_args or #py_args == 0 then return B.None end
            local obj, prefix = py_args[1], py_args[2]
            local clean_obj = clean_for_log(obj)
            if not is_none(prefix) then
                mw.logObject(clean_obj, RUtils.to_str(prefix, false))
            else
                mw.logObject(clean_obj)
            end
            return B.None
        end
    }

    mw_proxy.ustring = {}
    for k, v in pairs(mw.ustring) do mw_proxy.ustring[k] = v end
    mw_proxy.ustring.gmatch = function(s, pattern) return make_py_iterator(mw.ustring.gmatch(s, pattern)) end
    mw_proxy.ustring.gcodepoint = function(s, i, j) return make_py_iterator(mw.ustring.gcodepoint(s, i, j)) end

    mw_proxy.text = {}
    for k, v in pairs(mw.text) do mw_proxy.text[k] = v end
    mw_proxy.text.gsplit = function(s, pattern, plain) return make_py_iterator(mw.text.gsplit(s, pattern, plain)) end

    -- 1. mw.title 庫特化
    if mw.title then
        mw_proxy.title = {
            equals = {
                __type = "builtin_method", __name = "equals",
                __call = function(args, kwargs)
                    check_no_kwargs(kwargs, "mw.title.equals")
                    if #args ~= 2 then
                        Runtime.raise("TypeError", "mw.title.equals() takes exactly 2 arguments")
                    end
                    return mw.title.equals(py2lua(args[1]), py2lua(args[2]))
                end
            },
            compare = {
                __type = "builtin_method", __name = "compare",
                __call = function(args, kwargs)
                    check_no_kwargs(kwargs, "mw.title.compare")
                    if #args ~= 2 then
                        Runtime.raise("TypeError", "mw.title.compare() takes exactly 2 arguments")
                    end
                    return mw.title.compare(py2lua(args[1]), py2lua(args[2]))
                end
            },
            getCurrentTitle = {
                __type = "builtin_method", __name = "getCurrentTitle",
                __call = function(args, kwargs)
                    check_no_kwargs(kwargs, "mw.title.getCurrentTitle")
                    local t = mw.title.getCurrentTitle()
                    return t and wrap_title_object(t) or B.None
                end
            },
            new = {
                __type = "builtin_method", __name = "new",
                __call = function(args, kwargs)
                    local text_or_id = args[1] or (kwargs and (kwargs.text or kwargs.id))
                    local ns = args[2] or (kwargs and kwargs.namespace)
                    if is_none(text_or_id) then return B.None end
                    local ok, t = pcall(mw.title.new, py2lua(text_or_id), not is_none(ns) and py2lua(ns) or nil)
                    if not ok or not t then return B.None end
                    return wrap_title_object(t)
                end
            },
            makeTitle = {
                __type = "builtin_method", __name = "makeTitle",
                __call = function(args, kwargs)
                    local ns = args[1] or (kwargs and kwargs.namespace)
                    local title = args[2] or (kwargs and kwargs.title)
                    local frag = args[3] or (kwargs and kwargs.fragment)
                    local iw = args[4] or (kwargs and kwargs.interwiki)
                    if is_none(ns) or is_none(title) then
                        Runtime.raise("TypeError", "mw.title.makeTitle() requires at least (namespace, title)")
                    end
                    local ok, t = pcall(
                        mw.title.makeTitle,
                        py2lua(ns),
                        RUtils.to_str(title, false),
                        not is_none(frag) and RUtils.to_str(frag, false) or nil,
                        not is_none(iw) and RUtils.to_str(iw, false) or nil
                    )
                    if not ok or not t then return B.None end
                    return wrap_title_object(t)
                end
            },
            newBatch = {
                __type = "builtin_method", __name = "newBatch",
                __call = function(args, kwargs)
                    if not mw.title.newBatch then
                        Runtime.raise("NotImplementedError", "mw.title.newBatch is not available in this MediaWiki version")
                    end
                    local list_arg = args[1] or (kwargs and kwargs.list)
                    local def_ns = args[2] or (kwargs and kwargs.defaultNamespace)
                    if is_none(list_arg) then
                        Runtime.raise("TypeError", "mw.title.newBatch() missing required list of page titles")
                    end
                    local pages = {}
                    local iter_fn = RUtils.get_iterator(list_arg)
                    while true do
                        local hn, item = iter_fn()
                        if not hn then break end
                        if type(item) ~= "string" then
                            Runtime.raise("TypeError", "mw.title.newBatch() list must only contain strings")
                        end
                        table.insert(pages, item)
                    end
                    local b_obj = mw.title.newBatch(pages, not is_none(def_ns) and py2lua(def_ns) or nil)
                    return wrap_title_batch_object(b_obj)
                end
            }
        }
    end

    -- 2. 委派子模組 Module:PythonScript/Mw/Html 掛載 mw.html、mw.svg 與 MarkupBuilderBase 類別家族
    MwHtml.attach_to_mw_proxy(mw_proxy)

    mw_proxy.TitleObject = TitleObjectClass
    mw_proxy.FrameObject = FrameObjectClass

    return make_lazy_namespace("mw", mw_proxy)
end

MwAPI.py2lua = py2lua
MwAPI.lua2py = lua2py
MwAPI.wrap_lua_instance = wrap_lua_instance
MwAPI.make_lazy_namespace = make_lazy_namespace
MwAPI.make_py_iterator = make_py_iterator
MwAPI.wrap_frame_object = wrap_frame_object
MwAPI.wrap_title_object = wrap_title_object
MwAPI.wrap_html_object = wrap_html_object
MwAPI.wrap_svg_object = wrap_svg_object

return MwAPI