模組:沙盒/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 }