跳转到内容

模組:沙盒/a2569875/PythonScript/Librarys/Argparse

维基百科,自由的百科全书
-- Module:PythonScript/Librarys/Argparse
local PyClass = require("Module:沙盒/a2569875/PythonScript/Class")
local Runtime = require("Module:沙盒/a2569875/PythonScript/Runtime")

-- ==========================================
-- 內部輔助函數
-- ==========================================
local function wrap_func(func)
    return function(py_args, py_kwargs)
        return func(py_args or {}, py_kwargs or {})
    end
end

local function make_namespace(name, tbl)
    local dict = {}
    for k, v in pairs(tbl) do
        if type(v) == "function" then
            dict[k] = wrap_func(v)
        elseif type(v) == "table" then
            if v.__type then dict[k] = v else dict[k] = make_namespace(name .. "_" .. k, v) end
        else
            dict[k] = v
        end
    end
    return PyClass.make_instance(PyClass.make_class(name, {}, dict))
end

-- ==========================================
-- 【Sys 模組實作】
-- ==========================================
local Sys = {}
Sys.modules_registry = {} -- 儲存已載入的模組
Sys.argv_registry = {}    -- 儲存全域的 sys.argv
Sys.path_registry = {}    -- 儲存全域的 sys.path 搜尋路徑
Sys.last_exc = nil        -- sys.exc_info() 追蹤

-- 實作 TextIOWrapper (模擬 sys.stdout / sys.stderr / sys.stdin)
local function make_text_io(name, is_writable, io_buffer)
    local dict = {
        name = "<" .. name .. ">",
        write = {
            __type = "builtin_method",
            __call = function(args)
                if not is_writable then Runtime.raise("OSError", "not writable") end
                local text = args[1]
                if type(text) ~= "string" then text = tostring(text) end
                table.insert(io_buffer, text)
                return #text
            end
        },
        read = {
            __type = "builtin_method",
            __call = function(args)
                if is_writable then Runtime.raise("OSError", "not readable") end
                local size = args[1]
                local res = table.concat(io_buffer, "\n")
                if size and size > 0 then res = string.sub(res, 1, size) end
                io_buffer = {} -- 簡單模擬:讀取後清空
                return res
            end
        },
        flush = {
            __type = "builtin_method",
            __call = function() return nil end
        }
    }
    return PyClass.make_instance(PyClass.make_class("TextIOWrapper", {}, dict))
end

local sys_dict = {
    platform = "MediaWiki",
    version = "3.10.0 (MediaWiki Scribunto Lua 5.1 Environment)",
    version_info = { __type = "tuple", elements = {3, 10, 0, "final", 0} },
    maxsize = 9007199254740991, -- 2^53 - 1 (Lua 5.1 的安全整數極限)
    byteorder = "little", -- 絕大多數維基伺服器 (x86_64) 的架構
    defaultencoding = "utf-8",
    executable = "Module:PythonScript",
    
    getdefaultencoding = function() return "utf-8" end,
    getrecursionlimit = function() return 200 end,
    setrecursionlimit = function() Runtime.raise("NotImplementedError", "cannot modify recursion limit in Lua environment") end,
    getsizeof = function() Runtime.raise("NotImplementedError", "getsizeof is not available in Lua environment") end,
    
    exc_info = function()
        if not Sys.last_exc then return { __type = "tuple", elements = {nil, nil, nil} } end
        return { __type = "tuple", elements = {Sys.last_exc.__class, Sys.last_exc, nil} }
    end,
    
    exit = function(args, kwargs)
        local exit_code = (type(args) == "table" and args[1] or args) or 0
        Runtime.raise("SystemExit", exit_code)
    end
}

function Sys.get_module()
    local inst = make_namespace("sys", sys_dict)
    local py_argv = { __type = "list", elements = Sys.argv_registry }
    PyClass.set_attr(inst, "argv", py_argv)
    local py_modules = { __type = "dict", pairs = {} }
    for k, v in pairs(Sys.modules_registry) do table.insert(py_modules.pairs, { key = k, value = v }) end
    PyClass.set_attr(inst, "modules", py_modules)
    PyClass.set_attr(inst, "stdout", make_text_io("stdout", true, Runtime.stdout))
    PyClass.set_attr(inst, "stderr", make_text_io("stderr", true, Runtime.stdout))
    PyClass.set_attr(inst, "stdin", make_text_io("stdin", false, Runtime.stdin))
    local sys_path = { __type = "list", elements = Sys.path_registry }
    PyClass.set_attr(inst, "path", sys_path)
    return inst
end

-- ==========================================
-- 【Argparse 模組實作】
-- ==========================================
local Argparse = {}
local NamespaceClass = PyClass.make_class("Namespace", {}, {
    __repr__ = {
        __type = "builtin_method",
        __call = function(args)
            local self = args[1]
            local parts = {}
            for k, v in pairs(self.__dict) do
                table.insert(parts, k .. "=" .. Runtime.builtins.repr({v}))
            end
            table.sort(parts)
            return "Namespace(" .. table.concat(parts, ", ") .. ")"
        end
    }
})
NamespaceClass.__dict.__str__ = NamespaceClass.__dict.__repr__

-- 先行宣告 ArgumentError 例外類別,供內部呼叫
local ArgumentErrorClass = PyClass.make_class("ArgumentError", {Runtime.builtins.Exception}, {})

local ArgumentParser_dict = {
    __init__ = {
        __type = "builtin_method",
        __call = function(args, kwargs)
            local self = args[1]
            kwargs = kwargs or {}
            self.__dict.prog = kwargs.prog or Sys.argv_registry[1] or "script"
            self.__dict.usage = kwargs.usage
            self.__dict.description = kwargs.description
            self.__dict.epilog = kwargs.epilog
            self.__dict.add_help = kwargs.add_help ~= false
            self.__dict._actions = {}
            self.__dict._option_string_actions = {}
            self.__dict._mutually_exclusive_groups = {}
            self.__dict._argument_groups = {}
            
            if self.__dict.add_help then
                local help_action = { option_strings = {"-h", "--help"}, dest = "help", help = "show this help message and exit", action = "help" }
                table.insert(self.__dict._actions, help_action)
                self.__dict._option_string_actions["-h"] = help_action
                self.__dict._option_string_actions["--help"] = help_action
            end
            return nil
        end
    },
    add_argument = {
        __type = "builtin_method",
        __call = function(args, kwargs)
            local self = args[1]; kwargs = kwargs or {}
            local action = { option_strings = {}, dest = kwargs.dest, help = kwargs.help, action = kwargs.action or "store", nargs = kwargs.nargs, const = kwargs.const, default = kwargs.default, type = kwargs.type, choices = kwargs.choices, required = kwargs.required, metavar = kwargs.metavar }
            local is_positional = true
            for i = 2, #args do
                local arg_str = args[i]
                if type(arg_str) == "string" and (string.sub(arg_str, 1, 1) == "-" or string.sub(arg_str, 1, 2) == "--") then
                    table.insert(action.option_strings, arg_str); self.__dict._option_string_actions[arg_str] = action; is_positional = false
                    if not action.dest then action.dest = string.gsub(string.gsub(arg_str, "^--?", ""), "-", "_") end
                end
            end
            if is_positional then action.dest = action.dest or args[2] end
            table.insert(self.__dict._actions, action)
            return action
        end
    },
    -- 2. 更新 ArgumentParser_dict 內的 add_argument 與 parse_args (約第 137 行與第 188 行):
    add_argument = {
        __type = "builtin_method",
        __call = function(args, kwargs)
            local self = args[1]; kwargs = kwargs or {}
            local act_type = kwargs.action or "store"
            local default_val = kwargs.default
            if default_val == nil then
                if act_type == "store_true" then default_val = false
                elseif act_type == "store_false" then default_val = true
                elseif act_type == "count" then default_val = 0
                else default_val = Runtime.builtins.None end
            end

            local action = {
                option_strings = {}, dest = kwargs.dest, help = kwargs.help,
                action = act_type, nargs = kwargs.nargs, const = kwargs.const,
                default = default_val, type = kwargs.type, choices = kwargs.choices,
                required = kwargs.required, metavar = kwargs.metavar
            }
            local is_positional = true
            local first_long, first_short = nil, nil

            for i = 2, #args do
                local arg_str = args[i]
                if type(arg_str) == "string" and string.sub(arg_str, 1, 1) == "-" then
                    table.insert(action.option_strings, arg_str)
                    self.__dict._option_string_actions[arg_str] = action
                    is_positional = false
                    if string.sub(arg_str, 1, 2) == "--" then
                        if not first_long then first_long = string.gsub(string.sub(arg_str, 3), "-", "_") end
                    else
                        if not first_short then first_short = string.gsub(string.sub(arg_str, 2), "-", "_") end
                    end
                end
            end
            -- 優先採用長選項 (--verbose) 作為 dest,無長選項時才用短選項 (-v)
            if is_positional then
                action.dest = action.dest or args[2]
            else
                action.dest = action.dest or first_long or first_short
            end
            table.insert(self.__dict._actions, action)
            return action
        end
    },
    add_mutually_exclusive_group = {
        __type = "builtin_method",
        __call = function(args, kwargs)
            local self = args[1]; local group = PyClass.make_instance(PyClass.make_class("_MutuallyExclusiveGroup", {}, {}))
            group.__dict.required = kwargs and kwargs.required or false; group.__dict.parser = self; group.__dict._group_actions = {}
            local bound_add = { __type = "builtin_method", __call = function(g_args, g_kwargs) g_args[1] = self; local action = self.__dict.add_argument.__call(g_args, g_kwargs); table.insert(group.__dict._group_actions, action); return action end }
            PyClass.set_attr(group, "add_argument", bound_add); table.insert(self.__dict._mutually_exclusive_groups, group)
            return group
        end
    },
    format_help = {
        __type = "builtin_method",
        __call = function(args)
            local self = args[1]; local res = {}; table.insert(res, "usage: " .. self.__dict.prog .. " [...]")
            if self.__dict.description then table.insert(res, "\n" .. self.__dict.description .. "\n") end
            table.insert(res, "options:")
            for _, act in ipairs(self.__dict._actions) do
                local opts = table.concat(act.option_strings, ", "); if opts == "" then opts = act.dest end
                table.insert(res, "  " .. opts .. "\t" .. (act.help or ""))
            end
            if self.__dict.epilog then table.insert(res, "\n" .. self.__dict.epilog) end
            return table.concat(res, "\n")
        end
    },
    print_help = { __type = "builtin_method", __call = function(args) table.insert(Runtime.stdout, args[1].__dict.format_help.__call(args) .. "\n"); return nil end },
    print_usage = { __type = "builtin_method", __call = function(args) table.insert(Runtime.stdout, "usage: " .. args[1].__dict.prog .. " [...]\n"); return nil end },
    parse_args = {
        __type = "builtin_method",
        __call = function(args, kwargs)
            local self = args[1]; local py_argv = args[2]; local arg_list = {}
            if py_argv and py_argv.__type == "list" then arg_list = py_argv.elements else for i = 2, #Sys.argv_registry do table.insert(arg_list, Sys.argv_registry[i]) end end
            local namespace = (kwargs and kwargs.namespace) or PyClass.make_instance(NamespaceClass)
            for _, act in ipairs(self.__dict._actions) do
                if act.dest and act.action ~= "help" and act.default ~= nil then
                    PyClass.set_attr(namespace, act.dest, act.default)
                end
            end
            
            local function convert_val(act, raw_val)
                if act.type then
                    local Evaluator = require("Module:沙盒/a2569875/PythonScript/Evaluator")
                    return Evaluator.invoke(act.type, {raw_val}, {})
                end
                return raw_val
            end

            local i = 1
            while i <= #arg_list do
                local arg_str = arg_list[i]; local action = self.__dict._option_string_actions[arg_str]
                if action then
                    if action.action == "help" then self.__dict.print_help.__call({self}); sys_dict.exit(0)
                    elseif action.action == "store_true" then PyClass.set_attr(namespace, action.dest, true); i = i + 1
                    elseif action.action == "store_false" then PyClass.set_attr(namespace, action.dest, false); i = i + 1
                    elseif action.action == "store_const" then PyClass.set_attr(namespace, action.dest, action.const); i = i + 1
                    elseif action.action == "count" then PyClass.set_attr(namespace, action.dest, (namespace.__dict[action.dest] or 0) + 1); i = i + 1
                    elseif action.action == "append" then
                        local val = arg_list[i+1]; if not val then Runtime.raise(ArgumentErrorClass, "argument " .. arg_str .. ": expected one argument") end
                        val = convert_val(action, val)
                        local lst = namespace.__dict[action.dest]; if type(lst) ~= "table" or lst.__type ~= "list" then lst = {__type="list", elements={}} end
                        table.insert(lst.elements, val); PyClass.set_attr(namespace, action.dest, lst); i = i + 2
                    else
                        local val = arg_list[i+1]; if not val then Runtime.raise(ArgumentErrorClass, "argument " .. arg_str .. ": expected one argument") end
                        PyClass.set_attr(namespace, action.dest, convert_val(action, val)); i = i + 2
                    end
                else
                    local assigned = false
                    for _, act in ipairs(self.__dict._actions) do
                        local cur = namespace.__dict[act.dest]
                        local is_unset = (cur == nil) or (type(cur) == "table" and cur.__class and cur.__class.__name == "NoneType")
                        if #act.option_strings == 0 and is_unset then
                            PyClass.set_attr(namespace, act.dest, convert_val(act, arg_str))
                            assigned = true; break
                        end
                    end
                    if not assigned then table.insert(Runtime.stdout, "usage: " .. self.__dict.prog .. " [...]\nerror: unrecognized arguments: " .. arg_str .. "\n"); sys_dict.exit(2) end
                    i = i + 1
                end
            end
            return namespace
        end
    }
}

function Argparse.get_module()
    local dict = { ArgumentParser = PyClass.make_class("ArgumentParser", {}, ArgumentParser_dict), Namespace = NamespaceClass, ArgumentError = ArgumentErrorClass }
    dict.ArgumentParser.__dict["__module__"] = "argparse"
    dict.Namespace.__dict["__module__"] = "argparse"
    dict.ArgumentError.__dict["__module__"] = "argparse"
    return make_namespace("argparse", dict)
end

-- ==========================================
-- 【Os 模組實作】
-- ==========================================
local Os = {}
local virtual_fds = {}
local next_fd = 3

function Os.get_module()
    local env_dict = {
        MW_SERVER = mw.site.server,
        MW_SITENAME = mw.site.siteName,
        MW_SCRIPTPATH = mw.site.scriptPath,
        MW_VERSION = mw.site.currentVersion,
        OS = "Scribunto posix",
        USER = "WikiUser"
    }
    local py_environ = { __type = "dict", pairs = {} }
    for k, v in pairs(env_dict) do table.insert(py_environ.pairs, { key = k, value = v }) end
    
    local os_path_dict = {}
    
    os_path_dict.join = function(args)
        local parts = {}
        for _, arg in ipairs(args) do if type(arg) == "string" then table.insert(parts, arg) end end
        local res = parts[1] or ""
        for i = 2, #parts do
            if string.sub(parts[i], 1, 1) == "/" then res = parts[i] else
                if string.sub(res, -1) ~= "/" then res = res .. "/" end
                res = res .. parts[i]
            end
        end
        return res
    end
    
    os_path_dict.abspath = function(args) return args[1] or "" end
    os_path_dict.realpath = function(args) return args[1] or "" end
    
    os_path_dict.basename = function(args)
        local p = args[1] or ""; local i = p:match(".*()/")
        if not i then return p end
        return p:sub(i + 1)
    end
    
    os_path_dict.dirname = function(args)
        local p = args[1] or ""; local i = p:match(".*()/")
        if not i then return "" end
        if i == 1 then return "/" end
        return p:sub(1, i - 1)
    end
    
    os_path_dict.split = function(args)
        local p = args[1] or ""; local i = p:match(".*()/")
        if not i then return { __type = "tuple", elements = {"", p} } end
        if i == 1 then return { __type = "tuple", elements = {"/", p:sub(2)} } end
        return { __type = "tuple", elements = {p:sub(1, i - 1), p:sub(i + 1)} }
    end
    
    os_path_dict.splitext = function(args)
        local p = args[1] or ""; local slash_idx = p:match(".*()/") or 0
        local dot_idx = p:match(".*()%.")
        if not dot_idx or dot_idx <= slash_idx or dot_idx == slash_idx + 1 then
            return { __type = "tuple", elements = {p, ""} }
        end
        return { __type = "tuple", elements = {p:sub(1, dot_idx - 1), p:sub(dot_idx)} }
    end
    
    os_path_dict.isabs = function(args) return string.sub(args[1] or "", 1, 1) == "/" end
    
    os_path_dict.commonprefix = function(args)
        local lst = args[1]
        if not lst or lst.__type ~= "list" or #lst.elements == 0 then return "" end
        local s1 = lst.elements[1]
        for i = 2, #lst.elements do
            local s2 = lst.elements[i]; local prefix_len = 0
            for j = 1, math.min(#s1, #s2) do
                if s1:sub(j,j) == s2:sub(j,j) then prefix_len = j else break end
            end
            s1 = s1:sub(1, prefix_len)
        end
        return s1
    end
    
    os_path_dict.commonpath = function(args)
        local lst = args[1]
        if not lst or lst.__type ~= "list" or #lst.elements == 0 then Runtime.raise("ValueError", "commonpath() arg is an empty sequence") end
        local paths = {}
        for _, p in ipairs(lst.elements) do
            local parts = {}; for part in p:gmatch("[^/]+") do table.insert(parts, part) end
            table.insert(paths, parts)
        end
        local common = {}; local p1 = paths[1]
        for i = 1, #p1 do
            local match = true
            for j = 2, #paths do if paths[j][i] ~= p1[i] then match = false; break end end
            if match then table.insert(common, p1[i]) else break end
        end
        local prefix = (lst.elements[1]:sub(1,1) == "/") and "/" or ""
        local res = table.concat(common, "/")
        if res == "" and prefix == "/" then return "/" end
        return prefix .. res
    end
    
    os_path_dict.relpath = function(args)
        local p = args[1] or ""; local start = args[2] or ""
        if p == start then return "." end
        if start ~= "" and p:sub(1, #start) == start then
            local rel = p:sub(#start + 1)
            if rel:sub(1,1) == "/" then return rel:sub(2) end
            return rel
        end
        return p
    end
    
    os_path_dict.samefile = function(args)
        local t1, t2 = mw.title.new(args[1] or ""), mw.title.new(args[2] or "")
        if t1 and t2 then return t1.prefixedText == t2.prefixedText end
        return args[1] == args[2]
    end
    
    os_path_dict.isfile = function(args)
        local t = mw.title.new(args[1] or "")
        return (t ~= nil) and (t.exists or (t.file and t.file.exists))
    end
    
    os_path_dict.exists = function(args) return os_path_dict.isfile(args) end
    
    os_path_dict.getsize = function(args)
        local path = args[1] or ""
        local t = mw.title.new(path)
        if not t or not (t.exists or (t.file and t.file.exists)) then
            Runtime.raise("OSError", "[Errno 2] No such file or directory: '" .. tostring(path) .. "'")
        end
        if t.file and t.file.exists then return t.file.size end
        local content = t:getContent()
        return content and #content or 0
    end
    
    os_path_dict.getmtime = function(args)
        local path = args[1] or ""
        local t = mw.title.new(path)
        if not t or not (t.exists or (t.file and t.file.exists)) then
            Runtime.raise("OSError", "[Errno 2] No such file or directory: '" .. tostring(path) .. "'")
        end
        local ts_str = mw.getCurrentFrame():callParserFunction("REVISIONTIMESTAMP", path)
        if type(ts_str) == "string" and #ts_str >= 14 then
            local y, m, d, h, min, s = ts_str:match("^(%d%d%d%d)(%d%d)(%d%d)(%d%d)(%d%d)(%d%d)")
            if y then return os.time({year=tonumber(y), month=tonumber(m), day=tonumber(d), hour=tonumber(h), min=tonumber(min), sec=tonumber(s)}) end
        end
        return os.time()
    end
    
    os_path_dict.getatime = function(args) return os_path_dict.getmtime(args) end
    os_path_dict.getctime = function(args) return os_path_dict.getmtime(args) end
    
    local os_dict = {
        error = Runtime.builtins.OSError,
        name = "posix",
        environ = py_environ,
        
        ctermid = function() return "Module:PythonScript" end,
        getenv = function(args)
            local key, default = args[1], args[2]
            for _, p in ipairs(py_environ.pairs) do if p.key == key then return p.value end end
            return default
        end,
        getenvb = function(args)
            local key, default = args[1], args[2]
            for _, p in ipairs(py_environ.pairs) do if p.key == key then return p.value end end
            return default
        end,
        
        getcwd = function()
            local success, t = pcall(mw.title.getCurrentTitle)
            if success and t then return t.prefixedText end
            return "/"
        end,
        getcwdb = function()
            local success, t = pcall(mw.title.getCurrentTitle)
            if success and t then return t.prefixedText end
            return "/"
        end,
        curdir = ".",
        
        chdir = function() Runtime.raise("OSError", "chdir not supported") end,
        fchdir = function() Runtime.raise("OSError", "fchdir not supported") end,
        chroot = function() Runtime.raise("OSError", "chroot not supported") end,
        
        PathLike = PyClass.make_class("PathLike", {}, {}),
        
        get_exec_path = function() return { __type = "list", elements = { "Module:PythonScript" } } end,
        getegid = function() Runtime.raise("NotImplementedError", "getegid not available") end,
        geteuid = function() Runtime.raise("NotImplementedError", "geteuid not available") end,
        putenv = function() Runtime.raise("OSError", "putenv not supported") end,
        
        open = function(args)
            local path = args[1]
            local success, t = pcall(mw.title.new, path)
            if not success or not t or not t.exists then Runtime.raise("OSError", "[Errno 2] No such file or directory: '" .. tostring(path) .. "'") end
            local content = t:getContent() or ""
            local fd = next_fd; next_fd = next_fd + 1
            virtual_fds[fd] = { content = content, pos = 1 }
            return fd
        end,
        fdopen = function(args)
            local fd = args[1]
            if not virtual_fds[fd] then Runtime.raise("OSError", "Bad file descriptor") end
            local fd_obj_dict = {
                read = { __type = "builtin_method", __call = function(margs)
                    local f = virtual_fds[fd]; local n = margs[2] or #f.content
                    local res = string.sub(f.content, f.pos, f.pos + n - 1); f.pos = f.pos + n
                    return res
                end },
                readline = { __type = "builtin_method", __call = function() 
                    local f = virtual_fds[fd]; local newline_pos = string.find(f.content, "\n", f.pos, true)
                    if newline_pos then local res = string.sub(f.content, f.pos, newline_pos); f.pos = newline_pos + 1; return res
                    else local res = string.sub(f.content, f.pos); f.pos = #f.content + 1; return res end
                end },
                close = { __type = "builtin_method", __call = function() virtual_fds[fd] = nil; return nil end }
            }
            return PyClass.make_instance(PyClass.make_class("TextIOWrapper", {}, fd_obj_dict))
        end,
        close = function(args)
            local fd = args[1]; if not virtual_fds[fd] then Runtime.raise("OSError", "Bad file descriptor") end
            virtual_fds[fd] = nil; return nil
        end,
        read = function(args)
            local fd, n = args[1], args[2] or 999999
            local f = virtual_fds[fd]; if not f then Runtime.raise("OSError", "Bad file descriptor") end
            local res = string.sub(f.content, f.pos, f.pos + n - 1); f.pos = f.pos + n
            return res
        end,
        write = function() Runtime.raise("OSError", "write not supported") end,
        abort = function() Runtime.raise("SystemExit", 134) end,
        
        times = function() return { __type = "tuple", elements = { os.clock(), 0.0, 0.0, 0.0, os.time() } } end,
        cpu_count = function() return nil end,
        getrandom = function(args)
            local size = args[1] or 1; math.randomseed(os.time() + os.clock() * 1000)
            local bytes = {}; for i = 1, size do table.insert(bytes, string.char(math.random(0, 255))) end
            return table.concat(bytes)
        end,
        path = make_namespace("path", os_path_dict)
    }
    return make_namespace("os", os_dict)
end

return { Argparse = Argparse, Sys = Sys, Os = Os }