跳转到内容

模組:PythonScript

本页使用了标题或全文手工转换
维基百科,自由的百科全书

-- Module:PythonScript
-- 這是 Python 直譯器的對外接口 (Entry Point)
local require_=require;local require=require_("Module:沙盒/a2569875/PythonScript/RequireWrapper")
local p = {}

local Lexer = require("Module:PythonScript/Lexer")
local Parser = require("Module:PythonScript/Parser")
local Evaluator = require("Module:PythonScript/Evaluator")
local Runtime = require("Module:PythonScript/Runtime")
local SysRegistry = require("Module:PythonScript/Librarys/Argparse").Sys

local function add_sys_path(dir_str)
    if type(dir_str) ~= "string" then return end
    SysRegistry.path_registry = SysRegistry.path_registry or {}
    for _, existing in ipairs(SysRegistry.path_registry) do
        if existing == dir_str then return end
    end
    table.insert(SysRegistry.path_registry, dir_str)
end

local function extract_script_dir(path_str)
    if type(path_str) ~= "string" or path_str == "" then return "" end
    local clean = path_str:gsub("%.py$", ""):gsub("/__init__$", "")
    return clean:match("^(.*)/[^/]+$") or ""
end

-- ==========================================
-- 外部 API: 供其他 Lua 腳本以 Lua 原生方式 require Python 腳本
-- @param module_name: string, 要載入的 Python 腳本頁面名稱
-- @return: 封裝後的 Proxy Table,可直接作為 Lua 物件或函數調用
-- ==========================================
function p.require(module_name)
    local Import = require("Module:PythonScript/Import")
    local MwAPI = require("Module:PythonScript/Mw")
    local PyClass = require("Module:PythonScript/Class")
    
    if not SysRegistry.argv_registry[1] then
        SysRegistry.argv_registry[1] = "Module:PythonScript"
    end

    local req_dir = extract_script_dir(module_name)
    if req_dir ~= "" then add_sys_path(req_dir) end
    add_sys_path("")
    
    local success, py_module = pcall(Import.execute_import, module_name, {
        ["__name__"] = module_name,
        ["__dir__"] = req_dir
    })
    if not success then
        error("Python ImportError: Failed to load '" .. tostring(module_name) .. "'\n" .. tostring(py_module), 2)
    end
    
    local proxy_func_map = setmetatable({}, { __mode = "k" })
    local make_py_proxy

    local function unwrap_proxy_arg(v)
        if type(v) == "table" and v.__is_py_proxy and v.__py_obj ~= nil then
            return v.__py_obj
        elseif type(v) == "function" and proxy_func_map[v] ~= nil then
            return proxy_func_map[v]
        end
        return MwAPI.lua2py(v)
    end

    local function make_proxy_binop(op_str)
        return function(a, b)
            local eval_expr = require("Module:PythonScript/Evaluator/Core").getCoreFunction()
            local dummy_scope = require("Module:PythonScript/Evaluator/Utils").create_scope(nil)
            local ok, res = pcall(eval_expr, {
                type = "BinaryOp", op = op_str,
                left = { type = "DirectValue", value = unwrap_proxy_arg(a) },
                right = { type = "DirectValue", value = unwrap_proxy_arg(b) }
            }, dummy_scope)
            if not ok then
                if type(res) == "table" and res.__type == "instance" then
                    local exc_name = res.__class and res.__class.__name or "Exception"
                    local exc_msg = res.__dict and res.__dict.msg or ""
                    error("PythonException: " .. exc_name .. ": " .. exc_msg, 2)
                end
                error("Python Runtime Error: " .. tostring(res), 2)
            end
            return make_py_proxy(res)
        end
    end

    local function proxy_unm(a)
        local eval_expr = require("Module:PythonScript/Evaluator/Core").getCoreFunction()
        local dummy_scope = require("Module:PythonScript/Evaluator/Utils").create_scope(nil)
        local ok, res = pcall(eval_expr, {
            type = "UnaryOp", op = "-",
            right = { type = "DirectValue", value = unwrap_proxy_arg(a) }
        }, dummy_scope)
        if not ok then error(tostring(res), 2) end
        return make_py_proxy(res)
    end

    local function proxy_eq(a, b)
        local EvalUtils = require("Module:PythonScript/Evaluator/Utils")
        return EvalUtils.py_eq(unwrap_proxy_arg(a), unwrap_proxy_arg(b))
    end

    local function proxy_lt(a, b)
        local EvalUtils = require("Module:PythonScript/Evaluator/Utils")
        return EvalUtils.py_cmp(unwrap_proxy_arg(a), unwrap_proxy_arg(b)) < 0
    end

    local function proxy_le(a, b)
        local EvalUtils = require("Module:PythonScript/Evaluator/Utils")
        return EvalUtils.py_cmp(unwrap_proxy_arg(a), unwrap_proxy_arg(b)) <= 0
    end

    local function proxy_concat(a, b)
        return tostring(a) .. tostring(b)
    end

    local op_add = make_proxy_binop("+")
    local op_sub = make_proxy_binop("-")
    local op_mul = make_proxy_binop("*")
    local op_div = make_proxy_binop("/")
    local op_mod = make_proxy_binop("%")
    local op_pow = make_proxy_binop("**")

    make_py_proxy = function(py_obj, seen)
        if type(py_obj) ~= "table" then return py_obj end
        
        if py_obj.__type == "LuaInstance" or (py_obj.__class and py_obj.__class.__name == "LuaInstance") then
            return py_obj.__dict._lua_obj
        end
        
        seen = seen or {}
        if seen[py_obj] then return seen[py_obj] end
        
        -- 【情況 A】: Python 函數、方法、類別
        if py_obj.__type == "function" or py_obj.__type == "bound_method" or py_obj.__type == "builtin_method" or py_obj.__type == "class" then
            local proxy_func = function(...)
                local args = {...}
                local py_args = {}
                
                local func_to_call = py_obj
                local self_obj = nil
                
                if py_obj.__type == "bound_method" then
                    func_to_call = py_obj.__func
                    self_obj = py_obj.__self
                end
                
                local start_idx = 1
                if py_obj.__type == "bound_method" and #args >= 1 then
                    if type(args[1]) == "table" and args[1].__is_py_proxy and args[1].__py_obj == py_obj.__self then
                        start_idx = 2
                    end
                end
                
                for i = start_idx, #args do 
                    table.insert(py_args, unwrap_proxy_arg(args[i])) 
                end
                
                if py_obj.__type == "class" then
                    local invoke_success, res = pcall(Evaluator.invoke, py_obj, py_args, {})
                    if not invoke_success then error("Python Class Init Error: " .. tostring(res), 2) end
                    return make_py_proxy(res, seen)
                end

                local invoke_success, res = pcall(Evaluator.invoke, func_to_call, py_args, {}, self_obj)
                if not invoke_success then 
                    if type(res) == "table" and res.__type == "instance" then
                        local exc_name = res.__class and res.__class.__name or "Exception"
                        local exc_msg = res.__dict.msg or ""
                        error("PythonException: " .. exc_name .. ": " .. exc_msg, 2)
                    else
                        error("Python Runtime Error in Lua call: " .. tostring(res), 2)
                    end
                end
                
                return make_py_proxy(res, seen)
            end
            proxy_func_map[proxy_func] = py_obj
            seen[py_obj] = proxy_func
            return proxy_func
        end
        
        -- 【情況 B】: 基礎容器型態
        local is_dict = py_obj.__type == "dict"
        local is_seq = py_obj.__type == "list" or py_obj.__type == "tuple" or py_obj.__type == "set"
        
        if is_dict or is_seq then
            local t = {}
            seen[py_obj] = t
            if is_dict then
                for _, pair in ipairs(py_obj.pairs or {}) do 
                    t[make_py_proxy(pair.key, seen)] = make_py_proxy(pair.value, seen) 
                end
            else
                for i, v in ipairs(py_obj.elements or {}) do 
                    t[i] = make_py_proxy(v, seen) 
                end
            end
            
            local mt = {
                __index = function(_, key)
                    if key == "__is_py_proxy" then return true end
                    if key == "__py_obj" then return py_obj end
                    local get_success, res = pcall(PyClass.get_attr, py_obj, key)
                    if get_success then return make_py_proxy(res, seen) end
                    return nil
                end,
                __newindex = function(_, key, value)
                    pcall(PyClass.set_attr, py_obj, key, unwrap_proxy_arg(value))
                end,
                __tostring = function(_)
                    local str_success, s = pcall(Runtime.builtins.str.__call, {py_obj})
                    return str_success and s or "<Python Collection Proxy>"
                end,
                __add = op_add, __sub = op_sub, __mul = op_mul,
                __div = op_div, __mod = op_mod, __pow = op_pow,
                __unm = proxy_unm, __eq = proxy_eq, __lt = proxy_lt, __le = proxy_le,
                __concat = proxy_concat,
                __metatable = "Python Collection Proxy"
            }
            setmetatable(t, mt)
            return t
        end
        
        -- 【情況 C】: Module, Instance, BigInt 等 -> Lazy Proxy
        local proxy = {}
        seen[py_obj] = proxy
        local mt = {
            __index = function(_, key)
                if key == "__is_py_proxy" then return true end
                if key == "__py_obj" then return py_obj end
                
                local get_success, res = pcall(PyClass.get_attr, py_obj, key)
                if get_success then
                    if type(res) == "table" and res.__type == "bound_method" then
                        local host_name = res.__self and res.__self.__class and res.__self.__class.__name
                        if host_name and (string.find(host_name, ":") or string.find(host_name, "/") or string.find(host_name, "%.py$")) then
                            res = res.__func -- 強制還原為純函數
                        end
                    end
                    return make_py_proxy(res, seen)
                end
                return nil
            end,
            __newindex = function(_, key, value)
                pcall(PyClass.set_attr, py_obj, key, unwrap_proxy_arg(value))
            end,
            __tostring = function(_)
                local str_success, s = pcall(Runtime.builtins.str.__call, {py_obj})
                return str_success and s or "<PythonObject Proxy>"
            end,
            __add = op_add, __sub = op_sub, __mul = op_mul,
            __div = op_div, __mod = op_mod, __pow = op_pow,
            __unm = proxy_unm, __eq = proxy_eq, __lt = proxy_lt, __le = proxy_le,
            __concat = proxy_concat,
            __metatable = "Python Object Proxy"
        }
        setmetatable(proxy, mt)
        return proxy
    end
    
    return make_py_proxy(py_module)
end

-- ==========================================
-- 輔助函式:Python 物件轉字串、JSON 結構檢視與 yield from 展開器
-- ==========================================
local MAX_YIELD_FROM_ITEMS = 1000

local function is_py_none(val)
    return val == nil or (type(val) == "table" and val.__class and val.__class.__name == "NoneType")
end

local function py_to_str(val)
    local ok, s = pcall(Runtime.builtins.str.__call, {val})
    return ok and tostring(s) or tostring(val)
end

-- 提取物件的第一層屬性為 { {key=..., value=...}, ... }
local function extract_object_layer(obj)
    local PyClass = require("Module:PythonScript/Class")
    local MwAPI = require("Module:PythonScript/Mw")
    local pairs_list = {}
    local seen_keys = {}
    local SKIP_KEYS = {
        __traceback_info__ = true, _raw_lua_tbl = true,
        _is_external = true, _name = true, _lua_obj = true
    }

    local function add_kv(k, v)
        local ks = tostring(k)
        if not seen_keys[ks] and not SKIP_KEYS[ks] then
            seen_keys[ks] = true
            table.insert(pairs_list, { key = k, value = v })
        end
    end

    if type(obj) == "table" then
        if obj.__type == "LuaInstance" or (obj.__class and obj.__class.__name == "LuaInstance") then
            local raw = obj.__dict and obj.__dict._lua_obj
            if type(raw) == "table" then
                for k, v in pairs(raw) do add_kv(MwAPI.lua2py(k), MwAPI.lua2py(v)) end
            end
        elseif obj.__class and obj.__class.__name == "module" and obj.__dict and type(obj.__dict._raw_lua_tbl) == "table" then
            for k, v in pairs(obj.__dict._raw_lua_tbl) do add_kv(tostring(k), MwAPI.lua2py(v)) end
            for k, v in pairs(obj.__dict) do add_kv(k, v) end
        else
            if obj.__dict then
                for k, v in pairs(obj.__dict) do add_kv(k, v) end
            end
            if obj.__type == "instance" and obj.__class and obj.__class.__mro then
                for _, base_cls in ipairs(obj.__class.__mro) do
                    for k, cval in pairs(base_cls.__dict or {}) do
                        if type(cval) == "table" and cval.__type == "property" and cval.fget and not seen_keys[k] then
                            local ok_p, pval = pcall(PyClass.get_attr, obj, k)
                            if ok_p then add_kv(k, pval) end
                        end
                    end
                end
            end
        end
    end

    table.sort(pairs_list, function(a, b) return tostring(a.key) < tostring(b.key) end)
    return pairs_list
end

-- 將任意 Python 物件遞迴轉為可被 mw.text.jsonEncode 序列化的結構
local function to_json_inspectable(val, seen)
    if is_py_none(val) then return nil end
    local t = type(val)
    if t == "boolean" or t == "string" then return val end
    if t == "number" then
        if val ~= val then return "NaN"
        elseif val == math.huge then return "Infinity"
        elseif val == -math.huge then return "-Infinity" end
        return val
    end
    if t ~= "table" then return tostring(val) end

    if val.__type == "int" and val.is_big then
        local MathCore = require("Module:PythonScript/Runtime/Cmath")
        return val.value or MathCore.BigInt.to_string(val.sign, val.limbs)
    elseif val.__type == "complex" then
        return { __type = "complex", real = val.real, imag = val.imag }
    elseif val.__type == "bytes" or val.__type == "bytearray" or val.__type == "memoryview" then
        return py_to_str(val)
    elseif val.__type == "range" then
        return { __type = "range", start = val.start, stop = val.stop, step = val.step }
    elseif val.__type == "function" or val.__type == "builtin_method" or val.__type == "bound_method" then
        return py_to_str(val)
    end

    seen = seen or {}
    if seen[val] then return "<circular reference>" end
    seen[val] = true

    local res
    if val.__type == "list" or val.__type == "tuple" or val.__type == "set" or val.__type == "frozenset" then
        res = {}
        for i, v in ipairs(val.elements or {}) do
            res[i] = to_json_inspectable(v, seen)
        end
    elseif val.__type == "dict" then
        res = {}
        for _, p in ipairs(val.pairs or {}) do
            local k_str = py_to_str(p.key)
            local v_json = to_json_inspectable(p.value, seen)
            res[k_str] = (v_json == nil) and "None" or v_json
        end
    elseif val.__type == "instance" or val.__type == "class" or val.__type == "module" then
        if val.__builtin_value ~= nil and (not val.__dict or next(val.__dict) == nil) then
            res = to_json_inspectable(val.__builtin_value, seen)
        else
            res = {}
            if val.__type == "instance" and val.__class and val.__class.__name then
                res["__class__"] = val.__class.__name
            elseif val.__type == "class" and val.__name then
                res["__class__"] = "type:" .. val.__name
            end
            for _, p in ipairs(extract_object_layer(val)) do
                local k_str = py_to_str(p.key)
                local v_json = to_json_inspectable(p.value, seen)
                res[k_str] = (v_json == nil) and "None" or v_json
            end
        end
    else
        res = py_to_str(val)
    end

    seen[val] = nil
    return res
end

local function encode_as_json(data, json_flags)
    if is_py_none(data) then return "null" end
    local inspectable = to_json_inspectable(data)
    local ok, j_str = pcall(mw.text.jsonEncode, inspectable, json_flags)
    if ok then return j_str end
    return py_to_str(data)
end

-- 處理頂層 yield from x 語法糖
local function format_yield_from(x, as_json, json_flags)
    local PyClass = require("Module:PythonScript/Class")

    -- 1. 字典或 UserDict 家族:展開鍵值對 (上限 1000 個)
    local is_dict_obj = (type(x) == "table" and x.__type == "dict")
        or (type(x) == "table" and x.__type == "instance" and x.__dict and type(x.__dict.data) == "table" and x.__dict.data.__type == "dict")
    if is_dict_obj then
        local pairs_list = (x.__type == "dict") and (x.pairs or {}) or (x.__dict.data.pairs or {})
        if as_json then
            local out = {}
            for i = 1, math.min(#pairs_list, MAX_YIELD_FROM_ITEMS) do
                local p = pairs_list[i]
                out[py_to_str(p.key)] = py_to_str(p.value)
            end
            if #pairs_list > MAX_YIELD_FROM_ITEMS then out["..."] = "..." end
            local ok, j_str = pcall(mw.text.jsonEncode, out, json_flags)
            if ok then return j_str end
        end
        local parts = {}
        for i = 1, math.min(#pairs_list, MAX_YIELD_FROM_ITEMS) do
            local p = pairs_list[i]
            table.insert(parts, py_to_str(p.key) .. ": " .. py_to_str(p.value))
        end
        if #pairs_list > MAX_YIELD_FROM_ITEMS then table.insert(parts, "...") end
        return "{" .. table.concat(parts, ", ") .. "}"
    end

    -- 2. 判斷是否為迭代器、產生器或可迭代序列
    local is_iterable_seq = false
    if type(x) == "string" then
        is_iterable_seq = true
    elseif type(x) == "table" then
        local pt = x.__type
        if pt == "list" or pt == "tuple" or pt == "set" or pt == "frozenset" or pt == "range" or pt == "bytes" or pt == "bytearray" or pt == "memoryview" then
            is_iterable_seq = true
        elseif pt == "instance" then
            local cname = x.__class and x.__class.__name
            if cname ~= "module" and cname ~= "LazyNamespace" and cname ~= "LuaInstance" and cname ~= "super_proxy" then
                local ok_n, next_m = pcall(PyClass.get_attr, x, "__next__")
                local ok_i, iter_m = pcall(PyClass.get_attr, x, "__iter__")
                if (ok_n and next_m) or (ok_i and iter_m) then
                    is_iterable_seq = true
                end
            end
        end
    end

    if is_iterable_seq then
        local iter_obj
        local ok_n, next_m = pcall(PyClass.get_attr, x, "__next__")
        if ok_n and next_m then
            iter_obj = x
        else
            iter_obj = Runtime.builtins.iter({x})
        end
        local sentinel = {}
        local items = {}
        local count = 0
        local truncated = false
        while true do
            local v = Runtime.builtins.next({iter_obj, sentinel})
            if v == sentinel then break end
            count = count + 1
            if count > MAX_YIELD_FROM_ITEMS then
                truncated = true
                break
            end
            table.insert(items, py_to_str(v))
        end
        if truncated then
            table.insert(items, "...")
        end
        if as_json then
            local ok, j_str = pcall(mw.text.jsonEncode, items, json_flags)
            if ok then return j_str end
        end
        return "[" .. table.concat(items, ", ") .. "]"
    end

    -- 3. 一般物件 (instance, class, module, LuaInstance, LazyNamespace):展開 1 層轉為字典格式
    if type(x) == "table" and (x.__type == "instance" or x.__type == "class" or x.__type == "module" or x.__type == "LuaInstance") then
        local pairs_list = extract_object_layer(x)
        if as_json then
            local out = {}
            for i = 1, math.min(#pairs_list, MAX_YIELD_FROM_ITEMS) do
                local p = pairs_list[i]
                out[py_to_str(p.key)] = py_to_str(p.value)
            end
            if #pairs_list > MAX_YIELD_FROM_ITEMS then out["..."] = "..." end
            local ok, j_str = pcall(mw.text.jsonEncode, out, json_flags)
            if ok then return j_str end
        end
        local parts = {}
        for i = 1, math.min(#pairs_list, MAX_YIELD_FROM_ITEMS) do
            local p = pairs_list[i]
            table.insert(parts, py_to_str(p.key) .. ": " .. py_to_str(p.value))
        end
        if #pairs_list > MAX_YIELD_FROM_ITEMS then table.insert(parts, "...") end
        return "{" .. table.concat(parts, ", ") .. "}"
    end

    -- 4. 其他基本型別 Fallback
    if as_json then return encode_as_json(x, json_flags) end
    return py_to_str(x)
end

local function format_return_signal(sig, as_json, json_flags)
    if not sig then return "" end
    if sig.is_yield_from then
        return format_yield_from(sig.value, as_json, json_flags)
    end
    if as_json then
        return encode_as_json(sig.value, json_flags)
    end
    return py_to_str(sig.value)
end

-- ==========================================
-- 內部 API: 供其他 Lua Module 呼叫 (AST管線與錯誤捕捉引擎)
-- ==========================================
function p.execute(script, global_env, stdin_text, options)
    if type(script) ~= "string" or script == "" then return "" end

    options = options or {}
    local output_mode = options.output_mode or "both" -- "both" | "stdout" | "return"
    local as_json = options.as_json or false
    local json_flags = options.json_flags

    local mod_env = global_env or {}
    if not mod_env["__name__"] then mod_env["__name__"] = "__main__" end

    if not mod_env["__file__"] then
        local ok_t, cur_t = pcall(mw.title.getCurrentTitle)
        if ok_t and cur_t and cur_t.prefixedText and cur_t.prefixedText ~= "" then
            mod_env["__file__"] = cur_t.prefixedText
        else
            mod_env["__file__"] = mod_env["__name__"]
        end
    end
    if not mod_env["__dir__"] then
        if mod_env["__name__"] == "__main__" and mod_env["__file__"] ~= "__main__" then
            mod_env["__dir__"] = mod_env["__file__"]:gsub("%.py$", "")
        else
            mod_env["__dir__"] = extract_script_dir(mod_env["__file__"])
        end
    end

    -- 初始化模組快取與 sys.path 搜尋路徑 (當前頁面子目錄優先 -> 父目錄 -> 全域根目錄)
    SysRegistry.modules_registry = {}
    SysRegistry.path_registry = {}
    if mod_env["__dir__"] and mod_env["__dir__"] ~= "" then
        add_sys_path(mod_env["__dir__"])
        local parent_dir = extract_script_dir(mod_env["__dir__"])
        if parent_dir ~= "" then add_sys_path(parent_dir) end
    end
    add_sys_path("")

    -- 1. 重置虛擬 I/O 與全域 Traceback 環境
    Runtime.stdout = {}
    Runtime.stdin = {}
    Runtime.Traceback.files = { [1] = { name = mod_env["__name__"], raw_code = script } }
    Runtime.Traceback.current_file_idx = 1
    Runtime.Traceback.current_func_name = "<module>"
    Runtime.Traceback.current_line = 1
    Runtime.Traceback.stack = {}
    
    if stdin_text and stdin_text ~= "" then
        stdin_text = stdin_text:gsub("\r\n", "\n")
        for line in string.gmatch(stdin_text .. "\n", "(.-)\n") do
            if line then table.insert(Runtime.stdin, line) end
        end
    end

    -- 2. 封裝整個管線,以便單一 pcall 捕捉所有層級錯誤
    local is_invoke = options.is_invoke and "invoke" or "module"
    local function run_pipeline()
        local ok_parse, ast_or_err = pcall(function()
            local tokens = Lexer.tokenize(script)
            return Parser.parse(tokens, is_invoke)
        end)
        if not ok_parse then
            if options.is_ambiguous then
                local page_label = mod_env["__file__"] or mod_env["__name__"] or "<unknown>"
                Runtime.raise("ImportError", "Source page '" .. tostring(page_label) .. "' does not appear to be a valid Python script.")
            end
            error(ast_or_err, 0)
        end
        return Evaluator.execute(ast_or_err, mod_env)
    end

    local success, res_or_err = pcall(run_pipeline)
    local return_sig = nil
    local has_unhandled_error = false

    -- 3. 統一處理錯誤與輸出
    if success then
        if type(res_or_err) == "table" then
            return_sig = res_or_err.__return_signal
        end
    else
        local err_obj = res_or_err
        if type(err_obj) == "table" and err_obj.__type == "instance" then
            -- 【純 Python 錯誤】
            if err_obj.__class and err_obj.__class.__name == "SystemExit" then
                local exit_val = err_obj.code
                if exit_val == nil and err_obj.__dict then exit_val = err_obj.__dict.code end
                local is_numeric_code = (type(exit_val) == "number") or (type(exit_val) == "table" and exit_val.__type == "int")
                if not is_py_none(exit_val) and not is_numeric_code then
                    return_sig = { type = "exit", value = exit_val }
                end
            else
                has_unhandled_error = true
                local function format_single_exc(exc)
                    local tb_lines = {"Traceback (most recent call last):"}
                    local tb_info = exc.__dict.__traceback_info__ or {}
                    for _, frame in ipairs(tb_info) do
                        local f_idx, f_name, l_num = frame[1], frame[2], frame[3]
                        local f_file = Runtime.Traceback.files[f_idx] and Runtime.Traceback.files[f_idx].name or "<unknown>"
                        table.insert(tb_lines, string.format('  File "%s", line %d, in %s', f_file, l_num, f_name))
                        
                        local raw_code = Runtime.Traceback.files[f_idx] and Runtime.Traceback.files[f_idx].raw_code or ""
                        if raw_code ~= "" then
                            local cur_l = 1
                            for line_str in string.gmatch(raw_code .. "\n", "([^\n]*)\n") do
                                if cur_l == l_num then
                                    local trimmed = line_str:match("^%s*(.-)%s*$")
                                    if trimmed and trimmed ~= "" then table.insert(tb_lines, "    " .. trimmed) end
                                    break
                                end
                                cur_l = cur_l + 1
                            end
                        end
                    end
                    local exc_name = exc.__class and exc.__class.__name or "Exception"
                    local exc_msg = exc.__dict.msg or ""
                    local err_str = exc_name
                    if exc_msg ~= "" and exc_msg ~= nil then err_str = err_str .. ": " .. exc_msg end
                    table.insert(tb_lines, err_str)
                    return table.concat(tb_lines, "\n")
                end

                local chain_strs = {}
                local current_exc = err_obj
                local visited = {}
                
                while current_exc and type(current_exc) == "table" and not visited[current_exc] do
                    visited[current_exc] = true
                    local exc_str = format_single_exc(current_exc)
                    
                    local cause = current_exc.__dict.__cause__
                    local context = current_exc.__dict.__context__
                    
                    if cause then
                        if type(cause) == "table" and cause.__class and cause.__class.__name == "NoneType" then
                            table.insert(chain_strs, 1, exc_str)
                            break
                        else
                            table.insert(chain_strs, 1, "\nThe above exception was the direct cause of the following exception:\n\n" .. exc_str)
                            current_exc = cause
                        end
                    elseif context and not current_exc.__dict.__suppress_context__ then
                        table.insert(chain_strs, 1, "\nDuring handling of the above exception, another exception occurred:\n\n" .. exc_str)
                        current_exc = context
                    else
                        table.insert(chain_strs, 1, exc_str)
                        break
                    end
                end
                
                table.insert(Runtime.stdout, '<strong class="error" style="color:red; font-family:monospace;">\n' .. mw.text.nowiki(table.concat(chain_strs, "")) .. '</strong>\n')
            end
        else
            -- 【語法分析期錯誤 或 底層引擎 SystemError】
            has_unhandled_error = true
            local raw_err = tostring(err_obj)
            local lua_file, lua_line, clean_err = raw_err:match("([^:]+):(%d+):%s*(.*)")
            if not clean_err then clean_err = raw_err end
            
            local is_syntax = clean_err:match("^SyntaxError:") or clean_err:match("^IndentationError:")
            local tb_lines = {"Traceback (most recent call last):"}
            
            if is_syntax then
                local line_num = tonumber(clean_err:match("at line (%d+)")) or Runtime.Traceback.current_line
                clean_err = clean_err:gsub(" at line %d+", "")
                local curr_file = Runtime.Traceback.files[Runtime.Traceback.current_file_idx] and Runtime.Traceback.files[Runtime.Traceback.current_file_idx].name or "<unknown>"
                table.insert(tb_lines, string.format('  File "%s", line %d, in <module>', curr_file, line_num))
                
                local raw_code = Runtime.Traceback.files[Runtime.Traceback.current_file_idx] and Runtime.Traceback.files[Runtime.Traceback.current_file_idx].raw_code or ""
                if raw_code ~= "" then
                    local cur_l = 1
                    for line_str in string.gmatch(raw_code .. "\n", "([^\n]*)\n") do
                        if cur_l == line_num then
                            local trimmed = line_str:match("^%s*(.-)%s*$")
                            if trimmed and trimmed ~= "" then table.insert(tb_lines, "    " .. trimmed) end
                            break
                        end
                        cur_l = cur_l + 1
                    end
                end
                table.insert(tb_lines, clean_err)
            else
                for _, frame in ipairs(Runtime.Traceback.stack) do
                    local f_idx, f_name, l_num = frame[1], frame[2], frame[3]
                    local f_file = Runtime.Traceback.files[f_idx] and Runtime.Traceback.files[f_idx].name or "<unknown>"
                    table.insert(tb_lines, string.format('  File "%s", line %d, in %s', f_file, l_num, f_name))
                end
                local curr_file = Runtime.Traceback.files[Runtime.Traceback.current_file_idx] and Runtime.Traceback.files[Runtime.Traceback.current_file_idx].name or "<unknown>"
                table.insert(tb_lines, string.format('  File "%s", line %d, in <module>', curr_file, Runtime.Traceback.current_line))
                local loc_info = (lua_file and lua_line) and string.format("%s:%d: ", lua_file, tonumber(lua_line)) or ""
                table.insert(tb_lines, "SystemError: internal execution failure (Lua traceback: " .. loc_info .. clean_err .. ")")
            end
            table.insert(Runtime.stdout, '<strong class="error" style="color:red; font-family:monospace;">\n' .. mw.text.nowiki(table.concat(tb_lines, "\n")) .. '</strong>\n')
        end
    end

	local stdout_str = table.concat(Runtime.stdout, "")
    local ret_str = ""
    if return_sig then
        ret_str = format_return_signal(return_sig, as_json, json_flags)
    end

    if output_mode == "stdout" then
        if return_sig and return_sig.type == "exit" then
            return stdout_str .. ret_str
        end
        return stdout_str
    elseif output_mode == "return" then
        if has_unhandled_error and ret_str == "" then
            return stdout_str
        end
        return ret_str
    else
        -- 預設 "both":同時輸出 stdout 與 return 值
        return stdout_str .. '\n' .. ret_str
    end
end

-- ==========================================
-- 外部 API: 供 Wikitext {{#invoke:PythonScript|main}} 呼叫
-- ==========================================
local RESERVED_INVOKE_PARAMS = {
    code = true, stdin = true, output = true, mode = true, format = true, json = true
}

function p.main(frame)
    local args = frame.args
    local parent_args = frame:getParent() and frame:getParent().args or {}

    local script = args.code or args[1] or parent_args.code or parent_args[1] or ""
    local stdin = args.stdin or args[2] or parent_args.stdin or parent_args[2] or ""
	script = mw.text.unstripNoWiki(script)
    -- 解析輸出控制參數 (output / mode) 與 JSON 格式化參數 (format / json)
    local raw_output = args.output or args.mode or parent_args.output or parent_args.mode or "both"
    raw_output = tostring(raw_output):lower():match("^%s*(.-)%s*$")
    local output_mode = "both"
    if raw_output == "stdout" or raw_output == "print" then
        output_mode = "stdout"
    elseif raw_output == "return" or raw_output == "ret" or raw_output == "result" then
        output_mode = "return"
    end

    local raw_fmt = tostring(args.format or parent_args.format or ""):lower():match("^%s*(.-)%s*$")
    local raw_json = tostring(args.json or parent_args.json or ""):lower():match("^%s*(.-)%s*$")
    local as_json = (raw_fmt == "json" or raw_fmt == "pretty_json" or raw_json == "1" or raw_json == "true" or raw_json == "yes" or raw_json == "pretty")
    local json_flags = (raw_fmt == "pretty_json" or raw_json == "pretty") and mw.text.JSON_PRETTY or nil

    local argv_start_idx = 1
    if not (args.code or parent_args.code) then
        argv_start_idx = 2
        if not (args.stdin or parent_args.stdin) and (args[2] or parent_args[2]) then
            argv_start_idx = 3
        end
    end

    SysRegistry.argv_registry = { "Module:PythonScript" } 
    
    local max_idx = 0
    for k, _ in pairs(args) do if type(k) == "number" and k > max_idx then max_idx = k end end
    for k, _ in pairs(parent_args) do if type(k) == "number" and k > max_idx then max_idx = k end end

    for i = argv_start_idx, max_idx do
        local val = args[i] or parent_args[i]
        if val then table.insert(SysRegistry.argv_registry, mw.text.unstripNoWiki(val)) end
    end
    
    for k, v in pairs(parent_args) do
        if type(k) == "string" and not RESERVED_INVOKE_PARAMS[k] then
            table.insert(SysRegistry.argv_registry, "--" .. k)
            if v ~= "" then table.insert(SysRegistry.argv_registry, mw.text.unstripNoWiki(v)) end
        end
    end
    for k, v in pairs(args) do
        if type(k) == "string" and not RESERVED_INVOKE_PARAMS[k] then
            table.insert(SysRegistry.argv_registry, "--" .. k)
            if v ~= "" then table.insert(SysRegistry.argv_registry, mw.text.unstripNoWiki(v)) end
        end
    end
    
    local env = {}
    for k, v in pairs(parent_args) do
        if not RESERVED_INVOKE_PARAMS[k] and k ~= 1 and k ~= 2 then
            env[k] = mw.text.unstripNoWiki(v)
        end
    end
    for k, v in pairs(args) do
        if not RESERVED_INVOKE_PARAMS[k] and k ~= 1 and k ~= 2 then
            env[k] = mw.text.unstripNoWiki(v)
        end
    end

    -- 動態偵測呼叫端維基頁面標題 (優先順序:parent frame 標題 -> getCurrentTitle -> 當前 frame 標題)
    local caller_title = nil
    local parent_frame = frame:getParent()
    if parent_frame and type(parent_frame.getTitle) == "function" then
        local pt = parent_frame:getTitle()
        if pt and pt ~= "" then caller_title = pt end
    end
    local ok_ct, cur_t = pcall(mw.title.getCurrentTitle)
    if ok_ct and cur_t and cur_t.prefixedText and cur_t.prefixedText ~= "" then
        if not caller_title or caller_title:match("^Template:") or caller_title:match("^Module:") then
            caller_title = cur_t.prefixedText
        end
    end
    if (not caller_title or caller_title == "") and type(frame.getTitle) == "function" then
        caller_title = frame:getTitle()
    end
    if caller_title and caller_title ~= "" then
        local norm_caller = caller_title:gsub("_", "_")
        env["__file__"] = norm_caller
        env["__dir__"] = norm_caller:gsub("%.py$", "")
    end

    local exec_options = {
        output_mode = output_mode,
        as_json = as_json,
        json_flags = json_flags,
        is_invoke = true  -- 啟用頂層 return / yield 的特權模式
    }

    -- pcall 的防護已內建在 p.execute 中,這裡外層再包覆純粹作為最後防線
    local success, result = pcall(p.execute, script, env, stdin, exec_options)

    if success then
        return result
    else
        return '<strong class="error" style="color:red; font-family:monospace;">SystemError: internal execution failure (' .. mw.text.nowiki(tostring(result)) .. ')</strong>'
    end
end

-- ==========================================
-- 路徑特徵識別、相對路徑轉絕對路徑與直接執行引擎
-- ==========================================
-- 判斷字串是否具備路徑或檔名特徵(含冒號、空格、點號/句號、正反斜線、括弧)
local function is_path_like(key)
    if type(key) ~= "string" or key == "" then return false end
    if key:find(":", 1, true) or key:find(":", 1, true)
        or key:find(" ", 1, true)
        or key:find(".", 1, true) or key:find("。", 1, true)
        or key:find("/", 1, true) or key:find("\\", 1, true)
        or key:find("(", 1, true) or key:find(")", 1, true)
        or key:find("(", 1, true) or key:find(")", 1, true) then
        return true
    end
    return false
end

-- 取得當前呼叫端頁面路徑(優先取 frame:getParent():getTitle(),無 parent 則取 frame:getTitle() 或 getCurrentTitle)
local function get_caller_dir(frame)
    frame = frame or (mw and mw.getCurrentFrame and mw.getCurrentFrame())
    local caller_title = nil

    if frame and type(frame.getParent) == "function" then
        local ok_p, parent_frame = pcall(frame.getParent, frame)
        if ok_p and parent_frame and type(parent_frame.getTitle) == "function" then
            local pt = parent_frame:getTitle()
            if pt and pt ~= "" then caller_title = pt end
        end
    end

    if (not caller_title or caller_title == "") and frame and type(frame.getTitle) == "function" then
        local ft = frame:getTitle()
        if ft and ft ~= "" and not ft:match("^Module:PythonScript") and not ft:match("^模組:沙盒/a2569875/PythonScript") and not ft:match("^模塊:沙盒/a2569875/PythonScript") then
            caller_title = ft
        end
    end

    if not caller_title or caller_title == "" then
        local ok_ct, cur_t = pcall(mw.title.getCurrentTitle)
        if ok_ct and cur_t and cur_t.prefixedText and cur_t.prefixedText ~= "" then
            caller_title = cur_t.prefixedText
        end
    end

    if caller_title and caller_title ~= "" then
        return caller_title:gsub("_", "_"):gsub("%.py$", "")
    end
    return ""
end

-- 將相對路徑(如 ./foo、./foo.py、../bar)轉換為完整絕對路徑(如 User:Example/Bar/foo.py)
local function to_absolute_path(raw_key, frame)
    if type(raw_key) ~= "string" then return "" end
	if mw.text.trim(raw_key) == '.' then
		local wf = mw.getCurrentFrame()
		local parent = wf:getParent()
		return (parent and type(parent.getTitle)==type(mw.getCurrentFrame) and parent:getTitle()) or wf:getTitle() or "" 
	end
    local path = raw_key:match("^%s*(.-)%s*$"):gsub("\\", "/"):gsub("_", "_")
    path = path:gsub("^。/", "./"):gsub("^。。/", "../")

    local is_rel = (path:sub(1, 2) == "./" or path:sub(1, 3) == "../" or path:sub(1, 1) == "/")
    if is_rel then
        local base_dir = get_caller_dir(frame)
        if path:sub(1, 1) == "/" then
            path = path:gsub("^/+", "")
        else
            while path:sub(1, 2) == "./" or path:sub(1, 3) == "../" do
                if path:sub(1, 2) == "./" then
                    path = path:sub(3):gsub("^/+", "")
                elseif path:sub(1, 3) == "../" then
                    path = path:sub(4):gsub("^/+", "")
                    base_dir = base_dir:match("^(.*)/[^/]+$") or ""
                end
            end
        end
        if base_dir ~= "" then
            return (path ~= "") and (base_dir .. "/" .. path) or base_dir
        end
        return path
    end
    return path
end

local function is_module_namespace(path_str)
    if type(path_str) ~= "string" then return false end
    return path_str:match("^[Mm]odule:") ~= nil
        or path_str:match("^模組:") ~= nil
        or path_str:match("^模塊:") ~= nil
end

-- ==========================================
-- 直接執行 .py 維基頁面腳本 (支援 {{#invoke:PythonScript|XXX.py}} 與 Lua (py[[XXX.py]])())
-- ==========================================
local function resolve_script_target(script_key, caller_dir)
    local Import = require("Module:PythonScript/Import")
    local probe = Import.probe_wiki_page
    local clean = (script_key or ""):match("^%s*(.-)%s*$"):gsub("\\", "/"):gsub("_", "_")
    local no_ext = clean:gsub("%.py$", "")

    -- 1. 優先以絕對路徑直接探測(例如已展開的 User:Example/Bar/foo 或 Wikipedia:方針)
    local direct_info = probe(clean) or probe(no_ext .. ".py") or probe(no_ext)
    if direct_info then return direct_info end

    -- 2. 退回搜尋 caller_dir 與 sys.path
    local prefixes = {}
    local seen = { [""] = true }
    local function add_p(pfx)
        if pfx and pfx ~= "" and not seen[pfx] then
            seen[pfx] = true
            table.insert(prefixes, pfx)
        end
    end

    local base_dir = (caller_dir or ""):gsub("_", "_"):gsub("/+$", "")
    if base_dir ~= "" then
        add_p(base_dir)
        local parent_d = base_dir:match("^(.*)/[^/]+$")
        if parent_d and parent_d ~= "" then add_p(parent_d) end
    end
    for _, sp in ipairs(SysRegistry.path_registry or {}) do
        if type(sp) == "string" then add_p(sp) end
    end

    for _, pfx in ipairs(prefixes) do
        local full = pfx .. "/" .. no_ext
        local info = probe(full .. ".py") or probe(full)
        if info then return info end
    end

    -- 3. 若未帶命名空間前綴,嘗試探測 Module: 命名空間
    if not clean:find(":", 1, true) then
        local mod_info = probe("Module:" .. no_ext .. ".py") or probe("Module:" .. no_ext)
        if mod_info then return mod_info end
    end

    return nil
end

function p.run_script_file(script_key, first_arg, ...)
    local is_frame = (type(first_arg) == "table" and type(first_arg.args) == "table" and type(first_arg.getParent) == "function")
    local frame_obj = is_frame and first_arg or nil

    -- 確保相對路徑已轉為絕對路徑
    script_key = to_absolute_path(script_key, frame_obj)

    -- 若為 Module: 開頭,先測試是否為系統原生 Lua 模組
    if is_module_namespace(script_key) then
        local ok_lua, lua_mod = pcall(require, script_key)
        if ok_lua and lua_mod ~= nil then
            return lua_mod
        end
    end

    local caller_dir = get_caller_dir(frame_obj)

    -- 情況 A:由 Wikitext {{#invoke:PythonScript|...}} 調用
    if is_frame then
        local frame = first_arg
        local args = frame.args
        local parent_args = frame:getParent() and frame:getParent().args or {}

        local page_info = resolve_script_target(script_key, caller_dir)
        if not page_info then
            return '<strong class="error" style="color:red; font-family:monospace;">FileNotFoundError: No such Python script: \'' .. mw.text.nowiki(tostring(script_key)) .. '\'</strong>'
        end
        if page_info.is_invalid_model then
            return '<strong class="error" style="color:red; font-family:monospace;">ImportError: Page \'' .. mw.text.nowiki(tostring(page_info.prefixedText)) .. '\' cannot be executed as Python (unsupported content model: ' .. mw.text.nowiki(tostring(page_info.content_model)) .. ')</strong>'
        end
        if page_info.is_not_python then
            return '<strong class="error" style="color:red; font-family:monospace;">ImportError: Source page \'' .. mw.text.nowiki(tostring(page_info.prefixedText)) .. '\' does not appear to be a valid Python script.</strong>'
        end

        local stdin = args.stdin or parent_args.stdin or ""
        stdin = mw.text.unstripNoWiki(stdin)

        local raw_output = args.output or args.mode or parent_args.output or parent_args.mode or "both"
        raw_output = tostring(raw_output):lower():match("^%s*(.-)%s*$")
        local output_mode = "both"
        if raw_output == "stdout" or raw_output == "print" then output_mode = "stdout"
        elseif raw_output == "return" or raw_output == "ret" or raw_output == "result" then output_mode = "return" end

        local raw_fmt = tostring(args.format or parent_args.format or ""):lower():match("^%s*(.-)%s*$")
        local raw_json = tostring(args.json or parent_args.json or ""):lower():match("^%s*(.-)%s*$")
        local as_json = (raw_fmt == "json" or raw_fmt == "pretty_json" or raw_json == "1" or raw_json == "true" or raw_json == "yes" or raw_json == "pretty")
        local json_flags = (raw_fmt == "pretty_json" or raw_json == "pretty") and mw.text.JSON_PRETTY or nil

        local norm_file = page_info.prefixedText
        local script_dir = extract_script_dir(norm_file)
        SysRegistry.argv_registry = { norm_file }

        local max_idx = 0
        for k, _ in pairs(args) do if type(k) == "number" and k > max_idx then max_idx = k end end
        for k, _ in pairs(parent_args) do if type(k) == "number" and k > max_idx then max_idx = k end end

        for i = 1, max_idx do
            local val = args[i] or parent_args[i]
            if val then table.insert(SysRegistry.argv_registry, mw.text.unstripNoWiki(val)) end
        end

        local env = {}
        for k, v in pairs(parent_args) do
            if not RESERVED_INVOKE_PARAMS[k] then
                local clean_v = mw.text.unstripNoWiki(v)
                env[k] = clean_v
                if type(k) == "string" then
                    table.insert(SysRegistry.argv_registry, "--" .. k)
                    if clean_v ~= "" then table.insert(SysRegistry.argv_registry, clean_v) end
                end
            end
        end
        for k, v in pairs(args) do
            if not RESERVED_INVOKE_PARAMS[k] then
                local clean_v = mw.text.unstripNoWiki(v)
                env[k] = clean_v
                if type(k) == "string" then
                    table.insert(SysRegistry.argv_registry, "--" .. k)
                    if clean_v ~= "" then table.insert(SysRegistry.argv_registry, clean_v) end
                end
            end
        end

        env["__name__"] = "__main__"
        env["__file__"] = norm_file
        env["__dir__"] = (script_dir ~= "") and script_dir or caller_dir

        local exec_options = {
            output_mode = output_mode,
            as_json = as_json,
            json_flags = json_flags,
            is_invoke = true,
            is_ambiguous = page_info.is_ambiguous
        }
        local ok, res = pcall(p.execute, page_info.py_code, env, stdin, exec_options)
        if ok then return res end
        return '<strong class="error" style="color:red; font-family:monospace;">SystemError: internal execution failure (' .. mw.text.nowiki(tostring(res)) .. ')</strong>'
    end

    -- 情況 B:由其他 Lua 模組以 (py[[...]])(...) 方式直接呼叫
    local lua_args = { first_arg, ... }
    local page_info = resolve_script_target(script_key, caller_dir)
    if not page_info then
        error("FileNotFoundError: No such Python script: '" .. tostring(script_key) .. "'", 2)
    end
    if page_info.is_invalid_model then
        error("ImportError: Page '" .. tostring(page_info.prefixedText) .. "' cannot be executed as Python (unsupported content model: " .. tostring(page_info.content_model) .. ")", 2)
    end
    if page_info.is_not_python then
        error("ImportError: Source page '" .. tostring(page_info.prefixedText) .. "' does not appear to be a valid Python script.", 2)
    end

    local norm_file = page_info.prefixedText
    local script_dir = extract_script_dir(norm_file)
    SysRegistry.argv_registry = { norm_file }

    local env = {}
    local stdin = ""
    local output_mode = "both"
    local as_json = false
    local json_flags = nil

    for _, arg in ipairs(lua_args) do
        if type(arg) == "table" and not arg.__type then
            for k, v in pairs(arg) do
                if k == "stdin" then stdin = tostring(v)
                elseif k == "output" or k == "mode" then output_mode = tostring(v)
                elseif k == "json" or k == "format" then
                    as_json = (v == true or v == "json" or v == "pretty" or v == "pretty_json")
                    if v == "pretty" or v == "pretty_json" then json_flags = mw.text.JSON_PRETTY end
                else
                    env[k] = v
                end
            end
        elseif arg ~= nil then
            table.insert(SysRegistry.argv_registry, tostring(arg))
        end
    end

    env["__name__"] = "__main__"
    env["__file__"] = norm_file
    env["__dir__"] = (script_dir ~= "") and script_dir or caller_dir

    return p.execute(page_info.py_code, env, stdin, {
        output_mode = output_mode,
        as_json = as_json,
        json_flags = json_flags,
        is_invoke = true,
        is_ambiguous = page_info.is_ambiguous
    })
end

-- 掛載 Metatable __index 魔法:攔截所有路徑特徵鍵值
setmetatable(p, {
    __index = function(_, key)
        if is_path_like(key) then
            local path = key
			--協定: 連續底線要換成全形,以便選中 "__init__" 等頁面
			path = mw.ustring.gsub(path, "(__+)", function(x)return tostring(x):gsub("_","_") end)
            local abs_path = to_absolute_path(path, nil)
			if mw.text.trim(abs_path) == '' then return nil end
				
            -- 若為 Module: 開頭,先測試是否為系統原生 Lua 模組;若解析成功則直接回傳 require(abs_path)
            if is_module_namespace(abs_path) then
                local ok_lua, lua_mod = pcall(require, abs_path)
                if ok_lua and lua_mod ~= nil then
                    return lua_mod
                end
            end

            return function(first_arg, ...)
                local is_frame = (type(first_arg) == "table" and type(first_arg.args) == "table" and type(first_arg.getParent) == "function")
                local target_path = is_frame and to_absolute_path(path, first_arg) or abs_path
                return p.run_script_file(target_path, first_arg, ...)
            end
        end
        return nil
    end
})

return p