模組:PythonScript/Import
外观
-- Module:PythonScript/Import
-- 支援子頁面階層式套件 (__init__.py 與隱式命名空間套件)、相對匯入 (from . import ...)、
-- 動態 sys.path 尋址與兩階段註冊防循環引用的匯入引擎
local Import = {}
local SysRegistry = require("Module:PythonScript/Librarys/Argparse").Sys
local Runtime = require("Module:PythonScript/Runtime")
-- 不支援作為 Python 執行的 Content Model 黑名單
local UNSUPPORTED_CONTENT_MODELS = {
javascript = "javascript",
css = "css",
["sanitized-css"] = "sanitized-css",
json = "json"
}
local UNSUPPORTED_EXT_MODELS = {
js = "javascript",
css = "css",
json = "json"
}
-- 輔助函式:從 Wikitext 中精準提取 Python 程式碼,並回傳 (code, has_py_tag, non_py_lang)
local function extract_python_code(content)
if not content then return "", false, nil end
local code = content:match("<syntaxhighlight[^<>]+lang=['\"]?[Pp]ython3?['\"]?[^<>]*>(.-)</syntaxhighlight>")
if not code then
code = content:match("<source[^<>]+lang=['\"]?[Pp]ython3?['\"]?[^<>]*>(.-)</source>")
end
if code then
return code, true, nil
end
-- 若未包含 Python 標籤,但明確標註了其他程式語言的 syntaxhighlight / source 標籤,直接識別為該語言
local other_lang = content:match("<syntaxhighlight[^<>]+lang=['\"]?([%w_%-]+)['\"]?[^<>]*>")
or content:match("<source[^<>]+lang=['\"]?([%w_%-]+)['\"]?[^<>]*>")
if other_lang then
local low = other_lang:lower()
if low ~= "python" and low ~= "py" and low ~= "python3" then
return "", false, low
end
end
return content, false, nil
end
-- 零誤殺快速特徵預檢:僅針對模糊地帶頁面,秒殺 100% 不可能為合法 Python 開頭的 Wikitext / Lua / JS 標記
local function is_obvious_non_python(code)
local first_line = code:match("^[\r\n%s]*([^\r\n]+)")
if not first_line then return false end
if first_line:match("^==+[^=]")
or first_line:match("^{|")
or first_line:match("^local%s+[%a_][%w_]*%s*=")
or first_line:match("^/%*")
or first_line:match("^<!DOCTYPE")
or first_line:match("^<[Hh][Tt][Mm][Ll]") then
return true
end
return false
end
-- 將全形底線「_」(U+FF3F) 正規化為標準半形底線「_」
local function to_halfwidth_underscore(s)
if type(s) ~= "string" then return s end
return (s:gsub("_", "_"))
end
-- 產生半形「_」與全形「_」的候選標題清單 (解決 MediaWiki 自動合併連續半形底線導致無法建立 __init__.py 的限制)
local function build_title_variants(title_str)
local variants = {}
local seen = {}
local function add_v(v)
if v and v ~= "" and not seen[v] then
seen[v] = true
table.insert(variants, v)
end
end
local half = to_halfwidth_underscore(title_str)
add_v(half:gsub("__+", function(m) return string.rep("_", #m) end))
add_v(half:gsub("([^/:]+)$", function(seg) return (seg:gsub("_", "_")) end))
add_v(title_str)
add_v(half)
add_v((half:gsub("_", "_")))
add_v((half:gsub("__+", "_")))
return variants
end
-- 頁面探針快取:避免重複查詢相同標題
local page_probe_cache = {}
local function probe_wiki_page(title_str)
if not title_str or title_str == "" then return nil end
local half_key = to_halfwidth_underscore(title_str)
if page_probe_cache[title_str] ~= nil then
return page_probe_cache[title_str] or nil
end
if page_probe_cache[half_key] ~= nil then
return page_probe_cache[half_key] or nil
end
-- 第一關 (A):副檔名快速秒殺 (.js, .css, .json)
local ext = half_key:match("%.([a-zA-Z0-9]+)$")
if ext and UNSUPPORTED_EXT_MODELS[ext:lower()] then
local info = {
prefixedText = half_key,
is_invalid_model = true,
content_model = UNSUPPORTED_EXT_MODELS[ext:lower()]
}
page_probe_cache[title_str] = info
page_probe_cache[half_key] = info
return info
end
local candidates = build_title_variants(title_str)
for _, cand_str in ipairs(candidates) do
local ok, t = pcall(mw.title.new, cand_str)
if ok and t and rawget(t, "exists") ~= false then
local ok_c, raw_content = pcall(function() return t:getContent() end)
if ok_c and raw_content then
local ok_r, redir = pcall(function() return t.redirectTarget end)
if ok_r and redir then
t = redir
raw_content = t:getContent() or raw_content
end
local norm_prefixed = to_halfwidth_underscore(t.prefixedText or cand_str)
-- 第一關 (B):檢查 MediaWiki 頁面 Content Model (秒殺 javascript / css / json)
local ok_cm, cm = pcall(function() return t.contentModel end)
local cm_low = (ok_cm and type(cm) == "string") and cm:lower() or "wikitext"
if UNSUPPORTED_CONTENT_MODELS[cm_low] then
local info = {
title = t,
prefixedText = norm_prefixed,
is_invalid_model = true,
content_model = UNSUPPORTED_CONTENT_MODELS[cm_low]
}
page_probe_cache[title_str] = info
page_probe_cache[half_key] = info
return info
end
local py_code, has_py_tag, other_lang = extract_python_code(raw_content)
if other_lang then
local info = {
title = t,
prefixedText = norm_prefixed,
is_invalid_model = true,
content_model = other_lang
}
page_probe_cache[title_str] = info
page_probe_cache[half_key] = info
return info
end
if mw.ustring.gsub(py_code, "[\n\r%s]+", "") ~= "" then
-- 判斷是否具備明確的檔名+.py 後綴 (注意:條目 [[.py]] 的點號前無檔名,故 has_py_ext 為 false)
local has_py_ext = (norm_prefixed:match("[^/:]+%.py$") ~= nil)
local is_ambiguous = (not has_py_tag) and (not has_py_ext)
local is_not_py = is_ambiguous and is_obvious_non_python(py_code)
local info = {
title = t,
raw_prefixedText = t.prefixedText or cand_str,
prefixedText = norm_prefixed,
namespace = t.namespace or 0,
py_code = py_code,
has_py_tag = has_py_tag,
has_py_ext = has_py_ext,
is_ambiguous = is_ambiguous,
is_not_python = is_not_py
}
page_probe_cache[title_str] = info
page_probe_cache[half_key] = info
page_probe_cache[cand_str] = info
page_probe_cache[norm_prefixed] = info
return info
end
end
end
end
page_probe_cache[title_str] = false
page_probe_cache[half_key] = false
return nil
end
-- 路徑處理工具
local function get_parent_dir(path_str)
if not path_str or path_str == "" then return "" end
local clean = path_str:gsub("%.py$", ""):gsub("/__init__$", ""):gsub("/+$", "")
return clean:match("^(.*)/[^/]+$") or ""
end
local function join_wiki_path(base_dir, rel_path)
local clean_rel = (rel_path or ""):gsub("^/+", "")
if not base_dir or base_dir == "" or base_dir == "/" then
return clean_rel
end
local clean_base = base_dir:gsub("/+$", "")
if clean_rel == "" then return clean_base end
return clean_base .. "/" .. clean_rel
end
local function pop_dir_levels(base_dir, count)
local cur = (base_dir or ""):gsub("/+$", "")
for _ = 1, count do
local parent = cur:match("^(.*)/[^/]+$")
if parent then
cur = parent
elseif cur ~= "" then
cur = ""
else
return nil
end
end
return cur
end
local function resolve_candidate_at(path_prefix, explicit_py)
if not path_prefix or path_prefix == "" then return nil end
local clean_prefix = path_prefix:gsub("%.py$", "")
if not explicit_py then
local init_py = probe_wiki_page(clean_prefix .. "/__init__.py")
or probe_wiki_page(clean_prefix .. "/__init__")
if init_py and not init_py.is_invalid_model and not init_py.is_not_python then
return {
kind = "package",
page = init_py,
mod_dir = clean_prefix,
canonical_key = clean_prefix
}
end
end
local mod_page = probe_wiki_page(clean_prefix .. ".py")
or probe_wiki_page(path_prefix)
or probe_wiki_page(clean_prefix)
if mod_page then
return {
kind = "module",
page = mod_page,
mod_dir = get_parent_dir(clean_prefix),
pkg_dir = clean_prefix,
canonical_key = clean_prefix
}
end
return nil
end
local function get_or_create_namespace_pkg(mod_name, dir_path)
local reg = SysRegistry.modules_registry
if reg[dir_path] then
reg[mod_name] = reg[dir_path]
return reg[dir_path]
end
if reg[mod_name] then return reg[mod_name] end
local PyClass = require("Module:PythonScript/Class")
local module_class = Runtime.builtins.module_type or { __type = "class", __name = "module", __mro = {} }
local pkg_obj = PyClass.make_instance(module_class)
pkg_obj.__dict = {
["__name__"] = mod_name,
["__package__"] = mod_name,
["__dir__"] = dir_path,
["__path__"] = { __type = "list", elements = { dir_path } }
}
reg[mod_name] = pkg_obj
reg[dir_path] = pkg_obj
return pkg_obj
end
-- 兩階段註冊並執行 Python 腳本頁面 (含 Content Model 攔截與模糊地帶 SyntaxError 改寫)
local function load_and_exec_wiki_module(mod_name, cand_info, pkg_name)
local page = cand_info.page
local f_key = page.prefixedText
-- 攔截不支援的 Content Model (如 javascript / css / json)
if page.is_invalid_model then
Runtime.raise("ImportError", "Page '" .. tostring(f_key) .. "' cannot be executed as Python (unsupported content model: " .. tostring(page.content_model) .. ")")
end
-- 攔截預檢確認非 Python 的頁面
if page.is_not_python then
Runtime.raise("ImportError", "Source page '" .. tostring(f_key) .. "' does not appear to be a valid Python script.")
end
local reg = SysRegistry.modules_registry
local c_key = cand_info.canonical_key
local existing = reg[c_key] or reg[f_key] or reg[mod_name]
if existing then
reg[mod_name] = existing
reg[c_key] = existing
return existing
end
local PyClass = require("Module:PythonScript/Class")
local Lexer = require("Module:PythonScript/Lexer")
local Parser = require("Module:PythonScript/Parser")
local EvalUtils = require("Module:PythonScript/Evaluator/Utils")
local _, _, _, exec_block = require("Module:PythonScript/Evaluator/Core").getCoreFunction()
local mod_scope = EvalUtils.create_scope(nil)
local vars = mod_scope.vars
vars["__name__"] = mod_name
vars["__file__"] = f_key
vars["__dir__"] = cand_info.mod_dir
vars["__package__"] = pkg_name or (cand_info.kind == "package" and mod_name or get_parent_dir(mod_name):gsub("/", "."))
if cand_info.kind == "package" then
vars["__path__"] = { __type = "list", elements = { cand_info.mod_dir } }
elseif cand_info.pkg_dir then
vars["__path__"] = { __type = "list", elements = { cand_info.pkg_dir } }
end
local module_class = Runtime.builtins.module_type or { __type = "class", __name = "module", __mro = {} }
local loaded_obj = PyClass.make_instance(module_class)
loaded_obj.__dict = vars
reg[mod_name] = loaded_obj
reg[c_key] = loaded_obj
reg[f_key] = loaded_obj
local py_code = page.py_code
local new_file_idx = #Runtime.Traceback.files + 1
Runtime.Traceback.files[new_file_idx] = { name = f_key, raw_code = py_code }
local prev_file_idx = Runtime.Traceback.current_file_idx
local prev_func_name = Runtime.Traceback.current_func_name or "<module>"
local prev_line = Runtime.Traceback.current_line
table.insert(Runtime.Traceback.stack, { prev_file_idx, prev_func_name, prev_line })
Runtime.Traceback.current_file_idx = new_file_idx
Runtime.Traceback.current_func_name = "<module>"
Runtime.Traceback.current_line = 1
-- 先執行詞法與語法解析,若為模糊地帶頁面且發生 SyntaxError / IndentationError,改寫為 ImportError
local ok_parse, ast_or_err = pcall(function()
local tokens = Lexer.tokenize(py_code)
return Parser.parse(tokens, "module")
end)
if not ok_parse then
table.remove(Runtime.Traceback.stack)
Runtime.Traceback.current_file_idx = prev_file_idx
Runtime.Traceback.current_func_name = prev_func_name
Runtime.Traceback.current_line = prev_line
reg[mod_name] = nil
reg[c_key] = nil
reg[f_key] = nil
if page.is_ambiguous then
Runtime.raise("ImportError", "Source page '" .. tostring(f_key) .. "' does not appear to be a valid Python script.")
end
error(ast_or_err, 0)
end
local ok_exec, err_or_sig = pcall(function()
return exec_block(ast_or_err.body, mod_scope)
end)
table.remove(Runtime.Traceback.stack)
Runtime.Traceback.current_file_idx = prev_file_idx
Runtime.Traceback.current_func_name = prev_func_name
Runtime.Traceback.current_line = prev_line
if not ok_exec then
reg[mod_name] = nil
reg[c_key] = nil
reg[f_key] = nil
error(err_or_sig, 0)
end
return loaded_obj
end
-- 沿著基底目錄 base_dir 階層式載入 parts 陣列 (自動執行沿途的 __init__.py 並掛載子模組屬性)
local function load_chain_from_base(base_dir, parts, mod_name_prefix)
local cur_dir = base_dir
local parent_mod = nil
local cur_mod_name = mod_name_prefix or ""
for i = 1, #parts do
local part = parts[i]
cur_mod_name = (cur_mod_name ~= "") and (cur_mod_name .. "." .. part) or part
local step_path = join_wiki_path(cur_dir, part)
local is_last = (i == #parts)
local cand = resolve_candidate_at(step_path, false)
local step_mod
if cand then
local pkg_name = (cand.kind == "package") and cur_mod_name or (parent_mod and parent_mod.__dict["__name__"] or "")
step_mod = load_and_exec_wiki_module(cur_mod_name, cand, pkg_name)
cur_dir = (cand.kind == "package") and cand.mod_dir or (cand.pkg_dir or step_path)
elseif not is_last then
-- 中間層級無 __init__.py,建立隱式命名空間套件繼續深入子頁面
step_mod = get_or_create_namespace_pkg(cur_mod_name, step_path)
cur_dir = step_path
else
return nil
end
if parent_mod and parent_mod.__dict then
parent_mod.__dict[part] = step_mod
end
parent_mod = step_mod
end
return parent_mod
end
function Import.execute_import(module_name, global_env)
local loaded_obj
-- 將匯入名稱中的全形「_」統一轉為半形「_」
module_name = to_halfwidth_underscore(module_name)
-- 若傳入的 global_env 為嵌套函式作用域,自動向上追溯至該模組的根作用域
local root_env = global_env
if type(root_env) == "table" and root_env.vars then
while root_env.parent do root_env = root_env.parent end
end
local env_vars = (type(root_env) == "table" and root_env.vars) or root_env or {}
-- 若沙盒環境將 __builtins__ 設為 None 或未包含 __import__ 的白名單字典,阻斷 import 語句
local cb = env_vars["__builtins__"]
if cb ~= nil then
if cb == Runtime.builtins.None or (type(cb) == "table" and cb.__class and cb.__class.__name == "NoneType") then
Runtime.raise("ImportError", "__import__ not found")
elseif type(cb) == "table" and cb.__type == "dict" then
local has_imp = false
for _, p in ipairs(cb.pairs or {}) do
if p.key == "__import__" and p.value ~= Runtime.builtins.None then
has_imp = true; break
end
end
if not has_imp then
Runtime.raise("ImportError", "__import__ not found")
end
end
end
local caller_name = env_vars["__name__"] or "__main__"
local caller_dir = env_vars["__dir__"]
if not caller_dir then
if caller_name ~= "__main__" then
local reg_caller = SysRegistry.modules_registry[caller_name]
if reg_caller and reg_caller.__dict and reg_caller.__dict["__dir__"] then
caller_dir = reg_caller.__dict["__dir__"]
else
caller_dir = get_parent_dir(caller_name:gsub("%.", "/"))
end
elseif SysRegistry.path_registry and SysRegistry.path_registry[1] then
caller_dir = SysRegistry.path_registry[1]
end
end
-- ==========================================
-- 情況 1: 顯式相對匯入 (from . import utils / from ..pkg import mod)
-- ==========================================
if string.sub(module_name, 1, 1) == "." then
local dots, rest = string.match(module_name, "^(%.+)(.*)$")
if not caller_dir or (caller_dir == "" and caller_name == "__main__") then
Runtime.raise("ImportError", "attempted relative import with no known parent package")
end
local target_base = pop_dir_levels(caller_dir, #dots - 1)
if not target_base then
Runtime.raise("ImportError", "attempted relative import beyond top-level package")
end
-- 若單獨請求 "." 或 ".." (例如 Stmt.lua 在 from . import utils 第一步探測 ".")
if rest == "" then
local pkg_cand = resolve_candidate_at(target_base, false)
if pkg_cand and pkg_cand.kind == "package" then
local pkg_name = target_base:match("([^/:]+)$") or target_base
return load_and_exec_wiki_module(pkg_name, pkg_cand, pkg_name)
end
return get_or_create_namespace_pkg(target_base, target_base)
end
local parts = {}
for part in string.gmatch(rest:gsub("%.", "/"), "[^/]+") do
table.insert(parts, part)
end
local base_pkg_name = target_base:match("([^/:]+)$") or ""
local rel_mod = load_chain_from_base(target_base, parts, base_pkg_name)
if rel_mod then
SysRegistry.modules_registry[module_name] = rel_mod
return rel_mod
end
Runtime.raise("ImportError", "cannot import name '" .. rest .. "' from '" .. (target_base ~= "" and target_base or ".") .. "'")
end
-- ==========================================
-- 情況 2: 系統內建模組與 Python 標準庫攔截
-- ==========================================
if module_name == "mw" then
if not SysRegistry.modules_registry["mw"] then
SysRegistry.modules_registry["mw"] = require("Module:PythonScript/Mw").get_module()
end
return SysRegistry.modules_registry["mw"]
end
local Library = require("Module:PythonScript/Library")
if Library.has_module(module_name) then
if not SysRegistry.modules_registry[module_name] then
SysRegistry.modules_registry[module_name] = Library.get_module(module_name, env_vars)
end
return SysRegistry.modules_registry[module_name]
end
if SysRegistry.modules_registry[module_name] then
return SysRegistry.modules_registry[module_name]
end
-- ==========================================
-- 情況 3: 嘗試作為原生 Lua 模組 (Namespace 828) 或維基模板 (Namespace 10) 載入
-- ==========================================
local is_explicit_py = (module_name:sub(-3):lower() == ".py")
if not is_explicit_py then
local success, title_checker = pcall(mw.title.new, module_name)
if success and title_checker and title_checker.namespace == 828 then
local ok_lua, lua_mod = pcall(require, module_name)
if ok_lua and type(lua_mod) == "table" then
local MwAPI = require("Module:PythonScript/Mw")
loaded_obj = {}
for k, v in pairs(lua_mod) do loaded_obj[k] = v end
loaded_obj = MwAPI.make_lazy_namespace(module_name, loaded_obj, true)
SysRegistry.modules_registry[module_name] = loaded_obj
return loaded_obj
end
end
if success and title_checker and title_checker.namespace == 10 then
local template_callable = {
__type = "function",
__name = module_name,
__qualname__ = module_name,
__module__ = (title_checker and title_checker.prefixedText) or "Template",
__call = function(py_args, py_kwargs)
local mw_args = {}
if py_args then
for i, arg in ipairs(py_args) do
local success_str, str_val = pcall(Runtime.builtins.str, {arg})
mw_args[i] = success_str and str_val or tostring(arg)
end
end
if py_kwargs then
for k, v in pairs(py_kwargs) do
local success_str, str_val = pcall(Runtime.builtins.str, {v})
mw_args[tostring(k)] = success_str and str_val or tostring(v)
end
end
local frame = mw.getCurrentFrame()
local success_expand, result = pcall(function()
return frame:expandTemplate{ title = module_name, args = mw_args }
end)
if not success_expand then
Runtime.raise("RuntimeError", "Template expansion failed for '" .. module_name .. "': " .. tostring(result))
end
return result
end
}
SysRegistry.modules_registry[module_name] = template_callable
return template_callable
end
end
-- ==========================================
-- 情況 4: 階層式搜尋並載入維基 Python 套件與腳本 (支援 __init__.py 與 sys.path)
-- ==========================================
-- 建構搜尋根目錄清單:1. 呼叫者所在目錄 (__dir__) -> 2. sys.path -> 3. 維基全域根目錄 ("")
local search_roots = {}
local seen_roots = {}
local function add_root(r)
if type(r) == "string" and not seen_roots[r] then
seen_roots[r] = true
table.insert(search_roots, r)
end
end
if caller_dir and caller_dir ~= "" then add_root(caller_dir) end
for _, p in ipairs(SysRegistry.path_registry or {}) do
if type(p) == "string" then add_root(p) end
end
add_root("")
-- [4A] 若傳入完整維基頁面路徑 (含 ':' 或 '/' 或 '.py',如 "Module:Foo/somefile.py")
if module_name:find(":", 1, true) or module_name:find("/", 1, true) or is_explicit_py then
local clean_mod = module_name:gsub("%.py$", "")
for _, root in ipairs(search_roots) do
local full_p = join_wiki_path(root, clean_mod)
local cand = resolve_candidate_at(full_p, is_explicit_py)
if cand then
-- 若父目錄存在 __init__.py 且尚未執行,先初始化父套件
local p_dir = cand.mod_dir
if p_dir and p_dir ~= "" and not SysRegistry.modules_registry[p_dir] then
local init_cand = resolve_candidate_at(p_dir, false)
if init_cand and init_cand.kind == "package" then
local p_name = p_dir:match("([^/:]+)$") or p_dir
load_and_exec_wiki_module(p_name, init_cand, p_name)
end
end
local mod_obj = load_and_exec_wiki_module(module_name, cand, p_dir)
SysRegistry.modules_registry[module_name] = mod_obj
return mod_obj
end
end
end
-- [4B] 標準點號路徑拆解 (如 "腳本"、"腳本.腳本1"、"utils")
local parts = {}
for part in string.gmatch(module_name:gsub("%.py$", ""), "[^%.]+") do
table.insert(parts, part)
end
if #parts > 0 then
local rel_joined = table.concat(parts, "/")
for _, root in ipairs(search_roots) do
local full_target = join_wiki_path(root, rel_joined)
-- 確認最終目標 (腳本1.py 或 腳本/__init__.py) 確實存在於此 root 下,才沿途啟動階層載入
local leaf_cand = resolve_candidate_at(full_target, false)
if leaf_cand then
local loaded = load_chain_from_base(root, parts, "")
if loaded then
SysRegistry.modules_registry[module_name] = loaded
-- 若為多層級 import A.B,同步確保頂層套件 A 可在作用域中存取
local top_name = parts[1]
if #parts > 1 and SysRegistry.modules_registry[top_name] and env_vars[top_name] == nil then
env_vars[top_name] = SysRegistry.modules_registry[top_name]
end
return loaded
end
end
end
end
Runtime.raise("ImportError", "No module named '" .. module_name .. "'")
end
Import.probe_wiki_page = probe_wiki_page
Import.to_halfwidth_underscore = to_halfwidth_underscore
return Import